mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-11-02 19:02:28 +00:00
Merge branch 'unstable' into security-fix-disable-dev-mode-6861215856535760775
This commit is contained in:
20 files changed
+2659
-1717
No files matched your search
@@ -305,3 +305,98 @@ await (async () => {
|
||||
assert.ok(detail);
|
||||
assert.equal(detail.pubkey, SAMPLE_PUBKEY);
|
||||
})();
|
||||
|
||||
// === Tests for loadOwnProfile logic ===
|
||||
|
||||
await (async () => {
|
||||
resetState();
|
||||
const PUBKEY = SAMPLE_PUBKEY;
|
||||
|
||||
// Test Fast Relay Success
|
||||
{
|
||||
const mockNostrClient = {
|
||||
relays: ["wss://fast1.com", "wss://fast2.com", "wss://slow.com"],
|
||||
pool: {
|
||||
list: async (relays) => {
|
||||
const url = relays[0];
|
||||
if (url === "wss://fast1.com") {
|
||||
return [{
|
||||
pubkey: PUBKEY,
|
||||
created_at: 100,
|
||||
content: JSON.stringify({ name: "Fast User" })
|
||||
}];
|
||||
}
|
||||
return new Promise(() => {}); // Hang others
|
||||
}
|
||||
},
|
||||
handleEvent: () => {}
|
||||
};
|
||||
|
||||
const service = new AuthService({ nostrClient: mockNostrClient });
|
||||
// Override setProfileCacheEntry to avoid side effects or errors from real implementation if not needed,
|
||||
// but here we just let it run. However, the service needs nostrClient.pool
|
||||
|
||||
// We need to make sure setProfileCacheEntry doesn't fail.
|
||||
// It imports from cache.js which uses profileCache.
|
||||
// profileCache is imported from state/profileCache.js.
|
||||
|
||||
// The previous tests didn't seem to mock setProfileCacheEntry.
|
||||
|
||||
const profile = await service.loadOwnProfile(PUBKEY);
|
||||
assert.equal(profile.name, "Fast User");
|
||||
}
|
||||
|
||||
// Test All Relays Fail
|
||||
{
|
||||
const mockNostrClient = {
|
||||
relays: ["wss://fail1.com", "wss://fail2.com"],
|
||||
pool: {
|
||||
list: async () => {
|
||||
throw new Error("Failed");
|
||||
}
|
||||
},
|
||||
handleEvent: () => {}
|
||||
};
|
||||
|
||||
const service = new AuthService({ nostrClient: mockNostrClient });
|
||||
const profile = await service.loadOwnProfile(PUBKEY);
|
||||
assert.equal(profile.name, "Unknown");
|
||||
}
|
||||
|
||||
// Test Timeout Fallback
|
||||
{
|
||||
let backgroundResolve;
|
||||
const backgroundPromise = new Promise(r => backgroundResolve = r);
|
||||
|
||||
const mockNostrClient = {
|
||||
relays: ["wss://fast-fail.com", "wss://fast-fail2.com", "wss://background-ok.com", "wss://background-ok-2.com"],
|
||||
pool: {
|
||||
list: async (relays) => {
|
||||
const url = relays[0];
|
||||
if (url.includes("fast")) {
|
||||
// Simulate timeout or error
|
||||
throw new Error("Fast fail");
|
||||
}
|
||||
if (url.includes("background")) {
|
||||
await backgroundPromise;
|
||||
return [{
|
||||
pubkey: PUBKEY,
|
||||
created_at: 200,
|
||||
content: JSON.stringify({ name: "Background User" })
|
||||
}];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
},
|
||||
handleEvent: () => {}
|
||||
};
|
||||
|
||||
const service = new AuthService({ nostrClient: mockNostrClient });
|
||||
|
||||
const profile = await service.loadOwnProfile(PUBKEY);
|
||||
assert.equal(profile.name, "Unknown");
|
||||
|
||||
// We resolve background promise to let background logic finish eventually (though test finishes here)
|
||||
backgroundResolve();
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,73 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { createKidsScorerStage } from "../../js/feedEngine/kidsScoring.js";
|
||||
|
||||
test("kids scorer correctly identifies dominant positive component", async () => {
|
||||
const stage = createKidsScorerStage({
|
||||
weights: {
|
||||
w_age: 0.2,
|
||||
w_edu: 0.2,
|
||||
w_author: 0.2,
|
||||
w_pop: 0.2,
|
||||
w_fresh: 0.2,
|
||||
w_risk: 0
|
||||
}
|
||||
});
|
||||
|
||||
// Create an item where one component is clearly dominant
|
||||
// Age appropriateness: 1.0 (perfect duration + tags)
|
||||
// Others: 0
|
||||
const items = [
|
||||
{
|
||||
video: {
|
||||
id: "v1",
|
||||
pubkey: "pk1",
|
||||
duration: 300, // 5 min (perfect for toddler/preschool)
|
||||
tags: [["t", "preschool"], ["t", "learning"]], // Matches preferred
|
||||
created_at: Date.now() / 1000 - 10000000, // Old
|
||||
viewCount: 0
|
||||
},
|
||||
metadata: {},
|
||||
},
|
||||
];
|
||||
|
||||
const why = [];
|
||||
const context = {
|
||||
runtime: {
|
||||
ageGroup: "preschool",
|
||||
trustedAuthors: []
|
||||
},
|
||||
addWhy: (detail) => why.push(detail)
|
||||
};
|
||||
|
||||
await stage(items, context);
|
||||
|
||||
assert.equal(why.length, 1);
|
||||
assert.equal(why[0].reason, "age-appropriateness");
|
||||
assert.ok(why[0].value > 0);
|
||||
});
|
||||
|
||||
test("kids scorer picks educational-boost when dominant", async () => {
|
||||
const stage = createKidsScorerStage({
|
||||
educationalTags: ["math"],
|
||||
weights: { w_age: 0, w_edu: 1, w_author: 0, w_pop: 0, w_fresh: 0, w_risk: 0 }
|
||||
});
|
||||
|
||||
const items = [{
|
||||
video: {
|
||||
id: "v2",
|
||||
tags: [["t", "math"]],
|
||||
duration: 300
|
||||
},
|
||||
metadata: {}
|
||||
}];
|
||||
|
||||
const why = [];
|
||||
await stage(items, {
|
||||
runtime: { educationalTags: ["math"] },
|
||||
addWhy: (d) => why.push(d)
|
||||
});
|
||||
|
||||
assert.equal(why.length, 1);
|
||||
assert.equal(why[0].reason, "educational-boost");
|
||||
});
|
||||
@@ -165,6 +165,53 @@ describe("NostrClient", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("fetchVideos", () => {
|
||||
it("should delegate to subscribeVideos and return result on eose", async () => {
|
||||
let subOpts = null;
|
||||
// Override subscribeVideos to verify delegation
|
||||
client.subscribeVideos = (cb, opts) => {
|
||||
subOpts = opts;
|
||||
return {
|
||||
on: (evt, handler) => {
|
||||
if (evt === "eose") setTimeout(handler, 0);
|
||||
},
|
||||
unsub: () => {},
|
||||
};
|
||||
};
|
||||
|
||||
// Mock helpers
|
||||
client.getActiveVideos = () => [{ id: "v1" }];
|
||||
client.populateNip71MetadataForVideos = async () => {};
|
||||
client.applyRootCreatedAt = () => {};
|
||||
|
||||
const result = await client.fetchVideos({ limit: 5 });
|
||||
assert.equal(result[0].id, "v1");
|
||||
assert.equal(subOpts.limit, 5);
|
||||
// Ensure we force full fetch by default
|
||||
assert.equal(subOpts.since, 0);
|
||||
});
|
||||
|
||||
it("should respect since parameter if provided", async () => {
|
||||
let subOpts = null;
|
||||
client.subscribeVideos = (cb, opts) => {
|
||||
subOpts = opts;
|
||||
return {
|
||||
on: (evt, handler) => {
|
||||
if (evt === "eose") setTimeout(handler, 0);
|
||||
},
|
||||
unsub: () => {},
|
||||
};
|
||||
};
|
||||
|
||||
client.getActiveVideos = () => [];
|
||||
client.populateNip71MetadataForVideos = async () => {};
|
||||
client.applyRootCreatedAt = () => {};
|
||||
|
||||
await client.fetchVideos({ since: 12345 });
|
||||
assert.equal(subOpts.since, 12345);
|
||||
});
|
||||
});
|
||||
|
||||
describe("publishVideo", () => {
|
||||
it("should throw if not logged in", async () => {
|
||||
await assert.rejects(
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
|
||||
import { describe, it, beforeEach, mock } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { Nip46Connector } from "../../js/nostr/nip46Connector.js";
|
||||
|
||||
const VALID_HEX_KEY = "0000000000000000000000000000000000000000000000000000000000000001";
|
||||
const VALID_PUBKEY = "0000000000000000000000000000000000000000000000000000000000000002";
|
||||
|
||||
// Mock dependencies
|
||||
const mockDeps = {
|
||||
parseNip46ConnectionString: mock.fn(),
|
||||
generateNip46Secret: mock.fn(),
|
||||
readStoredNip46Session: mock.fn(),
|
||||
writeStoredNip46Session: mock.fn(),
|
||||
clearStoredNip46Session: mock.fn(),
|
||||
normalizeNip46EncryptionAlgorithm: mock.fn(),
|
||||
decryptNip46Session: mock.fn(),
|
||||
attemptDecryptNip46HandshakePayload: mock.fn(),
|
||||
sanitizeNip46Metadata: mock.fn((m) => m || {}),
|
||||
resolveNip46Relays: mock.fn((r) => r || []),
|
||||
normalizeNostrPubkey: mock.fn((k) => k),
|
||||
encodeHexToNpub: mock.fn((k) => "npub" + k),
|
||||
bytesToHex: mock.fn((b) => VALID_HEX_KEY),
|
||||
signEventWithPrivateKey: mock.fn(),
|
||||
Nip46RpcClient: class MockRpcClient {
|
||||
constructor() {
|
||||
this.destroy = mock.fn();
|
||||
this.ensureSubscription = mock.fn(() => Promise.resolve());
|
||||
this.connect = mock.fn(() => Promise.resolve());
|
||||
this.getUserPubkey = mock.fn(() => Promise.resolve(VALID_PUBKEY));
|
||||
this.metadata = {};
|
||||
}
|
||||
},
|
||||
ensureNostrTools: mock.fn(async () => ({
|
||||
generateSecretKey: () => new Uint8Array(32),
|
||||
getPublicKey: () => VALID_PUBKEY,
|
||||
})),
|
||||
getCachedNostrTools: mock.fn(() => ({
|
||||
generateSecretKey: () => new Uint8Array(32),
|
||||
getPublicKey: () => VALID_PUBKEY,
|
||||
})),
|
||||
NIP46_RPC_KIND: 24133,
|
||||
NIP46_HANDSHAKE_TIMEOUT_MS: 100,
|
||||
NIP46_AUTH_CHALLENGE_MAX_ATTEMPTS: 3,
|
||||
};
|
||||
|
||||
describe("Nip46Connector", () => {
|
||||
let connector;
|
||||
let mockNostrClient;
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset mocks
|
||||
Object.values(mockDeps).forEach((m) => {
|
||||
if (m.mock) m.mock.resetCalls();
|
||||
});
|
||||
|
||||
mockNostrClient = {
|
||||
pool: {
|
||||
publish: mock.fn(() => Promise.resolve()),
|
||||
sub: mock.fn(() => ({
|
||||
on: mock.fn(),
|
||||
unsub: mock.fn(),
|
||||
})),
|
||||
},
|
||||
ensurePool: mock.fn(async () => mockNostrClient.pool),
|
||||
connectToRelays: mock.fn(async () => []),
|
||||
emitRemoteSignerChange: mock.fn(),
|
||||
relays: ["wss://relay.example.com"],
|
||||
};
|
||||
|
||||
connector = new Nip46Connector(mockNostrClient, mockDeps);
|
||||
});
|
||||
|
||||
describe("createKeyPair", () => {
|
||||
it("should generate a new key pair if none provided", async () => {
|
||||
mockDeps.generateNip46Secret.mock.mockImplementationOnce(() => "mock-secret");
|
||||
|
||||
const result = await connector.createKeyPair();
|
||||
|
||||
assert.deepEqual(result, {
|
||||
privateKey: VALID_HEX_KEY,
|
||||
publicKey: VALID_PUBKEY,
|
||||
});
|
||||
});
|
||||
|
||||
it("should use existing keys if provided", async () => {
|
||||
const result = await connector.createKeyPair(
|
||||
VALID_HEX_KEY,
|
||||
VALID_PUBKEY
|
||||
);
|
||||
assert.deepEqual(result, {
|
||||
privateKey: VALID_HEX_KEY,
|
||||
publicKey: VALID_PUBKEY,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("prepareHandshake", () => {
|
||||
it("should parse connection string and return details", async () => {
|
||||
mockDeps.generateNip46Secret.mock.mockImplementationOnce(() => "handshake-secret");
|
||||
|
||||
const result = await connector.prepareHandshake({
|
||||
connectionString: "bunker://...",
|
||||
});
|
||||
|
||||
assert.equal(result.clientPrivateKey, VALID_HEX_KEY);
|
||||
assert.equal(result.secret, "handshake-secret");
|
||||
assert.match(result.connectionString, /^nostrconnect:\/\//);
|
||||
});
|
||||
});
|
||||
|
||||
describe("connect", () => {
|
||||
it("should connect directly if remote pubkey is known", async () => {
|
||||
const options = {
|
||||
connectionString: "bunker://remote-pub?relay=wss://relay.com",
|
||||
clientPrivateKey: VALID_HEX_KEY,
|
||||
};
|
||||
|
||||
mockDeps.parseNip46ConnectionString.mock.mockImplementationOnce(() => ({
|
||||
relays: ["wss://relay.com"],
|
||||
remotePubkey: VALID_PUBKEY,
|
||||
type: "bunker",
|
||||
}));
|
||||
mockDeps.normalizeNip46EncryptionAlgorithm.mock.mockImplementationOnce(() => "nip44");
|
||||
mockDeps.normalizeNostrPubkey.mock.mockImplementation((k) => k);
|
||||
|
||||
const result = await connector.connect(options);
|
||||
|
||||
assert.equal(result.pubkey, VALID_PUBKEY);
|
||||
assert.ok(result.client instanceof mockDeps.Nip46RpcClient);
|
||||
assert.equal(mockDeps.writeStoredNip46Session.mock.callCount(), 1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("reconnectStored", () => {
|
||||
it("should throw if no stored session", async () => {
|
||||
mockDeps.readStoredNip46Session.mock.mockImplementationOnce(() => null);
|
||||
await assert.rejects(connector.reconnectStored(), /No remote signer session is stored/);
|
||||
});
|
||||
|
||||
it("should reconnect using stored session", async () => {
|
||||
mockDeps.readStoredNip46Session.mock.mockImplementationOnce(() => ({
|
||||
userPubkey: "user-pub",
|
||||
remotePubkey: "remote-pub",
|
||||
relays: [],
|
||||
clientPrivateKey: "client-priv",
|
||||
secret: "secret",
|
||||
}));
|
||||
mockDeps.decryptNip46Session.mock.mockImplementationOnce((s) => s);
|
||||
|
||||
const result = await connector.reconnectStored();
|
||||
|
||||
assert.equal(result.pubkey, VALID_PUBKEY);
|
||||
assert.ok(result.client instanceof mockDeps.Nip46RpcClient);
|
||||
});
|
||||
});
|
||||
|
||||
describe("disconnect", () => {
|
||||
it("should clear stored session and emit change", async () => {
|
||||
await connector.disconnect({ keepStored: false });
|
||||
assert.equal(mockDeps.clearStoredNip46Session.mock.callCount(), 1);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,63 @@
|
||||
import { describe, it } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
normalizeHexString,
|
||||
normalizeHexId,
|
||||
normalizeHexPubkey,
|
||||
HEX64_REGEX,
|
||||
} from "../../js/utils/hex.js";
|
||||
|
||||
describe("hex utils", () => {
|
||||
describe("normalizeHexString", () => {
|
||||
it("should return empty string for non-string inputs", () => {
|
||||
assert.equal(normalizeHexString(undefined), "");
|
||||
assert.equal(normalizeHexString(null), "");
|
||||
assert.equal(normalizeHexString(123), "");
|
||||
assert.equal(normalizeHexString({}), "");
|
||||
});
|
||||
|
||||
it("should return empty string for empty or whitespace-only strings", () => {
|
||||
assert.equal(normalizeHexString(""), "");
|
||||
assert.equal(normalizeHexString(" "), "");
|
||||
});
|
||||
|
||||
it("should trim and lowercase valid hex strings", () => {
|
||||
assert.equal(normalizeHexString("ABC"), "abc");
|
||||
assert.equal(normalizeHexString(" DEF "), "def");
|
||||
assert.equal(normalizeHexString("AbCdEf"), "abcdef");
|
||||
});
|
||||
});
|
||||
|
||||
describe("aliases", () => {
|
||||
it("should export normalizeHexId as an alias", () => {
|
||||
assert.equal(normalizeHexId, normalizeHexString);
|
||||
});
|
||||
|
||||
it("should export normalizeHexPubkey as an alias", () => {
|
||||
assert.equal(normalizeHexPubkey, normalizeHexString);
|
||||
});
|
||||
});
|
||||
|
||||
describe("HEX64_REGEX", () => {
|
||||
it("should match valid 64-character hex strings", () => {
|
||||
const validHex = "a".repeat(64);
|
||||
assert.match(validHex, HEX64_REGEX);
|
||||
const mixedCase = "A".repeat(32) + "b".repeat(32);
|
||||
assert.match(mixedCase, HEX64_REGEX);
|
||||
});
|
||||
|
||||
it("should not match strings with incorrect length", () => {
|
||||
assert.doesNotMatch("a".repeat(63), HEX64_REGEX);
|
||||
assert.doesNotMatch("a".repeat(65), HEX64_REGEX);
|
||||
});
|
||||
|
||||
it("should not match strings with non-hex characters", () => {
|
||||
const invalidChar = "a".repeat(63) + "g";
|
||||
assert.doesNotMatch(invalidChar, HEX64_REGEX);
|
||||
});
|
||||
|
||||
it("should not match empty strings", () => {
|
||||
assert.doesNotMatch("", HEX64_REGEX);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,74 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { sanitizeProfileMediaUrl } from "../../js/utils/profileMedia.js";
|
||||
|
||||
test("sanitizeProfileMediaUrl handles non-string inputs", () => {
|
||||
assert.equal(sanitizeProfileMediaUrl(null), "");
|
||||
assert.equal(sanitizeProfileMediaUrl(undefined), "");
|
||||
assert.equal(sanitizeProfileMediaUrl(123), "");
|
||||
assert.equal(sanitizeProfileMediaUrl({}), "");
|
||||
});
|
||||
|
||||
test("sanitizeProfileMediaUrl handles empty or whitespace-only strings", () => {
|
||||
assert.equal(sanitizeProfileMediaUrl(""), "");
|
||||
assert.equal(sanitizeProfileMediaUrl(" "), "");
|
||||
assert.equal(sanitizeProfileMediaUrl("\t\n"), "");
|
||||
});
|
||||
|
||||
test("sanitizeProfileMediaUrl trims whitespace and removes quotes", () => {
|
||||
assert.equal(sanitizeProfileMediaUrl(" http://example.com "), "https://example.com");
|
||||
assert.equal(sanitizeProfileMediaUrl("'http://example.com'"), "https://example.com");
|
||||
assert.equal(sanitizeProfileMediaUrl('"http://example.com"'), "https://example.com");
|
||||
assert.equal(sanitizeProfileMediaUrl(" ' http://example.com ' "), "https://example.com");
|
||||
});
|
||||
|
||||
test("sanitizeProfileMediaUrl allows data:image/ URLs", () => {
|
||||
const dataUrl = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=";
|
||||
assert.equal(sanitizeProfileMediaUrl(dataUrl), dataUrl);
|
||||
});
|
||||
|
||||
test("sanitizeProfileMediaUrl allows blob: URLs", () => {
|
||||
const blobUrl = "blob:http://example.com/550e8400-e29b-41d4-a716-446655440000";
|
||||
assert.equal(sanitizeProfileMediaUrl(blobUrl), blobUrl);
|
||||
});
|
||||
|
||||
test("sanitizeProfileMediaUrl rejects specific placeholder images", () => {
|
||||
assert.equal(sanitizeProfileMediaUrl("images/robot.png"), "");
|
||||
});
|
||||
|
||||
test("sanitizeProfileMediaUrl normalizes IPFS URLs", () => {
|
||||
assert.equal(sanitizeProfileMediaUrl("ipfs://CID"), "https://ipfs.io/ipfs/CID");
|
||||
assert.equal(sanitizeProfileMediaUrl("ipfs/CID"), "https://ipfs.io/ipfs/CID");
|
||||
});
|
||||
|
||||
test("sanitizeProfileMediaUrl handles protocol-relative URLs", () => {
|
||||
assert.equal(sanitizeProfileMediaUrl("//example.com/image.jpg"), "https://example.com/image.jpg");
|
||||
});
|
||||
|
||||
test("sanitizeProfileMediaUrl allows relative paths", () => {
|
||||
assert.equal(sanitizeProfileMediaUrl("/assets/image.jpg"), "/assets/image.jpg");
|
||||
assert.equal(sanitizeProfileMediaUrl("./image.jpg"), "./image.jpg");
|
||||
assert.equal(sanitizeProfileMediaUrl("../image.jpg"), "../image.jpg");
|
||||
assert.equal(sanitizeProfileMediaUrl("assets/image.jpg"), "assets/image.jpg");
|
||||
});
|
||||
|
||||
test("sanitizeProfileMediaUrl adds protocol to domains and localhost", () => {
|
||||
assert.equal(sanitizeProfileMediaUrl("localhost:8080/image.jpg"), "http://localhost:8080/image.jpg");
|
||||
assert.equal(sanitizeProfileMediaUrl("example.com/image.jpg"), "https://example.com/image.jpg");
|
||||
assert.equal(sanitizeProfileMediaUrl("sub.example.com/image.jpg"), "https://sub.example.com/image.jpg");
|
||||
// IP address pattern in code: (?:\d{1,3}\.){3}\d{1,3}
|
||||
assert.equal(sanitizeProfileMediaUrl("127.0.0.1:8080/image.jpg"), "http://127.0.0.1:8080/image.jpg");
|
||||
});
|
||||
|
||||
test("sanitizeProfileMediaUrl coerces http to https except for localhost", () => {
|
||||
assert.equal(sanitizeProfileMediaUrl("http://example.com/image.jpg"), "https://example.com/image.jpg");
|
||||
assert.equal(sanitizeProfileMediaUrl("http://localhost:8080/image.jpg"), "http://localhost:8080/image.jpg");
|
||||
assert.equal(sanitizeProfileMediaUrl("http://127.0.0.1:8080/image.jpg"), "http://127.0.0.1:8080/image.jpg");
|
||||
});
|
||||
|
||||
test("sanitizeProfileMediaUrl rejects unsupported patterns", () => {
|
||||
// The code logs a warning and returns "" for things that don't match localhost or domain patterns
|
||||
// if they don't start with http/https
|
||||
assert.equal(sanitizeProfileMediaUrl("javascript:alert(1)"), "");
|
||||
assert.equal(sanitizeProfileMediaUrl("ftp://example.com/file"), ""); // Starts with ftp, not http/https/ipfs
|
||||
});
|
||||
@@ -3,13 +3,22 @@ import { test } from "node:test";
|
||||
|
||||
import { validateInstanceConfig } from "../config/validate-config.js";
|
||||
|
||||
// Common valid overrides to use as a base
|
||||
const VALID_OVERRIDES = {
|
||||
ADMIN_SUPER_NPUB: "npub1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq",
|
||||
PLATFORM_FEE_PERCENT: 5,
|
||||
PLATFORM_LUD16_OVERRIDE: "[email protected]",
|
||||
};
|
||||
|
||||
test("validateInstanceConfig succeeds with valid configuration", () => {
|
||||
assert.doesNotThrow(() => validateInstanceConfig(VALID_OVERRIDES));
|
||||
});
|
||||
|
||||
test(
|
||||
"validateInstanceConfig throws when platform fee is positive without a platform override",
|
||||
() => {
|
||||
const overrides = {
|
||||
ADMIN_SUPER_NPUB:
|
||||
"npub1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq",
|
||||
PLATFORM_FEE_PERCENT: 5,
|
||||
...VALID_OVERRIDES,
|
||||
PLATFORM_LUD16_OVERRIDE: " ",
|
||||
};
|
||||
|
||||
@@ -19,3 +28,108 @@ test(
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
test("validateInstanceConfig throws when ADMIN_SUPER_NPUB is empty", () => {
|
||||
const overrides = {
|
||||
...VALID_OVERRIDES,
|
||||
ADMIN_SUPER_NPUB: " ",
|
||||
};
|
||||
|
||||
assert.throws(() => validateInstanceConfig(overrides), {
|
||||
message: /ADMIN_SUPER_NPUB is required/,
|
||||
});
|
||||
});
|
||||
|
||||
test("validateInstanceConfig throws when ADMIN_SUPER_NPUB does not start with npub", () => {
|
||||
const overrides = {
|
||||
...VALID_OVERRIDES,
|
||||
ADMIN_SUPER_NPUB: "invalid_npub",
|
||||
};
|
||||
|
||||
assert.throws(() => validateInstanceConfig(overrides), {
|
||||
message: /ADMIN_SUPER_NPUB should be a bech32 npub/,
|
||||
});
|
||||
});
|
||||
|
||||
test("validateInstanceConfig throws when PLATFORM_FEE_PERCENT is negative", () => {
|
||||
const overrides = {
|
||||
...VALID_OVERRIDES,
|
||||
PLATFORM_FEE_PERCENT: -1,
|
||||
};
|
||||
|
||||
assert.throws(() => validateInstanceConfig(overrides), {
|
||||
message: /PLATFORM_FEE_PERCENT must be between 0 and 100 inclusive/,
|
||||
});
|
||||
});
|
||||
|
||||
test("validateInstanceConfig throws when PLATFORM_FEE_PERCENT is greater than 100", () => {
|
||||
const overrides = {
|
||||
...VALID_OVERRIDES,
|
||||
PLATFORM_FEE_PERCENT: 101,
|
||||
};
|
||||
|
||||
assert.throws(() => validateInstanceConfig(overrides), {
|
||||
message: /PLATFORM_FEE_PERCENT must be between 0 and 100 inclusive/,
|
||||
});
|
||||
});
|
||||
|
||||
test("validateInstanceConfig throws when PLATFORM_FEE_PERCENT is not finite", () => {
|
||||
const overrides = {
|
||||
...VALID_OVERRIDES,
|
||||
PLATFORM_FEE_PERCENT: Infinity,
|
||||
};
|
||||
|
||||
assert.throws(() => validateInstanceConfig(overrides), {
|
||||
message: /PLATFORM_FEE_PERCENT must be a finite number/,
|
||||
});
|
||||
});
|
||||
|
||||
test("validateInstanceConfig throws when THEME_ACCENT_OVERRIDES has invalid hex color", () => {
|
||||
const overrides = {
|
||||
...VALID_OVERRIDES,
|
||||
THEME_ACCENT_OVERRIDES: {
|
||||
light: {
|
||||
accent: "invalid-hex",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
assert.throws(() => validateInstanceConfig(overrides), {
|
||||
message: /must be a #RRGGBB hex color/,
|
||||
});
|
||||
});
|
||||
|
||||
test("validateInstanceConfig throws when THEME_ACCENT_OVERRIDES structure is invalid", () => {
|
||||
const overrides = {
|
||||
...VALID_OVERRIDES,
|
||||
THEME_ACCENT_OVERRIDES: {
|
||||
light: "not-an-object",
|
||||
},
|
||||
};
|
||||
|
||||
assert.throws(() => validateInstanceConfig(overrides), {
|
||||
message: /must be an object with accent overrides/,
|
||||
});
|
||||
});
|
||||
|
||||
test("validateInstanceConfig throws when optional URL is invalid", () => {
|
||||
const overrides = {
|
||||
...VALID_OVERRIDES,
|
||||
BLOG_URL: "not-a-valid-url",
|
||||
};
|
||||
|
||||
assert.throws(() => validateInstanceConfig(overrides), {
|
||||
message: /BLOG_URL must be a valid http\(s\) URL/,
|
||||
});
|
||||
});
|
||||
|
||||
test("validateInstanceConfig throws when optional URL has invalid protocol", () => {
|
||||
const overrides = {
|
||||
...VALID_OVERRIDES,
|
||||
BLOG_URL: "ftp://example.com",
|
||||
};
|
||||
|
||||
assert.throws(() => validateInstanceConfig(overrides), {
|
||||
message: /BLOG_URL must be a valid http\(s\) URL/,
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user