package store_test import ( "context" "errors" "testing" "atlas9.dev/c/core" "atlas9.dev/c/core/assert" "atlas9.dev/c/core/dbi" "atlas9.dev/c/demo/lib/access" "atlas9.dev/c/demo/lib/mfa" "atlas9.dev/c/demo/store" ) // seedChallengeUser inserts the users row the challenge FK requires. func seedChallengeUser(t *testing.T, db dbi.DBI, ctx context.Context, id core.ID) { t.Helper() _, err := db.Exec(ctx, `INSERT INTO users (id, email, name, verified) VALUES ($1, $2, '', 1)`, id, id.String()+"@test") assert.Ok(t, err) } func TestSqliteMfaChallengeStore(t *testing.T) { db := setupTestDB(t) tx, err := db.Begin() assert.Ok(t, err) t.Cleanup(func() { tx.Rollback() }) w := dbi.WrapTx(tx) ctx := t.Context() user := core.NewID("usr") seedChallengeUser(t, w, ctx, user) s := store.NewSqliteMfaChallengeStore(w, access.AllowAll{}) // The correct code verifies once, then the challenge is consumed. t.Run("correct code verifies once", func(t *testing.T) { id, code, err := s.Create(ctx, user) assert.Ok(t, err) got, err := s.Verify(ctx, id, code) assert.Ok(t, err) assert.Eq(t, got, user) _, err = s.Verify(ctx, id, code) assert.Eq(t, errors.Is(err, core.ErrNotFound), true) }) // A wrong code is rejected but leaves the challenge usable while attempts // remain. t.Run("wrong code keeps the challenge", func(t *testing.T) { id, code, err := s.Create(ctx, user) assert.Ok(t, err) wrong := "000000" if wrong == code { wrong = "111111" } _, err = s.Verify(ctx, id, wrong) assert.Eq(t, errors.Is(err, mfa.ErrInvalidCode), true) got, err := s.Verify(ctx, id, code) assert.Ok(t, err) assert.Eq(t, got, user) }) // Too many wrong attempts discards the challenge entirely. t.Run("discarded after max attempts", func(t *testing.T) { id, _, err := s.Create(ctx, user) assert.Ok(t, err) for range mfa.MaxAttempts { s.Verify(ctx, id, "000000") } _, err = s.Verify(ctx, id, "000000") assert.Eq(t, errors.Is(err, core.ErrNotFound), true) }) // An expired challenge is treated as missing. t.Run("expired challenge not found", func(t *testing.T) { _, err := w.Exec(ctx, `INSERT INTO mfa_challenges (id, user_id, code_hash, expires_at, attempts) VALUES ('expired', $1, 'x', datetime('now','-1 minute'), 0)`, user) assert.Ok(t, err) _, err = s.Verify(ctx, "expired", "000000") assert.Eq(t, errors.Is(err, core.ErrNotFound), true) }) } // RequiredForUser is true exactly when a tenant the user belongs to has the // requirement on. func TestSqliteMfaPolicyStore_RequiredForUser(t *testing.T) { db := setupTestDB(t) tx, err := db.Begin() assert.Ok(t, err) t.Cleanup(func() { tx.Rollback() }) w := dbi.WrapTx(tx) ctx := t.Context() tenant := core.NewID("t") user := core.NewID("usr") _, err = w.Exec(ctx, `INSERT INTO tenants (id, name) VALUES ($1, 't')`, tenant) assert.Ok(t, err) seedEntitlements(t, w, ctx, tenant) seedChallengeUser(t, w, ctx, user) insertMemberGrant(t, w, ctx, tenant, user) s := store.NewSqliteMfaPolicyStore(w, access.AllowAll{}) required, err := s.RequiredForUser(ctx, user) assert.Ok(t, err) assert.Eq(t, required, false) assert.Ok(t, s.Set(ctx, tenant, true)) required, err = s.RequiredForUser(ctx, user) assert.Ok(t, err) assert.Eq(t, required, true) }