package api import ( "encoding/json" "testing" ) func TestNormalizeGreencellTLS(t *testing.T) { for _, in := range []string{"on", "ON", "true", "yes", "1", " tls ", "mqtts"} { if got := normalizeGreencellTLS(in); got != "on" { t.Errorf("normalizeGreencellTLS(%q) = %q, want on", in, got) } } for _, in := range []string{"off", "FALSE", "no", "0", "plain"} { if got := normalizeGreencellTLS(in); got != "off" { t.Errorf("normalizeGreencellTLS(%q) = %q, want off", in, got) } } // Unset and unrecognized both resolve to "", which lets the cascade fall // through to the next layer rather than pinning it to a guess. for _, in := range []string{"", " ", "maybe"} { if got := normalizeGreencellTLS(in); got != "" { t.Errorf("normalizeGreencellTLS(%q) = %q, want empty", in, got) } } } func TestNormalizeGreencellPort(t *testing.T) { cases := map[string]string{ "1883": "1883", " 8883 ": "8883", "1": "1", "65535": "65535", "": "", "0": "", "-1": "", "65536": "", "abc": "", "18 83": "", } for in, want := range cases { if got := normalizeGreencellPort(in); got != want { t.Errorf("normalizeGreencellPort(%q) = %q, want %q", in, got, want) } } } func TestNormalizeGreencellTimeout(t *testing.T) { cases := map[string]string{ "12": "12", " 60 ": "60", "1": "1", "": "", "0": "", "61": "", "-5": "", "abc": "", } for in, want := range cases { if got := normalizeGreencellTimeout(in); got != want { t.Errorf("normalizeGreencellTimeout(%q) = %q, want %q", in, got, want) } } } func TestGreencellLockedFor(t *testing.T) { // A value set above the caller's editable layer is locked to them. if !greencellLockedFor("global", "user") { t.Error("a global value should lock the user layer") } if !greencellLockedFor("org", "user") { t.Error("an org value should lock the user layer") } if greencellLockedFor("user", "user") { t.Error("a caller's own layer is never locked to them") } if greencellLockedFor("org", "org") { t.Error("an org admin may edit the org layer") } if !greencellLockedFor("global", "org") { t.Error("a global value should lock the org layer") } // Unset is editable: the caller may be the first to set it. if greencellLockedFor("unset", "user") { t.Error("an unset field should be editable") } // A superadmin manages the global layer in the panel, not here. if !greencellLockedFor("unset", "none") { t.Error("the read-only scope locks everything") } } func TestGreencellMaskPresent(t *testing.T) { if got := greencellMaskPresent("hunter2"); got != greencellSecretMask { t.Errorf("a present secret should mask, got %q", got) } for _, in := range []string{"", " "} { if got := greencellMaskPresent(in); got != "" { t.Errorf("an absent secret should stay empty, got %q", got) } } } func TestMergeGreencellPreservesOtherPlugins(t *testing.T) { existing := json.RawMessage(`{"toyota":{"enabled":true},"ankerSolix":{"enabled":true}}`) out := mergeGreencell(existing, func(gs *greencellStored) { gs.Enabled = true gs.Config.Host = "10.2.1.10" }) var doc map[string]json.RawMessage if err := json.Unmarshal(out, &doc); err != nil { t.Fatalf("unmarshal: %v", err) } for _, key := range []string{"toyota", "ankerSolix", "greencell"} { if _, ok := doc[key]; !ok { t.Errorf("key %q should survive the merge", key) } } var gs greencellStored if err := json.Unmarshal(doc["greencell"], &gs); err != nil { t.Fatalf("unmarshal greencell: %v", err) } if !gs.Enabled || gs.Config.Host != "10.2.1.10" { t.Errorf("merged entry = %+v", gs) } } func TestMergeGreencellHandlesEmptyAndNullBlobs(t *testing.T) { for _, existing := range []json.RawMessage{nil, json.RawMessage(`null`), json.RawMessage(``)} { out := mergeGreencell(existing, func(gs *greencellStored) { gs.Config.Serial = "SN1" }) var doc greencellSettingsDoc if err := json.Unmarshal(out, &doc); err != nil { t.Fatalf("unmarshal %q: %v", existing, err) } if doc.Greencell.Config.Serial != "SN1" { t.Errorf("blob %q merged to %+v", existing, doc.Greencell) } } } func TestMergeGreencellKeepsUntouchedFields(t *testing.T) { // Toggling the enable flag must not wipe the stored broker config. first := mergeGreencell(nil, func(gs *greencellStored) { gs.Config = greencellConfig{Host: "mqtt.local", Port: "1883", Password: "secret"} }) second := mergeGreencell(first, func(gs *greencellStored) { gs.Enabled = true }) var doc greencellSettingsDoc if err := json.Unmarshal(second, &doc); err != nil { t.Fatalf("unmarshal: %v", err) } if doc.Greencell.Config.Host != "mqtt.local" || doc.Greencell.Config.Password != "secret" { t.Errorf("config lost across the merge: %+v", doc.Greencell.Config) } if !doc.Greencell.Enabled { t.Error("enable flag should have been set") } } func TestGreencellPluginConfigCarriesEveryField(t *testing.T) { res := greencellResolution{eff: greencellConfig{ Host: "10.2.1.10", Port: "8883", TLS: "on", Username: "dv", Password: "secret", Serial: "SN1", CommandTopic: "/greencell/evse/{sn}/command", Timeout: "20", }} cfg := greencellPluginConfig(res) want := map[string]string{ "host": "10.2.1.10", "port": "8883", "tls": "on", "username": "dv", "password": "secret", "serial": "SN1", "commandTopic": "/greencell/evse/{sn}/command", "timeout": "20", } for k, v := range want { if cfg[k] != v { t.Errorf("config[%q] = %q, want %q", k, cfg[k], v) } } if len(cfg) != len(want) { t.Errorf("config carries %d keys, want %d: %v", len(cfg), len(want), cfg) } } func TestGreencellGate(t *testing.T) { full := greencellResolution{ available: true, orgEnabled: true, enabled: true, eff: greencellConfig{Host: "mqtt.local"}, } if reason := greencellGate(full, true); reason != "" { t.Errorf("a fully-configured integration should pass, got %q", reason) } cases := []struct { name string res greencellResolution }{ {"master switch off", greencellResolution{orgEnabled: true, enabled: true, eff: greencellConfig{Host: "h"}}}, {"org gate off", greencellResolution{available: true, enabled: true, eff: greencellConfig{Host: "h"}}}, {"no broker", greencellResolution{available: true, orgEnabled: true, enabled: true}}, } for _, tc := range cases { if reason := greencellGate(tc.res, true); reason == "" { t.Errorf("%s should have been gated", tc.name) } } // The personal opt-in gates data reads but not a probe: the probe is how you // check the settings before turning the integration on. noOptIn := greencellResolution{available: true, orgEnabled: true, eff: greencellConfig{Host: "h"}} if reason := greencellGate(noOptIn, true); reason == "" { t.Error("a data read requires the personal opt-in") } if reason := greencellGate(noOptIn, false); reason != "" { t.Errorf("a probe should not require the opt-in, got %q", reason) } }