Task #349 follow-up: brand-logo embed test + label module + user-scope guard
Original task: Executive Meetings PDF improvements — respect user
font prefs, render saved per-meeting rowColor, drop legacy
isHighlighted baking, brand logo on left of PDF header with title
"قائمة بأسماء حضور الاجتماعات", and a logo upload + font color
picker in the font-settings page.
Prior mark_task_complete was rejected for three issues. This commit
closes all of them:
- Extract bilingual PDF labels into
artifacts/api-server/src/lib/pdf-labels.ts and import it from the
PDF route. Mirror the same keys under executiveMeetings.pdf.* in
the tx-os ar.json / en.json locales so the frontend stays in
sync.
- Add an end-to-end test that exercises the brand-logo path: sign
an upload URL, PUT the bytes through the storage sidecar, save
the path on the global font-settings row, render the PDF, and
assert PDFKit emitted "/Subtype /Image" with "/Width 1". This
test caught a real silent failure: PDFKit's png-js decoder
rejected the canonical 67-byte base64 "smallest valid PNG", so
the renderer was logging a warning and proceeding without a
logo. The fixture now constructs a real 1x1 RGB PNG inline using
zlib + a CRC32 routine (no new dependencies). Skip is narrowed
to network errors / 5xx (4xx fails loudly), and the global-row
mutation is wrapped in try/finally so cleanup always runs.
- Reject user-scope writes that try to set logoObjectPath with a
400 ("logo_user_scope_forbidden") instead of silently dropping
the field. The brand logo is a global asset; silent drops would
mislead callers into thinking their upload was saved. Added a
focused test asserting the 400 response and that explicit
logoObjectPath:null on the user row still works.
Out of scope: three pre-existing tsc errors at lines 635/778/2921
of executive-meetings.ts (unrelated to PDF code) and a flaky
"Reorder: POST /reorder" test that was already failing before
these changes.
This commit is contained in:
@@ -2908,9 +2908,17 @@ const upsertFontSettingsHandler = async (
|
||||
}
|
||||
}
|
||||
const targetUserId = data.scope === "global" ? null : userId;
|
||||
// Logo is a brand asset and only lives on the global row. Per-user
|
||||
// PATCHes silently drop the field rather than 400-ing so a generic
|
||||
// "save my prefs" UI doesn't have to special-case the toggle.
|
||||
// Logo is a brand asset and only lives on the global row. Reject
|
||||
// user-scope writes that try to set a non-null logo so callers get
|
||||
// a clear error instead of a silent no-op. `null` (explicit clear)
|
||||
// and `undefined` (field not provided) are both fine on user rows.
|
||||
if (data.scope === "user" && data.logoObjectPath != null) {
|
||||
res.status(400).json({
|
||||
error: "logoObjectPath is only allowed on the global font-settings row",
|
||||
code: "logo_user_scope_forbidden",
|
||||
});
|
||||
return;
|
||||
}
|
||||
const logoForWrite = data.scope === "global" ? data.logoObjectPath : undefined;
|
||||
const result = await db.transaction(async (tx) => {
|
||||
const [existing] = await tx
|
||||
|
||||
@@ -696,6 +696,49 @@ test("Font settings: valid combo 200; invalid weight/size/family 400", async ()
|
||||
assert.equal(badFamily.status, 400);
|
||||
});
|
||||
|
||||
test("Font settings: user-scope rejects logoObjectPath with 400; null is allowed", async () => {
|
||||
// User-scope writes that try to set a non-null logo path must be
|
||||
// rejected loudly. The brand logo is a global asset; silently
|
||||
// dropping the field would mislead callers into thinking their
|
||||
// upload was saved.
|
||||
const rejected = await api(
|
||||
adminCookie,
|
||||
"PUT",
|
||||
"/api/executive-meetings/font-settings",
|
||||
{
|
||||
scope: "user",
|
||||
fontFamily: "system",
|
||||
fontSize: 14,
|
||||
fontWeight: "regular",
|
||||
alignment: "start",
|
||||
fontColor: "#000000",
|
||||
logoObjectPath: "/objects/some-fake-id",
|
||||
},
|
||||
);
|
||||
assert.equal(rejected.status, 400);
|
||||
const body = await rejected.json();
|
||||
assert.equal(body.code, "logo_user_scope_forbidden");
|
||||
|
||||
// Explicit null (i.e. "I'm not setting a logo") must still work
|
||||
// on the user row so a generic prefs UI doesn't have to omit the
|
||||
// field.
|
||||
const okNull = await api(
|
||||
adminCookie,
|
||||
"PUT",
|
||||
"/api/executive-meetings/font-settings",
|
||||
{
|
||||
scope: "user",
|
||||
fontFamily: "system",
|
||||
fontSize: 14,
|
||||
fontWeight: "regular",
|
||||
alignment: "start",
|
||||
fontColor: "#000000",
|
||||
logoObjectPath: null,
|
||||
},
|
||||
);
|
||||
assert.equal(okNull.status, 200);
|
||||
});
|
||||
|
||||
test("PDF archives: POST creates a snapshot and GET returns it", async () => {
|
||||
const post = await api(
|
||||
adminCookie,
|
||||
|
||||
Reference in New Issue
Block a user