~bigbes/lethe

ref: 57c0d49169c3d3cdc6da22b536fb19a4e1433a16 lethe/internal/testutil/oidcstub/codestore_test.go -rw-r--r-- 3.0 KiB
57c0d491 — Eugene Blikh collector: persist skipped-only parser progress 24 days ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
package oidcstub

import (
	"strings"
	"sync"
	"testing"
	"time"
)

// TestCodeStore_Issue_DistinctURLSafeCodes verifies that Issue returns distinct,
// URL-safe codes with sufficient entropy (32 bytes → 43-char base64url string).
func TestCodeStore_Issue_DistinctURLSafeCodes(t *testing.T) {
	cs := newCodeStore(nil)
	seen := make(map[string]bool)
	for i := 0; i < 20; i++ {
		code := cs.Issue("sub", "challenge", "http://x/cb", 5*time.Minute)
		if code == "" {
			t.Fatal("Issue returned empty code")
		}
		// URL-safe: must not contain +, /, or =
		if strings.ContainsAny(code, "+/=") {
			t.Errorf("code %q is not URL-safe (contains +, /, or =)", code)
		}
		// 32 bytes base64url-encoded without padding = 43 chars
		if len(code) < 43 {
			t.Errorf("code %q too short (len=%d, want >=43)", code, len(code))
		}
		if seen[code] {
			t.Errorf("duplicate code issued: %q", code)
		}
		seen[code] = true
	}
}

// TestCodeStore_Consume_SingleUse verifies IV2: code is deleted on first Consume.
func TestCodeStore_Consume_SingleUse(t *testing.T) {
	cs := newCodeStore(nil)
	code := cs.Issue("alice", "challenge", "http://x/cb", 5*time.Minute)

	entry, ok := cs.Consume(code)
	if !ok {
		t.Fatal("Consume: expected ok=true on first call")
	}
	if entry.Sub != "alice" {
		t.Errorf("entry.Sub = %q; want alice", entry.Sub)
	}
	if entry.CodeChallenge != "challenge" {
		t.Errorf("entry.CodeChallenge = %q; want challenge", entry.CodeChallenge)
	}
	if entry.RedirectURI != "http://x/cb" {
		t.Errorf("entry.RedirectURI = %q; want http://x/cb", entry.RedirectURI)
	}

	// Second call must return false (IV2).
	_, ok2 := cs.Consume(code)
	if ok2 {
		t.Fatal("Consume: expected ok=false on second call (single-use)")
	}
}

// TestCodeStore_Consume_Expired verifies IV3: expired entries are rejected.
func TestCodeStore_Consume_Expired(t *testing.T) {
	// Start at time zero, issue with 5m TTL.
	now := time.Unix(0, 0)
	cs := newCodeStore(func() time.Time { return now })

	code := cs.Issue("bob", "challenge", "http://x/cb", 5*time.Minute)

	// Advance past TTL.
	now = now.Add(6 * time.Minute)

	_, ok := cs.Consume(code)
	if ok {
		t.Fatal("Consume: expected ok=false for expired code (IV3)")
	}
}

// TestCodeStore_Consume_UnknownCode verifies false on unknown code.
func TestCodeStore_Consume_UnknownCode(t *testing.T) {
	cs := newCodeStore(nil)
	_, ok := cs.Consume("no-such-code")
	if ok {
		t.Fatal("Consume: expected ok=false for unknown code")
	}
}

// TestCodeStore_ConcurrentAccess verifies race-freedom (run with -race).
func TestCodeStore_ConcurrentAccess(t *testing.T) {
	cs := newCodeStore(nil)
	var wg sync.WaitGroup

	// Writers
	codes := make(chan string, 100)
	for i := 0; i < 10; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			for j := 0; j < 10; j++ {
				code := cs.Issue("sub", "challenge", "http://x/cb", 5*time.Minute)
				codes <- code
			}
		}()
	}

	// Readers
	for i := 0; i < 5; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			for j := 0; j < 5; j++ {
				cs.Consume("probably-not-there")
			}
		}()
	}

	wg.Wait()
	close(codes)
	for code := range codes {
		cs.Consume(code)
	}
}