Files
DriverVault/API Server/internal/plugins/builtin/ankersolix/chargerdetails_test.go
T
tajniak81andClaude Opus 5 3f9d5b943f Four questions asked of an account that answers twenty-one
The connector called ten endpoints of the read surface the map lists, and
the charger card showed four views. Everything else an EV charger can
reach is now a capability too: the sessions and the history, the savings,
the sharing, the binding, the group, the Wi-Fi, the firmware and its
update log, the tamper records, the site's own detail, price, networks
and energy — plus the vehicle catalogue, dynamic pricing, the currencies
and the notification views. Thirty-eight endpoints, one action each. The
two message views are GET, so the request path grew a GET half that shares
the login retry with the POST one.

The per-charger fan-out asks all of them, six at a time rather than one
after another, and a charger that belongs to a site brings that site's
four views with it once the by-serial lookup has found it. A view that
answers with nothing now says so instead of vanishing: the station record
is empty for a standalone charger because it has no station, which is an
answer worth reading. And "source 0" in the OCPP box carries the address
the account's endpoint list gives it.

Anker's account-level writes stay out, as do the endpoints whose payloads
were only ever read out of the app package.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 22:53:36 +02:00

98 lines
3.6 KiB
Go

package ankersolix
import "testing"
// The site views are only worth asking for once the by-serial lookup has found a
// site, and that lookup names it differently depending on which shape answered.
func TestSiteIDFromReadsWhicheverKeyTheLookupUsed(t *testing.T) {
cases := []struct {
name string
attrs map[string]string
want string
}{
{"snake case", map[string]string{"site_id": "s-1"}, "s-1"},
{"camel case", map[string]string{"siteId": "s-2"}, "s-2"},
{"nested", map[string]string{"site_info.site_id": "s-3"}, "s-3"},
{"standalone charger", map[string]string{"device_sn": "AT1"}, ""},
{"blank is not an answer", map[string]string{"site_id": " "}, ""},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
views := []chargerDetailView{{ID: "ocpp", Attrs: map[string]string{"site_id": "wrong"}}, {ID: "siteBySn", Attrs: tc.attrs}}
if got := siteIDFrom(views); got != tc.want {
t.Fatalf("siteIDFrom = %q, want %q", got, tc.want)
}
})
}
}
// "source 0" is a number until the account's endpoint list gives it an address.
func TestNoteOcppSourceNamesTheBackend(t *testing.T) {
views := []chargerDetailView{
{ID: "ocpp", Attrs: map[string]string{"source": "1"}},
{ID: "ocppEndpoints", Attrs: map[string]string{
"list[0].source": "0", "list[0].url": "wss://ocpp.anker.example/0",
"list[1].source": "1", "list[1].url": "wss://ocpp.anker.example/1",
}},
}
noteOcppSource(views)
if want := "source 1 = wss://ocpp.anker.example/1"; views[0].Note != want {
t.Fatalf("note = %q, want %q", views[0].Note, want)
}
}
// A number the list does not carry stays a number: a guessed backend would be
// worse than saying nothing.
func TestNoteOcppSourceSaysNothingWhenTheListCannotAnswer(t *testing.T) {
views := []chargerDetailView{
{ID: "ocpp", Attrs: map[string]string{"source": "7"}},
{ID: "ocppEndpoints", Attrs: map[string]string{"list[0].source": "0", "list[0].url": "wss://x/0"}},
}
noteOcppSource(views)
if views[0].Note != "" {
t.Fatalf("note = %q, want none", views[0].Note)
}
}
// The entry's own address, not a neighbour's.
func TestEndpointForSourceStaysWithinItsEntry(t *testing.T) {
endpoints := map[string]string{
"list[0].source": "0", "list[0].endpoint": "wss://a",
"list[1].source": "2", "list[1].endpoint": "wss://b",
}
if got := endpointForSource(endpoints, "2"); got != "wss://b" {
t.Fatalf("endpointForSource = %q, want wss://b", got)
}
if got := endpointForSource(endpoints, "3"); got != "" {
t.Fatalf("endpointForSource = %q, want empty", got)
}
}
// The multi-serial views accept either spelling, and reject neither silently.
func TestSerialList(t *testing.T) {
if got := serialList(invokeParams{SN: "AT1"}); len(got) != 1 || got[0] != "AT1" {
t.Fatalf("single serial = %v", got)
}
got := serialList(invokeParams{SN: "AT1", SNs: []string{"AT2", " ", "AT3"}})
if len(got) != 2 || got[0] != "AT2" || got[1] != "AT3" {
t.Fatalf("several serials = %v", got)
}
if got := serialList(invokeParams{}); len(got) != 0 {
t.Fatalf("nothing given = %v", got)
}
}
// The paged and multi-view endpoints get a sane ask when the caller does not
// care, rather than a zero the cloud reads as "no page at all".
func TestPagingAndViewDefaults(t *testing.T) {
if pageNum(0) != 1 || pageNum(3) != 3 {
t.Fatalf("pageNum: %d %d", pageNum(0), pageNum(3))
}
if pageSize(0) != 20 || pageSize(500) != 20 || pageSize(50) != 50 {
t.Fatalf("pageSize: %d %d %d", pageSize(0), pageSize(500), pageSize(50))
}
if upgradeRecordView(0) != 1 || upgradeRecordView(4) != 1 || upgradeRecordView(2) != 2 {
t.Fatalf("upgradeRecordView: %d %d %d", upgradeRecordView(0), upgradeRecordView(4), upgradeRecordView(2))
}
}