From 376d0eb599d31c79e7a620f81d16bcb20db25491 Mon Sep 17 00:00:00 2001 From: Ricardo Arturo Cabral Mejia Date: Sat, 27 Aug 2022 03:36:37 +0000 Subject: [PATCH] test: improve coverage for events and runes --- src/utils/event.ts | 46 +++---- src/utils/runes.ts | 31 +---- test/unit/utils/event.spec.ts | 54 ++++++++ test/unit/utils/runes.spec.ts | 219 +++++++++++++++++++++++++++++++++ test/unit/utils/stream.spec.ts | 19 ++- 5 files changed, 316 insertions(+), 53 deletions(-) create mode 100644 test/unit/utils/runes.spec.ts diff --git a/src/utils/event.ts b/src/utils/event.ts index 8ec60f3..3944fc1 100644 --- a/src/utils/event.ts +++ b/src/utils/event.ts @@ -109,24 +109,6 @@ export const isDelegatedEventValid = async (event: Event): Promise => { return false } - const serializedDelegationTag = `nostr:${delegation[0]}:${event.pubkey}:${delegation[2]}` - - const token = await secp256k1.utils.sha256(Buffer.from(serializedDelegationTag)) - - // Token generation to be decided: - // const serializedDelegationTag = [ - // delegation[0], // 'delegation' - // delegation[1], // - // event.pubkey, // - // delegation[2], // - // ] - // const token = await secp256k1.utils.sha256(Buffer.from(JSON.stringify(serializedDelegationTag))) - - // Validate delegation signature - const verification = await secp256k1.schnorr.verify(delegation[3], token, delegation[1]) - if (!verification) { - return false - } // Validate rune const runifiedEvent = (converge( @@ -144,14 +126,32 @@ export const isDelegatedEventValid = async (event: Event): Promise => { ], ) as any)(event) + let result: boolean try { - const [result] = Rune.from(delegation[2]).test(runifiedEvent) - - return result + [result] = Rune.from(delegation[2]).test(runifiedEvent) } catch (error) { - console.error('Invalid rune') - return false + result = false } + + if (!result) { + return false + } + + const serializedDelegationTag = `nostr:${delegation[0]}:${event.pubkey}:${delegation[2]}` + + const token = await secp256k1.utils.sha256(Buffer.from(serializedDelegationTag)) + + // Token generation to be decided: + // const serializedDelegationTag = [ + // delegation[0], // 'delegation' + // delegation[1], // + // event.pubkey, // + // delegation[2], // + // ] + // const token = await secp256k1.utils.sha256(Buffer.from(JSON.stringify(serializedDelegationTag))) + + // Validate delegation signature + return secp256k1.schnorr.verify(delegation[3], token, delegation[1]) } export const isEventIdValid = async (event: Event): Promise => { diff --git a/src/utils/runes.ts b/src/utils/runes.ts index b4ef37a..e83a16a 100644 --- a/src/utils/runes.ts +++ b/src/utils/runes.ts @@ -16,7 +16,7 @@ export class Alternative { } if (!new Set(['!', '=', '/', '^', '$', '~', '<', '>', '}', '{', '#']).has(this.cond)) { - throw new Error('Cond not valid') + throw new Error('Cond is not valid') } } @@ -69,10 +69,8 @@ export class Alternative { } case '{': return why(val < this.value, this.field, `is the same or ordered after ${this.value}`) - case '{': + case '}': return why(val > this.value, this.field, `is the same or ordered before ${this.value}`) - default: - throw new Error('Invalid condition') } } @@ -80,14 +78,6 @@ export class Alternative { return `${this.field}${this.cond}${this.value.replace(/[\\|&]/g, '\\$&')}` } - public valueOf(): string { - return this.encode() - } - - public toString() { - return this.encode() - } - public static decode(encodedStr: string): [Alternative, string] { let cond = undefined let endOff = 0 @@ -133,7 +123,6 @@ export class Alternative { return new Alternative(field, cond, value) } - } export class Restriction { @@ -162,14 +151,6 @@ export class Restriction { return this.alternatives.map((alternative) => alternative.encode()).join('|') } - public valueOf(): string { - return this.encode() - } - - public toString() { - return this.encode() - } - public static decode(encodedStr: string): [Restriction, string] { let encStr = encodedStr let alternative: Alternative @@ -219,14 +200,6 @@ export class Rune { return this.restrictions.map((restriction) => restriction.encode()).join('&') } - public valueOf() { - return this.encode() - } - - public toString() { - return this.encode() - } - public static from(encodedStr: string): Rune { const restrictions: Restriction[] = [] let restriction: Restriction diff --git a/test/unit/utils/event.spec.ts b/test/unit/utils/event.spec.ts index fc42aa8..cc49b4b 100644 --- a/test/unit/utils/event.spec.ts +++ b/test/unit/utils/event.spec.ts @@ -4,6 +4,7 @@ import { CanonicalEvent, Event } from '../../../src/@types/event' import { isDelegatedEvent, isDelegatedEventValid, + isDeleteEvent, isEphemeralEvent, isEventIdValid, isEventMatchingFilter, @@ -430,5 +431,58 @@ describe('NIP-26', () => { it('resolves with true if delegated event is valid', async () => { expect(await isDelegatedEventValid(event)).to.be.true }) + + it('resolves with false if no delegation tag is found', async () => { + event.tags = [] + expect(await isDelegatedEventValid(event)).to.be.false + }) + + it('resolves with false if delegation signature is invalid', async () => { + event.tags[0][3] = 'f' + expect(await isDelegatedEventValid(event)).to.be.false + }) + + it('resolves with false if delegation rule is not a valid rune', async () => { + event.tags[0][2] = '@' + expect(await isDelegatedEventValid(event)).to.be.false + }) + + + it('resolves with false if no delegation rule does not match', async () => { + event.tags[0][2] = 'a=1' + expect(await isDelegatedEventValid(event)).to.be.false + }) + }) + + describe('isEventMatchingFilter', () => { + it('returns true if author is delegator', () => { + expect( + isEventMatchingFilter({ authors: ['86f0689bd48dcd19c67a19d994f938ee34f251d8c39976290955ff585f2db42e'] })(event) + ).to.be.true + }) + + it('returns false if author is not delegator', () => { + expect( + isEventMatchingFilter({ authors: ['e8b487c079b0f67c695ae6c4c2552a47f38adfa2533cc5926bd2c102942fdcb7'] })(event) + ).to.be.false + }) + }) +}) + +describe('NIP-09', () => { + describe('isDeleteEvent', () => { + it('returns true if event is kind 5', () => { + const event: Event = { + kind: 5, + } as any + expect(isDeleteEvent(event)).to.be.true + }) + + it('returns false if event is not kind 5', () => { + const event: Event = { + kind: 5 * 100000, + } as any + expect(isDeleteEvent(event)).to.be.false + }) }) }) diff --git a/test/unit/utils/runes.spec.ts b/test/unit/utils/runes.spec.ts new file mode 100644 index 0000000..a1c7b6d --- /dev/null +++ b/test/unit/utils/runes.spec.ts @@ -0,0 +1,219 @@ +import { expect } from 'chai' +import sinon from 'sinon' + +import { Alternative } from '../../../src/utils/runes' + +describe.only('Alternative', () => { + describe('constructor', () => { + it('throw error if field has punctuations', () => { + expect(() => new Alternative('%', '=', 'value')).to.throw(Error, 'Field is not valid') + }) + + it('throw error if cond is not valid', () => { + expect(() => new Alternative('field', '@', 'value')).to.throw(Error, 'Cond is not valid') + }) + + it('returns an Alternative if given valid fields', () => { + expect(new Alternative('field', '=', 'value')).to.be.an.instanceOf(Alternative) + }) + }) + + describe('test', () => { + it('returns undefined always if rule is #', () => { + expect(new Alternative('', '#', '').test({})).to.be.undefined + }) + + it('returns reason if field not present with rule field=value', () => { + expect(new Alternative('field', '=', 'value').test({})).to.equal('field: is missing') + }) + + it('returns undefined if field is not present with rule field!', () => { + expect(new Alternative('field', '!', '').test({})).to.be.undefined + }) + + it('returns reason if field is present with rule field!', () => { + expect(new Alternative('field', '!', '').test({ field: 'value' })).to.equal('field: is present') + }) + + it('calls function if field is a function', () => { + const spy = sinon.fake.returns('reason') + const alternative = new Alternative('field', '=', 'value') + + const result = alternative.test({ field: spy }) + + expect(spy).to.have.been.calledOnceWithExactly(alternative) + expect(result).to.equal('reason') + }) + + it('returns reason if field not equals value with rule field=value', () => { + expect(new Alternative('field', '=', 'value').test({ field: 'not value' })).to.equal('field: != value') + }) + + it('returns undefined if field equals value with rule field=value', () => { + expect(new Alternative('field', '=', 'value').test({ field: 'value' })).to.be.undefined + }) + + it('returns undefined if field not equals value with rule field/value', () => { + expect(new Alternative('field', '/', 'value').test({ field: 'not value' })).to.be.undefined + }) + + it('returns reason if field equals value with rule field/value', () => { + expect(new Alternative('field', '/', 'value').test({ field: 'value' })).to.equal('field: = value') + }) + + it('returns undefined if field starts with value with rule field^value', () => { + expect(new Alternative('field', '^', 'value').test({ field: 'value <- here' })).to.be.undefined + }) + + it('returns reason if field does not start value with rule field^value', () => { + expect(new Alternative('field', '^', 'value').test({ field: 'nope' })).to.equal('field: does not start with value') + }) + + it('returns undefined if field ends with value with rule field$value', () => { + expect(new Alternative('field', '$', 'value').test({ field: 'ends -> value' })).to.be.undefined + }) + + it('returns reason if field does not end value with rule field$value', () => { + expect(new Alternative('field', '$', 'value').test({ field: 'nope' })).to.equal('field: does not end with value') + }) + + it('returns undefined if field contains value with in string rule field~value', () => { + expect(new Alternative('field', '~', 'value').test({ field: '-> value <-' })).to.be.undefined + }) + + it('returns reason if field does not contain value in string with rule field~value', () => { + expect(new Alternative('field', '~', 'value').test({ field: 'nope' })).to.equal('field: does not contain value') + }) + + it('returns undefined if field contains value in array with rule field~value', () => { + expect(new Alternative('field', '~', 'value').test({ field: ['value'] })).to.be.undefined + }) + + it('returns reason if field does not contain value in array with rule field~value', () => { + expect(new Alternative('field', '~', 'value').test({ field: [] })).to.equal('field: does not contain value') + }) + + + + + it('returns undefined if field is less than value with rule field { + expect(new Alternative('field', '<', '0').test({ field: -1 })).to.be.undefined + }) + + it('returns reason if field does not less than value with rule field { + expect(new Alternative('field', '<', '0').test({ field: 0 })).to.equal('field: >= 0') + }) + + it('returns undefined if field is greater than value with rule field>value', () => { + expect(new Alternative('field', '>', '0').test({ field: 1 })).to.be.undefined + }) + + it('returns reason if field does not greater than value with rule field>value', () => { + expect(new Alternative('field', '>', '0').test({ field: 0 })).to.equal('field: <= 0') + }) + + it('returns reason if field is not an integer with rule field { + expect(new Alternative('field', '<', '0').test({ field: 'not an integer' })).to.equal('field: not an integer field') + }) + + it('returns reason if field is not an integer with rule field>value', () => { + expect(new Alternative('field', '>', '0').test({ field: 'not an integer' })).to.equal('field: not an integer field') + }) + + it('returns reason if field is not an integer with rule field { + expect(new Alternative('field', '<', 'not an integer').test({ field: 1 })).to.equal('field: not a valid integer') + }) + + it('returns reason if field is not an integer with rule field>value', () => { + expect(new Alternative('field', '>', 'not an integer').test({ field: 1 })).to.equal('field: not a valid integer') + }) + + + + it('returns undefined if field is same as value with rule field{value', () => { + expect(new Alternative('field', '{', 'b').test({ field: 'b' })).to.equal('field: is the same or ordered after b') + }) + + it('returns undefined if field is ordered before value with rule field{value', () => { + expect(new Alternative('field', '{', 'b').test({ field: 'a' })).to.be.undefined + }) + + it('returns reason if field is ordered after value with rule field{value', () => { + expect(new Alternative('field', '{', 'b').test({ field: 'c' })).to.equal('field: is the same or ordered after b') + }) + + it('returns undefined if field is same as value with rule field}value', () => { + expect(new Alternative('field', '}', 'b').test({ field: 'b' })).to.equal('field: is the same or ordered before b') + }) + + it('returns undefined if field is ordered after value with rule field}value', () => { + expect(new Alternative('field', '}', 'b').test({ field: 'c' })).to.be.undefined + }) + + it('returns reason if field is ordered before value with rule field}value', () => { + expect(new Alternative('field', '}', 'b').test({ field: 'a' })).to.equal('field: is the same or ordered before b') + }) + }) + + describe('encode', () => { + it('returns encoded alternative field = value', () => { + expect(new Alternative('field', '=', 'value').encode()).to.equal('field=value') + }) + + it('returns encoded alternative #', () => { + expect(new Alternative('', '#', '').encode()).to.equal('#') + }) + + it('returns encoded alternative field!', () => { + expect(new Alternative('field', '!', '').encode()).to.equal('field!') + }) + + it('returns encoded alternative field=\\|&', () => { + expect(new Alternative('field', '=', '\\|&').encode()).to.equal('field=\\\\\\|\\&') + }) + }) + + describe('decode', () => { + it('decodes #', () => { + const [alternative] = Alternative.decode('#') + expect(alternative.encode()).to.equal('#') + }) + + it('decodes field!', () => { + const [alternative] = Alternative.decode('field!') + expect(alternative.encode()).to.equal('field!') + }) + + it('decodes field=value', () => { + const [alternative] = Alternative.decode('field=value') + expect(alternative.encode()).to.equal('field=value') + }) + + it('decodes field=\\\\\\|\\&', () => { + const [alternative] = Alternative.decode('field=\\\\\\|\\&') + expect(alternative.encode()).to.equal('field=\\\\\\|\\&') + }) + + it('throw error decoding value', () => { + expect(() => Alternative.decode('value')).to.throw(Error, 'value does not contain any operator') + }) + + it('decodes until first | and consumes it', () => { + const [alternative, remainder] = Alternative.decode('a=1|b=2') + expect(alternative.encode()).to.equal('a=1') + expect(remainder).to.equal('b=2') + }) + + it('decodes until first &', () => { + const [alternative, remainder] = Alternative.decode('a=1&b=2') + expect(alternative.encode()).to.equal('a=1') + expect(remainder).to.equal('&b=2') + }) + }) + + describe('from', () => { + it('creates alternative rule with spaces "field = value"', () => { + expect(Alternative.from('field = value').encode()).to.equal('field=value') + }) + }) +}) diff --git a/test/unit/utils/stream.spec.ts b/test/unit/utils/stream.spec.ts index 15912a9..6915663 100644 --- a/test/unit/utils/stream.spec.ts +++ b/test/unit/utils/stream.spec.ts @@ -2,7 +2,7 @@ import * as chai from 'chai' import * as sinon from 'sinon' import sinonChai from 'sinon-chai' -import { streamEach, streamEnd, streamMap } from '../../../src/utils/stream' +import { streamEach, streamEnd, streamFilter, streamMap } from '../../../src/utils/stream' chai.use(sinonChai) @@ -54,3 +54,20 @@ describe('streamEnd', () => { }) }) +describe('streamFilter', () => { + it('passes elements that pass the given predicate downstream', () => { + const spy = sinon.spy() + + const stream = streamFilter(({ a }) => a > 1 && a < 4) + stream.on('data', spy) + stream.write({ a: 1 }) + stream.write({ a: 2 }) + stream.write({ a: 3 }) + stream.write({ a: 4 }) + stream.end() + + expect(spy).to.have.been.calledTwice + expect(spy.firstCall).to.have.been.calledWithExactly({ a: 2 }) + expect(spy.secondCall).to.have.been.calledWithExactly({ a: 3 }) + }) +})