Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add settings unit tests #14

Merged
merged 1 commit into from
Oct 21, 2022
Merged
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
5 changes: 3 additions & 2 deletions src/adapters/web-server-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ import { Duplex, EventEmitter } from 'stream'
import { IncomingMessage, Server, ServerResponse } from 'http'
import packageJson from '../../package.json'

import { ISettings } from '../@types/settings'
import { IWebServerAdapter } from '../@types/adapters'
import { Settings } from '../utils/settings'

export class WebServerAdapter extends EventEmitter implements IWebServerAdapter {

public constructor(
private readonly webServer: Server,
private readonly settings: () => ISettings,
) {
super()
this.webServer.on('request', this.onWebServerRequest.bind(this))
Expand All @@ -25,7 +26,7 @@ export class WebServerAdapter extends EventEmitter implements IWebServerAdapter
if (request.method === 'GET' && request.headers['accept'] === 'application/nostr+json') {
const {
info: { name, description, pubkey, contact },
} = Settings
} = this.settings()

const relayInformationDocument = {
name,
Expand Down
6 changes: 4 additions & 2 deletions src/adapters/web-socket-server-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { IWebSocketAdapter, IWebSocketServerAdapter } from '../@types/adapters'
import { WebSocketAdapterEvent, WebSocketServerAdapterEvent } from '../constants/adapter'
import { Event } from '../@types/event'
import { Factory } from '../@types/base'
import { ISettings } from '../@types/settings'
import { propEq } from 'ramda'
import { WebServerAdapter } from './web-server-adapter'

Expand All @@ -22,9 +23,10 @@ export class WebSocketServerAdapter extends WebServerAdapter implements IWebSock
private readonly createWebSocketAdapter: Factory<
IWebSocketAdapter,
[WebSocket, IncomingMessage, IWebSocketServerAdapter]
>
>,
settings: () => ISettings,
) {
super(webServer)
super(webServer, settings)

this.webSocketsAdapters = new WeakMap()

Expand Down
12 changes: 8 additions & 4 deletions src/factories/message-handler-factory.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { IncomingMessage, MessageType } from '../@types/messages'
import { createSettings } from './settings-factory'
import { DelegatedEventMessageHandler } from '../handlers/delegated-event-message-handler'
import { delegatedEventStrategyFactory } from './delegated-event-strategy-factory'
import { EventMessageHandler } from '../handlers/event-message-handler'
import { eventStrategyFactory } from './event-strategy-factory'
import { IEventRepository } from '../@types/repositories'
import { isDelegatedEvent } from '../utils/event'
import { IWebSocketAdapter } from '../@types/adapters'
import { Settings } from '../utils/settings'
import { SubscribeMessageHandler } from '../handlers/subscribe-message-handler'
import { UnsubscribeMessageHandler } from '../handlers/unsubscribe-message-handler'

Expand All @@ -17,13 +17,17 @@ export const messageHandlerFactory = (
case MessageType.EVENT:
{
if (isDelegatedEvent(message[1])) {
return new DelegatedEventMessageHandler(adapter, delegatedEventStrategyFactory(eventRepository), Settings)
return new DelegatedEventMessageHandler(
adapter,
delegatedEventStrategyFactory(eventRepository),
createSettings
)
}

return new EventMessageHandler(adapter, eventStrategyFactory(eventRepository), Settings)
return new EventMessageHandler(adapter, eventStrategyFactory(eventRepository), createSettings)
}
case MessageType.REQ:
return new SubscribeMessageHandler(adapter, eventRepository)
return new SubscribeMessageHandler(adapter, eventRepository, createSettings)
case MessageType.CLOSE:
return new UnsubscribeMessageHandler(adapter,)
default:
Expand Down
4 changes: 4 additions & 0 deletions src/factories/settings-factory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { ISettings } from '../@types/settings'
import { SettingsStatic } from '../utils/settings'

export const createSettings = (): ISettings => SettingsStatic.createSettings()
4 changes: 2 additions & 2 deletions src/handlers/event-message-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export class EventMessageHandler implements IMessageHandler {
public constructor(
protected readonly webSocket: IWebSocketAdapter,
protected readonly strategyFactory: Factory<IEventStrategy<Event, Promise<void>>, [Event, IWebSocketAdapter]>,
private readonly settings: ISettings
private readonly settings: () => ISettings
) { }

public async handleMessage(message: IncomingEventMessage): Promise<void> {
Expand Down Expand Up @@ -49,7 +49,7 @@ export class EventMessageHandler implements IMessageHandler {

protected canAcceptEvent(event: Event): string | undefined {
const now = Math.floor(Date.now()/1000)
const limits = this.settings.limits.event
const limits = this.settings().limits.event
if (limits.createdAt.maxPositiveDelta > 0) {
if (event.created_at > now + limits.createdAt.maxPositiveDelta) {
return `created_at is more than ${limits.createdAt.maxPositiveDelta} seconds in the future`
Expand Down
7 changes: 4 additions & 3 deletions src/handlers/subscribe-message-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import { streamEach, streamEnd, streamFilter, streamMap } from '../utils/stream'
import { SubscriptionFilter, SubscriptionId } from '../@types/subscription'
import { Event } from '../@types/event'
import { IEventRepository } from '../@types/repositories'
import { ISettings } from '../@types/settings'
import { IWebSocketAdapter } from '../@types/adapters'
import { Settings } from '../utils/settings'
import { SubscribeMessage } from '../@types/messages'
import { WebSocketAdapterEvent } from '../constants/adapter'

Expand All @@ -19,6 +19,7 @@ export class SubscribeMessageHandler implements IMessageHandler, IAbortable {
public constructor(
private readonly webSocket: IWebSocketAdapter,
private readonly eventRepository: IEventRepository,
private readonly settings: () => ISettings,
) {
this.abortController = new AbortController()
}
Expand Down Expand Up @@ -66,15 +67,15 @@ export class SubscribeMessageHandler implements IMessageHandler, IAbortable {
}

private canSubscribe(subscriptionId: string, filters: SubscriptionFilter[]): string | undefined {
const maxSubscriptions = Settings.limits.client.subscription.maxSubscriptions
const maxSubscriptions = this.settings().limits.client.subscription.maxSubscriptions
if (maxSubscriptions > 0) {
const subscriptions = this.webSocket.getSubscriptions()
if (!subscriptions.has(subscriptionId) && subscriptions.size + 1 > maxSubscriptions) {
return `Too many subscriptions: Number of subscriptions must be less than ${maxSubscriptions}`
}
}

const maxFilters = Settings.limits.client.subscription.maxFilters
const maxFilters = this.settings().limits.client.subscription.maxFilters
if (maxFilters > 0) {
if (filters.length > maxFilters) {
return `Too many filters: Number of filters per susbscription must be less or equal to ${maxFilters}`
Expand Down
4 changes: 3 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import http from 'http'
import process from 'process'
import { WebSocketServer } from 'ws'

import { createSettings } from './factories/settings-factory'
import { EventRepository } from './repositories/event-repository'
import { getDbClient } from './database/client'
import packageJson from '../package.json'
Expand Down Expand Up @@ -64,7 +65,8 @@ if (cluster.isPrimary) {
const adapter = new WebSocketServerAdapter(
server,
wss,
webSocketAdapterFactory(eventRepository)
webSocketAdapterFactory(eventRepository),
createSettings,
)

adapter.listen(port)
Expand Down
149 changes: 79 additions & 70 deletions src/utils/settings.ts
Original file line number Diff line number Diff line change
@@ -1,90 +1,99 @@
import { existsSync, readFileSync, writeFileSync } from 'fs'
import fs from 'fs'
import { homedir } from 'os'
import { join } from 'path'
import { mergeDeepRight } from 'ramda'

import { ISettings } from '../@types/settings'
import packageJson from '../../package.json'

export const getSettingsFilePath = (filename = 'settings.json'): string => join(
process.env.NOSTR_CONFIG_DIR ?? join(homedir(), '.nostr'),
filename,
)
export class SettingsStatic {
static _settings: ISettings

let _settings: ISettings
public static getSettingsFilePath(filename = 'settings.json') {
return join(
process.env.NOSTR_CONFIG_DIR ?? join(homedir(), '.nostr'),
filename
)
}

export const getDefaultSettings = (): ISettings => ({
info: {
relay_url: `wss://${packageJson.name}.your-domain.com`,
name: `${packageJson.name}.your-domain.com`,
description: packageJson.description,
pubkey: '',
contact: '[email protected]',
},
limits: {
event: {
eventId: {
minLeadingZeroBits: 0,
},
kind: {
whitelist: [],
blacklist: [],
},
pubkey: {
minLeadingZeroBits: 0,
whitelist: [],
blacklist: [],
},
createdAt: {
maxPositiveDelta: 900, // +15 min
maxNegativeDelta: 0, // disabled
public static getDefaultSettings(): ISettings {
return {
info: {
relay_url: 'wss://nostr-ts-relay.your-domain.com',
name: `${packageJson.name}.your-domain.com`,
description: packageJson.description,
pubkey: 'replace-with-your-pubkey',
contact: '[email protected]',
},
},
client: {
subscription: {
maxSubscriptions: 10,
maxFilters: 10,
limits: {
event: {
eventId: {
minLeadingZeroBits: 0,
},
kind: {
whitelist: [],
blacklist: [],
},
pubkey: {
minLeadingZeroBits: 0,
whitelist: [],
blacklist: [],
},
createdAt: {
maxPositiveDelta: 900,
maxNegativeDelta: 0, // disabled
},
},
client: {
subscription: {
maxSubscriptions: 10,
maxFilters: 10,
},
},
},
},
},
})

const loadSettings = (path: string) => {
return JSON.parse(
readFileSync(
path,
{ encoding: 'utf-8' },
),
)
}

const createSettings = (): ISettings => {
const path = getSettingsFilePath()
const defaults = getDefaultSettings()
try {
if (_settings) {
return _settings
}
}

public static loadSettings(path: string) {
return JSON.parse(
fs.readFileSync(
path,
{ encoding: 'utf-8' }
)
)
}

if (!existsSync(path)) {
saveSettings(path, defaults)
public static createSettings(): ISettings {
if (SettingsStatic._settings) {
return SettingsStatic._settings
}
const path = SettingsStatic.getSettingsFilePath()
const defaults = SettingsStatic.getDefaultSettings()
try {

_settings = mergeDeepRight(defaults, loadSettings(path))
if (fs.existsSync(path)) {
SettingsStatic._settings = mergeDeepRight(
defaults,
SettingsStatic.loadSettings(path)
)
} else {
SettingsStatic.saveSettings(path, defaults)
SettingsStatic._settings = mergeDeepRight({}, defaults)
}

return _settings
} catch (error) {
console.error('Unable to read config file. Reason: %s', error.message)
return SettingsStatic._settings
} catch (error) {
console.error('Unable to read config file. Reason: %s', error.message)

return defaults
return defaults
}
}
}

export const saveSettings = (path: string, settings: ISettings) => {
return writeFileSync(
path,
JSON.stringify(settings, null, 2),
{ encoding: 'utf-8' }
)
public static saveSettings(path: string, settings: ISettings) {
return fs.writeFileSync(
path,
JSON.stringify(settings, null, 2),
{ encoding: 'utf-8' }
)
}
}
export const Settings = createSettings()
24 changes: 24 additions & 0 deletions test/unit/factories/settings-factory.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { expect } from 'chai'
import Sinon from 'sinon'

import { createSettings } from '../../../src/factories/settings-factory'
import { SettingsStatic } from '../../../src/utils/settings'

describe('getSettings', () => {
let createSettingsStub: Sinon.SinonStub

beforeEach(() => {
createSettingsStub = Sinon.stub(SettingsStatic, 'createSettings')
})

afterEach(() => {
createSettingsStub.restore()
})

it('calls createSettings and returns', () => {
const settings = Symbol()
createSettingsStub.returns(settings)

expect(createSettings()).to.equal(settings)
})
})
4 changes: 2 additions & 2 deletions test/unit/handlers/event-message-handler.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ describe('EventMessageHandler', () => {
handler = new EventMessageHandler(
webSocket as any,
strategyFactoryStub,
{} as any,
() => ({}) as any,
)
})

Expand Down Expand Up @@ -168,7 +168,7 @@ describe('EventMessageHandler', () => {
handler = new EventMessageHandler(
{} as any,
() => null,
settings,
() => settings,
)
})

Expand Down
Loading