import base64 import io import json import random import re import tarfile import httmock import pytest from httmock import HTTMock from pyquery import PyQuery from test_manager import login from webtest import Upload from hobo.applications.models import ( Application, AsyncJob, DeploymentError, Element, Relation, ScanError, Version, ) from hobo.environment.models import Authentic, Wcs pytestmark = pytest.mark.django_db WCS_AVAILABLE_OBJECTS = { "data": [ { "id": "forms", "text": "Forms", "singular": "Form", "urls": {"list": "https://wcs.example.invalid/api/export-import/forms/"}, }, { "id": "cards", "text": "Card Models", "singular": "Card Model", "urls": {"list": "https://wcs.example.invalid/api/export-import/cards/"}, }, { "id": "workflows", "text": "Workflows", "singular": "Workflow", "urls": {"list": "https://wcs.example.invalid/api/export-import/workflows/"}, }, { "id": "blocks", "text": "Blocks", "singular": "Block of fields", "minor": True, "urls": {"list": "https://wcs.example.invalid/api/export-import/blocks/"}, }, { "id": "data-sources", "text": "Data Sources", "singular": "Data Source", "minor": True, "urls": {"list": "https://wcs.example.invalid/api/export-import/data-sources/"}, }, { "id": "mail-templates", "text": "Mail Templates", "singular": "Mail Template", "minor": True, "urls": {"list": "https://wcs.example.invalid/api/export-import/mail-templates/"}, }, { "id": "comment-templates-categories", "text": "Categories (comment templates)", "singular": "Category (comment templates)", "minor": True, "urls": {"list": "https://wcs.example.invalid/api/export-import/comment-templates-categories/"}, }, { "id": "wscalls", "text": "Webservice Calls", "singular": "Webservice Call", "minor": True, "urls": {"list": "https://wcs.example.invalid/api/export-import/wscalls/"}, }, { "id": "roles", "text": "Roles", "singular": "Role", "minor": True, "urls": {"list": "https://wcs.example.invalid/api/export-import/roles/"}, }, ] } WCS_AVAILABLE_FORMS = { "data": [ { "id": "test-form", "text": "Test Form", "type": "forms", "urls": { "export": "https://wcs.example.invalid/api/export-import/forms/test-form/", "dependencies": "https://wcs.example.invalid/api/export-import/forms/test-form/dependencies/", "redirect": "https://wcs.example.invalid/api/export-import/forms/test-form/redirect/", }, }, { "id": "test2-form", "text": "Second Test Form", "type": "forms", "urls": { "export": "https://wcs.example.invalid/api/export-import/forms/test2-form/", "dependencies": "https://wcs.example.invalid/api/export-import/forms/test2-form/dependencies/", }, }, { "id": "foo2-form", "text": "Foo2 Test Form", "type": "forms", "category": "Foo", "urls": { "export": "https://wcs.example.invalid/api/export-import/forms/foo2-form/", "dependencies": "https://wcs.example.invalid/api/export-import/forms/foo2-form/dependencies/", }, }, { "id": "foo-form", "text": "Foo Test Form", "type": "forms", "category": "Foo", "urls": { "export": "https://wcs.example.invalid/api/export-import/forms/foo-form/", "dependencies": "https://wcs.example.invalid/api/export-import/forms/foo-form/dependencies/", }, }, { "id": "bar-form", "text": "Bar Test Form", "type": "forms", "category": "Bar", "urls": { "export": "https://wcs.example.invalid/api/export-import/forms/bar-form/", "dependencies": "https://wcs.example.invalid/api/export-import/forms/bar-form/dependencies/", }, }, ] } WCS_FORM_DEPENDENCIES = { "data": [ { "id": "test-card", "text": "Test Card", "type": "cards", "urls": { "export": "https://wcs.example.invalid/api/export-import/cards/test-card/", "dependencies": "https://wcs.example.invalid/api/export-import/cards/test-card/dependencies/", }, } ] } def mocked_http(url, request): assert url.query.count('&signature=') == 1 if url.netloc == 'idp.example.invalid': if url.path == '/api/roles/': return { 'content': json.dumps({'name': 'test', 'uuid': '123', 'slug': 'test'}), 'status_code': 200, } if url.path == '/api/provision/': return { 'content': json.dumps({'name': 'test', 'uuid': '123', 'slug': 'test'}), 'status_code': 200, } return {'content': '{}', 'status_code': 500} if url.netloc == 'wcs.example.invalid' and url.path == '/api/export-import/': return {'content': json.dumps(WCS_AVAILABLE_OBJECTS), 'status_code': 200} if url.path == '/api/export-import/forms/': return {'content': json.dumps(WCS_AVAILABLE_FORMS), 'status_code': 200} if url.path == '/api/export-import/forms/test-form/dependencies/': return {'content': json.dumps(WCS_FORM_DEPENDENCIES), 'status_code': 200} if url.path.endswith('/dependencies/'): return {'content': json.dumps({'data': []}), 'status_code': 200} if url.path == '/api/export-import/forms/test-form/': return {'content': '', 'status_code': 200, 'headers': {'content-length': '10'}} if url.path == '/api/export-import/cards/test-card/': return {'content': '', 'status_code': 200, 'headers': {'content-length': '10'}} if url.path == '/api/export-import/bundle-import/': return { 'content': json.dumps({'err': 0, 'url': 'https://wcs.example.invalid/api/jobs/job-uuid/'}), 'status_code': 200, } return {'content': json.dumps({'data': []}), 'status_code': 200} @pytest.mark.parametrize('analyze', [True, False]) def test_create_application(app, admin_user, settings, analyze): Wcs.objects.create(base_url='https://wcs.example.invalid', slug='foobar', title='Foobar') settings.KNOWN_SERVICES = { 'wcs': { 'blah': { # simulate an instance from another collectivity 'title': 'Unknown', 'url': 'https://unknown.example.invalid/', 'orig': 'example.org', 'secret': 'xxx', }, 'foobar': { 'title': 'Foobar', 'url': 'https://wcs.example.invalid/', 'orig': 'example.org', 'secret': 'xxx', }, } } login(app) resp = app.get('/applications/') resp = resp.click('Create') resp.form['name'] = 'Test' resp = resp.form.submit() with HTTMock(mocked_http): resp = resp.follow() assert 'You should now assemble the different parts of your application.' in resp.text # edit metadata resp = resp.click('Metadata') resp.form['description'] = 'Lorem ipsum' resp.form['documentation_url'] = 'http://foo.bar' resp = resp.form.submit().follow() application = Application.objects.get(slug='test') assert application.icon.name == '' assert application.documentation_url == 'http://foo.bar' # add forms assert '/add/forms/' in resp resp = resp.click('Forms') assert len(resp.pyquery('.application-elements div')) == 3 assert resp.pyquery('.application-elements div:nth-child(1) h4').text() == 'Bar' assert PyQuery(resp.pyquery('.application-elements div:nth-child(1) input')[0]).val() == 'bar-form' assert resp.pyquery('.application-elements div:nth-child(2) h4').text() == 'Foo' assert PyQuery(resp.pyquery('.application-elements div:nth-child(2) input')[0]).val() == 'foo-form' assert PyQuery(resp.pyquery('.application-elements div:nth-child(2) input')[1]).val() == 'foo2-form' assert resp.pyquery('.application-elements div:nth-child(3) h4').text() == 'Uncategorized' assert PyQuery(resp.pyquery('.application-elements div:nth-child(3) input')[0]).val() == 'test2-form' assert PyQuery(resp.pyquery('.application-elements div:nth-child(3) input')[1]).val() == 'test-form' resp.form.fields['elements'][4].checked = True resp = resp.form.submit().follow() assert application.elements.count() == 1 element = application.elements.all()[0] assert element.slug == 'test-form' assert 'Test Card' not in resp.text if analyze: resp = resp.click('Scan dependencies').follow() assert Application.objects.get(slug='test').elements.count() == 2 resp = resp.click('Generate application bundle') resp.form['number'] = '1.0' resp.form['notes'] = 'Foo bar blah.' resp = resp.form.submit().follow() assert 'Test Card' in resp.text version = Version.objects.latest('pk') assert version.number == '1.0' assert version.notes == 'Foo bar blah.' resp = resp.click('Download') assert resp.content_type == 'application/x-tar' # uncompressed tar, primitive check of contents assert b'' in resp.content assert b'' in resp.content assert b'"icon": null' in resp.content assert b'"documentation_url": "http://foo.bar"' in resp.content assert b'"version_number": "1.0"' in resp.content assert b'"version_notes": "Foo bar blah."' in resp.content resp = app.get('/applications/manifest/test/versions/') versions = [e.text() for e in resp.pyquery('h3').items()] assert versions.count('1.0') == 1 assert resp.text.count('Creating application bundle') == 1 resp = resp.click(href='/applications/manifest/test/download/%s/' % version.pk) assert resp.content_type == 'application/x-tar' assert b'"version_number": "1.0"' in resp.content # generate again without changing version number resp = app.get('/applications/manifest/test/generate/') assert resp.form['number'].value == '1.0' # last one assert resp.form['notes'].value == 'Foo bar blah.' # last one resp.form['notes'] = 'Foo bar blahha.' resp = resp.form.submit().follow() same_version = Version.objects.latest('pk') assert same_version.number == '1.0' assert same_version.notes == 'Foo bar blahha.' assert same_version.pk == version.pk resp = resp.click('Download') assert resp.content_type == 'application/x-tar' assert b'"version_number": "1.0"' in resp.content assert b'"version_notes": "Foo bar blahha."' in resp.content resp = app.get('/applications/manifest/test/versions/') versions = [e.text() for e in resp.pyquery('h3').items()] assert versions.count('1.0') == 1 assert resp.text.count('Creating application bundle') == 2 resp = resp.click(href='/applications/manifest/test/download/%s/' % same_version.pk) assert resp.content_type == 'application/x-tar' assert b'"version_number": "1.0"' in resp.content # add an icon resp = app.get('/applications/manifest/test/metadata/') resp.form['icon'] = Upload( 'foo.png', base64.decodebytes( b'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQAAAAA3bvkkAAAACklEQVQI12NoAAAAggCB3UNq9AAAAABJRU5ErkJggg==' ), 'image/png', ) resp.form['documentation_url'] = '' # and reset documentation_url resp = resp.form.submit().follow() application.refresh_from_db() assert re.match(r'applications/icons/foo(_\w+)?.png', application.icon.name) assert application.documentation_url == '' # try an icon in an invalid format resp = app.get('/applications/manifest/test/metadata/') resp.form['icon'] = Upload('test.txt', b'hello', 'text/plain') resp = resp.form.submit() assert 'The icon must be in JPEG or PNG format' in resp application.refresh_from_db() assert re.match(r'applications/icons/foo(_\w+)?.png', application.icon.name) resp = app.get('/applications/manifest/test/') resp = resp.click('Generate application bundle') resp.form['number'] = '2.0' resp.form['notes'] = 'Foo bar blah. But with an icon.' resp = resp.form.submit().follow() resp = resp.click('Download') assert resp.content_type == 'application/x-tar' # uncompressed tar, primitive check of contents assert b'' in resp.content assert b'' in resp.content assert b'"icon": "foo' in resp.content assert b'"documentation_url": ""' in resp.content assert b'"version_number": "2.0"' in resp.content assert b'"version_notes": "Foo bar blah. But with an icon."' in resp.content version = Version.objects.latest('pk') assert version.number == '2.0' assert version.notes == 'Foo bar blah. But with an icon.' assert version.pk != same_version.pk resp = app.get('/applications/manifest/test/versions/') versions = [e.text() for e in resp.pyquery('h3').items()] assert versions.count('1.0') == 1 assert versions.count('2.0') == 1 assert resp.text.count('Creating application bundle') == 3 resp = resp.click(href='/applications/manifest/test/download/%s/' % same_version.pk) assert resp.content_type == 'application/x-tar' assert b'"version_number": "1.0"' in resp.content resp = app.get('/applications/manifest/test/versions/') resp = resp.click(href='/applications/manifest/test/download/%s/' % version.pk) assert resp.content_type == 'application/x-tar' assert b'"version_number": "2.0"' in resp.content resp = app.get('/applications/manifest/test/generate/') assert resp.form['number'].value == '2.0' # last one assert resp.form['notes'].value == 'Foo bar blah. But with an icon.' # last one resp.form['number'] = '1.0' # old number resp = resp.form.submit().follow() new_version = Version.objects.latest('pk') assert new_version.number == '1.0' assert new_version.notes == 'Foo bar blah. But with an icon.' assert new_version.pk != version.pk # new version created resp = app.get('/applications/manifest/test/versions/') versions = [e.text() for e in resp.pyquery('h3').items()] assert versions.count('1.0') == 2 assert versions.count('2.0') == 1 assert resp.text.count('Creating application bundle') == 4 def response_content(url, request): if url.path == '/api/export-import/bundle-declare/': return {'status_code': 500} return mocked_http(url, request) resp = app.get('/applications/manifest/test/generate/') with HTTMock(response_content): with pytest.raises(DeploymentError) as e: resp.form.submit() assert str(e.value) == 'Failed to declare elements for module wcs (500)' job = AsyncJob.objects.latest('pk') assert job.status == 'failed' assert job.exception == 'Failed to declare elements for module wcs (500)' # non editable app application.editable = False application.save() app.get('/applications/manifest/test/metadata/', status=404) app.get('/applications/manifest/test/scandeps/', status=404) app.get('/applications/manifest/test/generate/', status=404) app.get('/applications/manifest/test/add/forms/', status=404) app.get('/applications/manifest/test/delete/%s/' % application.relation_set.first().pk, status=404) def test_manifest_ordering(app, admin_user, settings): Wcs.objects.create(base_url='https://wcs.example.invalid', slug='foobar', title='Foobar') settings.KNOWN_SERVICES = { 'wcs': { 'foobar': { 'title': 'Foobar', 'url': 'https://wcs.example.invalid/', 'orig': 'example.org', 'secret': 'xxx', } } } application = Application.objects.create(name='Test', slug='test') objects = [ # type, slug, auto_dependency ('forms', 'bar', True), ('forms', 'foo', True), ('cards', 'bar', True), ('cards', 'foo', True), ('workflows', 'bar', True), ('workflows', 'foo', True), ('forms', 'baaaar', False), ('forms', 'foooo', False), ('cards', 'baaaar', False), ('cards', 'foooo', False), ('workflows', 'baaaar', False), ('workflows', 'foooo', False), ] random.shuffle(objects) for _type, slug, auto_dependency in objects: element = Element.objects.create( type=_type, slug=slug, name=slug.title(), cache={}, ) Relation.objects.create(application=application, element=element, auto_dependency=auto_dependency) login(app) with HTTMock(mocked_http): resp = app.get('/applications/manifest/test/') assert resp.pyquery('.application-content li a').text() == ( 'Baaaar - Form remove ' 'Foooo - Form remove ' 'Baaaar - Card Model remove ' 'Foooo - Card Model remove ' 'Baaaar - Workflow remove ' 'Foooo - Workflow remove ' 'Bar - Form ' 'Foo - Form ' 'Bar - Card Model ' 'Foo - Card Model ' 'Bar - Workflow ' 'Foo - Workflow' ) def test_scandeps_on_unknown_element(app, admin_user, settings): Wcs.objects.create(base_url='https://wcs.example.invalid', slug='foobar', title='Foobar') settings.KNOWN_SERVICES = { 'wcs': { 'foobar': { 'title': 'Foobar', 'url': 'https://wcs.example.invalid/', 'orig': 'example.org', 'secret': 'xxx', } } } application = Application.objects.create(name='Test', slug='test') element = Element.objects.create( type='forms', slug='unknown', name='Unknown', cache={ 'urls': { "dependencies": "https://wcs.example.invalid/api/export-import/forms/unknown/dependencies/", } }, ) relation = Relation.objects.create(application=application, element=element) def response_content(url, request): if url.path == '/api/export-import/forms/unknown/dependencies/': return {'status_code': 404} return mocked_http(url, request) login(app) with HTTMock(response_content): with pytest.raises(ScanError) as e: app.get('/applications/manifest/test/scandeps/').follow() assert str(e.value) == 'Failed to scan "Unknown" (type forms, slug unknown) dependencies (404)' job = AsyncJob.objects.latest('pk') assert job.status == 'failed' assert job.exception == 'Failed to scan "Unknown" (type forms, slug unknown) dependencies (404)' relation.refresh_from_db() assert relation.error is True assert relation.error_status == 'notfound' def response_content(url, request): if url.path == '/api/export-import/forms/unknown/dependencies/': return {'status_code': 500} return mocked_http(url, request) with HTTMock(response_content): with pytest.raises(ScanError) as e: app.get('/applications/manifest/test/scandeps/').follow() assert str(e.value) == 'Failed to scan "Unknown" (type forms, slug unknown) dependencies (500)' job = AsyncJob.objects.latest('pk') assert job.status == 'failed' assert job.exception == 'Failed to scan "Unknown" (type forms, slug unknown) dependencies (500)' relation.refresh_from_db() assert relation.error is True assert relation.error_status == 'error' with HTTMock(mocked_http): app.get('/applications/manifest/test/scandeps/').follow() job = AsyncJob.objects.latest('pk') assert job.status == 'completed' relation.refresh_from_db() assert relation.error is False assert relation.error_status is None def test_scandeps_on_renamed_element(app, admin_user, settings): Wcs.objects.create(base_url='https://wcs.example.invalid', slug='foobar', title='Foobar') settings.KNOWN_SERVICES = { 'wcs': { 'foobar': { 'title': 'Foobar', 'url': 'https://wcs.example.invalid/', 'orig': 'example.org', 'secret': 'xxx', } } } application = Application.objects.create(name='Test', slug='test') element = Element.objects.create( type='forms', slug='test-form', name='Test 1 form', cache={ 'urls': {'redirect': 'https://wcs.example.invalid/api/export-import/forms/test-form/redirect/'} }, ) Relation.objects.create(application=application, element=element) element2 = Element.objects.create( type="cards", slug="test-card", name="Test Card", cache={ "urls": { "export": "https://wcs.example.invalid/api/export-import/cards/test-card/", "dependencies": "https://wcs.example.invalid/api/export-import/cards/test-card/dependencies/", } }, ) Relation.objects.create(application=application, element=element2, auto_dependency=True) def response_content(url, request): if url.path == '/api/export-import/forms/': return { 'content': { "data": [ { "id": "test-form", "text": "Test Form (renamed)", "type": "forms", "urls": { "export": "https://wcs.example.invalid/api/export-import/forms/test-form/", "dependencies": "https://wcs.example.invalid/api/export-import/forms/test-form/dependencies/", "redirect": "https://wcs.example.invalid/api/export-import/forms/test-form/redirect/", }, }, ] }, 'status_code': 200, } if url.path == '/api/export-import/forms/test-form/dependencies/': return { 'content': { "data": [ { "id": "test-card", "text": "Test Card (renamed)", "type": "cards", "urls": { "dependencies": "https://wcs.example.invalid/api/export-import/cards/test-card/dependencies/", }, } ] }, 'status_code': 200, } return mocked_http(url, request) login(app) with HTTMock(response_content): app.get('/applications/manifest/test/scandeps/').follow() job = AsyncJob.objects.latest('pk') assert job.status == 'completed' element.refresh_from_db() assert element.name == 'Test Form (renamed)' assert element.cache == { "id": "test-form", "text": "Test Form (renamed)", "type": "forms", "urls": { "export": "https://wcs.example.invalid/api/export-import/forms/test-form/", "dependencies": "https://wcs.example.invalid/api/export-import/forms/test-form/dependencies/", "redirect": "https://wcs.example.invalid/api/export-import/forms/test-form/redirect/", }, } element2 = Element.objects.get(relation__auto_dependency=True) assert element2.name == 'Test Card (renamed)' assert element2.cache == { "id": "test-card", "text": "Test Card (renamed)", "type": "cards", "urls": { "dependencies": "https://wcs.example.invalid/api/export-import/cards/test-card/dependencies/", }, } def response_content(url, request): if url.path == '/api/export-import/forms/': return {'status_code': 404} return mocked_http(url, request) with HTTMock(response_content): with pytest.raises(ScanError) as e: app.get('/applications/manifest/test/scandeps/').follow() assert str(e.value) == 'Failed to get elements of type forms (404)' job = AsyncJob.objects.latest('pk') assert job.status == 'failed' assert job.exception == 'Failed to get elements of type forms (404)' def response_content(url, request): if url.path == '/api/export-import/forms/': return {'status_code': 500} return mocked_http(url, request) with HTTMock(response_content): with pytest.raises(ScanError) as e: app.get('/applications/manifest/test/scandeps/').follow() assert str(e.value) == 'Failed to get elements of type forms (500)' job = AsyncJob.objects.latest('pk') assert job.status == 'failed' assert job.exception == 'Failed to get elements of type forms (500)' @pytest.mark.parametrize('editable', [True, False]) def test_redirect_application_element(app, admin_user, settings, editable): Wcs.objects.create(base_url='https://wcs.example.invalid', slug='foobar', title='Foobar') settings.KNOWN_SERVICES = { 'wcs': { 'foobar': { 'title': 'Foobar', 'url': 'https://wcs.example.invalid/', 'orig': 'example.org', 'secret': 'xxx', } } } application = Application.objects.create(name='Test', slug='test', editable=editable) element = Element.objects.create( type='forms', slug='test-form', name='Test 1 form', cache={ 'urls': {'redirect': 'https://wcs.example.invalid/api/export-import/forms/test-form/redirect/'} }, ) Relation.objects.create(application=application, element=element) element = Element.objects.create( type='forms', slug='test2-form', name='Test 2 form', cache={'urls': {'export': 'https://wcs.example.invalid/api/export-import/forms/test2-form/'}}, ) Relation.objects.create(application=application, element=element) element = Element.objects.create( type='forms', slug='test3-form', name='Test 3 form', cache={}, ) Relation.objects.create(application=application, element=element) element = Element.objects.create( type='roles', slug='test', name='Test', cache={'urls': {'redirect': 'https://wcs.example.invalid/api/export-import/roles/test/redirect/'}}, ) Relation.objects.create(application=application, element=element) login(app) with HTTMock(mocked_http): resp = app.get('/applications/manifest/test/') assert 'https://wcs.example.invalid/api/export-import/forms/test-form/redirect/' in resp assert ( 'https://wcs.example.invalid/api/export-import/forms/test2-form/redirect/' in resp ) # no redirect url, but it's ok from export url assert ( 'https://wcs.example.invalid/api/export-import/forms/test3-form/redirect/' not in resp ) # no urls assert ( 'https://wcs.example.invalid/api/export-import/roles/test/redirect/' not in resp ) # not for roles def test_delete_application(app, admin_user, settings): Wcs.objects.create(base_url='https://wcs.example.invalid', slug='foobar', title='Foobar') settings.KNOWN_SERVICES = { 'wcs': { 'foobar': { 'title': 'Foobar', 'url': 'https://wcs.example.invalid/', 'orig': 'example.org', 'secret': 'xxx', } } } login(app) Application.objects.create(name='AppToDelete', slug='app_to_delete') Application.objects.create(name='OtherApp', slug='other_app') assert Application.objects.count() == 2 with HTTMock(mocked_http): resp = app.get('/applications/manifest/app_to_delete/') resp = resp.click(re.compile('^Delete$')) resp = resp.forms[0].submit() resp = resp.follow() assert '/applications/' in resp assert 'AppToDelete' not in resp.text assert Application.objects.count() == 1 assert Application.objects.first().name == 'OtherApp' def response_content(url, request): if url.path == '/api/export-import/unlink/': return {'status_code': 500} return mocked_http(url, request) with HTTMock(response_content): resp = app.get('/applications/manifest/other_app/') resp = resp.click(re.compile('^Delete$')) resp = resp.forms[0].submit() resp = resp.follow() assert 'Failed to unlink application in module wcs (500)' in resp assert Application.objects.count() == 1 assert Application.objects.first().name == 'OtherApp' def test_404_unknown_app(app, admin_user, settings): login(app) app.get('/applications/manifest/xxx/', status=404) @pytest.fixture def app_bundle(): return get_bundle() @pytest.fixture def app_bundle_with_icon(): return get_bundle(with_icon=True) def get_bundle(with_icon=False): tar_io = io.BytesIO() with tarfile.open(mode='w', fileobj=tar_io) as tar: manifest_json = { 'application': 'Test', 'slug': 'test', 'icon': 'foo.png' if with_icon else None, 'description': '', 'documentation_url': 'http://foo.bar', 'version_number': '43.0' if with_icon else '42.0', 'version_notes': 'foo bar blah', 'elements': [ {'type': 'forms', 'slug': 'test', 'name': 'test', 'auto-dependency': False}, {'type': 'blocks', 'slug': 'test', 'name': 'test', 'auto-dependency': True}, {'type': 'workflows', 'slug': 'test', 'name': 'test', 'auto-dependency': True}, { 'type': 'comment-templates-categories', 'slug': 'test', 'name': 'test', 'auto-dependency': True, }, ], } manifest_fd = io.BytesIO(json.dumps(manifest_json, indent=2).encode()) tarinfo = tarfile.TarInfo('manifest.json') tarinfo.size = len(manifest_fd.getvalue()) tar.addfile(tarinfo, fileobj=manifest_fd) if with_icon: icon_fd = io.BytesIO( b'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQAAAAA3bvkkAAAACklEQVQI12NoAAAAggCB3UNq9AAAAABJRU5ErkJggg==' ) tarinfo = tarfile.TarInfo('foo.png') tarinfo.size = len(icon_fd.getvalue()) tar.addfile(tarinfo, fileobj=icon_fd) return tar_io.getvalue() @pytest.mark.parametrize('action', ['Install', 'Update']) def test_deploy_application(app, admin_user, settings, app_bundle, app_bundle_with_icon, action): Application.objects.all().delete() Wcs.objects.create(base_url='https://wcs.example.invalid', slug='foobar', title='Foobar') settings.KNOWN_SERVICES = { 'wcs': { 'foobar': { 'title': 'Foobar', 'url': 'https://wcs.example.invalid/', 'orig': 'example.org', 'secret': 'xxx', } } } login(app) if action == 'Update': Application.objects.create(name='Test', slug='test', editable=False) def install(resp, bundle): resp = resp.click(action) resp.form['bundle'] = Upload('app.tar', bundle, 'application/x-tar') with HTTMock(mocked_http): resp = resp.form.submit().follow() assert Application.objects.count() == 1 application = Application.objects.get(slug='test') assert application.name == 'Test' if bundle == app_bundle_with_icon: assert re.match(r'applications/icons/foo(_\w+)?.png', application.icon.name) else: assert application.icon.name == '' assert application.documentation_url == 'http://foo.bar' version = application.version_set.latest('pk') if bundle == app_bundle_with_icon: assert version.number == '43.0' else: assert version.number == '42.0' assert version.notes == 'foo bar blah' assert application.elements.count() == 4 job = AsyncJob.objects.latest('pk') assert job.status == 'completed' assert job.progression_urls == {'wcs': {'Foobar': 'https://wcs.example.invalid/api/jobs/job-uuid/'}} return version resp = app.get('/applications/') if action == 'Update': with HTTMock(mocked_http): resp = resp.click(href='/manifest/test/') version1 = install(resp, app_bundle) assert version1.application.version_set.count() == 1 version2 = install(resp, app_bundle) assert version2.application.version_set.count() == 1 assert version1.pk == version2.pk assert version1.creation_timestamp == version2.creation_timestamp assert version1.last_update_timestamp < version2.last_update_timestamp version3 = install(resp, app_bundle_with_icon) assert version2.application.version_set.count() == 2 assert version2.pk != version3.pk assert version2.creation_timestamp < version3.creation_timestamp assert version2.last_update_timestamp < version3.last_update_timestamp version4 = install(resp, app_bundle) assert version4.application.version_set.count() == 3 assert version3.pk != version4.pk assert version3.creation_timestamp < version4.creation_timestamp assert version3.last_update_timestamp < version4.last_update_timestamp resp = app.get('/applications/manifest/test/versions/') versions = [e.text() for e in resp.pyquery('h3').items()] assert versions.count('42.0') == 2 assert versions.count('43.0') == 1 assert resp.text.count('Deploying application bundle') == 4 def response_content(url, request): if url.path == '/api/export-import/bundle-import/': return {'status_code': 500} return mocked_http(url, request) resp = app.get('/applications/') if action == 'Update': with HTTMock(mocked_http): resp = resp.click(href='manifest/test/') resp = resp.click(action) resp.form['bundle'] = Upload('app.tar', app_bundle, 'application/x-tar') with HTTMock(response_content): with pytest.raises(DeploymentError) as e: resp.form.submit() assert str(e.value) == 'Failed to deploy module wcs (500)' job = AsyncJob.objects.latest('pk') assert job.status == 'failed' assert job.exception == 'Failed to deploy module wcs (500)' form_def = { "id": "test", "text": "Test", "type": "forms", "urls": { "export": "https://wcs.example.invalid/api/export-import/forms/test/", "dependencies": "https://wcs.example.invalid/api/export-import/forms/test/dependencies/", "redirect": "https://wcs.example.invalid/api/export-import/forms/test/redirect/", }, } def response_content(url, request): if url.path == '/api/export-import/forms/': return { 'content': {"data": [form_def]}, 'status_code': 200, } return mocked_http(url, request) resp = app.get('/applications/') if action == 'Update': with HTTMock(mocked_http): resp = resp.click(href='manifest/test/') resp = resp.click(action) resp.form['bundle'] = Upload('app.tar', app_bundle, 'application/x-tar') with HTTMock(response_content): resp.form.submit() application = Application.objects.get(slug='test') elements = application.elements.all().order_by('type') assert len(elements) == 4 assert elements[0].cache == {} assert elements[1].cache == {} assert elements[2].cache == form_def assert elements[3].cache == {} def response_content(url, request): if url.path == '/api/export-import/forms/': return {'status_code': 500} return mocked_http(url, request) resp = app.get('/applications/') if action == 'Update': with HTTMock(mocked_http): resp = resp.click(href='manifest/test/') resp = resp.click(action) resp.form['bundle'] = Upload('app.tar', app_bundle, 'application/x-tar') with HTTMock(response_content): with pytest.raises(ScanError) as e: resp.form.submit() assert str(e.value) == 'Failed to get elements of type forms (500)' job = AsyncJob.objects.latest('pk') assert job.status == 'failed' assert job.exception == 'Failed to get elements of type forms (500)' def test_update_application(app, admin_user, settings, app_bundle): Wcs.objects.create(base_url='https://wcs.example.invalid', slug='foobar', title='Foobar') settings.KNOWN_SERVICES = { 'wcs': { 'foobar': { 'title': 'Foobar', 'url': 'https://wcs.example.invalid/', 'orig': 'example.org', 'secret': 'xxx', } } } login(app) application = Application.objects.create(name='Test', slug='test', editable=True) app.get('/applications/manifest/test/update/', status=404) application.editable = False application.slug = 'wrong' application.save() resp = app.get('/applications/manifest/wrong/update/') resp.form['bundle'] = Upload('app.tar', app_bundle, 'application/x-tar') resp = resp.form.submit() assert resp.context['form'].errors == {'bundle': ['Can not update this application, wrong slug (test).']} @pytest.fixture def app_bundle_roles(): tar_io = io.BytesIO() with tarfile.open(mode='w', fileobj=tar_io) as tar: manifest_json = { 'application': 'Test', 'slug': 'test', 'description': '', 'elements': [ {'type': 'forms', 'slug': 'test', 'name': 'test', 'auto-dependency': False}, {'type': 'roles', 'slug': 'test-role', 'name': 'test', 'auto-dependency': True}, {'type': 'roles', 'slug': 'test-role2', 'name': 'test2', 'auto-dependency': True}, ], } manifest_fd = io.BytesIO(json.dumps(manifest_json, indent=2).encode()) tarinfo = tarfile.TarInfo('manifest.json') tarinfo.size = len(manifest_fd.getvalue()) tar.addfile(tarinfo, fileobj=manifest_fd) role_json = {'name': 'Test', 'slug': 'test-role', 'uuid': '061e5de7023946c79a2f7f1273afc5a2'} role_json_fd = io.BytesIO(json.dumps(role_json, indent=2).encode()) tarinfo = tarfile.TarInfo('roles/test-role') tarinfo.size = len(role_json_fd.getvalue()) tar.addfile(tarinfo, fileobj=role_json_fd) role_json = {'name': 'Test', 'slug': 'test-role2', 'uuid': '061e5de7023946c79a2f7f1273afc5a3'} role_json_fd = io.BytesIO(json.dumps(role_json, indent=2).encode()) tarinfo = tarfile.TarInfo('roles/test-role2') tarinfo.size = len(role_json_fd.getvalue()) tar.addfile(tarinfo, fileobj=role_json_fd) return tar_io.getvalue() def test_deploy_application_roles(app, admin_user, settings, app_bundle_roles): Application.objects.all().delete() Authentic.objects.create(base_url='https://idp.example.invalid', slug='idp', title='Foobar') Wcs.objects.create(base_url='https://wcs.example.invalid', slug='foobar', title='Foobar') settings.KNOWN_SERVICES = { 'authentic': { 'idp': { 'title': 'Foobar', 'url': 'https://idp.example.invalid/', 'orig': 'example.org', 'secret': 'xxx', } }, 'wcs': { 'foobar': { 'title': 'Foobar', 'url': 'https://wcs.example.invalid/', 'orig': 'example.org', 'secret': 'xxx', } }, } login(app) resp = app.get('/applications/') for i in range(2): resp = resp.click('Install') resp.form['bundle'] = Upload('app.tar', app_bundle_roles, 'application/x-tar') with HTTMock(httmock.remember_called(mocked_http)): resp = resp.form.submit().follow() # roles assert mocked_http.call['requests'][0].url.startswith( 'https://idp.example.invalid/api/roles/?update' ) assert mocked_http.call['requests'][1].url.startswith( 'https://idp.example.invalid/api/provision/' ) assert mocked_http.call['requests'][2].url.startswith( 'https://idp.example.invalid/api/roles/?update' ) assert mocked_http.call['requests'][3].url.startswith( 'https://idp.example.invalid/api/provision/' ) # then form assert 'wcs.example.invalid' in mocked_http.call['requests'][4].url assert mocked_http.call['requests'][5].url.startswith( 'https://wcs.example.invalid/api/export-import/?' ) # then element refresh available_objects = [o for o in WCS_AVAILABLE_OBJECTS['data'] if o['id'] in ['forms', 'roles']] assert mocked_http.call['count'] == 5 + 1 + len(available_objects) for i, object_type in enumerate(available_objects): assert mocked_http.call['requests'][6 + i].url.startswith( 'https://wcs.example.invalid/api/export-import/%s/?' % object_type['id'] ) def response_content(url, request): if url.path == '/api/roles/': return {'status_code': 500} return mocked_http(url, request) resp = app.get('/applications/install/') resp.form['bundle'] = Upload('app.tar', app_bundle_roles, 'application/x-tar') with HTTMock(response_content): with pytest.raises(DeploymentError) as e: resp.form.submit() assert str(e.value) == 'Failed to create role test-role (500)' job = AsyncJob.objects.latest('pk') assert job.status == 'failed' assert job.exception == 'Failed to create role test-role (500)' def response_content(url, request): if url.path == '/api/provision/': return {'status_code': 500} return mocked_http(url, request) resp = app.get('/applications/install/') resp.form['bundle'] = Upload('app.tar', app_bundle_roles, 'application/x-tar') with HTTMock(response_content): with pytest.raises(DeploymentError) as e: resp.form.submit() assert str(e.value) == 'Failed to provision role test-role (500)' job = AsyncJob.objects.latest('pk') assert job.status == 'failed' assert job.exception == 'Failed to provision role test-role (500)' def test_job_status_page(app, admin_user, settings): Wcs.objects.create(base_url='https://wcs.example.invalid', slug='foobar', title='Foobar') application = Application.objects.create(name='Test', slug='test') settings.KNOWN_SERVICES = { 'wcs': { 'foobar': { 'title': 'Foobar', 'url': 'https://wcs.example.invalid/', 'orig': 'example.org', 'secret': 'xxx', } } } login(app) for action in ['scandeps', 'create_bundle']: job = AsyncJob.objects.create( label=action, application=application, action=action, status='running', ) resp = app.get('/applications/manifest/test/job/%s/' % job.pk) assert 'Please wait…' in resp assert 'window.location.reload' in resp assert 'window.location = "/applications/manifest/test/"' not in resp job.status = 'failed' job.exception = 'foo bar exception' job.save() resp = app.get('/applications/manifest/test/job/%s/' % job.pk) assert 'Error running the job.' in resp assert 'Please wait…' not in resp assert 'window.location.reload' not in resp assert 'window.location = "/applications/manifest/test/"' not in resp job = AsyncJob.objects.create( label='deploy', application=application, action='deploy', progression_urls={'wcs': {'Foobar': 'https://wcs.example.invalid/api/jobs/job-uuid/'}}, status='completed', # completed, with async job on wcs part ) def response_content(url, request): if url.path.startswith('/api/jobs/'): return { 'content': json.dumps({'err': 0, 'data': {'status': 'running', 'completion_status': '42%'}}), 'status_code': 200, } return mocked_http(url, request) with HTTMock(response_content): resp = app.get('/applications/manifest/test/job/%s/' % job.pk) assert 'Please wait…' in resp assert 'Running: 42%' in resp assert 'window.location.reload' in resp assert 'window.location = "/applications/manifest/test/"' not in resp def response_content(url, request): if url.path.startswith('/api/jobs/'): return { 'content': json.dumps( {'err': 0, 'data': {'status': 'completed', 'completion_status': '100%'}} ), 'status_code': 200, } return mocked_http(url, request) with HTTMock(response_content): resp = app.get('/applications/manifest/test/job/%s/' % job.pk) assert 'Please wait…' in resp assert 'Completed: 100%' in resp assert 'window.location.reload' not in resp assert 'window.location = "/applications/manifest/test/"' in resp job.status = 'failed' job.exception = 'foo bar exception' job.save() resp = app.get('/applications/manifest/test/job/%s/' % job.pk) assert 'Error running the job.' in resp assert 'Please wait…' not in resp assert 'window.location.reload' not in resp assert 'window.location = "/applications/manifest/test/"' not in resp job.status = 'running' job.exception = '' job.save() def response_content(url, request): if url.path.startswith('/api/jobs/'): return { 'content': json.dumps({'err': 0, 'data': {'status': 'failed', 'completion_status': '42%'}}), 'status_code': 200, } return mocked_http(url, request) with HTTMock(response_content): resp = app.get('/applications/manifest/test/job/%s/' % job.pk) assert 'Error running the job.' in resp assert 'Please wait…' not in resp assert 'window.location.reload' not in resp assert 'window.location = "/applications/manifest/test/"' not in resp job.refresh_from_db() assert job.status == 'failed' assert job.exception == 'Failed to deploy module wcs'