Skip to content

Commit

Permalink
fix: strange behavior with nip 33 parameterized replacable events and…
Browse files Browse the repository at this point in the history
… nip 40 expiration tag (#316)

* fix: fix content-type on GetInvoiceStatusController

* test: fix flaky tests

* test: remove cache client from intg tests

* chore: lint fix

* test: add intg tests for nip-33 events w/ expiration tag
  • Loading branch information
cameri authored Jun 19, 2023
1 parent 33c2fd5 commit d1d4cb9
Show file tree
Hide file tree
Showing 8 changed files with 113 additions and 33 deletions.
10 changes: 5 additions & 5 deletions src/controllers/invoices/get-invoice-status-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export class GetInvoiceStatusController implements IController {
debug('invalid invoice id: %s', invoiceId)
response
.status(400)
.setHeader('content-type', 'text/plain; charset=utf8')
.setHeader('content-type', 'application/json; charset=utf8')
.send({ id: invoiceId, status: 'invalid invoice' })
return
}
Expand All @@ -32,24 +32,24 @@ export class GetInvoiceStatusController implements IController {
debug('invoice not found: %s', invoiceId)
response
.status(404)
.setHeader('content-type', 'text/plain; charset=utf8')
.setHeader('content-type', 'application/json; charset=utf8')
.send({ id: invoiceId, status: 'not found' })
return
}

response
.status(200)
.setHeader('content-type', 'application/json; charset=utf8')
.send(JSON.stringify({
.send({
id: invoice.id,
status: invoice.status,
}))
})
} catch (error) {
console.error(`get-invoice-status-controller: unable to get invoice ${invoiceId}:`, error)

response
.status(500)
.setHeader('content-type', 'text/plain; charset=utf8')
.setHeader('content-type', 'application/json; charset=utf8')
.send({ id: invoiceId, status: 'error' })
}
}
Expand Down
15 changes: 8 additions & 7 deletions src/handlers/event-message-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -290,14 +290,15 @@ export class EventMessageHandler implements IMessageHandler {

protected addExpirationMetadata(event: Event): Event | ExpiringEvent {
const eventExpiration: number = getEventExpiration(event)
if (eventExpiration) {
const expiringEvent: ExpiringEvent = {
...event,
[EventExpirationTimeMetadataKey]: eventExpiration,
}
return expiringEvent
} else {
if (!eventExpiration) {
return event
}

const expiringEvent: ExpiringEvent = {
...event,
[EventExpirationTimeMetadataKey]: eventExpiration,
}

return expiringEvent
}
}
3 changes: 1 addition & 2 deletions src/repositories/event-repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,10 +184,9 @@ export class EventRepository implements IEventRepository {
remote_address: path([ContextMetadataKey as any, 'remoteAddress', 'address']),
expires_at: ifElse(
propSatisfies(is(Number), EventExpirationTimeMetadataKey),
prop(EventExpirationTimeMetadataKey as any),
prop(EventExpirationTimeMetadataKey as any),
always(null),
),

})(event)

return this.masterDbClient('events')
Expand Down
16 changes: 10 additions & 6 deletions src/utils/event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -285,24 +285,28 @@ export const isDeleteEvent = (event: Event): boolean => {
}

