package onboarding import ( "bytes" "encoding/json" "net/http" "net/http/httptest" "testing" "data-terminal-cloud/internal/identity" "github.com/google/uuid" ) const webBLECSRF = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" func webBLESession(t *testing.T, f *fixture, p identity.Principal) string { t.Helper() session := randomToken(32) _, err := f.s.pool.Exec(f.ctx, `INSERT INTO onboarding_account_session(token_hash,csrf_hash,user_id,expires_at) SELECT $1,$2,id,now()+interval '1 hour' FROM user_account WHERE oidc_issuer=$3 AND oidc_subject=$4`, digest([]byte(session)), digest([]byte(webBLECSRF)), p.Issuer, p.Subject) must(t, err) return session } func webBLERequest(t *testing.T, s *Service, endpoint, session, csrf, origin string, in any) (int, map[string]any) { t.Helper() raw, _ := json.Marshal(in) r := httptest.NewRequest("POST", "https://platform.example/api/v1/web/"+endpoint, bytes.NewReader(raw)) r.Header.Set("Origin", origin) r.Header.Set("Content-Type", "application/json") r.Header.Set("X-Request-ID", uuid.NewString()) r.Header.Set("X-CSRF-Token", csrf) if session != "" { r.AddCookie(&http.Cookie{Name: s.cookieName(true), Value: session}) } w := httptest.NewRecorder() mux := http.NewServeMux() s.RegisterRoutes(mux) mux.ServeHTTP(w, r) var out map[string]any must(t, json.Unmarshal(w.Body.Bytes(), &out)) data, _ := out["data"].(map[string]any) return w.Code, data } func TestWebBLEStateRequiresOwnerOriginAndCSRF(t *testing.T) { f := serverFixture(t) f.web.Hash = "" p := accountOnlyUser(t, f) session := webBLESession(t, f, p) in := BLEDeviceInput{DeviceID: f.d.FactoryID} for _, endpoint := range []string{"ble-device-state", "device-subject", "ble-bind-tokens"} { for _, auth := range [][3]string{{"", webBLECSRF, "https://platform.example"}, {session, "wrong", "https://platform.example"}, {session, webBLECSRF, "https://evil.example"}} { status, _ := webBLERequest(t, f.s, endpoint, auth[0], auth[1], auth[2], in) if status != 401 && status != 403 { t.Fatalf("%s allowed invalid auth: %d", endpoint, status) } } } status, out := webBLERequest(t, f.s, "ble-bind-tokens", session, webBLECSRF, "https://platform.example", BLETokenInput{DeviceID: f.d.FactoryID}) if status != 200 { t.Fatal("account-first token", status, out) } _, err := f.s.BindBLE(f.ctx, f.d, uuid.NewString(), BLEBindInput{Token: out["bind_token"].(string)}) must(t, err) f.refresh() generation := f.d.Generation status, out = webBLERequest(t, f.s, "ble-device-state", session, webBLECSRF, "https://platform.example", in) if status != 200 || out["bound"] != true || out["id"] != f.d.PublicID { t.Fatal(status, out) } other := webBLESession(t, f, accountOnlyUser(t, f)) status, _ = webBLERequest(t, f.s, "ble-device-state", other, webBLECSRF, "https://platform.example", in) if status != 403 { t.Fatal("other account can reconfigure", status) } status, _ = webBLERequest(t, f.s, "ble-bind-tokens", session, webBLECSRF, "https://platform.example", BLETokenInput{DeviceID: f.d.FactoryID}) if status != 409 { t.Fatal("bound device received another claim", status) } f.refresh() if f.d.Generation != generation { t.Fatal("Wi-Fi eligibility changed ownership") } }