mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 16:02:26 +00:00
Merge pull request #1554 from PR0M3TH3AN/ai/schema-validate-20231027-11649813579808866368
chore(ai): event schema validation tool + fixes
This commit is contained in:
4 files changed
+643
-21
No files matched your search
@@ -311,3 +311,27 @@ device, etc.) should remain on-device via the
|
||||
Refer to the [`WatchHistoryService`](../js/watchHistoryService.js) for queue
|
||||
management hooks and metadata toggle controls that complement these schema
|
||||
definitions.【F:js/watchHistoryService.js†L695-L776】【F:js/watchHistoryService.js†L1040-L1093】
|
||||
|
||||
## Validation
|
||||
|
||||
bitvid now enforces schema validation at runtime during development. Every event
|
||||
builder automatically runs the generated event against its definition in
|
||||
`validateEventAgainstSchema` when `isDevMode` is true. Violations (missing
|
||||
required tags, invalid JSON content, etc.) are logged to the console via
|
||||
`devLogger`.
|
||||
|
||||
### Continuous Integration
|
||||
|
||||
A standalone validation suite runs in CI to ensure that all builders produce
|
||||
valid events:
|
||||
|
||||
```bash
|
||||
# Run the validation script
|
||||
node scripts/agent/validate-events.mjs
|
||||
|
||||
# Run the unit test suite
|
||||
npm run test:unit
|
||||
```
|
||||
|
||||
When modifying schemas or builders, use `scripts/agent/validate-events.mjs` to
|
||||
verify your changes locally.
|
||||
+154
-21
@@ -870,13 +870,19 @@ export function buildVideoMirrorEvent(params) {
|
||||
}
|
||||
});
|
||||
}
|
||||
return {
|
||||
const event = {
|
||||
kind: schema?.kind ?? 1063,
|
||||
pubkey,
|
||||
created_at,
|
||||
tags: combinedTags,
|
||||
content: typeof content === "string" ? content : String(content ?? ""),
|
||||
};
|
||||
|
||||
if (isDevMode) {
|
||||
validateEventAgainstSchema(NOTE_TYPES.VIDEO_MIRROR, event);
|
||||
}
|
||||
|
||||
return event;
|
||||
}
|
||||
|
||||
export function buildRepostEvent(params) {
|
||||
@@ -986,13 +992,19 @@ export function buildRepostEvent(params) {
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
const event = {
|
||||
kind: resolvedKind,
|
||||
pubkey,
|
||||
created_at,
|
||||
tags,
|
||||
content,
|
||||
};
|
||||
|
||||
if (isDevMode) {
|
||||
validateEventAgainstSchema(NOTE_TYPES.REPOST, event);
|
||||
}
|
||||
|
||||
return event;
|
||||
}
|
||||
|
||||
export function buildShareEvent(params) {
|
||||
@@ -1093,13 +1105,19 @@ export function buildShareEvent(params) {
|
||||
|
||||
const resolvedContent = ensureValidUtf8Content(content);
|
||||
|
||||
return {
|
||||
const event = {
|
||||
kind: schema?.kind ?? 1,
|
||||
pubkey,
|
||||
created_at,
|
||||
tags,
|
||||
content: resolvedContent,
|
||||
};
|
||||
|
||||
if (isDevMode) {
|
||||
validateEventAgainstSchema(NOTE_TYPES.SHARE, event);
|
||||
}
|
||||
|
||||
return event;
|
||||
}
|
||||
|
||||
export function buildRelayListEvent(params) {
|
||||
@@ -1164,13 +1182,19 @@ export function buildRelayListEvent(params) {
|
||||
tags.push(...sanitizedAdditionalTags.map((tag) => tag.slice()));
|
||||
}
|
||||
|
||||
return {
|
||||
const event = {
|
||||
kind: schema?.kind ?? 10002,
|
||||
pubkey,
|
||||
created_at,
|
||||
tags,
|
||||
content: "",
|
||||
};
|
||||
|
||||
if (isDevMode) {
|
||||
validateEventAgainstSchema(NOTE_TYPES.RELAY_LIST, event);
|
||||
}
|
||||
|
||||
return event;
|
||||
}
|
||||
|
||||
export function buildDmRelayListEvent(params) {
|
||||
@@ -1200,13 +1224,19 @@ export function buildDmRelayListEvent(params) {
|
||||
tags.push(...sanitizedAdditionalTags.map((tag) => tag.slice()));
|
||||
}
|
||||
|
||||
return {
|
||||
const event = {
|
||||
kind: schema?.kind ?? 10050,
|
||||
pubkey,
|
||||
created_at,
|
||||
tags,
|
||||
content: "",
|
||||
};
|
||||
|
||||
if (isDevMode) {
|
||||
validateEventAgainstSchema(NOTE_TYPES.DM_RELAY_LIST, event);
|
||||
}
|
||||
|
||||
return event;
|
||||
}
|
||||
|
||||
export function buildProfileMetadataEvent(params) {
|
||||
@@ -1236,13 +1266,19 @@ export function buildProfileMetadataEvent(params) {
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
const event = {
|
||||
kind: schema?.kind ?? 0,
|
||||
pubkey,
|
||||
created_at,
|
||||
tags,
|
||||
content: serializedContent,
|
||||
};
|
||||
|
||||
if (isDevMode) {
|
||||
validateEventAgainstSchema(NOTE_TYPES.PROFILE_METADATA, event);
|
||||
}
|
||||
|
||||
return event;
|
||||
}
|
||||
|
||||
export function buildMuteListEvent(params) {
|
||||
@@ -1277,13 +1313,19 @@ export function buildMuteListEvent(params) {
|
||||
tags.push(...sanitizedAdditionalTags.map((tag) => tag.slice()));
|
||||
}
|
||||
|
||||
return {
|
||||
const event = {
|
||||
kind: schema?.kind ?? 10000,
|
||||
pubkey,
|
||||
created_at,
|
||||
tags,
|
||||
content: typeof content === "string" ? content : "",
|
||||
};
|
||||
|
||||
if (isDevMode) {
|
||||
validateEventAgainstSchema(NOTE_TYPES.MUTE_LIST, event);
|
||||
}
|
||||
|
||||
return event;
|
||||
}
|
||||
|
||||
export function buildDeletionEvent(params) {
|
||||
@@ -1322,13 +1364,19 @@ export function buildDeletionEvent(params) {
|
||||
tags.push(...sanitizedAdditionalTags.map((tag) => tag.slice()));
|
||||
}
|
||||
|
||||
return {
|
||||
const event = {
|
||||
kind: schema?.kind ?? 5,
|
||||
pubkey,
|
||||
created_at,
|
||||
tags,
|
||||
content: typeof reason === "string" ? reason : "",
|
||||
};
|
||||
|
||||
if (isDevMode) {
|
||||
validateEventAgainstSchema(NOTE_TYPES.DELETION, event);
|
||||
}
|
||||
|
||||
return event;
|
||||
}
|
||||
|
||||
export function buildLegacyDirectMessageEvent(params) {
|
||||
@@ -1353,13 +1401,19 @@ export function buildLegacyDirectMessageEvent(params) {
|
||||
tags.push(...sanitizedAdditionalTags.map((tag) => tag.slice()));
|
||||
}
|
||||
|
||||
return {
|
||||
const event = {
|
||||
kind: schema?.kind ?? 4,
|
||||
pubkey,
|
||||
created_at,
|
||||
tags,
|
||||
content: typeof ciphertext === "string" ? ciphertext : "",
|
||||
};
|
||||
|
||||
if (isDevMode) {
|
||||
validateEventAgainstSchema(NOTE_TYPES.LEGACY_DM, event);
|
||||
}
|
||||
|
||||
return event;
|
||||
}
|
||||
|
||||
export function buildDmAttachmentEvent(params) {
|
||||
@@ -1403,13 +1457,19 @@ export function buildDmAttachmentEvent(params) {
|
||||
tags.push(...sanitizedAdditionalTags.map((tag) => tag.slice()));
|
||||
}
|
||||
|
||||
return {
|
||||
const event = {
|
||||
kind: schema?.kind ?? 15,
|
||||
pubkey,
|
||||
created_at,
|
||||
tags,
|
||||
content: "",
|
||||
};
|
||||
|
||||
if (isDevMode) {
|
||||
validateEventAgainstSchema(NOTE_TYPES.DM_ATTACHMENT, event);
|
||||
}
|
||||
|
||||
return event;
|
||||
}
|
||||
|
||||
export function buildDmReadReceiptEvent(params) {
|
||||
@@ -1444,13 +1504,19 @@ export function buildDmReadReceiptEvent(params) {
|
||||
tags.push(...sanitizedAdditionalTags.map((tag) => tag.slice()));
|
||||
}
|
||||
|
||||
return {
|
||||
const event = {
|
||||
kind: schema?.kind ?? 20001,
|
||||
pubkey,
|
||||
created_at,
|
||||
tags,
|
||||
content: "",
|
||||
};
|
||||
|
||||
if (isDevMode) {
|
||||
validateEventAgainstSchema(NOTE_TYPES.DM_READ_RECEIPT, event);
|
||||
}
|
||||
|
||||
return event;
|
||||
}
|
||||
|
||||
export function buildDmTypingIndicatorEvent(params) {
|
||||
@@ -1489,13 +1555,19 @@ export function buildDmTypingIndicatorEvent(params) {
|
||||
tags.push(...sanitizedAdditionalTags.map((tag) => tag.slice()));
|
||||
}
|
||||
|
||||
return {
|
||||
const event = {
|
||||
kind: schema?.kind ?? 20002,
|
||||
pubkey,
|
||||
created_at,
|
||||
tags,
|
||||
content: "",
|
||||
};
|
||||
|
||||
if (isDevMode) {
|
||||
validateEventAgainstSchema(NOTE_TYPES.DM_TYPING, event);
|
||||
}
|
||||
|
||||
return event;
|
||||
}
|
||||
|
||||
export function buildViewEvent(params) {
|
||||
@@ -1550,13 +1622,19 @@ export function buildViewEvent(params) {
|
||||
|
||||
const resolvedContent = ensureValidUtf8Content(content);
|
||||
|
||||
return {
|
||||
const event = {
|
||||
kind: schema?.kind ?? WATCH_HISTORY_KIND,
|
||||
pubkey,
|
||||
created_at,
|
||||
tags,
|
||||
content: resolvedContent,
|
||||
};
|
||||
|
||||
if (isDevMode) {
|
||||
validateEventAgainstSchema(NOTE_TYPES.VIEW_EVENT, event);
|
||||
}
|
||||
|
||||
return event;
|
||||
}
|
||||
|
||||
export function buildZapRequestEvent(params) {
|
||||
@@ -1624,13 +1702,19 @@ export function buildZapRequestEvent(params) {
|
||||
|
||||
const resolvedContent = ensureValidUtf8Content(content);
|
||||
|
||||
return {
|
||||
const event = {
|
||||
kind: schema?.kind ?? 9734,
|
||||
pubkey,
|
||||
created_at,
|
||||
tags,
|
||||
content: resolvedContent,
|
||||
};
|
||||
|
||||
if (isDevMode) {
|
||||
validateEventAgainstSchema(NOTE_TYPES.ZAP_REQUEST, event);
|
||||
}
|
||||
|
||||
return event;
|
||||
}
|
||||
|
||||
export function buildReactionEvent(params) {
|
||||
@@ -1756,13 +1840,19 @@ export function buildReactionEvent(params) {
|
||||
|
||||
const resolvedContent = ensureValidUtf8Content(content);
|
||||
|
||||
return {
|
||||
const event = {
|
||||
kind: schema?.kind ?? 7,
|
||||
pubkey,
|
||||
created_at,
|
||||
tags,
|
||||
content: resolvedContent,
|
||||
};
|
||||
|
||||
if (isDevMode) {
|
||||
validateEventAgainstSchema(NOTE_TYPES.VIDEO_REACTION, event);
|
||||
}
|
||||
|
||||
return event;
|
||||
}
|
||||
|
||||
export function buildCommentEvent(params) {
|
||||
@@ -2001,13 +2091,19 @@ export function buildCommentEvent(params) {
|
||||
|
||||
const resolvedContent = ensureValidUtf8Content(content);
|
||||
|
||||
return {
|
||||
const event = {
|
||||
kind: schema?.kind ?? 1111,
|
||||
pubkey,
|
||||
created_at,
|
||||
tags,
|
||||
content: resolvedContent,
|
||||
};
|
||||
|
||||
if (isDevMode) {
|
||||
validateEventAgainstSchema(NOTE_TYPES.VIDEO_COMMENT, event);
|
||||
}
|
||||
|
||||
return event;
|
||||
}
|
||||
|
||||
export function buildWatchHistoryEvent(params) {
|
||||
@@ -2043,13 +2139,19 @@ export function buildWatchHistoryEvent(params) {
|
||||
|
||||
appendSchemaTags(tags, schema);
|
||||
|
||||
return {
|
||||
const event = {
|
||||
kind: schema?.kind ?? WATCH_HISTORY_KIND,
|
||||
pubkey,
|
||||
created_at: Number.isFinite(created_at) ? created_at : Math.floor(Date.now() / 1000),
|
||||
tags,
|
||||
content: typeof content === "string" ? content : String(content ?? ""),
|
||||
};
|
||||
|
||||
if (isDevMode) {
|
||||
validateEventAgainstSchema(NOTE_TYPES.WATCH_HISTORY, event);
|
||||
}
|
||||
|
||||
return event;
|
||||
}
|
||||
|
||||
export function buildSubscriptionListEvent(params) {
|
||||
@@ -2072,13 +2174,20 @@ export function buildSubscriptionListEvent(params) {
|
||||
tags.push(["encrypted", normalizedEncryption]);
|
||||
}
|
||||
appendSchemaTags(tags, schema);
|
||||
return {
|
||||
|
||||
const event = {
|
||||
kind: schema?.kind ?? 30000,
|
||||
pubkey,
|
||||
created_at,
|
||||
tags,
|
||||
content: typeof content === "string" ? content : String(content ?? ""),
|
||||
};
|
||||
|
||||
if (isDevMode) {
|
||||
validateEventAgainstSchema(NOTE_TYPES.SUBSCRIPTION_LIST, event);
|
||||
}
|
||||
|
||||
return event;
|
||||
}
|
||||
|
||||
export function buildBlockListEvent(params) {
|
||||
@@ -2101,13 +2210,20 @@ export function buildBlockListEvent(params) {
|
||||
tags.push(["encrypted", normalizedEncryption]);
|
||||
}
|
||||
appendSchemaTags(tags, schema);
|
||||
return {
|
||||
|
||||
const event = {
|
||||
kind: schema?.kind ?? 10000,
|
||||
pubkey,
|
||||
created_at,
|
||||
tags,
|
||||
content: typeof content === "string" ? content : String(content ?? ""),
|
||||
};
|
||||
|
||||
if (isDevMode) {
|
||||
validateEventAgainstSchema(NOTE_TYPES.USER_BLOCK_LIST, event);
|
||||
}
|
||||
|
||||
return event;
|
||||
}
|
||||
|
||||
export function buildHashtagPreferenceEvent(params) {
|
||||
@@ -2124,13 +2240,20 @@ export function buildHashtagPreferenceEvent(params) {
|
||||
tags.push([identifierName, identifierValue]);
|
||||
}
|
||||
appendSchemaTags(tags, schema);
|
||||
return {
|
||||
|
||||
const event = {
|
||||
kind: schema?.kind ?? 30015,
|
||||
pubkey,
|
||||
created_at,
|
||||
tags,
|
||||
content: typeof content === "string" ? content : String(content ?? ""),
|
||||
};
|
||||
|
||||
if (isDevMode) {
|
||||
validateEventAgainstSchema(NOTE_TYPES.HASHTAG_PREFERENCES, event);
|
||||
}
|
||||
|
||||
return event;
|
||||
}
|
||||
|
||||
function resolveAdminNoteType(listKey) {
|
||||
@@ -2178,13 +2301,23 @@ export function buildAdminListEvent(listKey, params) {
|
||||
}
|
||||
});
|
||||
appendSchemaTags(tags, schema);
|
||||
return {
|
||||
|
||||
const event = {
|
||||
kind: schema?.kind ?? 30000,
|
||||
pubkey,
|
||||
created_at,
|
||||
tags,
|
||||
content: "",
|
||||
};
|
||||
|
||||
if (isDevMode) {
|
||||
const resolvedType = resolveAdminNoteType(listKey);
|
||||
if (resolvedType) {
|
||||
validateEventAgainstSchema(resolvedType, event);
|
||||
}
|
||||
}
|
||||
|
||||
return event;
|
||||
}
|
||||
|
||||
function hasTag(tags, tagName, tagValue = null) {
|
||||
|
||||
@@ -0,0 +1,240 @@
|
||||
import { strict as assert } from 'node:assert';
|
||||
|
||||
// Hook console.warn to capture validation warnings
|
||||
const originalWarn = console.warn;
|
||||
const warnings = [];
|
||||
console.warn = (...args) => {
|
||||
// Capture only [schema] or [nostrEventSchemas] warnings
|
||||
const msg = args.join(' ');
|
||||
if (msg.includes('[schema]') || msg.includes('[nostrEventSchemas]')) {
|
||||
warnings.push(msg);
|
||||
}
|
||||
// We can still print to stdout if needed, but for validation we want to capture
|
||||
// originalWarn.apply(console, args);
|
||||
};
|
||||
|
||||
// Import schemas AFTER hooking console
|
||||
const schemas = await import('../../js/nostrEventSchemas.js');
|
||||
const {
|
||||
validateEventAgainstSchema,
|
||||
NOTE_TYPES,
|
||||
buildVideoPostEvent,
|
||||
buildVideoMirrorEvent,
|
||||
buildRepostEvent,
|
||||
buildShareEvent,
|
||||
buildRelayListEvent,
|
||||
buildDmRelayListEvent,
|
||||
buildProfileMetadataEvent,
|
||||
buildMuteListEvent,
|
||||
buildDeletionEvent,
|
||||
buildLegacyDirectMessageEvent,
|
||||
buildDmAttachmentEvent,
|
||||
buildDmReadReceiptEvent,
|
||||
buildDmTypingIndicatorEvent,
|
||||
buildViewEvent,
|
||||
buildZapRequestEvent,
|
||||
buildReactionEvent,
|
||||
buildCommentEvent,
|
||||
buildWatchHistoryEvent,
|
||||
buildSubscriptionListEvent,
|
||||
buildBlockListEvent,
|
||||
buildHashtagPreferenceEvent,
|
||||
buildAdminListEvent
|
||||
} = schemas;
|
||||
|
||||
console.log("Running Event Schema Validator...");
|
||||
|
||||
const failures = [];
|
||||
|
||||
function check(name, builder, params, expectedType) {
|
||||
warnings.length = 0; // Clear warnings
|
||||
try {
|
||||
const event = builder(params);
|
||||
validateEventAgainstSchema(expectedType || event.kind, event); // Pass type if known, else relies on implementation or event.kind mapping
|
||||
|
||||
// Some builders might not set the type explicitly in validate calls if we don't instrument them yet.
|
||||
// So we manually call validateEventAgainstSchema with the expected NOTE_TYPE.
|
||||
if (expectedType) {
|
||||
validateEventAgainstSchema(expectedType, event);
|
||||
}
|
||||
|
||||
if (warnings.length > 0) {
|
||||
failures.push({ name, error: `Generated warnings: ${JSON.stringify(warnings)}` });
|
||||
}
|
||||
|
||||
// specific checks can go here
|
||||
if (!event.created_at) failures.push({ name, error: "Missing created_at" });
|
||||
if (!event.pubkey) failures.push({ name, error: "Missing pubkey" });
|
||||
|
||||
} catch (e) {
|
||||
failures.push({ name, error: `Threw exception: ${e.message}` });
|
||||
}
|
||||
}
|
||||
|
||||
const pubkey = "0000000000000000000000000000000000000000000000000000000000000001";
|
||||
const created_at = Math.floor(Date.now() / 1000);
|
||||
|
||||
// Test cases
|
||||
check("buildVideoPostEvent", buildVideoPostEvent, {
|
||||
pubkey,
|
||||
created_at,
|
||||
dTagValue: "video-123",
|
||||
content: {
|
||||
version: 3,
|
||||
title: "Test Video",
|
||||
videoRootId: "root-123",
|
||||
url: "https://example.com/video.mp4"
|
||||
}
|
||||
}, NOTE_TYPES.VIDEO_POST);
|
||||
|
||||
check("buildVideoMirrorEvent", buildVideoMirrorEvent, {
|
||||
pubkey,
|
||||
created_at,
|
||||
tags: [["t", "test"]],
|
||||
content: "Mirror description"
|
||||
}, NOTE_TYPES.VIDEO_MIRROR);
|
||||
|
||||
check("buildRepostEvent", buildRepostEvent, {
|
||||
pubkey,
|
||||
created_at,
|
||||
eventId: "1111111111111111111111111111111111111111111111111111111111111111",
|
||||
eventRelay: "wss://relay.example.com",
|
||||
serializedEvent: JSON.stringify({ id: "111", kind: 1 })
|
||||
}, NOTE_TYPES.REPOST);
|
||||
|
||||
check("buildShareEvent", buildShareEvent, {
|
||||
pubkey,
|
||||
created_at,
|
||||
content: "Check this out",
|
||||
video: { id: "2222222222222222222222222222222222222222222222222222222222222222" }
|
||||
}, NOTE_TYPES.SHARE);
|
||||
|
||||
check("buildRelayListEvent", buildRelayListEvent, {
|
||||
pubkey,
|
||||
created_at,
|
||||
relays: ["wss://relay.example.com", { url: "wss://read.example.com", mode: "read" }]
|
||||
}, NOTE_TYPES.RELAY_LIST);
|
||||
|
||||
check("buildDmRelayListEvent", buildDmRelayListEvent, {
|
||||
pubkey,
|
||||
created_at,
|
||||
relays: ["wss://dm.example.com"]
|
||||
}, NOTE_TYPES.DM_RELAY_LIST);
|
||||
|
||||
check("buildProfileMetadataEvent", buildProfileMetadataEvent, {
|
||||
pubkey,
|
||||
created_at,
|
||||
metadata: { name: "Test User", about: "Testing" }
|
||||
}, NOTE_TYPES.PROFILE_METADATA);
|
||||
|
||||
check("buildMuteListEvent", buildMuteListEvent, {
|
||||
pubkey,
|
||||
created_at,
|
||||
pTags: ["3333333333333333333333333333333333333333333333333333333333333333"]
|
||||
}, NOTE_TYPES.MUTE_LIST);
|
||||
|
||||
check("buildDeletionEvent", buildDeletionEvent, {
|
||||
pubkey,
|
||||
created_at,
|
||||
eventIds: ["4444444444444444444444444444444444444444444444444444444444444444"],
|
||||
reason: "Mistake"
|
||||
}, NOTE_TYPES.DELETION);
|
||||
|
||||
check("buildLegacyDirectMessageEvent", buildLegacyDirectMessageEvent, {
|
||||
pubkey,
|
||||
created_at,
|
||||
recipientPubkey: "5555555555555555555555555555555555555555555555555555555555555555",
|
||||
ciphertext: "encryped_content"
|
||||
}, NOTE_TYPES.LEGACY_DM);
|
||||
|
||||
check("buildDmAttachmentEvent", buildDmAttachmentEvent, {
|
||||
pubkey,
|
||||
created_at,
|
||||
recipientPubkey: "6666666666666666666666666666666666666666666666666666666666666666",
|
||||
attachment: { url: "https://example.com/file.jpg", x: "hash" }
|
||||
}, NOTE_TYPES.DM_ATTACHMENT);
|
||||
|
||||
check("buildDmReadReceiptEvent", buildDmReadReceiptEvent, {
|
||||
pubkey,
|
||||
created_at,
|
||||
recipientPubkey: "7777777777777777777777777777777777777777777777777777777777777777",
|
||||
eventId: "8888888888888888888888888888888888888888888888888888888888888888"
|
||||
}, NOTE_TYPES.DM_READ_RECEIPT);
|
||||
|
||||
check("buildDmTypingIndicatorEvent", buildDmTypingIndicatorEvent, {
|
||||
pubkey,
|
||||
created_at,
|
||||
recipientPubkey: "9999999999999999999999999999999999999999999999999999999999999999"
|
||||
}, NOTE_TYPES.DM_TYPING);
|
||||
|
||||
check("buildViewEvent", buildViewEvent, {
|
||||
pubkey,
|
||||
created_at,
|
||||
pointerValue: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
pointerTag: ["e", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"],
|
||||
dedupeTag: "session-123"
|
||||
}, NOTE_TYPES.VIEW_EVENT);
|
||||
|
||||
check("buildZapRequestEvent", buildZapRequestEvent, {
|
||||
pubkey,
|
||||
created_at,
|
||||
recipientPubkey: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
||||
amountSats: 100,
|
||||
relays: ["wss://zap.relay.com"]
|
||||
}, NOTE_TYPES.ZAP_REQUEST);
|
||||
|
||||
check("buildReactionEvent", buildReactionEvent, {
|
||||
pubkey,
|
||||
created_at,
|
||||
pointerTag: ["e", "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"],
|
||||
content: "+"
|
||||
}, NOTE_TYPES.VIDEO_REACTION);
|
||||
|
||||
check("buildCommentEvent", buildCommentEvent, {
|
||||
pubkey,
|
||||
created_at,
|
||||
videoEventId: "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd",
|
||||
content: "Nice video!"
|
||||
}, NOTE_TYPES.VIDEO_COMMENT);
|
||||
|
||||
check("buildWatchHistoryEvent", buildWatchHistoryEvent, {
|
||||
pubkey,
|
||||
created_at,
|
||||
monthIdentifier: "2023-10",
|
||||
content: JSON.stringify({ version: 2, month: "2023-10", items: [] })
|
||||
}, NOTE_TYPES.WATCH_HISTORY);
|
||||
|
||||
check("buildSubscriptionListEvent", buildSubscriptionListEvent, {
|
||||
pubkey,
|
||||
created_at,
|
||||
content: "encrypted_subs"
|
||||
}, NOTE_TYPES.SUBSCRIPTION_LIST);
|
||||
|
||||
check("buildBlockListEvent", buildBlockListEvent, {
|
||||
pubkey,
|
||||
created_at,
|
||||
content: "encrypted_blocks"
|
||||
}, NOTE_TYPES.USER_BLOCK_LIST);
|
||||
|
||||
check("buildHashtagPreferenceEvent", buildHashtagPreferenceEvent, {
|
||||
pubkey,
|
||||
created_at,
|
||||
content: "encrypted_prefs"
|
||||
}, NOTE_TYPES.HASHTAG_PREFERENCES);
|
||||
|
||||
check("buildAdminListEvent (moderation)", (p) => buildAdminListEvent("moderation", p), {
|
||||
pubkey,
|
||||
created_at,
|
||||
hexPubkeys: ["eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"]
|
||||
}, NOTE_TYPES.ADMIN_MODERATION_LIST);
|
||||
|
||||
if (failures.length > 0) {
|
||||
console.error("\n❌ Validation Failed:");
|
||||
failures.forEach(f => {
|
||||
console.error(`- ${f.name}: ${f.error}`);
|
||||
});
|
||||
process.exit(1);
|
||||
} else {
|
||||
console.log("\n✅ All events validated successfully.");
|
||||
process.exit(0);
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
import { describe, it, before, after } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
validateEventAgainstSchema,
|
||||
NOTE_TYPES,
|
||||
buildVideoPostEvent,
|
||||
buildVideoMirrorEvent,
|
||||
buildRepostEvent,
|
||||
buildShareEvent,
|
||||
buildRelayListEvent,
|
||||
buildDmRelayListEvent,
|
||||
buildProfileMetadataEvent,
|
||||
buildMuteListEvent,
|
||||
buildDeletionEvent,
|
||||
buildLegacyDirectMessageEvent,
|
||||
buildDmAttachmentEvent,
|
||||
buildDmReadReceiptEvent,
|
||||
buildDmTypingIndicatorEvent,
|
||||
buildViewEvent,
|
||||
buildZapRequestEvent,
|
||||
buildReactionEvent,
|
||||
buildCommentEvent,
|
||||
buildWatchHistoryEvent,
|
||||
buildSubscriptionListEvent,
|
||||
buildBlockListEvent,
|
||||
buildHashtagPreferenceEvent,
|
||||
buildAdminListEvent
|
||||
} from '../js/nostrEventSchemas.js';
|
||||
|
||||
describe('Nostr Event Schemas', () => {
|
||||
const pubkey = "0000000000000000000000000000000000000000000000000000000000000001";
|
||||
const created_at = Math.floor(Date.now() / 1000);
|
||||
let originalWarn;
|
||||
let warnings = [];
|
||||
|
||||
before(() => {
|
||||
originalWarn = console.warn;
|
||||
console.warn = (...args) => {
|
||||
const msg = args.join(' ');
|
||||
if (msg.includes('[schema]') || msg.includes('[nostrEventSchemas]')) {
|
||||
warnings.push(msg);
|
||||
}
|
||||
// originalWarn.apply(console, args);
|
||||
};
|
||||
});
|
||||
|
||||
after(() => {
|
||||
console.warn = originalWarn;
|
||||
});
|
||||
|
||||
function check(name, builder, params, expectedType) {
|
||||
it(`should validate ${name}`, () => {
|
||||
warnings.length = 0;
|
||||
const event = builder(params);
|
||||
|
||||
// Since we instrumented the builders, they run validation internally if IS_DEV_MODE is true.
|
||||
// We also run it explicitly here to be sure.
|
||||
validateEventAgainstSchema(expectedType || event.kind, event);
|
||||
if (expectedType) {
|
||||
validateEventAgainstSchema(expectedType, event);
|
||||
}
|
||||
|
||||
if (warnings.length > 0) {
|
||||
assert.fail(`Validation warnings for ${name}: ${JSON.stringify(warnings)}`);
|
||||
}
|
||||
|
||||
assert.ok(event.created_at, "Missing created_at");
|
||||
assert.ok(event.pubkey, "Missing pubkey");
|
||||
assert.ok(Array.isArray(event.tags), "Tags should be an array");
|
||||
});
|
||||
}
|
||||
|
||||
check("buildVideoPostEvent", buildVideoPostEvent, {
|
||||
pubkey,
|
||||
created_at,
|
||||
dTagValue: "video-123",
|
||||
content: {
|
||||
version: 3,
|
||||
title: "Test Video",
|
||||
videoRootId: "root-123",
|
||||
url: "https://example.com/video.mp4"
|
||||
}
|
||||
}, NOTE_TYPES.VIDEO_POST);
|
||||
|
||||
check("buildVideoMirrorEvent", buildVideoMirrorEvent, {
|
||||
pubkey,
|
||||
created_at,
|
||||
tags: [["t", "test"]],
|
||||
content: "Mirror description"
|
||||
}, NOTE_TYPES.VIDEO_MIRROR);
|
||||
|
||||
check("buildRepostEvent", buildRepostEvent, {
|
||||
pubkey,
|
||||
created_at,
|
||||
eventId: "1111111111111111111111111111111111111111111111111111111111111111",
|
||||
eventRelay: "wss://relay.example.com",
|
||||
serializedEvent: JSON.stringify({ id: "111", kind: 1 })
|
||||
}, NOTE_TYPES.REPOST);
|
||||
|
||||
check("buildShareEvent", buildShareEvent, {
|
||||
pubkey,
|
||||
created_at,
|
||||
content: "Check this out",
|
||||
video: { id: "2222222222222222222222222222222222222222222222222222222222222222" }
|
||||
}, NOTE_TYPES.SHARE);
|
||||
|
||||
check("buildRelayListEvent", buildRelayListEvent, {
|
||||
pubkey,
|
||||
created_at,
|
||||
relays: ["wss://relay.example.com", { url: "wss://read.example.com", mode: "read" }]
|
||||
}, NOTE_TYPES.RELAY_LIST);
|
||||
|
||||
check("buildDmRelayListEvent", buildDmRelayListEvent, {
|
||||
pubkey,
|
||||
created_at,
|
||||
relays: ["wss://dm.example.com"]
|
||||
}, NOTE_TYPES.DM_RELAY_LIST);
|
||||
|
||||
check("buildProfileMetadataEvent", buildProfileMetadataEvent, {
|
||||
pubkey,
|
||||
created_at,
|
||||
metadata: { name: "Test User", about: "Testing" }
|
||||
}, NOTE_TYPES.PROFILE_METADATA);
|
||||
|
||||
check("buildMuteListEvent", buildMuteListEvent, {
|
||||
pubkey,
|
||||
created_at,
|
||||
pTags: ["3333333333333333333333333333333333333333333333333333333333333333"]
|
||||
}, NOTE_TYPES.MUTE_LIST);
|
||||
|
||||
check("buildDeletionEvent", buildDeletionEvent, {
|
||||
pubkey,
|
||||
created_at,
|
||||
eventIds: ["4444444444444444444444444444444444444444444444444444444444444444"],
|
||||
reason: "Mistake"
|
||||
}, NOTE_TYPES.DELETION);
|
||||
|
||||
check("buildLegacyDirectMessageEvent", buildLegacyDirectMessageEvent, {
|
||||
pubkey,
|
||||
created_at,
|
||||
recipientPubkey: "5555555555555555555555555555555555555555555555555555555555555555",
|
||||
ciphertext: "encryped_content"
|
||||
}, NOTE_TYPES.LEGACY_DM);
|
||||
|
||||
check("buildDmAttachmentEvent", buildDmAttachmentEvent, {
|
||||
pubkey,
|
||||
created_at,
|
||||
recipientPubkey: "6666666666666666666666666666666666666666666666666666666666666666",
|
||||
attachment: { url: "https://example.com/file.jpg", x: "hash" }
|
||||
}, NOTE_TYPES.DM_ATTACHMENT);
|
||||
|
||||
check("buildDmReadReceiptEvent", buildDmReadReceiptEvent, {
|
||||
pubkey,
|
||||
created_at,
|
||||
recipientPubkey: "7777777777777777777777777777777777777777777777777777777777777777",
|
||||
eventId: "8888888888888888888888888888888888888888888888888888888888888888"
|
||||
}, NOTE_TYPES.DM_READ_RECEIPT);
|
||||
|
||||
check("buildDmTypingIndicatorEvent", buildDmTypingIndicatorEvent, {
|
||||
pubkey,
|
||||
created_at,
|
||||
recipientPubkey: "9999999999999999999999999999999999999999999999999999999999999999"
|
||||
}, NOTE_TYPES.DM_TYPING);
|
||||
|
||||
check("buildViewEvent", buildViewEvent, {
|
||||
pubkey,
|
||||
created_at,
|
||||
pointerValue: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
pointerTag: ["e", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"],
|
||||
dedupeTag: "session-123"
|
||||
}, NOTE_TYPES.VIEW_EVENT);
|
||||
|
||||
check("buildZapRequestEvent", buildZapRequestEvent, {
|
||||
pubkey,
|
||||
created_at,
|
||||
recipientPubkey: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
||||
amountSats: 100,
|
||||
relays: ["wss://zap.relay.com"]
|
||||
}, NOTE_TYPES.ZAP_REQUEST);
|
||||
|
||||
check("buildReactionEvent", buildReactionEvent, {
|
||||
pubkey,
|
||||
created_at,
|
||||
pointerTag: ["e", "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"],
|
||||
content: "+"
|
||||
}, NOTE_TYPES.VIDEO_REACTION);
|
||||
|
||||
check("buildCommentEvent", buildCommentEvent, {
|
||||
pubkey,
|
||||
created_at,
|
||||
videoEventId: "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd",
|
||||
content: "Nice video!"
|
||||
}, NOTE_TYPES.VIDEO_COMMENT);
|
||||
|
||||
check("buildWatchHistoryEvent", buildWatchHistoryEvent, {
|
||||
pubkey,
|
||||
created_at,
|
||||
monthIdentifier: "2023-10",
|
||||
content: JSON.stringify({ version: 2, month: "2023-10", items: [] })
|
||||
}, NOTE_TYPES.WATCH_HISTORY);
|
||||
|
||||
check("buildSubscriptionListEvent", buildSubscriptionListEvent, {
|
||||
pubkey,
|
||||
created_at,
|
||||
content: "encrypted_subs"
|
||||
}, NOTE_TYPES.SUBSCRIPTION_LIST);
|
||||
|
||||
check("buildBlockListEvent", buildBlockListEvent, {
|
||||
pubkey,
|
||||
created_at,
|
||||
content: "encrypted_blocks"
|
||||
}, NOTE_TYPES.USER_BLOCK_LIST);
|
||||
|
||||
check("buildHashtagPreferenceEvent", buildHashtagPreferenceEvent, {
|
||||
pubkey,
|
||||
created_at,
|
||||
content: "encrypted_prefs"
|
||||
}, NOTE_TYPES.HASHTAG_PREFERENCES);
|
||||
|
||||
check("buildAdminListEvent (moderation)", (p) => buildAdminListEvent("moderation", p), {
|
||||
pubkey,
|
||||
created_at,
|
||||
hexPubkeys: ["eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"]
|
||||
}, NOTE_TYPES.ADMIN_MODERATION_LIST);
|
||||
});
|
||||
Reference in new issue
Block a user