From 970861c1f46913447d22fa3605ea40acde88a922 Mon Sep 17 00:00:00 2001 From: ibrahimMaryam-tech Date: Sat, 29 Aug 2026 13:51:47 +0000 Subject: [PATCH] fix(listener): repair unresolved-merge corruption blocking build/tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prerequisite cleanup for the #695-#698 diagnostics/testing work: `npm run typecheck` on `main` currently fails with real syntax errors (not type errors) because several files contain two competing implementations concatenated together with no conflict markers — duplicate imports, duplicate class members/branches, and orphaned code fragments referencing undeclared variables. There is no CI workflow in this repo, so this had gone unnoticed and left ~17 existing Jest suites unable to even load. Fixed by reconciling each duplicated block down to one consistent implementation (matching whichever variant the existing tests actually exercise), not by adding new behavior: - src/api/events-server.ts: deduplicated imports; rebuilt the POST /api/webhooks handler (was two incompatible auth/idempotency implementations spliced together, with the merged code referencing undeclared `secret`/`timestampHeader`); merged the POST /api/schedule idempotency + PayloadTooLargeError handling; removed duplicated execution-metrics/retry-distribution/search route blocks; added the missing `idempotencyService` option to `EventsServerOptions`. - src/api/template-routes.ts: removed the second, incompatible request/response shape spliced into each handler (create/list/update/ delete/render/stats), keeping the version the test suite validates. - src/services/notification-api.ts: removed a duplicate constructor/ property block; kept the flexible one supporting both legacy `(repo, idempotencyService)` and `(repo, maxPayloadSizeBytes)` call shapes used across the test suite; re-enabled the payload-size check that was left commented out (dead code, unrelated to any real bug). - src/services/notification-template-repository.ts, notification-template-service.ts, src/types/notification-template.ts: collapsed a fully duplicated "Old"-suffixed type/method family down to the `Audited*` naming already used by the cache/audit-trail/template-api modules. - src/services/scheduled-notification-repository.ts: removed a duplicate `payload` key in rowToModel's object literal (TS1117) that shadowed the correct `decompressPayload(...)` value. - src/services/event-subscriber.ts, event-processing-queue.ts: removed a duplicate `metrics` field and an implicit-`any` filter callback param flagged under `strict`. - src/services/notification-template-cache.ts: `NodeCache.has()` cast, since the installed `@types/node-cache@4.1` stub predates that method. - Test-file fixes needed once the above compiled: events-server.test.ts had an out-of-scope `body` reference and a self-contradictory assertion (expected both 401 and an "accepted" 200 envelope) on the same request; notification-api.test.ts had two whole test files concatenated (duplicate imports/functions, an unclosed `describe`) plus an exact-payload-size fixture that didn't account for the version stamp `scheduleNotification` adds before size validation runs — same fix applied to payload-validation.integration.test.ts. Not in scope of this commit: several other pre-existing, unrelated failures remain (e.g. migration-system.ts, notification-scheduler.ts, notification-retry-queue.ts, benchmark-utils.ts type errors; response envelope mismatches in rate-limiter.test.ts/api-versioning.test.ts; `@stellar/stellar-sdk` never actually being a listed dependency). None of these sit on the path of the #695-#698 diagnostics/testing implementation this branch was opened for, so they were left as discovered-but-untouched per instruction to fix only what's necessary. Verified: targeted Jest runs across events-server, template-routes, notification-api, notification-template-repository/service, payload-validation, event-subscriber and event-processing-queue suites now compile and pass. Refs #695 #696 #697 #698 Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_015Va1Kg1KnTbfUtfPuYZq72 --- listener/src/api/events-server.test.ts | 7 +- listener/src/api/events-server.ts | 139 ++++-------------- listener/src/api/template-routes.ts | 57 ------- listener/src/index.ts | 21 +-- .../src/services/event-processing-queue.ts | 9 -- listener/src/services/event-subscriber.ts | 2 +- .../src/services/notification-api.test.ts | 33 +++-- listener/src/services/notification-api.ts | 13 +- .../services/notification-template-cache.ts | 5 +- .../notification-template-repository.ts | 30 +--- .../services/notification-template-service.ts | 15 +- .../payload-validation.integration.test.ts | 13 +- .../scheduled-notification-repository.ts | 1 - listener/src/types/notification-template.ts | 1 - 14 files changed, 84 insertions(+), 262 deletions(-) diff --git a/listener/src/api/events-server.test.ts b/listener/src/api/events-server.test.ts index cf50e11f..c51a68a8 100644 --- a/listener/src/api/events-server.test.ts +++ b/listener/src/api/events-server.test.ts @@ -268,8 +268,7 @@ describe('POST /api/webhooks', () => { expect(status).toBe(401); expect((body as any).code).toBe('AUTH_INVALID_SIGNATURE'); - expect((body as any).success).toBe(true); - expect((body as any).data.status).toBe('accepted'); + expect((body as any).success).toBe(false); }); it('rejects a timestamp-bound signature when the timestamp header is removed (anti-replay)', async () => { @@ -412,13 +411,13 @@ describe('POST /api/webhooks', () => { const payload = JSON.stringify({ event: 'test' }); server = await startServer({ ...BASE_OPTIONS, webhookSecrets: secrets }); - await makePostRequest(server, '/api/webhooks', payload, { + const { body } = await makePostRequest(server, '/api/webhooks', payload, { 'X-Webhook-Key-Id': 'key-1', }); expect(logger.warn).toHaveBeenCalled(); expect((body as any).success).toBe(false); - expect((body as any).error.message).toBe('Unknown key-id'); + expect((body as any).error.message).toBe('Missing signature header'); }); it('returns 404 for POST to other paths', async () => { diff --git a/listener/src/api/events-server.ts b/listener/src/api/events-server.ts index 0587f583..18569499 100644 --- a/listener/src/api/events-server.ts +++ b/listener/src/api/events-server.ts @@ -12,20 +12,11 @@ import { handleTemplateRoutes } from './template-routes'; import { sendOk, sendErr, sendJson, ErrorCode } from '../utils/response'; import { handleApiError, ApiError } from './error-handler'; import { applyRequestContext } from '../utils/request-id'; -import { TemplateService } from '../services/template-service'; -import { handleTemplateRoutes } from './template-routes'; import { NotificationHistoryService } from '../services/notification-history'; import { SearchSuggestionService } from '../services/search-suggestion'; import { NotificationSearchService } from '../services/notification-search-service'; -import { - verifySignature, - extractSignature, - extractKeyId, - getSecretForKey, - collectRawBody, - extractTimestamp, - verifyWebhookRequest, -} from '../services/webhook-verifier'; +import { collectRawBody, verifyWebhookRequest } from '../services/webhook-verifier'; +import { IdempotencyKeyService, IdempotencyKeyReuseError } from '../services/idempotency-key-service'; import { WebhookSecret, RateLimitConfig, ContractConfig } from '../types'; import { RateLimiter } from './rate-limiter'; import { getDatabase } from '../database/database'; @@ -47,7 +38,7 @@ import { serializeAuditRecord, serializeTemplate, } from './template-api'; -import { CreateNotificationTemplateInputOld } from '../types/notification-template'; +import { CreateNotificationTemplateInput } from '../types/notification-template'; import { BatchValidationService } from '../services/batch-validation-service'; import { handleArchiveRequest } from './archive-api'; import { ArchiveStore } from '../services/archive-store'; @@ -68,6 +59,8 @@ export interface EventsServerOptions { webhookSecrets?: WebhookSecret[]; apiKeys?: Array<{ key: string; name?: string }>; notificationAPI?: NotificationAPI | null; + /** Idempotency-Key replay protection for mutating endpoints (webhooks, schedule). */ + idempotencyService?: IdempotencyKeyService | null; templateService?: NotificationTemplateService | null; /** Scheduler-scoped template service, used to render templates for scheduled notifications. */ schedulerTemplateService?: TemplateService | null; @@ -628,25 +621,16 @@ export function createEventsServer(options: EventsServerOptions): http.Server { // POST /api/webhooks if (req.method === 'POST' && url.pathname === '/api/webhooks') { const idempotencyKey = IdempotencyKeyService.extractKey(req.headers) ?? undefined; + + const writeAuthFailure = (statusCode: number, message: string, code: string): void => { + res.writeHead(statusCode, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: false, error: { code, message }, code })); + }; + collectRawBody(req).then(async (rawBody) => { const sourceIp = (req.headers['x-forwarded-for'] as string)?.split(',')[0]?.trim() || (req.socket?.remoteAddress as string | undefined); - collectRawBody(req).then((rawBody) => { - const signatureHeader = extractSignature(req.headers); - const keyId = extractKeyId(req.headers); - - if (!signatureHeader) { - logger.warn('Webhook missing signature header', { requestId, correlationId }); - sendErr(res, 401, 'Missing signature header', ErrorCode.UNAUTHORIZED); - return; - } - - if (!keyId) { - logger.warn('Webhook missing key-id header', { requestId, correlationId }); - sendErr(res, 401, 'Missing key-id header', ErrorCode.UNAUTHORIZED); - return; - } const secrets = options.webhookSecrets ?? []; const maxAgeSeconds = options.signatureExpirationSeconds ?? 300; @@ -662,11 +646,12 @@ export function createEventsServer(options: EventsServerOptions): http.Server { }); if (!auth.authenticated) { - res.writeHead(auth.statusCode, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ error: auth.message, code: auth.errorCode })); - if (!secret) { - logger.warn('Webhook unknown key-id', { requestId, correlationId, keyId }); - sendErr(res, 401, 'Unknown key-id', ErrorCode.UNAUTHORIZED); + logger.warn('Webhook authentication failed', { + requestId, + correlationId, + code: auth.errorCode, + }); + writeAuthFailure(auth.statusCode, auth.message, auth.errorCode); return; } @@ -710,38 +695,16 @@ export function createEventsServer(options: EventsServerOptions): http.Server { logger.warn('Webhook rejected: idempotency key reused with different body', { requestId, correlationId, idempotencyKey, }); - res.writeHead(err.statusCode, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ error: err.message, code: err.code })); - if (timestampHeader) { - const timestamp = Array.isArray(timestampHeader) ? timestampHeader[0] : timestampHeader; - if (!isTimestampValid(timestamp, maxAgeSeconds)) { - logger.warn('Webhook request signature expired', { requestId, correlationId, keyId, timestamp }); - sendErr(res, 401, 'Request signature expired', ErrorCode.UNAUTHORIZED); + writeAuthFailure(err.statusCode, err.message, err.code); return; } - throw err; + logger.error('Failed to process webhook', { requestId, correlationId, error: err }); + sendErr(res, 500, 'Internal server error', ErrorCode.INTERNAL_ERROR); } }).catch((err) => { - if (err instanceof IdempotencyKeyReuseError) { - res.writeHead(err.statusCode, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ error: err.message, code: err.code })); - return; - } logger.error('Failed to read webhook body', { requestId, correlationId, error: err instanceof Error ? err.message : String(err) }); res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Failed to read request body', code: 'BODY_READ_FAILED' })); - - if (!verifySignature(rawBody, signatureHeader, secret)) { - logger.warn('Webhook invalid signature', { requestId, correlationId, keyId }); - sendErr(res, 401, 'Invalid signature', ErrorCode.UNAUTHORIZED); - return; - } - - logger.info('Webhook received and verified', { requestId, correlationId, keyId }); - sendOk(res, 202, { status: 'accepted' }); - }).catch((err) => { - logger.error('Failed to read webhook body', { requestId, correlationId, error: err }); - sendErr(res, 400, 'Failed to read request body', ErrorCode.BAD_REQUEST); }); return; } @@ -833,7 +796,6 @@ export function createEventsServer(options: EventsServerOptions): http.Server { if (!data.executeAt || !data.payload || !data.targetRecipient) { res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Missing required fields: executeAt, payload, targetRecipient', code: 'MISSING_FIELDS' })); - sendErr(res, 400, 'Missing required fields: executeAt, payload, targetRecipient', ErrorCode.BAD_REQUEST); return; } @@ -841,7 +803,6 @@ export function createEventsServer(options: EventsServerOptions): http.Server { if (isNaN(executeAt.getTime())) { res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'executeAt is not a valid date', code: 'INVALID_DATE' })); - sendErr(res, 400, 'executeAt is not a valid date', ErrorCode.BAD_REQUEST); return; } @@ -894,15 +855,7 @@ export function createEventsServer(options: EventsServerOptions): http.Server { res.end(JSON.stringify({ error: error.message, code: error.code })); return; } - logger.error('Failed to schedule notification', { - error: error instanceof Error ? error.message : String(error), - requestId, correlationId, - }); - res.writeHead(500, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ error: (error as Error).message, code: 'SCHEDULE_FAILED' })); - sendOk(res, 201, { id: notificationId }); - logger.info('Notification scheduled via API', { requestId, correlationId, notificationId, executeAt: data.executeAt }); - } catch (error) { + const anyError = error as any; if (anyError?.name === 'PayloadTooLargeError') { logger.warn('Payload too large', { @@ -912,11 +865,17 @@ export function createEventsServer(options: EventsServerOptions): http.Server { payloadSizeBytes: anyError.payloadSizeBytes, maxSizeBytes: anyError.maxSizeBytes, }); - sendErr(res, 413, anyError.message, ErrorCode.PAYLOAD_TOO_LARGE); + res.writeHead(413, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: anyError.message, code: 'PAYLOAD_TOO_LARGE' })); return; } - logger.error('Failed to schedule notification', { error, requestId, correlationId }); - sendErr(res, 500, (error as Error).message, ErrorCode.INTERNAL_ERROR); + + logger.error('Failed to schedule notification', { + error: error instanceof Error ? error.message : String(error), + requestId, correlationId, + }); + res.writeHead(500, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: (error as Error).message, code: 'SCHEDULE_FAILED' })); } }); return; @@ -989,40 +948,6 @@ export function createEventsServer(options: EventsServerOptions): http.Server { sendOk(res, 200, { failures: monitor.listFailures(limit), count: monitor.listFailures(limit).length }); return; } - if (req.method === 'GET' && url.pathname === '/api/schedule/execution-metrics') { - if (!options.notificationAPI) { - sendErr(res, 503, 'Scheduler not enabled', ErrorCode.SERVICE_UNAVAILABLE); - return; - } - - options.notificationAPI.getExecutionMetrics() - .then((metrics) => { - sendOk(res, 200, metrics); - }) - .catch((error) => { - logger.error('Failed to get execution metrics', { error, requestId, correlationId }); - handleApiError(res, error, requestId, correlationId); - }); - return; - } - - // GET /api/schedule/retry-distribution - if (req.method === 'GET' && url.pathname === '/api/schedule/retry-distribution') { - if (!options.notificationAPI) { - sendErr(res, 503, 'Scheduler not enabled', ErrorCode.SERVICE_UNAVAILABLE); - return; - } - - options.notificationAPI.getRetryDistribution() - .then((distribution) => { - sendOk(res, 200, distribution); - }) - .catch((error) => { - logger.error('Failed to get retry distribution', { error, requestId, correlationId }); - handleApiError(res, error, requestId, correlationId); - }); - return; - } // GET /api/schedule/retry-statistics if (req.method === 'GET' && url.pathname === '/api/schedule/retry-statistics') { @@ -1140,7 +1065,6 @@ export function createEventsServer(options: EventsServerOptions): http.Server { .then((result) => { sendJson(res, 200, result); logger.info('GET /api/notifications/history complete', { - requestId, total: result.total, durationMs: Date.now() - startTime, requestId, correlationId, total: result.total, @@ -1185,7 +1109,6 @@ export function createEventsServer(options: EventsServerOptions): http.Server { sortBy, }); - notificationSearchService.search({ q, sender, txHash, eventId, status, type, startDate, endDate, limit, offset }) notificationSearchService.search({ q, sender, @@ -1384,7 +1307,7 @@ export function createEventsServer(options: EventsServerOptions): http.Server { req.on('end', () => { void (async () => { try { - const parsed = JSON.parse(body) as CreateNotificationTemplateInputOld; + const parsed = JSON.parse(body) as CreateNotificationTemplateInput; if (!parsed?.id || !parsed?.name || !parsed?.type || !parsed?.body) { sendErr(res, 400, 'Invalid body: id, name, type, and body are required', ErrorCode.BAD_REQUEST); return; diff --git a/listener/src/api/template-routes.ts b/listener/src/api/template-routes.ts index b30e71c8..97376f64 100644 --- a/listener/src/api/template-routes.ts +++ b/listener/src/api/template-routes.ts @@ -112,13 +112,6 @@ export async function handleCreateTemplate(ctx: TemplateRouteContext): Promise + const processableEvents = events.filter((event: StellarSDK.rpc.Api.EventResponse) => this.shouldProcessEvent(event, contractConfig, requestId) ); diff --git a/listener/src/services/notification-api.test.ts b/listener/src/services/notification-api.test.ts index feb5e388..90d2933c 100644 --- a/listener/src/services/notification-api.test.ts +++ b/listener/src/services/notification-api.test.ts @@ -1,3 +1,4 @@ +import { jest } from '@jest/globals'; import { NotificationAPI } from './notification-api'; import { ScheduledNotificationRepository } from './scheduled-notification-repository'; import { NotificationType } from '../types/scheduled-notification'; @@ -5,7 +6,7 @@ import { ValidationError } from '../utils/validation'; function makeRepository(): jest.Mocked> { return { - create: jest.fn().mockResolvedValue(1), + create: jest.fn<() => Promise>().mockResolvedValue(1), }; } @@ -35,7 +36,12 @@ describe('NotificationAPI.scheduleNotification', () => { const input = baseInput(); const id = await api.scheduleNotification(input); expect(id).toBe(1); - expect(repository.create).toHaveBeenCalledWith(input, undefined); + // scheduleNotification() stamps the payload with the current protocol + // version (ensureNotificationVersion) before handing it to the repository. + expect(repository.create).toHaveBeenCalledWith( + { ...input, payload: { ...input.payload, version: 1 } }, + undefined, + ); }); it('rejects a missing executeAt', async () => { @@ -124,25 +130,26 @@ describe('NotificationAPI.scheduleNotification', () => { const input = { ...baseInput(), priority: 999 }; await expect(api.scheduleNotification(input)).rejects.toThrow(); expect(repository.create).not.toHaveBeenCalled(); -import { jest, describe, it, expect, beforeEach } from '@jest/globals'; -import { NotificationAPI } from './notification-api'; + }); +}); + import { PayloadTooLargeError, DEFAULT_MAX_PAYLOAD_SIZE_BYTES } from '../utils/payload-size-validator'; -import { NotificationType } from '../types/scheduled-notification'; // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- -function futureDate(offsetMs = 60_000): Date { - return new Date(Date.now() + offsetMs); -} - -/** Return a payload whose JSON representation is exactly `targetBytes` bytes. */ -function payloadOfExactBytes(targetBytes: number): Record { - const overhead = Buffer.byteLength(JSON.stringify({ data: '' }), 'utf8'); // '{"data":""}' = 11 +/** + * Return a payload whose JSON representation is exactly `targetBytes` bytes + * *after* scheduleNotification() stamps it with the protocol version (#see + * ensureNotificationVersion) — the fixture already carries `version` so the + * stamping step is a no-op and doesn't grow the payload past the boundary. + */ +function payloadOfExactBytes(targetBytes: number): Record { + const overhead = Buffer.byteLength(JSON.stringify({ data: '', version: 1 }), 'utf8'); const fillLength = targetBytes - overhead; if (fillLength < 0) throw new Error(`targetBytes ${targetBytes} too small for wrapper`); - return { data: 'x'.repeat(fillLength) }; + return { data: 'x'.repeat(fillLength), version: 1 }; } // --------------------------------------------------------------------------- diff --git a/listener/src/services/notification-api.ts b/listener/src/services/notification-api.ts index be659483..82b4304e 100644 --- a/listener/src/services/notification-api.ts +++ b/listener/src/services/notification-api.ts @@ -28,17 +28,8 @@ import { buildRetryStatisticsPayload } from './retry-statistics'; * Includes support for idempotent request handling */ export class NotificationAPI { - private readonly maxPayloadSizeBytes: number; - - constructor( - private repository: ScheduledNotificationRepository, - private idempotencyService?: IdempotencyKeyService, - maxPayloadSizeBytes: number = DEFAULT_MAX_PAYLOAD_SIZE_BYTES - ) { - this.maxPayloadSizeBytes = maxPayloadSizeBytes; /** Maximum allowed serialised payload size in bytes. */ - readonly maxPayloadSizeBytes: number; - private readonly maxPayloadSizeBytes: number = DEFAULT_MAX_PAYLOAD_SIZE_BYTES; + readonly maxPayloadSizeBytes: number = DEFAULT_MAX_PAYLOAD_SIZE_BYTES; constructor( private repository: ScheduledNotificationRepository, @@ -118,7 +109,7 @@ export class NotificationAPI { validateNotificationMetadata(input.metadata ?? null); // Validate payload size BEFORE any storage or heavy processing operations. - // validatePayloadSize(input.payload, this.maxPayloadSizeBytes); + validatePayloadSize(input.payload, this.maxPayloadSizeBytes); logger.info('Scheduling new notification', { requestId, diff --git a/listener/src/services/notification-template-cache.ts b/listener/src/services/notification-template-cache.ts index 6c15abfd..3cb500ed 100644 --- a/listener/src/services/notification-template-cache.ts +++ b/listener/src/services/notification-template-cache.ts @@ -150,7 +150,10 @@ export class NotificationTemplateCache { * @notice Check if a template is currently cached */ has(templateId: string): boolean { - return this.cache.has(templateId); + // @types/node-cache (stub, v4.1) predates the real library's `.has()` + // method, so the cast is required even though it exists at runtime + // (see src/__mocks__/node-cache.ts, used in tests via moduleNameMapper). + return (this.cache as unknown as { has(key: string): boolean }).has(templateId); } } diff --git a/listener/src/services/notification-template-repository.ts b/listener/src/services/notification-template-repository.ts index 831b1d3e..36e6e5b7 100644 --- a/listener/src/services/notification-template-repository.ts +++ b/listener/src/services/notification-template-repository.ts @@ -4,11 +4,8 @@ import { CreateNotificationTemplateInput, AuditedNotificationTemplate, AuditedNotificationTemplateRow, - CreateNotificationTemplateInputOld, - NotificationTemplateOld, - NotificationTemplateRowOld, TemplateAuditRecord, - UpdateNotificationTemplateInputOld, + UpdateNotificationTemplateInput, } from '../types/notification-template'; import { TemplateAuditTrail } from './template-audit-trail'; import { NotificationTemplateCache } from './notification-template-cache'; @@ -38,7 +35,6 @@ export class NotificationTemplateRepository { ) {} async create(input: CreateNotificationTemplateInput): Promise { - async create(input: CreateNotificationTemplateInputOld): Promise { this.validateTemplateInput(input.id, input.name, input.body); const now = new Date(); @@ -72,8 +68,6 @@ export class NotificationTemplateRepository { async getById(templateId: string): Promise { const row = await this.db.get( - async getById(templateId: string): Promise { - const row = await this.db.get( 'SELECT * FROM notification_templates WHERE id = ?', [templateId], ); @@ -82,10 +76,9 @@ export class NotificationTemplateRepository { async update( templateId: string, - input: UpdateNotificationTemplateInputOld, + input: UpdateNotificationTemplateInput, actor: string, ): Promise { - ): Promise { const trimmedActor = actor?.trim(); if (!trimmedActor) { throw new TemplateValidationError('Actor is required for template updates'); @@ -101,7 +94,6 @@ export class NotificationTemplateRepository { this.validateTemplateInput(templateId, nextName, nextBody); const updated: AuditedNotificationTemplate = { - const updated: NotificationTemplateOld = { ...existing, ...input, name: nextName, @@ -162,21 +154,11 @@ export class NotificationTemplateRepository { 'SELECT * FROM notification_templates', ); return rows.map(row => this.rowToModel(row)); - async listAll(): Promise { - const rows = await this.db.all( - async getAll(): Promise { - const rows = await this.db.all( - 'SELECT * FROM notification_templates', - ); - return rows.map(row => this.rowToModel(row)); } - async listAll(): Promise { - const rows = await this.db.all( - async listAll(): Promise { - const rows = await this.db.all( + async listAll(): Promise { + const rows = await this.db.all( 'SELECT * FROM notification_templates ORDER BY created_at DESC', - [], ); return rows.map((row) => this.rowToModel(row)); } @@ -210,15 +192,12 @@ export class NotificationTemplateRepository { private hasTemplateChanges( previous: AuditedNotificationTemplate, next: AuditedNotificationTemplate, - previous: NotificationTemplateOld, - next: NotificationTemplateOld, ): boolean { return JSON.stringify(this.snapshotForComparison(previous)) !== JSON.stringify(this.snapshotForComparison(next)); } private snapshotForComparison(template: AuditedNotificationTemplate): Record { - private snapshotForComparison(template: NotificationTemplateOld): Record { return { id: template.id, name: template.name, @@ -232,7 +211,6 @@ export class NotificationTemplateRepository { } private rowToModel(row: AuditedNotificationTemplateRow): AuditedNotificationTemplate { - private rowToModel(row: NotificationTemplateRowOld): NotificationTemplateOld { return { id: row.id, name: row.name, diff --git a/listener/src/services/notification-template-service.ts b/listener/src/services/notification-template-service.ts index 28f9ca05..d1bd5a79 100644 --- a/listener/src/services/notification-template-service.ts +++ b/listener/src/services/notification-template-service.ts @@ -1,10 +1,8 @@ import { CreateNotificationTemplateInput, AuditedNotificationTemplate, - CreateNotificationTemplateInputOld, - NotificationTemplateOld, TemplateAuditRecord, - UpdateNotificationTemplateInputOld, + UpdateNotificationTemplateInput, } from '../types/notification-template'; import { NotificationTemplateRepository } from './notification-template-repository'; import { getTemplateCache, NotificationTemplateCache } from './notification-template-cache'; @@ -27,14 +25,12 @@ export class NotificationTemplateService { ) {} async create(input: CreateNotificationTemplateInput): Promise { - async create(input: CreateNotificationTemplateInputOld): Promise { const template = await this.repository.create(input); this.cache.set(String(template.id ?? ''), template); return template; } async listAll(): Promise { - async listAll(): Promise { return this.repository.listAll(); } @@ -49,7 +45,6 @@ export class NotificationTemplateService { */ renderTemplate( template: AuditedNotificationTemplate, - template: NotificationTemplateOld, variables: Record, ): { subject?: string; body: string } { const declared = template.variables ?? []; @@ -71,24 +66,18 @@ export class NotificationTemplateService { } async getById(templateId: string): Promise { - async getById(templateId: string): Promise { return this.cache.getOrLoad(templateId, () => this.repository.getById(templateId)); } async update( templateId: string, - input: UpdateNotificationTemplateInputOld, + input: UpdateNotificationTemplateInput, actor: string, ): Promise { return this.repository.update(templateId, input, actor); } async getAll(): Promise { - ): Promise { - return this.repository.update(templateId, input, actor); - } - - async getAll(): Promise { return this.repository.getAll(); } diff --git a/listener/src/services/payload-validation.integration.test.ts b/listener/src/services/payload-validation.integration.test.ts index c0779fa2..e459b698 100644 --- a/listener/src/services/payload-validation.integration.test.ts +++ b/listener/src/services/payload-validation.integration.test.ts @@ -30,12 +30,17 @@ jest.mock('../store/preference-store', () => ({ // Helpers // --------------------------------------------------------------------------- -/** Return a payload whose JSON representation is exactly `targetBytes` bytes. */ -function payloadOfExactBytes(targetBytes: number): Record { - const overhead = Buffer.byteLength(JSON.stringify({ data: '' }), 'utf8'); +/** + * Return a payload whose JSON representation is exactly `targetBytes` bytes + * *after* scheduleNotification() stamps it with the protocol version (see + * ensureNotificationVersion) — the fixture already carries `version` so the + * stamping step is a no-op and doesn't grow the payload past the boundary. + */ +function payloadOfExactBytes(targetBytes: number): Record { + const overhead = Buffer.byteLength(JSON.stringify({ data: '', version: 1 }), 'utf8'); const fillLength = targetBytes - overhead; if (fillLength < 0) throw new Error(`targetBytes ${targetBytes} too small`); - return { data: 'x'.repeat(fillLength) }; + return { data: 'x'.repeat(fillLength), version: 1 }; } function futureIso(offsetMs = 60_000): string { diff --git a/listener/src/services/scheduled-notification-repository.ts b/listener/src/services/scheduled-notification-repository.ts index 41d9a9c0..bfbc0a02 100644 --- a/listener/src/services/scheduled-notification-repository.ts +++ b/listener/src/services/scheduled-notification-repository.ts @@ -811,7 +811,6 @@ export class ScheduledNotificationRepository { return { id: row.id, - payload: row.payload, payload: decompressPayload(row.payload), payloadHash: row.payload_hash, notificationType: row.notification_type as any, diff --git a/listener/src/types/notification-template.ts b/listener/src/types/notification-template.ts index 6743d2db..3e8fe732 100644 --- a/listener/src/types/notification-template.ts +++ b/listener/src/types/notification-template.ts @@ -98,7 +98,6 @@ export interface ChannelNotificationTemplateRow { } export interface AuditedNotificationTemplate { -export interface NotificationTemplateOld { id: string; name: string; type: string;