Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 59 additions & 1 deletion designer/server/src/lib/dead-letter-queue.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,9 @@ export function getEndpoint(dlq) {
/**
* @param {DeadLetterQueues} dlq
* @param {string} token
* @param {{ visibilityTimeout: number | undefined, waitTimeSeconds: number | undefined}} [options]
*/
export async function getDeadLetterQueueMessages(dlq, token) {
export async function getDeadLetterQueueMessages(dlq, token, options) {
const getJsonByType = /** @type {typeof getJson<{ messages: any[] }>} */ (
getJson
)
Expand All @@ -55,6 +56,20 @@ export async function getDeadLetterQueueMessages(dlq, token) {

const requestUrl = new URL(`./admin/deadletter${qualifier}/view`, endpoint)

if (options?.visibilityTimeout) {
requestUrl.searchParams.set(
'visibilityTimeout',
options.visibilityTimeout.toString()
)
}

if (options?.waitTimeSeconds) {
requestUrl.searchParams.set(
'waitTimeSeconds',
options.waitTimeSeconds.toString()
)
}

const { body } = await getJsonByType(requestUrl, getHeaders(token))

// Dedupe in case of duplicate messages
Expand All @@ -65,6 +80,49 @@ export async function getDeadLetterQueueMessages(dlq, token) {
return uniqueMessages.values().toArray()
}

/**
* @param {DeadLetterQueues} dlq
* @param {string} messageId
* @param {string} token
* @param {{ visibilityTimeout: number | undefined, waitTimeSeconds: number | undefined}} [options]
* @returns {Promise<any | undefined>}
*/
export async function getDeadLetterQueueMessage(
dlq,
messageId,
token,
options
) {
const getJsonByType = /** @type {typeof getJson<{ message: any }>} */ (
getJson
)

const { endpoint, qualifier } = getEndpoint(dlq)

const requestUrl = new URL(
`./admin/deadletter${qualifier}/view/${messageId}`,
endpoint
)

if (options?.visibilityTimeout) {
requestUrl.searchParams.set(
'visibilityTimeout',
options.visibilityTimeout.toString()
)
}

if (options?.waitTimeSeconds) {
requestUrl.searchParams.set(
'waitTimeSeconds',
options.waitTimeSeconds.toString()
)
}

const { body } = await getJsonByType(requestUrl, getHeaders(token))

return body.message ?? undefined
}

/**
* @param {DeadLetterQueues} dlq
* @param {string} token
Expand Down
57 changes: 57 additions & 0 deletions designer/server/src/lib/dead-letter-queue.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { DeadLetterQueues } from '@defra/forms-model'

import {
deleteDeadLetterQueueMessage,
getDeadLetterQueueMessage,
getDeadLetterQueueMessages,
getEndpoint,
redriveDeadLetterQueueMessages,
Expand Down Expand Up @@ -60,6 +61,62 @@ describe('dead-letter queue lib functions', () => {
)
expect(res).toEqual(['message1'])
})

it('should call endpoint with extra query params', async () => {
jest
.mocked(getJson)
// @ts-expect-error - partial mock of response
.mockResolvedValueOnce({ body: { messages: ['message1'] } })
const dlq = DeadLetterQueues.SubmissionsApiFormSubmissions
const res = await getDeadLetterQueueMessages(dlq, 'token', {
visibilityTimeout: 5,
waitTimeSeconds: 10
})
expect(getJson).toHaveBeenCalledWith(
new URL(
'http://localhost:3002/admin/deadletter/form-submissions/view?visibilityTimeout=5&waitTimeSeconds=10'
),
expect.anything()
)
expect(res).toEqual(['message1'])
})
})

describe('getDeadLetterQueueMessage', () => {
it('should call endpoint', async () => {
jest
.mocked(getJson)
// @ts-expect-error - partial mock of response
.mockResolvedValueOnce({ body: { message: 'message1' } })
const dlq = DeadLetterQueues.SubmissionsApiFormSubmissions
const res = await getDeadLetterQueueMessage(dlq, 'message-id', 'token')
expect(getJson).toHaveBeenCalledWith(
new URL(
'http://localhost:3002/admin/deadletter/form-submissions/view/message-id'
),
expect.anything()
)
expect(res).toBe('message1')
})

it('should call endpoint with extra query params', async () => {
jest
.mocked(getJson)
// @ts-expect-error - partial mock of response
.mockResolvedValueOnce({ body: { message: 'message1' } })
const dlq = DeadLetterQueues.SubmissionsApiFormSubmissions
const res = await getDeadLetterQueueMessage(dlq, 'message-id', 'token', {
visibilityTimeout: 5,
waitTimeSeconds: 10
})
expect(getJson).toHaveBeenCalledWith(
new URL(
'http://localhost:3002/admin/deadletter/form-submissions/view/message-id?visibilityTimeout=5&waitTimeSeconds=10'
),
expect.anything()
)
expect(res).toBe('message1')
})
})

describe('redriveDeadLetterQueueMessages', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,11 @@ exports[`Dead-letter queues routes Journey should render form with messages and
<div class="json-item" data-rootCopyToClip>
<div class="action-links-top-right">
<a class="govuk-link govuk-link--no-visited-state govuk-!-margin-right-9 govuk-visually-hidden" href="#" data-linkCopyToClip>Copy to clipboard</a>
<a class="govuk-link govuk-link--no-visited-state govuk-!-margin-right-9" href="audit-api/modify/message-id">Modify and resubmit</a>
<form method="post" action="audit-api/modify-redirect/message-id" class="app-inline-block-form govuk-!-margin-right-9">
<input type="hidden" name="action" value="modify" />
<input type="hidden" name="messageJsonText" value="{&quot;MessageId&quot;:&quot;message-id&quot;,&quot;Body&quot;:{&quot;field1&quot;:&quot;value1&quot;}}" />
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bit confused by this, why would we hardcode a value?

{
  "MessageId": "message-id",
  "Body": {
    "field1": "value1"
  }
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a snapshot for a unit test where that message content is setup in the unit test.
The actual NJK file contains dynamic content:

<input type="hidden" name="messageJsonText" value="{{ message.json | dump }}" />

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh so it is !! my bad, i saw this as the actual nunjucks view.

<button type="submit" class="govuk-link govuk-link--no-visited-state">Modify and resubmit</button>
</form>
<form method="post" action="audit-api/delete" class="app-inline-block-form">
<input type="hidden" name="action" value="confirm" />
<input type="hidden" name="messageId" value="message-id" />
Expand Down Expand Up @@ -844,18 +848,13 @@ exports[`Dead-letter queues routes Journey should render modify form with messag

<h2 class="govuk-heading-m">audit-api: Modifying message</h2>

<form method="post">
<form method="post" action="/admin/dead-letter-queues/audit-api/modify/message-id">
<div class="govuk-form-group">

<div id="message-json-hint" class="govuk-hint">
You can leave the &#39;MessageId&#39; value unchanged - it will get a new value when you resubmit
</div>
<textarea class="govuk-textarea" id="message-json" name="messageJson" rows="30" aria-describedby="message-json-hint">{
&quot;MessageId&quot;: &quot;message-id&quot;,
&quot;Body&quot;: {
&quot;field1&quot;: &quot;value1&quot;
}
}</textarea>
<textarea class="govuk-textarea" id="message-json" name="messageJson" rows="30" aria-describedby="message-json-hint">{}</textarea>
</div>


Expand Down
81 changes: 81 additions & 0 deletions designer/server/src/routes/admin/dead-letter-queue-helper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { formAdapterSubmissionMessagePayloadSchema } from '@defra/forms-engine-plugin/engine/types/schema.js'

import { createJoiError } from '~/src/lib/error-boom-helper.js'

/**
* @param {string} messageJson
*/
export function validateMessageJson(messageJson) {
let json
try {
json = JSON.parse(messageJson)
} catch (err) {
const typedError = /** @type {{ message?: string }} */ (err)
return {
error: createJoiError(
'messageJson',
`Invalid JSON: ${typedError.message}`
)
}
}

/**
* @type { FormAdapterSubmissionMessagePayload | undefined }
*/
const messageBody = json.Body
if (!messageBody) {
return {
error: createJoiError(
'messageJson',
'Invalid JSON: Missing "Body" element'
)
}
}

const { error } = formAdapterSubmissionMessagePayloadSchema.validate(
messageBody,
{
abortEarly: false,
stripUnknown: true
}
)
if (error) {
const errorText = error.details.map((d) => d.message).join(', ')
const joiError = createJoiError(
'messageJson',
`JSON does not match the schema: ${errorText}`
)
return {
error: joiError
}
}
return {
body: messageBody
}
}

/**
* Keep specific properties
* @param {any} message
* @returns {{ json: { MessageId: string, Body: any }}}
*/
export function dlqMessageMapper(message) {
return {
json: {
MessageId: message.MessageId,
Body: JSON.parse(message.Body)
}
}
}

/**
* @param {any[]} messages
* @returns {{ json: { MessageId: string, Body: any }}[]}
*/
export function dlqMessagesMapper(messages) {
return messages.map(dlqMessageMapper)
}

/**
* @import { FormAdapterSubmissionMessagePayload } from '@defra/forms-engine-plugin/engine/types.js'
*/
Loading
Loading