···72727373 // Initialize UI database first (required for all stores)
7474 fmt.Println("Initializing UI database...")
7575- uiEnabled := os.Getenv("ATCR_UI_ENABLED") != "false"
7676- dbPath := os.Getenv("ATCR_UI_DATABASE_PATH")
7777- if dbPath == "" {
7878- dbPath = "/var/lib/atcr/ui.db"
7979- }
7575+ uiEnabled := appview.GetUIEnabled()
7676+ dbPath := appview.GetUIDatabasePath()
8077 uiDatabase, uiReadOnlyDB, uiSessionStore := db.InitializeDatabase(uiEnabled, dbPath)
8178 if uiDatabase == nil {
8279 return fmt.Errorf("failed to initialize UI database - required for session storage")
···84818582 // Initialize hold health checker
8683 fmt.Println("Initializing hold health checker...")
8787-8888- // Parse health check cache TTL from environment (default: 15m)
8989- cacheTTL := 15 * time.Minute
9090- if cacheTTLStr := os.Getenv("ATCR_HEALTH_CACHE_TTL"); cacheTTLStr != "" {
9191- if parsed, err := time.ParseDuration(cacheTTLStr); err == nil {
9292- cacheTTL = parsed
9393- } else {
9494- fmt.Printf("Warning: Invalid ATCR_HEALTH_CACHE_TTL '%s', using default 15m\n", cacheTTLStr)
9595- }
9696- }
9797-8484+ cacheTTL := appview.GetHealthCacheTTL()
9885 healthChecker := holdhealth.NewChecker(cacheTTL)
998610087 // Initialize README cache
10188 fmt.Println("Initializing README cache...")
102102- readmeCacheTTL := 1 * time.Hour // Default: 1 hour
103103- if readmeTTLStr := os.Getenv("ATCR_README_CACHE_TTL"); readmeTTLStr != "" {
104104- if parsed, err := time.ParseDuration(readmeTTLStr); err == nil {
105105- readmeCacheTTL = parsed
106106- } else {
107107- fmt.Printf("Warning: Invalid ATCR_README_CACHE_TTL '%s', using default 1h\n", readmeTTLStr)
108108- }
109109- }
8989+ readmeCacheTTL := appview.GetReadmeCacheTTL()
11090 readmeCache := readme.NewCache(uiDatabase, readmeCacheTTL)
1119111292 // Start background health check worker
113113- // Parse refresh interval from environment (default: 15m)
114114- refreshInterval := 15 * time.Minute
115115- if refreshIntervalStr := os.Getenv("ATCR_HEALTH_CHECK_INTERVAL"); refreshIntervalStr != "" {
116116- if parsed, err := time.ParseDuration(refreshIntervalStr); err == nil {
117117- refreshInterval = parsed
118118- } else {
119119- fmt.Printf("Warning: Invalid ATCR_HEALTH_CHECK_INTERVAL '%s', using default 15m\n", refreshIntervalStr)
120120- }
121121- }
122122-9393+ refreshInterval := appview.GetHealthCheckInterval()
12394 startupDelay := 5 * time.Second // Wait for hold services to start (Docker compose)
12495 dbAdapter := holdhealth.NewDBAdapter(uiDatabase)
12596 healthWorker := holdhealth.NewWorkerWithStartupDelay(healthChecker, dbAdapter, refreshInterval, startupDelay)
···142113 fmt.Println("Using SQLite for device storage")
143114144115 // Get base URL from config or environment
145145- baseURL := os.Getenv("ATCR_BASE_URL")
146146- if baseURL == "" {
147147- // If addr is just a port (e.g., ":5000"), prepend localhost
148148- addr := config.HTTP.Addr
149149- if addr[0] == ':' {
150150- baseURL = fmt.Sprintf("http://127.0.0.1%s", addr)
151151- } else {
152152- baseURL = fmt.Sprintf("http://%s", addr)
153153- }
154154- }
155155-116116+ baseURL := appview.GetBaseURL(config.HTTP.Addr)
156117 fmt.Printf("DEBUG: Base URL for OAuth: %s\n", baseURL)
157118158119 // Extract default hold DID for OAuth server and backfill worker
···535496// healthChecker: hold endpoint health checker
536497func initializeUIRoutes(database *sql.DB, readOnlyDB *sql.DB, sessionStore *db.SessionStore, oauthApp *oauth.App, oauthStore *db.OAuthStore, refresher *oauth.Refresher, baseURL string, deviceStore *db.DeviceStore, defaultHoldDID string, healthChecker *holdhealth.Checker, readmeCache *readme.Cache) (*template.Template, *mux.Router) {
537498 // Check if UI is enabled
538538- uiEnabled := os.Getenv("ATCR_UI_ENABLED")
539539- if uiEnabled == "false" {
499499+ if !appview.GetUIEnabled() {
540500 return nil, nil
541501 }
542502···723683 }).Methods("GET", "POST")
724684725685 // Start Jetstream worker
726726- jetstreamURL := os.Getenv("JETSTREAM_URL")
727727- if jetstreamURL == "" {
728728- jetstreamURL = "wss://jetstream2.us-west.bsky.network/subscribe"
729729- }
686686+ jetstreamURL := appview.GetJetstreamURL()
730687731688 // Start real-time Jetstream worker with cursor tracking for reconnects
732689 go func() {
···744701 fmt.Println("Jetstream: Real-time worker started")
745702746703 // Start backfill worker (enabled by default, set ATCR_BACKFILL_ENABLED=false to disable)
747747- if backfillEnabled := os.Getenv("ATCR_BACKFILL_ENABLED"); backfillEnabled != "false" {
704704+ if appview.GetBackfillEnabled() {
748705 // Get relay endpoint for sync API (defaults to Bluesky's relay)
749749- relayEndpoint := os.Getenv("ATCR_RELAY_ENDPOINT")
750750- if relayEndpoint == "" {
751751- relayEndpoint = "https://relay1.us-east.bsky.network"
752752- }
706706+ relayEndpoint := appview.GetRelayEndpoint()
753707754708 // Check test mode
755755- testMode := os.Getenv("TEST_MODE") == "true"
709709+ testMode := appview.GetTestMode()
756710757711 backfillWorker, err := jetstream.NewBackfillWorker(database, relayEndpoint, defaultHoldDID, testMode)
758712 if err != nil {
···774728 }()
775729776730 // Start periodic backfill scheduler
777777- backfillInterval := os.Getenv("ATCR_BACKFILL_INTERVAL")
778778- if backfillInterval == "" {
779779- backfillInterval = "1h" // Default to 1 hour
780780- }
781781- interval, err := time.ParseDuration(backfillInterval)
782782- if err != nil {
783783- fmt.Printf("Warning: Invalid ATCR_BACKFILL_INTERVAL '%s', using default 1h: %v\n", backfillInterval, err)
784784- interval = time.Hour
785785- }
731731+ interval := appview.GetBackfillInterval()
786732787733 go func() {
788734 ticker := time.NewTicker(interval)
+100
pkg/appview/config.go
···320320321321 return false
322322}
323323+324324+// GetDurationOrDefault parses a duration from environment variable or returns default
325325+// Logs a warning if parsing fails
326326+func GetDurationOrDefault(envKey string, defaultValue time.Duration) time.Duration {
327327+ envVal := os.Getenv(envKey)
328328+ if envVal == "" {
329329+ return defaultValue
330330+ }
331331+332332+ parsed, err := time.ParseDuration(envVal)
333333+ if err != nil {
334334+ fmt.Printf("Warning: Invalid %s '%s', using default %s\n", envKey, envVal, defaultValue)
335335+ return defaultValue
336336+ }
337337+338338+ return parsed
339339+}
340340+341341+// GetBoolOrDefault returns a boolean from environment variable or returns default
342342+// Treats "false" as false, everything else (including empty) as the default value
343343+func GetBoolOrDefault(envKey string, defaultValue bool) bool {
344344+ envVal := os.Getenv(envKey)
345345+ if envVal == "" {
346346+ return defaultValue
347347+ }
348348+349349+ // Explicit false check
350350+ if envVal == "false" {
351351+ return false
352352+ }
353353+354354+ // Explicit true check
355355+ if envVal == "true" {
356356+ return true
357357+ }
358358+359359+ // For any other value, return default
360360+ return defaultValue
361361+}
362362+363363+// UI Configuration
364364+365365+// GetUIEnabled returns whether the UI is enabled (default: true)
366366+func GetUIEnabled() bool {
367367+ // UI is enabled unless explicitly set to "false"
368368+ return os.Getenv("ATCR_UI_ENABLED") != "false"
369369+}
370370+371371+// GetUIDatabasePath returns the path to the UI database (default: /var/lib/atcr/ui.db)
372372+func GetUIDatabasePath() string {
373373+ return GetEnvOrDefault("ATCR_UI_DATABASE_PATH", "/var/lib/atcr/ui.db")
374374+}
375375+376376+// Health & Cache Configuration
377377+378378+// GetHealthCacheTTL returns the hold health check cache TTL (default: 15m)
379379+func GetHealthCacheTTL() time.Duration {
380380+ return GetDurationOrDefault("ATCR_HEALTH_CACHE_TTL", 15*time.Minute)
381381+}
382382+383383+// GetReadmeCacheTTL returns the README cache TTL (default: 1h)
384384+func GetReadmeCacheTTL() time.Duration {
385385+ return GetDurationOrDefault("ATCR_README_CACHE_TTL", 1*time.Hour)
386386+}
387387+388388+// GetHealthCheckInterval returns the hold health check refresh interval (default: 15m)
389389+func GetHealthCheckInterval() time.Duration {
390390+ return GetDurationOrDefault("ATCR_HEALTH_CHECK_INTERVAL", 15*time.Minute)
391391+}
392392+393393+// Jetstream Configuration
394394+395395+// GetJetstreamURL returns the Jetstream WebSocket URL (default: wss://jetstream2.us-west.bsky.network/subscribe)
396396+func GetJetstreamURL() string {
397397+ return GetEnvOrDefault("JETSTREAM_URL", "wss://jetstream2.us-west.bsky.network/subscribe")
398398+}
399399+400400+// GetBackfillEnabled returns whether backfill is enabled (default: true)
401401+func GetBackfillEnabled() bool {
402402+ // Backfill is enabled unless explicitly set to "false"
403403+ return os.Getenv("ATCR_BACKFILL_ENABLED") != "false"
404404+}
405405+406406+// GetRelayEndpoint returns the relay endpoint for sync API (default: https://relay1.us-east.bsky.network)
407407+func GetRelayEndpoint() string {
408408+ return GetEnvOrDefault("ATCR_RELAY_ENDPOINT", "https://relay1.us-east.bsky.network")
409409+}
410410+411411+// GetBackfillInterval returns the backfill interval (default: 1h)
412412+func GetBackfillInterval() time.Duration {
413413+ return GetDurationOrDefault("ATCR_BACKFILL_INTERVAL", 1*time.Hour)
414414+}
415415+416416+// Test Mode Configuration
417417+418418+// GetTestMode returns whether test mode is enabled (default: false)
419419+// Test mode enables HTTP for local DID resolution and transition:generic scope
420420+func GetTestMode() bool {
421421+ return os.Getenv("TEST_MODE") == "true"
422422+}
+613
pkg/appview/config_test.go
···33import (
44 "os"
55 "testing"
66+ "time"
6778 "github.com/distribution/distribution/v3/configuration"
89)
···968969 })
969970 }
970971}
972972+973973+func TestGetDurationOrDefault(t *testing.T) {
974974+ tests := []struct {
975975+ name string
976976+ envKey string
977977+ envValue string
978978+ setEnv bool
979979+ defaultValue string
980980+ want string
981981+ }{
982982+ {
983983+ name: "env var not set",
984984+ envKey: "TEST_DURATION",
985985+ setEnv: false,
986986+ defaultValue: "5m",
987987+ want: "5m",
988988+ },
989989+ {
990990+ name: "env var set to valid duration",
991991+ envKey: "TEST_DURATION",
992992+ envValue: "10m",
993993+ setEnv: true,
994994+ defaultValue: "5m",
995995+ want: "10m",
996996+ },
997997+ {
998998+ name: "env var set to invalid duration",
999999+ envKey: "TEST_DURATION",
10001000+ envValue: "invalid",
10011001+ setEnv: true,
10021002+ defaultValue: "5m",
10031003+ want: "5m", // Falls back to default
10041004+ },
10051005+ {
10061006+ name: "env var set to empty string",
10071007+ envKey: "TEST_DURATION",
10081008+ envValue: "",
10091009+ setEnv: true,
10101010+ defaultValue: "15m",
10111011+ want: "15m",
10121012+ },
10131013+ }
10141014+10151015+ for _, tt := range tests {
10161016+ t.Run(tt.name, func(t *testing.T) {
10171017+ if tt.setEnv {
10181018+ t.Setenv(tt.envKey, tt.envValue)
10191019+ } else {
10201020+ os.Unsetenv(tt.envKey)
10211021+ }
10221022+10231023+ defaultDur := parseDuration(t, tt.defaultValue)
10241024+ wantDur := parseDuration(t, tt.want)
10251025+10261026+ got := GetDurationOrDefault(tt.envKey, defaultDur)
10271027+ if got != wantDur {
10281028+ t.Errorf("GetDurationOrDefault() = %v, want %v", got, wantDur)
10291029+ }
10301030+ })
10311031+ }
10321032+}
10331033+10341034+func TestGetBoolOrDefault(t *testing.T) {
10351035+ tests := []struct {
10361036+ name string
10371037+ envKey string
10381038+ envValue string
10391039+ setEnv bool
10401040+ defaultValue bool
10411041+ want bool
10421042+ }{
10431043+ {
10441044+ name: "env var not set - default true",
10451045+ envKey: "TEST_BOOL",
10461046+ setEnv: false,
10471047+ defaultValue: true,
10481048+ want: true,
10491049+ },
10501050+ {
10511051+ name: "env var not set - default false",
10521052+ envKey: "TEST_BOOL",
10531053+ setEnv: false,
10541054+ defaultValue: false,
10551055+ want: false,
10561056+ },
10571057+ {
10581058+ name: "env var set to true",
10591059+ envKey: "TEST_BOOL",
10601060+ envValue: "true",
10611061+ setEnv: true,
10621062+ defaultValue: false,
10631063+ want: true,
10641064+ },
10651065+ {
10661066+ name: "env var set to false",
10671067+ envKey: "TEST_BOOL",
10681068+ envValue: "false",
10691069+ setEnv: true,
10701070+ defaultValue: true,
10711071+ want: false,
10721072+ },
10731073+ {
10741074+ name: "env var set to invalid value - use default true",
10751075+ envKey: "TEST_BOOL",
10761076+ envValue: "invalid",
10771077+ setEnv: true,
10781078+ defaultValue: true,
10791079+ want: true,
10801080+ },
10811081+ {
10821082+ name: "env var set to invalid value - use default false",
10831083+ envKey: "TEST_BOOL",
10841084+ envValue: "invalid",
10851085+ setEnv: true,
10861086+ defaultValue: false,
10871087+ want: false,
10881088+ },
10891089+ {
10901090+ name: "env var set to empty string - use default",
10911091+ envKey: "TEST_BOOL",
10921092+ envValue: "",
10931093+ setEnv: true,
10941094+ defaultValue: true,
10951095+ want: true,
10961096+ },
10971097+ }
10981098+10991099+ for _, tt := range tests {
11001100+ t.Run(tt.name, func(t *testing.T) {
11011101+ if tt.setEnv {
11021102+ t.Setenv(tt.envKey, tt.envValue)
11031103+ } else {
11041104+ os.Unsetenv(tt.envKey)
11051105+ }
11061106+11071107+ got := GetBoolOrDefault(tt.envKey, tt.defaultValue)
11081108+ if got != tt.want {
11091109+ t.Errorf("GetBoolOrDefault() = %v, want %v", got, tt.want)
11101110+ }
11111111+ })
11121112+ }
11131113+}
11141114+11151115+func TestGetUIEnabled(t *testing.T) {
11161116+ tests := []struct {
11171117+ name string
11181118+ envValue string
11191119+ setEnv bool
11201120+ want bool
11211121+ }{
11221122+ {
11231123+ name: "env var not set - enabled by default",
11241124+ setEnv: false,
11251125+ want: true,
11261126+ },
11271127+ {
11281128+ name: "env var set to false",
11291129+ envValue: "false",
11301130+ setEnv: true,
11311131+ want: false,
11321132+ },
11331133+ {
11341134+ name: "env var set to true",
11351135+ envValue: "true",
11361136+ setEnv: true,
11371137+ want: true,
11381138+ },
11391139+ {
11401140+ name: "env var set to empty string - enabled by default",
11411141+ envValue: "",
11421142+ setEnv: true,
11431143+ want: true,
11441144+ },
11451145+ {
11461146+ name: "env var set to any other value - enabled",
11471147+ envValue: "yes",
11481148+ setEnv: true,
11491149+ want: true,
11501150+ },
11511151+ }
11521152+11531153+ for _, tt := range tests {
11541154+ t.Run(tt.name, func(t *testing.T) {
11551155+ if tt.setEnv {
11561156+ t.Setenv("ATCR_UI_ENABLED", tt.envValue)
11571157+ } else {
11581158+ os.Unsetenv("ATCR_UI_ENABLED")
11591159+ }
11601160+11611161+ got := GetUIEnabled()
11621162+ if got != tt.want {
11631163+ t.Errorf("GetUIEnabled() = %v, want %v", got, tt.want)
11641164+ }
11651165+ })
11661166+ }
11671167+}
11681168+11691169+func TestGetUIDatabasePath(t *testing.T) {
11701170+ tests := []struct {
11711171+ name string
11721172+ envValue string
11731173+ setEnv bool
11741174+ want string
11751175+ }{
11761176+ {
11771177+ name: "env var not set - use default",
11781178+ setEnv: false,
11791179+ want: "/var/lib/atcr/ui.db",
11801180+ },
11811181+ {
11821182+ name: "env var set to custom path",
11831183+ envValue: "/custom/path/ui.db",
11841184+ setEnv: true,
11851185+ want: "/custom/path/ui.db",
11861186+ },
11871187+ {
11881188+ name: "env var set to empty string - use default",
11891189+ envValue: "",
11901190+ setEnv: true,
11911191+ want: "/var/lib/atcr/ui.db",
11921192+ },
11931193+ }
11941194+11951195+ for _, tt := range tests {
11961196+ t.Run(tt.name, func(t *testing.T) {
11971197+ if tt.setEnv {
11981198+ t.Setenv("ATCR_UI_DATABASE_PATH", tt.envValue)
11991199+ } else {
12001200+ os.Unsetenv("ATCR_UI_DATABASE_PATH")
12011201+ }
12021202+12031203+ got := GetUIDatabasePath()
12041204+ if got != tt.want {
12051205+ t.Errorf("GetUIDatabasePath() = %v, want %v", got, tt.want)
12061206+ }
12071207+ })
12081208+ }
12091209+}
12101210+12111211+func TestGetHealthCacheTTL(t *testing.T) {
12121212+ tests := []struct {
12131213+ name string
12141214+ envValue string
12151215+ setEnv bool
12161216+ want string
12171217+ }{
12181218+ {
12191219+ name: "env var not set - use default 15m",
12201220+ setEnv: false,
12211221+ want: "15m",
12221222+ },
12231223+ {
12241224+ name: "env var set to custom duration",
12251225+ envValue: "30m",
12261226+ setEnv: true,
12271227+ want: "30m",
12281228+ },
12291229+ {
12301230+ name: "env var set to invalid duration - use default",
12311231+ envValue: "invalid",
12321232+ setEnv: true,
12331233+ want: "15m",
12341234+ },
12351235+ }
12361236+12371237+ for _, tt := range tests {
12381238+ t.Run(tt.name, func(t *testing.T) {
12391239+ if tt.setEnv {
12401240+ t.Setenv("ATCR_HEALTH_CACHE_TTL", tt.envValue)
12411241+ } else {
12421242+ os.Unsetenv("ATCR_HEALTH_CACHE_TTL")
12431243+ }
12441244+12451245+ wantDur := parseDuration(t, tt.want)
12461246+ got := GetHealthCacheTTL()
12471247+ if got != wantDur {
12481248+ t.Errorf("GetHealthCacheTTL() = %v, want %v", got, wantDur)
12491249+ }
12501250+ })
12511251+ }
12521252+}
12531253+12541254+func TestGetReadmeCacheTTL(t *testing.T) {
12551255+ tests := []struct {
12561256+ name string
12571257+ envValue string
12581258+ setEnv bool
12591259+ want string
12601260+ }{
12611261+ {
12621262+ name: "env var not set - use default 1h",
12631263+ setEnv: false,
12641264+ want: "1h",
12651265+ },
12661266+ {
12671267+ name: "env var set to custom duration",
12681268+ envValue: "2h",
12691269+ setEnv: true,
12701270+ want: "2h",
12711271+ },
12721272+ {
12731273+ name: "env var set to invalid duration - use default",
12741274+ envValue: "invalid",
12751275+ setEnv: true,
12761276+ want: "1h",
12771277+ },
12781278+ }
12791279+12801280+ for _, tt := range tests {
12811281+ t.Run(tt.name, func(t *testing.T) {
12821282+ if tt.setEnv {
12831283+ t.Setenv("ATCR_README_CACHE_TTL", tt.envValue)
12841284+ } else {
12851285+ os.Unsetenv("ATCR_README_CACHE_TTL")
12861286+ }
12871287+12881288+ wantDur := parseDuration(t, tt.want)
12891289+ got := GetReadmeCacheTTL()
12901290+ if got != wantDur {
12911291+ t.Errorf("GetReadmeCacheTTL() = %v, want %v", got, wantDur)
12921292+ }
12931293+ })
12941294+ }
12951295+}
12961296+12971297+func TestGetHealthCheckInterval(t *testing.T) {
12981298+ tests := []struct {
12991299+ name string
13001300+ envValue string
13011301+ setEnv bool
13021302+ want string
13031303+ }{
13041304+ {
13051305+ name: "env var not set - use default 15m",
13061306+ setEnv: false,
13071307+ want: "15m",
13081308+ },
13091309+ {
13101310+ name: "env var set to custom interval",
13111311+ envValue: "5m",
13121312+ setEnv: true,
13131313+ want: "5m",
13141314+ },
13151315+ {
13161316+ name: "env var set to invalid duration - use default",
13171317+ envValue: "invalid",
13181318+ setEnv: true,
13191319+ want: "15m",
13201320+ },
13211321+ }
13221322+13231323+ for _, tt := range tests {
13241324+ t.Run(tt.name, func(t *testing.T) {
13251325+ if tt.setEnv {
13261326+ t.Setenv("ATCR_HEALTH_CHECK_INTERVAL", tt.envValue)
13271327+ } else {
13281328+ os.Unsetenv("ATCR_HEALTH_CHECK_INTERVAL")
13291329+ }
13301330+13311331+ wantDur := parseDuration(t, tt.want)
13321332+ got := GetHealthCheckInterval()
13331333+ if got != wantDur {
13341334+ t.Errorf("GetHealthCheckInterval() = %v, want %v", got, wantDur)
13351335+ }
13361336+ })
13371337+ }
13381338+}
13391339+13401340+func TestGetJetstreamURL(t *testing.T) {
13411341+ tests := []struct {
13421342+ name string
13431343+ envValue string
13441344+ setEnv bool
13451345+ want string
13461346+ }{
13471347+ {
13481348+ name: "env var not set - use default",
13491349+ setEnv: false,
13501350+ want: "wss://jetstream2.us-west.bsky.network/subscribe",
13511351+ },
13521352+ {
13531353+ name: "env var set to custom URL",
13541354+ envValue: "wss://custom-jetstream.example.com/subscribe",
13551355+ setEnv: true,
13561356+ want: "wss://custom-jetstream.example.com/subscribe",
13571357+ },
13581358+ {
13591359+ name: "env var set to empty string - use default",
13601360+ envValue: "",
13611361+ setEnv: true,
13621362+ want: "wss://jetstream2.us-west.bsky.network/subscribe",
13631363+ },
13641364+ }
13651365+13661366+ for _, tt := range tests {
13671367+ t.Run(tt.name, func(t *testing.T) {
13681368+ if tt.setEnv {
13691369+ t.Setenv("JETSTREAM_URL", tt.envValue)
13701370+ } else {
13711371+ os.Unsetenv("JETSTREAM_URL")
13721372+ }
13731373+13741374+ got := GetJetstreamURL()
13751375+ if got != tt.want {
13761376+ t.Errorf("GetJetstreamURL() = %v, want %v", got, tt.want)
13771377+ }
13781378+ })
13791379+ }
13801380+}
13811381+13821382+func TestGetBackfillEnabled(t *testing.T) {
13831383+ tests := []struct {
13841384+ name string
13851385+ envValue string
13861386+ setEnv bool
13871387+ want bool
13881388+ }{
13891389+ {
13901390+ name: "env var not set - enabled by default",
13911391+ setEnv: false,
13921392+ want: true,
13931393+ },
13941394+ {
13951395+ name: "env var set to false",
13961396+ envValue: "false",
13971397+ setEnv: true,
13981398+ want: false,
13991399+ },
14001400+ {
14011401+ name: "env var set to true",
14021402+ envValue: "true",
14031403+ setEnv: true,
14041404+ want: true,
14051405+ },
14061406+ {
14071407+ name: "env var set to empty string - enabled by default",
14081408+ envValue: "",
14091409+ setEnv: true,
14101410+ want: true,
14111411+ },
14121412+ {
14131413+ name: "env var set to any other value - enabled",
14141414+ envValue: "yes",
14151415+ setEnv: true,
14161416+ want: true,
14171417+ },
14181418+ }
14191419+14201420+ for _, tt := range tests {
14211421+ t.Run(tt.name, func(t *testing.T) {
14221422+ if tt.setEnv {
14231423+ t.Setenv("ATCR_BACKFILL_ENABLED", tt.envValue)
14241424+ } else {
14251425+ os.Unsetenv("ATCR_BACKFILL_ENABLED")
14261426+ }
14271427+14281428+ got := GetBackfillEnabled()
14291429+ if got != tt.want {
14301430+ t.Errorf("GetBackfillEnabled() = %v, want %v", got, tt.want)
14311431+ }
14321432+ })
14331433+ }
14341434+}
14351435+14361436+func TestGetRelayEndpoint(t *testing.T) {
14371437+ tests := []struct {
14381438+ name string
14391439+ envValue string
14401440+ setEnv bool
14411441+ want string
14421442+ }{
14431443+ {
14441444+ name: "env var not set - use default",
14451445+ setEnv: false,
14461446+ want: "https://relay1.us-east.bsky.network",
14471447+ },
14481448+ {
14491449+ name: "env var set to custom endpoint",
14501450+ envValue: "https://custom-relay.example.com",
14511451+ setEnv: true,
14521452+ want: "https://custom-relay.example.com",
14531453+ },
14541454+ {
14551455+ name: "env var set to empty string - use default",
14561456+ envValue: "",
14571457+ setEnv: true,
14581458+ want: "https://relay1.us-east.bsky.network",
14591459+ },
14601460+ }
14611461+14621462+ for _, tt := range tests {
14631463+ t.Run(tt.name, func(t *testing.T) {
14641464+ if tt.setEnv {
14651465+ t.Setenv("ATCR_RELAY_ENDPOINT", tt.envValue)
14661466+ } else {
14671467+ os.Unsetenv("ATCR_RELAY_ENDPOINT")
14681468+ }
14691469+14701470+ got := GetRelayEndpoint()
14711471+ if got != tt.want {
14721472+ t.Errorf("GetRelayEndpoint() = %v, want %v", got, tt.want)
14731473+ }
14741474+ })
14751475+ }
14761476+}
14771477+14781478+func TestGetBackfillInterval(t *testing.T) {
14791479+ tests := []struct {
14801480+ name string
14811481+ envValue string
14821482+ setEnv bool
14831483+ want string
14841484+ }{
14851485+ {
14861486+ name: "env var not set - use default 1h",
14871487+ setEnv: false,
14881488+ want: "1h",
14891489+ },
14901490+ {
14911491+ name: "env var set to custom interval",
14921492+ envValue: "30m",
14931493+ setEnv: true,
14941494+ want: "30m",
14951495+ },
14961496+ {
14971497+ name: "env var set to invalid duration - use default",
14981498+ envValue: "invalid",
14991499+ setEnv: true,
15001500+ want: "1h",
15011501+ },
15021502+ }
15031503+15041504+ for _, tt := range tests {
15051505+ t.Run(tt.name, func(t *testing.T) {
15061506+ if tt.setEnv {
15071507+ t.Setenv("ATCR_BACKFILL_INTERVAL", tt.envValue)
15081508+ } else {
15091509+ os.Unsetenv("ATCR_BACKFILL_INTERVAL")
15101510+ }
15111511+15121512+ wantDur := parseDuration(t, tt.want)
15131513+ got := GetBackfillInterval()
15141514+ if got != wantDur {
15151515+ t.Errorf("GetBackfillInterval() = %v, want %v", got, wantDur)
15161516+ }
15171517+ })
15181518+ }
15191519+}
15201520+15211521+func TestGetTestMode(t *testing.T) {
15221522+ tests := []struct {
15231523+ name string
15241524+ envValue string
15251525+ setEnv bool
15261526+ want bool
15271527+ }{
15281528+ {
15291529+ name: "env var not set - disabled by default",
15301530+ setEnv: false,
15311531+ want: false,
15321532+ },
15331533+ {
15341534+ name: "env var set to true",
15351535+ envValue: "true",
15361536+ setEnv: true,
15371537+ want: true,
15381538+ },
15391539+ {
15401540+ name: "env var set to false",
15411541+ envValue: "false",
15421542+ setEnv: true,
15431543+ want: false,
15441544+ },
15451545+ {
15461546+ name: "env var set to empty string - disabled",
15471547+ envValue: "",
15481548+ setEnv: true,
15491549+ want: false,
15501550+ },
15511551+ {
15521552+ name: "env var set to any other value - disabled",
15531553+ envValue: "yes",
15541554+ setEnv: true,
15551555+ want: false,
15561556+ },
15571557+ }
15581558+15591559+ for _, tt := range tests {
15601560+ t.Run(tt.name, func(t *testing.T) {
15611561+ if tt.setEnv {
15621562+ t.Setenv("TEST_MODE", tt.envValue)
15631563+ } else {
15641564+ os.Unsetenv("TEST_MODE")
15651565+ }
15661566+15671567+ got := GetTestMode()
15681568+ if got != tt.want {
15691569+ t.Errorf("GetTestMode() = %v, want %v", got, tt.want)
15701570+ }
15711571+ })
15721572+ }
15731573+}
15741574+15751575+// parseDuration is a helper function to parse duration strings in tests
15761576+func parseDuration(t *testing.T, s string) time.Duration {
15771577+ t.Helper()
15781578+ d, err := time.ParseDuration(s)
15791579+ if err != nil {
15801580+ t.Fatalf("parseDuration(%q) failed: %v", s, err)
15811581+ }
15821582+ return d
15831583+}
+6-5
pkg/auth/hold_authorizer.go
···33import (
44 "context"
55 "fmt"
66+ "log/slog"
6778 "atcr.io/pkg/atproto"
89)
···5556// - Must be authenticated
5657// - Must be hold owner OR crew member
5758func CheckWriteAccessWithCaptain(captain *atproto.CaptainRecord, userDID string, isCrew bool) bool {
5858- fmt.Printf("[CheckWriteAccessWithCaptain] userDID=%s captain.Owner=%s isCrew=%v\n", userDID, captain.Owner, isCrew)
5959+ slog.Debug("Checking write access", "userDID", userDID, "owner", captain.Owner, "isCrew", isCrew)
59606061 if userDID == "" {
6162 // Anonymous writes not allowed
6262- fmt.Printf("[CheckWriteAccessWithCaptain] DENIED: Anonymous user\n")
6363+ slog.Debug("Write access denied: anonymous user")
6364 return false
6465 }
65666667 // Check if DID is the hold owner
6768 if userDID == captain.Owner {
6869 // Owner always has write access
6969- fmt.Printf("[CheckWriteAccessWithCaptain] ALLOWED: User is hold owner\n")
7070+ slog.Debug("Write access allowed: user is hold owner")
7071 return true
7172 }
72737374 // Check if DID is a crew member
7475 if isCrew {
7575- fmt.Printf("[CheckWriteAccessWithCaptain] ALLOWED: User is crew member\n")
7676+ slog.Debug("Write access allowed: user is crew member")
7677 } else {
7777- fmt.Printf("[CheckWriteAccessWithCaptain] DENIED: User is not owner or crew\n")
7878+ slog.Debug("Write access denied: user is not owner or crew")
7879 }
7980 return isCrew
8081}