diff --git a/migrations/20230203_185700_add_expires_at_to_events_table.js b/migrations/20230203_185700_add_expires_at_to_events_table.js new file mode 100644 index 0000000..e19777f --- /dev/null +++ b/migrations/20230203_185700_add_expires_at_to_events_table.js @@ -0,0 +1,11 @@ +exports.up = function (knex) { + return knex.schema.alterTable('events', function (table) { + table.integer('expires_at').unsigned().nullable().index() + }) +} + +exports.down = function (knex) { + return knex.schema.alterTable('events', function (table) { + table.dropColumn('expires_at') + }) +} diff --git a/package.json b/package.json index e5447aa..9bce439 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,8 @@ 22, 26, 28, - 33 + 33, + 40 ], "main": "src/index.ts", "scripts": { diff --git a/src/@types/event.ts b/src/@types/event.ts index e971d8b..5219456 100644 --- a/src/@types/event.ts +++ b/src/@types/event.ts @@ -1,5 +1,5 @@ import { ContextMetadata, EventId, Pubkey, Tag } from './base' -import { ContextMetadataKey, EventDeduplicationMetadataKey, EventDelegatorMetadataKey, EventKinds } from '../constants/base' +import { ContextMetadataKey, EventDeduplicationMetadataKey, EventDelegatorMetadataKey, EventExpirationTimeMetadataKey, EventKinds } from '../constants/base' export interface BaseEvent { id: EventId @@ -25,6 +25,10 @@ export interface DelegatedEvent extends Event { [EventDelegatorMetadataKey]?: Pubkey } +export interface ExpiringEvent extends Event { + [EventExpirationTimeMetadataKey]?: number +} + export interface ParameterizedReplaceableEvent extends Event { [EventDeduplicationMetadataKey]: string[] } @@ -42,6 +46,7 @@ export interface DBEvent { event_deduplication?: string | null first_seen: Date deleted_at?: Date + expires_at?: number } export interface CanonicalEvent { diff --git a/src/constants/base.ts b/src/constants/base.ts index 7af91f0..b5a29a0 100644 --- a/src/constants/base.ts +++ b/src/constants/base.ts @@ -36,6 +36,7 @@ export enum EventTags { // Multicast = 'm', Delegation = 'delegation', Deduplication = 'd', + Expiration = 'expiration', } export enum PaymentsProcessors { @@ -45,3 +46,4 @@ export enum PaymentsProcessors { export const EventDelegatorMetadataKey = Symbol('Delegator') export const EventDeduplicationMetadataKey = Symbol('Deduplication') export const ContextMetadataKey = Symbol('Context') +export const EventExpirationTimeMetadataKey = Symbol('Expiration') diff --git a/src/factories/event-strategy-factory.ts b/src/factories/event-strategy-factory.ts index 65c97d4..91a729f 100644 --- a/src/factories/event-strategy-factory.ts +++ b/src/factories/event-strategy-factory.ts @@ -10,7 +10,6 @@ import { IWebSocketAdapter } from '../@types/adapters' import { ParameterizedReplaceableEventStrategy } from '../handlers/event-strategies/parameterized-replaceable-event-strategy' import { ReplaceableEventStrategy } from '../handlers/event-strategies/replaceable-event-strategy' - export const eventStrategyFactory = ( eventRepository: IEventRepository, ): Factory>, [Event, IWebSocketAdapter]> => @@ -23,7 +22,7 @@ export const eventStrategyFactory = ( return new DeleteEventStrategy(adapter, eventRepository) } else if (isParameterizedReplaceableEvent(event)) { return new ParameterizedReplaceableEventStrategy(adapter, eventRepository) - } + } return new DefaultEventStrategy(adapter, eventRepository) } diff --git a/src/handlers/event-message-handler.ts b/src/handlers/event-message-handler.ts index 3201fa1..bbea877 100644 --- a/src/handlers/event-message-handler.ts +++ b/src/handlers/event-message-handler.ts @@ -1,10 +1,11 @@ +import { Event, ExpiringEvent } from '../@types/event' import { EventRateLimit, FeeSchedule, Settings } from '../@types/settings' -import { getEventProofOfWork, getPubkeyProofOfWork, isEventIdValid, isEventKindOrRangeMatch, isEventSignatureValid } from '../utils/event' +import { getEventExpiration, getEventProofOfWork, getPubkeyProofOfWork, isEventIdValid, isEventKindOrRangeMatch, isEventSignatureValid, isExpiredEvent } from '../utils/event' import { IEventStrategy, IMessageHandler } from '../@types/message-handlers' import { ContextMetadataKey } from '../constants/base' import { createCommandResult } from '../utils/messages' import { createLogger } from '../factories/logger-factory' -import { Event } from '../@types/event' +import { EventExpirationTimeMetadataKey } from '../constants/base' import { Factory } from '../@types/base' import { IncomingEventMessage } from '../@types/messages' import { IRateLimiter } from '../@types/utils' @@ -24,7 +25,7 @@ export class EventMessageHandler implements IMessageHandler { ) {} public async handleMessage(message: IncomingEventMessage): Promise { - const [, event] = message + let [, event] = message event[ContextMetadataKey] = message[ContextMetadataKey] @@ -35,6 +36,14 @@ export class EventMessageHandler implements IMessageHandler { return } + if (isExpiredEvent(event)) { + debug('event %s rejected: expired') + this.webSocket.emit(WebSocketAdapterEvent.Message, createCommandResult(event.id, false, 'event is expired')) + return + } + + event = this.addExpirationMetadata(event) + if (await this.isRateLimited(event)) { debug('event %s rejected: rate-limited') this.webSocket.emit(WebSocketAdapterEvent.Message, createCommandResult(event.id, false, 'rate-limited: slow down')) @@ -261,4 +270,17 @@ export class EventMessageHandler implements IMessageHandler { return 'blocked: insufficient balance' } } + + protected addExpirationMetadata(event: Event): Event | ExpiringEvent { + const eventExpiration: number = getEventExpiration(event) + if (eventExpiration) { + const expiringEvent: ExpiringEvent = { + ...event, + [EventExpirationTimeMetadataKey]: eventExpiration, + } + return expiringEvent + } else { + return event + } + } } diff --git a/src/repositories/event-repository.ts b/src/repositories/event-repository.ts index b9fb6a9..da14378 100644 --- a/src/repositories/event-repository.ts +++ b/src/repositories/event-repository.ts @@ -29,7 +29,7 @@ import { toPairs, } from 'ramda' -import { ContextMetadataKey, EventDeduplicationMetadataKey, EventDelegatorMetadataKey } from '../constants/base' +import { ContextMetadataKey, EventDeduplicationMetadataKey, EventDelegatorMetadataKey, EventExpirationTimeMetadataKey } from '../constants/base' import { DatabaseClient, EventId } from '../@types/base' import { DBEvent, Event } from '../@types/event' import { IEventRepository, IQueryResult } from '../@types/repositories' @@ -182,6 +182,12 @@ export class EventRepository implements IEventRepository { always(null), ), remote_address: path([ContextMetadataKey as any, 'remoteAddress', 'address']), + expires_at: ifElse( + propSatisfies(is(Number), EventExpirationTimeMetadataKey), + pipe(prop(EventExpirationTimeMetadataKey as any), toBuffer), + always(null), + ), + })(event) return this.masterDbClient('events') @@ -214,6 +220,11 @@ export class EventRepository implements IEventRepository { pipe(prop(EventDeduplicationMetadataKey as any), toJSON), ), remote_address: path([ContextMetadataKey as any, 'remoteAddress', 'address']), + expires_at: ifElse( + propSatisfies(is(Number), EventExpirationTimeMetadataKey), + pipe(prop(EventExpirationTimeMetadataKey as any), toBuffer), + always(null), + ), })(event) const query = this.masterDbClient('events') @@ -250,6 +261,7 @@ export class EventRepository implements IEventRepository { event_signature: pipe(always(''), toBuffer), event_delegator: always(null), event_deduplication: pipe(always([pubkey, 5]), toJSON), + expires_at: always(null), deleted_at: always(date.toISOString()), }) ) diff --git a/src/utils/event.ts b/src/utils/event.ts index c6580e4..7edd026 100644 --- a/src/utils/event.ts +++ b/src/utils/event.ts @@ -269,6 +269,29 @@ export const isDeleteEvent = (event: Event): boolean => { return event.kind === EventKinds.DELETE } +export const isExpiredEvent = (event: Event): boolean => { + if (!event.tags.length) return false + + const expirationTime = getEventExpiration(event) + + if (!expirationTime) return false + + const date = new Date() + const isExpired = expirationTime <= Math.floor(date.getTime() / 1000) + + return isExpired +} + +export const getEventExpiration = (event: Event): number | undefined => { + const [, rawExpirationTime] = event.tags.find((tag) => tag.length >= 2 && tag[0] === EventTags.Expiration) ?? [] + if (!rawExpirationTime) return + + const expirationTime = Number(rawExpirationTime) + if ((Number.isSafeInteger(expirationTime) && Math.log10(expirationTime))) { + return expirationTime + } +} + export const getEventProofOfWork = (eventId: EventId): number => { return getLeadingZeroBits(Buffer.from(eventId, 'hex')) } diff --git a/test/unit/handlers/event-message-handler.spec.ts b/test/unit/handlers/event-message-handler.spec.ts index da11f3e..a1e9004 100644 --- a/test/unit/handlers/event-message-handler.spec.ts +++ b/test/unit/handlers/event-message-handler.spec.ts @@ -111,7 +111,7 @@ describe('EventMessageHandler', () => { }) it('rejects event if invalid', async () => { - isEventValidStub.returns('reason') + isEventValidStub.resolves('reason') await handler.handleMessage(message) @@ -128,6 +128,28 @@ describe('EventMessageHandler', () => { expect(isUserAdmitted).to.have.been.calledWithExactly(event) expect(strategyFactoryStub).not.to.have.been.called }) + + it('rejects event if it is expired', async () => { + isEventValidStub.resolves(undefined) + + const expiredEvent = { + ...event, + tags: [ + ['expiration', '1600000'], + ], + } + + const expiredEventMessage: any = [MessageType.EVENT, expiredEvent] + + await handler.handleMessage(expiredEventMessage) + + expect(isEventValidStub).to.have.been.calledOnceWithExactly(expiredEvent) + + expect(onMessageSpy).to.have.been.calledOnceWithExactly( + [MessageType.OK, event.id, false, 'event is expired'], + ) + expect(strategyExecuteStub).not.to.have.been.called + }) it('does not call strategy if none given', async () => { isEventValidStub.returns(undefined) diff --git a/test/unit/repositories/event-repository.spec.ts b/test/unit/repositories/event-repository.spec.ts index fcd65ff..d61006d 100644 --- a/test/unit/repositories/event-repository.spec.ts +++ b/test/unit/repositories/event-repository.spec.ts @@ -435,7 +435,7 @@ describe('EventRepository', () => { const query = (repository as any).insert(event).toString() - expect(query).to.equal('insert into "events" ("event_content", "event_created_at", "event_delegator", "event_id", "event_kind", "event_pubkey", "event_signature", "event_tags", "remote_address") values (\'I\'\'ve set up mirroring between relays: https://i.imgur.com/HxCDipB.png\', 1648351380, NULL, X\'6b3cdd0302ded8068ad3f0269c74423ca4fee460f800f3d90103b63f14400407\', 1, X\'22e804d26ed16b68db5259e78449e96dab5d464c8f470bda3eb1a70467f2c793\', X\'b37adfed0e6398546d623536f9ddc92b95b7dc71927e1123266332659253ecd0ffa91ddf2c0a82a8426c5b363139d28534d6cac893b8a810149557a3f6d36768\', \'[["p","8355095016fddbe31fcf1453b26f613553e9758cf2263e190eac8fd96a3d3de9","wss://nostr-pub.wellorder.net"],["e","7377fa81fc6c7ae7f7f4ef8938d4a603f7bf98183b35ab128235cc92d4bebf96","wss://nostr-relay.untethr.me"]]\', \'::1\') on conflict do nothing') + expect(query).to.equal('insert into "events" ("event_content", "event_created_at", "event_delegator", "event_id", "event_kind", "event_pubkey", "event_signature", "event_tags", "expires_at", "remote_address") values (\'I\'\'ve set up mirroring between relays: https://i.imgur.com/HxCDipB.png\', 1648351380, NULL, X\'6b3cdd0302ded8068ad3f0269c74423ca4fee460f800f3d90103b63f14400407\', 1, X\'22e804d26ed16b68db5259e78449e96dab5d464c8f470bda3eb1a70467f2c793\', X\'b37adfed0e6398546d623536f9ddc92b95b7dc71927e1123266332659253ecd0ffa91ddf2c0a82a8426c5b363139d28534d6cac893b8a810149557a3f6d36768\', \'[["p","8355095016fddbe31fcf1453b26f613553e9758cf2263e190eac8fd96a3d3de9","wss://nostr-pub.wellorder.net"],["e","7377fa81fc6c7ae7f7f4ef8938d4a603f7bf98183b35ab128235cc92d4bebf96","wss://nostr-relay.untethr.me"]]\', NULL, \'::1\') on conflict do nothing') }) }) @@ -453,7 +453,7 @@ describe('EventRepository', () => { it('insert stubs by pubkey & event ids', () => { const query = repository.insertStubs('001122', ['aabbcc', 'ddeeff']).toString() - expect(query).to.equal('insert into "events" ("deleted_at", "event_content", "event_created_at", "event_deduplication", "event_delegator", "event_id", "event_kind", "event_pubkey", "event_signature", "event_tags") values (\'1970-01-20T08:57:15.425Z\', \'\', 1673835, \'["001122",5]\', NULL, X\'aabbcc\', 5, X\'001122\', X\'\', \'[]\'), (\'1970-01-20T08:57:15.425Z\', \'\', 1673835, \'["001122",5]\', NULL, X\'ddeeff\', 5, X\'001122\', X\'\', \'[]\') on conflict do nothing') + expect(query).to.equal('insert into "events" ("deleted_at", "event_content", "event_created_at", "event_deduplication", "event_delegator", "event_id", "event_kind", "event_pubkey", "event_signature", "event_tags", "expires_at") values (\'1970-01-20T08:57:15.425Z\', \'\', 1673835, \'["001122",5]\', NULL, X\'aabbcc\', 5, X\'001122\', X\'\', \'[]\', NULL), (\'1970-01-20T08:57:15.425Z\', \'\', 1673835, \'["001122",5]\', NULL, X\'ddeeff\', 5, X\'001122\', X\'\', \'[]\', NULL) on conflict do nothing') }) }) @@ -480,7 +480,7 @@ describe('EventRepository', () => { const query = repository.upsert(event).toString() - expect(query).to.equal('insert into "events" ("event_content", "event_created_at", "event_deduplication", "event_delegator", "event_id", "event_kind", "event_pubkey", "event_signature", "event_tags", "remote_address") values (\'{"name":"ottman@minds.io","about":"","picture":"https://feat-2311-nostr.minds.io/icon/1002952989368913934/medium/1564498626/1564498626/1653379539"}\', 1564498626, \'["55b702c167c85eb1c2d5ab35d68bedd1a35b94c01147364d2395c2f66f35a503",0]\', NULL, X\'e527fe8b0f64a38c6877f943a9e8841074056ba72aceb31a4c85e6d10b27095a\', 0, X\'55b702c167c85eb1c2d5ab35d68bedd1a35b94c01147364d2395c2f66f35a503\', X\'d1de98733de2b412549aa64454722d9b66ab3c68e9e0d0f9c5d42e7bd54c30a06174364b683d2c8dbb386ff47f31e6cb7e2f3c3498d8819ee80421216c8309a9\', \'[]\', \'::1\') on conflict (event_pubkey, event_kind, event_deduplication) WHERE (event_kind = 0 OR event_kind = 3 OR event_kind = 41 OR (event_kind >= 10000 AND event_kind < 20000)) OR (event_kind >= 30000 AND event_kind < 40000) do update set "event_id" = X\'e527fe8b0f64a38c6877f943a9e8841074056ba72aceb31a4c85e6d10b27095a\',"event_created_at" = 1564498626,"event_tags" = \'[]\',"event_content" = \'{"name":"ottman@minds.io","about":"","picture":"https://feat-2311-nostr.minds.io/icon/1002952989368913934/medium/1564498626/1564498626/1653379539"}\',"event_signature" = X\'d1de98733de2b412549aa64454722d9b66ab3c68e9e0d0f9c5d42e7bd54c30a06174364b683d2c8dbb386ff47f31e6cb7e2f3c3498d8819ee80421216c8309a9\',"event_delegator" = NULL,"remote_address" = \'::1\' where "events"."event_created_at" < 1564498626') + expect(query).to.equal('insert into "events" ("event_content", "event_created_at", "event_deduplication", "event_delegator", "event_id", "event_kind", "event_pubkey", "event_signature", "event_tags", "expires_at", "remote_address") values (\'{"name":"ottman@minds.io","about":"","picture":"https://feat-2311-nostr.minds.io/icon/1002952989368913934/medium/1564498626/1564498626/1653379539"}\', 1564498626, \'["55b702c167c85eb1c2d5ab35d68bedd1a35b94c01147364d2395c2f66f35a503",0]\', NULL, X\'e527fe8b0f64a38c6877f943a9e8841074056ba72aceb31a4c85e6d10b27095a\', 0, X\'55b702c167c85eb1c2d5ab35d68bedd1a35b94c01147364d2395c2f66f35a503\', X\'d1de98733de2b412549aa64454722d9b66ab3c68e9e0d0f9c5d42e7bd54c30a06174364b683d2c8dbb386ff47f31e6cb7e2f3c3498d8819ee80421216c8309a9\', \'[]\', NULL, \'::1\') on conflict (event_pubkey, event_kind, event_deduplication) WHERE (event_kind = 0 OR event_kind = 3 OR event_kind = 41 OR (event_kind >= 10000 AND event_kind < 20000)) OR (event_kind >= 30000 AND event_kind < 40000) do update set "event_id" = X\'e527fe8b0f64a38c6877f943a9e8841074056ba72aceb31a4c85e6d10b27095a\',"event_created_at" = 1564498626,"event_tags" = \'[]\',"event_content" = \'{"name":"ottman@minds.io","about":"","picture":"https://feat-2311-nostr.minds.io/icon/1002952989368913934/medium/1564498626/1564498626/1653379539"}\',"event_signature" = X\'d1de98733de2b412549aa64454722d9b66ab3c68e9e0d0f9c5d42e7bd54c30a06174364b683d2c8dbb386ff47f31e6cb7e2f3c3498d8819ee80421216c8309a9\',"event_delegator" = NULL,"remote_address" = \'::1\',"expires_at" = NULL where "events"."event_created_at" < 1564498626') }) it('replaces event based on event_pubkey, event_kind and event_deduplication', () => { @@ -498,7 +498,7 @@ describe('EventRepository', () => { const query = repository.upsert(event).toString() - expect(query).to.equal('insert into "events" ("event_content", "event_created_at", "event_deduplication", "event_delegator", "event_id", "event_kind", "event_pubkey", "event_signature", "event_tags", "remote_address") values (\'{"name":"ottman@minds.io","about":"","picture":"https://feat-2311-nostr.minds.io/icon/1002952989368913934/medium/1564498626/1564498626/1653379539"}\', 1564498626, \'["deduplication"]\', NULL, X\'e527fe8b0f64a38c6877f943a9e8841074056ba72aceb31a4c85e6d10b27095a\', 0, X\'55b702c167c85eb1c2d5ab35d68bedd1a35b94c01147364d2395c2f66f35a503\', X\'d1de98733de2b412549aa64454722d9b66ab3c68e9e0d0f9c5d42e7bd54c30a06174364b683d2c8dbb386ff47f31e6cb7e2f3c3498d8819ee80421216c8309a9\', \'[]\', \'::1\') on conflict (event_pubkey, event_kind, event_deduplication) WHERE (event_kind = 0 OR event_kind = 3 OR event_kind = 41 OR (event_kind >= 10000 AND event_kind < 20000)) OR (event_kind >= 30000 AND event_kind < 40000) do update set "event_id" = X\'e527fe8b0f64a38c6877f943a9e8841074056ba72aceb31a4c85e6d10b27095a\',"event_created_at" = 1564498626,"event_tags" = \'[]\',"event_content" = \'{"name":"ottman@minds.io","about":"","picture":"https://feat-2311-nostr.minds.io/icon/1002952989368913934/medium/1564498626/1564498626/1653379539"}\',"event_signature" = X\'d1de98733de2b412549aa64454722d9b66ab3c68e9e0d0f9c5d42e7bd54c30a06174364b683d2c8dbb386ff47f31e6cb7e2f3c3498d8819ee80421216c8309a9\',"event_delegator" = NULL,"remote_address" = \'::1\' where "events"."event_created_at" < 1564498626') + expect(query).to.equal('insert into "events" ("event_content", "event_created_at", "event_deduplication", "event_delegator", "event_id", "event_kind", "event_pubkey", "event_signature", "event_tags", "expires_at", "remote_address") values (\'{"name":"ottman@minds.io","about":"","picture":"https://feat-2311-nostr.minds.io/icon/1002952989368913934/medium/1564498626/1564498626/1653379539"}\', 1564498626, \'["deduplication"]\', NULL, X\'e527fe8b0f64a38c6877f943a9e8841074056ba72aceb31a4c85e6d10b27095a\', 0, X\'55b702c167c85eb1c2d5ab35d68bedd1a35b94c01147364d2395c2f66f35a503\', X\'d1de98733de2b412549aa64454722d9b66ab3c68e9e0d0f9c5d42e7bd54c30a06174364b683d2c8dbb386ff47f31e6cb7e2f3c3498d8819ee80421216c8309a9\', \'[]\', NULL, \'::1\') on conflict (event_pubkey, event_kind, event_deduplication) WHERE (event_kind = 0 OR event_kind = 3 OR event_kind = 41 OR (event_kind >= 10000 AND event_kind < 20000)) OR (event_kind >= 30000 AND event_kind < 40000) do update set "event_id" = X\'e527fe8b0f64a38c6877f943a9e8841074056ba72aceb31a4c85e6d10b27095a\',"event_created_at" = 1564498626,"event_tags" = \'[]\',"event_content" = \'{"name":"ottman@minds.io","about":"","picture":"https://feat-2311-nostr.minds.io/icon/1002952989368913934/medium/1564498626/1564498626/1653379539"}\',"event_signature" = X\'d1de98733de2b412549aa64454722d9b66ab3c68e9e0d0f9c5d42e7bd54c30a06174364b683d2c8dbb386ff47f31e6cb7e2f3c3498d8819ee80421216c8309a9\',"event_delegator" = NULL,"remote_address" = \'::1\',"expires_at" = NULL where "events"."event_created_at" < 1564498626') }) }) }) diff --git a/test/unit/utils/event.spec.ts b/test/unit/utils/event.spec.ts index b3109f5..0009dec 100644 --- a/test/unit/utils/event.spec.ts +++ b/test/unit/utils/event.spec.ts @@ -2,6 +2,7 @@ import { expect } from 'chai' import { CanonicalEvent, Event } from '../../../src/@types/event' import { + getEventExpiration, isDelegatedEvent, isDelegatedEventValid, isDeleteEvent, @@ -9,6 +10,7 @@ import { isEventIdValid, isEventMatchingFilter, isEventSignatureValid, + isExpiredEvent, isParameterizedReplaceableEvent, isReplaceableEvent, serializeEvent, @@ -496,3 +498,70 @@ describe('NIP-33', () => { }) }) }) +describe('NIP-40', () => { + let event: Event + beforeEach(() => { + event = { + 'id': 'a080fd288b60ac2225ff2e2d815291bd730911e583e177302cc949a15dc2b2dc', + 'pubkey': '62903b1ff41559daf9ee98ef1ae67cc52f301bb5ce26d14baba3052f649c3f49', + 'created_at': 1660896109, + 'kind': 1, + 'tags': [ + [ + 'delegation', + '86f0689bd48dcd19c67a19d994f938ee34f251d8c39976290955ff585f2db42e', + 'kind=1&created_at>1640995200', + 'c33c88ba78ec3c760e49db591ac5f7b129e3887c8af7729795e85a0588007e5ac89b46549232d8f918eefd73e726cb450135314bfda419c030d0b6affe401ec1', + ], + ], + 'content': 'Hello world', + 'sig': 'cd4a3cd20dc61dcbc98324de561a07fd23b3d9702115920c0814b5fb822cc5b7c5bcdaf3fa326d24ed50c5b9c8214d66c75bae34e3a84c25e4d122afccb66eb6', + } + }) + + describe('getEventExpiration', () => { + it('returns true if expiration is a safe integer', () => { + event.tags = [ + ['expiration', '160000000'], + ] + expect(getEventExpiration(event)).to.equal(160000000) + }) + + it('returns false if event does not have expiration tag', () => { + event.tags = [] + expect(getEventExpiration(event)).to.be.undefined + }) + + it('returns false if expiration is unsafe integer', () => { + event.tags = [ + ['expiration', '160000000000000000000'], + ] + expect(getEventExpiration(event)).to.be.undefined + }) + + it('returns false if expiration is malformed data', () => { + event.tags = [ + ['expiration', 'a'], + ] + expect(getEventExpiration(event)).to.be.undefined + }) + }) + + describe('isExpiredEvent', () => { + it('returns false if event does not have tags', () => { + event.tags = [] + expect(isExpiredEvent(event)).to.equal(false) + }) + + it('returns false if event does not have expiration tags', () => { + expect(isExpiredEvent(event)).to.equal(false) + }) + + it('returns true if event is expired', () => { + event.tags = [ + ['expiration', '100000'], + ] + expect(isExpiredEvent(event)).to.equal(true) + }) + }) +}) \ No newline at end of file