From 287908cafa5af9ee197632ba7813ef9d1540b149 Mon Sep 17 00:00:00 2001 From: Jez Barnsley Date: Fri, 6 Mar 2026 10:54:13 +0000 Subject: [PATCH 01/12] Handles deletion and rename of secrets --- docker-compose.integration-test.yml | 2 + package.json | 2 +- .../forms/repositories/secrets-repository.js | 93 ++++++ .../repositories/secrets-repository.test.js | 75 +++++ src/api/forms/service/secrets.js | 95 +++++- src/api/forms/service/secrets.test.js | 104 ++++++- src/api/types.js | 2 + src/messaging/mappers/form-events.js | 59 +++- src/messaging/publish.js | 43 +++ src/messaging/publish.test.js | 47 +++ src/routes/secrets.js | 63 +++- src/routes/secrets.test.js | 38 ++- ...ms-manager-ci-mock.postman_collection.json | 289 +++++++++++++++++- 13 files changed, 903 insertions(+), 9 deletions(-) diff --git a/docker-compose.integration-test.yml b/docker-compose.integration-test.yml index 4231feaa..393abd84 100644 --- a/docker-compose.integration-test.yml +++ b/docker-compose.integration-test.yml @@ -105,6 +105,8 @@ services: SNS_TOPIC_ARN: arn:aws:sns:eu-west-2:000000000000:forms_manager_events AWS_ACCESS_KEY_ID: test AWS_SECRET_ACCESS_KEY: test + # PUBLIC_KEY is a test value - do not use for any deployments + PUBLIC_KEY_FOR_SECRETS: "LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQ0lqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FnOEFNSUlDQ2dLQ0FnRUE0WU53YUJjenlBdWpJZ2k2aTBmawpINUVYeGxtTmNBd1AzUkpRaDB2ckxjeDIxU2RCSkFOUU5JWmdTcWhST3ViUlozWTJWQ2Zsbkd3cFVSTTNrY0tFCkNHUlk2Nk5leVUwL3QzS2hVOUY1cG1heVBSVUFiWkRjalFCL0xYbVNGUUdGelZDQlp0ZjBIeHJiSnBUNDFiSGIKR05udFpDdm94WjZQOUJxdU4wa2c0Y1JXcTdyWUozVE1xWXArc1RzQU01UFZwMVh6WklIRFJ2UVJ2bktnMDVUUQovRFA1cXcvYkdwelVsQ01WVU9MTHhxL2xTY040RnhDRTRUUi9kSWNNM25wTU1PUUVjaEhLcEE2SWs3UmhmQml0CjZBSEpINWN2c0dWUTFHUmlvUXBIWXBWclZLRFV5ZmIrS3VzbjlWUTVTQ3JGZDJ3WWRaRzBkdVNOZFp3VklJQm8KVElWZjlrSUMrZjl5YTVLMmcvalJhMXUyVGdNc3UvZnM4UWVRZHk2disxNkFtWFA1cXNXYUtFK00zRkJoRitrZAozbS91YjNJVFRRVmplYWY1M2ZsTVlMZlZmN2NVR2FQN0VIajlUY0dGYVlGTzhuWGNWbS9lT2lkMXBGL3p5NCtzCnpFZG5oOFNRYm5ncElwdFJiMk51Wmw5RWZNR01CTFlyQ3FiWnMyYU1KV0N5M1FGWFZDeTYzUzZhRDkrbzQ1N0cKUDNGVnNZY2d2eHAzTGVTQ0phUHMzL0FlSjh1MUFSeHZwMG02ZjdPLy9yMFFQYzJnd2RyZjRVMUkwdlduclh2eApCc2RMWU5NMXVYVjhMKzExYloxeGpVaFU4QldTd3dnTTdQMGxqVTJ4UE9WbG55MlFGeXh0ZTgzK0E5SXNBcmFxCmthNlhEdDhYZkNZYm84dEpnem9mbEs4Q0F3RUFBUT09Ci0tLS0tRU5EIFBVQkxJQyBLRVktLS0tLQ==" depends_on: mongo_test: condition: service_healthy diff --git a/package.json b/package.json index eb3251f7..c713b00c 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "test:integration:wait": "echo 'Waiting for app...' && sleep 15 && until curl --fail http://localhost:3001/health; do echo 'Still waiting...'; sleep 5; done; echo 'App ready!'", "test:integration:run": "docker compose -f docker-compose.integration-test.yml run --rm --entrypoint=\"\" newman sh -c \"npm install -g newman-reporter-htmlextra && newman run /etc/newman/forms-manager-ci-mock.postman_collection.json -e /etc/newman/forms-manager-ci-mock.postman_environment.json -r cli,json,htmlextra --reporter-json-export /etc/newman/reports/newman-summary.json --reporter-htmlextra-export /etc/newman/reports/newman-report.html --reporter-htmlextra-showConsoleLogs\"", "test:integration:stop": "docker compose -f docker-compose.integration-test.yml down -v --remove-orphans", - "test:integration": "npm run test:integration:setup && npm run test:integration:start && npm run test:integration:wait && npm run test:integration:run; EXIT_CODE=$? && exit $EXIT_CODE", + "test:integration": "npm run test:integration:setup && npm run test:integration:start && npm run test:integration:wait && npm run test:integration:run && npm run test:integration:stop; EXIT_CODE=$? && exit $EXIT_CODE", "server:watch": "NODE_ENV=development tsx watch --clear-screen=false --enable-source-maps --exclude \"**/*.test.*\"", "server:watch:dev": "npm run server:watch -- ./src/index.js", "server:watch:debug": "npm run server:watch -- --inspect-wait ./src/index.js", diff --git a/src/api/forms/repositories/secrets-repository.js b/src/api/forms/repositories/secrets-repository.js index d8e11c6d..b113d3f5 100644 --- a/src/api/forms/repositories/secrets-repository.js +++ b/src/api/forms/repositories/secrets-repository.js @@ -64,6 +64,7 @@ export async function exists(formId, secretName, session) { return { exists: !!document, createdAt: document?.createdAt, + renamedAt: document?.renamedAt, updatedAt: document?.updatedAt } } catch (err) { @@ -130,6 +131,98 @@ export async function save(formId, secretName, secretValue, session) { } } +/** + * Deletes a form secret from the database + * @param {string} formId - id of the form + * @param {string} secretName - name of the secret + * @param {ClientSession} session - mongo transaction session + */ +export async function deleteSecret(formId, secretName, session) { + logger.info(`Deleting secret '${secretName}' for form ID ${formId}`) + + const coll = /** @satisfies {Collection} */ ( + db.collection(SECRETS_COLLECTION_NAME) + ) + + try { + const result = await coll.deleteOne( + { + formId, + secretName + }, + { session } + ) + + logger.info( + `Secret deleted with name '${secretName}' for form ID ${formId}` + ) + + return result + } catch (err) { + const message = `Secret with name '${secretName}' for form ID ${formId} failed to delete` + + if (err instanceof MongoServerError) { + logger.error( + err, + `[mongoError] ${message} - MongoDB error code: ${err.code} - ${err.message}` + ) + } else { + logger.error(err, `[deleteError] ${message} - ${getErrorMessage(err)}`) + } + throw err + } +} + +/** + * Changes the name of a form secret in the database + * @param {string} formId - id of the form + * @param {string} secretNameFrom - name of the secret to be renamed + * @param {string} secretNameTo - the new name of the secret + * @param {ClientSession} session - mongo transaction session + */ +export async function rename(formId, secretNameFrom, secretNameTo, session) { + logger.info( + `Renaming secret '${secretNameFrom}' to '${secretNameTo}' for form ID ${formId}` + ) + + const coll = /** @satisfies {Collection} */ ( + db.collection(SECRETS_COLLECTION_NAME) + ) + + const now = new Date() + + try { + const result = await coll.findOneAndUpdate( + { + formId, + secretNameFrom + }, + { + $set: { secretName: secretNameTo, renamedAt: now } + }, + { session } + ) + + logger.info( + `Secret renamed from '${secretNameFrom}' to '${secretNameTo}' for form ID ${formId}` + ) + + return result + } catch (err) { + const message = `Secret with name '${secretNameFrom}' for form ID ${formId} failed to rename` + + if (err instanceof MongoServerError) { + logger.error( + err, + `[mongoError] ${message} - MongoDB error code: ${err.code} - ${err.message}` + ) + } else { + logger.error(err, `[renameError] ${message} - ${getErrorMessage(err)}`) + } + throw err + } +} + /** * @import { FormSecret } from '@defra/forms-model' * @import { ClientSession, Collection } from 'mongodb' diff --git a/src/api/forms/repositories/secrets-repository.test.js b/src/api/forms/repositories/secrets-repository.test.js index 33d411b3..c9993802 100644 --- a/src/api/forms/repositories/secrets-repository.test.js +++ b/src/api/forms/repositories/secrets-repository.test.js @@ -3,8 +3,10 @@ import { MongoServerError } from 'mongodb' import { buildMockCollection } from '~/src/api/forms/__stubs__/mongo.js' import { + deleteSecret, exists, get, + rename, save } from '~/src/api/forms/repositories/secrets-repository.js' import author from '~/src/api/forms/service/__stubs__/author.js' @@ -186,6 +188,79 @@ describe('secrets-repository', () => { }) }) + describe('delete', () => { + it('should delete if the secret exists', async () => { + mockCollection.deleteOne.mockResolvedValue({ deletedCount: 1 }) + const result = await deleteSecret(formId, 'my-secret', mockSession) + expect(result).toEqual({ deletedCount: 1 }) + }) + + it('should throw if db error', async () => { + mockCollection.deleteOne.mockImplementationOnce(() => { + throw new MongoServerError({ message: 'DB error deleting' }) + }) + await expect(() => + deleteSecret(formId, 'my-secret', mockSession) + ).rejects.toThrow('DB error deleting') + expect(mockLoggerError).toHaveBeenCalledWith( + expect.anything(), + "[mongoError] Secret with name 'my-secret' for form ID fe339c6a-1f6e-4ab8-88c6-73fa1528dc90 failed to delete - MongoDB error code: undefined - DB error deleting" + ) + }) + + it('should throw if other error', async () => { + mockCollection.deleteOne.mockImplementationOnce(() => { + throw new Error('DB error deleting') + }) + await expect(() => + deleteSecret(formId, 'my-secret', mockSession) + ).rejects.toThrow('DB error deleting') + expect(mockLoggerError).toHaveBeenCalledWith( + expect.anything(), + "[deleteError] Secret with name 'my-secret' for form ID fe339c6a-1f6e-4ab8-88c6-73fa1528dc90 failed to delete - DB error deleting" + ) + }) + }) + + describe('rename', () => { + it('should rename if the secret exists', async () => { + mockCollection.findOneAndUpdate.mockResolvedValue(secret) + const result = await rename( + formId, + 'my-secret before', + 'my secret after', + mockSession + ) + expect(result).toEqual(secret) + }) + + it('should throw if db error', async () => { + mockCollection.findOneAndUpdate.mockImplementationOnce(() => { + throw new MongoServerError({ message: 'DB error rename' }) + }) + await expect(() => + rename(formId, 'my-secret before', 'my secret after', mockSession) + ).rejects.toThrow('DB error rename') + expect(mockLoggerError).toHaveBeenCalledWith( + expect.anything(), + "[mongoError] Secret with name 'my-secret before' for form ID fe339c6a-1f6e-4ab8-88c6-73fa1528dc90 failed to rename - MongoDB error code: undefined - DB error rename" + ) + }) + + it('should throw if other error', async () => { + mockCollection.findOneAndUpdate.mockImplementationOnce(() => { + throw new Error('DB error rename') + }) + await expect(() => + rename(formId, 'my-secret before', 'my secret after', mockSession) + ).rejects.toThrow('DB error rename') + expect(mockLoggerError).toHaveBeenCalledWith( + expect.anything(), + "[renameError] Secret with name 'my-secret before' for form ID fe339c6a-1f6e-4ab8-88c6-73fa1528dc90 failed to rename - DB error rename" + ) + }) + }) + describe('save', () => { it('should return true if the secret exists', async () => { mockCollection.findOneAndUpdate.mockResolvedValue(secret) diff --git a/src/api/forms/service/secrets.js b/src/api/forms/service/secrets.js index e35c899b..ffdb3bcf 100644 --- a/src/api/forms/service/secrets.js +++ b/src/api/forms/service/secrets.js @@ -4,7 +4,11 @@ import * as formMetadata from '~/src/api/forms/repositories/form-metadata-reposi import * as secretsRepository from '~/src/api/forms/repositories/secrets-repository.js' import { encryptSecret } from '~/src/api/forms/service/helpers/crypto.js' import { logger } from '~/src/api/forms/service/shared.js' -import { publishSavedFormSecretEvent } from '~/src/messaging/publish.js' +import { + publishDeletedFormSecretEvent, + publishRenamedFormSecretEvent, + publishSavedFormSecretEvent +} from '~/src/messaging/publish.js' import { client } from '~/src/mongo.js' /** @@ -61,6 +65,95 @@ export async function saveFormSecret(formId, secretName, secretValue, author) { } } +/** + * Deletes a secret value. + * @param {string} formId - id of the form + * @param {string} secretName - name of the secret + * @param {FormMetadataAuthor} author + */ +export async function deleteFormSecret(formId, secretName, author) { + const session = client.startSession() + + try { + await session.withTransaction(async () => { + const metadata = await formMetadata.get(formId, session) + + await secretsRepository.deleteSecret(formId, secretName, session) + + await publishDeletedFormSecretEvent(metadata, secretName, author) + }) + + logger.info(`Deleted secret '${secretName}' to form ID ${formId}`) + } catch (err) { + logger.error( + err, + `[assignSections] Failed to delete secret '${secretName}' to form ID ${formId} - ${getErrorMessage(err)}` + ) + + throw err + } finally { + await session.endSession() + } +} + +/** + * Changes the name of a secret. + * @param {string} formId - id of the form + * @param {string} secretNameFrom - name of the secret to be changed + * @param {string} secretNameTo - name of the secret after the change + * @param {FormMetadataAuthor} author + */ +export async function renameFormSecret( + formId, + secretNameFrom, + secretNameTo, + author +) { + const session = client.startSession() + + try { + await session.withTransaction(async () => { + const metadata = await formMetadata.get(formId, session) + + logger.info( + `1. About to rename secret '${secretNameFrom}' to '${secretNameTo}' on form ID ${formId}` + ) + await secretsRepository.rename( + formId, + secretNameFrom, + secretNameTo, + session + ) + logger.info( + `2. Renamed secret '${secretNameFrom}' to '${secretNameTo}' on form ID ${formId}` + ) + + await publishRenamedFormSecretEvent( + metadata, + secretNameFrom, + secretNameTo, + author + ) + logger.info( + `3. Sent audit secret '${secretNameFrom}' to '${secretNameTo}' on form ID ${formId}` + ) + }) + + logger.info( + `Renamed secret '${secretNameFrom}' to '${secretNameTo}' on form ID ${formId}` + ) + } catch (err) { + logger.error( + err, + `[assignSections] Failed to rename secret '${secretNameFrom}' '${secretNameTo}' on form ID ${formId} - ${getErrorMessage(err)}` + ) + + throw err + } finally { + await session.endSession() + } +} + /** * @import { FormMetadataAuthor } from '@defra/forms-model' */ diff --git a/src/api/forms/service/secrets.test.js b/src/api/forms/service/secrets.test.js index 0f947377..9cb9b071 100644 --- a/src/api/forms/service/secrets.test.js +++ b/src/api/forms/service/secrets.test.js @@ -3,14 +3,18 @@ import { pino } from 'pino' import * as formMetadata from '~/src/api/forms/repositories/form-metadata-repository.js' import { + deleteSecret, exists, get, + rename, save } from '~/src/api/forms/repositories/secrets-repository.js' import { formMetadataDocument } from '~/src/api/forms/service/__stubs__/service.js' import { + deleteFormSecret, existsFormSecret, getFormSecret, + renameFormSecret, saveFormSecret } from '~/src/api/forms/service/secrets.js' import { getAuthor } from '~/src/helpers/get-author.js' @@ -60,7 +64,12 @@ describe('secrets', () => { const now = new Date() jest .mocked(exists) - .mockResolvedValueOnce({ exists: true, createdAt: now, updatedAt: now }) + .mockResolvedValueOnce({ + exists: true, + createdAt: now, + updatedAt: now, + renamedAt: undefined + }) const secretName = 'my-secret-name' const res = await existsFormSecret(formId, secretName) @@ -73,6 +82,99 @@ describe('secrets', () => { }) }) + describe('deleteFormSecret', () => { + it('should delete form secret and publish audit event', async () => { + jest.mocked(formMetadata.get).mockResolvedValue(formMetadataDocument) + const publishEventSpy = jest.spyOn(publishBase, 'publishEvent') + + const secretName = 'my-secret-name' + await deleteFormSecret(formId, secretName, defaultAuthor) + + // Verify repository was called with correct arguments + const [formIdCalled, secretNameCalled] = + jest.mocked(deleteSecret).mock.calls[0] + expect(formId).toBe(formIdCalled) + expect(secretName).toBe(secretNameCalled) + + // Verify audit event was published + const [auditMessage] = publishEventSpy.mock.calls[0] + expect(auditMessage).toMatchObject({ + type: AuditEventMessageType.FORM_SECRET_DELETED + }) + expect(auditMessage.data).toMatchObject({ + formId, + secretName: 'my-secret-name' + }) + }) + + it('should throw when error', async () => { + jest.mocked(formMetadata.get).mockResolvedValue(formMetadataDocument) + jest.mocked(deleteSecret).mockImplementationOnce(() => { + throw new Error('Deleting error') + }) + + const secretName = 'my-secret-name' + await expect(() => + deleteFormSecret(formId, secretName, defaultAuthor) + ).rejects.toThrow('Deleting error') + }) + }) + + describe('renameFormSecret', () => { + it('should rename form secret and publish audit event', async () => { + jest.mocked(formMetadata.get).mockResolvedValue(formMetadataDocument) + const publishEventSpy = jest.spyOn(publishBase, 'publishEvent') + + const secretNameBefore = 'my-secret-name-before' + const secretNameAfter = 'my-secret-name-after' + await renameFormSecret( + formId, + secretNameBefore, + secretNameAfter, + defaultAuthor + ) + + // Verify repository was called with correct arguments + const [formIdCalled, secretNameBeforeCalled, secretNameAfterCalled] = + jest.mocked(rename).mock.calls[0] + expect(formId).toBe(formIdCalled) + expect(secretNameBefore).toBe(secretNameBeforeCalled) + expect(secretNameAfter).toBe(secretNameAfterCalled) + + // Verify audit event was published + const [auditMessage] = publishEventSpy.mock.calls[0] + expect(auditMessage).toMatchObject({ + type: AuditEventMessageType.FORM_SECRET_RENAMED + }) + expect(auditMessage.data).toMatchObject({ + formId, + secretName: 'my-secret-name-before', + payload: { + secretNameFrom: secretNameBefore, + secretNameTo: secretNameAfter + } + }) + }) + + it('should throw when error', async () => { + jest.mocked(formMetadata.get).mockResolvedValue(formMetadataDocument) + jest.mocked(rename).mockImplementationOnce(() => { + throw new Error('Renaming error') + }) + + const secretNameBefore = 'my-secret-name-before' + const secretNameAfter = 'my-secret-name-after' + await expect(() => + renameFormSecret( + formId, + secretNameBefore, + secretNameAfter, + defaultAuthor + ) + ).rejects.toThrow('Renaming error') + }) + }) + describe('saveFormSecret', () => { it('should save form secret and publish audit event', async () => { jest.mocked(formMetadata.get).mockResolvedValue(formMetadataDocument) diff --git a/src/api/types.js b/src/api/types.js index 69b780a9..51233c82 100644 --- a/src/api/types.js +++ b/src/api/types.js @@ -25,6 +25,8 @@ * @typedef {Request<{ Server: { db: Db }, Params: {id: string, versionNumber: string} }>} RequestFormVersionById * @typedef {Request<{ Server: { db: Db }, Params: FormByIdInput, Payload: SectionAssignmentPayload }>} RequestSectionAssignment * @typedef {Request<{ Server: { db: Db }, Params: {id: string, name: string} }>} RequestGetFormSecret + * @typedef {Request<{ Server: { db: Db }, Params: {id: string, name: string} }>} RequestDeleteFormSecret + * @typedef {Request<{ Server: { db: Db }, Params: {id: string, nameBefore: string, nameAfter: string } }>} RequestRenameFormSecret * @typedef {Request<{ Server: { db: Db }, Params: {id: string, name: string}, Payload: { secretValue: string } }>} RequestSaveFormSecret */ diff --git a/src/messaging/mappers/form-events.js b/src/messaging/mappers/form-events.js index c0f44fd2..374639e5 100644 --- a/src/messaging/mappers/form-events.js +++ b/src/messaging/mappers/form-events.js @@ -484,7 +484,7 @@ export function formUpdatedMapper(metadata, requestType, { payload, s3Meta }) { * @param {AuditUser} createdBy * @returns {FormSecretSavedMessage} */ -export function savedFormSecretMapper(metadata, secretName, createdBy) { +export function createFormSecretBase(metadata, secretName, createdBy) { const baseData = createFormMessageDataBase(metadata) const now = new Date() return { @@ -504,6 +504,61 @@ export function savedFormSecretMapper(metadata, secretName, createdBy) { } /** - * @import { FormDefinitionS3Meta, FormUpdatedMessage, FormDefinitionRequestType, FormDraftDeletedMessage, AuditUser, FormTitleUpdatedMessageData, FormOrganisationUpdatedMessage, FormOrganisationUpdatedMessageData, FormMetadata, FormCreatedMessage, FormCreatedMessageData, FormTitleUpdatedMessage, FormTeamNameUpdatedMessage, FormTeamNameUpdatedMessageData, FormTeamEmailUpdatedMessage, FormTeamEmailUpdatedMessageData, FormPrivacyNoticeUpdatedMessage, FormPrivacyNoticeUpdatedMessageData, FormTermsAndConditionsAgreedMessage, FormTermsAndConditionsAgreedMessageData, FormSubmissionGuidanceUpdatedMessage, FormSubmissionGuidanceUpdatedMessageData, FormNotificationEmailUpdatedMessage, FormNotificationEmailUpdatedMessageData, FormSupportContactUpdatedMessage, FormSupportContactUpdatedMessageData, FormLiveCreatedFromDraftMessage, FormDraftCreatedFromLiveMessage, FormMigratedMessage, FormSecretSavedMessage } from '@defra/forms-model' + * @param {FormMetadata} metadata + * @param {string} secretName + * @param {AuditUser} createdBy + * @returns {FormSecretSavedMessage} + */ +export function savedFormSecretMapper(metadata, secretName, createdBy) { + const base = createFormSecretBase(metadata, secretName, createdBy) + + return { + ...base, + type: AuditEventMessageType.FORM_SECRET_SAVED + } +} + +/** + * @param {FormMetadata} metadata + * @param {string} secretName + * @param {AuditUser} createdBy + * @returns {FormSecretDeletedMessage} + */ +export function deletedFormSecretMapper(metadata, secretName, createdBy) { + const base = createFormSecretBase(metadata, secretName, createdBy) + + return { + ...base, + type: AuditEventMessageType.FORM_SECRET_DELETED + } +} + +/** + * @param {FormMetadata} metadata + * @param {string} secretNameFrom + * @param {string} secretNameTo + * @param {AuditUser} createdBy + * @returns {FormSecretRenamedMessage} + */ +export function renamedFormSecretMapper( + metadata, + secretNameFrom, + secretNameTo, + createdBy +) { + const base = createFormSecretBase(metadata, secretNameFrom, createdBy) + base.data.payload = { + secretNameFrom, + secretNameTo + } + + return { + ...base, + type: AuditEventMessageType.FORM_SECRET_RENAMED + } +} + +/** + * @import { FormDefinitionS3Meta, FormUpdatedMessage, FormDefinitionRequestType, FormDraftDeletedMessage, AuditUser, FormTitleUpdatedMessageData, FormOrganisationUpdatedMessage, FormOrganisationUpdatedMessageData, FormMetadata, FormCreatedMessage, FormCreatedMessageData, FormTitleUpdatedMessage, FormTeamNameUpdatedMessage, FormTeamNameUpdatedMessageData, FormTeamEmailUpdatedMessage, FormTeamEmailUpdatedMessageData, FormPrivacyNoticeUpdatedMessage, FormPrivacyNoticeUpdatedMessageData, FormTermsAndConditionsAgreedMessage, FormTermsAndConditionsAgreedMessageData, FormSubmissionGuidanceUpdatedMessage, FormSubmissionGuidanceUpdatedMessageData, FormNotificationEmailUpdatedMessage, FormNotificationEmailUpdatedMessageData, FormSupportContactUpdatedMessage, FormSupportContactUpdatedMessageData, FormLiveCreatedFromDraftMessage, FormDraftCreatedFromLiveMessage, FormMigratedMessage, FormSecretDeletedMessage, FormSecretRenamedMessage, FormSecretSavedMessage } from '@defra/forms-model' * @import { PartialFormMetadataDocument } from '~/src/api/types.js' */ diff --git a/src/messaging/publish.js b/src/messaging/publish.js index 518ea404..06b01d3d 100644 --- a/src/messaging/publish.js +++ b/src/messaging/publish.js @@ -3,6 +3,7 @@ import Joi from 'joi' import { mapForm } from '~/src/api/forms/service/shared.js' import { + deletedFormSecretMapper, formCreatedEventMapper, formDraftCreatedFromLiveMapper, formDraftDeletedMapper, @@ -10,6 +11,7 @@ import { formMigratedMapper, formTitleUpdatedMapper, formUpdatedMapper, + renamedFormSecretMapper, savedFormSecretMapper } from '~/src/messaging/mappers/form-events.js' import { publishEvent } from '~/src/messaging/publish-base.js' @@ -194,6 +196,47 @@ export async function publishSavedFormSecretEvent( return validateAndPublishEvent(auditMessage) } +/** + * Publish saved form secret event + * @param {WithId>} metadataDocument + * @param {string} secretName + * @param {FormMetadataAuthor} author + */ +export async function publishDeletedFormSecretEvent( + metadataDocument, + secretName, + author +) { + const metadata = mapForm(metadataDocument) + const auditMessage = deletedFormSecretMapper(metadata, secretName, author) + + return validateAndPublishEvent(auditMessage) +} + +/** + * Publish renamed form secret event + * @param {WithId>} metadataDocument + * @param {string} secretNameFrom + * @param {string} secretNameTo + * @param {FormMetadataAuthor} author + */ +export async function publishRenamedFormSecretEvent( + metadataDocument, + secretNameFrom, + secretNameTo, + author +) { + const metadata = mapForm(metadataDocument) + const auditMessage = renamedFormSecretMapper( + metadata, + secretNameFrom, + secretNameTo, + author + ) + + return validateAndPublishEvent(auditMessage) +} + /** * @import { FormDefinition, FormMetadataAuthor, FormMetadataDocument, AuditEventMessageType, FormMetadata, AuditMessage, AuditUser, FormDefinitionS3Meta } from '@defra/forms-model' * @import { WithId } from 'mongodb' diff --git a/src/messaging/publish.test.js b/src/messaging/publish.test.js index 0ab28db5..2350bd75 100644 --- a/src/messaging/publish.test.js +++ b/src/messaging/publish.test.js @@ -21,6 +21,7 @@ import { buildFormOrganisationUpdatedMessage } from '~/src/messaging/__stubs__/m import { publishEvent } from '~/src/messaging/publish-base.js' import { bulkPublishEvents, + publishDeletedFormSecretEvent, publishDraftCreatedFromLiveEvent, publishFormCreatedEvent, publishFormDraftDeletedEvent, @@ -29,6 +30,7 @@ import { publishFormTitleUpdatedEvent, publishFormUpdatedEvent, publishLiveCreatedFromDraftEvent, + publishRenamedFormSecretEvent, publishSavedFormSecretEvent } from '~/src/messaging/publish.js' import { saveToS3 } from '~/src/messaging/s3.js' @@ -327,6 +329,51 @@ describe('publish', () => { }) }) + describe('publishDeletedFormSecretEvent', () => { + it('should publish a FORM_SECRET_DELETED event', async () => { + await publishDeletedFormSecretEvent( + formMetadataDocument, + 'my-new-secret', + author + ) + + const [publishEventCall] = jest.mocked(publishEvent).mock.calls[0] + expect(publishEventCall).toMatchObject({ + schemaVersion: AuditEventMessageSchemaVersion.V1, + category: AuditEventMessageCategory.FORM, + type: AuditEventMessageType.FORM_SECRET_DELETED, + createdBy: author + }) + expect(publishEventCall.data).toMatchObject({ + slug: formMetadataDocument.slug, + secretName: 'my-new-secret' + }) + }) + }) + + describe('publishRenamedFormSecretEvent', () => { + it('should publish a FORM_SECRET_RENAMED event', async () => { + await publishRenamedFormSecretEvent( + formMetadataDocument, + 'my-new-secret-before', + 'my-new-secret-after', + author + ) + + const [publishEventCall] = jest.mocked(publishEvent).mock.calls[0] + expect(publishEventCall).toMatchObject({ + schemaVersion: AuditEventMessageSchemaVersion.V1, + category: AuditEventMessageCategory.FORM, + type: AuditEventMessageType.FORM_SECRET_RENAMED, + createdBy: author + }) + expect(publishEventCall.data).toMatchObject({ + slug: formMetadataDocument.slug, + secretName: 'my-new-secret-before' + }) + }) + }) + describe('publishSavedFormSecretEvent', () => { it('should publish a FORM_SECRET_SAVED event', async () => { await publishSavedFormSecretEvent( diff --git a/src/routes/secrets.js b/src/routes/secrets.js index 47490b4c..47b7e8ff 100644 --- a/src/routes/secrets.js +++ b/src/routes/secrets.js @@ -3,8 +3,10 @@ import { StatusCodes } from 'http-status-codes' import Joi from 'joi' import { + deleteFormSecret, existsFormSecret, getFormSecret, + renameFormSecret, saveFormSecret } from '~/src/api/forms/service/secrets.js' import { failAction } from '~/src/helpers/fail-action.js' @@ -21,6 +23,15 @@ export const formSecretSchema = Joi.object() }) .required() +// Schema to rename form secret by form id +export const formSecretRenameSchema = Joi.object() + .keys({ + id: idSchema, + nameBefore: nameSchema, + nameAfter: nameSchema + }) + .required() + export const formSecretPayloadSchema = Joi.object() .keys({ secretValue: Joi.string().trim().required() @@ -98,10 +109,60 @@ export default [ failAction } } + }, + { + method: 'DELETE', + path: ROUTE_SECRETS, + /** + * @param {RequestDeleteFormSecret} request + */ + async handler(request) { + const { auth, params } = request + const { id, name } = params + const author = getAuthor(auth.credentials.user) + + await deleteFormSecret(id, name, author) + + return StatusCodes.OK + }, + options: { + auth: { + scope: [`+${Scopes.FormEdit}`] + }, + validate: { + params: formSecretSchema, + failAction + } + } + }, + { + method: 'PUT', + path: '/forms/{id}/secrets/{nameBefore}/{nameAfter}', + /** + * @param {RequestRenameFormSecret} request + */ + async handler(request) { + const { auth, params } = request + const { id, nameBefore, nameAfter } = params + const author = getAuthor(auth.credentials.user) + + await renameFormSecret(id, nameBefore, nameAfter, author) + + return StatusCodes.OK + }, + options: { + auth: { + scope: [`+${Scopes.FormEdit}`] + }, + validate: { + params: formSecretRenameSchema, + failAction + } + } } ] /** * @import { ServerRoute } from '@hapi/hapi' - * @import { RequestGetFormSecret, RequestSaveFormSecret } from '~/src/api/types.js' + * @import { RequestDeleteFormSecret, RequestGetFormSecret, RequestRenameFormSecret, RequestSaveFormSecret } from '~/src/api/types.js' */ diff --git a/src/routes/secrets.test.js b/src/routes/secrets.test.js index 3f57b3bc..c61cd934 100644 --- a/src/routes/secrets.test.js +++ b/src/routes/secrets.test.js @@ -1,6 +1,8 @@ import { + deleteFormSecret, existsFormSecret, getFormSecret, + renameFormSecret, saveFormSecret } from '~/src/api/forms/service/secrets.js' import { createServer } from '~/src/api/server.js' @@ -45,7 +47,8 @@ describe('Secrets routes', () => { jest.mocked(existsFormSecret).mockResolvedValueOnce({ exists: true, createdAt: undefined, - updatedAt: undefined + updatedAt: undefined, + renamedAt: undefined }) const response = await server.inject({ @@ -79,6 +82,39 @@ describe('Secrets routes', () => { expect(calledName).toBe('my-new-secret') expect(calledValue).toBe('My new secret value') }) + + test('Testing PUT /forms/{id}/secrets/{nameBefore}/{nameAfter}', async () => { + const renameSecret = jest.mocked(renameFormSecret) + + const response = await server.inject({ + method: 'PUT', + url: `/forms/${id}/secrets/my-secret-before/my-secret-after`, + auth + }) + + expect(response.statusCode).toEqual(okStatusCode) + expect(response.headers['content-type']).toContain(jsonContentType) + expect(response.result).toBe(okStatusCode) + const [, calledNameBefore, calledNameAfter] = renameSecret.mock.calls[0] + expect(calledNameBefore).toBe('my-secret-before') + expect(calledNameAfter).toBe('my-secret-after') + }) + + test('Testing DELETE /forms/{id}/secrets/{name}', async () => { + const deleteSecret = jest.mocked(deleteFormSecret) + + const response = await server.inject({ + method: 'DELETE', + url: `/forms/${id}/secrets/my-secret`, + auth + }) + + expect(response.statusCode).toEqual(okStatusCode) + expect(response.headers['content-type']).toContain(jsonContentType) + expect(response.result).toBe(okStatusCode) + const [, calledName] = deleteSecret.mock.calls[0] + expect(calledName).toBe('my-secret') + }) }) describe('Error responses', () => { diff --git a/test/integration/postman/forms-manager-ci-mock.postman_collection.json b/test/integration/postman/forms-manager-ci-mock.postman_collection.json index cb963ccd..f48a0a09 100644 --- a/test/integration/postman/forms-manager-ci-mock.postman_collection.json +++ b/test/integration/postman/forms-manager-ci-mock.postman_collection.json @@ -1,6 +1,6 @@ { "info": { - "_postman_id": "25fad961-499a-4c75-8728-4d076cd26d45", + "_postman_id": "bdcca91c-fac2-4df3-aaae-2afb359a65e1", "name": "forms-manager", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", "_exporter_id": "23938788" @@ -1882,6 +1882,291 @@ } }, "response": [] + }, + { + "name": "Save secret", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"response is ok\", function () {", + " pm.response.to.have.status(200);", + "});" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"secretValue\": \"my-secret-value\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{root}}/forms/{{go_live_form_id}}/secrets/my-secret", + "host": [ + "{{root}}" + ], + "path": [ + "forms", + "{{go_live_form_id}}", + "secrets", + "my-secret" + ] + } + }, + "response": [] + }, + { + "name": "Get secret 1", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"response is ok\", function () {", + " pm.response.to.have.status(200);", + "});" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{root}}/forms/{{go_live_form_id}}/secrets/my-secret", + "host": [ + "{{root}}" + ], + "path": [ + "forms", + "{{go_live_form_id}}", + "secrets", + "my-secret" + ] + } + }, + "response": [] + }, + { + "name": "Rename secret", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"response is ok\", function () {", + " pm.response.to.have.status(200);", + "});" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{root}}/forms/{{go_live_form_id}}/secrets/my-secret/my-renamed-secret", + "host": [ + "{{root}}" + ], + "path": [ + "forms", + "{{go_live_form_id}}", + "secrets", + "my-secret", + "my-renamed-secret" + ] + } + }, + "response": [] + }, + { + "name": "Get renamed secret", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"response is ok\", function () {", + " pm.response.to.have.status(200);", + "});" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{root}}/forms/{{go_live_form_id}}/secrets/my-secret-renamed", + "host": [ + "{{root}}" + ], + "path": [ + "forms", + "{{go_live_form_id}}", + "secrets", + "my-secret-renamed" + ] + } + }, + "response": [] + }, + { + "name": "Delete renamed secret", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"response is ok\", function () {", + " pm.response.to.have.status(200);", + "});" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "DELETE", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{root}}/forms/{{go_live_form_id}}/secrets/my-secret-renamed", + "host": [ + "{{root}}" + ], + "path": [ + "forms", + "{{go_live_form_id}}", + "secrets", + "my-secret-renamed" + ] + } + }, + "response": [] + }, + { + "name": "Get deleted secret", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"response is not found\", function () {", + " pm.response.to.have.status(404);", + "});" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{root}}/forms/{{go_live_form_id}}/secrets/my-secret-renamed", + "host": [ + "{{root}}" + ], + "path": [ + "forms", + "{{go_live_form_id}}", + "secrets", + "my-secret-renamed" + ] + } + }, + "response": [] } ], "auth": { @@ -2030,4 +2315,4 @@ "value": "" } ] -} +} \ No newline at end of file From 4989a4c024f8a459213051a11f0ab353f7e438e4 Mon Sep 17 00:00:00 2001 From: Jez Barnsley Date: Fri, 6 Mar 2026 10:59:59 +0000 Subject: [PATCH 02/12] Lint and prettier --- src/api/forms/service/definition.test.js | 6 ++++-- src/api/forms/service/secrets.test.js | 14 ++++++-------- .../forms-manager-ci-mock.postman_collection.json | 2 +- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/api/forms/service/definition.test.js b/src/api/forms/service/definition.test.js index fa6a040d..12461328 100644 --- a/src/api/forms/service/definition.test.js +++ b/src/api/forms/service/definition.test.js @@ -121,7 +121,8 @@ describe('Forms service', () => { jest.mocked(existsFormSecret).mockResolvedValue({ exists: true, createdAt: undefined, - updatedAt: undefined + updatedAt: undefined, + renamedAt: undefined }) }) @@ -339,7 +340,8 @@ describe('Forms service', () => { jest.mocked(existsFormSecret).mockResolvedValueOnce({ exists: false, createdAt: undefined, - updatedAt: undefined + updatedAt: undefined, + renamedAt: undefined }) const definitionWithPayment = buildDefinition() diff --git a/src/api/forms/service/secrets.test.js b/src/api/forms/service/secrets.test.js index 9cb9b071..b5d98917 100644 --- a/src/api/forms/service/secrets.test.js +++ b/src/api/forms/service/secrets.test.js @@ -62,14 +62,12 @@ describe('secrets', () => { describe('existsFormSecret', () => { it('should return true if a form secret exists', async () => { const now = new Date() - jest - .mocked(exists) - .mockResolvedValueOnce({ - exists: true, - createdAt: now, - updatedAt: now, - renamedAt: undefined - }) + jest.mocked(exists).mockResolvedValueOnce({ + exists: true, + createdAt: now, + updatedAt: now, + renamedAt: undefined + }) const secretName = 'my-secret-name' const res = await existsFormSecret(formId, secretName) diff --git a/test/integration/postman/forms-manager-ci-mock.postman_collection.json b/test/integration/postman/forms-manager-ci-mock.postman_collection.json index f48a0a09..1f5a0fbb 100644 --- a/test/integration/postman/forms-manager-ci-mock.postman_collection.json +++ b/test/integration/postman/forms-manager-ci-mock.postman_collection.json @@ -2315,4 +2315,4 @@ "value": "" } ] -} \ No newline at end of file +} From 83781da5979e91c93694388497162f8a3de2a3ae Mon Sep 17 00:00:00 2001 From: Jez Barnsley Date: Fri, 6 Mar 2026 13:53:14 +0000 Subject: [PATCH 03/12] Model bump --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7e3e58c5..7561b0fd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "dependencies": { "@aws-sdk/client-s3": "^3.995.0", "@aws-sdk/client-sns": "^3.995.0", - "@defra/forms-model": "^3.0.629", + "@defra/forms-model": "^3.0.633", "@defra/hapi-tracing": "^1.29.0", "@elastic/ecs-pino-format": "^1.5.0", "@hapi/hapi": "^21.4.4", @@ -2759,9 +2759,9 @@ } }, "node_modules/@defra/forms-model": { - "version": "3.0.629", - "resolved": "https://registry.npmjs.org/@defra/forms-model/-/forms-model-3.0.629.tgz", - "integrity": "sha512-MnNgpAHON8eKhVttqI/2xjLpFNCAE43J7Iaqe84XftN2ML0UvLeZHFh4Jx1ePu/h+dvctmPXwCeooaZ+aPVWyg==", + "version": "3.0.633", + "resolved": "https://registry.npmjs.org/@defra/forms-model/-/forms-model-3.0.633.tgz", + "integrity": "sha512-qFrMMrpLQksWe00fKaALX56eIImwQeFmpzTkzbJ+3n8QTgqjxcAlr+BHzSeGgQxBA11oZT/A5GSyeX9viEqGLw==", "license": "OGL-UK-3.0", "dependencies": { "@joi/date": "^2.1.1", diff --git a/package.json b/package.json index c713b00c..c9a105bf 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "dependencies": { "@aws-sdk/client-s3": "^3.995.0", "@aws-sdk/client-sns": "^3.995.0", - "@defra/forms-model": "^3.0.629", + "@defra/forms-model": "^3.0.633", "@defra/hapi-tracing": "^1.29.0", "@elastic/ecs-pino-format": "^1.5.0", "@hapi/hapi": "^21.4.4", From e47cabd4ab3bb2ffabdf9334d9eb2f961d141c38 Mon Sep 17 00:00:00 2001 From: Jez Barnsley Date: Fri, 6 Mar 2026 13:59:53 +0000 Subject: [PATCH 04/12] Fixed integration test --- .../postman/forms-manager-ci-mock.postman_collection.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/postman/forms-manager-ci-mock.postman_collection.json b/test/integration/postman/forms-manager-ci-mock.postman_collection.json index 1f5a0fbb..f8cb5693 100644 --- a/test/integration/postman/forms-manager-ci-mock.postman_collection.json +++ b/test/integration/postman/forms-manager-ci-mock.postman_collection.json @@ -2063,7 +2063,7 @@ } }, "url": { - "raw": "{{root}}/forms/{{go_live_form_id}}/secrets/my-secret-renamed", + "raw": "{{root}}/forms/{{go_live_form_id}}/secrets/my-renamed-secret", "host": [ "{{root}}" ], From d7c6ae18e4b92ba5c598f48cdc0e468d0cc0da11 Mon Sep 17 00:00:00 2001 From: Jez Barnsley Date: Fri, 6 Mar 2026 14:15:22 +0000 Subject: [PATCH 05/12] Added key pair instructions --- README.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/README.md b/README.md index 8722437c..d958d4bb 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ Core delivery platform Node.js Backend Template. - [Node.js](#nodejs) - [Local development](#local-development) - [Setup](#setup) + - [Generating a public/private key pair](#generating-a-publicprivate-key-pair) - [Development](#development) - [Production](#production) - [Npm scripts](#npm-scripts) @@ -72,6 +73,26 @@ PUBLIC_KEY_FOR_SECRETS="" For proxy options, see https://www.npmjs.com/package/proxy-from-env which is used by https://github.com/TooTallNate/proxy-agents/tree/main/packages/proxy-agent. It's currently supports Hapi Wreck only, e.g. in the JWKS lookup. +### Generating a public/private key pair + +The public key is used by forms-manager to encrypt secrets. The private key is used by forms-runner to decrypt secrets. + +``` +openssl genrsa -out private.pem 4096 +openssl rsa -pubout -in private.pem -out public.pem +cat public.pem #[copy the output and paste as in the next command] +echo -n "" | base64 +``` + +Copy/paste the encoded result as the env var PUBLIC_KEY_FOR_SECRETS (in forms-manager) + +``` +cat private.pem #[copy the output and paste as in the next command] +echo -n "" | base64 +``` + +Copy/paste the encoded result as the env var PRIVATE_KEY_FOR_SECRETS (in forms-runner) + ### Development To run the application in `development` mode run: From d1eb58b4a735a673729f979e0a685d7df6fb3a13 Mon Sep 17 00:00:00 2001 From: Jez Barnsley Date: Fri, 6 Mar 2026 14:22:22 +0000 Subject: [PATCH 06/12] Fixed integration test --- .../postman/forms-manager-ci-mock.postman_collection.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/integration/postman/forms-manager-ci-mock.postman_collection.json b/test/integration/postman/forms-manager-ci-mock.postman_collection.json index f8cb5693..6f14adf7 100644 --- a/test/integration/postman/forms-manager-ci-mock.postman_collection.json +++ b/test/integration/postman/forms-manager-ci-mock.postman_collection.json @@ -2015,7 +2015,7 @@ } }, "url": { - "raw": "{{root}}/forms/{{go_live_form_id}}/secrets/my-secret/my-renamed-secret", + "raw": "{{root}}/forms/{{go_live_form_id}}/secrets/my-secret/my-secret-renamed", "host": [ "{{root}}" ], @@ -2024,7 +2024,7 @@ "{{go_live_form_id}}", "secrets", "my-secret", - "my-renamed-secret" + "my-secret-renamed" ] } }, @@ -2063,7 +2063,7 @@ } }, "url": { - "raw": "{{root}}/forms/{{go_live_form_id}}/secrets/my-renamed-secret", + "raw": "{{root}}/forms/{{go_live_form_id}}/secrets/my-secret-renamed", "host": [ "{{root}}" ], From 13ca3c8c7c3b1a8780a59027ebc4f425beb36707 Mon Sep 17 00:00:00 2001 From: Jez Barnsley Date: Fri, 6 Mar 2026 15:33:22 +0000 Subject: [PATCH 07/12] Fixed rename --- .../forms/repositories/secrets-repository.js | 2 +- src/api/forms/service/definition.js | 31 +++++++++++++++++-- src/api/forms/service/definition.test.js | 20 ++++++++---- 3 files changed, 44 insertions(+), 9 deletions(-) diff --git a/src/api/forms/repositories/secrets-repository.js b/src/api/forms/repositories/secrets-repository.js index b113d3f5..dafcd47f 100644 --- a/src/api/forms/repositories/secrets-repository.js +++ b/src/api/forms/repositories/secrets-repository.js @@ -195,7 +195,7 @@ export async function rename(formId, secretNameFrom, secretNameTo, session) { const result = await coll.findOneAndUpdate( { formId, - secretNameFrom + secretName: secretNameFrom }, { $set: { secretName: secretNameTo, renamedAt: now } diff --git a/src/api/forms/service/definition.js b/src/api/forms/service/definition.js index 2a3be6e5..94f04149 100644 --- a/src/api/forms/service/definition.js +++ b/src/api/forms/service/definition.js @@ -11,6 +11,11 @@ import { makeFormLiveErrorMessages } from '~/src/api/forms/constants.js' import * as formDefinition from '~/src/api/forms/repositories/form-definition-repository.js' import { deleteDraft } from '~/src/api/forms/repositories/form-definition-repository.js' import * as formMetadata from '~/src/api/forms/repositories/form-metadata-repository.js' +import { + deleteSecret, + exists, + rename +} from '~/src/api/forms/repositories/secrets-repository.js' import { getValidationSchema } from '~/src/api/forms/service/helpers/definition.js' import { getForm } from '~/src/api/forms/service/index.js' import { existsFormSecret } from '~/src/api/forms/service/secrets.js' @@ -30,6 +35,7 @@ import { import { client } from '~/src/mongo.js' export const PAYMENT_LIVE_API_KEY = 'payment-live-api-key' +export const PAYMENT_LIVE_API_KEY_PENDING = 'payment-live-api-key-pending' /** * Retrieves a paginated list of forms with filter options @@ -274,8 +280,11 @@ export async function createLiveFromDraft(formId, author) { /** @type {boolean} */ let paymentKeyExists if (formHasPayment) { - const { exists } = await existsFormSecret(formId, PAYMENT_LIVE_API_KEY) - paymentKeyExists = exists + const [liveExists, pendingExists] = await Promise.all([ + existsFormSecret(formId, PAYMENT_LIVE_API_KEY), + existsFormSecret(formId, PAYMENT_LIVE_API_KEY_PENDING) + ]) + paymentKeyExists = liveExists.exists || pendingExists.exists } else { paymentKeyExists = true } @@ -325,6 +334,24 @@ export async function createLiveFromDraft(formId, author) { await createFormVersion(formId, session) + // Swap over live payment keys if necessary + if (formHasPayment) { + const pendingKeyExists = await exists( + formId, + PAYMENT_LIVE_API_KEY_PENDING, + session + ) + if (pendingKeyExists.exists) { + await deleteSecret(formId, PAYMENT_LIVE_API_KEY, session) + await rename( + formId, + PAYMENT_LIVE_API_KEY_PENDING, + PAYMENT_LIVE_API_KEY, + session + ) + } + } + // Publish audit message await publishLiveCreatedFromDraftEvent(formId, now, author) }) diff --git a/src/api/forms/service/definition.test.js b/src/api/forms/service/definition.test.js index 12461328..f5008941 100644 --- a/src/api/forms/service/definition.test.js +++ b/src/api/forms/service/definition.test.js @@ -337,12 +337,20 @@ describe('Forms service', () => { } jest.mocked(formMetadata.get).mockResolvedValue(metadata) - jest.mocked(existsFormSecret).mockResolvedValueOnce({ - exists: false, - createdAt: undefined, - updatedAt: undefined, - renamedAt: undefined - }) + jest + .mocked(existsFormSecret) + .mockResolvedValueOnce({ + exists: false, + createdAt: undefined, + updatedAt: undefined, + renamedAt: undefined + }) + .mockResolvedValueOnce({ + exists: false, + createdAt: undefined, + updatedAt: undefined, + renamedAt: undefined + }) const definitionWithPayment = buildDefinition() definitionWithPayment.pages.push( From 912d436a719dd025ba45a7586d4da3b51912385e Mon Sep 17 00:00:00 2001 From: Jez Barnsley Date: Fri, 6 Mar 2026 15:42:42 +0000 Subject: [PATCH 08/12] Sonar fix --- src/api/forms/service/definition.js | 47 ++++++++++++++++++----------- 1 file changed, 30 insertions(+), 17 deletions(-) diff --git a/src/api/forms/service/definition.js b/src/api/forms/service/definition.js index 94f04149..49354fbb 100644 --- a/src/api/forms/service/definition.js +++ b/src/api/forms/service/definition.js @@ -259,6 +259,34 @@ function validateFormForPublishing( } } +/** + * Overwrite a live payment API key with the value of the pending live payment API key. + * Essentially make the pending key the live one for forms-runner to access. + * @param {boolean} formHasPayment - true if the form includes a payment question + * @param {string} formId - the id of the form + * @param {ClientSession} session + */ +export async function makePaymentKeyLive(formHasPayment, formId, session) { + if (!formHasPayment) { + return + } + + const pendingKeyExists = await exists( + formId, + PAYMENT_LIVE_API_KEY_PENDING, + session + ) + if (pendingKeyExists.exists) { + await deleteSecret(formId, PAYMENT_LIVE_API_KEY, session) + await rename( + formId, + PAYMENT_LIVE_API_KEY_PENDING, + PAYMENT_LIVE_API_KEY, + session + ) + } +} + /** * Creates the live form from the current draft state * @param {string} formId - ID of the form @@ -334,23 +362,8 @@ export async function createLiveFromDraft(formId, author) { await createFormVersion(formId, session) - // Swap over live payment keys if necessary - if (formHasPayment) { - const pendingKeyExists = await exists( - formId, - PAYMENT_LIVE_API_KEY_PENDING, - session - ) - if (pendingKeyExists.exists) { - await deleteSecret(formId, PAYMENT_LIVE_API_KEY, session) - await rename( - formId, - PAYMENT_LIVE_API_KEY_PENDING, - PAYMENT_LIVE_API_KEY, - session - ) - } - } + // Make payment key live if a pending one is stored + await makePaymentKeyLive(formHasPayment, formId, session) // Publish audit message await publishLiveCreatedFromDraftEvent(formId, now, author) From 783a9332e42f687144c0e2b83fee49753f972b32 Mon Sep 17 00:00:00 2001 From: Jez Barnsley Date: Fri, 6 Mar 2026 15:57:32 +0000 Subject: [PATCH 09/12] Extra coverage --- package.json | 2 +- src/api/forms/service/definition.test.js | 56 +++++++++++++++++++++++- 2 files changed, 55 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index c9a105bf..4b7ff5ef 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "test:integration:wait": "echo 'Waiting for app...' && sleep 15 && until curl --fail http://localhost:3001/health; do echo 'Still waiting...'; sleep 5; done; echo 'App ready!'", "test:integration:run": "docker compose -f docker-compose.integration-test.yml run --rm --entrypoint=\"\" newman sh -c \"npm install -g newman-reporter-htmlextra && newman run /etc/newman/forms-manager-ci-mock.postman_collection.json -e /etc/newman/forms-manager-ci-mock.postman_environment.json -r cli,json,htmlextra --reporter-json-export /etc/newman/reports/newman-summary.json --reporter-htmlextra-export /etc/newman/reports/newman-report.html --reporter-htmlextra-showConsoleLogs\"", "test:integration:stop": "docker compose -f docker-compose.integration-test.yml down -v --remove-orphans", - "test:integration": "npm run test:integration:setup && npm run test:integration:start && npm run test:integration:wait && npm run test:integration:run && npm run test:integration:stop; EXIT_CODE=$? && exit $EXIT_CODE", + "test:integration": "npm run test:integration:setup && npm run test:integration:start && npm run test:integration:wait && npm run test:integration:run; EXIT_CODE=$? && exit $EXIT_CODE", "server:watch": "NODE_ENV=development tsx watch --clear-screen=false --enable-source-maps --exclude \"**/*.test.*\"", "server:watch:dev": "npm run server:watch -- ./src/index.js", "server:watch:debug": "npm run server:watch -- --inspect-wait ./src/index.js", diff --git a/src/api/forms/service/definition.test.js b/src/api/forms/service/definition.test.js index f5008941..7cdf7a56 100644 --- a/src/api/forms/service/definition.test.js +++ b/src/api/forms/service/definition.test.js @@ -29,6 +29,11 @@ import { modifyReorderPages, modifyReorderSections } from '~/src/api/forms/repositories/helpers.js' +import { + deleteSecret, + exists, + rename +} from '~/src/api/forms/repositories/secrets-repository.js' import { formMetadataDocument, formMetadataInput, @@ -43,6 +48,7 @@ import { deleteDraftFormDefinition, getFormDefinition, listForms, + makePaymentKeyLive, missingPrivacyNotice, reorderDraftFormDefinitionComponents, reorderDraftFormDefinitionPages, @@ -66,6 +72,7 @@ jest.mock('~/src/helpers/get-author.js') jest.mock('~/src/api/forms/repositories/form-definition-repository.js') jest.mock('~/src/api/forms/repositories/form-metadata-repository.js') jest.mock('~/src/api/forms/repositories/form-versions-repository.js') +jest.mock('~/src/api/forms/repositories/secrets-repository.js') jest.mock('~/src/api/forms/templates.js') jest.mock('~/src/mongo.js') jest.mock('~/src/messaging/publish-base.js') @@ -1572,9 +1579,54 @@ describe('Forms service', () => { expect(missingPrivacyNotice(metadata)).toBe(false) }) }) + + describe('makePaymentKeyLive', () => { + const formId = 'ea8154b9-e724-4bb4-a9cb-46f4159a53fa' + const mockSession = /** @type {ClientSession} */ ({}) + + it('should ignore if not a payment question', async () => { + jest + .mocked(exists) + .mockResolvedValueOnce({ + exists: true, + createdAt: new Date(), + updatedAt: undefined, + renamedAt: undefined + }) + await makePaymentKeyLive(false, formId, mockSession) + expect(exists).not.toHaveBeenCalled() + }) + + it('should ignore if not exists', async () => { + jest + .mocked(exists) + .mockResolvedValueOnce({ + exists: false, + createdAt: new Date(), + updatedAt: undefined, + renamedAt: undefined + }) + await makePaymentKeyLive(true, formId, mockSession) + expect(rename).not.toHaveBeenCalled() + }) + + it('should delete and rename if exists', async () => { + jest + .mocked(exists) + .mockResolvedValueOnce({ + exists: true, + createdAt: new Date(), + updatedAt: undefined, + renamedAt: undefined + }) + await makePaymentKeyLive(true, formId, mockSession) + expect(deleteSecret).toHaveBeenCalled() + expect(rename).toHaveBeenCalled() + }) + }) }) /** - * @import { FormDefinition, FormMetadata, FormMetadataDocument, PageQuestion, QueryOptions } from '@defra/forms-model' - * @import { WithId } from 'mongodb' + * @import { FormDefinition, FormMetadata, FormMetadataDocument, QueryOptions } from '@defra/forms-model' + * @import { ClientSession, WithId } from 'mongodb' */ From 754bc7175de29bc54ba311dff411af094b493770 Mon Sep 17 00:00:00 2001 From: Jez Barnsley Date: Fri, 6 Mar 2026 15:58:48 +0000 Subject: [PATCH 10/12] Prettier --- src/api/forms/service/definition.test.js | 42 ++++++++++-------------- 1 file changed, 18 insertions(+), 24 deletions(-) diff --git a/src/api/forms/service/definition.test.js b/src/api/forms/service/definition.test.js index 7cdf7a56..726dd1ab 100644 --- a/src/api/forms/service/definition.test.js +++ b/src/api/forms/service/definition.test.js @@ -1585,40 +1585,34 @@ describe('Forms service', () => { const mockSession = /** @type {ClientSession} */ ({}) it('should ignore if not a payment question', async () => { - jest - .mocked(exists) - .mockResolvedValueOnce({ - exists: true, - createdAt: new Date(), - updatedAt: undefined, - renamedAt: undefined - }) + jest.mocked(exists).mockResolvedValueOnce({ + exists: true, + createdAt: new Date(), + updatedAt: undefined, + renamedAt: undefined + }) await makePaymentKeyLive(false, formId, mockSession) expect(exists).not.toHaveBeenCalled() }) it('should ignore if not exists', async () => { - jest - .mocked(exists) - .mockResolvedValueOnce({ - exists: false, - createdAt: new Date(), - updatedAt: undefined, - renamedAt: undefined - }) + jest.mocked(exists).mockResolvedValueOnce({ + exists: false, + createdAt: new Date(), + updatedAt: undefined, + renamedAt: undefined + }) await makePaymentKeyLive(true, formId, mockSession) expect(rename).not.toHaveBeenCalled() }) it('should delete and rename if exists', async () => { - jest - .mocked(exists) - .mockResolvedValueOnce({ - exists: true, - createdAt: new Date(), - updatedAt: undefined, - renamedAt: undefined - }) + jest.mocked(exists).mockResolvedValueOnce({ + exists: true, + createdAt: new Date(), + updatedAt: undefined, + renamedAt: undefined + }) await makePaymentKeyLive(true, formId, mockSession) expect(deleteSecret).toHaveBeenCalled() expect(rename).toHaveBeenCalled() From 287ec2995bd5b78bb72b990d28856e412c255639 Mon Sep 17 00:00:00 2001 From: Jez Barnsley Date: Mon, 9 Mar 2026 08:06:53 +0000 Subject: [PATCH 11/12] Removed unnecessary endpoint --- src/api/forms/service/secrets.js | 59 --------------------------- src/api/forms/service/secrets.test.js | 57 -------------------------- src/messaging/publish.js | 25 ------------ src/messaging/publish.test.js | 24 ----------- src/routes/secrets.js | 26 ------------ src/routes/secrets.test.js | 18 -------- 6 files changed, 209 deletions(-) diff --git a/src/api/forms/service/secrets.js b/src/api/forms/service/secrets.js index ffdb3bcf..ddd59604 100644 --- a/src/api/forms/service/secrets.js +++ b/src/api/forms/service/secrets.js @@ -6,7 +6,6 @@ import { encryptSecret } from '~/src/api/forms/service/helpers/crypto.js' import { logger } from '~/src/api/forms/service/shared.js' import { publishDeletedFormSecretEvent, - publishRenamedFormSecretEvent, publishSavedFormSecretEvent } from '~/src/messaging/publish.js' import { client } from '~/src/mongo.js' @@ -96,64 +95,6 @@ export async function deleteFormSecret(formId, secretName, author) { } } -/** - * Changes the name of a secret. - * @param {string} formId - id of the form - * @param {string} secretNameFrom - name of the secret to be changed - * @param {string} secretNameTo - name of the secret after the change - * @param {FormMetadataAuthor} author - */ -export async function renameFormSecret( - formId, - secretNameFrom, - secretNameTo, - author -) { - const session = client.startSession() - - try { - await session.withTransaction(async () => { - const metadata = await formMetadata.get(formId, session) - - logger.info( - `1. About to rename secret '${secretNameFrom}' to '${secretNameTo}' on form ID ${formId}` - ) - await secretsRepository.rename( - formId, - secretNameFrom, - secretNameTo, - session - ) - logger.info( - `2. Renamed secret '${secretNameFrom}' to '${secretNameTo}' on form ID ${formId}` - ) - - await publishRenamedFormSecretEvent( - metadata, - secretNameFrom, - secretNameTo, - author - ) - logger.info( - `3. Sent audit secret '${secretNameFrom}' to '${secretNameTo}' on form ID ${formId}` - ) - }) - - logger.info( - `Renamed secret '${secretNameFrom}' to '${secretNameTo}' on form ID ${formId}` - ) - } catch (err) { - logger.error( - err, - `[assignSections] Failed to rename secret '${secretNameFrom}' '${secretNameTo}' on form ID ${formId} - ${getErrorMessage(err)}` - ) - - throw err - } finally { - await session.endSession() - } -} - /** * @import { FormMetadataAuthor } from '@defra/forms-model' */ diff --git a/src/api/forms/service/secrets.test.js b/src/api/forms/service/secrets.test.js index b5d98917..3cdcb327 100644 --- a/src/api/forms/service/secrets.test.js +++ b/src/api/forms/service/secrets.test.js @@ -6,7 +6,6 @@ import { deleteSecret, exists, get, - rename, save } from '~/src/api/forms/repositories/secrets-repository.js' import { formMetadataDocument } from '~/src/api/forms/service/__stubs__/service.js' @@ -14,7 +13,6 @@ import { deleteFormSecret, existsFormSecret, getFormSecret, - renameFormSecret, saveFormSecret } from '~/src/api/forms/service/secrets.js' import { getAuthor } from '~/src/helpers/get-author.js' @@ -118,61 +116,6 @@ describe('secrets', () => { }) }) - describe('renameFormSecret', () => { - it('should rename form secret and publish audit event', async () => { - jest.mocked(formMetadata.get).mockResolvedValue(formMetadataDocument) - const publishEventSpy = jest.spyOn(publishBase, 'publishEvent') - - const secretNameBefore = 'my-secret-name-before' - const secretNameAfter = 'my-secret-name-after' - await renameFormSecret( - formId, - secretNameBefore, - secretNameAfter, - defaultAuthor - ) - - // Verify repository was called with correct arguments - const [formIdCalled, secretNameBeforeCalled, secretNameAfterCalled] = - jest.mocked(rename).mock.calls[0] - expect(formId).toBe(formIdCalled) - expect(secretNameBefore).toBe(secretNameBeforeCalled) - expect(secretNameAfter).toBe(secretNameAfterCalled) - - // Verify audit event was published - const [auditMessage] = publishEventSpy.mock.calls[0] - expect(auditMessage).toMatchObject({ - type: AuditEventMessageType.FORM_SECRET_RENAMED - }) - expect(auditMessage.data).toMatchObject({ - formId, - secretName: 'my-secret-name-before', - payload: { - secretNameFrom: secretNameBefore, - secretNameTo: secretNameAfter - } - }) - }) - - it('should throw when error', async () => { - jest.mocked(formMetadata.get).mockResolvedValue(formMetadataDocument) - jest.mocked(rename).mockImplementationOnce(() => { - throw new Error('Renaming error') - }) - - const secretNameBefore = 'my-secret-name-before' - const secretNameAfter = 'my-secret-name-after' - await expect(() => - renameFormSecret( - formId, - secretNameBefore, - secretNameAfter, - defaultAuthor - ) - ).rejects.toThrow('Renaming error') - }) - }) - describe('saveFormSecret', () => { it('should save form secret and publish audit event', async () => { jest.mocked(formMetadata.get).mockResolvedValue(formMetadataDocument) diff --git a/src/messaging/publish.js b/src/messaging/publish.js index 06b01d3d..693f1618 100644 --- a/src/messaging/publish.js +++ b/src/messaging/publish.js @@ -11,7 +11,6 @@ import { formMigratedMapper, formTitleUpdatedMapper, formUpdatedMapper, - renamedFormSecretMapper, savedFormSecretMapper } from '~/src/messaging/mappers/form-events.js' import { publishEvent } from '~/src/messaging/publish-base.js' @@ -213,30 +212,6 @@ export async function publishDeletedFormSecretEvent( return validateAndPublishEvent(auditMessage) } -/** - * Publish renamed form secret event - * @param {WithId>} metadataDocument - * @param {string} secretNameFrom - * @param {string} secretNameTo - * @param {FormMetadataAuthor} author - */ -export async function publishRenamedFormSecretEvent( - metadataDocument, - secretNameFrom, - secretNameTo, - author -) { - const metadata = mapForm(metadataDocument) - const auditMessage = renamedFormSecretMapper( - metadata, - secretNameFrom, - secretNameTo, - author - ) - - return validateAndPublishEvent(auditMessage) -} - /** * @import { FormDefinition, FormMetadataAuthor, FormMetadataDocument, AuditEventMessageType, FormMetadata, AuditMessage, AuditUser, FormDefinitionS3Meta } from '@defra/forms-model' * @import { WithId } from 'mongodb' diff --git a/src/messaging/publish.test.js b/src/messaging/publish.test.js index 2350bd75..0aad124c 100644 --- a/src/messaging/publish.test.js +++ b/src/messaging/publish.test.js @@ -30,7 +30,6 @@ import { publishFormTitleUpdatedEvent, publishFormUpdatedEvent, publishLiveCreatedFromDraftEvent, - publishRenamedFormSecretEvent, publishSavedFormSecretEvent } from '~/src/messaging/publish.js' import { saveToS3 } from '~/src/messaging/s3.js' @@ -351,29 +350,6 @@ describe('publish', () => { }) }) - describe('publishRenamedFormSecretEvent', () => { - it('should publish a FORM_SECRET_RENAMED event', async () => { - await publishRenamedFormSecretEvent( - formMetadataDocument, - 'my-new-secret-before', - 'my-new-secret-after', - author - ) - - const [publishEventCall] = jest.mocked(publishEvent).mock.calls[0] - expect(publishEventCall).toMatchObject({ - schemaVersion: AuditEventMessageSchemaVersion.V1, - category: AuditEventMessageCategory.FORM, - type: AuditEventMessageType.FORM_SECRET_RENAMED, - createdBy: author - }) - expect(publishEventCall.data).toMatchObject({ - slug: formMetadataDocument.slug, - secretName: 'my-new-secret-before' - }) - }) - }) - describe('publishSavedFormSecretEvent', () => { it('should publish a FORM_SECRET_SAVED event', async () => { await publishSavedFormSecretEvent( diff --git a/src/routes/secrets.js b/src/routes/secrets.js index 47b7e8ff..cfb21066 100644 --- a/src/routes/secrets.js +++ b/src/routes/secrets.js @@ -6,7 +6,6 @@ import { deleteFormSecret, existsFormSecret, getFormSecret, - renameFormSecret, saveFormSecret } from '~/src/api/forms/service/secrets.js' import { failAction } from '~/src/helpers/fail-action.js' @@ -134,31 +133,6 @@ export default [ failAction } } - }, - { - method: 'PUT', - path: '/forms/{id}/secrets/{nameBefore}/{nameAfter}', - /** - * @param {RequestRenameFormSecret} request - */ - async handler(request) { - const { auth, params } = request - const { id, nameBefore, nameAfter } = params - const author = getAuthor(auth.credentials.user) - - await renameFormSecret(id, nameBefore, nameAfter, author) - - return StatusCodes.OK - }, - options: { - auth: { - scope: [`+${Scopes.FormEdit}`] - }, - validate: { - params: formSecretRenameSchema, - failAction - } - } } ] diff --git a/src/routes/secrets.test.js b/src/routes/secrets.test.js index c61cd934..99bd8222 100644 --- a/src/routes/secrets.test.js +++ b/src/routes/secrets.test.js @@ -2,7 +2,6 @@ import { deleteFormSecret, existsFormSecret, getFormSecret, - renameFormSecret, saveFormSecret } from '~/src/api/forms/service/secrets.js' import { createServer } from '~/src/api/server.js' @@ -83,23 +82,6 @@ describe('Secrets routes', () => { expect(calledValue).toBe('My new secret value') }) - test('Testing PUT /forms/{id}/secrets/{nameBefore}/{nameAfter}', async () => { - const renameSecret = jest.mocked(renameFormSecret) - - const response = await server.inject({ - method: 'PUT', - url: `/forms/${id}/secrets/my-secret-before/my-secret-after`, - auth - }) - - expect(response.statusCode).toEqual(okStatusCode) - expect(response.headers['content-type']).toContain(jsonContentType) - expect(response.result).toBe(okStatusCode) - const [, calledNameBefore, calledNameAfter] = renameSecret.mock.calls[0] - expect(calledNameBefore).toBe('my-secret-before') - expect(calledNameAfter).toBe('my-secret-after') - }) - test('Testing DELETE /forms/{id}/secrets/{name}', async () => { const deleteSecret = jest.mocked(deleteFormSecret) From 3435a33b0af8cd13fecdeeb2e7a462538439b868 Mon Sep 17 00:00:00 2001 From: Jez Barnsley Date: Mon, 9 Mar 2026 08:19:10 +0000 Subject: [PATCH 12/12] Fixed integration tests --- ...ms-manager-ci-mock.postman_collection.json | 102 +----------------- 1 file changed, 5 insertions(+), 97 deletions(-) diff --git a/test/integration/postman/forms-manager-ci-mock.postman_collection.json b/test/integration/postman/forms-manager-ci-mock.postman_collection.json index 6f14adf7..3dea77f0 100644 --- a/test/integration/postman/forms-manager-ci-mock.postman_collection.json +++ b/test/integration/postman/forms-manager-ci-mock.postman_collection.json @@ -1986,99 +1986,7 @@ "response": [] }, { - "name": "Rename secret", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test(\"response is ok\", function () {", - " pm.response.to.have.status(200);", - "});" - ], - "type": "text/javascript", - "packages": {}, - "requests": {} - } - } - ], - "request": { - "method": "PUT", - "header": [], - "body": { - "mode": "raw", - "raw": "", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{root}}/forms/{{go_live_form_id}}/secrets/my-secret/my-secret-renamed", - "host": [ - "{{root}}" - ], - "path": [ - "forms", - "{{go_live_form_id}}", - "secrets", - "my-secret", - "my-secret-renamed" - ] - } - }, - "response": [] - }, - { - "name": "Get renamed secret", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test(\"response is ok\", function () {", - " pm.response.to.have.status(200);", - "});" - ], - "type": "text/javascript", - "packages": {}, - "requests": {} - } - } - ], - "protocolProfileBehavior": { - "disableBodyPruning": true - }, - "request": { - "method": "GET", - "header": [], - "body": { - "mode": "raw", - "raw": "", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{root}}/forms/{{go_live_form_id}}/secrets/my-secret-renamed", - "host": [ - "{{root}}" - ], - "path": [ - "forms", - "{{go_live_form_id}}", - "secrets", - "my-secret-renamed" - ] - } - }, - "response": [] - }, - { - "name": "Delete renamed secret", + "name": "Delete secret", "event": [ { "listen": "test", @@ -2107,7 +2015,7 @@ } }, "url": { - "raw": "{{root}}/forms/{{go_live_form_id}}/secrets/my-secret-renamed", + "raw": "{{root}}/forms/{{go_live_form_id}}/secrets/my-secret", "host": [ "{{root}}" ], @@ -2115,7 +2023,7 @@ "forms", "{{go_live_form_id}}", "secrets", - "my-secret-renamed" + "my-secret" ] } }, @@ -2154,7 +2062,7 @@ } }, "url": { - "raw": "{{root}}/forms/{{go_live_form_id}}/secrets/my-secret-renamed", + "raw": "{{root}}/forms/{{go_live_form_id}}/secrets/my-secret", "host": [ "{{root}}" ], @@ -2162,7 +2070,7 @@ "forms", "{{go_live_form_id}}", "secrets", - "my-secret-renamed" + "my-secret" ] } },