Files
2026-08-15 07:13:00 +00:00

136 lines
3.7 KiB
Go

package auth
import (
"strings"
"testing"
)
func TestMintParseRoundTrip(t *testing.T) {
seen := map[string]bool{}
for i := 0; i < 100; i++ {
token, keyID, hash, err := Mint()
if err != nil {
t.Fatalf("Mint: %v", err)
}
if seen[keyID] {
t.Fatalf("Mint reused key id %q", keyID)
}
seen[keyID] = true
if !strings.HasPrefix(token, Prefix+"_") {
t.Errorf("token %q lacks the %q prefix", token, Prefix)
}
gotID, secret, err := Parse(token)
if err != nil {
t.Fatalf("Parse(%q): %v", token, err)
}
if gotID != keyID {
t.Errorf("Parse key id = %q, want %q", gotID, keyID)
}
if len(gotID) != KeyIDLen {
t.Errorf("key id length = %d, want %d", len(gotID), KeyIDLen)
}
if !SecretMatches(secret, hash[:]) {
t.Error("minted secret does not match its own hash")
}
if strings.Contains(token, keyID+"_"+keyID) {
t.Error("secret must not repeat the key id")
}
if !ValidKeyID(keyID) {
t.Errorf("ValidKeyID rejected a minted id %q", keyID)
}
}
}
func TestParseRejectsMalformed(t *testing.T) {
good, keyID, _, err := Mint()
if err != nil {
t.Fatal(err)
}
_, secret, err := Parse(good)
if err != nil {
t.Fatal(err)
}
cases := []struct {
name string
token string
}{
{"empty", ""},
{"no prefix", keyID + "_" + secret},
{"wrong prefix", "xyz_" + keyID + "_" + secret},
{"prefix only", "pgs_"},
{"no separator", "pgs_" + keyID + secret},
{"short key id", "pgs_" + keyID[:15] + "_" + secret},
{"long key id", "pgs_" + keyID + "a_" + secret},
{"short secret", "pgs_" + keyID + "_" + secret[:42]},
{"long secret", "pgs_" + keyID + "_" + secret + "a"},
{"uppercase key id", "pgs_" + strings.ToUpper(keyID) + "_" + secret},
{"key id with 0 (not in base32)", "pgs_0" + keyID[1:] + "_" + secret},
{"key id with 1 (not in base32)", "pgs_1" + keyID[1:] + "_" + secret},
{"secret with padding", "pgs_" + keyID + "_" + secret[:42] + "="},
{"secret with slash", "pgs_" + keyID + "_" + secret[:42] + "/"},
{"secret with plus", "pgs_" + keyID + "_" + secret[:42] + "+"},
{"embedded NUL", "pgs_" + keyID + "_" + secret[:42] + "\x00"},
{"leading space", " " + good},
{"newline", good + "\n"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if _, _, err := Parse(tc.token); err == nil {
t.Errorf("Parse(%q) accepted a malformed token", tc.token)
}
})
}
// The parse error must never quote the input: these errors reach logs.
if _, _, err := Parse(good[:len(good)-1] + "x"); err != nil {
if strings.Contains(err.Error(), secret[:20]) {
t.Error("parse error leaks part of the presented secret")
}
}
}
func TestSecretMatches(t *testing.T) {
hash := HashSecret("correct horse battery staple")
if !SecretMatches("correct horse battery staple", hash[:]) {
t.Error("matching secret rejected")
}
if SecretMatches("correct horse battery stapl", hash[:]) {
t.Error("truncated secret accepted")
}
if SecretMatches("", hash[:]) {
t.Error("empty secret accepted")
}
if SecretMatches("correct horse battery staple", nil) {
t.Error("nil stored hash accepted")
}
if SecretMatches("correct horse battery staple", hash[:16]) {
t.Error("truncated stored hash accepted")
}
}
func TestValidKeyID(t *testing.T) {
cases := []struct {
in string
want bool
}{
{"abcdefghijklmnop", true},
{"234567234567abcd", true},
{"", false},
{"abcdefghijklmno", false}, // 15
{"abcdefghijklmnopq", false}, // 17
{"ABCDEFGHIJKLMNOP", false},
{"abcdefghijklmno0", false},
{"abcdefghijklmno1", false},
{"abcdefghijklmno8", false},
{"abcdefghijklmno-", false},
{"abcdefghijklmn/p", false},
}
for _, tc := range cases {
if got := ValidKeyID(tc.in); got != tc.want {
t.Errorf("ValidKeyID(%q) = %v, want %v", tc.in, got, tc.want)
}
}
}