Files
buzz/desktop/src/features/projects/projectState.test.mjs
2026-09-01 16:52:54 -04:00

174 lines
4.5 KiB
JavaScript

import assert from "node:assert/strict";
import test from "node:test";
import { finalizeEvent, getPublicKey } from "nostr-tools/pure";
import {
buildProjectRelatedChannelChangeTemplate,
changeProjectRelatedChannels,
fetchProjectState,
parseProjectState,
} from "./projectState.ts";
const RELAY_SECRET = new Uint8Array(32).fill(7);
const RELAY_PUBKEY = getPublicKey(RELAY_SECRET);
const OWNER = "a".repeat(64);
const IDENTITY_ID = "b".repeat(64);
const COORDINATE = `30621:${OWNER}:buzz`;
const CHANNEL_A = "11111111-1111-4111-8111-111111111111";
const CHANNEL_B = "22222222-2222-4222-8222-222222222222";
function stateEvent({
revision = "1",
identityId = IDENTITY_ID,
content,
} = {}) {
return finalizeEvent(
{
kind: 30623,
created_at: 100,
content:
content ??
JSON.stringify({
v: 1,
deleted: false,
project_tags: [
["d", "buzz"],
["name", "Buzz"],
["buzz-related-channel", CHANNEL_A],
],
}),
tags: [
["d", "c".repeat(64)],
["a", COORDINATE],
["rev", revision],
["e", identityId, "", "identity"],
["e", "d".repeat(64), "", "change"],
],
},
RELAY_SECRET,
);
}
test("parseProjectState accepts a signed exact-coordinate strict v1 projection", () => {
const state = parseProjectState(stateEvent(), RELAY_PUBKEY, COORDINATE);
assert.equal(state.revision, "1");
assert.equal(state.identityEventId, IDENTITY_ID);
assert.deepEqual(state.projectTags.at(-1), [
"buzz-related-channel",
CHANNEL_A,
]);
});
test("parseProjectState rejects wrong authors, signatures, revisions, coordinates, and unknown content fields", () => {
assert.throws(
() => parseProjectState(stateEvent(), "e".repeat(64), COORDINATE),
/signed by this relay/,
);
const badSignature = JSON.parse(JSON.stringify(stateEvent()));
badSignature.sig = "0".repeat(128);
assert.throws(
() => parseProjectState(badSignature, RELAY_PUBKEY, COORDINATE),
/signed by this relay/,
);
assert.throws(
() =>
parseProjectState(
stateEvent({ revision: "01" }),
RELAY_PUBKEY,
COORDINATE,
),
/revision is not canonical/,
);
assert.throws(
() => parseProjectState(stateEvent(), RELAY_PUBKEY, `30621:${OWNER}:other`),
/requested coordinate/,
);
assert.throws(
() =>
parseProjectState(
stateEvent({
content: JSON.stringify({
v: 1,
deleted: false,
project_tags: [],
extra: true,
}),
}),
RELAY_PUBKEY,
COORDINATE,
),
/strict version 1/,
);
});
test("buildProjectRelatedChannelChangeTemplate emits the exact sorted kind:47010 command", () => {
assert.deepEqual(
buildProjectRelatedChannelChangeTemplate(
COORDINATE,
{ add: [CHANNEL_B, CHANNEL_A], remove: [] },
"7",
),
{
kind: 47010,
tags: [
["a", COORDINATE],
["expected-revision", "7"],
],
content: JSON.stringify({
v: 1,
patch: {
related_channels: { add: [CHANNEL_A, CHANNEL_B], remove: [] },
},
}),
},
);
});
test("fetchProjectState distinguishes absence from a present untrusted projection", async () => {
const deps = {
getRelayPubkey: async () => RELAY_PUBKEY,
fetchEvents: async () => [],
};
assert.equal(await fetchProjectState(COORDINATE, deps), null);
await assert.rejects(
fetchProjectState(COORDINATE, {
...deps,
fetchEvents: async () => [{ ...stateEvent(), content: "{}" }],
}),
/untrusted or unsupported/,
);
});
test("changeProjectRelatedChannels refetches and retries one revision conflict", async () => {
const states = [stateEvent({ revision: "4" }), stateEvent({ revision: "5" })];
const signedTemplates = [];
let fetchCount = 0;
let publishCount = 0;
await changeProjectRelatedChannels(
{ projectAddress: COORDINATE },
{ add: [CHANNEL_B], remove: [] },
{
getRelayPubkey: async () => RELAY_PUBKEY,
fetchEvents: async () => [states[fetchCount++]],
signEvent: async (template) => {
signedTemplates.push(template);
return stateEvent();
},
publishEvent: async () => {
publishCount += 1;
if (publishCount === 1) {
throw new Error("conflict: Project revision is 5");
}
},
},
);
assert.equal(fetchCount, 2);
assert.equal(publishCount, 2);
assert.deepEqual(
signedTemplates.map((template) => template.tags[1][1]),
["4", "5"],
);
});