package api import ( "context" "net/http" "net/http/httptest" "strings" "testing" "drivervault/apiserver/internal/config" "drivervault/apiserver/internal/ocpp" "drivervault/apiserver/internal/pb" ) // TestOCPPRouteAndControlAuth exercises the real HTTP stack (all middleware // included) to confirm the OCPP endpoint and the control REST routes are wired, // and that the charger-auth gate rejects an unknown token. PocketBase is left // unconfigured, so the successful (charger-connected) path is out of scope here — // that is covered by the internal/ocpp own/proxy tests. func TestOCPPRouteAndControlAuth(t *testing.T) { s := New(config.Config{UsersCollection: "users"}, pb.New("", "", "")) srv := httptest.NewServer(s.Handler()) defer srv.Close() wsBase := "ws" + strings.TrimPrefix(srv.URL, "http") // A charger connecting with no OCPP Basic auth is rejected (401). if _, err := ocpp.Dial(context.Background(), wsBase+"/ocpp/CP1", []string{"ocpp1.6"}, nil); err == nil || !strings.Contains(err.Error(), "401") { t.Fatalf("unauthenticated charger connect: want HTTP 401, got %v", err) } // A bogus token is unknown to the index (PB unconfigured, so the rebuild is a // no-op) and is likewise rejected. h := http.Header{} h.Set("Authorization", ocpp.BasicAuthHeader("CP1", "bogus-token")) if _, err := ocpp.Dial(context.Background(), wsBase+"/ocpp/CP1", []string{"ocpp1.6"}, h); err == nil || !strings.Contains(err.Error(), "401") { t.Fatalf("bogus-token charger connect: want HTTP 401, got %v", err) } // The control REST routes are registered (401 for a missing bearer token, not // 404 for an unknown route) — this also proves the {sn}/control, // {sn}/control/token and {sn}/{action} patterns don't shadow each other. routes := []struct { method, path string }{ {http.MethodGet, "/api/integrations/anker-solix/chargers/CP1/control"}, {http.MethodPost, "/api/integrations/anker-solix/chargers/CP1/control/token"}, {http.MethodPost, "/api/integrations/anker-solix/chargers/CP1/start"}, } for _, rt := range routes { req, _ := http.NewRequest(rt.method, srv.URL+rt.path, nil) resp, err := http.DefaultClient.Do(req) if err != nil { t.Fatalf("%s %s: %v", rt.method, rt.path, err) } resp.Body.Close() if resp.StatusCode != http.StatusUnauthorized { t.Errorf("%s %s: status %d, want 401 (route registered, bearer required)", rt.method, rt.path, resp.StatusCode) } } } // TestOCPPRequireTLS confirms a plaintext charger connection is rejected when TLS // is required, and that a TLS-terminating proxy (X-Forwarded-Proto) satisfies the // check (falling through to the auth gate). func TestOCPPRequireTLS(t *testing.T) { s := New(config.Config{UsersCollection: "users", OCPPRequireTLS: true}, pb.New("", "", "")) srv := httptest.NewServer(s.Handler()) defer srv.Close() wsBase := "ws" + strings.TrimPrefix(srv.URL, "http") // Plaintext ws:// with no forwarded-proto → rejected before auth (403). if _, err := ocpp.Dial(context.Background(), wsBase+"/ocpp/CP1", []string{"ocpp1.6"}, nil); err == nil || !strings.Contains(err.Error(), "403") { t.Fatalf("plaintext connect under require-TLS: want HTTP 403, got %v", err) } // Behind a TLS-terminating proxy the connection is accepted past the TLS gate // and then fails auth (401), proving the gate ordering. h := http.Header{} h.Set("X-Forwarded-Proto", "https") if _, err := ocpp.Dial(context.Background(), wsBase+"/ocpp/CP1", []string{"ocpp1.6"}, h); err == nil || !strings.Contains(err.Error(), "401") { t.Fatalf("forwarded-https connect: want HTTP 401 (past TLS gate), got %v", err) } }