mirror of
https://github.com/Cameri/nostream.git
synced 2025-05-07 08:30:14 +02:00
feat: add integration tests w/ docker
This commit is contained in:
parent
82225c47b1
commit
851693a966
39
.github/workflows/checks.yml
vendored
39
.github/workflows/checks.yml
vendored
@ -27,7 +27,6 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
container:
|
container:
|
||||||
image: node:18-alpine3.16
|
image: node:18-alpine3.16
|
||||||
needs: [lint]
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-node@v3
|
- uses: actions/setup-node@v3
|
||||||
@ -38,12 +37,11 @@ jobs:
|
|||||||
run: npm ci
|
run: npm ci
|
||||||
- name: Run ESLint
|
- name: Run ESLint
|
||||||
run: npm run build
|
run: npm run build
|
||||||
test:
|
test-and-cover:
|
||||||
name: Tests
|
name: Unit Tests
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
container:
|
container:
|
||||||
image: node:18-alpine3.16
|
image: node:18-alpine3.16
|
||||||
needs: [build]
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-node@v3
|
- uses: actions/setup-node@v3
|
||||||
@ -52,23 +50,9 @@ jobs:
|
|||||||
cache: npm
|
cache: npm
|
||||||
- name: Install package dependencies
|
- name: Install package dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
- name: Run tests
|
- name: Run unit tests
|
||||||
run: npm run test:unit
|
run: npm run test:unit
|
||||||
coverage:
|
- name: Run coverage for unit tests
|
||||||
name: Coverage
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
container:
|
|
||||||
image: node:18-alpine3.16
|
|
||||||
needs: [build]
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
- uses: actions/setup-node@v3
|
|
||||||
with:
|
|
||||||
node-version-file: .nvmrc
|
|
||||||
cache: npm
|
|
||||||
- name: Install package dependencies
|
|
||||||
run: npm ci
|
|
||||||
- name: Run coverage
|
|
||||||
run: npm run cover
|
run: npm run cover
|
||||||
- name: Coveralls
|
- name: Coveralls
|
||||||
uses: coverallsapp/github-action@master
|
uses: coverallsapp/github-action@master
|
||||||
@ -76,3 +60,18 @@ jobs:
|
|||||||
path-to-lcov: ./.coverage/lcov.info
|
path-to-lcov: ./.coverage/lcov.info
|
||||||
flag-name: Unit
|
flag-name: Unit
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
test-integration:
|
||||||
|
name: Integration Tests
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container:
|
||||||
|
image: node:18-alpine3.16
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version-file: .nvmrc
|
||||||
|
cache: npm
|
||||||
|
- name: Install package dependencies
|
||||||
|
run: npm ci
|
||||||
|
- name: Run integration tests
|
||||||
|
run: npm run docker:test:integration
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
const config = [
|
const config = [
|
||||||
'test/integration/features/**/*.feature',
|
'test/integration/features/**/*.feature',
|
||||||
'--require-module ts-node/register',
|
'--require-module ts-node/register',
|
||||||
'--require tests/integration/features/**/*.ts',
|
'--require test/integration/features/**/*.ts',
|
||||||
'--format progress-bar',
|
'--require test/integration/features/*.ts',
|
||||||
'--format json:report.json',
|
'--format @cucumber/pretty-formatter',
|
||||||
'--publish-quiet',
|
'--publish',
|
||||||
].join(' ')
|
].join(' ')
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
@ -8,6 +8,8 @@ services:
|
|||||||
DB_USER: nostr_ts_relay
|
DB_USER: nostr_ts_relay
|
||||||
DB_PASSWORD: nostr_ts_relay
|
DB_PASSWORD: nostr_ts_relay
|
||||||
DB_NAME: nostr_ts_relay
|
DB_NAME: nostr_ts_relay
|
||||||
|
DB_MIN_POOL_SIZE: 1
|
||||||
|
DB_MAX_POOL_SIZE: 2
|
||||||
NOSTR_CONFIG_DIR: /home/node/
|
NOSTR_CONFIG_DIR: /home/node/
|
||||||
user: node:node
|
user: node:node
|
||||||
volumes:
|
volumes:
|
||||||
|
@ -7,7 +7,6 @@ module.exports = {
|
|||||||
password: process.env.DB_PASSWORD ?? 'postgres',
|
password: process.env.DB_PASSWORD ?? 'postgres',
|
||||||
database: process.env.DB_NAME ?? 'nostr-ts-relay',
|
database: process.env.DB_NAME ?? 'nostr-ts-relay',
|
||||||
},
|
},
|
||||||
pool: { min: 4, max: 16 },
|
|
||||||
seeds: {
|
seeds: {
|
||||||
directory: './seeds',
|
directory: './seeds',
|
||||||
},
|
},
|
||||||
|
64
package-lock.json
generated
64
package-lock.json
generated
@ -19,6 +19,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@cucumber/cucumber": "8.7.0",
|
"@cucumber/cucumber": "8.7.0",
|
||||||
|
"@cucumber/pretty-formatter": "1.0.0",
|
||||||
"@types/chai": "^4.3.1",
|
"@types/chai": "^4.3.1",
|
||||||
"@types/chai-as-promised": "^7.1.5",
|
"@types/chai-as-promised": "^7.1.5",
|
||||||
"@types/mocha": "^9.1.1",
|
"@types/mocha": "^9.1.1",
|
||||||
@ -674,6 +675,34 @@
|
|||||||
"uuid": "dist/bin/uuid"
|
"uuid": "dist/bin/uuid"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@cucumber/pretty-formatter": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@cucumber/pretty-formatter/-/pretty-formatter-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-wcnIMN94HyaHGsfq72dgCvr1d8q6VGH4Y6Gl5weJ2TNZw1qn2UY85Iki4c9VdaLUONYnyYH3+178YB+9RFe/Hw==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-styles": "^5.0.0",
|
||||||
|
"cli-table3": "^0.6.0",
|
||||||
|
"figures": "^3.2.0",
|
||||||
|
"ts-dedent": "^2.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@cucumber/cucumber": ">=7.0.0",
|
||||||
|
"@cucumber/messages": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@cucumber/pretty-formatter/node_modules/ansi-styles": {
|
||||||
|
"version": "5.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
|
||||||
|
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
|
||||||
|
"dev": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@cucumber/tag-expressions": {
|
"node_modules/@cucumber/tag-expressions": {
|
||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@cucumber/tag-expressions/-/tag-expressions-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@cucumber/tag-expressions/-/tag-expressions-4.1.0.tgz",
|
||||||
@ -5104,6 +5133,15 @@
|
|||||||
"tree-kill": "cli.js"
|
"tree-kill": "cli.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ts-dedent": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==",
|
||||||
|
"dev": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ts-node": {
|
"node_modules/ts-node": {
|
||||||
"version": "10.9.1",
|
"version": "10.9.1",
|
||||||
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz",
|
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz",
|
||||||
@ -6136,6 +6174,26 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@cucumber/pretty-formatter": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@cucumber/pretty-formatter/-/pretty-formatter-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-wcnIMN94HyaHGsfq72dgCvr1d8q6VGH4Y6Gl5weJ2TNZw1qn2UY85Iki4c9VdaLUONYnyYH3+178YB+9RFe/Hw==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"ansi-styles": "^5.0.0",
|
||||||
|
"cli-table3": "^0.6.0",
|
||||||
|
"figures": "^3.2.0",
|
||||||
|
"ts-dedent": "^2.0.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-styles": {
|
||||||
|
"version": "5.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
|
||||||
|
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
|
||||||
|
"dev": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"@cucumber/tag-expressions": {
|
"@cucumber/tag-expressions": {
|
||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@cucumber/tag-expressions/-/tag-expressions-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@cucumber/tag-expressions/-/tag-expressions-4.1.0.tgz",
|
||||||
@ -9499,6 +9557,12 @@
|
|||||||
"integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==",
|
"integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"ts-dedent": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"ts-node": {
|
"ts-node": {
|
||||||
"version": "10.9.1",
|
"version": "10.9.1",
|
||||||
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz",
|
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz",
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
],
|
],
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "ts-node src/index.ts",
|
"dev": "node -r ts-node/register src/index.ts",
|
||||||
"clean": "rimraf ./dist",
|
"clean": "rimraf ./dist",
|
||||||
"build": "tsc --project tsconfig.build.json",
|
"build": "tsc --project tsconfig.build.json",
|
||||||
"prestart": "npm run build",
|
"prestart": "npm run build",
|
||||||
@ -35,7 +35,8 @@
|
|||||||
"predocker:compose:up": "[ -d \"$HOME/.nostr\" ] || mkdir -p $HOME/.nostr",
|
"predocker:compose:up": "[ -d \"$HOME/.nostr\" ] || mkdir -p $HOME/.nostr",
|
||||||
"docker:compose:up": "docker compose up --build",
|
"docker:compose:up": "docker compose up --build",
|
||||||
"docker:compose:down": "docker compose down",
|
"docker:compose:down": "docker compose down",
|
||||||
"docker:compose:rm": "docker compose rm"
|
"docker:compose:rm": "docker compose rm",
|
||||||
|
"docker:test:integration": "docker compose -f ./test/integration/docker-compose.yml up tests --build --exit-code-from tests"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@ -53,6 +54,7 @@
|
|||||||
"homepage": "https://github.com/Cameri/nostr-ts-relay#readme",
|
"homepage": "https://github.com/Cameri/nostr-ts-relay#readme",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@cucumber/cucumber": "8.7.0",
|
"@cucumber/cucumber": "8.7.0",
|
||||||
|
"@cucumber/pretty-formatter": "1.0.0",
|
||||||
"@types/chai": "^4.3.1",
|
"@types/chai": "^4.3.1",
|
||||||
"@types/chai-as-promised": "^7.1.5",
|
"@types/chai-as-promised": "^7.1.5",
|
||||||
"@types/mocha": "^9.1.1",
|
"@types/mocha": "^9.1.1",
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import cluster from 'cluster'
|
||||||
import { EventEmitter } from 'stream'
|
import { EventEmitter } from 'stream'
|
||||||
import { IncomingMessage as IncomingHttpMessage } from 'http'
|
import { IncomingMessage as IncomingHttpMessage } from 'http'
|
||||||
import { WebSocket } from 'ws'
|
import { WebSocket } from 'ws'
|
||||||
@ -60,10 +61,12 @@ export class WebSocketAdapter extends EventEmitter implements IWebSocketAdapter
|
|||||||
|
|
||||||
public onBroadcast(event: Event): void {
|
public onBroadcast(event: Event): void {
|
||||||
this.webSocketServer.emit(WebSocketServerAdapterEvent.Broadcast, event)
|
this.webSocketServer.emit(WebSocketServerAdapterEvent.Broadcast, event)
|
||||||
process.send({
|
if (cluster.isWorker) {
|
||||||
eventName: WebSocketServerAdapterEvent.Broadcast,
|
process.send({
|
||||||
event,
|
eventName: WebSocketServerAdapterEvent.Broadcast,
|
||||||
})
|
event,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public onSendEvent(event: Event): void {
|
public onSendEvent(event: Event): void {
|
||||||
|
@ -44,6 +44,7 @@ export class WebSocketServerAdapter extends WebServerAdapter implements IWebSock
|
|||||||
}
|
}
|
||||||
|
|
||||||
public close(callback: () => void): void {
|
public close(callback: () => void): void {
|
||||||
|
this.onClose()
|
||||||
this.webSocketServer.close(() => {
|
this.webSocketServer.close(() => {
|
||||||
this.webServer.close(callback)
|
this.webServer.close(callback)
|
||||||
})
|
})
|
||||||
|
@ -6,7 +6,7 @@ export class AppWorker implements IRunnable {
|
|||||||
private readonly process: NodeJS.Process,
|
private readonly process: NodeJS.Process,
|
||||||
private readonly adapter: IWebSocketServerAdapter
|
private readonly adapter: IWebSocketServerAdapter
|
||||||
) {
|
) {
|
||||||
process
|
this.process
|
||||||
.on('message', this.onMessage.bind(this))
|
.on('message', this.onMessage.bind(this))
|
||||||
.on('SIGINT', this.onExit.bind(this))
|
.on('SIGINT', this.onExit.bind(this))
|
||||||
.on('SIGHUP', this.onExit.bind(this))
|
.on('SIGHUP', this.onExit.bind(this))
|
||||||
@ -34,11 +34,12 @@ export class AppWorker implements IRunnable {
|
|||||||
|
|
||||||
private onExit() {
|
private onExit() {
|
||||||
console.log(`worker ${process.pid} - exiting`)
|
console.log(`worker ${process.pid} - exiting`)
|
||||||
this.adapter.close(() => {
|
this.close(() => {
|
||||||
// dbClient.destroy(() => {
|
this.process.exit(0)
|
||||||
// process.exit(0)
|
|
||||||
// })
|
|
||||||
process.exit(0)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public close(callback?: () => void) {
|
||||||
|
this.adapter.close(callback)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,7 @@ import { getDbClient } from '../database/client'
|
|||||||
import { webSocketAdapterFactory } from './websocket-adapter-factory'
|
import { webSocketAdapterFactory } from './websocket-adapter-factory'
|
||||||
import { WebSocketServerAdapter } from '../adapters/web-socket-server-adapter'
|
import { WebSocketServerAdapter } from '../adapters/web-socket-server-adapter'
|
||||||
|
|
||||||
export const workerFactory = () => {
|
export const workerFactory = (): AppWorker => {
|
||||||
const dbClient = getDbClient()
|
const dbClient = getDbClient()
|
||||||
const eventRepository = new EventRepository(dbClient)
|
const eventRepository = new EventRepository(dbClient)
|
||||||
|
|
||||||
|
@ -90,7 +90,7 @@ export class EventRepository implements IEventRepository {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
}),
|
} as any),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
])(currentFilter[filterName] as string[])
|
])(currentFilter[filterName] as string[])
|
||||||
|
64
test/integration/docker-compose.yml
Normal file
64
test/integration/docker-compose.yml
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
services:
|
||||||
|
tests:
|
||||||
|
build:
|
||||||
|
context: ../../
|
||||||
|
dockerfile: Dockerfile.test
|
||||||
|
env_file:
|
||||||
|
- ../../test.env
|
||||||
|
environment:
|
||||||
|
NOSTR_CONFIG_DIR: /code
|
||||||
|
volumes:
|
||||||
|
- ../../src:/code/src
|
||||||
|
- ../../test:/code/test
|
||||||
|
working_dir: /code
|
||||||
|
ports:
|
||||||
|
- "8008:8008"
|
||||||
|
command:
|
||||||
|
["sh", "-c", "whoami && pwd && ls -hall test/integration && npm run test:integration"]
|
||||||
|
depends_on:
|
||||||
|
db-test:
|
||||||
|
condition: service_healthy
|
||||||
|
migrations-test:
|
||||||
|
condition: service_completed_successfully
|
||||||
|
networks:
|
||||||
|
- nostr-ts-relay-test
|
||||||
|
links:
|
||||||
|
- db-test
|
||||||
|
db-test:
|
||||||
|
image: postgres
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: postgres
|
||||||
|
POSTGRES_PASSWORD: postgres
|
||||||
|
POSTGRES_DB: nostr_ts_relay_test
|
||||||
|
networks:
|
||||||
|
- nostr-ts-relay-test
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
||||||
|
timeout: 5s
|
||||||
|
start_period: 5s
|
||||||
|
retries: 0
|
||||||
|
migrations-test:
|
||||||
|
image: node:18-alpine3.16
|
||||||
|
environment:
|
||||||
|
DB_HOST: db-test
|
||||||
|
DB_PORT: 5432
|
||||||
|
DB_USER: postgres
|
||||||
|
DB_PASSWORD: postgres
|
||||||
|
DB_NAME: nostr_ts_relay_test
|
||||||
|
entrypoint:
|
||||||
|
- sh
|
||||||
|
- -c
|
||||||
|
- 'cd code && npm install -g knex@2.3.0 && npm install knex --quiet && npm run db:migrate'
|
||||||
|
volumes:
|
||||||
|
- ../../package.json:/code/package.json
|
||||||
|
- ../../migrations:/code/migrations
|
||||||
|
- ../../knexfile.js:/code/knexfile.js
|
||||||
|
depends_on:
|
||||||
|
- db-test
|
||||||
|
networks:
|
||||||
|
- nostr-ts-relay-test
|
||||||
|
links:
|
||||||
|
- db-test
|
||||||
|
|
||||||
|
networks:
|
||||||
|
nostr-ts-relay-test:
|
6
test/integration/features/nip-01/nip-01.feature
Normal file
6
test/integration/features/nip-01/nip-01.feature
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
Feature: NIP-01
|
||||||
|
Scenario: Alice posts set_metadata event
|
||||||
|
Given I am Alice
|
||||||
|
And I subscribe to author Alice
|
||||||
|
When I send a set_metadata event as Alice
|
||||||
|
Then I receive a set_metadata event from Alice
|
201
test/integration/features/nip-01/nip-01.feature-step.ts
Normal file
201
test/integration/features/nip-01/nip-01.feature-step.ts
Normal file
@ -0,0 +1,201 @@
|
|||||||
|
import * as secp256k1 from '@noble/secp256k1'
|
||||||
|
import {
|
||||||
|
After,
|
||||||
|
Before,
|
||||||
|
Given,
|
||||||
|
Then,
|
||||||
|
When,
|
||||||
|
World,
|
||||||
|
} from '@cucumber/cucumber'
|
||||||
|
import { RawData, WebSocket } from 'ws'
|
||||||
|
import chai from 'chai'
|
||||||
|
import { createHmac } from 'crypto'
|
||||||
|
import sinonChai from 'sinon-chai'
|
||||||
|
|
||||||
|
import { Event } from '../../../../src/@types/event'
|
||||||
|
import { MessageType } from '../../../../src/@types/messages'
|
||||||
|
import { serializeEvent } from '../../../../src/utils/event'
|
||||||
|
import { SubscriptionFilter } from '../../../../src/@types/subscription'
|
||||||
|
|
||||||
|
chai.use(sinonChai)
|
||||||
|
const { expect } = chai
|
||||||
|
|
||||||
|
Before(async function () {
|
||||||
|
const ws = new WebSocket('ws://localhost:8008')
|
||||||
|
this.parameters.ws = ws
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
ws
|
||||||
|
.once('open', resolve)
|
||||||
|
.once('error', reject)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
After(function () {
|
||||||
|
const ws = this.parameters.ws as WebSocket
|
||||||
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||||
|
ws.close()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
Given(/I am (\w+)/, function(name: string) {
|
||||||
|
this.parameters.authors = this.parameters.authors ?? {}
|
||||||
|
this.parameters.authors[name] = this.parameters.authors[name] ?? createIdentity(name)
|
||||||
|
})
|
||||||
|
|
||||||
|
When(/I subscribe to author (\w+)/, async function(this: World<Record<string, any>>, name: string) {
|
||||||
|
const ws = this.parameters.ws as WebSocket
|
||||||
|
const pubkey = this.parameters.authors[name].pubkey
|
||||||
|
this.parameters.subscriptions = this.parameters.subscriptions ?? []
|
||||||
|
const subscription = { name: `test-${Math.random()}`, filters: [{ authors: [pubkey] }] }
|
||||||
|
this.parameters.subscriptions.push(subscription)
|
||||||
|
|
||||||
|
await createSubscription(ws, subscription.name, subscription.filters)
|
||||||
|
|
||||||
|
await waitForEOSE(ws, subscription.name)
|
||||||
|
})
|
||||||
|
|
||||||
|
When(/I send a set_metadata event as (\w+)/, async function(name: string) {
|
||||||
|
const ws = this.parameters.ws as WebSocket
|
||||||
|
const { pubkey, privkey } = this.parameters.authors[name]
|
||||||
|
|
||||||
|
const content = JSON.stringify({ name })
|
||||||
|
const event: Event = await createEvent({ pubkey, kind: 0, content }, privkey)
|
||||||
|
|
||||||
|
await sendEvent(ws, event)
|
||||||
|
|
||||||
|
this.parameters.events = this.parameters.events ?? []
|
||||||
|
this.parameters.events.push(event)
|
||||||
|
})
|
||||||
|
|
||||||
|
Then(/I receive a set_metadata event from (\w+)/, async function(author: string) {
|
||||||
|
const expectedEvent = this.parameters.events.pop()
|
||||||
|
const subscription = this.parameters.subscriptions[this.parameters.subscriptions.length - 1]
|
||||||
|
const receivedEvent = await waitForNextEvent(this.parameters.ws, subscription.name)
|
||||||
|
expect(receivedEvent.pubkey).to.equal(this.parameters.authors[author].pubkey)
|
||||||
|
expect(receivedEvent).to.deep.equal(expectedEvent)
|
||||||
|
})
|
||||||
|
|
||||||
|
async function createEvent(input: Partial<Event>, privkey: any): Promise<Event> {
|
||||||
|
const event: Event = {
|
||||||
|
pubkey: input.pubkey,
|
||||||
|
kind: input.kind,
|
||||||
|
created_at: input.created_at ?? Math.floor(Date.now() / 1000),
|
||||||
|
content: input.content ?? '',
|
||||||
|
tags: input.tags ?? [],
|
||||||
|
} as any
|
||||||
|
|
||||||
|
const id = Buffer.from(
|
||||||
|
await secp256k1.utils.sha256(
|
||||||
|
Buffer.from(JSON.stringify(serializeEvent(event)))
|
||||||
|
)
|
||||||
|
).toString('hex')
|
||||||
|
|
||||||
|
const sig = Buffer.from(
|
||||||
|
await secp256k1.schnorr.sign(id, privkey)
|
||||||
|
).toString('hex')
|
||||||
|
|
||||||
|
return { id, ...event, sig }
|
||||||
|
}
|
||||||
|
|
||||||
|
function createIdentity(name: string) {
|
||||||
|
const hmac = createHmac('sha256', process.env.SECRET ?? Math.random().toString())
|
||||||
|
hmac.update(name)
|
||||||
|
const privkey = hmac.digest().toString('hex')
|
||||||
|
const pubkey = Buffer.from(secp256k1.getPublicKey(privkey, true)).toString('hex').substring(2)
|
||||||
|
const author = {
|
||||||
|
name,
|
||||||
|
privkey,
|
||||||
|
pubkey,
|
||||||
|
}
|
||||||
|
return author
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createSubscription(
|
||||||
|
ws: WebSocket,
|
||||||
|
subscriptionName: string,
|
||||||
|
subscriptionFilters: SubscriptionFilter[],
|
||||||
|
): Promise<void> {
|
||||||
|
return new Promise<void>((resolve, reject) => {
|
||||||
|
const message = JSON.stringify([
|
||||||
|
'REQ',
|
||||||
|
subscriptionName,
|
||||||
|
...subscriptionFilters,
|
||||||
|
])
|
||||||
|
|
||||||
|
ws.send(message, (error: Error) => {
|
||||||
|
if (error) {
|
||||||
|
reject(error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resolve()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onError(error: Error) {
|
||||||
|
reject(error)
|
||||||
|
cleanup()
|
||||||
|
}
|
||||||
|
ws.once('error', onError)
|
||||||
|
|
||||||
|
function onMessage(raw: RawData) {
|
||||||
|
const message = JSON.parse(raw.toString('utf-8'))
|
||||||
|
if (message[0] === MessageType.EOSE && message[1] === subscription) {
|
||||||
|
resolve()
|
||||||
|
cleanup()
|
||||||
|
} else if (message[0] === MessageType.NOTICE) {
|
||||||
|
reject(new Error(message[1]))
|
||||||
|
cleanup()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.on('message', onMessage)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitForNextEvent(ws: WebSocket, subscription: string): Promise<Event> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
function cleanup() {
|
||||||
|
ws.removeListener('message', onMessage)
|
||||||
|
ws.removeListener('error', onError)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onError(error: Error) {
|
||||||
|
reject(error)
|
||||||
|
cleanup()
|
||||||
|
}
|
||||||
|
ws.once('error', onError)
|
||||||
|
|
||||||
|
function onMessage(raw: RawData) {
|
||||||
|
ws.removeListener('error', onError)
|
||||||
|
const message = JSON.parse(raw.toString('utf-8'))
|
||||||
|
if (message[0] === MessageType.EVENT && message[1] === subscription) {
|
||||||
|
resolve(message[2])
|
||||||
|
cleanup()
|
||||||
|
} else if (message[0] === MessageType.NOTICE) {
|
||||||
|
reject(new Error(message[1]))
|
||||||
|
cleanup()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ws.on('message', onMessage)
|
||||||
|
})
|
||||||
|
}
|
23
test/integration/features/shared.ts
Normal file
23
test/integration/features/shared.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { AfterAll, BeforeAll } from '@cucumber/cucumber'
|
||||||
|
|
||||||
|
import { AppWorker } from '../../../src/app/worker'
|
||||||
|
import { DatabaseClient } from '../../../src/@types/base'
|
||||||
|
import { getDbClient } from '../../../src/database/client'
|
||||||
|
import { workerFactory } from '../../../src/factories/worker-factory'
|
||||||
|
|
||||||
|
let worker: AppWorker
|
||||||
|
|
||||||
|
let dbClient: DatabaseClient
|
||||||
|
|
||||||
|
BeforeAll({ timeout: 6000 }, async function () {
|
||||||
|
dbClient = getDbClient()
|
||||||
|
await dbClient.raw('SELECT 1=1')
|
||||||
|
worker = workerFactory()
|
||||||
|
worker.run()
|
||||||
|
})
|
||||||
|
|
||||||
|
AfterAll(async function() {
|
||||||
|
worker.close(async () => {
|
||||||
|
await dbClient.destroy()
|
||||||
|
})
|
||||||
|
})
|
@ -9,7 +9,7 @@
|
|||||||
"target": "es6",
|
"target": "es6",
|
||||||
"outDir": "./dist",
|
"outDir": "./dist",
|
||||||
"moduleResolution": "Node",
|
"moduleResolution": "Node",
|
||||||
"types": ["node", "mocha"],
|
"types": ["node", "mocha", "@cucumber/cucumber"],
|
||||||
"typeRoots": ["./node_modules/@types"],
|
"typeRoots": ["./node_modules/@types"],
|
||||||
"incremental": true,
|
"incremental": true,
|
||||||
"declarationMap": true,
|
"declarationMap": true,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user