问题:移动网络共享出口 IP 会造成游客取号被误限流。 实现:移除公开取号 IP 限制,改用项目总量与手机号 HMAC 限流,并支持 Retry-After 倒计时。
141 lines
5.5 KiB
Go
141 lines
5.5 KiB
Go
package config
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestLoadAcceptsExactly32ByteKeys(t *testing.T) {
|
|
values := map[string]string{
|
|
"DATABASE_URL": "postgres://localhost/test",
|
|
"DATA_ENCRYPTION_KEY_BASE64": base64.StdEncoding.EncodeToString([]byte(strings.Repeat("a", 32))),
|
|
"PHONE_HMAC_KEY_BASE64": base64.StdEncoding.EncodeToString([]byte(strings.Repeat("b", 32))),
|
|
"SESSION_COOKIE_SECURE": "false",
|
|
}
|
|
cfg, err := load(func(key string) (string, bool) { value, ok := values[key]; return value, ok })
|
|
if err != nil {
|
|
t.Fatalf("load returned error: %v", err)
|
|
}
|
|
if len(cfg.EncryptionKey) != 32 || len(cfg.PhoneHMACKey) != 32 {
|
|
t.Fatalf("unexpected key lengths: %d, %d", len(cfg.EncryptionKey), len(cfg.PhoneHMACKey))
|
|
}
|
|
}
|
|
|
|
func TestLoadRejectsWrongKeyLength(t *testing.T) {
|
|
values := map[string]string{
|
|
"DATABASE_URL": "postgres://localhost/test",
|
|
"DATA_ENCRYPTION_KEY_BASE64": base64.StdEncoding.EncodeToString([]byte(strings.Repeat("a", 31))),
|
|
"PHONE_HMAC_KEY_BASE64": base64.StdEncoding.EncodeToString([]byte(strings.Repeat("b", 32))),
|
|
}
|
|
_, err := load(func(key string) (string, bool) { value, ok := values[key]; return value, ok })
|
|
if err == nil || !strings.Contains(err.Error(), "exactly 32 bytes") {
|
|
t.Fatalf("expected exact length error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestLoadRequiresSecureCookieInProduction(t *testing.T) {
|
|
key := base64.StdEncoding.EncodeToString([]byte(strings.Repeat("x", 32)))
|
|
values := map[string]string{
|
|
"APP_ENV": "production",
|
|
"DATABASE_URL": "postgres://localhost/test",
|
|
"DATA_ENCRYPTION_KEY_BASE64": key,
|
|
"PHONE_HMAC_KEY_BASE64": key,
|
|
"SESSION_COOKIE_SECURE": "false",
|
|
}
|
|
_, err := load(func(key string) (string, bool) { value, ok := values[key]; return value, ok })
|
|
if err == nil || !strings.Contains(err.Error(), "must be true") {
|
|
t.Fatalf("expected production cookie error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestLoadRequiresTLSAndReleaseMigrationsInProduction(t *testing.T) {
|
|
key := base64.StdEncoding.EncodeToString([]byte(strings.Repeat("x", 32)))
|
|
values := map[string]string{
|
|
"APP_ENV": "production",
|
|
"DATABASE_URL": "postgres://queue:secret@pg.internal/queue?sslmode=verify-full",
|
|
"DATA_ENCRYPTION_KEY_BASE64": key,
|
|
"PHONE_HMAC_KEY_BASE64": key,
|
|
"SESSION_COOKIE_SECURE": "true",
|
|
"MIGRATE_ON_START": "false",
|
|
}
|
|
if _, err := load(func(key string) (string, bool) { value, ok := values[key]; return value, ok }); err != nil {
|
|
t.Fatalf("secure production configuration rejected: %v", err)
|
|
}
|
|
|
|
values["DATABASE_URL"] = "postgres://queue:secret@pg.internal/queue?sslmode=disable"
|
|
if _, err := load(func(key string) (string, bool) { value, ok := values[key]; return value, ok }); err == nil || !strings.Contains(err.Error(), "TLS") {
|
|
t.Fatalf("expected production database TLS error, got %v", err)
|
|
}
|
|
|
|
values["DATABASE_URL"] = "postgres://queue:secret@pg.internal/queue?sslmode=verify-full"
|
|
values["MIGRATE_ON_START"] = "true"
|
|
if _, err := load(func(key string) (string, bool) { value, ok := values[key]; return value, ok }); err == nil || !strings.Contains(err.Error(), "MIGRATE_ON_START") {
|
|
t.Fatalf("expected production migration mode error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestLoadConfiguresPublicTicketRateLimits(t *testing.T) {
|
|
key := base64.StdEncoding.EncodeToString([]byte(strings.Repeat("x", 32)))
|
|
values := map[string]string{
|
|
"DATABASE_URL": "postgres://localhost/test",
|
|
"DATA_ENCRYPTION_KEY_BASE64": key,
|
|
"PHONE_HMAC_KEY_BASE64": key,
|
|
"SESSION_COOKIE_SECURE": "false",
|
|
}
|
|
lookup := func(name string) (string, bool) { value, ok := values[name]; return value, ok }
|
|
|
|
cfg, err := load(lookup)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if cfg.PublicTicketProjectLimit != 1000 || cfg.PublicTicketPhoneLimit != 5 || cfg.PublicTicketRateWindow != time.Minute {
|
|
t.Fatalf("unexpected public ticket limits: project=%d phone=%d window=%s",
|
|
cfg.PublicTicketProjectLimit, cfg.PublicTicketPhoneLimit, cfg.PublicTicketRateWindow)
|
|
}
|
|
|
|
values["PUBLIC_TICKET_PROJECT_LIMIT"] = "1200"
|
|
values["PUBLIC_TICKET_PHONE_LIMIT"] = "8"
|
|
values["PUBLIC_TICKET_RATE_WINDOW"] = "2m"
|
|
cfg, err = load(lookup)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if cfg.PublicTicketProjectLimit != 1200 || cfg.PublicTicketPhoneLimit != 8 || cfg.PublicTicketRateWindow != 2*time.Minute {
|
|
t.Fatalf("configured public ticket limits not applied: project=%d phone=%d window=%s",
|
|
cfg.PublicTicketProjectLimit, cfg.PublicTicketPhoneLimit, cfg.PublicTicketRateWindow)
|
|
}
|
|
}
|
|
|
|
func TestLoadRejectsInvalidPublicTicketRateLimits(t *testing.T) {
|
|
key := base64.StdEncoding.EncodeToString([]byte(strings.Repeat("x", 32)))
|
|
base := map[string]string{
|
|
"DATABASE_URL": "postgres://localhost/test",
|
|
"DATA_ENCRYPTION_KEY_BASE64": key,
|
|
"PHONE_HMAC_KEY_BASE64": key,
|
|
"SESSION_COOKIE_SECURE": "false",
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
key string
|
|
value string
|
|
}{
|
|
{name: "project limit", key: "PUBLIC_TICKET_PROJECT_LIMIT", value: "0"},
|
|
{name: "phone limit", key: "PUBLIC_TICKET_PHONE_LIMIT", value: "0"},
|
|
{name: "window", key: "PUBLIC_TICKET_RATE_WINDOW", value: "0s"},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
values := make(map[string]string, len(base)+1)
|
|
for name, value := range base {
|
|
values[name] = value
|
|
}
|
|
values[test.key] = test.value
|
|
if _, err := load(func(name string) (string, bool) { value, ok := values[name]; return value, ok }); err == nil {
|
|
t.Fatalf("%s=%q should be rejected", test.key, test.value)
|
|
}
|
|
})
|
|
}
|
|
}
|