mirror of
https://github.com/Cameri/nostream.git
synced 2025-03-17 21:31:48 +01:00
fix: integration tests
Signed-off-by: Ricardo Arturo Cabral Mejía <me@ricardocabral.io>
This commit is contained in:
parent
3fa30724d7
commit
1db3343ef8
21
package-lock.json
generated
21
package-lock.json
generated
@ -18,6 +18,7 @@
|
||||
"pg-query-stream": "4.2.4",
|
||||
"ramda": "0.28.0",
|
||||
"redis": "4.5.1",
|
||||
"rxjs": "7.8.0",
|
||||
"tor-control-ts": "^1.0.0",
|
||||
"ws": "8.11.0"
|
||||
},
|
||||
@ -10979,10 +10980,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/rxjs": {
|
||||
"version": "7.5.7",
|
||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.7.tgz",
|
||||
"integrity": "sha512-z9MzKh/UcOqB3i20H6rtrlaE/CgjLOvheWK/9ILrbhROGTweAi1BaFsTT9FbwZi5Trr1qNRs+MXkhmR06awzQA==",
|
||||
"dev": true,
|
||||
"version": "7.8.0",
|
||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.0.tgz",
|
||||
"integrity": "sha512-F2+gxDshqmIub1KdvZkaEfGDwLNpPvk9Fs6LD/MyQxNgMds/WH9OdDDXOmxUZpME+iSK3rQCctkL0DYyytUqMg==",
|
||||
"dependencies": {
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
@ -10990,8 +10990,7 @@
|
||||
"node_modules/rxjs/node_modules/tslib": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz",
|
||||
"integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==",
|
||||
"dev": true
|
||||
"integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA=="
|
||||
},
|
||||
"node_modules/safe-buffer": {
|
||||
"version": "5.2.1",
|
||||
@ -20749,10 +20748,9 @@
|
||||
}
|
||||
},
|
||||
"rxjs": {
|
||||
"version": "7.5.7",
|
||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.7.tgz",
|
||||
"integrity": "sha512-z9MzKh/UcOqB3i20H6rtrlaE/CgjLOvheWK/9ILrbhROGTweAi1BaFsTT9FbwZi5Trr1qNRs+MXkhmR06awzQA==",
|
||||
"dev": true,
|
||||
"version": "7.8.0",
|
||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.0.tgz",
|
||||
"integrity": "sha512-F2+gxDshqmIub1KdvZkaEfGDwLNpPvk9Fs6LD/MyQxNgMds/WH9OdDDXOmxUZpME+iSK3rQCctkL0DYyytUqMg==",
|
||||
"requires": {
|
||||
"tslib": "^2.1.0"
|
||||
},
|
||||
@ -20760,8 +20758,7 @@
|
||||
"tslib": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz",
|
||||
"integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==",
|
||||
"dev": true
|
||||
"integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA=="
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -118,6 +118,7 @@
|
||||
"ramda": "0.28.0",
|
||||
"redis": "4.5.1",
|
||||
"tor-control-ts": "^1.0.0",
|
||||
"rxjs": "7.8.0",
|
||||
"ws": "8.11.0"
|
||||
},
|
||||
"config": {
|
||||
|
@ -14,5 +14,6 @@ export interface IEventRepository {
|
||||
create(event: Event): Promise<number>
|
||||
upsert(event: Event): Promise<number>
|
||||
findByFilters(filters: SubscriptionFilter[]): IQueryResult<DBEvent[]>
|
||||
insertStubs(pubkey: string, eventIdsToDelete: EventId[]): Promise<number>
|
||||
deleteByPubkeyAndIds(pubkey: Pubkey, ids: EventId[]): Promise<number>
|
||||
}
|
||||
|
@ -41,7 +41,7 @@ export class WebSocketAdapter extends EventEmitter implements IWebSocketAdapter
|
||||
this.alive = true
|
||||
this.subscriptions = new Map()
|
||||
|
||||
this.clientId = Buffer.from(this.request.headers['sec-websocket-key'], 'base64').toString('hex')
|
||||
this.clientId = Buffer.from(this.request.headers['sec-websocket-key'] as string, 'base64').toString('hex')
|
||||
const remoteIpHeader = this.settings().network?.remote_ip_header ?? 'x-forwarded-for'
|
||||
this.clientAddress = (this.request.headers[remoteIpHeader] ?? this.request.socket.remoteAddress) as string
|
||||
|
||||
@ -88,7 +88,7 @@ export class WebSocketAdapter extends EventEmitter implements IWebSocketAdapter
|
||||
|
||||
public onBroadcast(event: Event): void {
|
||||
this.webSocketServer.emit(WebSocketServerAdapterEvent.Broadcast, event)
|
||||
if (cluster.isWorker) {
|
||||
if (cluster.isWorker && typeof process.send === 'function') {
|
||||
process.send({
|
||||
eventName: WebSocketServerAdapterEvent.Broadcast,
|
||||
event,
|
||||
@ -99,8 +99,9 @@ export class WebSocketAdapter extends EventEmitter implements IWebSocketAdapter
|
||||
public onSendEvent(event: Event): void {
|
||||
this.subscriptions.forEach((filters, subscriptionId) => {
|
||||
if (
|
||||
filters.map(isEventMatchingFilter).some((Matches) => Matches(event))
|
||||
filters.map(isEventMatchingFilter).some((isMatch) => isMatch(event))
|
||||
) {
|
||||
debug('sending event to client %s: %o', this.clientId, event)
|
||||
this.sendMessage(createOutgoingEventMessage(subscriptionId, event))
|
||||
}
|
||||
})
|
||||
@ -134,7 +135,7 @@ export class WebSocketAdapter extends EventEmitter implements IWebSocketAdapter
|
||||
|
||||
private async onClientMessage(raw: Buffer) {
|
||||
let abortable = false
|
||||
let messageHandler: IMessageHandler & IAbortable
|
||||
let messageHandler: IMessageHandler & IAbortable | undefined = undefined
|
||||
try {
|
||||
if (await this.isRateLimited(this.clientAddress)) {
|
||||
this.sendMessage(createNoticeMessage('rate limited'))
|
||||
@ -144,7 +145,12 @@ export class WebSocketAdapter extends EventEmitter implements IWebSocketAdapter
|
||||
const message = attemptValidation(messageSchema)(JSON.parse(raw.toString('utf8')))
|
||||
|
||||
messageHandler = this.createMessageHandler([message, this]) as IMessageHandler & IAbortable
|
||||
abortable = typeof messageHandler?.abort === 'function'
|
||||
if (!messageHandler) {
|
||||
debug('unhandled message: no handler found: %o', message)
|
||||
return
|
||||
}
|
||||
|
||||
abortable = typeof messageHandler.abort === 'function'
|
||||
|
||||
if (abortable) {
|
||||
const handlers = abortableMessageHandlers.get(this.client) ?? []
|
||||
@ -152,7 +158,7 @@ export class WebSocketAdapter extends EventEmitter implements IWebSocketAdapter
|
||||
abortableMessageHandlers.set(this.client, handlers)
|
||||
}
|
||||
|
||||
await messageHandler?.handleMessage(message)
|
||||
await messageHandler.handleMessage(message)
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
if (error.name === 'AbortError') {
|
||||
@ -169,11 +175,13 @@ export class WebSocketAdapter extends EventEmitter implements IWebSocketAdapter
|
||||
console.error('unable to handle message', error)
|
||||
}
|
||||
} finally {
|
||||
if (abortable) {
|
||||
if (abortable && messageHandler) {
|
||||
const handlers = abortableMessageHandlers.get(this.client)
|
||||
const index = handlers.indexOf(messageHandler)
|
||||
if (index >= 0) {
|
||||
handlers.splice(index, 1)
|
||||
if (handlers) {
|
||||
const index = handlers.indexOf(messageHandler)
|
||||
if (index >= 0) {
|
||||
handlers.splice(index, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -185,7 +193,7 @@ export class WebSocketAdapter extends EventEmitter implements IWebSocketAdapter
|
||||
ipWhitelist = [],
|
||||
} = this.settings().limits?.message ?? {}
|
||||
|
||||
if (ipWhitelist.includes(client)) {
|
||||
if (!Array.isArray(rateLimits) || !rateLimits.length || ipWhitelist.includes(client)) {
|
||||
return false
|
||||
}
|
||||
|
||||
|
@ -57,8 +57,7 @@ export class WebSocketServerAdapter extends WebServerAdapter implements IWebSock
|
||||
if (!propEq('readyState', OPEN)(webSocket)) {
|
||||
return
|
||||
}
|
||||
const webSocketAdapter = this.webSocketsAdapters.get(webSocket)
|
||||
debug('broadcasting event to client %s: %o', webSocketAdapter.getClientId(), event)
|
||||
const webSocketAdapter = this.webSocketsAdapters.get(webSocket) as IWebSocketAdapter
|
||||
webSocketAdapter.emit(WebSocketAdapterEvent.Event, event)
|
||||
})
|
||||
}
|
||||
@ -73,9 +72,10 @@ export class WebSocketServerAdapter extends WebServerAdapter implements IWebSock
|
||||
}
|
||||
|
||||
private onHeartbeat() {
|
||||
this.webSocketServer.clients.forEach((webSocket) =>
|
||||
this.webSocketsAdapters.get(webSocket).emit(WebSocketAdapterEvent.Heartbeat)
|
||||
)
|
||||
this.webSocketServer.clients.forEach((webSocket) => {
|
||||
const webSocketAdapter = this.webSocketsAdapters.get(webSocket) as IWebSocketAdapter
|
||||
webSocketAdapter.emit(WebSocketAdapterEvent.Heartbeat)
|
||||
})
|
||||
}
|
||||
|
||||
protected onClose() {
|
||||
|
@ -41,6 +41,6 @@ export class AppWorker implements IRunnable {
|
||||
|
||||
public close(callback?: () => void) {
|
||||
debug('closing')
|
||||
this.adapter.close(callback)
|
||||
this.adapter.close(callback as () => void)
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import { EventTags } from '../../constants/base'
|
||||
import { IEventRepository } from '../../@types/repositories'
|
||||
import { IEventStrategy } from '../../@types/message-handlers'
|
||||
import { IWebSocketAdapter } from '../../@types/adapters'
|
||||
import { Tag } from '../../@types/base'
|
||||
import { WebSocketAdapterEvent } from '../../constants/adapter'
|
||||
|
||||
const debug = createLogger('delete-event-strategy')
|
||||
@ -17,25 +18,37 @@ export class DeleteEventStrategy implements IEventStrategy<Event, Promise<void>>
|
||||
|
||||
public async execute(event: Event): Promise<void> {
|
||||
debug('received delete event: %o', event)
|
||||
|
||||
const count = await this.eventRepository.create(event)
|
||||
this.webSocket.emit(WebSocketAdapterEvent.Message, createCommandResult(event.id, true, (count) ? '' : 'duplicate:'))
|
||||
|
||||
const ids = event.tags.reduce(
|
||||
(eventIds, tag) => (tag.length >= 2 && tag[0] === EventTags.Event)
|
||||
if (count) {
|
||||
this.webSocket.emit(WebSocketAdapterEvent.Broadcast, event)
|
||||
}
|
||||
|
||||
const isValidETag = (tag: Tag) =>
|
||||
tag.length >= 2
|
||||
&& tag[0] === EventTags.Event
|
||||
&& /^[0-9a-f]{64}$/.test(tag[1])
|
||||
|
||||
const eventIdsToDelete = event.tags.reduce(
|
||||
(eventIds, tag) => isValidETag(tag)
|
||||
? [...eventIds, tag[1]]
|
||||
: eventIds,
|
||||
[] as string[]
|
||||
)
|
||||
|
||||
if (ids.length) {
|
||||
await this.eventRepository.deleteByPubkeyAndIds(
|
||||
if (eventIdsToDelete.length) {
|
||||
const count = await this.eventRepository.deleteByPubkeyAndIds(
|
||||
event.pubkey,
|
||||
ids
|
||||
eventIdsToDelete
|
||||
)
|
||||
}
|
||||
|
||||
if (count) {
|
||||
this.webSocket.emit(WebSocketAdapterEvent.Broadcast, event)
|
||||
if (!count) {
|
||||
await this.eventRepository.insertStubs(
|
||||
event.pubkey,
|
||||
eventIdsToDelete,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { createCommandResult } from '../../utils/messages'
|
||||
import { createLogger } from '../../factories/logger-factory'
|
||||
import { Event } from '../../@types/event'
|
||||
import { IEventStrategy } from '../../@types/message-handlers'
|
||||
@ -13,6 +14,10 @@ export class EphemeralEventStrategy implements IEventStrategy<Event, Promise<voi
|
||||
|
||||
public async execute(event: Event): Promise<void> {
|
||||
debug('received ephemeral event: %o', event)
|
||||
this.webSocket.emit(
|
||||
WebSocketAdapterEvent.Message,
|
||||
createCommandResult(event.id, true, ''),
|
||||
)
|
||||
this.webSocket.emit(WebSocketAdapterEvent.Broadcast, event)
|
||||
}
|
||||
}
|
||||
|
@ -16,10 +16,30 @@ export class ReplaceableEventStrategy implements IEventStrategy<Event, Promise<v
|
||||
|
||||
public async execute(event: Event): Promise<void> {
|
||||
debug('received replaceable event: %o', event)
|
||||
const count = await this.eventRepository.upsert(event)
|
||||
this.webSocket.emit(WebSocketAdapterEvent.Message, createCommandResult(event.id, true, (count) ? '' : 'duplicate:'))
|
||||
if (count) {
|
||||
this.webSocket.emit(WebSocketAdapterEvent.Broadcast, event)
|
||||
try {
|
||||
const count = await this.eventRepository.upsert(event)
|
||||
this.webSocket.emit(
|
||||
WebSocketAdapterEvent.Message,
|
||||
createCommandResult(event.id, true, (count) ? '' : 'duplicate:'),
|
||||
)
|
||||
if (count) {
|
||||
this.webSocket.emit(WebSocketAdapterEvent.Broadcast, event)
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
if (error.message.endsWith('duplicate key value violates unique constraint "events_event_id_unique"')) {
|
||||
this.webSocket.emit(
|
||||
WebSocketAdapterEvent.Message,
|
||||
createCommandResult(event.id, false, 'rejected: event already exists'),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
this.webSocket.emit(
|
||||
WebSocketAdapterEvent.Message,
|
||||
createCommandResult(event.id, false, 'error: '),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,13 +1,13 @@
|
||||
import { anyPass, equals, map, uniqWith } from 'ramda'
|
||||
import { anyPass, equals, isNil, map, propSatisfies, uniqWith } from 'ramda'
|
||||
import { pipeline } from 'stream/promises'
|
||||
|
||||
import { createEndOfStoredEventsNoticeMessage, createNoticeMessage, createOutgoingEventMessage } from '../utils/messages'
|
||||
import { DBEvent, Event } from '../@types/event'
|
||||
import { IAbortable, IMessageHandler } from '../@types/message-handlers'
|
||||
import { isEventMatchingFilter, toNostrEvent } from '../utils/event'
|
||||
import { streamEach, streamEnd, streamFilter, streamMap } from '../utils/stream'
|
||||
import { SubscriptionFilter, SubscriptionId } from '../@types/subscription'
|
||||
import { createLogger } from '../factories/logger-factory'
|
||||
import { Event } from '../@types/event'
|
||||
import { IEventRepository } from '../@types/repositories'
|
||||
import { ISettings } from '../@types/settings'
|
||||
import { IWebSocketAdapter } from '../@types/adapters'
|
||||
@ -57,12 +57,10 @@ export class SubscribeMessageHandler implements IMessageHandler, IAbortable {
|
||||
|
||||
const findEvents = this.eventRepository.findByFilters(filters).stream()
|
||||
|
||||
const isNotDeleted = (row: DBEvent) => { console.log(row); return true }
|
||||
|
||||
try {
|
||||
await pipeline(
|
||||
findEvents,
|
||||
streamFilter(isNotDeleted),
|
||||
streamFilter(propSatisfies(isNil, 'deleted_at')),
|
||||
streamMap(toNostrEvent),
|
||||
streamFilter(isSubscribedToEvent),
|
||||
streamEach(sendEvent),
|
||||
|
@ -10,6 +10,7 @@ import {
|
||||
forEach,
|
||||
forEachObjIndexed,
|
||||
groupBy,
|
||||
identity,
|
||||
ifElse,
|
||||
invoker,
|
||||
is,
|
||||
@ -229,11 +230,32 @@ export class EventRepository implements IEventRepository {
|
||||
} as Promise<number>
|
||||
}
|
||||
|
||||
public deleteByPubkeyAndIds(pubkey: string, ids: EventId[]): Promise<number> {
|
||||
debug('deleting events from %s: %o', pubkey, ids)
|
||||
public insertStubs(pubkey: string, eventIdsToDelete: EventId[]): Promise<number> {
|
||||
return this.dbClient('events').insert(
|
||||
eventIdsToDelete.map(
|
||||
applySpec({
|
||||
event_id: pipe(identity, toBuffer),
|
||||
event_pubkey: pipe(always(pubkey), toBuffer),
|
||||
event_created_at: always(Math.floor(Date.now() / 1000)),
|
||||
event_kind: always(5),
|
||||
event_tags: always('[]'),
|
||||
event_content: always(''),
|
||||
event_signature: pipe(always(''), toBuffer),
|
||||
event_delegator: always(null),
|
||||
event_deduplication: pipe(always([pubkey, 5]), toJSON),
|
||||
})
|
||||
)
|
||||
)
|
||||
.onConflict()
|
||||
.ignore() as Promise<any>
|
||||
}
|
||||
|
||||
public deleteByPubkeyAndIds(pubkey: string, eventIdsToDelete: EventId[]): Promise<number> {
|
||||
debug('deleting events from %s: %o', pubkey, eventIdsToDelete)
|
||||
|
||||
return this.dbClient('events')
|
||||
.where('event_pubkey', toBuffer(pubkey))
|
||||
.whereIn('event_id', map(toBuffer)(ids))
|
||||
.whereIn('event_id', map(toBuffer)(eventIdsToDelete))
|
||||
.whereNull('deleted_at')
|
||||
.update({
|
||||
deleted_at: this.dbClient.raw('now()'),
|
||||
|
@ -17,7 +17,7 @@ services:
|
||||
REDIS_USER: default
|
||||
REDIS_PASSWORD: nostr_ts_relay_test
|
||||
NOSTR_CONFIG_DIR: /code
|
||||
DEBUG: knex:query,primary:event-repository
|
||||
DEBUG: ""
|
||||
volumes:
|
||||
- ../../package.json:/code/package.json
|
||||
- ../../settings.sample.json:/code/settings.sample.json
|
||||
|
@ -1,30 +1,31 @@
|
||||
import * as secp256k1 from '@noble/secp256k1'
|
||||
import { createHash, createHmac, Hash } from 'crypto'
|
||||
import WebSocket, { RawData } from 'ws'
|
||||
import { Observable } from 'rxjs'
|
||||
import WebSocket from 'ws'
|
||||
|
||||
import { CommandResult, MessageType, OutgoingMessage } from '../../../src/@types/messages'
|
||||
import { Event } from '../../../src/@types/event'
|
||||
import { MessageType } from '../../../src/@types/messages'
|
||||
import { serializeEvent } from '../../../src/utils/event'
|
||||
import { streams } from './shared'
|
||||
import { SubscriptionFilter } from '../../../src/@types/subscription'
|
||||
|
||||
|
||||
secp256k1.utils.sha256Sync = (...messages: Uint8Array[]) =>
|
||||
messages.reduce((hash: Hash, message: Uint8Array) => hash.update(message), createHash('sha256')).digest()
|
||||
|
||||
export async function connect(_name: string) {
|
||||
export async function connect(_name: string): Promise<WebSocket> {
|
||||
const host = 'ws://localhost:18808'
|
||||
const ws = new WebSocket(host)
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
return new Promise<WebSocket>((resolve, reject) => {
|
||||
ws
|
||||
.once('open', () => {
|
||||
resolve()
|
||||
resolve(ws)
|
||||
})
|
||||
.once('error', reject)
|
||||
.once('close', () => {
|
||||
ws.removeAllListeners()
|
||||
})
|
||||
})
|
||||
return ws
|
||||
}
|
||||
|
||||
let eventCount = 0
|
||||
@ -71,87 +72,80 @@ export async function createSubscription(
|
||||
subscriptionFilters: SubscriptionFilter[],
|
||||
): Promise<void> {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const message = JSON.stringify([
|
||||
const data = JSON.stringify([
|
||||
'REQ',
|
||||
subscriptionName,
|
||||
...subscriptionFilters,
|
||||
])
|
||||
|
||||
ws.send(message, (error?: Error) => {
|
||||
ws.send(data, (error?: Error) => {
|
||||
if (error) {
|
||||
reject(error)
|
||||
return
|
||||
} else {
|
||||
resolve()
|
||||
}
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export async function waitForEOSE(ws: WebSocket, subscription: string): Promise<void> {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
function cleanup() {
|
||||
ws.removeListener('message', onMessage)
|
||||
ws.removeListener('error', onError)
|
||||
}
|
||||
const observable = streams.get(ws) as Observable<OutgoingMessage>
|
||||
|
||||
function onError(error: Error) {
|
||||
reject(error)
|
||||
cleanup()
|
||||
}
|
||||
ws.once('error', onError)
|
||||
|
||||
function onMessage(raw: RawData) {
|
||||
const message = JSON.parse(raw.toString('utf8'))
|
||||
const sub = observable.subscribe((message: OutgoingMessage) => {
|
||||
if (message[0] === MessageType.EOSE && message[1] === subscription) {
|
||||
resolve()
|
||||
cleanup()
|
||||
sub.unsubscribe()
|
||||
} else if (message[0] === MessageType.NOTICE) {
|
||||
reject(new Error(message[1]))
|
||||
cleanup()
|
||||
sub.unsubscribe()
|
||||
}
|
||||
}
|
||||
|
||||
ws.on('message', onMessage)
|
||||
})
|
||||
}
|
||||
|
||||
export async function sendEvent(ws: WebSocket, event: Event) {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
ws.send(JSON.stringify(['EVENT', event]), (err) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
return
|
||||
}
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export async function waitForNextEvent(ws: WebSocket, subscription: string): Promise<Event> {
|
||||
export async function sendEvent(ws: WebSocket, event: Event, successful = true) {
|
||||
return new Promise<OutgoingMessage>((resolve, reject) => {
|
||||
const observable = streams.get(ws) as Observable<OutgoingMessage>
|
||||
|
||||
const sub = observable.subscribe((message: OutgoingMessage) => {
|
||||
if (message[0] === MessageType.OK && message[1] === event.id) {
|
||||
if (message[2] === successful) {
|
||||
sub.unsubscribe()
|
||||
resolve(message)
|
||||
} else {
|
||||
sub.unsubscribe()
|
||||
reject(new Error(message[3]))
|
||||
}
|
||||
} else if (message[0] === MessageType.NOTICE) {
|
||||
sub.unsubscribe()
|
||||
reject(new Error(message[1]))
|
||||
}
|
||||
})
|
||||
|
||||
ws.send(JSON.stringify(['EVENT', event]), (err) => {
|
||||
if (err) {
|
||||
sub.unsubscribe()
|
||||
reject(err)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export async function waitForNextEvent(ws: WebSocket, subscription: string, content?: string): Promise<Event> {
|
||||
return new Promise((resolve, reject) => {
|
||||
ws.on('message', onMessage)
|
||||
ws.once('error', onError)
|
||||
const observable = streams.get(ws) as Observable<OutgoingMessage>
|
||||
|
||||
function cleanup() {
|
||||
ws.removeListener('message', onMessage)
|
||||
ws.removeListener('error', onError)
|
||||
}
|
||||
|
||||
function onError(error: Error) {
|
||||
reject(error)
|
||||
cleanup()
|
||||
}
|
||||
|
||||
function onMessage(raw: RawData) {
|
||||
const message = JSON.parse(raw.toString('utf8'))
|
||||
observable.subscribe((message: OutgoingMessage) => {
|
||||
if (message[0] === MessageType.EVENT && message[1] === subscription) {
|
||||
resolve(message[2])
|
||||
cleanup()
|
||||
const event = message[2] as Event
|
||||
if (typeof content !== 'string' || event.content === content) {
|
||||
resolve(message[2])
|
||||
}
|
||||
} else if (message[0] === MessageType.NOTICE) {
|
||||
reject(new Error(message[1]))
|
||||
cleanup()
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@ -164,27 +158,15 @@ export async function waitForEventCount(
|
||||
const events: Event[] = []
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
ws.on('message', onMessage)
|
||||
ws.once('error', onError)
|
||||
function cleanup() {
|
||||
ws.removeListener('message', onMessage)
|
||||
ws.removeListener('error', onError)
|
||||
}
|
||||
const observable = streams.get(ws) as Observable<OutgoingMessage>
|
||||
|
||||
function onError(error: Error) {
|
||||
reject(error)
|
||||
cleanup()
|
||||
}
|
||||
function onMessage(raw: RawData) {
|
||||
const message = JSON.parse(raw.toString('utf8'))
|
||||
observable.subscribe((message: OutgoingMessage) => {
|
||||
if (message[0] === MessageType.EVENT && message[1] === subscription) {
|
||||
events.push(message[2])
|
||||
if (!eose && events.length === count) {
|
||||
resolve(events)
|
||||
cleanup()
|
||||
} else if (events.length > count) {
|
||||
reject(new Error(`Expected ${count} but got ${events.length} events`))
|
||||
cleanup()
|
||||
}
|
||||
} else if (message[0] === MessageType.EOSE && message[1] === subscription) {
|
||||
if (!eose) {
|
||||
@ -194,61 +176,33 @@ export async function waitForEventCount(
|
||||
} else {
|
||||
resolve(events)
|
||||
}
|
||||
cleanup()
|
||||
} else if (message[0] === MessageType.NOTICE) {
|
||||
reject(new Error(message[1]))
|
||||
cleanup()
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export async function waitForNotice(ws: WebSocket): Promise<void> {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
function cleanup() {
|
||||
ws.removeListener('message', onMessage)
|
||||
ws.removeListener('error', onError)
|
||||
}
|
||||
export async function waitForNotice(ws: WebSocket): Promise<string> {
|
||||
return new Promise<string>((resolve) => {
|
||||
const observable = streams.get(ws) as Observable<OutgoingMessage>
|
||||
|
||||
function onError(error: Error) {
|
||||
reject(error)
|
||||
cleanup()
|
||||
}
|
||||
ws.once('error', onError)
|
||||
|
||||
function onMessage(raw: RawData) {
|
||||
const message = JSON.parse(raw.toString('utf8'))
|
||||
observable.subscribe((message: OutgoingMessage) => {
|
||||
if (message[0] === MessageType.NOTICE) {
|
||||
resolve(message[1])
|
||||
cleanup()
|
||||
}
|
||||
}
|
||||
|
||||
ws.on('message', onMessage)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export async function waitForCommand(ws: WebSocket): Promise<any> {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
function cleanup() {
|
||||
ws.removeListener('message', onMessage)
|
||||
ws.removeListener('error', onError)
|
||||
}
|
||||
export async function waitForCommand(ws: WebSocket): Promise<CommandResult> {
|
||||
return new Promise<CommandResult>((resolve) => {
|
||||
const observable = streams.get(ws) as Observable<OutgoingMessage>
|
||||
|
||||
function onError(error: Error) {
|
||||
reject(error)
|
||||
cleanup()
|
||||
}
|
||||
ws.once('error', onError)
|
||||
|
||||
function onMessage(raw: RawData) {
|
||||
const message = JSON.parse(raw.toString('utf8'))
|
||||
observable.subscribe((message: OutgoingMessage) => {
|
||||
if (message[0] === MessageType.OK) {
|
||||
resolve(message)
|
||||
cleanup()
|
||||
}
|
||||
}
|
||||
|
||||
ws.on('message', onMessage)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
@ -8,42 +8,48 @@ Feature: NIP-01
|
||||
|
||||
Scenario: Alice posts a set_metadata event
|
||||
Given someone called Alice
|
||||
And Alice subscribes to author Alice
|
||||
When Alice sends a set_metadata event
|
||||
And Alice subscribes to author Alice
|
||||
Then Alice receives a set_metadata event from Alice
|
||||
|
||||
Scenario: Alice reposts a set_metadata event
|
||||
Given someone called Alice
|
||||
When Alice sends a set_metadata event
|
||||
And Alice subscribes to author Alice
|
||||
Then Alice receives a set_metadata event from Alice
|
||||
|
||||
Scenario: Alice posts a text_note event
|
||||
Given someone called Alice
|
||||
And Alice subscribes to author Alice
|
||||
When Alice sends a text_note event with content "hello world"
|
||||
And Alice subscribes to author Alice
|
||||
Then Alice receives a text_note event from Alice with content "hello world"
|
||||
|
||||
Scenario: Alice posts a recommend_server event
|
||||
Given someone called Alice
|
||||
And Alice subscribes to author Alice
|
||||
When Alice sends a recommend_server event with content "https://nostr-relay.wlvs.space"
|
||||
And Alice subscribes to author Alice
|
||||
Then Alice receives a recommend_server event from Alice with content "https://nostr-relay.wlvs.space"
|
||||
|
||||
Scenario: Alice can't post a text_note event with an invalid signature
|
||||
Given someone called Alice
|
||||
When Alice sends a text_note event with invalid signature
|
||||
Then Alice receives an unsuccessful result
|
||||
When Alice drafts a text_note event with invalid signature
|
||||
Then Alice sends their last draft event unsuccessfully
|
||||
|
||||
Scenario: Alice and Bob exchange text_note events
|
||||
Given someone called Alice
|
||||
And someone called Bob
|
||||
And Alice subscribes to author Bob
|
||||
And Bob subscribes to author Alice
|
||||
When Bob sends a text_note event with content "hello alice"
|
||||
And Alice subscribes to author Bob
|
||||
Then Alice receives a text_note event from Bob with content "hello alice"
|
||||
When Alice sends a text_note event with content "hello bob"
|
||||
And Bob subscribes to author Alice
|
||||
Then Bob receives a text_note event from Alice with content "hello bob"
|
||||
|
||||
Scenario: Alice is interested in text_note events
|
||||
Given someone called Alice
|
||||
And someone called Bob
|
||||
And Alice subscribes to text_note events
|
||||
When Bob sends a text_note event with content "hello nostr"
|
||||
And Alice subscribes to text_note events
|
||||
Then Alice receives a text_note event from Bob with content "hello nostr"
|
||||
|
||||
Scenario: Alice is interested in the #NostrNovember hashtag
|
||||
@ -57,15 +63,14 @@ Feature: NIP-01
|
||||
Given someone called Alice
|
||||
And someone called Bob
|
||||
And someone called Charlie
|
||||
And Bob subscribes to author Bob
|
||||
And Charlie subscribes to author Charlie
|
||||
|
||||
When Bob sends a text_note event with content "I'm Bob"
|
||||
And Bob subscribes to author Bob
|
||||
And Bob receives a text_note event from Bob with content "I'm Bob"
|
||||
And Charlie sends a set_metadata event
|
||||
And Charlie subscribes to author Charlie
|
||||
And Charlie receives a set_metadata event from Charlie
|
||||
And Alice subscribes to text_note events from Bob and set_metadata events from Charlie
|
||||
|
||||
Then Alice receives 2 events from Bob and Charlie
|
||||
|
||||
Scenario: Alice is interested in Bob's events from back in November
|
||||
|
@ -18,6 +18,7 @@ import {
|
||||
waitForNotice,
|
||||
} from '../helpers'
|
||||
import { Event } from '../../../../src/@types/event'
|
||||
import { isDraft } from '../shared'
|
||||
|
||||
chai.use(sinonChai)
|
||||
const { expect } = chai
|
||||
@ -132,15 +133,15 @@ When(/^(\w+) sends a text_note event with content "([^"]+)" on (\d+)$/, async fu
|
||||
this.parameters.events[name].push(event)
|
||||
})
|
||||
|
||||
When(/(\w+) sends a text_note event with invalid signature/, async function(name: string) {
|
||||
const ws = this.parameters.clients[name] as WebSocket
|
||||
When(/(\w+) drafts a text_note event with invalid signature/, async function(name: string) {
|
||||
const { pubkey, privkey } = this.parameters.identities[name]
|
||||
|
||||
const event: Event = await createEvent({ pubkey, kind: 1, content: "I'm cheating" }, privkey)
|
||||
|
||||
event.sig = 'f'.repeat(128)
|
||||
|
||||
await sendEvent(ws, event)
|
||||
event[isDraft] = true
|
||||
|
||||
this.parameters.events[name].push(event)
|
||||
})
|
||||
|
||||
@ -157,7 +158,9 @@ When(/(\w+) sends a recommend_server event with content "(.+?)"/, async function
|
||||
Then(/(\w+) receives a set_metadata event from (\w+)/, async function(name: string, author: 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)
|
||||
const event = this.parameters.events[author][this.parameters.events[author].length - 1]
|
||||
|
||||
const receivedEvent = await waitForNextEvent(ws, subscription.name, event.content)
|
||||
|
||||
expect(receivedEvent.kind).to.equal(0)
|
||||
expect(receivedEvent.pubkey).to.equal(this.parameters.identities[author].pubkey)
|
||||
@ -166,7 +169,7 @@ Then(/(\w+) receives a set_metadata event from (\w+)/, async function(name: stri
|
||||
Then(/(\w+) receives a text_note event from (\w+) with content "([^"]+?)"/, async function(name: string, author: string, content: 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)
|
||||
const receivedEvent = await waitForNextEvent(ws, subscription.name, content)
|
||||
|
||||
expect(receivedEvent.kind).to.equal(1)
|
||||
expect(receivedEvent.pubkey).to.equal(this.parameters.identities[author].pubkey)
|
||||
@ -181,7 +184,7 @@ Then(/(\w+) receives a text_note event from (\w+) with content "(.+?)" on (\d+)/
|
||||
) {
|
||||
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)
|
||||
const receivedEvent = await waitForNextEvent(ws, subscription.name, content)
|
||||
|
||||
expect(receivedEvent.kind).to.equal(1)
|
||||
expect(receivedEvent.pubkey).to.equal(this.parameters.identities[author].pubkey)
|
||||
@ -225,7 +228,7 @@ Then(/(\w+) receives (\d+) events from (\w+) and (\w+)/, async function(
|
||||
Then(/(\w+) receives a recommend_server event from (\w+) with content "(.+?)"/, async function(name: string, author: string, content: 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)
|
||||
const receivedEvent = await waitForNextEvent(ws, subscription.name, content)
|
||||
|
||||
expect(receivedEvent.kind).to.equal(2)
|
||||
expect(receivedEvent.pubkey).to.equal(this.parameters.identities[author].pubkey)
|
||||
|
@ -1,14 +1,34 @@
|
||||
@Test
|
||||
Feature: NIP-09
|
||||
Scenario: Charlie deletes an event
|
||||
Given someone called Charlie
|
||||
Scenario: Alice deletes her text_note
|
||||
Given someone called Alice
|
||||
And someone called Bob
|
||||
And Charlie sends a text_note event with content "Twitter > Nostr"
|
||||
And Charlie subscribes to author Charlie
|
||||
And Charlie receives a text_note event from Charlie with content "Twitter > Nostr"
|
||||
And Charlie unsubscribes from author Charlie
|
||||
When Charlie sends a delete event for their last event
|
||||
And Charlie subscribes to author Charlie
|
||||
And Charlie receives 1 delete event from Charlie and EOSE
|
||||
Then Bob subscribes to author Charlie
|
||||
Then Bob receives 1 delete event from Charlie and EOSE
|
||||
And Alice sends a text_note event with content "Twitter > Nostr"
|
||||
When Alice sends a delete event for their last event
|
||||
And Alice subscribes to author Alice
|
||||
Then Alice receives 1 delete event from Alice and EOSE
|
||||
|
||||
Scenario: Alice deletes her set_metadata
|
||||
Given someone called Alice
|
||||
And someone called Bob
|
||||
And Alice drafts a set_metadata event
|
||||
When Alice sends a delete event for their last event
|
||||
And Alice subscribes to author Alice
|
||||
Then Alice receives 1 delete event from Alice and EOSE
|
||||
|
||||
Scenario: Alice sends a delete before deleted text_note
|
||||
Given someone called Alice
|
||||
And someone called Bob
|
||||
And Alice drafts a text_note event with content "Twitter > Nostr"
|
||||
When Alice sends a delete event for their last event
|
||||
And Alice sends their last draft event successfully
|
||||
And Alice subscribes to author Alice
|
||||
Then Alice receives 1 delete event from Alice and EOSE
|
||||
|
||||
Scenario: Alice sends a delete before deleted set_metadata
|
||||
Given someone called Alice
|
||||
And someone called Bob
|
||||
And Alice drafts a set_metadata event
|
||||
When Alice sends a delete event for their last event
|
||||
And Alice sends their last draft event unsuccessfully
|
||||
And Alice subscribes to author Alice
|
||||
Then Alice receives 1 delete event from Alice and EOSE
|
||||
|
@ -1,10 +1,11 @@
|
||||
import { Then, When } from '@cucumber/cucumber'
|
||||
import { Given, Then, When } from '@cucumber/cucumber'
|
||||
import { expect } from 'chai'
|
||||
import WebSocket from 'ws'
|
||||
|
||||
import { createEvent, sendEvent, waitForEventCount } from '../helpers'
|
||||
import { Event } from '../../../../src/@types/event'
|
||||
import { EventTags } from '../../../../src/constants/base'
|
||||
import { isDraft } from '../shared'
|
||||
import { Tag } from '../../../../src/@types/base'
|
||||
|
||||
When(/^(\w+) sends a delete event for their last event$/, async function(
|
||||
@ -19,14 +20,13 @@ When(/^(\w+) sends a delete event for their last event$/, async function(
|
||||
|
||||
const event: Event = await createEvent({ pubkey, kind: 5, content: '', tags }, privkey)
|
||||
|
||||
console.log('event', event)
|
||||
|
||||
await sendEvent(ws, event)
|
||||
|
||||
this.parameters.events[name].push(event)
|
||||
})
|
||||
|
||||
Then(
|
||||
/(\w+) receives (\d+) delete events? from (\w+) and EOSE/,
|
||||
/(\w+) receives (\d+) delete events? from (\w+) and EOSE$/,
|
||||
async function(name: string, count: string, author: string) {
|
||||
const ws = this.parameters.clients[name] as WebSocket
|
||||
const subscription = this.parameters.subscriptions[name][this.parameters.subscriptions[name].length - 1]
|
||||
@ -34,4 +34,36 @@ Then(
|
||||
|
||||
expect(event.kind).to.equal(5)
|
||||
expect(event.pubkey).to.equal(this.parameters.identities[author].pubkey)
|
||||
})
|
||||
})
|
||||
|
||||
Then(
|
||||
/(\w+) receives (\d+) delete events? from (\w+)$/,
|
||||
async function(name: string, count: string, author: string) {
|
||||
const ws = this.parameters.clients[name] as WebSocket
|
||||
const subscription = this.parameters.subscriptions[name][this.parameters.subscriptions[name].length - 1]
|
||||
const [event] = await waitForEventCount(ws, subscription.name, Number(count), false)
|
||||
|
||||
expect(event.kind).to.equal(5)
|
||||
expect(event.pubkey).to.equal(this.parameters.identities[author].pubkey)
|
||||
})
|
||||
|
||||
When(/^(\w+) drafts a text_note event with content "([^"]+)"$/, async function(name: string, content: string) {
|
||||
const { pubkey, privkey } = this.parameters.identities[name]
|
||||
|
||||
const event: Event = await createEvent({ pubkey, kind: 1, content }, privkey)
|
||||
|
||||
event[isDraft] = true
|
||||
|
||||
this.parameters.events[name].push(event)
|
||||
})
|
||||
|
||||
Given(/^(\w+) drafts a set_metadata event$/, async function(name: string) {
|
||||
const { pubkey, privkey } = this.parameters.identities[name]
|
||||
|
||||
const content = JSON.stringify({ name })
|
||||
const event: Event = await createEvent({ pubkey, kind: 0, content }, privkey)
|
||||
|
||||
event[isDraft] = true
|
||||
|
||||
this.parameters.events[name].push(event)
|
||||
})
|
||||
|
@ -1,4 +1,4 @@
|
||||
Feature: NIP-36 Event treatment
|
||||
Feature: NIP-16 Event treatment
|
||||
Scenario: Alice sends a replaceable event
|
||||
Given someone called Alice
|
||||
And Alice subscribes to author Alice
|
@ -23,7 +23,7 @@ Feature: NIP-28
|
||||
When Alice sends a channel_metadata event with content '{\"name\": \"Replaced\", \"about\": \"A different test channel.\", \"picture\": \"https://placekitten.com/400/400\"}'
|
||||
Then Alice receives a channel_metadata event from Alice with content '{\"name\": \"Replaced\", \"about\": \"A different test channel.\", \"picture\": \"https://placekitten.com/400/400\"}'
|
||||
|
||||
Scenario: Alice replaces metadata for a channel
|
||||
Scenario: Alice replaces metadata for a channel twice
|
||||
Given someone called Alice
|
||||
And Alice subscribes to author Alice
|
||||
And Alice sends a channel_creation event with content '{\"name\": \"Original\", \"about\": \"A test channel.\", \"picture\": \"https://placekitten.com/200/200\"}'
|
||||
|
@ -8,26 +8,31 @@ import {
|
||||
When,
|
||||
World,
|
||||
} from '@cucumber/cucumber'
|
||||
import { fromEvent, map, Observable, ReplaySubject, Subject, takeUntil } from 'rxjs'
|
||||
import WebSocket, { MessageEvent } from 'ws'
|
||||
import { assocPath } from 'ramda'
|
||||
import WebSocket from 'ws'
|
||||
|
||||
import { connect, createIdentity, createSubscription } from './helpers'
|
||||
import { connect, createIdentity, createSubscription, sendEvent } from './helpers'
|
||||
import { AppWorker } from '../../../src/app/worker'
|
||||
import { CacheClient } from '../../../src/@types/cache'
|
||||
//import { CacheClient } from '../../../src/@types/cache'
|
||||
import { DatabaseClient } from '../../../src/@types/base'
|
||||
import { getCacheClient } from '../../../src/cache/client'
|
||||
import { Event } from '../../../src/@types/event'
|
||||
//import { getCacheClient } from '../../../src/cache/client'
|
||||
import { getDbClient } from '../../../src/database/client'
|
||||
import { SettingsStatic } from '../../../src/utils/settings'
|
||||
import { workerFactory } from '../../../src/factories/worker-factory'
|
||||
|
||||
export const isDraft = Symbol('draft')
|
||||
|
||||
let worker: AppWorker
|
||||
|
||||
let dbClient: DatabaseClient
|
||||
let cacheClient: CacheClient
|
||||
//let cacheClient: CacheClient
|
||||
|
||||
BeforeAll({ timeout: 6000 }, async function () {
|
||||
export const streams = new WeakMap<WebSocket, Observable<unknown>>()
|
||||
|
||||
BeforeAll({ timeout: 1000 }, async function () {
|
||||
process.env.RELAY_PORT = '18808'
|
||||
cacheClient = getCacheClient()
|
||||
dbClient = getDbClient()
|
||||
await dbClient.raw('SELECT 1=1')
|
||||
|
||||
@ -40,15 +45,10 @@ BeforeAll({ timeout: 6000 }, async function () {
|
||||
})
|
||||
|
||||
AfterAll(async function() {
|
||||
worker.close(async () => {
|
||||
await Promise.all([
|
||||
cacheClient.disconnect(),
|
||||
dbClient.destroy(),
|
||||
])
|
||||
})
|
||||
worker.close(async () => dbClient.destroy())
|
||||
})
|
||||
|
||||
Before(async function () {
|
||||
Before(function () {
|
||||
this.parameters.identities = {}
|
||||
this.parameters.subscriptions = {}
|
||||
this.parameters.clients = {}
|
||||
@ -74,11 +74,24 @@ After(async function () {
|
||||
})
|
||||
|
||||
Given(/someone called (\w+)/, async function(name: string) {
|
||||
const connection = connect(name)
|
||||
const connection = await connect(name)
|
||||
this.parameters.identities[name] = this.parameters.identities[name] ?? createIdentity(name)
|
||||
this.parameters.clients[name] = await connection
|
||||
this.parameters.clients[name] = connection
|
||||
this.parameters.subscriptions[name] = []
|
||||
this.parameters.events[name] = []
|
||||
const subject = new Subject()
|
||||
connection.once('close', subject.next.bind(subject))
|
||||
|
||||
const project = (raw: MessageEvent) => JSON.parse(raw.data.toString('utf8'))
|
||||
|
||||
const replaySubject = new ReplaySubject(2, 1000)
|
||||
|
||||
fromEvent(connection, 'message').pipe(map(project) as any,takeUntil(subject)).subscribe(replaySubject)
|
||||
|
||||
streams.set(
|
||||
connection,
|
||||
replaySubject,
|
||||
)
|
||||
})
|
||||
|
||||
When(/(\w+) subscribes to author (\w+)$/, async function(this: World<Record<string, any>>, from: string, to: string) {
|
||||
@ -93,5 +106,20 @@ When(/(\w+) subscribes to author (\w+)$/, async function(this: World<Record<stri
|
||||
Then(/(\w+) unsubscribes from author \w+/, async function(from: string) {
|
||||
const ws = this.parameters.clients[from] as WebSocket
|
||||
const subscription = this.parameters.subscriptions[from].pop()
|
||||
ws.send(JSON.stringify(['CLOSE', subscription.name]))
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
ws.send(JSON.stringify(['CLOSE', subscription.name]), (err) => err ? reject(err) : resolve())
|
||||
})
|
||||
})
|
||||
|
||||
Then(/^(\w+) sends their last draft event (successfully|unsuccessfully)$/, async function(
|
||||
name: string,
|
||||
successfullyOrNot: string,
|
||||
) {
|
||||
const ws = this.parameters.clients[name] as WebSocket
|
||||
|
||||
const event = this.parameters.events[name].findLast((event: Event) => event[isDraft])
|
||||
|
||||
delete event[isDraft]
|
||||
|
||||
await sendEvent(ws, event, (successfullyOrNot) === 'successfully')
|
||||
})
|
||||
|
@ -151,9 +151,10 @@ describe('DelegatedEventMessageHandler', () => {
|
||||
})
|
||||
|
||||
it('does not reject if strategy rejects', async () => {
|
||||
const error = new Error('mistakes were made')
|
||||
isEventValidStub.returns(undefined)
|
||||
canAcceptEventStub.returns(undefined)
|
||||
strategyExecuteStub.rejects()
|
||||
strategyExecuteStub.rejects(error)
|
||||
|
||||
return expect(handler.handleMessage(message)).to.eventually.be.fulfilled
|
||||
})
|
||||
|
@ -22,8 +22,8 @@ describe('DeleteEventStrategy', () => {
|
||||
id: 'id',
|
||||
pubkey: 'pubkey',
|
||||
tags: [
|
||||
[EventTags.Event, 'event id 1'],
|
||||
[EventTags.Event, 'event id 2'],
|
||||
[EventTags.Event, '00000000'.repeat(8)],
|
||||
[EventTags.Event, 'ffffffff'.repeat(8)],
|
||||
],
|
||||
} as any
|
||||
let webSocket: IWebSocketAdapter
|
||||
@ -32,6 +32,7 @@ describe('DeleteEventStrategy', () => {
|
||||
let webSocketEmitStub: Sinon.SinonStub
|
||||
let eventRepositoryCreateStub: Sinon.SinonStub
|
||||
let eventRepositoryDeleteByPubkeyAndIdsStub: Sinon.SinonStub
|
||||
let eventRepositoryInsertStubsStub: Sinon.SinonStub
|
||||
|
||||
let strategy: IEventStrategy<Event, Promise<void>>
|
||||
|
||||
@ -42,6 +43,7 @@ describe('DeleteEventStrategy', () => {
|
||||
|
||||
eventRepositoryCreateStub = sandbox.stub(EventRepository.prototype, 'create')
|
||||
eventRepositoryDeleteByPubkeyAndIdsStub = sandbox.stub(EventRepository.prototype, 'deleteByPubkeyAndIds')
|
||||
eventRepositoryInsertStubsStub = sandbox.stub(EventRepository.prototype, 'insertStubs')
|
||||
|
||||
webSocketEmitStub = sandbox.stub()
|
||||
webSocket = {
|
||||
@ -64,12 +66,27 @@ describe('DeleteEventStrategy', () => {
|
||||
expect(eventRepositoryCreateStub).to.have.been.calledOnceWithExactly(event)
|
||||
})
|
||||
|
||||
it('inserts stubs', async () => {
|
||||
await strategy.execute(event)
|
||||
|
||||
expect(eventRepositoryInsertStubsStub).to.have.been.calledOnceWithExactly(
|
||||
event.pubkey,
|
||||
[
|
||||
'0000000000000000000000000000000000000000000000000000000000000000',
|
||||
'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff',
|
||||
]
|
||||
)
|
||||
})
|
||||
|
||||
it('deletes events if it has e tags', async () => {
|
||||
await strategy.execute(event)
|
||||
|
||||
expect(eventRepositoryDeleteByPubkeyAndIdsStub).to.have.been.calledOnceWithExactly(
|
||||
event.pubkey,
|
||||
['event id 1', 'event id 2'],
|
||||
[
|
||||
'0000000000000000000000000000000000000000000000000000000000000000',
|
||||
'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff',
|
||||
]
|
||||
)
|
||||
})
|
||||
|
||||
@ -117,6 +134,7 @@ describe('DeleteEventStrategy', () => {
|
||||
|
||||
expect(eventRepositoryCreateStub).to.have.been.calledOnceWithExactly(event)
|
||||
expect(eventRepositoryDeleteByPubkeyAndIdsStub).not.to.have.been.called
|
||||
expect(eventRepositoryInsertStubsStub).to.not.have.been.called
|
||||
expect(webSocketEmitStub).not.to.have.been.called
|
||||
})
|
||||
})
|
||||
|
@ -8,6 +8,7 @@ import { EphemeralEventStrategy } from '../../../../src/handlers/event-strategie
|
||||
import { Event } from '../../../../src/@types/event'
|
||||
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'
|
||||
|
||||
const { expect } = chai
|
||||
@ -39,7 +40,16 @@ describe('EphemeralEventStrategy', () => {
|
||||
it('broadcasts event', async () => {
|
||||
await strategy.execute(event)
|
||||
|
||||
expect(webSocketEmitStub).to.have.been.calledOnceWithExactly(
|
||||
expect(webSocketEmitStub.firstCall).to.have.been.calledWithExactly(
|
||||
WebSocketAdapterEvent.Message,
|
||||
[
|
||||
MessageType.OK,
|
||||
event.id,
|
||||
true,
|
||||
'',
|
||||
]
|
||||
)
|
||||
expect(webSocketEmitStub.secondCall).to.have.been.calledWithExactly(
|
||||
WebSocketAdapterEvent.Broadcast,
|
||||
event
|
||||
)
|
||||
|
@ -89,10 +89,13 @@ describe('ReplaceableEventStrategy', () => {
|
||||
const error = new Error()
|
||||
eventRepositoryUpsertStub.rejects(error)
|
||||
|
||||
await expect(strategy.execute(event)).to.eventually.be.rejectedWith(error)
|
||||
await strategy.execute(event)
|
||||
|
||||
expect(eventRepositoryUpsertStub).to.have.been.calledOnceWithExactly(event)
|
||||
expect(webSocketEmitStub).not.to.have.been.called
|
||||
expect(webSocketEmitStub).to.have.been.calledOnceWithExactly(
|
||||
WebSocketAdapterEvent.Message,
|
||||
['OK', 'id', false, 'error: ']
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -437,6 +437,24 @@ describe('EventRepository', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('insertStubs', () => {
|
||||
let clock: sinon.SinonFakeTimers
|
||||
|
||||
beforeEach(() => {
|
||||
clock = sinon.useFakeTimers(1673835425)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
clock.restore()
|
||||
})
|
||||
|
||||
it('insert stubs by pubkey & event ids', () => {
|
||||
const query = repository.insertStubs('001122', ['aabbcc', 'ddeeff']).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") values (\'\', 1673835, \'["001122",5]\', NULL, X\'aabbcc\', 5, X\'001122\', X\'\', \'[]\'), (\'\', 1673835, \'["001122",5]\', NULL, X\'ddeeff\', 5, X\'001122\', X\'\', \'[]\') on conflict do nothing')
|
||||
})
|
||||
})
|
||||
|
||||
describe('deleteByPubkeyAndIds', () => {
|
||||
it('marks event as deleted by pubkey & event_id if not deleted', () => {
|
||||
const query = repository.deleteByPubkeyAndIds('001122', ['aabbcc', 'ddeeff']).toString()
|
||||
|
@ -70,7 +70,7 @@ describe('SettingsStatic', () => {
|
||||
let sandbox: Sinon.SinonSandbox
|
||||
|
||||
beforeEach(() => {
|
||||
SettingsStatic._settings = undefined
|
||||
SettingsStatic._settings = undefined as any
|
||||
|
||||
sandbox = Sinon.createSandbox()
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user