Files
YMhut-box-C-/server/unified-management/internal/auth/auth_test.go
T

209 lines
6.3 KiB
Go

package auth
import (
"context"
"errors"
"net/http"
"net/http/httptest"
"path/filepath"
"testing"
"ymhut-box/server/unified-management/internal/config"
"ymhut-box/server/unified-management/internal/db"
)
func TestBootstrapOnlyReturnsDefaultPasswordStateToAuthenticatedCaller(t *testing.T) {
root := t.TempDir()
cfg := &config.Config{
StorageDir: root,
Database: config.DatabaseConfig{
Provider: "sqlite",
SQLitePath: root + "/test.sqlite",
FailoverEnabled: true,
HealthIntervalSec: 3600,
},
}
store, err := db.Open(cfg)
if err != nil {
t.Fatal(err)
}
defer store.Close()
if err := store.EnsureDefaultAdmin(context.Background()); err != nil {
t.Fatal(err)
}
service := NewService(store)
publicPayload, err := service.Bootstrap(context.Background(), false)
if err != nil {
t.Fatal(err)
}
if _, exists := publicPayload["isDefaultPassword"]; exists || publicPayload["defaultPassword"] != nil || publicPayload["defaultUsername"] != nil {
t.Fatalf("public bootstrap leaked administrator state: %#v", publicPayload)
}
payload, err := service.Bootstrap(context.Background(), true)
if err != nil {
t.Fatal(err)
}
if payload["isDefaultPassword"] != true || payload["defaultPassword"] != nil || payload["defaultUsername"] != nil {
t.Fatalf("unexpected authenticated bootstrap payload: %#v", payload)
}
if err := store.ChangeAdminPassword(context.Background(), "admin", "admin", "changed-password"); err != nil {
t.Fatal(err)
}
payload, err = service.Bootstrap(context.Background(), true)
if err != nil {
t.Fatal(err)
}
if payload["isDefaultPassword"] != false || payload["defaultPassword"] != nil || payload["defaultUsername"] != nil {
t.Fatalf("default password fields leaked after change: %#v", payload)
}
}
func TestChangeAdminPasswordPersistsAfterReopen(t *testing.T) {
root := t.TempDir()
dbPath := filepath.Join(root, "test.sqlite")
cfg := &config.Config{
StorageDir: root,
Database: config.DatabaseConfig{
Provider: "sqlite",
SQLitePath: dbPath,
FailoverEnabled: true,
HealthIntervalSec: 3600,
},
}
store, err := db.Open(cfg)
if err != nil {
t.Fatal(err)
}
if err := store.EnsureDefaultAdmin(context.Background()); err != nil {
t.Fatal(err)
}
if err := store.ChangeAdminPassword(context.Background(), "admin", "admin", "persisted-password"); err != nil {
t.Fatal(err)
}
_ = store.Close()
reopened, err := db.Open(cfg)
if err != nil {
t.Fatal(err)
}
defer reopened.Close()
if _, ok, err := reopened.VerifyAdminPassword(context.Background(), "admin", "persisted-password"); err != nil || !ok {
t.Fatalf("new password did not persist, ok=%v err=%v", ok, err)
}
if _, ok, err := reopened.VerifyAdminPassword(context.Background(), "admin", "admin"); err != nil || ok {
t.Fatalf("old password still works, ok=%v err=%v", ok, err)
}
}
func TestLoginLocksAfterRepeatedFailures(t *testing.T) {
root := t.TempDir()
cfg := &config.Config{
StorageDir: root,
Database: config.DatabaseConfig{
Provider: "sqlite",
SQLitePath: filepath.Join(root, "test.sqlite"),
FailoverEnabled: true,
HealthIntervalSec: 3600,
},
}
store, err := db.Open(cfg)
if err != nil {
t.Fatal(err)
}
defer store.Close()
if err := store.EnsureDefaultAdmin(context.Background()); err != nil {
t.Fatal(err)
}
service := NewService(store)
for i := 0; i < loginMaxFails; i++ {
if _, _, ok, err := service.Login(context.Background(), "admin", "wrong", "bad-captcha", "00000", "127.0.0.1"); err != nil || ok {
t.Fatalf("failed login %d returned ok=%v err=%v", i, ok, err)
}
}
captcha, err := service.NewCaptcha()
if err != nil {
t.Fatal(err)
}
service.mu.Lock()
answer := service.captchas[captcha.ID].answer
service.mu.Unlock()
if _, _, failure, err := service.LoginDetailed(context.Background(), "admin", "admin", captcha.ID, answer, "127.0.0.1"); err != nil || failure != LoginFailureLocked {
t.Fatalf("locked login returned failure=%q err=%v", failure, err)
}
}
func TestLoginDetailedDistinguishesCaptchaFailure(t *testing.T) {
root := t.TempDir()
store, err := db.Open(&config.Config{
StorageDir: root,
Database: config.DatabaseConfig{Provider: "sqlite", SQLitePath: filepath.Join(root, "captcha.sqlite"), HealthIntervalSec: 3600},
})
if err != nil {
t.Fatal(err)
}
defer store.Close()
if err := store.EnsureDefaultAdmin(context.Background()); err != nil {
t.Fatal(err)
}
service := NewService(store)
_, _, failure, err := service.LoginDetailed(context.Background(), "admin", "admin", "missing", "00000", "127.0.0.1")
if err != nil || failure != LoginFailureCaptcha {
t.Fatalf("captcha login returned failure=%q err=%v", failure, err)
}
}
func TestLoginDatabaseCancellationDoesNotCountAsCredentialFailure(t *testing.T) {
root := t.TempDir()
store, err := db.Open(&config.Config{
StorageDir: root,
Database: config.DatabaseConfig{
Provider: "sqlite",
SQLitePath: filepath.Join(root, "cancel-login.sqlite"),
FailoverEnabled: true,
HealthIntervalSec: 3600,
},
})
if err != nil {
t.Fatal(err)
}
defer store.Close()
if err := store.EnsureDefaultAdmin(context.Background()); err != nil {
t.Fatal(err)
}
service := NewService(store)
captcha, err := service.NewCaptcha()
if err != nil {
t.Fatal(err)
}
service.mu.Lock()
answer := service.captchas[captcha.ID].answer
service.mu.Unlock()
ctx, cancel := context.WithCancel(context.Background())
cancel()
_, _, ok, err := service.Login(ctx, "admin", "admin", captcha.ID, answer, "127.0.0.1")
if !errors.Is(err, context.Canceled) || ok {
t.Fatalf("canceled login returned ok=%v err=%v", ok, err)
}
service.mu.Lock()
_, exists := service.loginAttempts[loginAttemptKey("admin", "127.0.0.1")]
service.mu.Unlock()
if exists {
t.Fatal("database cancellation was counted as a credential failure")
}
}
func TestSessionCookieUsesSecureForForwardedHTTPS(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "/api/admin/auth/login", nil)
req.Header.Set("X-Forwarded-Proto", "https")
res := httptest.NewRecorder()
SetSessionCookieForRequest(res, req, "session-id")
cookies := res.Result().Cookies()
if len(cookies) != 1 {
t.Fatalf("expected one cookie, got %d", len(cookies))
}
if !cookies[0].Secure {
t.Fatalf("expected secure cookie for forwarded https: %#v", cookies[0])
}
}