package wristband import ( "context" "errors" "strings" "testing" ) // newConsentFixture builds a Server over the in-memory backend with one // usable client and one pending request owned by no one yet (userID 0 // means "unconsented", matching Authorize's freshly created row). func newConsentFixture(t *testing.T) (*Server, string) { t.Helper() s := NewServer(DefaultOptions()) s.opts.Issuer = "https://plytarium-consent-test.example" b := newMemoryBackend() s.SetBackend(b) ctx := context.Background() err := b.WithinTx(ctx, func(tx Tx) error { client := &ClientRecord{ ClientID: "cli-consent-test", ClientName: "Test Client", RedirectURIs: []string{"https://client.example.test/cb"}, TokenEndpointAuthMethod: "none", } if err := tx.CreateWithCap(ctx, client, 200); err != nil { return err } state := "consent-test-state" pending := &AuthCodeRecord{ ClientID: client.ClientID, RedirectURI: "https://client.example.test/cb", Scopes: []string{"read", "write"}, CodeChallenge: strings.Repeat("a", 43), CodeChallengeMethod: "S256", State: &state, ExpiresAt: s.now().Add(s.opts.PendingRequestTTL), } requestID, err := s.randomBytes(32) if err != nil { return err } pending.RequestID = &requestID return tx.CreatePending(ctx, pending) }) if err != nil { t.Fatal(err) } var requestID string for _, c := range b.codes { if c.RequestID != nil { requestID = *c.RequestID } } return s, requestID } func TestPendingRequestReturnsClientAndScopes(t *testing.T) { s, requestID := newConsentFixture(t) view, err := s.PendingRequest(context.Background(), requestID, 1) if err != nil { t.Fatalf("PendingRequest: %v", err) } if view.ClientName != "Test Client" { t.Fatalf("ClientName = %q, want %q", view.ClientName, "Test Client") } if view.RedirectHost != "client.example.test" { t.Fatalf("RedirectHost = %q, want client.example.test", view.RedirectHost) } if len(view.ScopesRequested) != 2 || view.ScopesRequested[0] != "read" || view.ScopesRequested[1] != "write" { t.Fatalf("ScopesRequested = %v, want [read write]", view.ScopesRequested) } } func TestPendingRequestMissingHandleIsNotFound(t *testing.T) { s, _ := newConsentFixture(t) _, err := s.PendingRequest(context.Background(), "does-not-exist", 1) if !errors.Is(err, ErrPendingNotFound) { t.Fatalf("err = %v, want ErrPendingNotFound", err) } } // TestPendingRequestForeignOwnerIsNotFound proves T-08-CROSS-USER: once a // pending row is bound to one user (by a prior consent attempt), a // different user's lookup is indistinguishable from missing. func TestPendingRequestForeignOwnerIsNotFound(t *testing.T) { s, requestID := newConsentFixture(t) ctx := context.Background() if _, err := s.IssueCode(ctx, requestID, 1, []string{"read"}, []uint{9}); err != nil { t.Fatalf("IssueCode: %v", err) } // The code is now issued (CodeHash set); a second lookup by anyone, // including the original owner, must miss (single-use). if _, err := s.PendingRequest(ctx, requestID, 1); !errors.Is(err, ErrPendingNotFound) { t.Fatalf("err = %v, want ErrPendingNotFound after issuance", err) } } func TestIssueCodeGrantsOnlySubmittedScopesAndReturnsOrderedRedirect(t *testing.T) { s, requestID := newConsentFixture(t) ctx := context.Background() redirectTo, err := s.IssueCode(ctx, requestID, 42, []string{"read"}, []uint{7}) if err != nil { t.Fatalf("IssueCode: %v", err) } if !strings.HasPrefix(redirectTo, "https://client.example.test/cb?code=") { t.Fatalf("redirectTo = %q, want code= prefix", redirectTo) } if !strings.Contains(redirectTo, "&iss=") || !strings.Contains(redirectTo, "&state=consent-test-state") { t.Fatalf("redirectTo = %q, want ordered iss/state", redirectTo) } } func TestIssueCodeEmptyGrantedScopesIsRejected(t *testing.T) { s, requestID := newConsentFixture(t) if _, err := s.IssueCode(context.Background(), requestID, 1, nil, nil); !errors.Is(err, ErrNoGrantableScopes) { t.Fatalf("err = %v, want ErrNoGrantableScopes", err) } } func TestDenyPendingConsumesAndReturnsAccessDeniedRedirect(t *testing.T) { s, requestID := newConsentFixture(t) ctx := context.Background() redirectTo, err := s.DenyPending(ctx, requestID, 1) if err != nil { t.Fatalf("DenyPending: %v", err) } if !strings.Contains(redirectTo, "error=access_denied") { t.Fatalf("redirectTo = %q, want error=access_denied", redirectTo) } // A second action on the same handle (allow or deny) is not found // (single-use), never a distinguishable 403. if _, err := s.DenyPending(ctx, requestID, 1); !errors.Is(err, ErrPendingNotFound) { t.Fatalf("second deny err = %v, want ErrPendingNotFound", err) } }