- Move remaining beach packages and embedded admin assets\n- Rewrite framework, example, build, and gate paths
140 lines
4.7 KiB
Go
140 lines
4.7 KiB
Go
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)
|
|
}
|
|
}
|