Files
at-container-registry/pkg/appview/config_test.go
T
Evan Jarrett ab4a4ebf9d admin panel long running imrovements, billing fixes, ui cleanup
1. Multiple registry domains + per-user domain preference

The biggest feature. The appview can serve several registry domains (e.g. buoy.cr, atcr.io),
and users can now pick which one shows up in their pull/push commands.

- Lexicon/record: adds registryDomain (and documents ociClient)
to the sailor profile (lexicons/.../profile.json, pkg/atproto/lexicon.go).
- DB: new registry_domain column on users (schema.sql + migration 0027),
with GetUserByDID/Handle reads, UpdateUserRegistryDomain writer,
and Jetstream caching it on profile updates (writes unconditionally so clearing propagates).
- UI/handlers: new UpdateRegistryDomainHandler + /api/profile/registry-domain route,
a <select> in the user settings panel (only shown when >1 domain configured), and resolveRegistryURL()
which falls back to the primary domain if the user's pref is stale/removed. Tests added for all of it.

2. default_hold_did removed → first managed_holds entry is the default

Consolidates two overlapping config fields into one. ServerConfig.DefaultHoldDID is gone;
PrimaryHoldDID() now returns managed_holds[0]. managed_holds is now REQUIRED.
Updated in config, validation, server wiring, test harness, example YAML, and the deploy template.

3. Admin long-running operations → generic background-job framework

New pkg/hold/admin/jobs.go introduces a reusable startJob/jobRegistry pattern
 (a detached context.Background() job + a /admin/api/jobs/{key}/status polling endpoint).
This replaces the bespoke scan-backfill goroutine state machine, and now also wraps crew tier remap and crew import
all three previously looped synchronously on the request context and got 504'd/cancelled mid-run by the reverse proxy.
 Forms switched from POST-redirect to htmx fragments (job_progress.html, job_result.html, crew_import_results.html)
 the old crew_import_results.html page and scan_backfill_progress.html partial were deleted.
This is also captured as a new rule in CLAUDE.md.

4. Cascade-delete manifest on last-tag deletion

DeleteTagHandler now, after removing the last tag pointing to a digest, cascade-deletes the manifest itself
 (PDS + DB + hold blob purge) — but only if it's not a child of a manifest list (multi-arch parent).
 New GetTagDigest and ShouldCascadeDeleteManifest queries back it, plus cascade_delete_test.go.
 Also switches tag rkey computation to the atproto.RepositoryTagToRKey helper.

5. Billing simplification

Drops the OwnerBadge config option (hold-owner supporter badge).
The user-profile template no longer special-cases an "owner" badge value (only "Captain").
Example tiers renamed to the nautical scheme (deckhand/bosun/quartermaster).

6. Build/deploy: go generate always runs via Make

make generate is now a phony target that always runs go generate ./... (regenerating cbor_gen, icon sprites, etc.),
 and build-trixie depends on it. The deploy tooling (provision.go/update.go)
drops its own runGenerate calls since the Makefile handles it.

7. New cmd/firehose-tap tool (untracked)

A standalone CLI that subscribes to a com.atproto.sync.subscribeRepos endpoint and pretty-prints events,
with emphasis on Sync 1.1 compliance fields (per-op prev CIDs, commit prevData) and a --validate CI mode.
Fits with the recent "more sync1.1 compliant" commit.
2026-06-05 20:57:25 -05:00

302 lines
7.6 KiB
Go

package appview
import (
"os"
"strings"
"testing"
"time"
)
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")
}
}
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")
}
if _, ok := got.Distribution.Auth["token"]; !ok {
t.Error("distribution auth missing token config")
}
})
}
}
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")
}
}