diff --git a/backend/internal/db/postgres/tag_repo.go b/backend/internal/db/postgres/tag_repo.go index 232c8ba..329db6e 100644 --- a/backend/internal/db/postgres/tag_repo.go +++ b/backend/internal/db/postgres/tag_repo.go @@ -272,7 +272,7 @@ func (r *TagRepo) Create(ctx context.Context, t *domain.Tag) (*domain.Tag, error const query = ` WITH ins AS ( INSERT INTO data.tags (name, notes, color, category_id, metadata, creator_id, is_public) - VALUES ($1, $2, $3, $4, $5, $6, $7) + VALUES ($1, $2, NULLIF($3, ''), $4, $5, $6, $7) RETURNING * ) SELECT @@ -321,7 +321,7 @@ WITH upd AS ( UPDATE data.tags SET name = $2, notes = $3, - color = $4, + color = NULLIF($4, ''), category_id = $5, metadata = COALESCE($6, metadata), is_public = $7 diff --git a/backend/internal/integration/server_test.go b/backend/internal/integration/server_test.go index a5819f6..fded83f 100644 --- a/backend/internal/integration/server_test.go +++ b/backend/internal/integration/server_test.go @@ -891,6 +891,41 @@ func TestRecordPoolView(t *testing.T) { require.Equal(t, http.StatusNotFound, resp.StatusCode, resp.String()) } +// TestTagColorOptional verifies a tag can be created without a colour (stored as +// NULL rather than the colour input's default) and that an existing colour can +// be cleared back to none. +func TestTagColorOptional(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test in short mode") + } + + h := setupSuite(t) + adminToken := h.login("admin", "admin") + + // Created without a colour → color is null. + resp := h.doJSON("POST", "/tags", map[string]any{"name": "plain"}, adminToken) + require.Equal(t, http.StatusCreated, resp.StatusCode, resp.String()) + var plain map[string]any + resp.decode(t, &plain) + assert.Nil(t, plain["color"], "tag created without a colour should have null color") + + // Created with a colour → kept verbatim. + resp = h.doJSON("POST", "/tags", map[string]any{"name": "red", "color": "aabbcc"}, adminToken) + require.Equal(t, http.StatusCreated, resp.StatusCode, resp.String()) + var red map[string]any + resp.decode(t, &red) + assert.Equal(t, "aabbcc", red["color"]) + redID := red["id"].(string) + + // Clearing the colour (color: null) must store NULL — an empty string would + // violate the hex CHECK constraint and fail the update. + resp = h.doJSON("PATCH", "/tags/"+redID, map[string]any{"color": nil}, adminToken) + require.Equal(t, http.StatusOK, resp.StatusCode, resp.String()) + var cleared map[string]any + resp.decode(t, &cleared) + assert.Nil(t, cleared["color"], "cleared colour should be null") +} + // TestBulkTagAutoRule verifies the bulk add path also applies then_tags. func TestBulkTagAutoRule(t *testing.T) { if testing.Short() {