diff --git a/nip99.test.ts b/nip99.test.ts new file mode 100644 index 0000000..442cd6b --- /dev/null +++ b/nip99.test.ts @@ -0,0 +1,506 @@ +import { describe, expect, test } from 'bun:test' + +import { Event } from './core' +import { ClassifiedListing, DraftClassifiedListing } from './kinds' +import { ClassifiedListingObject, generateEventTemplate, parseEvent, validateEvent } from './nip99' +import { finalizeEvent, generateSecretKey } from './pure' + +describe('validateEvent', () => { + test('should return true for a valid classified listing event', () => { + const sk = generateSecretKey() + const event: Event = finalizeEvent( + { + created_at: Math.floor(Date.now() / 1000), + kind: ClassifiedListing, + content: + 'Lorem [ipsum][nostr:nevent1qqst8cujky046negxgwwm5ynqwn53t8aqjr6afd8g59nfqwxpdhylpcpzamhxue69uhhyetvv9ujuetcv9khqmr99e3k7mg8arnc9] dolor sit amet. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n\nRead more at nostr:naddr1qqzkjurnw4ksz9thwden5te0wfjkccte9ehx7um5wghx7un8qgs2d90kkcq3nk2jry62dyf50k0h36rhpdtd594my40w9pkal876jxgrqsqqqa28pccpzu.', + tags: [ + ['d', 'sample-title'], + ['title', 'Sample Title'], + ['summary', 'Sample Summary'], + ['published_at', '1296962229'], + ['location', 'NYC'], + ['price', '100', 'USD'], + ['image', 'https://example.com/image1.jpg', '800x600'], + ['image', 'https://example.com/image2.jpg'], + ['t', 'tag1'], + ['t', 'tag2'], + ['e', 'value1', 'value2'], + ['a', 'value1', 'value2'], + ], + }, + sk, + ) + + expect(validateEvent(event)).toBe(true) + }) + + test('should return false when the "d" tag is missing', () => { + const sk = generateSecretKey() + const event: Event = finalizeEvent( + { + created_at: Math.floor(Date.now() / 1000), + kind: ClassifiedListing, + content: + 'Lorem [ipsum][nostr:nevent1qqst8cujky046negxgwwm5ynqwn53t8aqjr6afd8g59nfqwxpdhylpcpzamhxue69uhhyetvv9ujuetcv9khqmr99e3k7mg8arnc9] dolor sit amet. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n\nRead more at nostr:naddr1qqzkjurnw4ksz9thwden5te0wfjkccte9ehx7um5wghx7un8qgs2d90kkcq3nk2jry62dyf50k0h36rhpdtd594my40w9pkal876jxgrqsqqqa28pccpzu.', + tags: [ + // Missing 'd' tag + ['title', 'Sample Title'], + ['summary', 'Sample Summary'], + ['published_at', '1296962229'], + ['location', 'NYC'], + ['price', '100', 'USD'], + ['image', 'https://example.com/image1.jpg', '800x600'], + ['image', 'https://example.com/image2.jpg'], + ['t', 'tag1'], + ['t', 'tag2'], + ['e', 'value1', 'value2'], + ['a', 'value1', 'value2'], + ], + }, + sk, + ) + + expect(validateEvent(event)).toBe(false) + }) + + test('should return false when the "title" tag is missing', () => { + const sk = generateSecretKey() + const event: Event = finalizeEvent( + { + created_at: Math.floor(Date.now() / 1000), + kind: ClassifiedListing, + content: + 'Lorem [ipsum][nostr:nevent1qqst8cujky046negxgwwm5ynqwn53t8aqjr6afd8g59nfqwxpdhylpcpzamhxue69uhhyetvv9ujuetcv9khqmr99e3k7mg8arnc9] dolor sit amet. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n\nRead more at nostr:naddr1qqzkjurnw4ksz9thwden5te0wfjkccte9ehx7um5wghx7un8qgs2d90kkcq3nk2jry62dyf50k0h36rhpdtd594my40w9pkal876jxgrqsqqqa28pccpzu.', + tags: [ + ['d', 'sample-title'], + // Missing 'title' tag + ['summary', 'Sample Summary'], + ['published_at', '1296962229'], + ['location', 'NYC'], + ['price', '100', 'USD'], + ['image', 'https://example.com/image1.jpg', '800x600'], + ['image', 'https://example.com/image2.jpg'], + ['t', 'tag1'], + ['t', 'tag2'], + ['e', 'value1', 'value2'], + ['a', 'value1', 'value2'], + ], + }, + sk, + ) + + expect(validateEvent(event)).toBe(false) + }) + + test('should return false when the "summary" tag is missing', () => { + const sk = generateSecretKey() + const event: Event = finalizeEvent( + { + created_at: Math.floor(Date.now() / 1000), + kind: ClassifiedListing, + content: + 'Lorem [ipsum][nostr:nevent1qqst8cujky046negxgwwm5ynqwn53t8aqjr6afd8g59nfqwxpdhylpcpzamhxue69uhhyetvv9ujuetcv9khqmr99e3k7mg8arnc9] dolor sit amet.\n\nRead more at nostr:naddr1qqzkjurnw4ksz9thwden5te0wfjkccte9ehx7um5wghx7un8qgs2d90kkcq3nk2jry62dyf50k0h36rhpdtd594my40w9pkal876jxgrqsqqqa28pccpzu.', + tags: [ + ['d', 'sample-title'], + ['title', 'Sample Title'], + // Missing 'summary' tag + ['published_at', '1296962229'], + ['location', 'NYC'], + ['price', '100', 'USD'], + ['image', 'https://example.com/image1.jpg', '800x600'], + ['image', 'https://example.com/image2.jpg'], + ['t', 'tag1'], + ['t', 'tag2'], + ['e', 'value1', 'value2'], + ['a', 'value1', 'value2'], + ], + }, + sk, + ) + + expect(validateEvent(event)).toBe(false) + }) + + test('should return false when the "published_at" tag is missing', () => { + const sk = generateSecretKey() + const event: Event = finalizeEvent( + { + created_at: Math.floor(Date.now() / 1000), + kind: ClassifiedListing, + content: + 'Lorem [ipsum][nostr:nevent1qqst8cujky046negxgwwm5ynqwn53t8aqjr6afd8g59nfqwxpdhylpcpzamhxue69uhhyetvv9ujuetcv9khqmr99e3k7mg8arnc9] dolor sit amet. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n\nRead more at nostr:naddr1qqzkjurnw4ksz9thwden5te0wfjkccte9ehx7um5wghx7un8qgs2d90kkcq3nk2jry62dyf50k0h36rhpdtd594my40w9pkal876jxgrqsqqqa28pccpzu.', + tags: [ + ['d', 'sample-title'], + ['title', 'Sample Title'], + ['summary', 'Sample Summary'], + // Missing 'published_at' tag + ['location', 'NYC'], + ['price', '100', 'USD'], + ['image', 'https://example.com/image1.jpg', '800x600'], + ['image', 'https://example.com/image2.jpg'], + ['t', 'tag1'], + ['t', 'tag2'], + ['e', 'value1', 'value2'], + ['a', 'value1', 'value2'], + ], + }, + sk, + ) + + expect(validateEvent(event)).toBe(false) + }) + + test('should return false when the "location" tag is missing', () => { + const sk = generateSecretKey() + const event: Event = finalizeEvent( + { + created_at: Math.floor(Date.now() / 1000), + kind: ClassifiedListing, + content: + 'Lorem [ipsum][nostr:nevent1qqst8cujky046negxgwwm5ynqwn53t8aqjr6afd8g59nfqwxpdhylpcpzamhxue69uhhyetvv9ujuetcv9khqmr99e3k7mg8arnc9] dolor sit amet. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n\nRead more at nostr:naddr1qqzkjurnw4ksz9thwden5te0wfjkccte9ehx7um5wghx7un8qgs2d90kkcq3nk2jry62dyf50k0h36rhpdtd594my40w9pkal876jxgrqsqqqa28pccpzu.', + tags: [ + ['d', 'sample-title'], + ['title', 'Sample Title'], + ['summary', 'Sample Summary'], + ['published_at', '1296962229'], + // Missing 'location' tag + ['price', '100', 'USD'], + ['image', 'https://example.com/image1.jpg', '800x600'], + ['image', 'https://example.com/image2.jpg'], + ['t', 'tag1'], + ['t', 'tag2'], + ['e', 'value1', 'value2'], + ['a', 'value1', 'value2'], + ], + }, + sk, + ) + + expect(validateEvent(event)).toBe(false) + }) + + test('should return false when the "price" tag is missing', () => { + const sk = generateSecretKey() + const event: Event = finalizeEvent( + { + created_at: Math.floor(Date.now() / 1000), + kind: ClassifiedListing, + content: + 'Lorem [ipsum][nostr:nevent1qqst8cujky046negxgwwm5ynqwn53t8aqjr6afd8g59nfqwxpdhylpcpzamhxue69uhhyetvv9ujuetcv9khqmr99e3k7mg8arnc9] dolor sit amet. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n\nRead more at nostr:naddr1qqzkjurnw4ksz9thwden5te0wfjkccte9ehx7um5wghx7un8qgs2d90kkcq3nk2jry62dyf50k0h36rhpdtd594my40w9pkal876jxgrqsqqqa28pccpzu.', + tags: [ + ['d', 'sample-title'], + ['title', 'Sample Title'], + ['summary', 'Sample Summary'], + ['published_at', '1296962229'], + ['location', 'NYC'], + // Missing 'price' tag + ['image', 'https://example.com/image1.jpg', '800x600'], + ['image', 'https://example.com/image2.jpg'], + ['t', 'tag1'], + ['t', 'tag2'], + ['e', 'value1', 'value2'], + ['a', 'value1', 'value2'], + ], + }, + sk, + ) + + expect(validateEvent(event)).toBe(false) + }) + + test('should return false when the "published_at" tag is not a valid timestamp', () => { + const sk = generateSecretKey() + const event: Event = finalizeEvent( + { + created_at: Math.floor(Date.now() / 1000), + kind: ClassifiedListing, + content: 'Lorem ipsum dolor sit amet.', + tags: [ + ['d', 'sample-title'], + ['title', 'Sample Title'], + ['summary', 'Sample Summary'], + ['published_at', 'not-a-valid-timestamp'], + ['location', 'NYC'], + ['price', '100', 'USD'], + ['image', 'https://example.com/image1.jpg', '800x600'], + ['image', 'https://example.com/image2.jpg'], + ['t', 'tag1'], + ['t', 'tag2'], + ], + }, + sk, + ) + + expect(validateEvent(event)).toBe(false) + }) + + test('should return false when the "price" tag has not a valid price', () => { + const sk = generateSecretKey() + const event: Event = finalizeEvent( + { + created_at: Math.floor(Date.now() / 1000), + kind: ClassifiedListing, + content: 'Lorem ipsum dolor sit amet.', + tags: [ + ['d', 'sample-title'], + ['title', 'Sample Title'], + ['summary', 'Sample Summary'], + ['published_at', '1296962229'], + ['location', 'NYC'], + ['price', 'not-a-valid-price', 'USD'], + ['image', 'https://example.com/image1.jpg', '800x600'], + ['image', 'https://example.com/image2.jpg'], + ['t', 'tag1'], + ['t', 'tag2'], + ], + }, + sk, + ) + + expect(validateEvent(event)).toBe(false) + }) + + test('should return false when the "price" tag has not a valid currency', () => { + const sk = generateSecretKey() + const event: Event = finalizeEvent( + { + created_at: Math.floor(Date.now() / 1000), + kind: ClassifiedListing, + content: 'Lorem ipsum dolor sit amet.', + tags: [ + ['d', 'sample-title'], + ['title', 'Sample Title'], + ['summary', 'Sample Summary'], + ['published_at', '1296962229'], + ['location', 'NYC'], + ['price', '100', 'not-a-valid-currency'], + ['image', 'https://example.com/image1.jpg', '800x600'], + ['image', 'https://example.com/image2.jpg'], + ['t', 'tag1'], + ['t', 'tag2'], + ], + }, + sk, + ) + + expect(validateEvent(event)).toBe(false) + }) + + test('should return false when the "price" tag has not a valid number of elements', () => { + const sk = generateSecretKey() + const event1: Event = finalizeEvent( + { + created_at: Math.floor(Date.now() / 1000), + kind: ClassifiedListing, + content: 'Lorem ipsum dolor sit amet.', + tags: [ + ['d', 'sample-title'], + ['title', 'Sample Title'], + ['summary', 'Sample Summary'], + ['published_at', '1296962229'], + ['location', 'NYC'], + ['price', '100'], + ['image', 'https://example.com/image1.jpg', '800x600'], + ['image', 'https://example.com/image2.jpg'], + ['t', 'tag1'], + ['t', 'tag2'], + ], + }, + sk, + ) + + expect(validateEvent(event1)).toBe(false) + }) + + test('should return false when the "a" tag has not a valid number of elements', () => { + const sk = generateSecretKey() + const event1: Event = finalizeEvent( + { + created_at: Math.floor(Date.now() / 1000), + kind: ClassifiedListing, + content: 'Lorem ipsum dolor sit amet.', + tags: [ + ['d', 'sample-title'], + ['title', 'Sample Title'], + ['summary', 'Sample Summary'], + ['published_at', '1296962229'], + ['location', 'NYC'], + ['price', '100', 'USD'], + ['image', 'https://example.com/image1.jpg', '800x600'], + ['image', 'https://example.com/image2.jpg'], + ['a', 'extra1'], + ['a', 'extra2', 'value2', 'extra3'], + ], + }, + sk, + ) + + const event2: Event = finalizeEvent( + { + created_at: Math.floor(Date.now() / 1000), + kind: ClassifiedListing, + content: 'Lorem ipsum dolor sit amet.', + tags: [ + ['d', 'sample-title'], + ['title', 'Sample Title'], + ['summary', 'Sample Summary'], + ['published_at', '1296962229'], + ['location', 'NYC'], + ['price', '100', 'USD'], + ['image', 'https://example.com/image1.jpg', '800x600'], + ['image', 'https://example.com/image2.jpg'], + ['e', 'extra1'], + ['e', 'extra2', 'value2', 'extra3'], + ], + }, + sk, + ) + + expect(validateEvent(event1)).toBe(false) + expect(validateEvent(event2)).toBe(false) + }) +}) + +describe('parseEvent', () => { + test('should parse a valid event', () => { + const sk = generateSecretKey() + const event: Event = finalizeEvent( + { + created_at: Math.floor(Date.now() / 1000), + kind: DraftClassifiedListing, + content: + 'Lorem [ipsum][nostr:nevent1qqst8cujky046negxgwwm5ynqwn53t8aqjr6afd8g59nfqwxpdhylpcpzamhxue69uhhyetvv9ujuetcv9khqmr99e3k7mg8arnc9] dolor sit amet. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n\nRead more at nostr:naddr1qqzkjurnw4ksz9thwden5te0wfjkccte9ehx7um5wghx7un8qgs2d90kkcq3nk2jry62dyf50k0h36rhpdtd594my40w9pkal876jxgrqsqqqa28pccpzu.', + tags: [ + ['d', 'sample-title'], + ['title', 'Sample Title'], + ['summary', 'Sample Summary'], + ['published_at', '1296962229'], + ['location', 'NYC'], + ['price', '100', 'USD'], + ['image', 'https://example.com/image1.jpg', '800x600'], + ['image', 'https://example.com/image2.jpg'], + ['t', 'tag1'], + ['t', 'tag2'], + ['e', 'value1', 'value2'], + ['a', 'value1', 'value2'], + ], + }, + sk, + ) + + const expectedListing = { + title: 'Sample Title', + summary: 'Sample Summary', + publishedAt: '1296962229', + location: 'NYC', + price: { + amount: '100', + currency: 'USD', + }, + images: [ + { + url: 'https://example.com/image1.jpg', + dimensions: '800x600', + }, + { + url: 'https://example.com/image2.jpg', + }, + ], + hashtags: ['tag1', 'tag2'], + additionalTags: { + e: ['value1', 'value2'], + a: ['value1', 'value2'], + }, + content: + 'Lorem [ipsum][nostr:nevent1qqst8cujky046negxgwwm5ynqwn53t8aqjr6afd8g59nfqwxpdhylpcpzamhxue69uhhyetvv9ujuetcv9khqmr99e3k7mg8arnc9] dolor sit amet. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n\nRead more at nostr:naddr1qqzkjurnw4ksz9thwden5te0wfjkccte9ehx7um5wghx7un8qgs2d90kkcq3nk2jry62dyf50k0h36rhpdtd594my40w9pkal876jxgrqsqqqa28pccpzu.', + isDraft: true, + } + + expect(parseEvent(event)).toEqual(expectedListing) + }) + + test('should throw an error for an invalid event', () => { + const sk = generateSecretKey() + const event: Event = finalizeEvent( + { + created_at: Math.floor(Date.now() / 1000), + kind: DraftClassifiedListing, + content: + 'Lorem [ipsum][nostr:nevent1qqst8cujky046negxgwwm5ynqwn53t8aqjr6afd8g59nfqwxpdhylpcpzamhxue69uhhyetvv9ujuetcv9khqmr99e3k7mg8arnc9] dolor sit amet. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n\nRead more at nostr:naddr1qqzkjurnw4ksz9thwden5te0wfjkccte9ehx7um5wghx7un8qgs2d90kkcq3nk2jry62dyf50k0h36rhpdtd594my40w9pkal876jxgrqsqqqa28pccpzu.', + tags: [ + // Missing 'd' tag + ['title', 'Sample Title'], + ['summary', 'Sample Summary'], + ['published_at', '1296962229'], + ['location', 'NYC'], + ['price', '100', 'USD'], + ['image', 'https://example.com/image1.jpg', '800x600'], + ['image', 'https://example.com/image2.jpg'], + ['t', 'tag1'], + ['t', 'tag2'], + ['e', 'value1', 'value2'], + ['a', 'value1', 'value2'], + ], + }, + sk, + ) + + expect(() => parseEvent(event)).toThrow(Error) + }) +}) + +describe('generateEventTemplate', () => { + test('should generate the correct event template for a classified listing', () => { + const listing: ClassifiedListingObject = { + title: 'Sample Title', + summary: 'Sample Summary', + publishedAt: '1296962229', + location: 'NYC', + price: { + amount: '100', + currency: 'USD', + }, + images: [ + { + url: 'https://example.com/image1.jpg', + dimensions: '800x600', + }, + { + url: 'https://example.com/image2.jpg', + }, + ], + hashtags: ['tag1', 'tag2'], + additionalTags: { + extra1: 'value1', + extra2: 'value2', + }, + content: + 'Lorem ipsum dolor sit amet. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n\nRead more at nostr:naddr1qqzkjurnw4ksz9thwden5te0wfjkccte9ehx7um5wghx7un8qgs2d90kkcq3nk2jry62dyf50k0h36rhpdtd594my40w9pkal876jxgrqsqqqa28pccpzu.', + isDraft: true, + } + + const expectedEventTemplate = { + kind: DraftClassifiedListing, + content: + 'Lorem ipsum dolor sit amet. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n\nRead more at nostr:naddr1qqzkjurnw4ksz9thwden5te0wfjkccte9ehx7um5wghx7un8qgs2d90kkcq3nk2jry62dyf50k0h36rhpdtd594my40w9pkal876jxgrqsqqqa28pccpzu.', + tags: [ + ['d', 'sample-title'], + ['title', 'Sample Title'], + ['published_at', '1296962229'], + ['summary', 'Sample Summary'], + ['location', 'NYC'], + ['price', '100', 'USD'], + ['image', 'https://example.com/image1.jpg', '800x600'], + ['image', 'https://example.com/image2.jpg'], + ['t', 'tag1'], + ['t', 'tag2'], + ['extra1', 'value1'], + ['extra2', 'value2'], + ], + created_at: expect.any(Number), + } + + expect(generateEventTemplate(listing)).toEqual(expectedEventTemplate) + }) +})