package api import ( "encoding/json" "testing" ) func TestChargerSourceByID(t *testing.T) { for _, id := range []string{ankerPlugin, greencellPlugin} { src, ok := chargerSourceByID(id) if !ok { t.Fatalf("charger provider %q is not registered", id) } if src.id() != id || src.label() == "" || src.service() == "" { t.Fatalf("provider %q describes itself badly: %+v", id, src) } } if _, ok := chargerSourceByID("toyota"); ok { t.Fatal("a vehicle provider must not resolve as a charger provider") } } // The Anker mapping reads the merged inventory the plugin's chargers capability // answers with — the same document the Settings list renders. func TestAnkerChargerSourceMapping(t *testing.T) { raw := json.RawMessage(`{"chargers":[ {"sn":"AT2DK1","name":"Garage","model":"A5191","siteName":"Home","statusDesc":"charging","online":true,"sources":["site"]}, {"sn":"AT2DK2","model":"A5191","online":false}, {"sn":"","name":"nameless"}],"count":3}`) got := ankerChargerSource{}.chargers(raw) if len(got) != 2 { t.Fatalf("got %d chargers, want 2 (the serial-less one is skipped)", len(got)) } first := got[0] if first.ID != "AT2DK1" || first.Name != "Garage" || first.Model != "A5191" { t.Fatalf("first charger = %+v", first) } if first.Vendor != "Anker Solix" || first.SiteName != "Home" || first.Status != "charging" { t.Fatalf("first charger not fully mapped: %+v", first) } if first.Online == nil || !*first.Online { t.Fatalf("online = %v, want true", first.Online) } if got[1].Online == nil || *got[1].Online { t.Fatalf("second charger online = %v, want false", got[1].Online) } if (ankerChargerSource{}).chargers(json.RawMessage(`not json`)) != nil { t.Fatal("a malformed payload should map to no chargers, not a panic") } } func TestGreencellChargerSourceMapping(t *testing.T) { raw := json.RawMessage(`{"chargers":[ {"sn":"GC-1","name":"Wallbox","model":"HabuDen"}, {"name":"no serial"}]}`) got := greencellChargerSource{}.chargers(raw) if len(got) != 1 { t.Fatalf("got %d chargers, want 1", len(got)) } if got[0].ID != "GC-1" || got[0].Name != "Wallbox" || got[0].Vendor != "Greencell" { t.Fatalf("charger = %+v", got[0]) } } func TestFindProviderCharger(t *testing.T) { list := []providerCharger{{ID: "AT2DK1"}, {ID: "GC-1"}} if _, ok := findProviderCharger(list, " at2dk1 "); !ok { t.Fatal("a serial should match whatever case and padding it arrives in") } if _, ok := findProviderCharger(list, "AT2DK9"); ok { t.Fatal("a serial not on the account must not match") } } // The import payload carries the provider link and the owner; a later rename // only ever touches the name, so the link cannot be broken by editing. func TestHomeChargerPayloadOmitsProviderLink(t *testing.T) { payload := homeChargerPayload(homeChargerRecord{Name: "Garage", Serial: "AT2DK1"}.toModel()) for _, key := range []string{"provider", "provider_charger_id", "owner"} { if _, present := payload[key]; present { t.Fatalf("payload must not carry %q — only the import endpoint sets it", key) } } if payload["name"] != "Garage" || payload["serial"] != "AT2DK1" { t.Fatalf("payload = %+v", payload) } } func TestHomeChargerRecordToModel(t *testing.T) { rec := homeChargerRecord{ ID: "rec1", Name: "Garage", Serial: "AT2DK1", Vendor: "Anker Solix", Model: "A5191", SiteName: "Home", PowerKw: 11, Connector: "Type 2", Provider: ankerPlugin, ProviderChargerID: "AT2DK1", Owner: "u1", Created: "2026-08-31 10:00:00.000Z", } m := rec.toModel() if m.ID != "rec1" || m.Name != "Garage" || m.Serial != "AT2DK1" || m.PowerKw != 11 { t.Fatalf("model = %+v", m) } if m.Provider != ankerPlugin || m.ProviderChargerID != "AT2DK1" { t.Fatalf("provider link lost: %+v", m) } // The owner is not part of what a client reads back. b, err := json.Marshal(m) if err != nil { t.Fatal(err) } var out map[string]any if err := json.Unmarshal(b, &out); err != nil { t.Fatal(err) } if _, present := out["owner"]; present { t.Fatalf("home charger JSON should not carry an owner: %s", b) } }