TASK-SECADV-001: WebAuthn/Passkeys (F022) - WebAuthn credential model, service, handler - Registration/authentication ceremony endpoints - CRUD operations (list, rename, delete passkeys) - Routes: GET/POST/PUT/DELETE /auth/passkeys/* TASK-SECADV-002: Configurable password policy (F015) - PasswordPolicyConfig with MinLength, MaxLength, RequireUpper/Lower/Number/Special - NewPasswordValidatorWithPolicy constructor - PasswordPolicyFromEnv() reads env vars (PASSWORD_MIN_LENGTH, etc.) - All character class checks now respect policy configuration TASK-SECADV-003: Géolocalisation connexions (F025) - GeoIPResolver interface + GeoIPService implementation - Country/city columns added to login_history table - LoginHistoryService.Record() performs GeoIP lookup - GetUserHistory returns geolocation data - GET /auth/login-history endpoint TASK-SECADV-004: Password expiration (F016) - password_changed_at column on users table - CheckPasswordExpiration() method on PasswordService - All password change/reset methods now set password_changed_at - NewPasswordServiceWithPolicy() supports expiration days config Migration: 971_security_advanced_v0133.sql Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
59 lines
1.5 KiB
Go
59 lines
1.5 KiB
Go
package services
|
|
|
|
import (
|
|
"os"
|
|
"testing"
|
|
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
func TestNewGeoIPServiceDisabled(t *testing.T) {
|
|
os.Unsetenv("GEOIP_DB_PATH")
|
|
logger := zap.NewNop()
|
|
svc := NewGeoIPService(logger)
|
|
if svc.IsEnabled() {
|
|
t.Error("expected GeoIP to be disabled when GEOIP_DB_PATH not set")
|
|
}
|
|
|
|
country, city := svc.Lookup("1.2.3.4")
|
|
if country != "" || city != "" {
|
|
t.Error("expected empty results when disabled")
|
|
}
|
|
}
|
|
|
|
func TestNewGeoIPServiceMissingDB(t *testing.T) {
|
|
os.Setenv("GEOIP_DB_PATH", "/nonexistent/path.mmdb")
|
|
defer os.Unsetenv("GEOIP_DB_PATH")
|
|
|
|
logger := zap.NewNop()
|
|
svc := NewGeoIPService(logger)
|
|
if svc.IsEnabled() {
|
|
t.Error("expected GeoIP to be disabled when DB file doesn't exist")
|
|
}
|
|
}
|
|
|
|
func TestGeoIPLookupPrivateIPs(t *testing.T) {
|
|
// Even if enabled, private IPs should return empty
|
|
svc := &GeoIPService{logger: zap.NewNop(), enabled: true}
|
|
|
|
tests := []string{"127.0.0.1", "::1", "192.168.1.1", "10.0.0.1", "0.0.0.0"}
|
|
for _, ip := range tests {
|
|
country, city := svc.Lookup(ip)
|
|
if country != "" || city != "" {
|
|
t.Errorf("expected empty for private IP %s, got country=%q city=%q", ip, country, city)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestGeoIPLookupInvalidIP(t *testing.T) {
|
|
svc := &GeoIPService{logger: zap.NewNop(), enabled: true}
|
|
country, city := svc.Lookup("not-an-ip")
|
|
if country != "" || city != "" {
|
|
t.Error("expected empty for invalid IP")
|
|
}
|
|
}
|
|
|
|
// TestGeoIPResolverInterface verifies GeoIPService satisfies GeoIPResolver.
|
|
func TestGeoIPResolverInterface(t *testing.T) {
|
|
var _ GeoIPResolver = &GeoIPService{}
|
|
}
|