Skip to content
Draft
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
4 changes: 4 additions & 0 deletions .changeset/nip-22-created-at-tests-empty.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
---

Add empty changeset for NIP-22 created_at integration test coverage (issue #505).
Comment on lines +1 to +4
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

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

This changeset is declared as empty/tests-only, but this PR also changes runtime behavior in EventMessageHandler (switching a rejection reason prefix to invalid:). If that behavior change is kept, the changeset should include the appropriate package release entry (or the code change should be moved/reverted so the changeset matches the PR scope).

Copilot uses AI. Check for mistakes.
29 changes: 29 additions & 0 deletions test/integration/features/nip-22/nip-22.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
@nip-22
Feature: NIP-22 created_at timestamp limits
Scenario: Event with created_at at current time is accepted
Given someone called Alice
And created_at limits are set to maxPositiveDelta 900 and maxNegativeDelta 0
When Alice drafts a text_note event with content "test event" and created_at 0 seconds from now
Then Alice sends their last draft event successfully
When Alice subscribes to author Alice
Then Alice receives a text_note event from Alice with content "test event"

Scenario: Event with created_at above positive delta limit is rejected
Given someone called Alice
And created_at limits are set to maxPositiveDelta 900 and maxNegativeDelta 0
When Alice drafts a text_note event with content "test event" and created_at 910 seconds from now
Then Alice sends their last draft event unsuccessfully with reason containing "rejected"

Scenario: Event older than configured negative delta limit is rejected
Given someone called Alice
And created_at limits are set to maxPositiveDelta 900 and maxNegativeDelta 3600
When Alice drafts a text_note event with content "test event" and created_at -3601 seconds from now
Then Alice sends their last draft event unsuccessfully with reason containing "rejected"

Scenario: Event within configured negative delta limit is accepted
Given someone called Alice
And created_at limits are set to maxPositiveDelta 900 and maxNegativeDelta 3600
When Alice drafts a text_note event with content "test event" and created_at -3590 seconds from now
Then Alice sends their last draft event successfully
When Alice subscribes to author Alice
Then Alice receives a text_note event from Alice with content "test event"
82 changes: 82 additions & 0 deletions test/integration/features/nip-22/nip-22.feature.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { After, Before, Given, Then, When } from '@cucumber/cucumber'
import { assocPath, pipe } from 'ramda'

import { CommandResult, MessageType } from '../../../../src/@types/messages'
import { createEvent, sendEvent } from '../helpers'

import { Event } from '../../../../src/@types/event'
import { expect } from 'chai'
import { isDraft } from '../shared'
import { SettingsStatic } from '../../../../src/utils/settings'
import WebSocket from 'ws'

const previousSettingsSnapshot = Symbol('nip22PreviousSettingsSnapshot')

const setCreatedAtLimits = (maxPositiveDelta: number, maxNegativeDelta: number) => {
const settings = SettingsStatic._settings ?? SettingsStatic.createSettings()

SettingsStatic._settings = pipe(
assocPath(['limits', 'event', 'createdAt', 'maxPositiveDelta'], maxPositiveDelta),
assocPath(['limits', 'event', 'createdAt', 'maxNegativeDelta'], maxNegativeDelta),
)(settings) as any
}

Before({ tags: '@nip-22' }, function(this: any) {
this[previousSettingsSnapshot] = SettingsStatic._settings
})

After({ tags: '@nip-22' }, function(this: any) {
SettingsStatic._settings = this[previousSettingsSnapshot]
delete this[previousSettingsSnapshot]
})
Comment on lines +15 to +31
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

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

The @nip-22 After hook resets created_at limits to hard-coded defaults (900/0). Integration tests globally override limits.event.createdAt.maxPositiveDelta to 0 in test/integration/features/shared.ts to avoid time-based flakiness; restoring to 900 here will leak settings changes into subsequent scenarios/features and can cause flaky failures. Snapshot the previous settings (or previous createdAt limits) before modifying, and restore that snapshot in After instead of hard-coding values.

Copilot uses AI. Check for mistakes.

Given(/^created_at limits are set to maxPositiveDelta (\d+) and maxNegativeDelta (\d+)$/, function(
maxPositiveDelta: string,
maxNegativeDelta: string,
) {
setCreatedAtLimits(Number(maxPositiveDelta), Number(maxNegativeDelta))
})

When(/^(\w+) drafts a text_note event with content "([^"]+)" and created_at (-?\d+) seconds from now$/, async function(
name: string,
content: string,
offsetSeconds: string,
) {
const { pubkey, privkey } = this.parameters.identities[name]
const createdAt = Math.floor(Date.now() / 1000) + Number(offsetSeconds)

const event: Event = await createEvent(
{
pubkey,
kind: 1,
content,
created_at: createdAt,
Comment on lines +40 to +53
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

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

The rejection test relies on a boundary of +901s vs a maxPositiveDelta of 900s, but createdAt is computed during the draft step and compared against now computed server-side when the event is later sent/handled. If ≥1s passes between computing createdAt and server validation, a +901s event can become effectively ≤+900s and be accepted, making this scenario flaky. Consider either computing created_at immediately before sending (minimize elapsed time) or using a larger buffer above the limit for the failing case while keeping a separate at-the-limit acceptance test.

Copilot uses AI. Check for mistakes.
},
privkey,
)

const draftEvent = event as any
draftEvent[isDraft] = true

this.parameters.events[name].push(event)
})

Then(/^(\w+) sends their last draft event unsuccessfully with reason containing "([^"]+)"$/, async function(
name: string,
expectedReason: string,
) {
const ws = this.parameters.clients[name] as WebSocket

const event = this.parameters.events[name].findLast((lastEvent: Event) => (lastEvent as any)[isDraft])
if (!event) {
throw new Error(`No draft event found for ${name}`)
}

delete (event as any)[isDraft]

const command = await sendEvent(ws, event, false) as CommandResult

expect(command[0]).to.equal(MessageType.OK)
expect(command[2]).to.equal(false)
expect(command[3].toLowerCase()).to.contain(expectedReason.toLowerCase())
})
6 changes: 3 additions & 3 deletions test/unit/handlers/event-message-handler.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -291,9 +291,9 @@ describe('EventMessageHandler', () => {
eventLimits.createdAt.maxPositiveDelta = 100
event.created_at += 101

expect((handler as any).canAcceptEvent(event)).to.equal(
'rejected: created_at is more than 100 seconds in the future',
)
expect(
(handler as any).canAcceptEvent(event)
).to.equal('rejected: created_at is more than 100 seconds in the future')
})
})

Expand Down
Loading