package config import ( "path/filepath" "strings" "testing" ) // The bug this exists to prevent, measured on a second Mac claimed to a live // shop: MQTT requires client ids to be unique, and a broker enforces it by // disconnecting the older session when a new one arrives with the same id. The // id was `behavision--` - identical on every computer claimed to // one shop - so the two took turns kicking each other off: // // broker connected / broker connection lost: EOF / broker connected / ... // // The damage is not confined to the new machine. The shop's own till is the // other half of that loop, so somebody signing in on a laptop to look at the // product stops the shop delivering visits. func TestTwoInstallsOnOneSiteGetDifferentClientIDs(t *testing.T) { dir := t.TempDir() one := writeClaimed(t, filepath.Join(dir, "a.json")) two := writeClaimed(t, filepath.Join(dir, "b.json")) if one.MQTTClientID() == two.MQTTClientID() { t.Fatalf("both installs answered to %q; the broker will disconnect one "+ "whenever the other connects", one.MQTTClientID()) } } // And the same install keeps its name across restarts, or a broker log is a // list of strangers and nobody can tell one till from a stream of new ones. func TestOneInstallKeepsItsClientIDAcrossRestarts(t *testing.T) { path := filepath.Join(t.TempDir(), "agent.json") first := writeClaimed(t, path) again, err := Load(path) if err != nil { t.Fatalf("reload: %v", err) } if got, want := again.MQTTClientID(), first.MQTTClientID(); got != want { t.Errorf("after a restart the id was %q, want %q", got, want) } } // The site stays in the id: it is what somebody reading a broker log is // reading FOR, and an opaque random string would make every connection // anonymous. func TestTheClientIDStillNamesTheShop(t *testing.T) { c := Config{ClientID: "tenext-retail", SiteID: "chennai", InstallID: "abcd1234"} id := c.MQTTClientID() for _, want := range []string{"tenext-retail", "chennai", "abcd1234"} { if !strings.Contains(id, want) { t.Errorf("client id %q does not contain %q", id, want) } } } // A config that could not be written still has to produce a UNIQUE id, or a // read-only install falls straight back into the collision. Per-run is the // right failure: the connection works, and the only cost is a new name in the // broker's log after a restart. func TestAnUnsavedConfigStillGetsAUniqueID(t *testing.T) { a := Config{ClientID: "c", SiteID: "s"} b := Config{ClientID: "c", SiteID: "s"} if a.MQTTClientID() == b.MQTTClientID() { t.Fatal("two configs with no install id produced the same client id") } } func writeClaimed(t *testing.T, path string) Config { t.Helper() cfg := Defaults() cfg.ClientID, cfg.SiteID = "tenext-retail", "chennai" if err := cfg.Save(path); err != nil { t.Fatalf("save: %v", err) } // Loading is what mints the id, so an installation that predates the // field gets one without anybody doing anything. got, err := Load(path) if err != nil { t.Fatalf("load: %v", err) } if got.InstallID == "" { t.Fatal("loading a config without an install id did not mint one") } return got }