"""Carrying a guide from one site to another. Sites are separate databases, and the ones that matter are air-gapped from wherever the guide was written. These tests are about what has to survive that trip and what must not: the blocks and their images travel, the ids do not. """ import base64 import json import pytest from plugins.guides.services import bundle as bundleservice PNG = base64.b64decode( 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk' 'YPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==') class FakeImage: def __init__(self, imageid, data=PNG, mimetype='image/png', filename='shot.png'): self.guideimageid = imageid self.data = data self.mimetype = mimetype self.filename = filename class FakeGuide: def __init__(self, blocks, slug='a-guide', title='A Guide', summary='What it does', tags='shopfloor, printers'): self.slug = slug self.title = title self.summary = summary self.tags = tags self.blocks = blocks def test_a_bundle_carries_what_the_far_side_needs(): guide = FakeGuide([{'id': 'b001', 'type': 'paragraph', 'text': 'Hello'}]) payload = bundleservice.build(guide, []) assert payload['format'] == 'shopdb-guide' assert payload['slug'] == 'a-guide' assert payload['title'] == 'A Guide' assert payload['summary'] == 'What it does' assert payload['tags'] == 'shopfloor, printers' assert payload['blocks'][0]['text'] == 'Hello' def test_ids_do_not_travel(): """A guide's id and revision belong to the database that issued them.""" guide = FakeGuide([{'id': 'b001', 'type': 'paragraph', 'text': 'Hello'}]) payload = bundleservice.build(guide, []) assert 'guideid' not in payload assert 'revision' not in payload assert 'publishedat' not in payload def test_an_image_travels_inside_the_file_not_beside_it(): """One file. A bundle that needs its images copied alongside it is a bundle that will eventually arrive without them.""" guide = FakeGuide([{'id': 'b001', 'type': 'image', 'imageid': 42, 'alt': 'the screen'}]) payload = bundleservice.build(guide, [FakeImage(42)]) assert len(payload['images']) == 1 assert payload['images'][0]['mimetype'] == 'image/png' assert base64.b64decode(payload['images'][0]['data']) == PNG block = payload['blocks'][0] assert 'imageid' not in block, 'a local id means nothing on the far side' assert block['imagetoken'] == payload['images'][0]['token'] assert block['alt'] == 'the screen' def test_the_same_image_used_twice_travels_once(): guide = FakeGuide([ {'id': 'b001', 'type': 'image', 'imageid': 42}, {'id': 'b002', 'type': 'image', 'imageid': 42}, ]) payload = bundleservice.build(guide, [FakeImage(42)]) assert len(payload['images']) == 1 assert payload['blocks'][0]['imagetoken'] == payload['blocks'][1]['imagetoken'] def test_an_image_the_guide_does_not_use_is_left_behind(): """An orphaned paste from a draft is not part of the procedure.""" guide = FakeGuide([{'id': 'b001', 'type': 'paragraph', 'text': 'Hi'}]) payload = bundleservice.build(guide, [FakeImage(42), FakeImage(43)]) assert payload['images'] == [] def test_a_block_pointing_at_a_missing_image_is_dropped(): """It would render as 'Image unavailable' on the far side, which reads as a failed import rather than as missing data.""" guide = FakeGuide([ {'id': 'b001', 'type': 'paragraph', 'text': 'Before'}, {'id': 'b002', 'type': 'image', 'imageid': 99}, {'id': 'b003', 'type': 'paragraph', 'text': 'After'}, ]) payload = bundleservice.build(guide, []) assert [b['type'] for b in payload['blocks']] == ['paragraph', 'paragraph'] def test_a_bundle_survives_the_round_trip(): guide = FakeGuide([ {'id': 'b001', 'type': 'heading', 'text': 'Steps'}, {'id': 'b002', 'type': 'image', 'imageid': 7}, {'id': 'b003', 'type': 'checklist', 'items': [{'id': 'i1', 'text': 'Tick me'}]}, ]) text = bundleservice.dumps(bundleservice.build(guide, [FakeImage(7)])) back = bundleservice.loads(text) assert back['slug'] == 'a-guide' assert [b['type'] for b in back['blocks']] == ['heading', 'image', 'checklist'] assert back['images'][0]['data'] == PNG, 'decoded back to bytes, not base64' assert back['blocks'][2]['items'][0]['text'] == 'Tick me' def test_something_that_is_not_a_bundle_is_refused_before_anything_is_written(): with pytest.raises(bundleservice.BundleError): bundleservice.loads('{"hello": "world"}') with pytest.raises(bundleservice.BundleError): bundleservice.loads('not json at all') with pytest.raises(bundleservice.BundleError): bundleservice.loads(json.dumps({'format': 'shopdb-guide', 'version': 1})) def test_a_bundle_from_a_newer_site_is_refused_rather_than_half_read(): newer = json.dumps({'format': 'shopdb-guide', 'version': 99, 'slug': 'x', 'blocks': []}) with pytest.raises(bundleservice.BundleError) as error: bundleservice.loads(newer) assert 'newer than this site can read' in str(error.value) def test_an_unreadable_image_names_itself(): broken = json.dumps({ 'format': 'shopdb-guide', 'version': 1, 'slug': 'x', 'blocks': [], 'images': [{'token': 'img001', 'mimetype': 'image/png', 'filename': 'x.png', 'data': 'not base64 !!'}]}) with pytest.raises(bundleservice.BundleError) as error: bundleservice.loads(broken) assert 'img001' in str(error.value)