mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-21 10:15:04 +02:00
fix(inventory): malformed rgba no longer bricks the Filaments page (#1055)
A single legacy spool with a 7-char rgba ('FFFFFFF', missing one F)
caused GET /api/v1/inventory/spools to 500 with a pydantic
ResponseValidationError, leaving the reporter with a blank Filaments
page and "Add Spool" silently failing. Root cause spans three layers:
1. Write path: SpoolUpdate.rgba had no pattern constraint (only
SpoolCreate did), so PATCH could plant malformed values in the DB.
2. Frontend: ColorSection hex input's `val.length <= 6 ? 'FF' : ''`
emitted 7-char rgba for 5-char input (XXXXX + FF = 7) and for
7-char typed input (no alpha appended).
3. Read path: SpoolResponse inherited the write-side pattern, so a
single bad row 500'd the entire list endpoint instead of being
tolerated through serialize.
SpoolUpdate.rgba now carries the same ^[0-9A-Fa-f]{8}$ pattern as
SpoolCreate. The hex input emits a fully-formed 8-char RRGGBBAA on
every keystroke — 8-char paste passes through, 7-char drops the
stray, shorter input pads RGB with '0' and appends FF alpha.
SpoolResponse.rgba is now Optional[str] with no pattern — write-side
validation is the right place for format rules; responses must
tolerate historical rows.
Tests: 16 schema tests (SpoolCreate/Update reject, SpoolResponse
tolerate), 7 frontend tests covering every input length 0–8 plus
non-hex strip. A user who already has a bad row in their DB now sees
it render with a default color instead of having to hand-edit SQLite.
This commit is contained in:
@@ -348,6 +348,73 @@ describe('SpoolFormModal weightTouched', () => {
|
||||
expect(payload).toHaveProperty('cost_per_kg', null);
|
||||
});
|
||||
|
||||
it('normalizes a malformed legacy rgba on edit-form load so PATCH is not rejected (#1055)', async () => {
|
||||
// #1055 regression guard: a spool with a legacy 7-char rgba (e.g. 'FFFFFFF')
|
||||
// was editable in the UI but any save 422'd because SpoolUpdate now enforces
|
||||
// the 8-char pattern. The form must sanitize the loaded value to a valid
|
||||
// default so users can edit unrelated fields without being forced to fix
|
||||
// a color they may not even have noticed was broken.
|
||||
const spoolWithBadRgba: InventorySpool = {
|
||||
...existingSpool,
|
||||
rgba: 'FFFFFFF', // 7 chars — the exact #1055 trigger pattern
|
||||
};
|
||||
|
||||
render(
|
||||
<SpoolFormModal
|
||||
isOpen={true}
|
||||
onClose={vi.fn()}
|
||||
spool={spoolWithBadRgba}
|
||||
currencySymbol="$"
|
||||
/>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Edit Spool')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const saveButton = screen.getByRole('button', { name: /save/i });
|
||||
fireEvent.click(saveButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(api.updateSpool).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
const [, payload] = vi.mocked(api.updateSpool).mock.calls[0];
|
||||
// The PATCH payload must carry a valid 8-char rgba — never the raw 7-char
|
||||
// value loaded from the stale DB row.
|
||||
expect(payload).toHaveProperty('rgba');
|
||||
expect(typeof (payload as { rgba: unknown }).rgba).toBe('string');
|
||||
expect((payload as { rgba: string }).rgba).toMatch(/^[0-9A-Fa-f]{8}$/);
|
||||
});
|
||||
|
||||
it('preserves a valid existing rgba on edit (no forced default)', async () => {
|
||||
// Sanity: the normalization only kicks in for malformed values. A valid
|
||||
// 8-char rgba must round-trip untouched so untouched edits don't quietly
|
||||
// reset a user's chosen color.
|
||||
render(
|
||||
<SpoolFormModal
|
||||
isOpen={true}
|
||||
onClose={vi.fn()}
|
||||
spool={existingSpool} // rgba = 'FF0000FF' (valid)
|
||||
currencySymbol="$"
|
||||
/>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Edit Spool')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const saveButton = screen.getByRole('button', { name: /save/i });
|
||||
fireEvent.click(saveButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(api.updateSpool).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
const [, payload] = vi.mocked(api.updateSpool).mock.calls[0];
|
||||
expect((payload as { rgba: string }).rgba).toBe('FF0000FF');
|
||||
});
|
||||
|
||||
it('displays correct catalog name when duplicates exist', async () => {
|
||||
const spoolWithCatalogId: InventorySpool = {
|
||||
...existingSpool,
|
||||
|
||||
Reference in New Issue
Block a user