Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2566fb8f20 | |||
| 4868f18dfd | |||
| 37e42b3104 | |||
| d617c69571 | |||
| 76d9850464 | |||
| 96dbb8c41d | |||
| 636fa713e1 | |||
| 6120b96c7c | |||
| eb92f99c4a | |||
| 587fd4ec95 | |||
| 8cf72d926d | |||
| 8721f0b63c | |||
| 027e012a58 | |||
| b3db206588 | |||
| 6538406db2 | |||
| e2eacbc9fe | |||
| e639cc82d1 | |||
| f2931d7be2 | |||
| d4a4ddce37 | |||
| bd384bdf5c | |||
| 411c42b2c4 | |||
| bf97849324 | |||
| 7181d41b24 | |||
| 4e9c4c5e08 | |||
| 16c959434b | |||
| 23484dc90a | |||
| 6a81a52a50 | |||
| 5a4b9a98bd | |||
| f7f88156e1 | |||
| 8af5a49d14 |
@@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"mcpServers": {
|
|
||||||
"gitea": {
|
|
||||||
"type": "http",
|
|
||||||
"url": "https://git-mcp.farh.net/mcp",
|
|
||||||
"headers": {
|
|
||||||
"Authorization": "Bearer ${GITEA_TOKEN}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+1
-1
@@ -333,7 +333,7 @@ This means:
|
|||||||
| # | Scenario | Steps | Expected |
|
| # | Scenario | Steps | Expected |
|
||||||
|---|----------|-------|----------|
|
|---|----------|-------|----------|
|
||||||
| TC-API-13.1 | Get business settings | GET /api/admin/settings | 200 OK, business settings returned. Response body **must NOT include `googleMapsApiKey`** — the encrypted secret is redacted from the projection (GRO-2294, defense-in-depth); non-secret fields (`businessName`, colors, `routeOptimizationProvider`, etc.) are still present |
|
| TC-API-13.1 | Get business settings | GET /api/admin/settings | 200 OK, business settings returned. Response body **must NOT include `googleMapsApiKey`** — the encrypted secret is redacted from the projection (GRO-2294, defense-in-depth); non-secret fields (`businessName`, colors, `routeOptimizationProvider`, etc.) are still present |
|
||||||
| TC-API-13.2 | Update business settings | PATCH /api/admin/settings with updated values | 200 OK, settings updated. Response body **must NOT include `googleMapsApiKey`** — the encrypted secret is redacted from the PATCH response symmetrically with the GET projection (GRO-2299, defense-in-depth); non-secret updated fields are still returned |
|
| TC-API-13.2 | Update business settings | PATCH /api/admin/settings with updated values | 200 OK, settings updated |
|
||||||
| TC-API-13.3 | Upload logo | POST /api/admin/settings/logo/upload with file | 200 OK, logo uploaded and stored |
|
| TC-API-13.3 | Upload logo | POST /api/admin/settings/logo/upload with file | 200 OK, logo uploaded and stored |
|
||||||
| TC-API-13.4 | View logo | GET /api/admin/settings/logo | 200 OK, logo image returned |
|
| TC-API-13.4 | View logo | GET /api/admin/settings/logo | 200 OK, logo image returned |
|
||||||
| TC-API-13.5 | Delete logo | DELETE /api/admin/settings/logo | 200 OK, logo removed |
|
| TC-API-13.5 | Delete logo | DELETE /api/admin/settings/logo | 200 OK, logo removed |
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import { Hono } from "hono";
|
|||||||
|
|
||||||
let selectRows: Record<string, unknown>[] = [];
|
let selectRows: Record<string, unknown>[] = [];
|
||||||
let insertReturning: Record<string, unknown>[] = [];
|
let insertReturning: Record<string, unknown>[] = [];
|
||||||
let updateReturning: Record<string, unknown>[] = [];
|
|
||||||
|
|
||||||
function makeChainable(data: unknown[]): unknown {
|
function makeChainable(data: unknown[]): unknown {
|
||||||
const arr = [...data];
|
const arr = [...data];
|
||||||
@@ -34,9 +33,6 @@ vi.mock("@groombook/db", () => {
|
|||||||
insert: () => ({
|
insert: () => ({
|
||||||
values: () => ({ returning: () => insertReturning }),
|
values: () => ({ returning: () => insertReturning }),
|
||||||
}),
|
}),
|
||||||
update: () => ({
|
|
||||||
set: () => ({ where: () => ({ returning: () => updateReturning }) }),
|
|
||||||
}),
|
|
||||||
}),
|
}),
|
||||||
businessSettings,
|
businessSettings,
|
||||||
eq: vi.fn(),
|
eq: vi.fn(),
|
||||||
@@ -55,17 +51,6 @@ const { settingsRouter } = await import("../routes/settings.js");
|
|||||||
const app = new Hono();
|
const app = new Hono();
|
||||||
app.route("/settings", settingsRouter);
|
app.route("/settings", settingsRouter);
|
||||||
|
|
||||||
// PATCH /settings is guarded by requireSuperUser(), which reads the staff record
|
|
||||||
// from context. Inject a super-user staff row so the handler runs.
|
|
||||||
const patchApp = new Hono<{
|
|
||||||
Variables: { staff: { id: string; isSuperUser: boolean } };
|
|
||||||
}>();
|
|
||||||
patchApp.use("*", async (c, next) => {
|
|
||||||
c.set("staff", { id: "staff-1", isSuperUser: true });
|
|
||||||
await next();
|
|
||||||
});
|
|
||||||
patchApp.route("/settings", settingsRouter);
|
|
||||||
|
|
||||||
const FULL_ROW = {
|
const FULL_ROW = {
|
||||||
id: "settings-uuid-1",
|
id: "settings-uuid-1",
|
||||||
businessName: "GroomBook",
|
businessName: "GroomBook",
|
||||||
@@ -104,42 +89,3 @@ describe("GET /settings — googleMapsApiKey redaction (GRO-2294)", () => {
|
|||||||
expect(body.id).toBe("settings-uuid-new");
|
expect(body.id).toBe("settings-uuid-new");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("PATCH /settings — googleMapsApiKey redaction (GRO-2299)", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
selectRows = [];
|
|
||||||
insertReturning = [];
|
|
||||||
updateReturning = [];
|
|
||||||
});
|
|
||||||
|
|
||||||
function patchRequest(body: Record<string, unknown>) {
|
|
||||||
return patchApp.request("/settings", {
|
|
||||||
method: "PATCH",
|
|
||||||
headers: { "content-type": "application/json" },
|
|
||||||
body: JSON.stringify(body),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
it("omits googleMapsApiKey from the PATCH response", async () => {
|
|
||||||
selectRows = [{ ...FULL_ROW }];
|
|
||||||
updateReturning = [{ ...FULL_ROW, businessName: "Updated Name" }];
|
|
||||||
const res = await patchRequest({ businessName: "Updated Name" });
|
|
||||||
expect(res.status).toBe(200);
|
|
||||||
const body = (await res.json()) as Record<string, unknown>;
|
|
||||||
expect(body).not.toHaveProperty("googleMapsApiKey");
|
|
||||||
// Non-secret updated fields are still returned.
|
|
||||||
expect(body.businessName).toBe("Updated Name");
|
|
||||||
expect(body.routeOptimizationProvider).toBe("google");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("omits googleMapsApiKey on the auto-create-then-update branch", async () => {
|
|
||||||
selectRows = [];
|
|
||||||
insertReturning = [{ ...FULL_ROW, id: "settings-uuid-new" }];
|
|
||||||
updateReturning = [{ ...FULL_ROW, id: "settings-uuid-new" }];
|
|
||||||
const res = await patchRequest({ primaryColor: "#123456" });
|
|
||||||
expect(res.status).toBe(200);
|
|
||||||
const body = (await res.json()) as Record<string, unknown>;
|
|
||||||
expect(body).not.toHaveProperty("googleMapsApiKey");
|
|
||||||
expect(body.id).toBe("settings-uuid-new");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -65,8 +65,7 @@ settingsRouter.patch(
|
|||||||
.where(eq(businessSettings.id, settingsId))
|
.where(eq(businessSettings.id, settingsId))
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
if (!updated) throw new Error("Failed to update settings");
|
return c.json(updated);
|
||||||
return c.json(redactSettings(updated));
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user