internal/oauth/oauth_test.go
Rama main · réplica de lectura del repositorio original.
package oauth
import (
"strings"
"testing"
)
// TestParseScopeExactlyThree proves the scope grammar recognizes exactly the
// three consent scopes and nothing else (SPEC-0007 REQ "Exactly Three Consent
// Scopes").
func TestParseScopeExactlyThree(t *testing.T) {
tests := []struct {
name string
in string
want string
wantErr bool
}{
{"empty defaults to all three", "", "artifacts:read artifacts:write annotations:write", false},
{"single valid", "artifacts:read", "artifacts:read", false},
{"all three any order", "annotations:write artifacts:read artifacts:write", "artifacts:read artifacts:write annotations:write", false},
{"duplicates collapse", "artifacts:read artifacts:read", "artifacts:read", false},
{"delete scope rejected", "artifacts:delete", "", true},
{"sharing scope rejected", "sharing:manage", "", true},
{"elevated among valid rejected", "artifacts:read artifacts:delete", "", true},
{"garbage rejected", "☃", "", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ParseScope(tt.in)
if tt.wantErr {
if err == nil {
t.Fatalf("ParseScope(%q) = %v, want error", tt.in, got)
}
return
}
if err != nil {
t.Fatalf("ParseScope(%q): %v", tt.in, err)
}
if JoinScope(got) != tt.want {
t.Fatalf("ParseScope(%q) = %q, want %q", tt.in, JoinScope(got), tt.want)
}
})
}
}
// TestConsentLines pins the exact three consent-screen lines (SPEC-0007 REQ
// "Consent Screen Content").
func TestConsentLines(t *testing.T) {
want := map[string]string{
ScopeArtifactsRead: "Read artifacts you can access",
ScopeArtifactsWrite: "Create & push new artifacts",
ScopeAnnotationsWrite: "Comment & react on your behalf",
}
for scope, line := range want {
if got := ConsentLine(scope); got != line {
t.Errorf("ConsentLine(%q) = %q, want %q", scope, got, line)
}
}
if got := ConsentLine("artifacts:delete"); got != "" {
t.Errorf("ConsentLine(artifacts:delete) = %q, want empty", got)
}
}
// TestPKCE proves S256 verification: a matching verifier passes, everything
// else — wrong verifier, malformed verifier, empty challenge — fails closed
// (SPEC-0007 REQ "OAuth 2.1 Authorization-Code + PKCE").
func TestPKCE(t *testing.T) {
verifier := strings.Repeat("v", 43)
challenge := S256Challenge(verifier)
if !ValidChallenge(challenge) {
t.Fatalf("derived challenge %q not valid", challenge)
}
if !VerifyPKCE(verifier, challenge) {
t.Fatal("matching verifier rejected")
}
if VerifyPKCE(strings.Repeat("x", 43), challenge) {
t.Fatal("wrong verifier accepted")
}
if VerifyPKCE("short", challenge) {
t.Fatal("too-short verifier accepted")
}
if VerifyPKCE(strings.Repeat("v", 129), challenge) {
t.Fatal("too-long verifier accepted")
}
if VerifyPKCE(verifier+"!", challenge) {
t.Fatal("verifier with illegal char accepted")
}
if VerifyPKCE(verifier, "") {
t.Fatal("empty challenge accepted")
}
}
func TestValidChallengeShape(t *testing.T) {
if ValidChallenge("") || ValidChallenge("tooshort") || ValidChallenge(strings.Repeat("A", 44)) {
t.Fatal("malformed challenge accepted")
}
if ValidChallenge(strings.Repeat("+", 43)) {
t.Fatal("non-base64url challenge accepted")
}
}
// TestValidateRedirectURI pins the registration rules: https anywhere, http
// only on loopback, nothing else (SPEC-0007 REQ "Redirect & SSRF Validation").
func TestValidateRedirectURI(t *testing.T) {
valid := []string{
"https://client.example/cb",
"https://client.example:8443/cb?x=1",
"http://localhost/cb",
"http://localhost:53682/cb",
"http://127.0.0.1:9000/callback",
"http://[::1]:8080/cb",
}
for _, u := range valid {
if err := ValidateRedirectURI(u); err != nil {
t.Errorf("ValidateRedirectURI(%q) = %v, want nil", u, err)
}
}
invalid := []string{
"",
"/relative/path",
"http://evil.example/cb", // http on a remote host
"http://169.254.169.254/latest", // http to a link-local metadata IP
"https://client.example/cb#frag", // fragment
"https://user:pass@client.example/cb", // userinfo
"myapp://callback", // custom scheme
"javascript:alert(1)",
"ftp://client.example/cb",
"https://a;b/c", // #64: semicolon in host — url.Parse accepts this as Host `a;b`
"https://a b.example/cb", // space in host (also rejected by url.Parse itself)
"https://a<b>.example/cb",
}
for _, u := range invalid {
if err := ValidateRedirectURI(u); err == nil {
t.Errorf("ValidateRedirectURI(%q) = nil, want error", u)
}
}
}
// TestValidHostSyntax pins the hostname-charset gate (#64): url.Parse is
// permissive about Host contents (it happily parses "https://a;b/c" with
// Host `a;b`), so ValidateRedirectURI and consentCSP both lean on this to
// reject anything that isn't a clean hostname or IP literal.
func TestValidHostSyntax(t *testing.T) {
valid := []string{
"client.example",
"CLIENT.EXAMPLE",
"a.b-c.example",
"localhost",
"127.0.0.1",
"::1",
"2001:db8::1",
}
for _, h := range valid {
if !ValidHostSyntax(h) {
t.Errorf("ValidHostSyntax(%q) = false, want true", h)
}
}
invalid := []string{
"",
"a;b",
"a b",
"a<b>",
"a/b",
"a@b",
"a\\b",
"a\tb",
}
for _, h := range invalid {
if ValidHostSyntax(h) {
t.Errorf("ValidHostSyntax(%q) = true, want false", h)
}
}
}
// TestMatchRedirectURI pins authorize-time matching: exact match only, with
// the RFC 8252 loopback-port allowance (SPEC-0007 scenario "Redirect to an
// unregistered URI").
func TestMatchRedirectURI(t *testing.T) {
registered := []string{"https://client.example/cb", "http://127.0.0.1:1/cb"}
tests := []struct {
presented string
want bool
}{
{"https://client.example/cb", true},
{"https://client.example/cb2", false},
{"https://client.example/cb?extra=1", false},
{"https://evil.example/cb", false},
{"http://127.0.0.1:49152/cb", true}, // loopback: port may vary
{"http://127.0.0.1:49152/cb2", false}, // ...but not the path
{"http://localhost:49152/cb", false}, // ...or the host spelling
{"http://evil.example/cb", false},
{"", false},
}
for _, tt := range tests {
if got := MatchRedirectURI(registered, tt.presented); got != tt.want {
t.Errorf("MatchRedirectURI(%q) = %v, want %v", tt.presented, got, tt.want)
}
}
}