package authelia import ( "os" "path/filepath" "strings" "testing" "gopkg.in/yaml.v3" ) func TestGenerateConfig(t *testing.T) { dir := t.TempDir() m := NewManager(dir) opts := ConfigOpts{ Domain: "auth.payne.io", JWTSecret: "test-jwt-secret", SessionSecret: "test-session-secret", StorageEncKey: "test-storage-encryption-key-min-20", OIDCHMACSecret: "test-oidc-hmac-secret", DefaultPolicy: "one_factor", SessionDomain: "payne.io", DataDir: dir, ListenAddr: "127.0.0.1:9091", } if err := m.GenerateConfig(opts); err != nil { t.Fatalf("GenerateConfig: %v", err) } // Verify file exists configPath := filepath.Join(dir, "configuration.yml") data, err := os.ReadFile(configPath) if err != nil { t.Fatalf("reading config: %v", err) } content := string(data) // Check key values are present checks := []string{ "tcp://127.0.0.1:9091/", "one_factor", "payne.io", "auth.payne.io", "Wild Cloud", "argon2id", "test-session-secret", "test-storage-encryption-key-min-20", } for _, check := range checks { if !strings.Contains(content, check) { t.Errorf("config missing expected value: %q", check) } } // Verify it's valid YAML var parsed map[string]any if err := yaml.Unmarshal(data, &parsed); err != nil { t.Errorf("config is not valid YAML: %v", err) } // Check file permissions info, _ := os.Stat(configPath) if info.Mode().Perm() != 0600 { t.Errorf("config permissions = %o, want 0600", info.Mode().Perm()) } } func TestGenerateConfigWithOIDCClients(t *testing.T) { dir := t.TempDir() m := NewManager(dir) opts := ConfigOpts{ Domain: "auth.payne.io", JWTSecret: "jwt", SessionSecret: "session", StorageEncKey: "storage-enc-key-20chars", OIDCHMACSecret: "oidc-hmac", SessionDomain: "payne.io", OIDCClients: []OIDCClient{ { ID: "gitea", Name: "Gitea", Secret: "$pbkdf2-sha512$hashed", RedirectURIs: []string{"https://git.payne.io/user/oauth2/authelia/callback"}, Scopes: []string{"openid", "profile", "email"}, Policy: "one_factor", }, }, } if err := m.GenerateConfig(opts); err != nil { t.Fatalf("GenerateConfig: %v", err) } data, _ := os.ReadFile(filepath.Join(dir, "configuration.yml")) content := string(data) if !strings.Contains(content, "gitea") { t.Error("config should contain OIDC client 'gitea'") } if !strings.Contains(content, "git.payne.io") { t.Error("config should contain redirect URI") } } func TestGenerateConfigDefaults(t *testing.T) { dir := t.TempDir() m := NewManager(dir) // Minimal opts — should use defaults opts := ConfigOpts{ Domain: "auth.example.com", JWTSecret: "jwt", SessionSecret: "session", StorageEncKey: "enc-key-twenty-chars", SessionDomain: "example.com", } if err := m.GenerateConfig(opts); err != nil { t.Fatalf("GenerateConfig: %v", err) } data, _ := os.ReadFile(filepath.Join(dir, "configuration.yml")) content := string(data) // Should default to one_factor if !strings.Contains(content, "one_factor") { t.Error("should default to one_factor policy") } // Should default to 127.0.0.1:9091 if !strings.Contains(content, "127.0.0.1:9091") { t.Error("should default to 127.0.0.1:9091") } // Without OIDCHMACSecret, should not have identity_providers if strings.Contains(content, "identity_providers") { t.Error("should not include OIDC section without HMAC secret") } } func TestUserCRUD(t *testing.T) { dir := t.TempDir() m := NewManager(dir) // List empty users, err := m.ListUsers() if err != nil { t.Fatalf("ListUsers empty: %v", err) } if len(users) != 0 { t.Errorf("expected 0 users, got %d", len(users)) } // Create err = m.CreateUser(User{ Username: "admin", Displayname: "Admin User", Password: "supersecret", Email: "admin@example.com", }) if err != nil { t.Fatalf("CreateUser: %v", err) } // List after create users, err = m.ListUsers() if err != nil { t.Fatalf("ListUsers: %v", err) } if len(users) != 1 { t.Fatalf("expected 1 user, got %d", len(users)) } if users[0].Username != "admin" { t.Errorf("username = %q, want %q", users[0].Username, "admin") } if users[0].Displayname != "Admin User" { t.Errorf("displayname = %q, want %q", users[0].Displayname, "Admin User") } if users[0].Email != "admin@example.com" { t.Errorf("email = %q, want %q", users[0].Email, "admin@example.com") } // Password should be omitted in listing if users[0].Password != "" { t.Error("password should be empty in listing") } // Duplicate create should fail err = m.CreateUser(User{Username: "admin", Password: "x", Displayname: "dup"}) if err == nil { t.Error("duplicate create should fail") } // Update newName := "Super Admin" err = m.UpdateUser("admin", UserUpdate{Displayname: &newName}) if err != nil { t.Fatalf("UpdateUser: %v", err) } users, _ = m.ListUsers() if users[0].Displayname != "Super Admin" { t.Errorf("after update, displayname = %q, want %q", users[0].Displayname, "Super Admin") } // Update nonexistent user err = m.UpdateUser("nobody", UserUpdate{Displayname: &newName}) if err == nil { t.Error("update nonexistent should fail") } // Delete err = m.DeleteUser("admin") if err != nil { t.Fatalf("DeleteUser: %v", err) } users, _ = m.ListUsers() if len(users) != 0 { t.Errorf("after delete, expected 0 users, got %d", len(users)) } // Delete nonexistent err = m.DeleteUser("admin") if err == nil { t.Error("delete nonexistent should fail") } } func TestCreateUserValidation(t *testing.T) { dir := t.TempDir() m := NewManager(dir) if err := m.CreateUser(User{Password: "x", Displayname: "y"}); err == nil { t.Error("empty username should fail") } if err := m.CreateUser(User{Username: "x", Displayname: "y"}); err == nil { t.Error("empty password should fail") } } func TestHashPassword(t *testing.T) { hash, err := hashPassword("testpassword") if err != nil { t.Fatalf("hashPassword: %v", err) } if !strings.HasPrefix(hash, "$argon2id$v=19$") { t.Errorf("hash should start with $argon2id$v=19$, got %q", hash[:30]) } // Two hashes of the same password should differ (different salt) hash2, _ := hashPassword("testpassword") if hash == hash2 { t.Error("two hashes of the same password should differ (random salt)") } } func TestUserCount(t *testing.T) { dir := t.TempDir() m := NewManager(dir) if m.UserCount() != 0 { t.Error("empty db should have 0 users") } m.CreateUser(User{Username: "a", Password: "p", Displayname: "A"}) m.CreateUser(User{Username: "b", Password: "p", Displayname: "B"}) if m.UserCount() != 2 { t.Errorf("expected 2 users, got %d", m.UserCount()) } } func TestEnsureUsersDB(t *testing.T) { dir := t.TempDir() m := NewManager(dir) if err := m.EnsureUsersDB(); err != nil { t.Fatalf("EnsureUsersDB: %v", err) } // File should exist if _, err := os.Stat(m.usersDBPath()); err != nil { t.Errorf("users db file should exist: %v", err) } // Should be idempotent if err := m.EnsureUsersDB(); err != nil { t.Fatalf("EnsureUsersDB (second call): %v", err) } } func TestEnsureJWKS(t *testing.T) { dir := t.TempDir() m := NewManager(dir) if err := m.EnsureJWKS(); err != nil { t.Fatalf("EnsureJWKS: %v", err) } // File should exist data, err := os.ReadFile(m.jwksPath()) if err != nil { t.Fatalf("reading JWKS: %v", err) } if !strings.Contains(string(data), "RSA PRIVATE KEY") { t.Error("JWKS file should contain RSA private key") } // Should be idempotent (doesn't regenerate) if err := m.EnsureJWKS(); err != nil { t.Fatalf("EnsureJWKS (second call): %v", err) } } func TestGenerateSecret(t *testing.T) { s1, err := GenerateSecret(32) if err != nil { t.Fatalf("GenerateSecret: %v", err) } if len(s1) != 64 { // 32 bytes = 64 hex chars t.Errorf("secret length = %d, want 64", len(s1)) } s2, _ := GenerateSecret(32) if s1 == s2 { t.Error("two generated secrets should differ") } } func TestSessionDomainFromAuthDomain(t *testing.T) { tests := []struct { input string want string }{ {"auth.payne.io", "payne.io"}, {"auth.sub.example.com", "sub.example.com"}, {"localhost", "localhost"}, } for _, tt := range tests { got := SessionDomainFromAuthDomain(tt.input) if got != tt.want { t.Errorf("SessionDomainFromAuthDomain(%q) = %q, want %q", tt.input, got, tt.want) } } } func TestIsInstalled(t *testing.T) { m := NewManager(t.TempDir()) // Just verify it doesn't panic — the result depends on whether authelia is installed _ = m.IsInstalled() }