137 lines
4.4 KiB
Go
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)
|
|
}
|
|
}
|