Anker documents neither rfid write, so the bodies are inferred from the field names get_device_cards answers with, and every write re-reads the list: what the card shows is what the account holds, never what an undocumented endpoint claimed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
100 lines
3.5 KiB
Go
100 lines
3.5 KiB
Go
package ankersolix
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// A card number typed with punctuation is the same card as the one the account
|
|
// stores bare — anything else deletes nothing and adds a duplicate.
|
|
func TestNormalizeCardNumber(t *testing.T) {
|
|
for _, tc := range []struct{ in, want string }{
|
|
{"4754DC2A", "4754DC2A"},
|
|
{"47:54:dc:2a", "4754DC2A"},
|
|
{" 47-54 dc 2a ", "4754DC2A"},
|
|
{"04dfe672151a90", "04DFE672151A90"},
|
|
{"", ""},
|
|
{" :- ", ""},
|
|
} {
|
|
if got := normalizeCardNumber(tc.in); got != tc.want {
|
|
t.Errorf("normalizeCardNumber(%q) = %q, want %q", tc.in, got, tc.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// The name a card gets when it is added without one, in the shape the Anker app
|
|
// writes so a card added here does not stand out in it.
|
|
func TestRfidCardLabel(t *testing.T) {
|
|
if got := rfidCardLabel("4754DC2A"); got != "RFID DC2A" {
|
|
t.Errorf("rfidCardLabel = %q, want %q", got, "RFID DC2A")
|
|
}
|
|
if got := rfidCardLabel("2A"); got != "RFID 2A" {
|
|
t.Errorf("short number: rfidCardLabel = %q, want %q", got, "RFID 2A")
|
|
}
|
|
}
|
|
|
|
func TestParseRfidCards(t *testing.T) {
|
|
body := []byte(`{"code":0,"data":{"list":[
|
|
{"alias_name":"RFID DC2A","card_number":"4754DC2A","create_time":1788187299},
|
|
{"alias_name":"RFID B93F","card_number":"DA7CB93F","create_time":1788187315}]}}`)
|
|
cards, err := parseRfidCards(body)
|
|
if err != nil {
|
|
t.Fatalf("parseRfidCards: %v", err)
|
|
}
|
|
if len(cards) != 2 {
|
|
t.Fatalf("got %d cards, want 2", len(cards))
|
|
}
|
|
if cards[0].Name != "RFID DC2A" || cards[0].Number != "4754DC2A" {
|
|
t.Fatalf("first card = %+v", cards[0])
|
|
}
|
|
if string(cards[0].Added) != "1788187299" {
|
|
t.Fatalf("create_time = %q, want it relayed as it arrived", cards[0].Added)
|
|
}
|
|
|
|
// A charger with no cards answers with an empty document, which is an answer
|
|
// and not a failure.
|
|
cards, err = parseRfidCards([]byte(`{"code":0,"data":{}}`))
|
|
if err != nil || len(cards) != 0 {
|
|
t.Fatalf("empty list: got %d cards, err %v", len(cards), err)
|
|
}
|
|
}
|
|
|
|
// Presence is what a write is judged by, and it is judged on the number rather
|
|
// than on how either side punctuated it.
|
|
func TestCardPresent(t *testing.T) {
|
|
cards := []rfidCard{{Number: "4754DC2A"}, {Number: "DA7CB93F"}}
|
|
if !cardPresent(cards, "47:54:dc:2a") {
|
|
t.Error("punctuated number: want present")
|
|
}
|
|
if cardPresent(cards, "DEADBEEF") {
|
|
t.Error("unknown number: want absent")
|
|
}
|
|
if cardPresent(nil, "4754DC2A") {
|
|
t.Error("empty charger: want absent")
|
|
}
|
|
}
|
|
|
|
// Neither write may reach the cloud without knowing which charger and which
|
|
// card: an endpoint nobody has documented is not one to send half a request to.
|
|
func TestCardWritesRefuseIncompleteRequests(t *testing.T) {
|
|
p := &Plugin{}
|
|
_ = p.Init(context.Background(), map[string]string{"email": "u@example.com", "password": "p"})
|
|
|
|
for _, action := range []string{"rfid-card-save", "rfid-card-delete"} {
|
|
if _, err := p.Invoke(context.Background(), action, nil); err == nil ||
|
|
!strings.Contains(err.Error(), "requires an sn") {
|
|
t.Errorf("%s without a serial: got %v, want an sn-required error", action, err)
|
|
}
|
|
// A serial but no card, and no card number to be found in punctuation
|
|
// either: both stop before anything is sent.
|
|
for _, params := range []string{`{"sn":"EVSN1"}`, `{"sn":"EVSN1","cardNumber":" :- "}`} {
|
|
_, err := p.Invoke(context.Background(), action, json.RawMessage(params))
|
|
if err == nil || !strings.Contains(err.Error(), "card number is required") {
|
|
t.Errorf("%s with %s: got %v, want a card-number-required error", action, params, err)
|
|
}
|
|
}
|
|
}
|
|
}
|