From 226f0427bc4dd54e7b5d0a850e747ac7063f9caf Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 20 Apr 2026 21:20:48 +0100 Subject: [PATCH] test: share nostr admin scope assertions --- .../nostr/src/nostr-profile-http.test.ts | 128 ++++++++---------- 1 file changed, 58 insertions(+), 70 deletions(-) diff --git a/extensions/nostr/src/nostr-profile-http.test.ts b/extensions/nostr/src/nostr-profile-http.test.ts index 8b58079aec1..9724158dd42 100644 --- a/extensions/nostr/src/nostr-profile-http.test.ts +++ b/extensions/nostr/src/nostr-profile-http.test.ts @@ -107,16 +107,21 @@ function createMockRequest( return req; } -function createMockResponse(): ServerResponse & { +type MockResponse = { _getData: () => string; _getStatusCode: () => number; -} { + write: (chunk: unknown) => boolean; + end: (chunk?: unknown) => MockResponse; + statusCode: number; +}; + +function createMockResponse(): MockResponse { let data = ""; let statusCode = 200; const res = Object.assign(new ServerResponse({} as IncomingMessage), { _getData: () => data, _getStatusCode: () => statusCode, - }); + }) as MockResponse; res.write = function (chunk: unknown) { data += responseChunkText(chunk); @@ -140,8 +145,6 @@ function createMockResponse(): ServerResponse & { return res; } -type MockResponse = ReturnType; - function createMockContext(overrides?: Partial): NostrProfileHttpContext { return { getConfigProfile: vi.fn().mockReturnValue(undefined), @@ -177,7 +180,7 @@ function createProfileHttpHarness( ctx, req, res, - run: () => handler(req, res), + run: () => handler(req, res as unknown as ServerResponse), }; } @@ -205,6 +208,27 @@ function mockSuccessfulProfileImport() { }); } +async function expectAdminScopeRejected(params: { + scopes: readonly string[] | undefined; + method: string; + url: string; + body: unknown; + expectOperationNotCalled: () => void; +}) { + setGatewayRuntimeScopes(params.scopes); + const { ctx, res, run } = createProfileHttpHarness(params.method, params.url, { + body: params.body, + }); + + await run(); + + expect(res._getStatusCode()).toBe(403); + const data = JSON.parse(res._getData()); + expect(data.error).toBe("missing scope: operator.admin"); + params.expectOperationNotCalled(); + expect(ctx.updateConfigProfile).not.toHaveBeenCalled(); +} + // ============================================================================ // Tests // ============================================================================ @@ -365,41 +389,23 @@ describe("nostr-profile-http", () => { }); it("rejects profile mutation when gateway caller is missing operator.admin", async () => { - setGatewayRuntimeScopes(["operator.read"]); - const { ctx, res, run } = createProfileHttpHarness( - "PUT", - "/api/channels/nostr/default/profile", - { - body: { name: "attacker" }, - }, - ); - - await run(); - - expect(res._getStatusCode()).toBe(403); - const data = JSON.parse(res._getData()); - expect(data.error).toBe("missing scope: operator.admin"); - expect(publishNostrProfile).not.toHaveBeenCalled(); - expect(ctx.updateConfigProfile).not.toHaveBeenCalled(); + await expectAdminScopeRejected({ + scopes: ["operator.read"], + method: "PUT", + url: "/api/channels/nostr/default/profile", + body: { name: "attacker" }, + expectOperationNotCalled: () => expect(publishNostrProfile).not.toHaveBeenCalled(), + }); }); it("rejects profile mutation when gateway scope context is missing", async () => { - setGatewayRuntimeScopes(undefined); - const { ctx, res, run } = createProfileHttpHarness( - "PUT", - "/api/channels/nostr/default/profile", - { - body: { name: "attacker" }, - }, - ); - - await run(); - - expect(res._getStatusCode()).toBe(403); - const data = JSON.parse(res._getData()); - expect(data.error).toBe("missing scope: operator.admin"); - expect(publishNostrProfile).not.toHaveBeenCalled(); - expect(ctx.updateConfigProfile).not.toHaveBeenCalled(); + await expectAdminScopeRejected({ + scopes: undefined, + method: "PUT", + url: "/api/channels/nostr/default/profile", + body: { name: "attacker" }, + expectOperationNotCalled: () => expect(publishNostrProfile).not.toHaveBeenCalled(), + }); }); it("rejects private IP in picture URL (SSRF protection)", async () => { @@ -564,41 +570,23 @@ describe("nostr-profile-http", () => { }); it("rejects profile import when gateway caller is missing operator.admin", async () => { - setGatewayRuntimeScopes(["operator.read"]); - const { ctx, res, run } = createProfileHttpHarness( - "POST", - "/api/channels/nostr/default/profile/import", - { - body: { autoMerge: true }, - }, - ); - - await run(); - - expect(res._getStatusCode()).toBe(403); - const data = JSON.parse(res._getData()); - expect(data.error).toBe("missing scope: operator.admin"); - expect(importProfileFromRelays).not.toHaveBeenCalled(); - expect(ctx.updateConfigProfile).not.toHaveBeenCalled(); + await expectAdminScopeRejected({ + scopes: ["operator.read"], + method: "POST", + url: "/api/channels/nostr/default/profile/import", + body: { autoMerge: true }, + expectOperationNotCalled: () => expect(importProfileFromRelays).not.toHaveBeenCalled(), + }); }); it("rejects profile import when gateway scope context is missing", async () => { - setGatewayRuntimeScopes(undefined); - const { ctx, res, run } = createProfileHttpHarness( - "POST", - "/api/channels/nostr/default/profile/import", - { - body: { autoMerge: true }, - }, - ); - - await run(); - - expect(res._getStatusCode()).toBe(403); - const data = JSON.parse(res._getData()); - expect(data.error).toBe("missing scope: operator.admin"); - expect(importProfileFromRelays).not.toHaveBeenCalled(); - expect(ctx.updateConfigProfile).not.toHaveBeenCalled(); + await expectAdminScopeRejected({ + scopes: undefined, + method: "POST", + url: "/api/channels/nostr/default/profile/import", + body: { autoMerge: true }, + expectOperationNotCalled: () => expect(importProfileFromRelays).not.toHaveBeenCalled(), + }); }); it("auto-merges when requested", async () => {