From 902386b32da8809f618f3fb8dabba0ec4bb53fb8 Mon Sep 17 00:00:00 2001 From: ecency Date: Tue, 7 Jul 2026 10:29:39 +0000 Subject: [PATCH 1/2] test: map axios to its CJS build so jest can load the suite --- package.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/package.json b/package.json index 8cbc8fd2..ad36e3bc 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,9 @@ "jest": "^26.0.0" }, "jest": { + "moduleNameMapper": { + "^axios$": "axios/dist/node/axios.cjs" + }, "transform": { "\\.(ts|tsx)$": "ts-jest", "\\.css$": "/node_modules/razzle/config/jest/cssTransform.js", From 6789aed53e122926474bb187e8d53aa5ddfc46de Mon Sep 17 00:00:00 2001 From: ecency Date: Tue, 7 Jul 2026 10:29:39 +0000 Subject: [PATCH 2/2] feat: add support-settings private-api endpoints --- src/server/handlers/private-api.test.ts | 46 ++++++++++++++++++++- src/server/handlers/private-api.ts | 53 +++++++++++++++++++++++++ src/server/index.tsx | 2 + 3 files changed, 100 insertions(+), 1 deletion(-) diff --git a/src/server/handlers/private-api.test.ts b/src/server/handlers/private-api.test.ts index ed59630a..bd48ae4a 100644 --- a/src/server/handlers/private-api.test.ts +++ b/src/server/handlers/private-api.test.ts @@ -1,4 +1,4 @@ -import { signupClientIp } from "./private-api"; +import { parseSupportSettingsPayload, signupClientIp } from "./private-api"; // Minimal express.Request stand-in: signupClientIp only reads `headers`. const reqWith = (headers: Record): any => ({ headers }); @@ -28,3 +28,47 @@ describe("signupClientIp", () => { ); }); }); + +describe("parseSupportSettingsPayload", () => { + it("accepts integers within 0..100", () => { + expect(parseSupportSettingsPayload({ beneficiary_percent: 5, curation_percent: 10 })).toEqual({ + beneficiary_percent: 5, + curation_percent: 10 + }); + }); + + it("accepts the 0 and 100 boundaries", () => { + expect(parseSupportSettingsPayload({ beneficiary_percent: 0, curation_percent: 100 })).toEqual({ + beneficiary_percent: 0, + curation_percent: 100 + }); + }); + + it("rejects values out of range", () => { + expect(parseSupportSettingsPayload({ beneficiary_percent: 101, curation_percent: 10 })).toBeNull(); + expect(parseSupportSettingsPayload({ beneficiary_percent: 5, curation_percent: -1 })).toBeNull(); + }); + + it("rejects floats", () => { + expect(parseSupportSettingsPayload({ beneficiary_percent: 5.5, curation_percent: 10 })).toBeNull(); + expect(parseSupportSettingsPayload({ beneficiary_percent: 5, curation_percent: 0.1 })).toBeNull(); + }); + + it("rejects strings", () => { + expect(parseSupportSettingsPayload({ beneficiary_percent: "5", curation_percent: 10 })).toBeNull(); + expect(parseSupportSettingsPayload({ beneficiary_percent: 5, curation_percent: "10" })).toBeNull(); + }); + + it("rejects booleans", () => { + expect(parseSupportSettingsPayload({ beneficiary_percent: true, curation_percent: 10 })).toBeNull(); + expect(parseSupportSettingsPayload({ beneficiary_percent: 5, curation_percent: false })).toBeNull(); + }); + + it("rejects missing fields and bodies", () => { + expect(parseSupportSettingsPayload({})).toBeNull(); + expect(parseSupportSettingsPayload({ beneficiary_percent: 5 })).toBeNull(); + expect(parseSupportSettingsPayload({ curation_percent: 5 })).toBeNull(); + expect(parseSupportSettingsPayload(undefined)).toBeNull(); + expect(parseSupportSettingsPayload(null)).toBeNull(); + }); +}); diff --git a/src/server/handlers/private-api.ts b/src/server/handlers/private-api.ts index eca0a0e1..2b681e59 100644 --- a/src/server/handlers/private-api.ts +++ b/src/server/handlers/private-api.ts @@ -1202,6 +1202,59 @@ export const bookmarksDelete = async (req: express.Request, res: express.Respons pipe(apiRequest(`bookmarks/${username}/${id}`, "DELETE"), res); } +/** + * Parse and validate the support-settings payload. Both fields must be integers + * within 0..100 (booleans, strings and floats are rejected). Returns the parsed + * pair, or null when the payload is invalid. Exported for unit tests. + */ +export const parseSupportSettingsPayload = ( + body: unknown +): { beneficiary_percent: number; curation_percent: number } | null => { + const { beneficiary_percent, curation_percent } = (body || {}) as { + beneficiary_percent?: unknown; + curation_percent?: unknown; + }; + const isPercent = (v: unknown): v is number => + typeof v === "number" && Number.isInteger(v) && v >= 0 && v <= 100; + if (!isPercent(beneficiary_percent) || !isPercent(curation_percent)) { + return null; + } + return { beneficiary_percent, curation_percent }; +}; + +// Support settings are per-user opt-ins, so the username is taken from the +// authenticated token (validateCode), never from the request body. +export const supportSettings = async (req: express.Request, res: express.Response) => { + const username = await requireAuthedUsername(req, res); + if (!username) { + return; + } + pipe(apiRequest(`support-settings/${username}`, "GET"), res); +}; + +export const supportSettingsUpdate = async (req: express.Request, res: express.Response) => { + const username = await requireAuthedUsername(req, res); + if (!username) { + return; + } + const parsed = parseSupportSettingsPayload(req.body); + if (!parsed) { + res.status(400).send( + "beneficiary_percent and curation_percent must be integers between 0 and 100" + ); + return; + } + const { beneficiary_percent, curation_percent } = parsed; + pipe( + apiRequest(`support-settings/${username}`, "PUT", {}, { + username, + beneficiary_percent, + curation_percent + }), + res + ); +}; + export const schedules = async (req: express.Request, res: express.Response) => { const username = await validateCode(req); if (!username) { diff --git a/src/server/index.tsx b/src/server/index.tsx index a591ebff..dfe26f07 100644 --- a/src/server/index.tsx +++ b/src/server/index.tsx @@ -113,6 +113,8 @@ server .post("^/private-api/bookmarks$", privateApi.bookmarks) .post("^/private-api/bookmarks-add$", privateApi.bookmarksAdd) .post("^/private-api/bookmarks-delete$", privateApi.bookmarksDelete) + .post("^/private-api/support-settings$", privateApi.supportSettings) + .post("^/private-api/support-settings-update$", privateApi.supportSettingsUpdate) .post("^/private-api/schedules$", privateApi.schedules) .post("^/private-api/schedules-add$", privateApi.schedulesAdd) .post("^/private-api/schedules-delete$", privateApi.schedulesDelete)