newdrive/internal/adapters/postgres/migrate_integration_test.go
Elijah 715423ab8e
Some checks failed
CI / Backend (push) Failing after 1s
CI / Frontend (push) Successful in 13s
CI / Contracts and repository policy (push) Failing after 3s
CI / Container (push) Has been skipped
Implement owner setup and browser authentication sessions
2026-07-16 19:45:40 -07:00

137 lines
4.4 KiB
Go

package postgres
import (
"context"
"errors"
"os"
"sync"
"testing"
"time"
"drive.local/drivev2/internal/adapters/authn"
"drive.local/drivev2/internal/application"
)
type integrationPasswordHasher struct{}
func (integrationPasswordHasher) Hash(password string) (string, error) {
return "integration:" + password, nil
}
func (integrationPasswordHasher) Verify(password, encoded string) (bool, error) {
return encoded == "integration:"+password, nil
}
func TestMigrationsAndStore(t *testing.T) {
databaseURL := os.Getenv("DRIVE_TEST_DATABASE_URL")
if databaseURL == "" {
t.Skip("DRIVE_TEST_DATABASE_URL is not set")
}
if err := Migrate(databaseURL); err != nil {
t.Fatalf("first migration run failed: %v", err)
}
if err := Migrate(databaseURL); err != nil {
t.Fatalf("second migration run failed: %v", err)
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
store, err := Open(ctx, databaseURL)
if err != nil {
t.Fatalf("Open returned an error: %v", err)
}
defer store.Close()
if err := store.DatabaseHealth(ctx); err != nil {
t.Fatalf("DatabaseHealth returned an error: %v", err)
}
initialized, err := store.OwnerExists(ctx)
if err != nil {
t.Fatalf("OwnerExists returned an error: %v", err)
}
if initialized {
t.Fatal("OwnerExists = true on a fresh database, want false")
}
authentication := application.NewAuthenticationService(
store,
integrationPasswordHasher{},
authn.CredentialGenerator{},
authn.SystemClock{},
)
inputs := []application.SetupOwnerInput{
{Username: "owner-one", DisplayName: "Owner One", Password: "correct horse battery staple"},
{Username: "owner-two", DisplayName: "Owner Two", Password: "correct horse battery staple"},
}
results := make([]application.SetupOwnerResult, len(inputs))
errorsByAttempt := make([]error, len(inputs))
var waitGroup sync.WaitGroup
for index := range inputs {
waitGroup.Add(1)
go func() {
defer waitGroup.Done()
results[index], errorsByAttempt[index] = authentication.SetupOwner(ctx, inputs[index])
}()
}
waitGroup.Wait()
winner := -1
losers := 0
for index, setupErr := range errorsByAttempt {
switch {
case setupErr == nil:
winner = index
case errors.Is(setupErr, application.ErrSetupAlreadyComplete):
losers++
default:
t.Fatalf("setup attempt %d returned unexpected error: %v", index, setupErr)
}
}
if winner < 0 || losers != 1 {
t.Fatalf("winner = %d, losing attempts = %d, want one of each", winner, losers)
}
if len(results[winner].RecoveryCodes) != application.RecoveryCodeCount {
t.Fatalf("recovery code count = %d, want %d", len(results[winner].RecoveryCodes), application.RecoveryCodeCount)
}
counts, err := store.queries.GetAuthenticationInvariantCounts(ctx)
if err != nil {
t.Fatalf("GetAuthenticationInvariantCounts returned an error: %v", err)
}
if counts.Owners != 1 || counts.Roots != 1 || counts.RecoveryCodes != 10 || counts.Changes != 1 ||
counts.AuditEvents != 1 || counts.Sessions != 1 || counts.ActiveSessions != 1 {
t.Fatalf("unexpected post-setup counts: %#v", counts)
}
loggedIn, err := authentication.Login(ctx, application.LoginInput{
Username: inputs[winner].Username, Password: inputs[winner].Password,
})
if err != nil {
t.Fatalf("Login returned an error: %v", err)
}
current, err := authentication.CurrentSession(ctx, loggedIn.SessionToken)
if err != nil {
t.Fatalf("CurrentSession returned an error: %v", err)
}
if current.ID != loggedIn.Session.ID {
t.Fatalf("current session ID = %q, want %q", current.ID, loggedIn.Session.ID)
}
if err := authentication.Logout(ctx, loggedIn.SessionToken, loggedIn.CSRFToken, "wrong-csrf"); !errors.Is(err, application.ErrCSRFValidation) {
t.Fatalf("Logout with invalid CSRF error = %v, want %v", err, application.ErrCSRFValidation)
}
if err := authentication.Logout(ctx, loggedIn.SessionToken, loggedIn.CSRFToken, loggedIn.CSRFToken); err != nil {
t.Fatalf("Logout returned an error: %v", err)
}
if _, err := authentication.CurrentSession(ctx, loggedIn.SessionToken); !errors.Is(err, application.ErrSessionNotFound) {
t.Fatalf("CurrentSession after logout error = %v, want %v", err, application.ErrSessionNotFound)
}
counts, err = store.queries.GetAuthenticationInvariantCounts(ctx)
if err != nil {
t.Fatalf("GetAuthenticationInvariantCounts after logout returned an error: %v", err)
}
if counts.Sessions != 2 || counts.ActiveSessions != 1 || counts.AuditEvents != 3 {
t.Fatalf("unexpected post-logout counts: %#v", counts)
}
}