Go · 7334 bytes Raw Blame History
1 // SPDX-License-Identifier: AGPL-3.0-or-later
2
3 package config
4
5 import (
6 "strings"
7 "testing"
8 "time"
9 )
10
11 func TestDefaults_Validate(t *testing.T) {
12 t.Parallel()
13 cfg := Defaults()
14 if err := Validate(&cfg); err != nil {
15 t.Fatalf("Validate(Defaults()): %v", err)
16 }
17 }
18
19 func TestValidate_RejectsBadEnv(t *testing.T) {
20 t.Parallel()
21 cfg := Defaults()
22 cfg.Env = "production" // typo of "prod"
23 if err := Validate(&cfg); err == nil {
24 t.Errorf("expected validation error for env=production")
25 }
26 }
27
28 func TestValidate_RejectsTracingWithoutEndpoint(t *testing.T) {
29 t.Parallel()
30 cfg := Defaults()
31 cfg.Tracing.Enabled = true
32 if err := Validate(&cfg); err == nil {
33 t.Errorf("expected validation error when tracing.enabled=true and endpoint empty")
34 }
35 }
36
37 func TestValidate_RejectsBadSampleRate(t *testing.T) {
38 t.Parallel()
39 cfg := Defaults()
40 cfg.Tracing.Enabled = true
41 cfg.Tracing.Endpoint = "http://otel:4318"
42 cfg.Tracing.SampleRate = 2.0
43 if err := Validate(&cfg); err == nil {
44 t.Errorf("expected validation error for sample_rate=2.0")
45 }
46 }
47
48 func TestValidate_BillingRequiresStripeSettings(t *testing.T) {
49 t.Parallel()
50 cfg := Defaults()
51 cfg.Billing.Enabled = true
52 if err := Validate(&cfg); err == nil || !strings.Contains(err.Error(), "billing.stripe.secret_key") {
53 t.Fatalf("Validate missing secret key: got %v", err)
54 }
55
56 cfg.Billing.Stripe.SecretKey = "sk_test_123"
57 if err := Validate(&cfg); err == nil || !strings.Contains(err.Error(), "billing.stripe.webhook_secret") {
58 t.Fatalf("Validate missing webhook secret: got %v", err)
59 }
60
61 cfg.Billing.Stripe.WebhookSecret = "whsec_123"
62 if err := Validate(&cfg); err == nil || !strings.Contains(err.Error(), "billing.stripe.team_price_id") {
63 t.Fatalf("Validate missing team price: got %v", err)
64 }
65
66 cfg.Billing.Stripe.TeamPriceID = "price_123"
67 if err := Validate(&cfg); err != nil {
68 t.Fatalf("Validate complete billing config: %v", err)
69 }
70 }
71
72 func TestValidate_BillingRejectsBadURLs(t *testing.T) {
73 t.Parallel()
74 cfg := Defaults()
75 cfg.Billing.Stripe.SuccessURL = "/relative"
76 if err := Validate(&cfg); err == nil || !strings.Contains(err.Error(), "billing.stripe.success_url") {
77 t.Fatalf("Validate bad success URL: got %v", err)
78 }
79
80 cfg = Defaults()
81 cfg.Billing.Enabled = true
82 cfg.Billing.Stripe.SecretKey = "sk_test_123"
83 cfg.Billing.Stripe.WebhookSecret = "whsec_123"
84 cfg.Billing.Stripe.TeamPriceID = "price_123"
85 cfg.Auth.BaseURL = "shithub.local"
86 if err := Validate(&cfg); err == nil || !strings.Contains(err.Error(), "auth.base_url") {
87 t.Fatalf("Validate bad auth base URL with billing enabled: got %v", err)
88 }
89 }
90
91 func TestMergeEnv_AppliesNestedKeys(t *testing.T) {
92 t.Parallel()
93 cfg := Defaults()
94 env := []string{
95 "SHITHUB_WEB__ADDR=:9090",
96 "SHITHUB_DB__MAX_CONNS=42",
97 "SHITHUB_TRACING__ENABLED=true",
98 "SHITHUB_TRACING__ENDPOINT=http://otel:4318",
99 "SHITHUB_DB__CONNECT_TIMEOUT=8s",
100 "SHITHUB_BILLING__ENABLED=true",
101 "SHITHUB_BILLING__GRACE_PERIOD=240h",
102 "SHITHUB_BILLING__STRIPE__SECRET_KEY=sk_test_123",
103 "SHITHUB_BILLING__STRIPE__WEBHOOK_SECRET=whsec_123",
104 "SHITHUB_BILLING__STRIPE__TEAM_PRICE_ID=price_123",
105 "SHITHUB_BILLING__STRIPE__AUTOMATIC_TAX=true",
106 }
107 if err := mergeEnv(&cfg, env); err != nil {
108 t.Fatalf("mergeEnv: %v", err)
109 }
110 if cfg.Web.Addr != ":9090" {
111 t.Errorf("Web.Addr: got %q", cfg.Web.Addr)
112 }
113 if cfg.DB.MaxConns != 42 {
114 t.Errorf("DB.MaxConns: got %d", cfg.DB.MaxConns)
115 }
116 if !cfg.Tracing.Enabled {
117 t.Errorf("Tracing.Enabled: not set")
118 }
119 if cfg.DB.ConnectTimeout != 8*time.Second {
120 t.Errorf("DB.ConnectTimeout: got %v", cfg.DB.ConnectTimeout)
121 }
122 if !cfg.Billing.Enabled {
123 t.Errorf("Billing.Enabled: not set")
124 }
125 if cfg.Billing.GracePeriod != 240*time.Hour {
126 t.Errorf("Billing.GracePeriod: got %v", cfg.Billing.GracePeriod)
127 }
128 if cfg.Billing.Stripe.SecretKey != "sk_test_123" {
129 t.Errorf("Billing.Stripe.SecretKey: got %q", cfg.Billing.Stripe.SecretKey)
130 }
131 if cfg.Billing.Stripe.WebhookSecret != "whsec_123" {
132 t.Errorf("Billing.Stripe.WebhookSecret: got %q", cfg.Billing.Stripe.WebhookSecret)
133 }
134 if cfg.Billing.Stripe.TeamPriceID != "price_123" {
135 t.Errorf("Billing.Stripe.TeamPriceID: got %q", cfg.Billing.Stripe.TeamPriceID)
136 }
137 if !cfg.Billing.Stripe.AutomaticTax {
138 t.Errorf("Billing.Stripe.AutomaticTax: not set")
139 }
140 }
141
142 func TestPrintRedacted_HidesSecrets(t *testing.T) {
143 t.Parallel()
144 cfg := Defaults()
145 cfg.DB.URL = "postgres://shithub:hunter2@localhost/shithub"
146 cfg.Session.KeyB64 = "supersecretkey"
147 cfg.Metrics.BasicAuthPass = "metrics-pass"
148 cfg.ErrorReporting.DSN = "https://abc@sentry.example/1"
149 cfg.Billing.Stripe.SecretKey = "sk_live_secret"
150 cfg.Billing.Stripe.WebhookSecret = "whsec_secret"
151
152 out, err := PrintRedacted(cfg)
153 if err != nil {
154 t.Fatalf("PrintRedacted: %v", err)
155 }
156
157 for _, leak := range []string{"hunter2", "supersecretkey", "metrics-pass", "https://abc@sentry", "sk_live_secret", "whsec_secret"} {
158 if strings.Contains(out, leak) {
159 t.Errorf("PrintRedacted leaked %q\noutput: %s", leak, out)
160 }
161 }
162 if !strings.Contains(out, "***") {
163 t.Errorf("PrintRedacted produced no *** redactions; output:\n%s", out)
164 }
165 }
166
167 func TestMergeFlags_OverridesEnv(t *testing.T) {
168 t.Parallel()
169 cfg := Defaults()
170 if err := mergeEnv(&cfg, []string{"SHITHUB_WEB__ADDR=:9090"}); err != nil {
171 t.Fatalf("mergeEnv: %v", err)
172 }
173 if err := mergeFlags(&cfg, map[string]string{"web.addr": ":7777"}); err != nil {
174 t.Fatalf("mergeFlags: %v", err)
175 }
176 if cfg.Web.Addr != ":7777" {
177 t.Errorf("Web.Addr: got %q, want :7777", cfg.Web.Addr)
178 }
179 }
180
181 func TestDefaults_RateLimitAPI(t *testing.T) {
182 t.Parallel()
183 cfg := Defaults()
184 if cfg.RateLimit.API.AuthedPerHour != 5000 {
185 t.Errorf("RateLimit.API.AuthedPerHour: got %d, want 5000", cfg.RateLimit.API.AuthedPerHour)
186 }
187 if cfg.RateLimit.API.AnonPerHour != 60 {
188 t.Errorf("RateLimit.API.AnonPerHour: got %d, want 60", cfg.RateLimit.API.AnonPerHour)
189 }
190 }
191
192 func TestValidate_RejectsNegativeRateLimit(t *testing.T) {
193 t.Parallel()
194 cfg := Defaults()
195 cfg.RateLimit.API.AuthedPerHour = -1
196 if err := Validate(&cfg); err == nil {
197 t.Errorf("expected validation error for ratelimit.api.authed_per_hour=-1")
198 }
199 cfg = Defaults()
200 cfg.RateLimit.API.AnonPerHour = -5
201 if err := Validate(&cfg); err == nil {
202 t.Errorf("expected validation error for ratelimit.api.anon_per_hour=-5")
203 }
204 }
205
206 func TestValidate_RateLimitZeroFillsDefault(t *testing.T) {
207 t.Parallel()
208 cfg := Defaults()
209 cfg.RateLimit.API.AuthedPerHour = 0
210 cfg.RateLimit.API.AnonPerHour = 0
211 if err := Validate(&cfg); err != nil {
212 t.Fatalf("Validate: %v", err)
213 }
214 if cfg.RateLimit.API.AuthedPerHour != 5000 {
215 t.Errorf("zero-fill authed: got %d, want 5000", cfg.RateLimit.API.AuthedPerHour)
216 }
217 if cfg.RateLimit.API.AnonPerHour != 60 {
218 t.Errorf("zero-fill anon: got %d, want 60", cfg.RateLimit.API.AnonPerHour)
219 }
220 }
221
222 func TestMergeEnv_RateLimitAPI(t *testing.T) {
223 t.Parallel()
224 cfg := Defaults()
225 env := []string{
226 "SHITHUB_RATELIMIT__API__AUTHED_PER_HOUR=10000",
227 "SHITHUB_RATELIMIT__API__ANON_PER_HOUR=120",
228 }
229 if err := mergeEnv(&cfg, env); err != nil {
230 t.Fatalf("mergeEnv: %v", err)
231 }
232 if cfg.RateLimit.API.AuthedPerHour != 10000 {
233 t.Errorf("AuthedPerHour: got %d, want 10000", cfg.RateLimit.API.AuthedPerHour)
234 }
235 if cfg.RateLimit.API.AnonPerHour != 120 {
236 t.Errorf("AnonPerHour: got %d, want 120", cfg.RateLimit.API.AnonPerHour)
237 }
238 }
239