From 525e720b986c57a86c20336eb823f039d5d10ebb Mon Sep 17 00:00:00 2001 From: Priyanshubhartistm Date: Wed, 12 Aug 2026 20:04:01 +0530 Subject: [PATCH 1/2] feat(dvm): trap NIP-90 job request events and record them via job repository Signed-off-by: Priyanshubhartistm --- .changeset/dvm-job-ingestion.md | 5 +++ .knip.json | 1 - src/constants/base.ts | 3 ++ src/factories/event-strategy-factory.ts | 16 +++++-- src/factories/message-handler-factory.ts | 18 +++++++- src/factories/websocket-adapter-factory.ts | 17 +++++++- src/factories/worker-factory.ts | 10 ++++- .../dvm-job-request-event-strategy.ts | 42 +++++++++++++++++++ src/utils/event.ts | 30 ++++++++----- 9 files changed, 121 insertions(+), 21 deletions(-) create mode 100644 .changeset/dvm-job-ingestion.md create mode 100644 src/handlers/event-strategies/dvm-job-request-event-strategy.ts diff --git a/.changeset/dvm-job-ingestion.md b/.changeset/dvm-job-ingestion.md new file mode 100644 index 00000000..f0117531 --- /dev/null +++ b/.changeset/dvm-job-ingestion.md @@ -0,0 +1,5 @@ +--- +"nostream": minor +--- + +feat(dvm): trap NIP-90 job request events (kind 5000-5999) and record them via the job repository diff --git a/.knip.json b/.knip.json index f47eb4b9..7129b278 100644 --- a/.knip.json +++ b/.knip.json @@ -17,7 +17,6 @@ "ignore": [ ".nostr/**", "src/repositories/invite-code-repository.ts", - "src/repositories/dvm-job-repository.ts", "src/utils/relay-probe/**" ], "commitlint": false, diff --git a/src/constants/base.ts b/src/constants/base.ts index 200dd478..0715ecc0 100644 --- a/src/constants/base.ts +++ b/src/constants/base.ts @@ -41,6 +41,9 @@ export enum EventKinds { // Lightning zaps ZAP_REQUEST = 9734, ZAP_RECEIPT = 9735, + // NIP-90: Data Vending Machines — job request events + DVM_JOB_REQUEST_FIRST = 5000, + DVM_JOB_REQUEST_LAST = 5999, // Replaceable events REPLACEABLE_FIRST = 10000, // NIP-65: Relay List Metadata diff --git a/src/factories/event-strategy-factory.ts b/src/factories/event-strategy-factory.ts index a9843ea2..37f67e9c 100644 --- a/src/factories/event-strategy-factory.ts +++ b/src/factories/event-strategy-factory.ts @@ -1,7 +1,8 @@ import { ICacheAdapter, IWebSocketAdapter } from '../@types/adapters' -import { IEventRepository, IInviteCodeRepository, IUserRepository } from '../@types/repositories' +import { IDvmJobRepository, IEventRepository, IInviteCodeRepository, IUserRepository } from '../@types/repositories' import { isDeleteEvent, + isDvmJobRequestEvent, isEphemeralEvent, isGiftWrapEvent, isMarmotGroupEvent, @@ -14,6 +15,7 @@ import { isNip43JoinRequest, isNip43LeaveRequest } from '../utils/nip43' import { isRelayListEvent } from '../utils/nip65' import { DefaultEventStrategy } from '../handlers/event-strategies/default-event-strategy' import { DeleteEventStrategy } from '../handlers/event-strategies/delete-event-strategy' +import { DvmJobRequestEventStrategy } from '../handlers/event-strategies/dvm-job-request-event-strategy' import { EphemeralEventStrategy } from '../handlers/event-strategies/ephemeral-event-strategy' import { Event } from '../@types/event' import { Factory } from '../@types/base' @@ -33,6 +35,7 @@ export const eventStrategyFactory = eventRepository: IEventRepository, userRepository: IUserRepository, inviteCodeRepository: IInviteCodeRepository, + dvmJobRepository: IDvmJobRepository, cache: ICacheAdapter, settings: () => Settings, ): Factory>, [Event, IWebSocketAdapter]> => @@ -47,12 +50,17 @@ export const eventStrategyFactory = return new TimestampEventStrategy(adapter, eventRepository) } else if (isRelayListEvent(event) || isReplaceableEvent(event)) { return new ReplaceableEventStrategy(adapter, eventRepository) - // NIP-43: Join/Leave requests MUST be checked before the generic ephemeral - // handler, because kinds 28934/28936 fall in the ephemeral range (20000-29999). + // NIP-43: Join/Leave requests MUST be checked before the generic ephemeral + // handler, because kinds 28934/28936 fall in the ephemeral range (20000-29999). } else if (isNip43JoinRequest(event)) { return new JoinRequestEventStrategy(adapter, inviteCodeRepository, userRepository, cache, settings) } else if (isNip43LeaveRequest(event)) { return new LeaveRequestEventStrategy(adapter, userRepository, cache, settings) + // NIP-90: DVM job requests (kind 5000-5999) checked early, same reasoning + // as the NIP-43 checks above — kept explicit rather than relying on it + // falling through to DefaultEventStrategy. + } else if (isDvmJobRequestEvent(event)) { + return new DvmJobRequestEventStrategy(adapter, eventRepository, dvmJobRepository) } else if (isEphemeralEvent(event)) { return new EphemeralEventStrategy(adapter) } else if (isDeleteEvent(event)) { @@ -62,4 +70,4 @@ export const eventStrategyFactory = } return new DefaultEventStrategy(adapter, eventRepository) - } \ No newline at end of file + } diff --git a/src/factories/message-handler-factory.ts b/src/factories/message-handler-factory.ts index e7ae66de..6981a194 100644 --- a/src/factories/message-handler-factory.ts +++ b/src/factories/message-handler-factory.ts @@ -1,5 +1,11 @@ import { ICacheAdapter, IWebSocketAdapter } from '../@types/adapters' -import { IEventRepository, IInviteCodeRepository, INip05VerificationRepository, IUserRepository } from '../@types/repositories' +import { + IDvmJobRepository, + IEventRepository, + IInviteCodeRepository, + INip05VerificationRepository, + IUserRepository, +} from '../@types/repositories' import { IncomingMessage, MessageType } from '../@types/messages' import { createSettings } from './settings-factory' import { AuthMessageHandler } from '../handlers/auth-message-handler' @@ -26,13 +32,21 @@ export const messageHandlerFactory = userRepository: IUserRepository, nip05VerificationRepository: INip05VerificationRepository, inviteCodeRepository: IInviteCodeRepository, + dvmJobRepository: IDvmJobRepository, ) => ([message, adapter]: [IncomingMessage, IWebSocketAdapter]) => { switch (message[0]) { case MessageType.EVENT: { return new EventMessageHandler( adapter, - eventStrategyFactory(eventRepository, userRepository, inviteCodeRepository, getCache(), createSettings), + eventStrategyFactory( + eventRepository, + userRepository, + inviteCodeRepository, + dvmJobRepository, + getCache(), + createSettings, + ), eventRepository, userRepository, createSettings, diff --git a/src/factories/websocket-adapter-factory.ts b/src/factories/websocket-adapter-factory.ts index 433f1de5..67cb8764 100644 --- a/src/factories/websocket-adapter-factory.ts +++ b/src/factories/websocket-adapter-factory.ts @@ -1,7 +1,13 @@ import { IncomingMessage } from 'http' import { WebSocket } from 'ws' -import { IEventRepository, IInviteCodeRepository, INip05VerificationRepository, IUserRepository } from '../@types/repositories' +import { + IDvmJobRepository, + IEventRepository, + IInviteCodeRepository, + INip05VerificationRepository, + IUserRepository, +} from '../@types/repositories' import { createSettings } from './settings-factory' import { IWebSocketServerAdapter } from '../@types/adapters' import { messageHandlerFactory } from './message-handler-factory' @@ -14,13 +20,20 @@ export const webSocketAdapterFactory = userRepository: IUserRepository, nip05VerificationRepository: INip05VerificationRepository, inviteCodeRepository: IInviteCodeRepository, + dvmJobRepository: IDvmJobRepository, ) => ([client, request, webSocketServerAdapter]: [WebSocket, IncomingMessage, IWebSocketServerAdapter]) => new WebSocketAdapter( client, request, webSocketServerAdapter, - messageHandlerFactory(eventRepository, userRepository, nip05VerificationRepository, inviteCodeRepository), + messageHandlerFactory( + eventRepository, + userRepository, + nip05VerificationRepository, + inviteCodeRepository, + dvmJobRepository, + ), rateLimiterFactory, createSettings, ) diff --git a/src/factories/worker-factory.ts b/src/factories/worker-factory.ts index 8b97d782..ce9f2f42 100644 --- a/src/factories/worker-factory.ts +++ b/src/factories/worker-factory.ts @@ -8,6 +8,7 @@ import { AppWorker } from '../app/worker' import { createLogger } from './logger-factory' import { createSettings } from '../factories/settings-factory' import { createWebApp } from './web-app-factory' +import { DvmJobRepository } from '../repositories/dvm-job-repository' import { EventRepository } from '../repositories/event-repository' import { InviteCodeRepository } from '../repositories/invite-code-repository' import { Nip05VerificationRepository } from '../repositories/nip05-verification-repository' @@ -24,6 +25,7 @@ export const workerFactory = (): AppWorker => { const userRepository = new UserRepository(dbClient, eventRepository) const nip05VerificationRepository = new Nip05VerificationRepository(dbClient) const inviteCodeRepository = new InviteCodeRepository(dbClient) + const dvmJobRepository = new DvmJobRepository(dbClient) const settings = createSettings() @@ -65,7 +67,13 @@ export const workerFactory = (): AppWorker => { const adapter = new WebSocketServerAdapter( server, webSocketServer, - webSocketAdapterFactory(eventRepository, userRepository, nip05VerificationRepository, inviteCodeRepository), + webSocketAdapterFactory( + eventRepository, + userRepository, + nip05VerificationRepository, + inviteCodeRepository, + dvmJobRepository, + ), createSettings, ) diff --git a/src/handlers/event-strategies/dvm-job-request-event-strategy.ts b/src/handlers/event-strategies/dvm-job-request-event-strategy.ts new file mode 100644 index 00000000..92b3ab7f --- /dev/null +++ b/src/handlers/event-strategies/dvm-job-request-event-strategy.ts @@ -0,0 +1,42 @@ +import { createEventCommandResult } from '../../telemetry/event-metrics' +import { createLogger } from '../../factories/logger-factory' +import { Event } from '../../@types/event' +import { IDvmJobRepository, IEventRepository } from '../../@types/repositories' +import { IEventStrategy } from '../../@types/message-handlers' +import { IWebSocketAdapter } from '../../@types/adapters' +import { WebSocketAdapterEvent } from '../../constants/adapter' + +const logger = createLogger('dvm-job-request-event-strategy') + +export class DvmJobRequestEventStrategy implements IEventStrategy> { + public constructor( + private readonly webSocket: IWebSocketAdapter, + private readonly eventRepository: IEventRepository, + private readonly dvmJobRepository: IDvmJobRepository, + ) {} + + public async execute(event: Event): Promise { + logger('received dvm job request: %o', event) + + const count = await this.eventRepository.create(event) + this.webSocket.emit( + WebSocketAdapterEvent.Message, + createEventCommandResult(event.id, true, count ? '' : 'duplicate:'), + ) + + if (!count) { + return + } + + this.webSocket.emit(WebSocketAdapterEvent.Broadcast, event) + + try { + await this.dvmJobRepository.create(event.id, event.pubkey, event.kind) + } catch (error) { + // Job-state recording is best-effort: the event itself is already + // stored and broadcast correctly, so a repository failure here must + // not surface as a rejection of a valid event. + logger.error('unable to record dvm job for event %s: %o', event.id, error) + } + } +} diff --git a/src/utils/event.ts b/src/utils/event.ts index 7de72d40..1b1c2296 100644 --- a/src/utils/event.ts +++ b/src/utils/event.ts @@ -40,18 +40,20 @@ export const isEventMatchingFilter = (filter: SubscriptionFilter) => (event: Event): boolean => { const startsWith = (input: string) => (prefix: string) => input.startsWith(prefix) - const isMatchingGenericTagCriterion = (key: string, criterion: string) => (tag: Tag): boolean => { - const [, tagName] = key - if (tag[0] !== tagName) { - return false - } + const isMatchingGenericTagCriterion = + (key: string, criterion: string) => + (tag: Tag): boolean => { + const [, tagName] = key + if (tag[0] !== tagName) { + return false + } - if (isGeohashPrefixCriterion(key, criterion)) { - return tag[1].startsWith(stripGeohashPrefixWildcard(criterion)) - } + if (isGeohashPrefixCriterion(key, criterion)) { + return tag[1].startsWith(stripGeohashPrefixWildcard(criterion)) + } - return tag[1] === criterion - } + return tag[1] === criterion + } // NIP-01: Basic protocol flow description @@ -96,7 +98,9 @@ export const isEventMatchingFilter = Object.entries(filter) .filter(([key, criteria]) => isGenericTagQuery(key) && Array.isArray(criteria)) .some(([key, criteria]) => { - return !event.tags.some((tag) => criteria.some((criterion) => isMatchingGenericTagCriterion(key, criterion)(tag))) + return !event.tags.some((tag) => + criteria.some((criterion) => isMatchingGenericTagCriterion(key, criterion)(tag)), + ) }) ) { return false @@ -205,6 +209,10 @@ export const isEphemeralEvent = (event: Event): boolean => { return event.kind >= EventKinds.EPHEMERAL_FIRST && event.kind <= EventKinds.EPHEMERAL_LAST } +export const isDvmJobRequestEvent = (event: Event): boolean => { + return event.kind >= EventKinds.DVM_JOB_REQUEST_FIRST && event.kind <= EventKinds.DVM_JOB_REQUEST_LAST +} + export const isParameterizedReplaceableEvent = (event: Event): boolean => { return ( event.kind >= EventKinds.PARAMETERIZED_REPLACEABLE_FIRST && event.kind <= EventKinds.PARAMETERIZED_REPLACEABLE_LAST From 18d866a6cb6b7d228e776c51e0ac10788ecbd42b Mon Sep 17 00:00:00 2001 From: Priyanshubhartistm Date: Wed, 12 Aug 2026 20:04:57 +0530 Subject: [PATCH 2/2] test(dvm): add tests for dvm job request ingestion Signed-off-by: Priyanshubhartistm --- .../factories/event-strategy-factory.spec.ts | 31 +++- .../factories/message-handler-factory.spec.ts | 19 ++- .../websocket-adapter-factory.spec.ts | 17 ++- .../dvm-job-request-event-strategy.spec.ts | 132 ++++++++++++++++++ test/unit/utils/event.spec.ts | 19 +++ 5 files changed, 210 insertions(+), 8 deletions(-) create mode 100644 test/unit/handlers/event-strategies/dvm-job-request-event-strategy.spec.ts diff --git a/test/unit/factories/event-strategy-factory.spec.ts b/test/unit/factories/event-strategy-factory.spec.ts index 37dc159e..e24c0294 100644 --- a/test/unit/factories/event-strategy-factory.spec.ts +++ b/test/unit/factories/event-strategy-factory.spec.ts @@ -1,8 +1,14 @@ import { expect } from 'chai' -import { IEventRepository, IInviteCodeRepository, IUserRepository } from '../../../src/@types/repositories' +import { + IDvmJobRepository, + IEventRepository, + IInviteCodeRepository, + IUserRepository, +} from '../../../src/@types/repositories' import { DefaultEventStrategy } from '../../../src/handlers/event-strategies/default-event-strategy' import { DeleteEventStrategy } from '../../../src/handlers/event-strategies/delete-event-strategy' +import { DvmJobRequestEventStrategy } from '../../../src/handlers/event-strategies/dvm-job-request-event-strategy' import { EphemeralEventStrategy } from '../../../src/handlers/event-strategies/ephemeral-event-strategy' import { Event } from '../../../src/@types/event' import { EventKinds } from '../../../src/constants/base' @@ -24,6 +30,7 @@ describe('eventStrategyFactory', () => { let eventRepository: IEventRepository let userRepository: IUserRepository let inviteCodeRepository: IInviteCodeRepository + let dvmJobRepository: IDvmJobRepository let cache: ICacheAdapter let settings: () => Settings let event: Event @@ -34,12 +41,20 @@ describe('eventStrategyFactory', () => { eventRepository = {} as any userRepository = {} as any inviteCodeRepository = {} as any + dvmJobRepository = {} as any cache = {} as any - settings = () => ({ info: { relay_url: 'wss://test.relay' } } as any) + settings = () => ({ info: { relay_url: 'wss://test.relay' } }) as any event = {} as any adapter = {} as any - factory = eventStrategyFactory(eventRepository, userRepository, inviteCodeRepository, cache, settings) + factory = eventStrategyFactory( + eventRepository, + userRepository, + inviteCodeRepository, + dvmJobRepository, + cache, + settings, + ) }) it('returns ReplaceableEvent given a set_metadata event', () => { @@ -136,4 +151,14 @@ describe('eventStrategyFactory', () => { event.kind = EventKinds.NIP43_LEAVE_REQUEST expect(factory([event, adapter])).to.be.an.instanceOf(LeaveRequestEventStrategy) }) + + it('returns DvmJobRequestEventStrategy given a DVM job request (kind 5000-5999)', () => { + event.kind = EventKinds.DVM_JOB_REQUEST_FIRST + expect(factory([event, adapter])).to.be.an.instanceOf(DvmJobRequestEventStrategy) + }) + + it('returns DvmJobRequestEventStrategy given the last DVM job request kind (5999)', () => { + event.kind = EventKinds.DVM_JOB_REQUEST_LAST + expect(factory([event, adapter])).to.be.an.instanceOf(DvmJobRequestEventStrategy) + }) }) diff --git a/test/unit/factories/message-handler-factory.spec.ts b/test/unit/factories/message-handler-factory.spec.ts index 68844389..9ce8a284 100644 --- a/test/unit/factories/message-handler-factory.spec.ts +++ b/test/unit/factories/message-handler-factory.spec.ts @@ -1,6 +1,12 @@ import { expect } from 'chai' -import { IEventRepository, IInviteCodeRepository, INip05VerificationRepository, IUserRepository } from '../../../src/@types/repositories' +import { + IDvmJobRepository, + IEventRepository, + IInviteCodeRepository, + INip05VerificationRepository, + IUserRepository, +} from '../../../src/@types/repositories' import { IncomingMessage, MessageType } from '../../../src/@types/messages' import { AuthMessageHandler } from '../../../src/handlers/auth-message-handler' import { Event } from '../../../src/@types/event' @@ -19,6 +25,7 @@ describe('messageHandlerFactory', () => { let userRepository: IUserRepository let nip05VerificationRepository: INip05VerificationRepository let inviteCodeRepository: IInviteCodeRepository + let dvmJobRepository: IDvmJobRepository let message: IncomingMessage let adapter: IWebSocketAdapter let factory @@ -42,11 +49,18 @@ describe('messageHandlerFactory', () => { userRepository = {} as any nip05VerificationRepository = {} as any inviteCodeRepository = {} as any + dvmJobRepository = {} as any adapter = {} as any event = { tags: [], } as any - factory = messageHandlerFactory(eventRepository, userRepository, nip05VerificationRepository, inviteCodeRepository) + factory = messageHandlerFactory( + eventRepository, + userRepository, + nip05VerificationRepository, + inviteCodeRepository, + dvmJobRepository, + ) }) afterEach(() => { @@ -89,4 +103,3 @@ describe('messageHandlerFactory', () => { expect(() => factory([message, adapter])).to.throw(Error, 'Unknown message type: undefined') }) }) - diff --git a/test/unit/factories/websocket-adapter-factory.spec.ts b/test/unit/factories/websocket-adapter-factory.spec.ts index d1ec9405..48c34ba6 100644 --- a/test/unit/factories/websocket-adapter-factory.spec.ts +++ b/test/unit/factories/websocket-adapter-factory.spec.ts @@ -3,7 +3,13 @@ import { IncomingMessage } from 'http' import Sinon from 'sinon' import WebSocket from 'ws' -import { IEventRepository, IInviteCodeRepository, INip05VerificationRepository, IUserRepository } from '../../../src/@types/repositories' +import { + IDvmJobRepository, + IEventRepository, + IInviteCodeRepository, + INip05VerificationRepository, + IUserRepository, +} from '../../../src/@types/repositories' import { IWebSocketServerAdapter } from '../../../src/@types/adapters' import { SettingsStatic } from '../../../src/utils/settings' import { WebSocketAdapter } from '../../../src/adapters/web-socket-adapter' @@ -33,6 +39,7 @@ describe('webSocketAdapterFactory', () => { const userRepository: IUserRepository = {} as any const nip05VerificationRepository: INip05VerificationRepository = {} as any const inviteCodeRepository: IInviteCodeRepository = {} as any + const dvmJobRepository: IDvmJobRepository = {} as any const client: WebSocket = { on: onStub, @@ -48,7 +55,13 @@ describe('webSocketAdapterFactory', () => { } as any const webSocketServerAdapter: IWebSocketServerAdapter = {} as any - const factory = webSocketAdapterFactory(eventRepository, userRepository, nip05VerificationRepository, inviteCodeRepository) + const factory = webSocketAdapterFactory( + eventRepository, + userRepository, + nip05VerificationRepository, + inviteCodeRepository, + dvmJobRepository, + ) expect(factory([client, request, webSocketServerAdapter])).to.be.an.instanceOf(WebSocketAdapter) }) }) diff --git a/test/unit/handlers/event-strategies/dvm-job-request-event-strategy.spec.ts b/test/unit/handlers/event-strategies/dvm-job-request-event-strategy.spec.ts new file mode 100644 index 00000000..cf2cad88 --- /dev/null +++ b/test/unit/handlers/event-strategies/dvm-job-request-event-strategy.spec.ts @@ -0,0 +1,132 @@ +import chai from 'chai' +import chaiAsPromised from 'chai-as-promised' +import Sinon from 'sinon' + +chai.use(chaiAsPromised) + +const { expect } = chai + +import { DvmJobRequestEventStrategy } from '../../../../src/handlers/event-strategies/dvm-job-request-event-strategy' +import { Event } from '../../../../src/@types/event' +import { IDvmJobRepository, IEventRepository } from '../../../../src/@types/repositories' +import { IEventStrategy } from '../../../../src/@types/message-handlers' +import { IWebSocketAdapter } from '../../../../src/@types/adapters' +import { MessageType } from '../../../../src/@types/messages' +import { WebSocketAdapterEvent } from '../../../../src/constants/adapter' + +describe('DvmJobRequestEventStrategy', () => { + const event: Event = { + id: 'event-id', + pubkey: 'requester-pubkey', + kind: 5000, + } as any + + let webSocket: IWebSocketAdapter + let eventRepository: IEventRepository + let dvmJobRepository: IDvmJobRepository + + let webSocketEmitStub: Sinon.SinonStub + let eventRepositoryCreateStub: Sinon.SinonStub + let dvmJobRepositoryCreateStub: Sinon.SinonStub + + let strategy: IEventStrategy> + + let sandbox: Sinon.SinonSandbox + + beforeEach(() => { + sandbox = Sinon.createSandbox() + + webSocketEmitStub = sandbox.stub() + webSocket = { + emit: webSocketEmitStub, + } as any + + eventRepositoryCreateStub = sandbox.stub() + eventRepository = { + create: eventRepositoryCreateStub, + } as any + + dvmJobRepositoryCreateStub = sandbox.stub() + dvmJobRepository = { + create: dvmJobRepositoryCreateStub, + } as any + + strategy = new DvmJobRequestEventStrategy(webSocket, eventRepository, dvmJobRepository) + }) + + afterEach(() => { + sandbox.restore() + }) + + describe('execute', () => { + it('creates the event', async () => { + eventRepositoryCreateStub.resolves(1) + dvmJobRepositoryCreateStub.resolves({}) + + await strategy.execute(event) + + expect(eventRepositoryCreateStub).to.have.been.calledOnceWithExactly(event) + }) + + it('records a dvm job when the event is newly created', async () => { + eventRepositoryCreateStub.resolves(1) + dvmJobRepositoryCreateStub.resolves({}) + + await strategy.execute(event) + + expect(dvmJobRepositoryCreateStub).to.have.been.calledOnceWithExactly('event-id', 'requester-pubkey', 5000) + }) + + it('broadcasts the event when newly created', async () => { + eventRepositoryCreateStub.resolves(1) + dvmJobRepositoryCreateStub.resolves({}) + + await strategy.execute(event) + + expect(webSocketEmitStub).to.have.been.calledWithExactly(WebSocketAdapterEvent.Message, [ + MessageType.OK, + 'event-id', + true, + '', + ]) + expect(webSocketEmitStub).to.have.been.calledWithExactly(WebSocketAdapterEvent.Broadcast, event) + }) + + it('does not broadcast or record a job when the event is a duplicate', async () => { + eventRepositoryCreateStub.resolves(0) + + await strategy.execute(event) + + expect(webSocketEmitStub).to.have.been.calledOnceWithExactly(WebSocketAdapterEvent.Message, [ + MessageType.OK, + 'event-id', + true, + 'duplicate:', + ]) + expect(dvmJobRepositoryCreateStub).not.to.have.been.called + }) + + it('does not reject the event when job recording fails', async () => { + eventRepositoryCreateStub.resolves(1) + dvmJobRepositoryCreateStub.rejects(new Error('db unavailable')) + + await expect(strategy.execute(event)).to.eventually.be.fulfilled + + expect(webSocketEmitStub).to.have.been.calledWithExactly(WebSocketAdapterEvent.Message, [ + MessageType.OK, + 'event-id', + true, + '', + ]) + }) + + it('rejects if unable to create the event', async () => { + const error = new Error('event creation failed') + eventRepositoryCreateStub.rejects(error) + + await expect(strategy.execute(event)).to.eventually.be.rejectedWith(error) + + expect(dvmJobRepositoryCreateStub).not.to.have.been.called + }) + }) +}) diff --git a/test/unit/utils/event.spec.ts b/test/unit/utils/event.spec.ts index 39d26c33..ad81b2b0 100644 --- a/test/unit/utils/event.spec.ts +++ b/test/unit/utils/event.spec.ts @@ -4,6 +4,7 @@ import { getEventExpiration, isDeleteEvent, isDirectMessageEvent, + isDvmJobRequestEvent, isEphemeralEvent, isEventIdValid, isEventMatchingFilter, @@ -415,6 +416,24 @@ describe('NIP-16', () => { expect(isEphemeralEvent({ kind: 30000 } as any)).to.be.false }) }) + + describe('isDvmJobRequestEvent', () => { + it('returns true for the first kind in the DVM job request range (5000)', () => { + expect(isDvmJobRequestEvent({ kind: 5000 } as any)).to.be.true + }) + + it('returns true for the last kind in the DVM job request range (5999)', () => { + expect(isDvmJobRequestEvent({ kind: 5999 } as any)).to.be.true + }) + + it('returns false for a kind below the DVM job request range', () => { + expect(isDvmJobRequestEvent({ kind: 4999 } as any)).to.be.false + }) + + it('returns false for a kind above the DVM job request range', () => { + expect(isDvmJobRequestEvent({ kind: 6000 } as any)).to.be.false + }) + }) }) describe('NIP-17', () => {