123 lines
4.1 KiB
Go
123 lines
4.1 KiB
Go
package postgres
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"os"
|
|
"strconv"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/longpeng/configcenter/internal/domain"
|
|
storepkg "github.com/longpeng/configcenter/internal/store"
|
|
)
|
|
|
|
func TestMigrationChecksum(t *testing.T) {
|
|
const want = "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
|
|
if got := migrationChecksum([]byte("abc")); got != want {
|
|
t.Fatalf("unexpected SHA-256 checksum: got %s, want %s", got, want)
|
|
}
|
|
}
|
|
|
|
func TestUserLifecycleAndAuditIntegration(t *testing.T) {
|
|
databaseURL := os.Getenv("TEST_DATABASE_URL")
|
|
if databaseURL == "" {
|
|
t.Skip("TEST_DATABASE_URL is not configured")
|
|
}
|
|
ctx := context.Background()
|
|
repository, err := Open(ctx, databaseURL)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer repository.Close()
|
|
if err := repository.Migrate(ctx); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
suffix := strconv.FormatInt(time.Now().UnixNano(), 36)
|
|
rootName := "root-" + suffix
|
|
backupName := "backup-" + suffix
|
|
appCode := "audit-" + suffix
|
|
defer func() {
|
|
_, _ = repository.pool.Exec(context.Background(), `DELETE FROM users WHERE username IN ($1,$2)`, rootName, backupName)
|
|
_, _ = repository.pool.Exec(context.Background(), `DELETE FROM applications WHERE app_code=$1`, appCode)
|
|
}()
|
|
|
|
if err := repository.EnsureBootstrapAdmin(ctx, rootName, "root-hash", "Root"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
root, err := repository.FindUserByUsername(ctx, rootName)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if root.TokenVersion != 1 || root.UpdatedAt.IsZero() {
|
|
t.Fatalf("unexpected bootstrap credential: %#v", root)
|
|
}
|
|
backup, err := repository.CreateUser(ctx, domain.User{Username: backupName, DisplayName: "Backup", IsAdmin: true, UpdatedBy: rootName}, "backup-hash")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if _, err := repository.SetUserDisabled(ctx, root.ID, true, backupName); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
rootAfterDisable, err := repository.FindUserByID(ctx, root.ID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !rootAfterDisable.Disabled || rootAfterDisable.TokenVersion != 2 {
|
|
t.Fatalf("disable did not rotate token version: %#v", rootAfterDisable)
|
|
}
|
|
if err := repository.DeleteUser(ctx, backup.ID, rootName); !errors.Is(err, storepkg.ErrLastAdmin) {
|
|
t.Fatalf("deleting last active admin must fail: %v", err)
|
|
}
|
|
if _, err := repository.SetUserDisabled(ctx, root.ID, false, backupName); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if err := repository.ResetUserPassword(ctx, backup.ID, "new-backup-hash", rootName); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
backupCredential, err := repository.FindUserByID(ctx, backup.ID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if backupCredential.TokenVersion != 2 || backupCredential.PasswordHash != "new-backup-hash" {
|
|
t.Fatalf("password reset did not rotate credential: %#v", backupCredential)
|
|
}
|
|
|
|
app, err := repository.CreateApplication(ctx, domain.Application{Code: appCode, Name: "Audit Test", UpdatedBy: rootName})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := repository.SetUserAppRole(ctx, domain.UserAppRole{UserID: backup.ID, AppID: app.ID, Role: "viewer", UpdatedBy: rootName}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
backupCredential, _ = repository.FindUserByID(ctx, backup.ID)
|
|
if backupCredential.TokenVersion != 3 {
|
|
t.Fatalf("role change did not rotate token version: %d", backupCredential.TokenVersion)
|
|
}
|
|
if err := repository.DeleteUserAppRole(ctx, backup.ID, app.ID, rootName); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
backupCredential, _ = repository.FindUserByID(ctx, backup.ID)
|
|
if backupCredential.TokenVersion != 4 {
|
|
t.Fatalf("role removal did not rotate token version: %d", backupCredential.TokenVersion)
|
|
}
|
|
|
|
page, err := repository.ListAuditLogs(ctx, domain.AuditLogQuery{Actor: rootName, TargetType: "user", Limit: 1})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(page.Items) != 1 || page.NextCursor == nil {
|
|
t.Fatalf("expected filtered cursor page: %#v", page)
|
|
}
|
|
next, err := repository.ListAuditLogs(ctx, domain.AuditLogQuery{Actor: rootName, TargetType: "user", Limit: 10, BeforeID: *page.NextCursor})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(next.Items) == 0 || next.Items[0].ID >= page.Items[0].ID {
|
|
t.Fatalf("audit cursor did not advance: first=%#v next=%#v", page.Items, next.Items)
|
|
}
|
|
}
|