package main // Pairing-code flow integration tests. The register response mints a // 6-digit code shown on the registering device's screen; the person // holding the device types it into THEIR account (POST /api/devices/claim). // This replaces the retired broadcast-list approve, where // first-click-wins let any account claim (or reject) a device it could // see but not name. // // Devices claimed here land on the fresh per-run regular user, so the // harness cascade removes them; identifiers are uuid-suffixed to stay // clear of the UNIQUE(device_identifier) index across runs. import ( "bookhoard/internal/database" "bytes" "context" "encoding/json" "fmt" "net/http" "net/http/httptest" "strings" "testing" "github.com/google/uuid" "github.com/jackc/pgx/v5/pgtype" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // registerPairingDevice initiates a registration and returns its decoded // response (registration_id, pairing_code, ...). func registerPairingDevice(t *testing.T, setup *TestServerSetup, identifier string) map[string]interface{} { t.Helper() regRequest := map[string]interface{}{ "device_name": "Test Pairing Device", "device_type": "koreader", "device_identifier": identifier, } regBody, _ := json.Marshal(regRequest) req := httptest.NewRequest("POST", "/api/devices/register", bytes.NewReader(regBody)) req.Header.Set("Content-Type", "application/json") rec := httptest.NewRecorder() setup.Server.Config.Handler.ServeHTTP(rec, req) require.Equal(t, http.StatusCreated, rec.Code, "Should initiate registration") var regResponse map[string]interface{} json.Unmarshal(rec.Body.Bytes(), ®Response) return regResponse } // pollRegistrationStatus hits the device-side status endpoint (no auth — // the registration UUID is the bearer). func pollRegistrationStatus(t *testing.T, setup *TestServerSetup, registrationID string) (int, map[string]interface{}) { t.Helper() body, _ := json.Marshal(map[string]string{"registration_id": registrationID}) req := httptest.NewRequest("POST", "/api/devices/register/status", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") rec := httptest.NewRecorder() setup.Server.Config.Handler.ServeHTTP(rec, req) var resp map[string]interface{} json.Unmarshal(rec.Body.Bytes(), &resp) return rec.Code, resp } func pairingCodeBody(code string) []byte { body, _ := json.Marshal(map[string]string{"pairing_code": code}) return body } // pairingLookupOrClaim POSTs to the given claim-family endpoint with the // given bearer token and returns status + decoded body. func pairingLookupOrClaim(t *testing.T, setup *TestServerSetup, path, token, code string) (int, map[string]interface{}) { t.Helper() req := httptest.NewRequest("POST", path, bytes.NewReader(pairingCodeBody(code))) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+token) rec := httptest.NewRecorder() setup.Server.Config.Handler.ServeHTTP(rec, req) var resp map[string]interface{} json.Unmarshal(rec.Body.Bytes(), &resp) return rec.Code, resp } // wrongPairingCode derives a code guaranteed to differ from the target's // digits (rotate each digit +1 mod 10). func wrongPairingCode(code string) string { rotated := make([]byte, len(code)) for i := 0; i < len(code); i++ { rotated[i] = byte('0' + (int(code[i]-'0')+1)%10) } return string(rotated) } // pairingFormRequest POSTs urlencoded form data — the Content-Type the // browser's htmx forms actually send — with a Bearer token. func pairingFormRequest(t *testing.T, setup *TestServerSetup, path, token, body string) (int, map[string]interface{}) { t.Helper() req := httptest.NewRequest("POST", path, strings.NewReader(body)) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") if token != "" { req.Header.Set("Authorization", "Bearer "+token) } rec := httptest.NewRecorder() setup.Server.Config.Handler.ServeHTTP(rec, req) var resp map[string]interface{} json.Unmarshal(rec.Body.Bytes(), &resp) return rec.Code, resp } // TestPairingClaimAcceptsBrowserFormEncoding pins the htmx path: the // confirm card claims via a urlencoded