The charger's own card list gets a door, and says where it differs
0104 was already implemented and already in the action catalogue; nothing routed to it, so the only way to see the device's list was as a side effect of writing a card. It has an endpoint now, and the panel a button. The two lists are compared where they meet: a card the charger holds and the account has forgotten still opens it, and a card only the account holds will not, and neither shows anywhere else. The comparison is drawn only when they disagree, and the device's reading is dropped on a refresh rather than measured against an account list from a later moment. The new test asks all four card routes without a token: a capability the plugin implements and the catalogue advertises is still unusable if nothing routes to it, and no other test here would notice. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
7176867eb3
commit
3064bff8ad
@@ -648,7 +648,24 @@ func (s *Server) handleAnkerCardScan(w http.ResponseWriter, r *http.Request) {
|
||||
s.ankerCardWrite(w, r, who, cfg, "rfid-card-scan", sn, map[string]any{"sn": sn})
|
||||
}
|
||||
|
||||
// ankerCardGate is everything both writes need before they may run: a caller, a
|
||||
// GET /api/integrations/anker-solix/chargers/{sn}/rfid-cards/charger — the list
|
||||
// of cards the charger itself holds, asked of the device with 0104 rather than
|
||||
// of the account.
|
||||
//
|
||||
// The two lists are written together and can still come apart: a card the
|
||||
// account has forgotten still opens the charger until the device is told
|
||||
// otherwise, and the account's copy is the only one every other view here
|
||||
// draws. Asking the device is the only way to see the difference. It answers
|
||||
// with UIDs and nothing else — the charger has no field for a card's name.
|
||||
func (s *Server) handleAnkerChargerCards(w http.ResponseWriter, r *http.Request) {
|
||||
who, sn, cfg, ok := s.ankerCardGate(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
s.ankerCardWrite(w, r, who, cfg, "rfid-cards-charger", sn, map[string]any{"sn": sn})
|
||||
}
|
||||
|
||||
// ankerCardGate is everything a card call needs before it may run: a caller, a
|
||||
// serial, an integration that is on and has credentials, and a rate limit. A
|
||||
// gate that is off answers 409 rather than the reads' 200-with-a-reason: a write
|
||||
// that did not happen is not a state to render, it is a request that failed.
|
||||
@@ -670,7 +687,7 @@ func (s *Server) ankerCardGate(w http.ResponseWriter, r *http.Request) (*callerI
|
||||
return nil, "", nil, false
|
||||
}
|
||||
if !s.ctlRL.allow(who.ID + "|" + sn) {
|
||||
writeError(w, http.StatusTooManyRequests, "too many card changes; please slow down")
|
||||
writeError(w, http.StatusTooManyRequests, "too many card requests; please slow down")
|
||||
return nil, "", nil, false
|
||||
}
|
||||
return who, sn, map[string]string{
|
||||
|
||||
@@ -3,10 +3,12 @@ package api
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"drivervault/apiserver/internal/config"
|
||||
"drivervault/apiserver/internal/pb"
|
||||
)
|
||||
|
||||
@@ -217,3 +219,25 @@ func TestOCPPEndpoint(t *testing.T) {
|
||||
t.Errorf("tls endpoint = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
// The four card routes are reachable at all. A capability the plugin implements
|
||||
// and the action catalogue advertises is still unusable if nothing routes to it,
|
||||
// and that is not a failure any other test here would notice: the plugin's own
|
||||
// tests pass, and the panel simply has no button. Each route is asked for
|
||||
// without a token, so what is being checked is that the request reached the
|
||||
// authentication middleware rather than a 404.
|
||||
func TestAnkerCardRoutesAreRegistered(t *testing.T) {
|
||||
h := New(config.Config{}, nil).Handler()
|
||||
for _, tc := range []struct{ method, path string }{
|
||||
{"POST", "/api/integrations/anker-solix/chargers/SN1/rfid-cards"},
|
||||
{"POST", "/api/integrations/anker-solix/chargers/SN1/rfid-cards/scan"},
|
||||
{"GET", "/api/integrations/anker-solix/chargers/SN1/rfid-cards/charger"},
|
||||
{"DELETE", "/api/integrations/anker-solix/chargers/SN1/rfid-cards/AABBCCDD"},
|
||||
} {
|
||||
rr := httptest.NewRecorder()
|
||||
h.ServeHTTP(rr, httptest.NewRequest(tc.method, tc.path, nil))
|
||||
if rr.Code == http.StatusNotFound {
|
||||
t.Errorf("%s %s is not routed", tc.method, tc.path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,6 +55,7 @@
|
||||
// POST /api/integrations/anker-solix/chargers/{sn}/rfid-cards
|
||||
// POST /api/integrations/anker-solix/chargers/{sn}/rfid-cards/scan
|
||||
// DELETE /api/integrations/anker-solix/chargers/{sn}/rfid-cards/{number}
|
||||
// GET /api/integrations/anker-solix/chargers/{sn}/rfid-cards/charger
|
||||
// GET /api/integrations/greencell PUT /api/integrations/greencell
|
||||
// POST /api/integrations/greencell/health
|
||||
// GET /api/integrations/greencell/chargers
|
||||
@@ -447,6 +448,7 @@ func (s *Server) Handler() http.Handler {
|
||||
mux.HandleFunc("POST /api/integrations/anker-solix/chargers/{sn}/rfid-cards", s.handleAnkerCardSave)
|
||||
mux.HandleFunc("POST /api/integrations/anker-solix/chargers/{sn}/rfid-cards/scan", s.handleAnkerCardScan)
|
||||
mux.HandleFunc("DELETE /api/integrations/anker-solix/chargers/{sn}/rfid-cards/{number}", s.handleAnkerCardDelete)
|
||||
mux.HandleFunc("GET /api/integrations/anker-solix/chargers/{sn}/rfid-cards/charger", s.handleAnkerChargerCards)
|
||||
mux.HandleFunc("GET /api/integrations/greencell", s.handleGetGreencell)
|
||||
mux.HandleFunc("PUT /api/integrations/greencell", s.handlePutGreencell)
|
||||
mux.HandleFunc("POST /api/integrations/greencell/health", s.handleGreencellHealth)
|
||||
|
||||
Reference in New Issue
Block a user