submit blocks to Kinto blocklist bucket (v2) (#13602)

* submit blocks to Kinto blocklist bucket (v2)

* add is_imported_from_kinto_regex test

* change KINTO_API_IS_TEST_SERVER default to False
This commit is contained in:
Andrew Williamson 2020-03-03 18:27:34 +00:00 коммит произвёл GitHub
Родитель a81ca7277e
Коммит 068d33509f
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
13 изменённых файлов: 498 добавлений и 9 удалений

Просмотреть файл

@ -19,6 +19,7 @@ INSTALLED_APPS += (
'debug_toolbar',
)
# django-debug-doolbar middleware needs to be inserted as high as possible
# but after GZip middleware
def insert_debug_toolbar_middleware(middlewares):
@ -32,6 +33,7 @@ def insert_debug_toolbar_middleware(middlewares):
return tuple(ret_middleware)
MIDDLEWARE = insert_debug_toolbar_middleware(MIDDLEWARE)
DEBUG_TOOLBAR_CONFIG = {
@ -65,7 +67,8 @@ DOMAIN = SERVICES_DOMAIN = urlparse(SITE_URL).netloc
SERVICES_URL = SITE_URL
EXTERNAL_SITE_URL = SITE_URL
CODE_MANAGER_URL = os.environ.get('CODE_MANAGER_URL') or 'http://localhost:3000'
CODE_MANAGER_URL = (
os.environ.get('CODE_MANAGER_URL') or 'http://localhost:3000')
ALLOWED_HOSTS = ALLOWED_HOSTS + [SERVICES_DOMAIN]
@ -104,7 +107,7 @@ FXA_CONFIG = {
'client_secret': env(
'FXA_CLIENT_SECRET',
default='d7d5f1148a35b12c067fb9eafafc29d35165a90f5d8b0032f1fcd37468ae49fe'), # noqa
# fxa redirects to http://localhost:3000/api/auth/authenticate-callback/?config=local #noqa
# noqa fxa redirects to http://localhost:3000/api/auth/authenticate-callback/?config=local #noqa
},
}
FXA_CONTENT_HOST = 'https://stable.dev.lcip.org'
@ -132,6 +135,8 @@ CUSTOMS_API_KEY = 'customssecret'
WAT_API_URL = 'http://wat:10102/'
WAT_API_KEY = 'watsecret'
KINTO_API_IS_TEST_SERVER = True
# If you have settings you want to overload, put them in a local_settings.py.
try:
from local_settings import * # noqa

Просмотреть файл

@ -7,6 +7,7 @@ from django.db import models
from django.utils.html import format_html
from django.utils.functional import cached_property
import waffle
from django_extensions.db.fields.json import JSONField
from olympia import amo
@ -21,7 +22,8 @@ from olympia.versions.compare import addon_version_int
from olympia.versions.models import Version
from .utils import (
block_activity_log_save, block_activity_log_delete, splitlines)
block_activity_log_save, block_activity_log_delete, legacy_delete_blocks,
legacy_publish_blocks, splitlines)
class Block(ModelBase):
@ -61,6 +63,10 @@ class Block(ModelBase):
assert self.updated_by
return super().save(**kwargs)
@property
def is_imported_from_kinto_regex(self):
return self.kinto_id.startswith('*')
@cached_property
def addon(self):
return Addon.unfiltered.filter(
@ -399,6 +405,8 @@ class BlockSubmission(ModelBase):
modified_datetime = datetime.datetime.now()
all_guids_to_block = [block['guid'] for block in self.to_block]
kinto_submit_legacy_switch = waffle.switch_is_active(
'blocklist_legacy_submit')
for guids_chunk in chunked(all_guids_to_block, 100):
blocks = self._get_block_instances_to_save(guids_chunk)
Block.preload_addon_versions(blocks)
@ -412,6 +420,8 @@ class BlockSubmission(ModelBase):
block.submission.add(self)
block_activity_log_save(
block, change=change, submission_obj=self)
if kinto_submit_legacy_switch:
legacy_publish_blocks(blocks)
self.save()
self.update(signoff_state=self.SIGNOFF_PUBLISHED)
@ -420,13 +430,17 @@ class BlockSubmission(ModelBase):
assert self.is_submission_ready
assert self.action == self.ACTION_DELETE
block_ids_to_delete = [block['id'] for block in self.to_block]
kinto_submit_legacy_switch = waffle.switch_is_active(
'blocklist_legacy_submit')
for ids_chunk in chunked(block_ids_to_delete, 100):
blocks = Block.objects.filter(id__in=ids_chunk)
blocks = list(Block.objects.filter(id__in=ids_chunk))
Block.preload_addon_versions(blocks)
for block in blocks:
block_activity_log_delete(block, submission_obj=self)
if kinto_submit_legacy_switch:
legacy_delete_blocks(blocks)
self.save()
blocks.delete()
Block.objects.filter(id__in=ids_chunk).delete()
self.update(signoff_state=self.SIGNOFF_PUBLISHED)

Просмотреть файл

@ -885,7 +885,9 @@ class TestBlockSubmissionAdmin(TestCase):
assert Block.objects.count() == 0
assert LogEntry.objects.count() == 0
def test_signoff_approve(self):
@override_switch('blocklist_legacy_submit', active=True)
@mock.patch('olympia.blocklist.models.legacy_publish_blocks')
def test_signoff_approve(self, legacy_publish_blocks_mock):
addon = addon_factory(guid='guid@', name='Danger Danger')
mbs = BlockSubmission.objects.create(
input_guids='guid@\ninvalid@',
@ -947,6 +949,10 @@ class TestBlockSubmissionAdmin(TestCase):
assert signoff_log.arguments == [addon, addon.guid, 'add', new_block]
assert signoff_log.user == user
# blocks would have been submitted to kinto legacy collection
legacy_publish_blocks_mock.assert_called()
legacy_publish_blocks_mock.assert_called_with([new_block])
assert mbs.to_block == [
{'guid': 'guid@',
'id': None,
@ -968,7 +974,9 @@ class TestBlockSubmissionAdmin(TestCase):
response.content)
assert b'not a Block!' not in response.content
def test_signoff_reject(self):
@override_switch('blocklist_legacy_submit', active=True)
@mock.patch('olympia.blocklist.models.legacy_publish_blocks')
def test_signoff_reject(self, legacy_publish_blocks_mock):
addon = addon_factory(guid='guid@', name='Danger Danger')
mbs = BlockSubmission.objects.create(
input_guids='guid@\ninvalid@',
@ -1004,6 +1012,9 @@ class TestBlockSubmissionAdmin(TestCase):
assert mbs.url != 'new.url'
assert mbs.reason != 'a reason'
# blocks would not have been submitted to kinto legacy collection
legacy_publish_blocks_mock.assert_not_called()
# And the blocksubmission was rejected, so no Blocks created
assert mbs.signoff_state == BlockSubmission.SIGNOFF_REJECTED
assert Block.objects.count() == 0
@ -1548,7 +1559,9 @@ class TestBlockAdminBulkDelete(TestCase):
]
assert not submission.block_set.all().exists()
def test_submit_no_dual_signoff(self):
@override_switch('blocklist_legacy_submit', active=True)
@mock.patch('olympia.blocklist.models.legacy_delete_blocks')
def test_submit_no_dual_signoff(self, legacy_delete_blocks_mock):
addon_adu = settings.DUAL_SIGNOFF_AVERAGE_DAILY_USERS_THRESHOLD
block_with_addon, block_no_addon = self._test_delete_multiple_submit(
addon_adu=addon_adu)
@ -1556,8 +1569,12 @@ class TestBlockAdminBulkDelete(TestCase):
block_with_addon,
block_no_addon,
has_signoff=False)
legacy_delete_blocks_mock.assert_called_with(
[block_with_addon, block_no_addon])
def test_submit_dual_signoff(self):
@override_switch('blocklist_legacy_submit', active=True)
@mock.patch('olympia.blocklist.models.legacy_delete_blocks')
def test_submit_dual_signoff(self, legacy_delete_blocks_mock):
addon_adu = settings.DUAL_SIGNOFF_AVERAGE_DAILY_USERS_THRESHOLD + 1
block_with_addon, block_no_addon = self._test_delete_multiple_submit(
addon_adu=addon_adu)
@ -1574,6 +1591,8 @@ class TestBlockAdminBulkDelete(TestCase):
block_with_addon,
block_no_addon,
has_signoff=True)
legacy_delete_blocks_mock.assert_called_with(
[block_with_addon, block_no_addon])
def test_edit_with_delete_submission(self):
threshold = settings.DUAL_SIGNOFF_AVERAGE_DAILY_USERS_THRESHOLD

Просмотреть файл

@ -75,6 +75,7 @@ class TestImportBlocklist(TestCase):
assert block.kinto_id == '*' + this_block['id']
assert block.include_in_legacy
assert block.modified == datetime(2019, 11, 29, 22, 22, 46, 785000)
assert block.is_imported_from_kinto_regex
assert KintoImport.objects.count() == 6
assert KintoImport.objects.filter(
outcome=KintoImport.OUTCOME_NOMATCH).count() == 4
@ -101,6 +102,7 @@ class TestImportBlocklist(TestCase):
assert blocks[0].kinto_id == blocklist_json['data'][1]['id']
assert blocks[0].include_in_legacy
assert blocks[0].modified == datetime(2019, 11, 29, 15, 32, 56, 477000)
assert not blocks[0].is_imported_from_kinto_regex
assert blocks[1].guid == 'Ytarkovpn.5.14@firefox.com'
assert blocks[1].url == blocklist_json['data'][2]['details']['bug']
@ -112,6 +114,8 @@ class TestImportBlocklist(TestCase):
assert blocks[1].kinto_id == blocklist_json['data'][2]['id']
assert blocks[1].include_in_legacy
assert blocks[1].modified == datetime(2019, 11, 22, 16, 49, 58, 416000)
assert not blocks[1].is_imported_from_kinto_regex
assert KintoImport.objects.count() == 6
assert KintoImport.objects.filter(
outcome=KintoImport.OUTCOME_NOMATCH).count() == 3

Просмотреть файл

@ -31,6 +31,17 @@ class TestBlock(TestCase):
assert block.is_version_blocked('10.1')
assert block.is_version_blocked('10.%s' % (MAX_VERSION_PART + 1))
def test_is_imported_from_kinto_regex(self):
block = Block.objects.create(guid='foo@baa', updated_by=user_factory())
# no kinto_id
assert not block.is_imported_from_kinto_regex
# from a regex kinto_id
block.update(kinto_id='*123456789')
assert block.is_imported_from_kinto_regex
# and a normal one
block.update(kinto_id='1234567890')
assert not block.is_imported_from_kinto_regex
class TestMultiBlockSubmission(TestCase):
def test_is_submission_ready(self):

Просмотреть файл

@ -0,0 +1,78 @@
from unittest import mock
import pytest
from olympia.amo.tests import user_factory
from olympia.blocklist.models import Block
from olympia.blocklist.utils import legacy_delete_blocks, legacy_publish_blocks
from olympia.lib.kinto import KintoServer
@pytest.mark.django_db
@mock.patch.object(KintoServer, 'publish_record')
@mock.patch.object(KintoServer, 'delete_record')
def test_legacy_publish_blocks(delete_mock, publish_mock):
publish_mock.return_value = {'id': 'a-kinto-id'}
block_new = Block.objects.create(
guid='new@guid', include_in_legacy=True, updated_by=user_factory())
block_regex = Block.objects.create(
guid='regex@guid', include_in_legacy=True, updated_by=user_factory(),
kinto_id='*regex')
block_legacy_dropped = Block.objects.create(
guid='drop@guid', include_in_legacy=False, updated_by=user_factory(),
kinto_id='dropped_legacy')
block_never_legacy = Block.objects.create(
guid='never@guid', include_in_legacy=False, updated_by=user_factory())
block_update = Block.objects.create(
guid='update@guid', include_in_legacy=True, updated_by=user_factory(),
kinto_id='update')
# Currently we don't ever mix include_in_legacy=True and False together,
# but the function should handle it.
data = {
'details': {
'bug': '',
'why': '',
'name': ''
},
'enabled': True,
'versionRange': [{
'severity': 3,
'minVersion': '0',
'maxVersion': '*',
}],
}
legacy_publish_blocks(
[block_new, block_regex, block_legacy_dropped, block_never_legacy,
block_update])
assert publish_mock.call_args_list == [
mock.call(dict(guid='new@guid', **data)),
mock.call(dict(guid='regex@guid', **data)),
mock.call(dict(guid='update@guid', **data), 'update')]
assert delete_mock.call_args_list == [
mock.call('dropped_legacy')]
assert block_new.kinto_id == 'a-kinto-id'
assert block_regex.kinto_id == 'a-kinto-id' # it'd be unique if not mocked
assert block_legacy_dropped.kinto_id == ''
assert block_update.kinto_id == 'update' # it's not changed
@pytest.mark.django_db
@mock.patch.object(KintoServer, 'delete_record')
def test_legacy_delete_blocks(delete_record_mock):
block = Block.objects.create(
guid='legacy@guid', include_in_legacy=True, updated_by=user_factory(),
kinto_id='legacy')
block_regex = Block.objects.create(
guid='regex@guid', include_in_legacy=True, updated_by=user_factory(),
kinto_id='*regex')
block_not_legacy = Block.objects.create(
guid='not@guid', include_in_legacy=False, updated_by=user_factory(),
kinto_id='not_legacy')
block_not_imported = Block.objects.create(
guid='new@guid', include_in_legacy=True, updated_by=user_factory())
legacy_delete_blocks(
[block, block_regex, block_not_legacy, block_not_imported])
assert delete_record_mock.call_args_list == [mock.call('legacy')]
assert block.kinto_id == ''

Просмотреть файл

@ -5,10 +5,14 @@ from filtercascade import FilterCascade
import olympia.core.logger
from olympia import amo
from olympia.activity import log_create
from olympia.lib.kinto import KintoServer
log = olympia.core.logger.getLogger('z.amo.blocklist')
KINTO_BUCKET = 'staging'
KINTO_COLLECTION_LEGACY = 'addons'
def add_version_log_for_blocked_versions(obj, al):
from olympia.activity.models import VersionLog
@ -116,3 +120,57 @@ def generateMLBF(stats, *, blocked, not_blocked, capacity, diffMetaFile=None):
log.debug("Filter cascade layers: {layers}, bit: {bits}".format(
layers=cascade.layerCount(), bits=cascade.bitCount()))
return cascade
def legacy_publish_blocks(blocks):
server = KintoServer(KINTO_BUCKET, KINTO_COLLECTION_LEGACY)
for block in blocks:
needs_updating = block.include_in_legacy and block.kinto_id
needs_creating = block.include_in_legacy and not needs_updating
needs_deleting = block.kinto_id and not block.include_in_legacy
if needs_updating or needs_creating:
if block.is_imported_from_kinto_regex:
log.debug(
f'Block [{block.guid}] was imported from a regex guid so '
'can\'t be safely updated. Creating as a new Block '
'instead.')
needs_creating = True
data = {
'guid': block.guid,
'details': {
'bug': block.url,
'why': block.reason,
'name': str(block.reason).partition('.')[0], # required
},
'enabled': True,
'versionRange': [{
'severity': 3, # Always high severity now.
'minVersion': block.min_version,
'maxVersion': block.max_version,
}],
}
if needs_creating:
record = server.publish_record(data)
block.update(kinto_id=record.get('id', ''))
else:
server.publish_record(data, block.kinto_id)
elif needs_deleting:
server.delete_record(block.kinto_id)
block.update(kinto_id='')
# else no existing kinto record and it shouldn't be in legacy so skip
server.signoff_request()
def legacy_delete_blocks(blocks):
server = KintoServer(KINTO_BUCKET, KINTO_COLLECTION_LEGACY)
for block in blocks:
if block.kinto_id and block.include_in_legacy:
if block.is_imported_from_kinto_regex:
log.debug(
f'Block [{block.guid}] was imported from a regex guid so '
'can\'t be safely deleted. Skipping.')
else:
server.delete_record(block.kinto_id)
block.update(kinto_id='')
server.signoff_request()

Просмотреть файл

@ -124,3 +124,5 @@ FXA_SQS_AWS_QUEUE_URL = (
'amo-account-change-dev')
VAMO_URL = 'https://versioncheck-dev.allizom.org'
KINTO_API_IS_TEST_SERVER = True

Просмотреть файл

@ -108,3 +108,5 @@ FXA_SQS_AWS_QUEUE_URL = (
EXTENSION_WORKSHOP_URL = env(
'EXTENSION_WORKSHOP_URL', default='https://extensionworkshop.com')
KINTO_API_URL = 'http://settings-writer.prod.mozaws.net/v1/'

Просмотреть файл

@ -123,3 +123,5 @@ VAMO_URL = 'https://versioncheck.allizom.org'
EXTENSION_WORKSHOP_URL = env(
'EXTENSION_WORKSHOP_URL', default='https://extensionworkshop.allizom.org')
KINTO_API_URL = 'https://settings-writer.stage.mozaws.net/v1/'

138
src/olympia/lib/kinto.py Normal file
Просмотреть файл

@ -0,0 +1,138 @@
from base64 import b64encode
from django.conf import settings
import requests
import olympia.core.logger
log = olympia.core.logger.getLogger('lib.kinto')
class KintoServer(object):
username = None
password = None
bucket = None
collection = None
_setup_done = False
_needs_signoff = False
def __init__(self, bucket, collection):
self.username = settings.BLOCKLIST_KINTO_USERNAME
self.password = settings.BLOCKLIST_KINTO_PASSWORD
self.bucket = bucket
self.collection = collection
def setup(self):
if self._setup_done:
return
if settings.KINTO_API_IS_TEST_SERVER:
self.setup_test_server_auth()
self.bucket = f'{self.bucket}_{self.username}'
self.setup_test_server_collection()
self._setup_done = True
@property
def headers(self):
b64 = b64encode(f'{self.username}:{self.password}'.encode()).decode()
return {
'Content-Type': 'application/json',
'Authorization': f'Basic {b64}'}
def setup_test_server_auth(self):
# check if the user already exists in kinto's accounts
host = settings.KINTO_API_URL
response = requests.get(host, headers=self.headers)
user_id = response.json().get('user', {}).get('id')
if user_id != f'account:{self.username}':
# lets create it
log.info('Creating kinto test account for %s' % self.username)
response = requests.put(
f'{host}accounts/{self.username}',
json={'data': {'password': self.password}},
headers={'Content-Type': 'application/json'})
if response.status_code != 201:
log.error(
'Creating kinto test account for %s failed. [%s]' %
(self.username, response.content),
stack_info=True)
raise ConnectionError('Kinto account not created')
def setup_test_server_collection(self):
# check if the bucket and collection exist
host = settings.KINTO_API_URL
url = (
f'{host}buckets/{self.bucket}/'
f'collections/{self.collection}/records')
headers = self.headers
response = requests.get(url, headers=headers)
if response.status_code == 403:
# lets create them
data = {'permissions': {'read': ["system.Everyone"]}}
log.info(
'Creating kinto bucket %s and collection %s' %
(self.bucket, self.collection))
response = requests.put(
f'{host}buckets/{self.bucket}',
json=data,
headers=headers)
response = requests.put(
f'{host}buckets/{self.bucket}/collections/{self.collection}',
json=data,
headers=headers)
if response.status_code != 201:
log.error(
'Creating collection %s/%s failed: %s' %
(self.bucket, self.collection, response.content),
stack_info=True)
raise ConnectionError('Kinto collection not created')
def publish_record(self, data, kinto_id=None):
"""Publish a record to kinto. If `kinto_id` is not None the existing
record will be updated (PUT); otherwise a new record will be created
(POST)."""
self.setup()
add_url = (
f'{settings.KINTO_API_URL}buckets/{self.bucket}/'
f'collections/{self.collection}/records')
json_data = {'data': data}
if not kinto_id:
log.info('Creating record for [%s]' % data.get('guid'))
response = requests.post(
add_url, json=json_data, headers=self.headers)
else:
log.info(
'Updating record [%s] for [%s]' % (kinto_id, data.get('guid')))
update_url = f'{add_url}/{kinto_id}'
response = requests.put(
update_url, json=json_data, headers=self.headers)
if response.status_code not in (200, 201):
log.error(
'Creating record for [%s] failed: %s' %
(data.get('guid'), response.content),
stack_info=True)
raise ConnectionError('Kinto record not created/updated')
self._needs_signoff = True
return response.json().get('data', {})
def delete_record(self, kinto_id):
self.setup()
url = (
f'{settings.KINTO_API_URL}buckets/{self.bucket}/'
f'collections/{self.collection}/records/{kinto_id}')
requests.delete(
url, headers=self.headers)
self._needs_signoff = True
def signoff_request(self):
if not self._needs_signoff:
return
self.setup()
url = (
f'{settings.KINTO_API_URL}buckets/{self.bucket}/'
f'collections/{self.collection}')
requests.patch(
url, json={'data': {'status': 'to-review'}}, headers=self.headers)
self._needs_signoff = False

Просмотреть файл

@ -1924,6 +1924,13 @@ YARA_GIT_REPOSITORY = env('YARA_GIT_REPOSITORY', default=None)
# Addon.average_daily_user count that forces dual sign-off for Blocklist Blocks
DUAL_SIGNOFF_AVERAGE_DAILY_USERS_THRESHOLD = 100_000
KINTO_API_URL = 'https://kinto.dev.mozaws.net/v1/'
# The kinto test server needs accounts and setting up before using.
KINTO_API_IS_TEST_SERVER = False
BLOCKLIST_KINTO_USERNAME = env(
'BLOCKLIST_KINTO_USERNAME', default='amo_dev')
BLOCKLIST_KINTO_PASSWORD = env(
'BLOCKLIST_KINTO_PASSWORD', default='amo_dev_password')
# The path to the current google service account configuration. This is
# being used to query Google BigQuery as part of our stats processing.

Просмотреть файл

@ -0,0 +1,149 @@
from django.conf import settings
from django.test.utils import override_settings
import responses
from olympia.amo.tests import TestCase
from olympia.lib.kinto import KintoServer
@override_settings(
BLOCKLIST_KINTO_USERNAME='test_username',
BLOCKLIST_KINTO_PASSWORD='test_password')
class TestKintoServer(TestCase):
def test_setup_test_server_auth(self):
server = KintoServer('foo', 'baa')
responses.add(
responses.GET,
settings.KINTO_API_URL,
content_type='application/json',
json={'user': {'id': ''}})
responses.add(
responses.PUT,
settings.KINTO_API_URL + 'accounts/test_username',
content_type='application/json',
json={'data': {'password': 'test_password'}},
status=201)
server.setup_test_server_auth()
# If repeated then the account should exist the 2nd time
responses.add(
responses.GET,
settings.KINTO_API_URL,
content_type='application/json',
json={'user': {'id': 'account:test_username'}})
server.setup_test_server_auth()
def test_setup_test_server_collection(self):
server = KintoServer('foo', 'baa')
responses.add(
responses.GET,
settings.KINTO_API_URL + 'buckets/foo/collections/baa/records',
content_type='application/json',
status=403)
responses.add(
responses.PUT,
settings.KINTO_API_URL + 'buckets/foo',
content_type='application/json')
responses.add(
responses.PUT,
settings.KINTO_API_URL + 'buckets/foo/collections/baa',
content_type='application/json',
status=201)
server.setup_test_server_collection()
# If repeated then the collection shouldn't 403 a second time
responses.add(
responses.GET,
settings.KINTO_API_URL + 'buckets/foo/collections/baa/records',
content_type='application/json')
server.setup_test_server_collection()
@override_settings(KINTO_API_IS_TEST_SERVER=False)
def test_setup_not_test_server(self):
server = KintoServer('foo', 'baa')
server.setup() # will just return
assert server._setup_done
assert server.bucket == 'foo'
@override_settings(KINTO_API_IS_TEST_SERVER=True)
def test_setup(self):
server = KintoServer('foo', 'baa')
responses.add(
responses.GET,
settings.KINTO_API_URL,
content_type='application/json',
json={'user': {'id': 'account:test_username'}})
records_url = (
settings.KINTO_API_URL +
'buckets/foo_test_username/collections/baa/records')
responses.add(
responses.GET,
records_url,
content_type='application/json')
server.setup()
assert server._setup_done
assert server.bucket == 'foo_test_username'
server.setup() # a second time shouldn't make any requests
def test_publish_record(self):
server = KintoServer('foo', 'baa')
server._setup_done = True
assert not server._needs_signoff
responses.add(
responses.POST,
settings.KINTO_API_URL + 'buckets/foo/collections/baa/records',
content_type='application/json',
json={'data': {'id': 'new!'}})
record = server.publish_record({'something': 'somevalue'})
assert server._needs_signoff
assert record == {'id': 'new!'}
url = (
settings.KINTO_API_URL +
'buckets/foo/collections/baa/records/an-id')
responses.add(
responses.PUT,
url,
content_type='application/json',
json={'data': {'id': 'updated'}})
record = server.publish_record({'something': 'somevalue'}, 'an-id')
assert record == {'id': 'updated'}
def test_delete_record(self):
server = KintoServer('foo', 'baa')
server._setup_done = True
assert not server._needs_signoff
url = (
settings.KINTO_API_URL +
'buckets/foo/collections/baa/records/an-id')
responses.add(
responses.DELETE,
url,
content_type='application/json')
server.delete_record('an-id')
assert server._needs_signoff
def test_signoff(self):
server = KintoServer('foo', 'baa')
server._setup_done = True
# should return because nothing to signoff
server.signoff_request()
server._needs_signoff = True
url = (
settings.KINTO_API_URL +
'buckets/foo/collections/baa')
responses.add(
responses.PATCH,
url,
content_type='application/json')
server.signoff_request()
assert not server._needs_signoff