package appview import ( "net/http" "net/http/httptest" "os" "strings" "testing" "time" "atcr.io/pkg/appview/registryauth" ) func Test_getServiceName(t *testing.T) { tests := []struct { name string baseURL string want string }{ { name: "localhost - use default", baseURL: "http://localhost:5000", want: "atcr.io", }, { name: "127.0.0.1 - use default", baseURL: "http://127.0.0.1:5000", want: "atcr.io", }, { name: "custom domain", baseURL: "https://registry.example.com", want: "registry.example.com", }, { name: "domain with port", baseURL: "https://registry.example.com:443", want: "registry.example.com", }, { name: "invalid URL - use default", baseURL: "://invalid", want: "atcr.io", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := getServiceName(tt.baseURL) if got != tt.want { t.Errorf("getServiceName() = %v, want %v", got, tt.want) } }) } } func TestBuildStorageConfig(t *testing.T) { got := buildStorageConfig() // Verify inmemory driver exists if _, ok := got["inmemory"]; !ok { t.Error("buildStorageConfig() missing inmemory driver") } // Verify maintenance config maintenance, ok := got["maintenance"] if !ok { t.Fatal("buildStorageConfig() missing maintenance config") } uploadPurging, ok := maintenance["uploadpurging"] if !ok { t.Fatal("buildStorageConfig() missing uploadpurging config") } // Verify uploadpurging is map[any]any (for distribution validation) purging, ok := uploadPurging.(map[any]any) if !ok { t.Fatalf("uploadpurging is %T, want map[any]any", uploadPurging) } if purging["enabled"] != false { t.Error("uploadpurging enabled should be false") } // Manifest deletion must be enabled: distribution v3.1.1's DeleteManifest // handler short-circuits with UNSUPPORTED unless storage.delete.enabled is // true, which is what lets `skopeo delete` / OCI DELETE reach our stores. deleteCfg, ok := got["delete"] if !ok { t.Fatal("buildStorageConfig() missing delete config — OCI DELETE would return UNSUPPORTED") } if deleteCfg["enabled"] != true { t.Errorf("storage.delete.enabled = %v, want true", deleteCfg["enabled"]) } } func TestBuildMiddlewareConfig(t *testing.T) { tests := []struct { name string defaultHoldDID string baseURL string }{ { name: "default hold and base URL", defaultHoldDID: "did:web:hold01.atcr.io", baseURL: "https://atcr.io", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := buildMiddlewareConfig(tt.defaultHoldDID, tt.baseURL) registryMW, ok := got["registry"] if !ok { t.Fatal("buildMiddlewareConfig() missing registry middleware") } if len(registryMW) != 1 { t.Fatalf("buildMiddlewareConfig() registry middleware count = %v, want 1", len(registryMW)) } mw := registryMW[0] if mw.Name != "atproto-resolver" { t.Errorf("middleware name = %v, want atproto-resolver", mw.Name) } if mw.Options["default_hold_did"] != tt.defaultHoldDID { t.Errorf("default_hold_did = %v, want %v", mw.Options["default_hold_did"], tt.defaultHoldDID) } if mw.Options["base_url"] != tt.baseURL { t.Errorf("base_url = %v, want %v", mw.Options["base_url"], tt.baseURL) } }) } } func TestBuildHealthConfig(t *testing.T) { got := buildHealthConfig() if !got.StorageDriver.Enabled { t.Error("buildHealthConfig().StorageDriver.Enabled = false, want true") } if got.StorageDriver.Interval.Seconds() != 10 { t.Errorf("buildHealthConfig().StorageDriver.Interval = %v, want 10s", got.StorageDriver.Interval) } if got.StorageDriver.Threshold != 3 { t.Errorf("buildHealthConfig().StorageDriver.Threshold = %v, want 3", got.StorageDriver.Threshold) } } func TestLoadConfig(t *testing.T) { tests := []struct { name string envHoldDID string setHoldDID bool wantError bool }{ { name: "valid config", envHoldDID: "did:web:hold01.atcr.io", setHoldDID: true, wantError: false, }, { name: "missing default hold DID", setHoldDID: false, wantError: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if tt.setHoldDID { t.Setenv("ATCR_SERVER_MANAGED_HOLDS", tt.envHoldDID) } else { os.Unsetenv("ATCR_SERVER_MANAGED_HOLDS") } // Clear other env vars to use defaults os.Unsetenv("ATCR_SERVER_BASE_URL") got, err := LoadConfig("") if (err != nil) != tt.wantError { t.Errorf("LoadConfig() error = %v, wantError %v", err, tt.wantError) return } if tt.wantError { return } // Verify config structure if got.Version != "0.1" { t.Errorf("version = %v, want 0.1", got.Version) } if got.LogLevel != "info" { t.Errorf("log level = %v, want info", got.LogLevel) } if got.Server.Addr != ":5000" { t.Errorf("HTTP addr = %v, want :5000", got.Server.Addr) } if got.Server.PrimaryHoldDID() != tt.envHoldDID { t.Errorf("primary hold DID = %v, want %v", got.Server.PrimaryHoldDID(), tt.envHoldDID) } if got.UI.DatabasePath != "/var/lib/atcr/ui.db" { t.Errorf("UI database path = %v, want /var/lib/atcr/ui.db", got.UI.DatabasePath) } if got.Health.CacheTTL != 15*time.Minute { t.Errorf("health cache TTL = %v, want 15m", got.Health.CacheTTL) } if len(got.Jetstream.URLs) != 4 || got.Jetstream.URLs[0] != "wss://jetstream2.us-west.bsky.network/subscribe" { t.Errorf("jetstream URLs = %v, want 4 endpoints starting with us-west-2", got.Jetstream.URLs) } if len(got.Jetstream.RelayEndpoints) != 2 || got.Jetstream.RelayEndpoints[0] != "https://relay1.us-east.bsky.network" { t.Errorf("jetstream RelayEndpoints = %v, want 2 endpoints starting with us-east", got.Jetstream.RelayEndpoints) } // Verify distribution config was built if got.Distribution == nil { t.Error("distribution config is nil") } if _, ok := got.Distribution.Storage["inmemory"]; !ok { t.Error("distribution storage missing inmemory driver") } if _, ok := got.Distribution.Middleware["registry"]; !ok { t.Error("distribution middleware missing registry") } params, ok := got.Distribution.Auth[registryauth.AuthType] if !ok { t.Fatalf("distribution auth missing %q config", registryauth.AuthType) } // The controller builds one delegate per entry, so this list is // what decides which domains get their own audience. A nil check // would pass on garbage. services, ok := params["services"].([]string) if !ok { t.Fatalf("distribution auth services = %T, want []string", params["services"]) } if len(services) == 0 { t.Fatal("distribution auth services is empty; every /v2/ request would fail to authorize") } // The issuer must be one of the audiences the controller accepts, // otherwise the unknown-host fallback mints tokens no delegate // honours. if issuer := params["issuer"]; issuer != services[0] { t.Errorf("issuer = %v, want primary service %q", issuer, services[0]) } }) } } func TestDefaultConfig(t *testing.T) { cfg := DefaultConfig() if cfg.Version != "0.1" { t.Errorf("DefaultConfig().Version = %q, want \"0.1\"", cfg.Version) } if cfg.LogLevel != "info" { t.Errorf("DefaultConfig().LogLevel = %q, want \"info\"", cfg.LogLevel) } if cfg.Server.Addr != ":5000" { t.Errorf("DefaultConfig().Server.Addr = %q, want \":5000\"", cfg.Server.Addr) } if cfg.UI.DatabasePath != "/var/lib/atcr/ui.db" { t.Errorf("DefaultConfig().UI.DatabasePath = %q, want \"/var/lib/atcr/ui.db\"", cfg.UI.DatabasePath) } if cfg.Health.CacheTTL != 15*time.Minute { t.Errorf("DefaultConfig().Health.CacheTTL = %v, want 15m", cfg.Health.CacheTTL) } if cfg.Server.ClientName != "AT Container Registry" { t.Errorf("DefaultConfig().Server.ClientName = %q, want \"AT Container Registry\"", cfg.Server.ClientName) } } func TestExampleYAML(t *testing.T) { out, err := ExampleYAML() if err != nil { t.Fatalf("ExampleYAML() error: %v", err) } s := string(out) // Should contain the title if !strings.Contains(s, "ATCR AppView Configuration") { t.Error("expected title in YAML output") } // Should contain key fields with defaults if !strings.Contains(s, "addr:") { t.Error("expected addr field in YAML output") } if !strings.Contains(s, "database_path:") { t.Error("expected database_path field in YAML output") } // Should contain comments if !strings.Contains(s, "# Listen address") { t.Error("expected comment for addr field") } if !strings.Contains(s, "# Log level") { t.Error("expected comment for log_level field") } } func Test_deriveServices(t *testing.T) { tests := []struct { name string baseURL string registryDomains []string want []string }{ { name: "no registry domains falls back to base url host", baseURL: "https://atcr.io", registryDomains: nil, want: []string{"atcr.io"}, }, { name: "registry domains in order, first is primary", baseURL: "https://seamark.dev", registryDomains: []string{"buoy.cr", "seamark.cr", "atcr.io"}, want: []string{"buoy.cr", "seamark.cr", "atcr.io"}, }, { name: "ports stripped to match port-stripped request hosts", baseURL: "http://127.0.0.1:5000", registryDomains: []string{"127.0.0.1:5000", "atcr.io"}, want: []string{"127.0.0.1", "atcr.io"}, }, { name: "normalized and deduplicated", baseURL: "https://seamark.dev", registryDomains: []string{"BUOY.cr", "buoy.cr:443", " atcr.io "}, want: []string{"buoy.cr", "atcr.io"}, }, { name: "blank entries dropped", baseURL: "https://seamark.dev", registryDomains: []string{"", " ", "atcr.io"}, want: []string{"atcr.io"}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cfg := &Config{} cfg.Server.BaseURL = tt.baseURL cfg.Server.RegistryDomains = tt.registryDomains got := deriveServices(cfg) if len(got) != len(tt.want) { t.Fatalf("deriveServices() = %v, want %v", got, tt.want) } for i := range got { if got[i] != tt.want[i] { t.Fatalf("deriveServices() = %v, want %v", got, tt.want) } } }) } } // DomainRoutingMiddleware matches a port-stripped host, so it is handed // cfg.Auth.Services rather than the raw registry_domains. A domain configured // with a port has to route to /v2/ rather than being redirected to the UI. func TestDomainRoutingMiddleware_UsesNormalizedDomains(t *testing.T) { handler := DomainRoutingMiddleware( deriveServices(&Config{Server: ServerConfig{ BaseURL: "https://seamark.dev", RegistryDomains: []string{"127.0.0.1:5000", "ATCR.io"}, }}), "https://seamark.dev", )(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusTeapot) // reached the registry })) tests := []struct { name string host string want int }{ {"configured with a port, requested without", "127.0.0.1", http.StatusTeapot}, {"configured with a port, requested with", "127.0.0.1:5000", http.StatusTeapot}, {"configured uppercase", "atcr.io", http.StatusTeapot}, {"unknown host still redirects", "example.com", http.StatusTemporaryRedirect}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/v2/", nil) req.Host = tt.host rec := httptest.NewRecorder() handler.ServeHTTP(rec, req) if rec.Code != tt.want { t.Errorf("host %q: status = %d, want %d", tt.host, rec.Code, tt.want) } }) } } // The credential helper brand must come from config, not from literals baked // into the install scripts and templates. A rebranded deployment configures // credential_helper.name; everything else (binary name, credHelpers value, // config directory, script env prefix) derives from it. func TestLoadConfigCredentialHelperBrand(t *testing.T) { tests := []struct { name string envName string wantName string wantBinary string wantConfigDir string wantEnvPrefix string wantLoadFailed bool }{ { name: "default is atcr", wantName: "atcr", wantBinary: "docker-credential-atcr", wantConfigDir: "~/.atcr", wantEnvPrefix: "ATCR", }, { name: "seamark deployment", envName: "seamark", wantName: "seamark", wantBinary: "docker-credential-seamark", wantConfigDir: "~/.seamark", wantEnvPrefix: "SEAMARK", }, { name: "unsafe name is refused at load", envName: "sea;rm -rf /", wantLoadFailed: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Setenv("ATCR_SERVER_MANAGED_HOLDS", "did:web:hold01.atcr.io") t.Setenv("ATCR_CREDENTIAL_HELPER_NAME", tt.envName) cfg, err := LoadConfig("") if tt.wantLoadFailed { if err == nil { t.Fatal("LoadConfig() accepted an unsafe credential_helper.name") } return } if err != nil { t.Fatalf("LoadConfig() error = %v", err) } brand := cfg.CredentialHelperBrand if brand.Name != tt.wantName { t.Errorf("Name = %q, want %q", brand.Name, tt.wantName) } if brand.BinaryName() != tt.wantBinary { t.Errorf("BinaryName() = %q, want %q", brand.BinaryName(), tt.wantBinary) } if brand.ConfigDir() != tt.wantConfigDir { t.Errorf("ConfigDir() = %q, want %q", brand.ConfigDir(), tt.wantConfigDir) } if brand.EnvPrefix() != tt.wantEnvPrefix { t.Errorf("EnvPrefix() = %q, want %q", brand.EnvPrefix(), tt.wantEnvPrefix) } }) } }