012 turned three descriptive columns into identifiers other systems keep: in agent.json on a shop counter, in a saved URL, in a scheduled report. All three were already treated as stable and none of it was enforced. - clients.slug is an MQTT topic segment the broker ACL is written against. Rename one and that tenant's whole estate is silently refused by the broker, with no way to tell the agents. - sites.slug is what a shop PC calls itself - agent.json holds "site_id": "chennai", never the uuid. A rename orphans the PC from the shop it is standing in. - site_cameras.camera_id lands in visits.camera_id, which is text and not a foreign key. A rename orphans every visit already attributed to the old name: the footfall is still there and no longer joins to a camera. This was half-enforced in handleUpdateCamera and nowhere else, which is the shape of a rule that holds until somebody adds a second write path. - visitors.number is assigned once from the tenant's counter and read back as V-42. A trigger, not a CHECK: a CHECK cannot see the old row and the rule is about the transition. The DISPLAY name is deliberately not frozen - "TeNext Chennai", "Front door" - it is what a person reads, nothing keys on it, and a system that cannot fix a typo in a shop's name has confused the two. Also records why the uuid stays where a slug would do. The length was never the problem; needing it was, and that is fixed. Replacing it would touch eight foreign keys on a live database to shorten a field clients are already told not to use, and a sequential id would make any future tenancy hole walkable by counting. It is NOT because ids must be minted offline - sites, visitors and visits are all created server-side with a database in hand, and claiming otherwise would defend the status quo rather than explain it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HViLj9gYNRtSr7YVZmW5sn
58 lines
2.1 KiB
Go
58 lines
2.1 KiB
Go
package store
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// A reference clients are told to use must not be able to change underneath
|
|
// them.
|
|
//
|
|
// 012 turned three descriptive columns into IDENTIFIERS other people store: in
|
|
// agent.json on a shop counter, in a saved URL, in a scheduled report. All
|
|
// three were already treated as stable and none of it was enforced - the
|
|
// camera case was half-enforced in one handler and nowhere else, which is the
|
|
// shape of a rule that holds until somebody adds a second write path.
|
|
//
|
|
// Only a real database can test this: the rule is a trigger, and an in-memory
|
|
// fake would happily agree with any implementation.
|
|
func TestLiveAReferenceCannotBeRenamed(t *testing.T) {
|
|
st := liveStore(t)
|
|
ctx := context.Background()
|
|
site := seedAgentSite(t, st, "frozen-"+stamp())
|
|
|
|
if _, err := st.pool.Exec(ctx, `
|
|
INSERT INTO site_cameras (client_id, site_id, camera_id, label, host)
|
|
VALUES ($1::uuid, $2::uuid, 'Office1', 'Front door', '10.0.0.5')`,
|
|
site.ClientID, site.SiteID); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
for _, c := range []struct{ what, sql string }{
|
|
{"a shop's slug", `UPDATE sites SET slug = 'moved' WHERE id = $1::uuid`},
|
|
{"a camera's id", `UPDATE site_cameras SET camera_id = 'Office2' WHERE site_id = $1::uuid`},
|
|
} {
|
|
_, err := st.pool.Exec(ctx, c.sql, site.SiteID)
|
|
if err == nil {
|
|
t.Fatalf("%s was renamed - it is a reference other systems store", c.what)
|
|
}
|
|
if !strings.Contains(err.Error(), "cannot be changed") {
|
|
t.Fatalf("%s: unexpected error %v", c.what, err)
|
|
}
|
|
}
|
|
|
|
// The DISPLAY name is not frozen and must not be. It is what a person
|
|
// reads, nothing keys on it, and a system that cannot fix a typo in a
|
|
// shop's name has confused the two.
|
|
if _, err := st.pool.Exec(ctx,
|
|
`UPDATE sites SET name = 'Renamed Shop' WHERE id = $1::uuid`, site.SiteID); err != nil {
|
|
t.Fatalf("a shop's display name must stay editable: %v", err)
|
|
}
|
|
if _, err := st.pool.Exec(ctx,
|
|
`UPDATE site_cameras SET label = 'Back door' WHERE site_id = $1::uuid`,
|
|
site.SiteID); err != nil {
|
|
t.Fatalf("a camera's label must stay editable: %v", err)
|
|
}
|
|
}
|