package api import ( "context" "encoding/json" "net/http" "net/http/httptest" "strings" "testing" "time" "github.com/cosmic-clash/cosmic-clash/server/domain" ) type queueBackendSpy struct{ createCalls, heartbeatCalls, cancelCalls, getCalls int } type sessionBackendSpy struct{ calls int } func (s *sessionBackendSpy) Authenticate(_ context.Context, sessionID, _ string, _ time.Time) (domain.Session, error) { s.calls++ return domain.Session{SessionID: sessionID, PlayerID: "player-1"}, nil } type steamLoginSpy struct{ calls int } func (s *steamLoginSpy) Authenticate(_ context.Context, ticket string, _ time.Time) (domain.VerifiedIdentity, error) { s.calls++ if ticket != "valid-web-ticket" { return domain.VerifiedIdentity{}, domain.ErrTicketRejected } return domain.VerifiedIdentity{PlayerID: "player-1", SteamID: "steam-1"}, nil } func (b *queueBackendSpy) Create(_ context.Context, playerID, ticketID, _ string, spec domain.QueueSpec, now time.Time) (domain.QueueTicket, error) { b.createCalls++ return domain.QueueTicket{TicketID: ticketID, PlayerID: playerID, Playlist: spec.Playlist, State: domain.Queued, EnqueuedAt: now, ExpiresAt: now.Add(domain.QueueExpiryWindow)}, nil } func (b *queueBackendSpy) Heartbeat(_ context.Context, playerID, ticketID, _ string, revision uint64, now time.Time) (domain.QueueTicket, error) { b.heartbeatCalls++ return domain.QueueTicket{TicketID: ticketID, PlayerID: playerID, State: domain.Queued, Revision: revision + 1, EnqueuedAt: now, ExpiresAt: now.Add(domain.QueueExpiryWindow)}, nil } func (b *queueBackendSpy) Cancel(_ context.Context, playerID, ticketID, _ string, revision uint64, now time.Time) (domain.QueueTicket, error) { b.cancelCalls++ return domain.QueueTicket{TicketID: ticketID, PlayerID: playerID, State: domain.Cancelled, Revision: revision + 1, EnqueuedAt: now, ExpiresAt: now}, nil } func (b *queueBackendSpy) Get(_ context.Context, playerID, ticketID string, now time.Time) (domain.QueueTicket, error) { b.getCalls++ return domain.QueueTicket{TicketID: ticketID, PlayerID: playerID, State: domain.Queued, EnqueuedAt: now, ExpiresAt: now.Add(domain.QueueExpiryWindow)}, nil } func TestAuthenticatedQueueAPIUsesServerCandidateAndRevisionedMutations(t *testing.T) { now := time.Unix(1000, 0).UTC() sessions := domain.NewSessionStore() session, token, err := sessions.Issue("player-1", time.Hour, now) if err != nil { t.Fatal(err) } queue := domain.NewQueue() service := &Service{Sessions: sessions, Queue: queue, Now: func() time.Time { return now }, Candidate: func(playerID, ticketID string) (domain.Candidate, error) { return domain.Candidate{PlayerID: playerID, TicketID: ticketID, EnqueuedAt: now, PredictedRTT: map[string]float64{"EU": 20}}, nil }} server := httptest.NewServer(service.Handler()) defer server.Close() request := func(method, path, body string, headers map[string]string) *http.Response { req, _ := http.NewRequest(method, server.URL+path, strings.NewReader(body)) for key, value := range headers { req.Header.Set(key, value) } response, requestErr := http.DefaultClient.Do(req) if requestErr != nil { t.Fatal(requestErr) } return response } headers := map[string]string{"Authorization": "Bearer " + session.SessionID + ":" + token, "Idempotency-Key": "create-key-123456"} response := request(http.MethodPost, "/v1/queue", `{"ticket_id":"ticket-1","playlist":"casual","client_build":"build-1","protocol_version":1}`, headers) if response.StatusCode != http.StatusCreated { t.Fatalf("create status = %d", response.StatusCode) } var created queueResponse if err := json.NewDecoder(response.Body).Decode(&created); err != nil { t.Fatal(err) } _ = response.Body.Close() if created.PlayerID != "player-1" || created.State != "QUEUED" || created.Revision != 0 { t.Fatalf("created = %+v", created) } response = request(http.MethodPost, "/v1/queue/ticket-1/heartbeat", `{}`, map[string]string{"Authorization": headers["Authorization"], "Idempotency-Key": "heartbeat-key-123456", "If-Match-Revision": "0"}) if response.StatusCode != http.StatusOK { t.Fatalf("heartbeat status = %d", response.StatusCode) } _ = response.Body.Close() response = request(http.MethodPost, "/v1/queue/ticket-1/cancel", `{}`, map[string]string{"Authorization": headers["Authorization"], "Idempotency-Key": "cancel-key-123456", "If-Match-Revision": "0"}) if response.StatusCode != http.StatusConflict { t.Fatalf("stale cancel status = %d", response.StatusCode) } _ = response.Body.Close() } func TestQueueAPIRejectsUnauthenticatedUnknownAndOversizedInput(t *testing.T) { service := &Service{Sessions: domain.NewSessionStore(), Queue: domain.NewQueue(), Candidate: func(string, string) (domain.Candidate, error) { return domain.Candidate{}, nil }} server := httptest.NewServer(service.Handler()) defer server.Close() request, _ := http.NewRequest(http.MethodPost, server.URL+"/v1/queue", strings.NewReader(`{"ticket_id":"ticket-1","playlist":"casual","client_build":"build-1","protocol_version":1,"player_id":"attacker"}`)) request.Header.Set("Idempotency-Key", "create-key-123456") response, err := http.DefaultClient.Do(request) if err != nil { t.Fatal(err) } if response.StatusCode != http.StatusUnauthorized { t.Fatalf("unauthenticated status = %d", response.StatusCode) } _ = response.Body.Close() sessionStore := domain.NewSessionStore() session, token, _ := sessionStore.Issue("player-1", time.Hour, time.Now()) service.Sessions = sessionStore request, _ = http.NewRequest(http.MethodPost, server.URL+"/v1/queue", strings.NewReader(`{"ticket_id":"ticket-1","playlist":"casual","client_build":"build-1","protocol_version":1,"unknown":true}`)) request.Header.Set("Authorization", "Bearer "+session.SessionID+":"+token) request.Header.Set("Idempotency-Key", "create-key-123456") response, err = http.DefaultClient.Do(request) if err != nil { t.Fatal(err) } if response.StatusCode != http.StatusBadRequest { t.Fatalf("unknown field status = %d", response.StatusCode) } _ = response.Body.Close() request, _ = http.NewRequest(http.MethodPost, server.URL+"/v1/queue", strings.NewReader(`{"ticket_id":`)) request.Header.Set("Authorization", "Bearer "+session.SessionID+":"+token) request.Header.Set("Idempotency-Key", "create-key-654321") response, err = http.DefaultClient.Do(request) if err != nil { t.Fatal(err) } if response.StatusCode != http.StatusBadRequest { t.Fatalf("malformed body status = %d", response.StatusCode) } _ = response.Body.Close() request, _ = http.NewRequest(http.MethodPost, server.URL+"/v1/queue", strings.NewReader(`{"ticket_id":"ticket-1","playlist":"casual","client_build":"build-1","protocol_version":1}{"ticket_id":"ticket-2"}`)) request.Header.Set("Authorization", "Bearer "+session.SessionID+":"+token) request.Header.Set("Idempotency-Key", "create-key-789012") response, err = http.DefaultClient.Do(request) if err != nil { t.Fatal(err) } if response.StatusCode != http.StatusBadRequest { t.Fatalf("trailing JSON status = %d", response.StatusCode) } _ = response.Body.Close() } func TestQueueCreateRequiresCompatibilityMetadataAndPassesItToProvider(t *testing.T) { now := time.Unix(1000, 0).UTC() sessions := domain.NewSessionStore() session, token, err := sessions.Issue("player-1", time.Hour, now) if err != nil { t.Fatal(err) } var got domain.QueueSpec service := &Service{ Sessions: sessions, Queue: domain.NewQueue(), Now: func() time.Time { return now }, CandidateV2: func(_ string, ticketID string, spec domain.QueueSpec) (domain.Candidate, error) { got = spec return domain.Candidate{PlayerID: "player-1", TicketID: ticketID, Playlist: spec.Playlist, ClientBuild: spec.ClientBuild, ProtocolVersion: spec.ProtocolVersion, EnqueuedAt: now}, nil }, } server := httptest.NewServer(service.Handler()) defer server.Close() request := func(body string) *http.Response { req, _ := http.NewRequest(http.MethodPost, server.URL+"/v1/queue", strings.NewReader(body)) req.Header.Set("Authorization", "Bearer "+session.SessionID+":"+token) req.Header.Set("Idempotency-Key", "create-key-123456") response, requestErr := http.DefaultClient.Do(req) if requestErr != nil { t.Fatal(requestErr) } return response } response := request(`{"ticket_id":"ticket-1"}`) if response.StatusCode != http.StatusBadRequest { t.Fatalf("missing metadata status = %d", response.StatusCode) } _ = response.Body.Close() response = request(`{"ticket_id":"ticket-1","playlist":"invalid","client_build":"build-1","protocol_version":1}`) if response.StatusCode != http.StatusBadRequest { t.Fatalf("invalid playlist status = %d", response.StatusCode) } _ = response.Body.Close() response = request(`{"ticket_id":"ticket-1","playlist":"ranked","client_build":"build-1","protocol_version":7}`) if response.StatusCode != http.StatusCreated { t.Fatalf("valid metadata status = %d", response.StatusCode) } _ = response.Body.Close() if got.Playlist != domain.Ranked || got.ClientBuild != "build-1" || got.ProtocolVersion != 7 { t.Fatalf("provider received %+v", got) } } func TestQueueCreateRejectsCandidateMetadataMismatch(t *testing.T) { now := time.Unix(1000, 0).UTC() sessions := domain.NewSessionStore() session, token, _ := sessions.Issue("player-1", time.Hour, now) service := &Service{Sessions: sessions, Queue: domain.NewQueue(), Now: func() time.Time { return now }, CandidateV2: func(_ string, ticketID string, spec domain.QueueSpec) (domain.Candidate, error) { spec.ClientBuild = "tampered" return domain.Candidate{PlayerID: "player-1", TicketID: ticketID, Playlist: spec.Playlist, ClientBuild: spec.ClientBuild, ProtocolVersion: spec.ProtocolVersion, EnqueuedAt: now}, nil }} server := httptest.NewServer(service.Handler()) defer server.Close() req, _ := http.NewRequest(http.MethodPost, server.URL+"/v1/queue", strings.NewReader(`{"ticket_id":"ticket-1","playlist":"ranked","client_build":"build-1","protocol_version":1}`)) req.Header.Set("Authorization", "Bearer "+session.SessionID+":"+token) req.Header.Set("Idempotency-Key", "create-key-123456") response, err := http.DefaultClient.Do(req) if err != nil { t.Fatal(err) } defer response.Body.Close() if response.StatusCode != http.StatusUnprocessableEntity { t.Fatalf("mismatch status = %d", response.StatusCode) } } func TestQueueAPIUsesInjectedPersistentBackendWithoutCandidateProvider(t *testing.T) { now := time.Unix(1000, 0).UTC() sessions := domain.NewSessionStore() session, token, _ := sessions.Issue("player-1", time.Hour, now) backend := &queueBackendSpy{} service := &Service{Sessions: sessions, QueueBackend: backend, Now: func() time.Time { return now }} server := httptest.NewServer(service.Handler()) defer server.Close() req, _ := http.NewRequest(http.MethodPost, server.URL+"/v1/queue", strings.NewReader(`{"ticket_id":"ticket-1","playlist":"ranked","client_build":"build-1","protocol_version":1}`)) req.Header.Set("Authorization", "Bearer "+session.SessionID+":"+token) req.Header.Set("Idempotency-Key", "create-key-123456") response, err := http.DefaultClient.Do(req) if err != nil { t.Fatal(err) } defer response.Body.Close() if response.StatusCode != http.StatusCreated || backend.createCalls != 1 { t.Fatalf("status=%d backend_calls=%d", response.StatusCode, backend.createCalls) } } func TestQueueAPIDelegatesAllMutationsAndRecoveryToBackend(t *testing.T) { now := time.Unix(1000, 0).UTC() sessions := domain.NewSessionStore() session, token, _ := sessions.Issue("player-1", time.Hour, now) backend := &queueBackendSpy{} service := &Service{Sessions: sessions, QueueBackend: backend, Now: func() time.Time { return now }} server := httptest.NewServer(service.Handler()) defer server.Close() auth := "Bearer " + session.SessionID + ":" + token request := func(method, path, body, key, revision string) *http.Response { req, _ := http.NewRequest(method, server.URL+path, strings.NewReader(body)) req.Header.Set("Authorization", auth) if key != "" { req.Header.Set("Idempotency-Key", key) } if revision != "" { req.Header.Set("If-Match-Revision", revision) } response, err := http.DefaultClient.Do(req) if err != nil { t.Fatal(err) } return response } response := request(http.MethodGet, "/v1/queue/ticket-1", "", "", "") if response.StatusCode != http.StatusOK { t.Fatalf("get status = %d", response.StatusCode) } response.Body.Close() response = request(http.MethodPost, "/v1/queue/ticket-1/heartbeat", `{}`, "heartbeat-key-123456", "0") if response.StatusCode != http.StatusOK { t.Fatalf("heartbeat status = %d", response.StatusCode) } response.Body.Close() response = request(http.MethodPost, "/v1/queue/ticket-1/cancel", `{}`, "cancel-key-123456", "1") if response.StatusCode != http.StatusOK { t.Fatalf("cancel status = %d", response.StatusCode) } response.Body.Close() if backend.getCalls != 1 || backend.heartbeatCalls != 1 || backend.cancelCalls != 1 { t.Fatalf("backend calls = %+v", backend) } } func TestQueueAPIUsesInjectedSessionBackend(t *testing.T) { backend := &sessionBackendSpy{} queue := &queueBackendSpy{} service := &Service{SessionBackend: backend, QueueBackend: queue, Now: func() time.Time { return time.Unix(1000, 0).UTC() }} server := httptest.NewServer(service.Handler()) defer server.Close() req, _ := http.NewRequest(http.MethodGet, server.URL+"/v1/queue/ticket-1", nil) req.Header.Set("Authorization", "Bearer durable-session:durable-token") response, err := http.DefaultClient.Do(req) if err != nil { t.Fatal(err) } defer response.Body.Close() if response.StatusCode != http.StatusOK || backend.calls != 1 || queue.getCalls != 1 { t.Fatalf("status=%d session_calls=%d queue_calls=%d", response.StatusCode, backend.calls, queue.getCalls) } } func TestSteamSessionAPIRequiresBackendVerificationAndIssuesOpaqueSession(t *testing.T) { now := time.Unix(1000, 0).UTC() sessions := domain.NewSessionStore() provider := &steamLoginSpy{} service := &Service{Sessions: sessions, SteamLogin: provider, Now: func() time.Time { return now }} server := httptest.NewServer(service.Handler()) defer server.Close() request := func(body string) *http.Response { req, _ := http.NewRequest(http.MethodPost, server.URL+"/v1/session/steam", strings.NewReader(body)) response, err := http.DefaultClient.Do(req) if err != nil { t.Fatal(err) } return response } response := request(`{"web_api_ticket":"valid-web-ticket","steam_id":"spoofed"}`) if response.StatusCode != http.StatusBadRequest { t.Fatalf("extra field status = %d", response.StatusCode) } response.Body.Close() response = request(`{"web_api_ticket":"invalid"}`) if response.StatusCode != http.StatusUnauthorized { t.Fatalf("invalid ticket status = %d", response.StatusCode) } response.Body.Close() response = request(`{"web_api_ticket":"valid-web-ticket"}`) if response.StatusCode != http.StatusOK { t.Fatalf("valid ticket status = %d", response.StatusCode) } var result struct { PlayerID string `json:"player_id"` AccessToken string `json:"access_token"` } if err := json.NewDecoder(response.Body).Decode(&result); err != nil { t.Fatal(err) } response.Body.Close() if result.PlayerID != "player-1" || !strings.Contains(result.AccessToken, ":") || provider.calls != 2 { t.Fatalf("session result=%+v provider_calls=%d", result, provider.calls) } } func TestQueueRecoveryAPIIsAuthenticatedOwnerOnlyAndExpiresStaleTickets(t *testing.T) { now := time.Unix(1000, 0).UTC() sessions := domain.NewSessionStore() ownerSession, ownerToken, err := sessions.Issue("player-1", time.Hour, now) if err != nil { t.Fatal(err) } otherSession, otherToken, err := sessions.Issue("player-2", time.Hour, now) if err != nil { t.Fatal(err) } queue := domain.NewQueue() service := &Service{Sessions: sessions, Queue: queue, Now: func() time.Time { return now }, Candidate: func(playerID, ticketID string) (domain.Candidate, error) { return domain.Candidate{PlayerID: playerID, TicketID: ticketID, EnqueuedAt: now}, nil }} server := httptest.NewServer(service.Handler()) defer server.Close() create, _ := http.NewRequest(http.MethodPost, server.URL+"/v1/queue", strings.NewReader(`{"ticket_id":"ticket-recovery-123456","playlist":"casual","client_build":"build-1","protocol_version":1}`)) create.Header.Set("Authorization", "Bearer "+ownerSession.SessionID+":"+ownerToken) create.Header.Set("Idempotency-Key", "queue-create-recovery-123456") response, err := http.DefaultClient.Do(create) if err != nil || response.StatusCode != http.StatusCreated { t.Fatalf("create status=%v err=%v", response.StatusCode, err) } _ = response.Body.Close() get, _ := http.NewRequest(http.MethodGet, server.URL+"/v1/queue/ticket-recovery-123456", nil) get.Header.Set("Authorization", "Bearer "+ownerSession.SessionID+":"+ownerToken) response, err = http.DefaultClient.Do(get) if err != nil || response.StatusCode != http.StatusOK { t.Fatalf("owner recovery status=%v err=%v", response.StatusCode, err) } _ = response.Body.Close() get.Header.Set("Authorization", "Bearer "+otherSession.SessionID+":"+otherToken) response, err = http.DefaultClient.Do(get) if err != nil || response.StatusCode != http.StatusForbidden { t.Fatalf("cross-player recovery status=%v err=%v", response.StatusCode, err) } _ = response.Body.Close() service.Now = func() time.Time { return now.Add(domain.QueueExpiryWindow) } get.Header.Set("Authorization", "Bearer "+ownerSession.SessionID+":"+ownerToken) response, err = http.DefaultClient.Do(get) if err != nil || response.StatusCode != http.StatusGone { t.Fatalf("expired recovery status=%v err=%v", response.StatusCode, err) } _ = response.Body.Close() } func TestAuthenticatedProposalAPIUsesRevisionAndIdempotencyPolicy(t *testing.T) { now := time.Unix(1000, 0).UTC() sessions := domain.NewSessionStore() session, token, err := sessions.Issue("player-a", time.Hour, now) if err != nil { t.Fatal(err) } proposal, err := domain.NewProposal("proposal-123456789", domain.Casual, []string{"player-a", "player-b"}, now) if err != nil { t.Fatal(err) } service := &Service{Sessions: sessions, Proposals: map[string]*domain.Proposal{proposal.ProposalID: &proposal}, Now: func() time.Time { return now }} server := httptest.NewServer(service.Handler()) defer server.Close() req, _ := http.NewRequest(http.MethodPost, server.URL+"/v1/proposals/"+proposal.ProposalID+"/accept", nil) req.Header.Set("Authorization", "Bearer "+session.SessionID+":"+token) req.Header.Set("Idempotency-Key", "proposal-response-123456") req.Header.Set("If-Match-Revision", "0") response, err := http.DefaultClient.Do(req) if err != nil { t.Fatal(err) } if response.StatusCode != http.StatusOK { t.Fatalf("proposal accept status = %d", response.StatusCode) } _ = response.Body.Close() req, _ = http.NewRequest(http.MethodPost, server.URL+"/v1/proposals/"+proposal.ProposalID+"/accept", nil) req.Header.Set("Authorization", "Bearer "+session.SessionID+":"+token) req.Header.Set("Idempotency-Key", "proposal-response-654321") req.Header.Set("If-Match-Revision", "0") response, err = http.DefaultClient.Do(req) if err != nil { t.Fatal(err) } if response.StatusCode != http.StatusConflict { t.Fatalf("stale proposal response status = %d", response.StatusCode) } _ = response.Body.Close() } func TestRankedProfileAPIReturnsBackendTierAndHidesCasualData(t *testing.T) { now := time.Unix(1000, 0).UTC() sessions := domain.NewSessionStore() session, token, err := sessions.Issue("player-a", time.Hour, now) if err != nil { t.Fatal(err) } policy, err := domain.NewTierPolicy([]domain.TierBand{{Tier: domain.RankTierBronze, MinRating: 0}, {Tier: domain.RankTierGold, MinRating: 1500}}) if err != nil { t.Fatal(err) } service := &Service{ Sessions: sessions, RankedProfiles: map[string]domain.RankedProfile{"player-a": {Rating: domain.Rating{Value: 1600, RD: 200, Volatility: 0.06}, RankedGames: 10, LastSeasonID: "season-1"}}, TierPolicy: policy, Now: func() time.Time { return now }, } server := httptest.NewServer(service.Handler()) defer server.Close() req, _ := http.NewRequest(http.MethodGet, server.URL+"/v1/profile/ranked", nil) req.Header.Set("Authorization", "Bearer "+session.SessionID+":"+token) response, err := http.DefaultClient.Do(req) if err != nil { t.Fatal(err) } defer response.Body.Close() if response.StatusCode != http.StatusOK { t.Fatalf("ranked profile status = %d", response.StatusCode) } var body rankedProfileResponse if err := json.NewDecoder(response.Body).Decode(&body); err != nil { t.Fatal(err) } if body.Tier != string(domain.RankTierGold) || body.Provisional || body.RankedGames != 10 || body.SeasonID != "season-1" { t.Fatalf("ranked profile response = %+v", body) } } func TestProbeAPIUsesServerEvidenceAndRejectsClientRTTField(t *testing.T) { now := time.Unix(1000, 0).UTC() sessions := domain.NewSessionStore() session, token, err := sessions.Issue("player-a", time.Hour, now) if err != nil { t.Fatal(err) } called := false service := &Service{Sessions: sessions, Now: func() time.Time { return now }, Probe: func(playerID, region string, location, nonce []byte, receivedAt time.Time) (domain.ProbeEvidence, []byte, error) { called = true if playerID != "player-a" || region != "EU" || string(location) != "opaque" || string(nonce) != "nonce" || !receivedAt.Equal(now) { t.Fatalf("probe provider arguments = %q %s %q %q %v", playerID, region, location, nonce, receivedAt) } return domain.ProbeEvidence{OpaqueLocation: location, Nonce: nonce, IssuedAt: now, Region: region, ServerRTT: 40 * time.Millisecond}, nonce, nil }} server := httptest.NewServer(service.Handler()) defer server.Close() request := `{"opaque_location":"b3BhcXVl","nonce":"bm9uY2U="}` req, _ := http.NewRequest(http.MethodPost, server.URL+"/v1/probes/EU", strings.NewReader(request)) req.Header.Set("Authorization", "Bearer "+session.SessionID+":"+token) response, err := http.DefaultClient.Do(req) if err != nil || response.StatusCode != http.StatusAccepted || !called { t.Fatalf("valid probe status=%v err=%v called=%v", response.StatusCode, err, called) } _ = response.Body.Close() request = `{"opaque_location":"b3BhcXVl","nonce":"bm9uY2U=","server_rtt_ms":1}` req, _ = http.NewRequest(http.MethodPost, server.URL+"/v1/probes/EU", strings.NewReader(request)) req.Header.Set("Authorization", "Bearer "+session.SessionID+":"+token) response, err = http.DefaultClient.Do(req) if err != nil || response.StatusCode != http.StatusBadRequest { t.Fatalf("client RTT field status=%v err=%v", response.StatusCode, err) } _ = response.Body.Close() } func TestProposalRecoveryIsParticipantScopedAndExpiresAtReadBoundary(t *testing.T) { now := time.Unix(1000, 0).UTC() sessions := domain.NewSessionStore() owner, ownerToken, err := sessions.Issue("player-a", time.Hour, now) if err != nil { t.Fatal(err) } other, otherToken, err := sessions.Issue("player-z", time.Hour, now) if err != nil { t.Fatal(err) } proposal, err := domain.NewProposal("proposal-recovery", domain.Casual, []string{"player-a", "player-b"}, now) if err != nil { t.Fatal(err) } current := now service := &Service{Sessions: sessions, Proposals: map[string]*domain.Proposal{proposal.ProposalID: &proposal}, Now: func() time.Time { return current }} server := httptest.NewServer(service.Handler()) defer server.Close() get := func(session domain.Session, token string) (int, proposalResponse) { req, _ := http.NewRequest(http.MethodGet, server.URL+"/v1/proposals/proposal-recovery", nil) req.Header.Set("Authorization", "Bearer "+session.SessionID+":"+token) response, requestErr := http.DefaultClient.Do(req) if requestErr != nil { t.Fatal(requestErr) } defer response.Body.Close() var body proposalResponse if response.StatusCode == http.StatusOK { if err := json.NewDecoder(response.Body).Decode(&body); err != nil { t.Fatal(err) } } return response.StatusCode, body } status, recovered := get(owner, ownerToken) if status != http.StatusOK || recovered.State != string(domain.Open) || recovered.Revision != 0 { t.Fatalf("owner recovery status=%d body=%+v", status, recovered) } status, _ = get(other, otherToken) if status != http.StatusNotFound { t.Fatalf("non-participant recovery status=%d, want 404", status) } current = now.Add(domain.ProposalWindow) status, recovered = get(owner, ownerToken) if status != http.StatusOK || recovered.State != string(domain.Expired) || recovered.Revision != 1 { t.Fatalf("expired recovery status=%d body=%+v", status, recovered) } }