mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-09-23 10:44:16 +00:00
server.test_mode survived the build-tag refactor only to feed five behavioral branches: the registry's fall-back to the default hold when the user's hold is unreachable, backfill warning suppression for external holds, the appview listener close on shutdown, the hold's relay-crawl skip, and the hold's appview-issuer tolerance. Every one of them is a "this is a local development build" decision, which is what the tag already says, and local development has to build with the tag or nothing resolves. So they read atproto.TestModeBuild now, and the flag, SetTestMode, IsTestMode, the middleware option, the backfill constructor parameter, the never-read field on RemoteHoldAuthorizer, the example and template YAML lines, and the docker-compose env vars are gone. The registry keeps the fallback as a field seeded from the constant so the production-path tests can pin it off under the tag. The 24 SetTestMode calls in tests were dead already: stripping them and running the affected packages tagged changed nothing. Tests that resolve a loopback did:web used to t.Fatal naming the tag, which left a bare `go test ./...` permanently red in five packages. They now live under `//go:build testmode`: whole-file constraints where every test needs it, and sibling *_testmode_test.go files holding the moved tests plus their fixtures where a file mixed. The harness carries the constraint too, with its package doc in an untagged doc.go so the package still exists without it. An untagged run compiles those tests out and passes; make test keeps the tag and runs everything. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UwYzaG3Yy7uA8FbZ5qk3tQ
485 lines
14 KiB
Go
485 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
|
|
}{
|
|
{
|
|
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)
|
|
}
|
|
})
|
|
}
|
|
}
|