feat: implement T22, T18, T23 — dev stack, profile tests, server binary
- T22: docker-compose.dev.yml dev stack, Dockerfile, root Makefile - T18: Profile test suite (Scenario A) — 8 integration tests with real handlers - T23: Server binary wiring all components, config validation, /healthz - Config: ValidateConfig with startup validation 14 test packages pass. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
fa27adbc77
commit
c18adb6441
9 changed files with 1345 additions and 2 deletions
225
src/internal/config/config_test.go
Normal file
225
src/internal/config/config_test.go
Normal file
|
|
@ -0,0 +1,225 @@
|
|||
package config_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"keycape/internal/config"
|
||||
)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// writeTempFile creates a temporary file with the given content and returns its path.
|
||||
func writeTempFile(t *testing.T, content string) string {
|
||||
t.Helper()
|
||||
f, err := os.CreateTemp(t.TempDir(), "keycape-test-*")
|
||||
if err != nil {
|
||||
t.Fatalf("create temp file: %v", err)
|
||||
}
|
||||
if _, err := f.WriteString(content); err != nil {
|
||||
t.Fatalf("write temp file: %v", err)
|
||||
}
|
||||
f.Close()
|
||||
return f.Name()
|
||||
}
|
||||
|
||||
// validConfig returns a minimal valid Config for use in tests.
|
||||
func validConfig(keyPath string) *config.Config {
|
||||
return &config.Config{
|
||||
Issuer: "https://auth.example.com",
|
||||
Port: 8080,
|
||||
TokenLifetime: "15m",
|
||||
PrivateKeyPEM: keyPath,
|
||||
Environment: "dev",
|
||||
Clients: []config.ClientConfig{
|
||||
{
|
||||
ClientID: "test-app",
|
||||
DisplayName: "Test App",
|
||||
RedirectURIs: []string{"https://app.example.com/callback"},
|
||||
ClientType: "public",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Load tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestLoad_ValidYAML(t *testing.T) {
|
||||
keyPath := writeTempFile(t, "placeholder-key")
|
||||
yaml := `
|
||||
issuer: "https://auth.example.com"
|
||||
port: 8080
|
||||
tokenLifetime: "15m"
|
||||
privateKeyPem: "` + keyPath + `"
|
||||
environment: "dev"
|
||||
clients:
|
||||
- clientId: "demo"
|
||||
displayName: "Demo"
|
||||
redirectUris:
|
||||
- "https://demo.example.com/cb"
|
||||
clientType: "public"
|
||||
`
|
||||
cfgPath := writeTempFile(t, yaml)
|
||||
|
||||
cfg, err := config.Load(cfgPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Load: unexpected error: %v", err)
|
||||
}
|
||||
if cfg.Issuer != "https://auth.example.com" {
|
||||
t.Errorf("Issuer: want %q, got %q", "https://auth.example.com", cfg.Issuer)
|
||||
}
|
||||
if cfg.Port != 8080 {
|
||||
t.Errorf("Port: want 8080, got %d", cfg.Port)
|
||||
}
|
||||
if len(cfg.Clients) != 1 {
|
||||
t.Errorf("Clients: want 1, got %d", len(cfg.Clients))
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoad_FileNotFound(t *testing.T) {
|
||||
_, err := config.Load(filepath.Join(t.TempDir(), "nonexistent.yaml"))
|
||||
if err == nil {
|
||||
t.Error("Load: expected error for missing file, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoad_InvalidYAML(t *testing.T) {
|
||||
bad := writeTempFile(t, "not: valid: yaml: [[[")
|
||||
_, err := config.Load(bad)
|
||||
if err == nil {
|
||||
t.Error("Load: expected error for invalid YAML, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Validate tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestValidate_ValidConfig(t *testing.T) {
|
||||
keyPath := writeTempFile(t, "key")
|
||||
errs := config.ValidateConfig(validConfig(keyPath))
|
||||
if len(errs) != 0 {
|
||||
t.Errorf("ValidateConfig: expected no errors, got %v", errs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidate_MissingIssuer(t *testing.T) {
|
||||
keyPath := writeTempFile(t, "key")
|
||||
cfg := validConfig(keyPath)
|
||||
cfg.Issuer = ""
|
||||
errs := config.ValidateConfig(cfg)
|
||||
if !containsErr(errs, "issuer") {
|
||||
t.Errorf("expected issuer error, got %v", errs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidate_InvalidIssuerURL(t *testing.T) {
|
||||
keyPath := writeTempFile(t, "key")
|
||||
cfg := validConfig(keyPath)
|
||||
cfg.Issuer = "not a url"
|
||||
errs := config.ValidateConfig(cfg)
|
||||
if !containsErr(errs, "issuer") {
|
||||
t.Errorf("expected issuer URL error, got %v", errs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidate_PortZero(t *testing.T) {
|
||||
keyPath := writeTempFile(t, "key")
|
||||
cfg := validConfig(keyPath)
|
||||
cfg.Port = 0
|
||||
errs := config.ValidateConfig(cfg)
|
||||
if !containsErr(errs, "port") {
|
||||
t.Errorf("expected port error, got %v", errs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidate_PortTooHigh(t *testing.T) {
|
||||
keyPath := writeTempFile(t, "key")
|
||||
cfg := validConfig(keyPath)
|
||||
cfg.Port = 70000
|
||||
errs := config.ValidateConfig(cfg)
|
||||
if !containsErr(errs, "port") {
|
||||
t.Errorf("expected port error, got %v", errs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidate_NoClients(t *testing.T) {
|
||||
keyPath := writeTempFile(t, "key")
|
||||
cfg := validConfig(keyPath)
|
||||
cfg.Clients = nil
|
||||
errs := config.ValidateConfig(cfg)
|
||||
if !containsErr(errs, "client") {
|
||||
t.Errorf("expected client error, got %v", errs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidate_ClientMissingRedirectURI(t *testing.T) {
|
||||
keyPath := writeTempFile(t, "key")
|
||||
cfg := validConfig(keyPath)
|
||||
cfg.Clients[0].RedirectURIs = nil
|
||||
errs := config.ValidateConfig(cfg)
|
||||
if !containsErr(errs, "redirect") {
|
||||
t.Errorf("expected redirect_uri error, got %v", errs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidate_MissingPrivateKeyPEM(t *testing.T) {
|
||||
cfg := validConfig("")
|
||||
cfg.PrivateKeyPEM = ""
|
||||
errs := config.ValidateConfig(cfg)
|
||||
if !containsErr(errs, "privateKeyPem") {
|
||||
t.Errorf("expected privateKeyPem error, got %v", errs)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Env var loading test
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestLoad_FromEnvVar(t *testing.T) {
|
||||
keyPath := writeTempFile(t, "key")
|
||||
yaml := `
|
||||
issuer: "https://auth.example.com"
|
||||
port: 9090
|
||||
tokenLifetime: "30m"
|
||||
privateKeyPem: "` + keyPath + `"
|
||||
environment: "dev"
|
||||
clients:
|
||||
- clientId: "env-app"
|
||||
displayName: "Env App"
|
||||
redirectUris:
|
||||
- "https://env.example.com/cb"
|
||||
clientType: "public"
|
||||
`
|
||||
cfgPath := writeTempFile(t, yaml)
|
||||
t.Setenv("KEYCAPE_CONFIG", cfgPath)
|
||||
|
||||
// Load with empty path triggers env var lookup.
|
||||
cfg, err := config.Load("")
|
||||
if err != nil {
|
||||
t.Fatalf("Load with env var: %v", err)
|
||||
}
|
||||
if cfg.Port != 9090 {
|
||||
t.Errorf("Port: want 9090, got %d", cfg.Port)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helper
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func containsErr(errs []string, substring string) bool {
|
||||
for _, e := range errs {
|
||||
for i := 0; i <= len(e)-len(substring); i++ {
|
||||
if e[i:i+len(substring)] == substring {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue