diff --git a/lib/model/query/forms.js b/lib/model/query/forms.js index 2e267f26c..5bc524c44 100644 --- a/lib/model/query/forms.js +++ b/lib/model/query/forms.js @@ -292,7 +292,20 @@ createVersion.audit = (newDef, partial, form, publish) => (log) => ((publish === ? log('form.update.publish', form, { oldDefId: form.currentDefId, newDefId: newDef.id }) : log('form.update.draft.set', form, { oldDraftDefId: form.draftDefId, newDraftDefId: newDef.id })); createVersion.audit.withResult = true; +createVersion.audit.logEvenIfAnonymous = true; + +// This is used in the rare case where we want to change and update a FormDef in place without +// creating a new def. This is basically a wrapper around _updateDef that also logs an event. +const replaceDef = (partial, form) => async ({ Forms }) => { + const { version, hash, sha, sha256 } = partial.def; + await Forms._updateDef(form.def, { xml: partial.xml, version, hash, sha, sha256 }); + // all this does is changed updatedAt + await Forms._update(form, { updatedAt: (new Date()).toISOString() }); +}; +replaceDef.audit = (_, form) => (log) => + log('form.update.draft.replace', form, { upgrade: 'Updated entities-version in form to 2024.1' }); +replaceDef.audit.logEvenIfAnonymous = true; //////////////////////////////////////////////////////////////////////////////// // PUBLISHING MANAGEMENT @@ -807,7 +820,7 @@ module.exports = { _insertFormFields, _createNew, createNew, _createNewDef, createVersion, publish, clearDraft, - _update, update, _updateDef, del, restore, purge, + _update, update, _updateDef, replaceDef, del, restore, purge, clearUnneededDrafts, setManagedKey, getByAuthForOpenRosa, diff --git a/lib/worker/form.js b/lib/worker/form.js index a44770607..277c408c7 100644 --- a/lib/worker/form.js +++ b/lib/worker/form.js @@ -57,8 +57,9 @@ const updateEntitiesVersion = async ({ Forms }, event) => { const publishedVersion = await Forms.getByProjectAndXmlFormId(projectId, xmlFormId, true, Form.PublishedVersion).then(o => o.orNull()); if (publishedVersion && publishedVersion.currentDefId != null) { const partial = await _upgradeEntityVersion(publishedVersion); - if (partial != null) + if (partial != null) { await Forms.createVersion(partial, publishedVersion, true, true); + } } const draftVersion = await Forms.getByProjectAndXmlFormId(projectId, xmlFormId, true, Form.DraftVersion).then(o => o.orNull()); @@ -66,7 +67,7 @@ const updateEntitiesVersion = async ({ Forms }, event) => { const partial = await _upgradeEntityVersion(draftVersion); // update xml and version in place if (partial != null) - await Forms._updateDef(draftVersion.def, { xml: partial.xml, version: partial.def.version }); + await Forms.replaceDef(partial, draftVersion); } }; diff --git a/test/integration/other/form-entities-version.js b/test/integration/other/form-entities-version.js index 2fafa4a06..65b9fd82e 100644 --- a/test/integration/other/form-entities-version.js +++ b/test/integration/other/form-entities-version.js @@ -1,4 +1,6 @@ const { readFileSync } = require('fs'); +const should = require('should'); +const config = require('config'); const appRoot = require('app-root-path'); const { testService } = require('../setup'); const testData = require('../../data/xml'); @@ -309,9 +311,234 @@ describe('Update / migrate entities-version within form', () => { ]); }); })); + + it('should update the formList once the form changes', testService(async (service, container) => { + const { Forms, Audits } = container; + const asAlice = await service.login('alice'); + const domain = config.get('default.env.domain'); + + // Publish a form + await asAlice.post('/v1/projects/1/forms?publish=true') + .send(testData.forms.updateEntity) + .set('Content-Type', 'application/xml') + .expect(200); + + // Create a draft as well + await asAlice.post('/v1/projects/1/forms/updateEntity/draft') + .expect(200); + + const token = await asAlice.get('/v1/projects/1/forms/updateEntity/draft') + .expect(200) + .then(({ body }) => body.draftToken); + + await asAlice.get('/v1/projects/1/formList') + .set('X-OpenRosa-Version', '1.0') + .expect(200) + .then(({ text }) => text.should.containEql(` + updateEntity + updateEntity + 1.0 + md5:e4902c380ef428aa3d35e4ed17ea6c04 + ${domain}/v1/projects/1/forms/updateEntity.xml + `)); + + await asAlice.get(`/v1/test/${token}/projects/1/forms/updateEntity/draft/formList`) + .set('X-OpenRosa-Version', '1.0') + .expect(200) + .then(({ text }) => text.should.containEql(` + updateEntity + updateEntity + 1.0 + md5:e4902c380ef428aa3d35e4ed17ea6c04 + ${domain}/v1/test/${token}/projects/1/forms/updateEntity/draft.xml + `)); + + const { acteeId } = await Forms.getByProjectAndXmlFormId(1, 'updateEntity').then(o => o.get()); + await Audits.log(null, 'upgrade.process.form.entities_version', { acteeId }); + + // Run form upgrade + await exhaust(container); + + await asAlice.get('/v1/projects/1/formList') + .set('X-OpenRosa-Version', '1.0') + .expect(200) + .then(({ text }) => text.should.containEql(` + updateEntity + updateEntity + 1.0[upgrade] + md5:77292dd9e1ad532bb5a4f7128e0a9596 + ${domain}/v1/projects/1/forms/updateEntity.xml + `)); + + await asAlice.get(`/v1/test/${token}/projects/1/forms/updateEntity/draft/formList`) + .set('X-OpenRosa-Version', '1.0') + .expect(200) + .then(({ text }) => text.should.containEql(` + updateEntity + updateEntity + 1.0[upgrade] + md5:77292dd9e1ad532bb5a4f7128e0a9596 + ${domain}/v1/test/${token}/projects/1/forms/updateEntity/draft.xml + `)); + })); + + it('should update the updatedAt timestamps on the form when updating a draft form', testService(async (service, container) => { + const { Forms, Audits } = container; + const asAlice = await service.login('alice'); + + // Upload a form and publish it + await asAlice.post('/v1/projects/1/forms') + .send(testData.forms.updateEntity) + .set('Content-Type', 'application/xml') + .expect(200); + + // check updatedAt on the draft form + await asAlice.get('/v1/projects/1/forms/updateEntity') + .expect(200) + .then(({ body }) => { + should(body.updatedAt).be.null(); + }); + + const { acteeId } = await Forms.getByProjectAndXmlFormId(1, 'updateEntity').then(o => o.get()); + await Audits.log(null, 'upgrade.process.form.entities_version', { acteeId }); + + // Run form upgrade + await exhaust(container); + + await asAlice.get('/v1/projects/1/forms/updateEntity') + .expect(200) + .then(({ body }) => { + body.updatedAt.should.be.a.recentIsoDate(); + }); + })); + + it('should update the updatedAt timestamps on the form when updating a published form', testService(async (service, container) => { + const { Forms, Audits } = container; + const asAlice = await service.login('alice'); + + // Upload a form and publish it + await asAlice.post('/v1/projects/1/forms?publish=true') + .send(testData.forms.updateEntity) + .set('Content-Type', 'application/xml') + .expect(200); + + // check updatedAt on the draft form + await asAlice.get('/v1/projects/1/forms/updateEntity') + .expect(200) + .then(({ body }) => { + should(body.updatedAt).be.null(); + }); + + const { acteeId } = await Forms.getByProjectAndXmlFormId(1, 'updateEntity').then(o => o.get()); + await Audits.log(null, 'upgrade.process.form.entities_version', { acteeId }); + + // Run form upgrade + await exhaust(container); + + await asAlice.get('/v1/projects/1/forms/updateEntity') + .expect(200) + .then(({ body }) => { + body.updatedAt.should.be.a.recentIsoDate(); + }); + })); }); describe('audit logging and errors', () => { + it('should log events about the upgrade for a published form', testService(async (service, container) => { + const { Forms, Audits } = container; + const asAlice = await service.login('alice'); + + // Upload a form and publish it + await asAlice.post('/v1/projects/1/forms?publish=true') + .send(testData.forms.updateEntity) + .set('Content-Type', 'application/xml') + .expect(200); + + const { acteeId } = await Forms.getByProjectAndXmlFormId(1, 'updateEntity').then(o => o.get()); + await Audits.log(null, 'upgrade.process.form.entities_version', { acteeId }); + + // Run form upgrade + await exhaust(container); + + await asAlice.get('/v1/audits') + .expect(200) + .then(({ body }) => { + const actions = body.map(a => a.action); + actions.should.eql([ + 'form.update.publish', + 'upgrade.process.form.entities_version', + 'form.update.publish', + 'dataset.create', + 'form.create', + 'user.session.create' + ]); + }); + })); + + it('should log events about the upgrade for a draft form', testService(async (service, container) => { + const { Forms, Audits } = container; + const asAlice = await service.login('alice'); + + // Upload a form and publish it + await asAlice.post('/v1/projects/1/forms') + .send(testData.forms.updateEntity) + .set('Content-Type', 'application/xml') + .expect(200); + + const { acteeId } = await Forms.getByProjectAndXmlFormId(1, 'updateEntity').then(o => o.get()); + await Audits.log(null, 'upgrade.process.form.entities_version', { acteeId }); + + // Run form upgrade + await exhaust(container); + + await asAlice.get('/v1/audits') + .expect(200) + .then(({ body }) => { + const actions = body.map(a => a.action); + actions.should.eql([ + 'form.update.draft.replace', + 'upgrade.process.form.entities_version', + 'form.create', + 'user.session.create' + ]); + }); + })); + + it('should log events about the upgrade for a published and draft form', testService(async (service, container) => { + const { Forms, Audits } = container; + const asAlice = await service.login('alice'); + + // Upload a form and publish it + await asAlice.post('/v1/projects/1/forms?publish=true') + .send(testData.forms.updateEntity) + .set('Content-Type', 'application/xml') + .expect(200); + + await asAlice.post('/v1/projects/1/forms/updateEntity/draft'); + + const { acteeId } = await Forms.getByProjectAndXmlFormId(1, 'updateEntity').then(o => o.get()); + await Audits.log(null, 'upgrade.process.form.entities_version', { acteeId }); + + // Run form upgrade + await exhaust(container); + + await asAlice.get('/v1/audits') + .expect(200) + .then(({ body }) => { + const actions = body.map(a => a.action); + actions.should.eql([ + 'form.update.draft.replace', + 'form.update.publish', + 'upgrade.process.form.entities_version', + 'form.update.draft.set', + 'form.update.publish', + 'dataset.create', + 'form.create', + 'user.session.create' + ]); + }); + })); + it('should update the audit log event for a successful upgrade', testService(async (service, container) => { const { Forms, Audits } = container; const asAlice = await service.login('alice');