Go · 3914 bytes Raw Blame History
1 // SPDX-License-Identifier: AGPL-3.0-or-later
2
3 package pat
4
5 import (
6 "crypto/sha256"
7 "strings"
8 "testing"
9 "time"
10 )
11
12 func TestMint_FormatAndUniqueness(t *testing.T) {
13 t.Parallel()
14 seen := map[string]bool{}
15 for i := 0; i < 100; i++ {
16 raw, hash, prefix, err := Mint()
17 if err != nil {
18 t.Fatalf("Mint: %v", err)
19 }
20 if !strings.HasPrefix(raw, Prefix) {
21 t.Fatalf("missing prefix: %s", raw)
22 }
23 if len(raw) != len(Prefix)+PayloadLen {
24 t.Fatalf("wrong length: %d", len(raw))
25 }
26 if !strings.HasPrefix(prefix, Prefix) {
27 t.Fatalf("display prefix wrong: %s", prefix)
28 }
29 if len(prefix) != DisplayPrefixLen {
30 t.Fatalf("display prefix length: %d", len(prefix))
31 }
32 if seen[raw] {
33 t.Fatalf("duplicate raw token: %s", raw)
34 }
35 seen[raw] = true
36 want := sha256.Sum256([]byte(raw))
37 if !EqualHash(want[:], hash) {
38 t.Fatalf("hash mismatch")
39 }
40 }
41 }
42
43 func TestHashOf_RoundTrip(t *testing.T) {
44 t.Parallel()
45 raw, hash, _, _ := Mint()
46 got, err := HashOf(raw)
47 if err != nil {
48 t.Fatalf("HashOf: %v", err)
49 }
50 if !EqualHash(got, hash) {
51 t.Fatalf("hash round-trip mismatch")
52 }
53 }
54
55 func TestHashOf_RejectsMalformed(t *testing.T) {
56 t.Parallel()
57 cases := []string{
58 "",
59 "not-a-pat",
60 Prefix + "tooshort",
61 Prefix + strings.Repeat("a", PayloadLen-1),
62 Prefix + strings.Repeat("a", PayloadLen+1),
63 Prefix + strings.Repeat("!", PayloadLen),
64 }
65 for _, c := range cases {
66 if _, err := HashOf(c); err == nil {
67 t.Errorf("expected error for %q", c)
68 }
69 }
70 }
71
72 func TestLooksLike(t *testing.T) {
73 t.Parallel()
74 raw, _, _, _ := Mint()
75 if !LooksLike(raw) {
76 t.Fatal("LooksLike rejected its own output")
77 }
78 if LooksLike("not-a-pat") {
79 t.Fatal("LooksLike accepted nonsense")
80 }
81 }
82
83 // ----- scopes -----
84
85 func TestHasScope(t *testing.T) {
86 t.Parallel()
87 if !HasScope([]string{"repo:read"}, ScopeRepoRead) {
88 t.Fatal("repo:read should grant repo:read")
89 }
90 if !HasScope([]string{"repo:write"}, ScopeRepoRead) {
91 t.Fatal("repo:write should imply repo:read")
92 }
93 if HasScope([]string{"repo:read"}, ScopeRepoWrite) {
94 t.Fatal("repo:read should NOT imply repo:write")
95 }
96 if !HasScope([]string{"user:write"}, ScopeUserRead) {
97 t.Fatal("user:write should imply user:read")
98 }
99 if HasScope(nil, ScopeRepoRead) {
100 t.Fatal("empty held should grant nothing")
101 }
102 }
103
104 func TestNormalizeScopes(t *testing.T) {
105 t.Parallel()
106 in := []string{"user:read", "bogus", "repo:read", "repo:read", "user:read"}
107 got := NormalizeScopes(in)
108 want := []string{"repo:read", "user:read"}
109 if len(got) != len(want) {
110 t.Fatalf("got %v, want %v", got, want)
111 }
112 for i := range got {
113 if got[i] != want[i] {
114 t.Fatalf("got %v, want %v", got, want)
115 }
116 }
117 }
118
119 // ----- debouncer -----
120
121 func TestDebouncer_FirstCallTouchesAndSubsequentSuppresses(t *testing.T) {
122 t.Parallel()
123 d := NewDebouncer(60 * time.Second)
124 if !d.ShouldTouch(1) {
125 t.Fatal("first call must touch")
126 }
127 if d.ShouldTouch(1) {
128 t.Fatal("second call within window must suppress")
129 }
130 }
131
132 func TestDebouncer_DifferentTokensIndependent(t *testing.T) {
133 t.Parallel()
134 d := NewDebouncer(60 * time.Second)
135 if !d.ShouldTouch(1) {
136 t.Fatal("first call for 1")
137 }
138 if !d.ShouldTouch(2) {
139 t.Fatal("first call for 2")
140 }
141 }
142
143 func TestDebouncer_WindowReset(t *testing.T) {
144 t.Parallel()
145 d := NewDebouncer(50 * time.Millisecond)
146 if !d.ShouldTouch(1) {
147 t.Fatal("first")
148 }
149 time.Sleep(80 * time.Millisecond)
150 if !d.ShouldTouch(1) {
151 t.Fatal("after window")
152 }
153 }
154
155 func TestDebouncer_HighRate(t *testing.T) {
156 t.Parallel()
157 d := NewDebouncer(60 * time.Second)
158 touches := 0
159 for i := 0; i < 100; i++ {
160 if d.ShouldTouch(42) {
161 touches++
162 }
163 }
164 if touches != 1 {
165 t.Fatalf("got %d touches over 100 calls, want 1", touches)
166 }
167 }
168
169 func TestDebouncer_Forget(t *testing.T) {
170 t.Parallel()
171 d := NewDebouncer(60 * time.Second)
172 d.ShouldTouch(1)
173 d.Forget(1)
174 if !d.ShouldTouch(1) {
175 t.Fatal("after Forget, ShouldTouch must return true again")
176 }
177 }
178