export const isExpiredEvent = (event: Event): boolean => {
if (!event.tags.length) return false
if (!event.tags.length) {
return false
}

const expirationTime = getEventExpiration(event)

if (!expirationTime) return false
if (!expirationTime) {
return false
}

const date = new Date()
const isExpired = expirationTime <= Math.floor(date.getTime() / 1000)
const now = Math.floor(new Date().getTime() / 1000)

return isExpired
return expirationTime <= now
}

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))) {

if ((Number.isSafeInteger(expirationTime) && Math.log10(expirationTime) < 10)) {
return expirationTime
}
}
Expand Down
5 changes: 2 additions & 3 deletions test/integration/features/nip-16/nip-16.feature
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
Feature: NIP-16 Event treatment
Scenario: Alice sends a replaceable event
Given someone called Alice
And Alice subscribes to author Alice
When Alice sends a replaceable_event_0 event with content "created"
Then Alice receives a replaceable_event_0 event from Alice with content "created"
When Alice sends a replaceable_event_0 event with content "updated"
And Alice sends a replaceable_event_0 event with content "updated"
And Alice subscribes to author Alice
Then Alice receives a replaceable_event_0 event from Alice with content "updated"
Then Alice unsubscribes from author Alice
When Alice subscribes to author Alice
Expand Down
31 changes: 26 additions & 5 deletions test/integration/features/nip-33/nip-33.feature
Original file line number Diff line number Diff line change
@@ -1,11 +1,32 @@
Feature: NIP-33 Parameterized replaceable events
Scenario: Alice sends a parameterized replaceable event
Given someone called Alice
And Alice subscribes to author Alice
When Alice sends a parameterized_replaceable_event_0 event with content "1" and tag d containing "variable"
Then Alice receives a parameterized_replaceable_event_0 event from Alice with content "1" and tag d containing "variable"
When Alice sends a parameterized_replaceable_event_0 event with content "2" and tag d containing "variable"
Then Alice receives a parameterized_replaceable_event_0 event from Alice with content "2" and tag d containing "variable"
Then Alice unsubscribes from author Alice
When Alice subscribes to author Alice
Then Alice receives 1 parameterized_replaceable_event_0 event from Alice with content "2" and EOSE
Then Alice receives a parameterized_replaceable_event_0 event from Alice with content "2" and tag d containing "variable"

Scenario: Alice adds an expiration tag to a parameterized replaceable event
Given someone called Alice
And someone called Bob
When Alice sends a parameterized_replaceable_event_1 event with content "woot" and tag d containing "stuff"
And Alice sends a parameterized_replaceable_event_1 event with content "nostr.watch" and tag d containing "stuff" and expiring in the future
And Bob subscribes to author Alice
Then Bob receives a parameterized_replaceable_event_1 event from Alice with content "nostr.watch" and tag d containing "stuff"

Scenario: Alice removes an expiration tag to a parameterized replaceable event
Given someone called Alice
And someone called Bob
When Alice sends a parameterized_replaceable_event_1 event with content "nostr.watch" and tag d containing "hey" and expiring in the future
And Alice sends a parameterized_replaceable_event_1 event with content "woot" and tag d containing "hey"
And Bob subscribes to author Alice
Then Bob receives a parameterized_replaceable_event_1 event from Alice with content "woot" and tag d containing "hey"

Scenario: Alice adds and removes an expiration tag to a parameterized replaceable event
Given someone called Alice
And someone called Bob
When Alice sends a parameterized_replaceable_event_1 event with content "first" and tag d containing "friends"
And Alice sends a parameterized_replaceable_event_1 event with content "second" and tag d containing "friends" and expiring in the future
And Alice sends a parameterized_replaceable_event_1 event with content "third" and tag d containing "friends"
And Bob subscribes to author Alice
Then Bob receives a parameterized_replaceable_event_1 event from Alice with content "third" and tag d containing "friends"
60 changes: 60 additions & 0 deletions test/integration/features/nip-33/nip-33.feature.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { expect } from 'chai'
import WebSocket from 'ws'

import { createEvent, sendEvent, waitForEventCount, waitForNextEvent } from '../helpers'
import { EventKinds, EventTags } from '../../../../src/constants/base'
import { Event } from '../../../../src/@types/event'

