Files
at-container-registry/pkg/appview/config_test.go
T
Evan JarrettandClaude Opus 5 9d8bd513da appview: render the install scripts from config instead of shipping ATCR's
seamark.dev's /install and /settings/devices told users to pipe
seamark.dev/static/install.sh into bash. That file was the unmodified ATCR
script: it announced itself as the "ATCR Credential Helper Installer",
installed docker-credential-atcr, and finished by telling the user to configure
credHelpers for atcr.io, the wrong registry for that deployment. Anyone
following the documented setup ended up pointed at another service. The
templates hardcoded docker-credential-atcr, "atcr" and ~/.atcr/device.json
alongside a correctly themed {{ .RegistryURL }}.

The scripts are now rendered from config by a handler, rather than forked per
brand. A theme overlay was the alternative and was worse: it needed a full copy
of both install.sh and install.ps1 per brand, four scripts to keep in sync, and
the operator asked for these values to come from config.

credential_helper.name is the single knob. Docker resolves a credHelpers value
x by exec'ing docker-credential-x, so the credHelpers value, the binary suffix
and the config directory are genuinely one word, not three that can drift. It
is validated against a strict pattern because it is interpolated into a shell
script.

install.sh renders byte-identical to the deleted static file under the atcr
default, so existing installs are unaffected. install.ps1 differs by one line,
where a stale usage comment named a path the script is not served at.

Two behaviour changes worth noting: these two URLs drop from a one-year
Cache-Control to five minutes, since the body now depends on deployment config;
and credential_helper.tangled_repo becomes a real overridable default. It was
previously assigned over unconditionally and read by nothing, while the shipped
script used a different URL form.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PDqoCE1j3njokkZ9b1C5n9
2026-09-02 21:38:10 -05:00

500 lines
14 KiB
Go

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
testMode bool
wantTestMode bool
}{
{
name: "normal mode",
defaultHoldDID: "did:web:hold01.atcr.io",
baseURL: "https://atcr.io",
testMode: false,
wantTestMode: false,
},
{
name: "test mode enabled",
defaultHoldDID: "did:web:hold01.atcr.io",
baseURL: "https://atcr.io",
testMode: true,
wantTestMode: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := buildMiddlewareConfig(tt.defaultHoldDID, tt.baseURL, tt.testMode)
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)
}
if mw.Options["test_mode"] != tt.wantTestMode {
t.Errorf("test_mode = %v, want %v", mw.Options["test_mode"], tt.wantTestMode)
}
})
}
}
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)
}
})
}
}