feat: persist participant-scoped proposal recovery

This commit is contained in:
Josh Creek
2026-09-01 07:50:11 +01:00
parent 1eb6e8f9f4
commit 30b4560bd5
7 changed files with 217 additions and 20 deletions
+56
View File
@@ -21,6 +21,19 @@ type queueBackendSpy struct{ createCalls, heartbeatCalls, cancelCalls, getCalls
type sessionBackendSpy struct{ calls int }
type proposalBackendSpy struct {
proposal domain.Proposal
calls int
}
func (b *proposalBackendSpy) Get(_ context.Context, playerID, _ string, _ time.Time) (domain.Proposal, error) {
b.calls++
if !b.proposal.HasParticipant(playerID) {
return domain.Proposal{}, domain.ErrNotParticipant
}
return b.proposal, nil
}
func (s *sessionBackendSpy) Authenticate(_ context.Context, sessionID, _ string, _ time.Time) (domain.Session, error) {
s.calls++
return domain.Session{SessionID: sessionID, PlayerID: "player-1"}, nil
@@ -341,6 +354,49 @@ func TestStateChangingAPIActionsPublishTargetedEvents(t *testing.T) {
}
}
func TestProposalRecoveryUsesDurableBackendAndRemainsParticipantScoped(t *testing.T) {
now := time.Unix(1000, 0).UTC()
proposal, err := domain.NewProposal("proposal-1234567890123456", domain.Casual, []string{"player-1", "player-2"}, now)
if err != nil {
t.Fatal(err)
}
backend := &proposalBackendSpy{proposal: proposal}
sessions := domain.NewSessionStore()
participantSession, participantToken, err := sessions.Issue("player-1", time.Hour, now)
if err != nil {
t.Fatal(err)
}
outsiderSession, outsiderToken, err := sessions.Issue("outsider", time.Hour, now)
if err != nil {
t.Fatal(err)
}
service := &Service{Sessions: sessions, Proposals: map[string]*domain.Proposal{}, ProposalBackend: backend, Now: func() time.Time { return now }}
server := httptest.NewServer(service.Handler())
defer server.Close()
get := func(session domain.Session, token string) int {
request, err := http.NewRequest(http.MethodGet, server.URL+"/v1/proposals/"+proposal.ProposalID, nil)
if err != nil {
t.Fatal(err)
}
request.Header.Set("Authorization", "Bearer "+session.SessionID+":"+token)
response, err := server.Client().Do(request)
if err != nil {
t.Fatal(err)
}
_ = response.Body.Close()
return response.StatusCode
}
if status := get(participantSession, participantToken); status != http.StatusOK {
t.Fatalf("participant recovery status = %d", status)
}
if status := get(outsiderSession, outsiderToken); status != http.StatusNotFound {
t.Fatalf("outsider recovery status = %d", status)
}
if backend.calls != 2 {
t.Fatalf("durable backend calls = %d", backend.calls)
}
}
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())