When(/^(\w+) sends a parameterized_replaceable_event_0 event with content "([^"]+)" and tag (\w) containing "([^"]+)"$/, async function(
Expand All @@ -20,6 +21,52 @@ When(/^(\w+) sends a parameterized_replaceable_event_0 event with content "([^"]
this.parameters.events[name].push(event)
})

When(/^(\w+) sends a parameterized_replaceable_event_1 event with content "([^"]+)" and tag (\w) containing "([^"]+)"$/, async function(
name: string,
content: string,
tag: string,
value: string,
) {
const ws = this.parameters.clients[name] as WebSocket
const { pubkey, privkey } = this.parameters.identities[name]

const event: Event = await createEvent(
{
pubkey,
kind: EventKinds.PARAMETERIZED_REPLACEABLE_FIRST + 1,
content,
tags: [[tag, value]],
},
privkey,
)

await sendEvent(ws, event)
this.parameters.events[name].push(event)
})

When(/^(\w+) sends a parameterized_replaceable_event_1 event with content "([^"]+)" and tag (\w) containing "([^"]+)" and expiring in the future$/, async function(
name: string,
content: string,
tag: string,
value: string,
) {
const ws = this.parameters.clients[name] as WebSocket
const { pubkey, privkey } = this.parameters.identities[name]

const event: Event = await createEvent(
{
pubkey,
kind: EventKinds.PARAMETERIZED_REPLACEABLE_FIRST + 1,
content,
tags: [[tag, value], [EventTags.Expiration, Math.floor(new Date().getTime() / 1000 + 10).toString()]],
},
privkey,
)

await sendEvent(ws, event)
this.parameters.events[name].push(event)
})

Then(
/(\w+) receives a parameterized_replaceable_event_0 event from (\w+) with content "([^"]+?)" and tag (\w+) containing "([^"]+?)"/,
async function(name: string, author: string, content: string, tagName: string, tagValue: string) {
Expand All @@ -33,6 +80,19 @@ Then(
expect(receivedEvent.tags[0]).to.deep.equal([tagName, tagValue])
})

Then(
/(\w+) receives a parameterized_replaceable_event_1 event from (\w+) with content "([^"]+?)" and tag (\w+) containing "([^"]+?)"/,
async function(name: string, author: string, content: string, tagName: string, tagValue: string) {
const ws = this.parameters.clients[name] as WebSocket
const subscription = this.parameters.subscriptions[name][this.parameters.subscriptions[name].length - 1]
const receivedEvent = await waitForNextEvent(ws, subscription.name)

expect(receivedEvent.kind).to.equal(30001)
expect(receivedEvent.pubkey).to.equal(this.parameters.identities[author].pubkey)
expect(receivedEvent.content).to.equal(content)
expect(receivedEvent.tags[0]).to.deep.equal([tagName, tagValue])
})

Then(/(\w+) receives (\d+) parameterized_replaceable_event_0 events? from (\w+) with content "([^"]+?)" and EOSE/, async function(
name: string,
count: string,
Expand Down
6 changes: 1 addition & 5 deletions test/integration/features/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,8 @@ import Sinon from 'sinon'
import { connect, createIdentity, createSubscription, sendEvent } from './helpers'
import { getMasterDbClient, getReadReplicaDbClient } from '../../../src/database/client'
import { AppWorker } from '../../../src/app/worker'
import { CacheClient } from '../../../src/@types/cache'
import { DatabaseClient } from '../../../src/@types/base'
import { Event } from '../../../src/@types/event'
import { getCacheClient } from '../../../src/cache/client'
import { SettingsStatic } from '../../../src/utils/settings'
import { workerFactory } from '../../../src/factories/worker-factory'

Expand All @@ -29,14 +27,12 @@ let worker: AppWorker

let dbClient: DatabaseClient
let rrDbClient: DatabaseClient
let cacheClient: CacheClient

export const streams = new WeakMap<WebSocket, Observable<unknown>>()

BeforeAll({ timeout: 1000 }, async function () {
process.env.RELAY_PORT = '18808'
process.env.SECRET = Math.random().toString().repeat(6)
cacheClient = getCacheClient()
dbClient = getMasterDbClient()
rrDbClient = getReadReplicaDbClient()
await dbClient.raw('SELECT 1=1')
Expand All @@ -58,7 +54,7 @@ BeforeAll({ timeout: 1000 }, async function () {

AfterAll(async function() {
worker.close(async () => {
await Promise.all([cacheClient.disconnect(), dbClient.destroy(), rrDbClient.destroy()])
await Promise.all([dbClient.destroy(), rrDbClient.destroy()])
})
})

Expand Down

0 comments on commit d1d4cb9

Please sign in to comment.