package store import ( "io" "log" "path/filepath" "testing" ) func langStore(t *testing.T) *Store { t.Helper() s, err := New(filepath.Join(t.TempDir(), "lang.db"), log.New(io.Discard, "", 0)) if err != nil { t.Fatal(err) } t.Cleanup(func() { s.Close() }) return s } func saveCfg(t *testing.T, s *Store, id, lang string) { t.Helper() if err := s.SaveCustomerConfig(&CustomerConfig{ CustomerID: id, CustomerName: id, Domain: "x.example", RetrievalPassword: "p", APIKey: "k-" + id, ConfigJSON: "{}", Language: lang, }); err != nil { t.Fatal(err) } } // S2 — the language order IS the design: last reported → created-with → Hungarian. // // Each step is checked on its own AND in combination, because the interesting failure is not "the // wrong default" but "the reported one is ignored" — a household that switches their dashboard to // English and keeps getting Hungarian mail would look exactly like the feature not shipping at all. func TestCustomerLanguageOrder(t *testing.T) { s := langStore(t) // 3. Neither: Hungarian. if got := s.CustomerLanguage("nobody"); got != "hu" { t.Errorf("an unknown customer = %q, want hu", got) } // 2. Created-with, no report yet — the window the claim mail and the bind page live in. saveCfg(t, s, "c-en", "en") saveCfg(t, s, "c-hu", "hu") if got := s.CustomerLanguage("c-en"); got != "en" { t.Errorf("created-with-en, no report = %q, want en", got) } if got := s.CustomerLanguage("c-hu"); got != "hu" { t.Errorf("created-with-hu, no report = %q, want hu", got) } // 1. Reported WINS over created-with, in both directions. The household outranks the operator. if err := s.SaveReport("c-en", []byte(`{"controller_version":"0.256.0","language":"hu"}`)); err != nil { t.Fatal(err) } if got := s.CustomerLanguage("c-en"); got != "hu" { t.Errorf("created en but reported hu = %q, want hu — the household's choice lost", got) } if err := s.SaveReport("c-hu", []byte(`{"controller_version":"0.256.0","language":"en"}`)); err != nil { t.Fatal(err) } if got := s.CustomerLanguage("c-hu"); got != "en" { t.Errorf("created hu but reported en = %q, want en — the household's choice lost", got) } } // A box that reports NOTHING (a controller older than 0.247.0) must not erase the creation-time // language. This is the live case: three boxes in the fleet report no language at all. func TestOldControllerReportDoesNotOverrideTheCreationLanguage(t *testing.T) { s := langStore(t) saveCfg(t, s, "old", "en") if err := s.SaveReport("old", []byte(`{"controller_version":"0.245.0"}`)); err != nil { t.Fatal(err) } if got := s.CustomerLanguage("old"); got != "en" { t.Errorf("a report with no language = %q, want en — the creation default was erased", got) } } // Garbage on the wire is stored as "never told us", NOT as Hungarian. Storing it as Hungarian would // make a later real choice indistinguishable from the absence of one. func TestGarbageReportedLanguageIsNotStoredAsHungarian(t *testing.T) { s := langStore(t) saveCfg(t, s, "g", "en") for _, bad := range []string{`"de"`, `"EN-gb"`, `""`, `"; DROP TABLE reports"`} { if err := s.SaveReport("g", []byte(`{"language":`+bad+`}`)); err != nil { t.Fatal(err) } if got := s.CustomerLanguage("g"); got != "en" { t.Errorf("reported %s = %q, want en (the creation default, because the report said nothing usable)", bad, got) } } // ...and a REAL one still lands. if err := s.SaveReport("g", []byte(`{"language":"hu"}`)); err != nil { t.Fatal(err) } if got := s.CustomerLanguage("g"); got != "hu" { t.Errorf("a real reported language after garbage = %q, want hu", got) } } // The NEWEST report wins, not the first or an arbitrary one. func TestNewestReportedLanguageWins(t *testing.T) { s := langStore(t) saveCfg(t, s, "n", "hu") for _, lang := range []string{"hu", "en", "hu", "en"} { if err := s.SaveReport("n", []byte(`{"language":"`+lang+`"}`)); err != nil { t.Fatal(err) } } if got := s.CustomerLanguage("n"); got != "en" { t.Errorf("after hu,en,hu,en the language is %q, want en (the newest)", got) } } // A creation-time language that is not a real language stores as Hungarian rather than as itself: // the form is operator-driven, and an unsupported value must never reach a mail renderer. func TestCreationLanguageIsValidated(t *testing.T) { s := langStore(t) saveCfg(t, s, "bad", "klingon") cfg, err := s.GetCustomerConfig("bad") if err != nil { t.Fatal(err) } if cfg.Language != "hu" { t.Errorf("an unsupported creation language stored as %q, want hu", cfg.Language) } } // The language survives a round trip through the config record — the value configgen writes into // controller.yaml comes from here. func TestCustomerConfigLanguageRoundTrips(t *testing.T) { s := langStore(t) saveCfg(t, s, "rt", "en") cfg, err := s.GetCustomerConfig("rt") if err != nil { t.Fatal(err) } if cfg.Language != "en" { t.Errorf("GetCustomerConfig language = %q, want en", cfg.Language) } list, err := s.ListCustomerConfigs() if err != nil { t.Fatal(err) } for _, c := range list { if c.CustomerID == "rt" && c.Language != "en" { t.Errorf("ListCustomerConfigs language = %q, want en", c.Language) } } }