package onboarding import ( "bytes" "encoding/base64" "encoding/json" "net/http" "net/http/httptest" "strconv" "strings" "testing" "time" "github.com/google/uuid" ) func (f *fixture) deviceCall(path string, body any, id string, secure bool) (int, map[string]any) { f.t.Helper() raw, ok := body.([]byte) if !ok { raw = requestJSON(body) } if id == "" { id = uuid.NewString() } path = "/api/v1/device/" + path r := httptest.NewRequest("POST", "https://platform.example"+path, bytes.NewReader(raw)) if !secure { r.TLS = nil } nonce, timestamp := randomToken(16), time.Now().UnixMilli() r.Header.Set("Content-Type", "application/json") if !strings.HasSuffix(path, "time-challenges") { r.Header.Set("X-Device-ID", f.d.FactoryID) r.Header.Set("X-Key-Version", "1") r.Header.Set("X-Timestamp-Ms", strconv.FormatInt(timestamp, 10)) r.Header.Set("X-Nonce", nonce) r.Header.Set("X-Request-ID", id) r.Header.Set("Authorization", "Device-HMAC-SHA256 "+RequestMAC(f.d.Key, "POST", path, f.d.FactoryID, 1, timestamp, nonce, id, raw)) } mux := http.NewServeMux() f.s.RegisterRoutes(mux) w := httptest.NewRecorder() mux.ServeHTTP(w, r) var result map[string]any must(f.t, json.Unmarshal(w.Body.Bytes(), &result)) return w.Code, result } func fieldsEqual(t *testing.T, v map[string]any, fields string) { t.Helper() names := strings.Fields(fields) if len(v) != len(names) { t.Fatalf("unexpected fields: %v; expected %s", mapKeys(v), fields) } for _, name := range names { if _, ok := v[name]; !ok { t.Fatalf("missing %s", name) } } } func mapKeys(v map[string]any) []string { keys := []string{} for k := range v { keys = append(keys, k) } return keys } func dataOK(t *testing.T, status int, v map[string]any, fields string) map[string]any { t.Helper() if status != 200 { t.Fatalf("status=%d code=%v", status, v["code"]) } fieldsEqual(t, v, "ok code request_id server_time_ms data") d := v["data"].(map[string]any) fieldsEqual(t, d, fields) if state, ok := d["state"].(map[string]any); ok { fieldsEqual(t, state, "device_id device_revision binding_generation binding_state activation_state state_hash") } return d } func TestIntegrationDeviceWireContract(t *testing.T) { f := newFixture(t) // C01 uses plain HTTP without a device Authorization header. Only a signed // time packet is returned; key material and business data are absent. in := TimeChallengeInput{DeviceID: f.d.FactoryID, KeyVersion: 1, BootID: f.d.BootID, ClientNonce: randomToken(16)} status, response := f.deviceCall("time-challenges", in, "", false) if status != 200 { t.Fatal(status) } fieldsEqual(t, response, "payload_b64 mac_hex") if !sameSecret(domainMAC(f.d.Key, "TIME1\n"+response["payload_b64"].(string)), response["mac_hex"].(string)) { t.Fatal("time MAC") } decoded, err := base64.RawURLEncoding.DecodeString(response["payload_b64"].(string)) must(t, err) var packet map[string]any must(t, json.Unmarshal(decoded, &packet)) fieldsEqual(t, packet, "device_id boot_id client_nonce challenge_id server_time_ms expires_at_ms") if packet["client_nonce"] != in.ClientNonce || packet["boot_id"] != in.BootID || packet["device_id"] != in.DeviceID { t.Fatal("time echo") } reg := RegisterInput{ChallengeID: packet["challenge_id"].(string), BootID: f.d.BootID, Model: f.d.Model, HardwareRevision: f.d.HardwareRevision, FirmwareVersion: "1.2.0", SDKVersion: "1.0.0"} status, response = f.deviceCall("register", reg, "", true) dataOK(t, status, response, "state contract_version") setup := SetupInput{BootID: f.d.BootID, LocalSetupID: uuid.NewString(), Transport: "LOCAL_WEB"} // Keep the established fixture setup: a repeated creation with the same // local identity is independently checked in the setup/recovery tests. must(t, f.s.pool.QueryRow(f.ctx, `SELECT local_setup_id::text FROM device_setup_session WHERE id=$1::uuid`, f.setup).Scan(&setup.LocalSetupID)) status, response = f.deviceCall("setup-sessions", setup, "", true) dataOK(t, status, response, "setup_id expires_at_ms poll_after_ms state") handoff := HandoffInput{SetupID: f.setup, LocalID: setup.LocalSetupID, Challenge: encode64(make([]byte, 32))} status, response = f.deviceCall("handoffs", handoff, "", true) dataOK(t, status, response, "handoff_token expires_at_ms entry_url") f.pending() status, response = f.deviceCall("claim-confirmations", f.confirm, "", true) dataOK(t, status, response, "claim_id state already_applied") f.refresh() poll := PollInput{SetupID: f.setup, BootID: f.d.BootID} status, response = f.deviceCall("poll", poll, "", true) data := dataOK(t, status, response, "state command poll_after_ms") st := data["state"].(map[string]any) ack := StateAckInput{Revision: int64(st["device_revision"].(float64)), Generation: f.d.Generation, Hash: st["state_hash"].(string), LocalGeneration: 1} ackID := uuid.NewString() status, response = f.deviceCall("state-acks", ack, ackID, true) dataOK(t, status, response, "accepted_revision setup_can_close") f.s.now = func() time.Time { return time.Now().Add(31 * time.Second) } status, response = f.deviceCall("state-acks", ack, ackID, true) if !dataOK(t, status, response, "accepted_revision setup_can_close")["setup_can_close"].(bool) { t.Fatal("ACK replay kept stale close result") } f.s.now = time.Now j := f.job() attempt := AttemptInput{ActivationID: j.ID, ConfigVersion: j.ConfigVersion, AttemptID: uuid.NewString()} status, response = f.deviceCall("activation-attempts", attempt, "", true) dataOK(t, status, response, "allowed attempt_id permit_expires_at_ms state") result := ResultInput{ActivationID: j.ID, AttemptID: attempt.AttemptID, Stage: "SDK_REGISTER", Outcome: "SUCCESS_OBSERVED"} status, response = f.deviceCall("activation-results", result, "", true) dataOK(t, status, response, "activation_id state reconcile_required") activationAck := ActivationAckInput{ActivationID: j.ID, ConfigVersion: j.ConfigVersion, LocalGeneration: 2} status, response = f.deviceCall("activation-acks", activationAck, "", true) dataOK(t, status, response, "accepted state") attempt.AttemptID = uuid.NewString() status, response = f.deviceCall("activation-attempts", attempt, "", true) dataOK(t, status, response, "allowed attempt_id permit_expires_at_ms state") result.AttemptID = attempt.AttemptID result.Stage = "CONVERSATION_TEST" status, response = f.deviceCall("activation-results", result, "", true) data = dataOK(t, status, response, "activation_id state reconcile_required") st = data["state"].(map[string]any) if st["activation_state"] != "READY" { t.Fatal("direct SDK first call not READY") } closeInput := CloseInput{SetupID: f.setup, Reason: "CANCELLED", Revision: int64(st["device_revision"].(float64))} status, response = f.deviceCall("setup-close", closeInput, "", true) dataOK(t, status, response, "close_allowed state") } func TestIntegrationDeviceRejectsRemovedAndMissingFields(t *testing.T) { f := newFixture(t) for _, body := range []map[string]any{ {"setup_id": f.setup, "boot_id": f.d.BootID}, {"setup_id": f.setup, "boot_id": f.d.BootID, "known_revision": nil}, {"setup_id": f.setup, "boot_id": f.d.BootID, "known_revision": 2147483648}, } { status, r := f.deviceCall("poll", body, "", true) if status != 400 { t.Fatal("invalid required counter", status) } fieldsEqual(t, r, "ok code request_id message") } for path, body := range map[string]map[string]any{ "activation-attempts": {"activation_id": uuid.NewString(), "config_version": 1, "attempt_id": uuid.NewString(), "stage": "SDK_REGISTER"}, "activation-results": {"activation_id": uuid.NewString(), "attempt_id": uuid.NewString(), "stage": "SDK_REGISTER", "sdk_result": 0, "outcome": "SUCCESS_OBSERVED", "evidence": map[string]any{}}, "activation-acks": {"activation_id": uuid.NewString(), "config_version": 1, "local_generation": 1, "config_digest": strings.Repeat("a", 64)}, } { status, _ := f.deviceCall(path, body, "", true) if status != 400 { t.Fatal(path, status) } } status, r := f.deviceCall("poll", PollInput{SetupID: f.setup, BootID: f.d.BootID}, "", false) if status != 400 || r["code"] != "HTTPS_REQUIRED" { t.Fatal("business HTTP accepted") } } func TestIntegrationRebootInvalidatesUncertainPermit(t *testing.T) { f := newFixture(t) f.bound() must(t, f.s.ProcessActivation(f.ctx, f.event())) f.refresh() j := f.job() input := AttemptInput{ActivationID: j.ID, ConfigVersion: j.ConfigVersion, AttemptID: uuid.NewString()} id := uuid.NewString() _, err := f.s.Attempt(f.ctx, f.d, id, requestJSON(input), input) must(t, err) boot := uuid.NewString() challenge, err := f.s.TimeChallenge(f.ctx, TimeChallengeInput{DeviceID: f.d.FactoryID, KeyVersion: 1, BootID: boot, ClientNonce: randomToken(16)}) must(t, err) raw, err := base64.RawURLEncoding.DecodeString(challenge["payload_b64"].(string)) must(t, err) var packet map[string]any must(t, json.Unmarshal(raw, &packet)) reg := RegisterInput{ChallengeID: packet["challenge_id"].(string), BootID: boot, Model: f.d.Model, HardwareRevision: f.d.HardwareRevision, FirmwareVersion: "1.2", SDKVersion: "1.0"} _, err = f.s.Register(f.ctx, f.d, uuid.NewString(), requestJSON(reg), reg) must(t, err) f.d, err = f.s.findFactoryDevice(f.ctx, f.d.FactoryID, 1) must(t, err) _, err = f.s.Attempt(f.ctx, f.d, id, requestJSON(input), input) if err == nil { t.Fatal("reboot replayed an uncertain SDK permit") } input.AttemptID = uuid.NewString() _, err = f.s.Attempt(f.ctx, f.d, uuid.NewString(), requestJSON(input), input) if err == nil { t.Fatal("reboot issued another SDK permit") } if f.job().Status != "RECONCILE_REQUIRED" { t.Fatal("reboot lost uncertainty") } }