package sshcert import ( "strings" "testing" ) func TestNewBuilderRequiresTrustDomain(t *testing.T) { _, err := NewBuilder(Config{}) if err == nil { t.Fatal("expected error for empty trust domain") } } func TestNewBuilderAcceptsValidConfig(t *testing.T) { b, err := NewBuilder(Config{TrustDomain: "example.org"}) if err != nil { t.Fatalf("unexpected error: %v", err) } if b == nil { t.Fatal("builder should not be nil") } } func TestNewBuilderAppliesSpecDefaults(t *testing.T) { b, err := NewBuilder(Config{TrustDomain: "example.org"}) if err != nil { t.Fatalf("unexpected error: %v", err) } if b.config.MaxValidSeconds != 3600 { t.Errorf("MaxValidSeconds: got %d, want 3600", b.config.MaxValidSeconds) } if b.config.MinValidSeconds != 30 { t.Errorf("MinValidSeconds: got %d, want 30", b.config.MinValidSeconds) } if b.config.DefaultValidSeconds != 300 { t.Errorf("DefaultValidSeconds: got %d, want 300", b.config.DefaultValidSeconds) } } func TestNewBuilderRejectsInvalidTTLRange(t *testing.T) { _, err := NewBuilder(Config{ TrustDomain: "example.org", MinValidSeconds: 500, MaxValidSeconds: 100, }) if err == nil { t.Fatal("expected error when min > max") } if !strings.Contains(err.Error(), "min TTL") { t.Errorf("expected min/max error, got: %v", err) } } func TestNewBuilderRejectsDefaultOutsideRange(t *testing.T) { _, err := NewBuilder(Config{ TrustDomain: "example.org", MinValidSeconds: 60, MaxValidSeconds: 3600, DefaultValidSeconds: 10, // below min }) if err == nil { t.Fatal("expected error when default < min") } if !strings.Contains(err.Error(), "default TTL") { t.Errorf("expected default-outside-range error, got: %v", err) } } func TestBuildRequiresSpiffeID(t *testing.T) { b, _ := NewBuilder(Config{TrustDomain: "example.org"}) _, err := b.Build(&CertRequest{}) if err == nil { t.Fatal("expected error for empty spiffe ID") } } func TestBuildRejectsTTLAboveMax(t *testing.T) { b, _ := NewBuilder(Config{ TrustDomain: "example.org", MaxValidSeconds: 3600, }) _, err := b.Build(&CertRequest{ SpiffeID: "spiffe://example.org/ns/prod/sa/web", ValidSeconds: 86400, // 24 hours }) if err == nil { t.Fatal("expected error for TTL above max") } if !strings.Contains(err.Error(), "exceeds maximum") { t.Errorf("expected max TTL error, got: %v", err) } } func TestBuildRejectsTTLBelowMin(t *testing.T) { b, _ := NewBuilder(Config{ TrustDomain: "example.org", MinValidSeconds: 30, }) _, err := b.Build(&CertRequest{ SpiffeID: "spiffe://example.org/ns/prod/sa/web", ValidSeconds: 5, }) if err == nil { t.Fatal("expected error for TTL below min") } if !strings.Contains(err.Error(), "below minimum") { t.Errorf("expected min TTL error, got: %v", err) } } func TestBuildDefaultsTTL(t *testing.T) { b, _ := NewBuilder(Config{ TrustDomain: "example.org", DefaultValidSeconds: 300, }) // ValidSeconds 0 should use default — Build should succeed past TTL checks _, err := b.Build(&CertRequest{ SpiffeID: "spiffe://example.org/ns/prod/sa/web", ValidSeconds: 0, }) // Should get "not yet implemented" (past TTL validation) if err == nil || !strings.Contains(err.Error(), "not yet implemented") { t.Fatalf("expected not-yet-implemented with default TTL, got: %v", err) } } func TestBuildAcceptsTTLInRange(t *testing.T) { b, _ := NewBuilder(Config{ TrustDomain: "example.org", MinValidSeconds: 30, MaxValidSeconds: 3600, }) _, err := b.Build(&CertRequest{ SpiffeID: "spiffe://example.org/ns/prod/sa/web", ValidSeconds: 600, }) // Should get past TTL checks to "not yet implemented" if err == nil || !strings.Contains(err.Error(), "not yet implemented") { t.Fatalf("expected not-yet-implemented with valid TTL, got: %v", err) } } func TestBuildRejectsUnauthorizedPrincipal(t *testing.T) { b, _ := NewBuilder(Config{ TrustDomain: "example.org", AllowedPrincipals: []string{"web", "api"}, }) _, err := b.Build(&CertRequest{ SpiffeID: "spiffe://example.org/ns/prod/sa/web", Principals: []string{"root"}, }) if err == nil { t.Fatal("expected error for unauthorized principal") } if !strings.Contains(err.Error(), "not in allowed list") { t.Errorf("expected principal error, got: %v", err) } } func TestBuildAcceptsAllowedPrincipal(t *testing.T) { b, _ := NewBuilder(Config{ TrustDomain: "example.org", AllowedPrincipals: []string{"web", "api"}, }) _, err := b.Build(&CertRequest{ SpiffeID: "spiffe://example.org/ns/prod/sa/web", Principals: []string{"web"}, }) // Should get past principal checks to "not yet implemented" if err == nil || !strings.Contains(err.Error(), "not yet implemented") { t.Fatalf("expected not-yet-implemented with allowed principal, got: %v", err) } } func TestBuildDefaultsPrincipalToSPIFFELeaf(t *testing.T) { b, _ := NewBuilder(Config{TrustDomain: "example.org"}) // Empty Principals should default to SPIFFE ID leaf _, err := b.Build(&CertRequest{ SpiffeID: "spiffe://example.org/ns/prod/sa/web-server", }) // Should get past principal checks to "not yet implemented" if err == nil || !strings.Contains(err.Error(), "not yet implemented") { t.Fatalf("expected not-yet-implemented with default principal, got: %v", err) } } func TestBuildRejectsInvalidSPIFFEID(t *testing.T) { b, _ := NewBuilder(Config{TrustDomain: "example.org"}) tests := []struct { name string spiffeID string }{ {"http scheme", "http://example.org/sa/web"}, {"no path", "spiffe://example.org"}, {"trailing slash", "spiffe://example.org/sa/web/"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { _, err := b.Build(&CertRequest{SpiffeID: tt.spiffeID}) if err == nil { t.Fatalf("expected error for SPIFFE ID %q", tt.spiffeID) } if !strings.Contains(err.Error(), "SPIFFE ID") && !strings.Contains(err.Error(), "spiffe") { t.Errorf("expected SPIFFE ID error, got: %v", err) } }) } } func TestSpiffeIDLeaf(t *testing.T) { tests := []struct { input string wantLeaf string wantErr bool }{ {"spiffe://guildhouse.dev/ns/prod/sa/web-server", "web-server", false}, {"spiffe://guildhouse.dev/operator/tyler", "tyler", false}, {"spiffe://guildhouse.dev/service", "service", false}, {"spiffe://guildhouse.dev", "", true}, // no path {"spiffe://guildhouse.dev/", "", true}, // empty path {"spiffe://guildhouse.dev/ns/prod/sa/web/", "", true}, // trailing slash {"http://example.com/web", "", true}, // wrong scheme {"not-a-uri", "", true}, // no scheme at all } for _, tt := range tests { t.Run(tt.input, func(t *testing.T) { leaf, err := SpiffeIDLeaf(tt.input) if tt.wantErr { if err == nil { t.Fatalf("expected error for %q, got leaf %q", tt.input, leaf) } } else { if err != nil { t.Fatalf("unexpected error for %q: %v", tt.input, err) } if leaf != tt.wantLeaf { t.Errorf("SpiffeIDLeaf(%q) = %q, want %q", tt.input, leaf, tt.wantLeaf) } } }) } }