Ctrl K
ComunidadGuías…

internal/oauth/oauth_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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)
		}
	}
}