feat: authsession service
This commit is contained in:
@@ -0,0 +1,682 @@
|
||||
package confirmemailcode
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"github.com/stretchr/testify/require"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"galaxy/authsession/internal/domain/challenge"
|
||||
"galaxy/authsession/internal/domain/common"
|
||||
"galaxy/authsession/internal/domain/devicesession"
|
||||
"galaxy/authsession/internal/domain/userresolution"
|
||||
"galaxy/authsession/internal/service/shared"
|
||||
"galaxy/authsession/internal/testkit"
|
||||
)
|
||||
|
||||
func TestExecuteConfirmsChallengeForExistingUser(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
deps := newConfirmDeps(t)
|
||||
if err := deps.userDirectory.SeedExisting(common.Email("pilot@example.com"), common.UserID("user-1")); err != nil {
|
||||
require.Failf(t, "test failed", "SeedExisting() returned error: %v", err)
|
||||
}
|
||||
if err := deps.challengeStore.Create(context.Background(), sentChallengeFixture(t, deps.hasher, "challenge-1", "pilot@example.com", "654321", deps.now.Add(-time.Minute), deps.now.Add(time.Minute))); err != nil {
|
||||
require.Failf(t, "test failed", "Create() returned error: %v", err)
|
||||
}
|
||||
|
||||
service := mustNewConfirmService(t, deps)
|
||||
result, err := service.Execute(context.Background(), Input{
|
||||
ChallengeID: "challenge-1",
|
||||
Code: "654321",
|
||||
ClientPublicKey: publicKeyString(),
|
||||
})
|
||||
if err != nil {
|
||||
require.Failf(t, "test failed", "Execute() returned error: %v", err)
|
||||
}
|
||||
if result.DeviceSessionID != "device-session-1" {
|
||||
require.Failf(t, "test failed", "Execute().DeviceSessionID = %q, want %q", result.DeviceSessionID, "device-session-1")
|
||||
}
|
||||
|
||||
record, err := deps.sessionStore.Get(context.Background(), common.DeviceSessionID("device-session-1"))
|
||||
if err != nil {
|
||||
require.Failf(t, "test failed", "Get() returned error: %v", err)
|
||||
}
|
||||
if record.Status != devicesession.StatusActive {
|
||||
require.Failf(t, "test failed", "session status = %q, want %q", record.Status, devicesession.StatusActive)
|
||||
}
|
||||
|
||||
challengeRecord, err := deps.challengeStore.Get(context.Background(), common.ChallengeID("challenge-1"))
|
||||
if err != nil {
|
||||
require.Failf(t, "test failed", "Get() returned error: %v", err)
|
||||
}
|
||||
if challengeRecord.Status != challenge.StatusConfirmedPendingExpire || challengeRecord.Confirmation == nil {
|
||||
require.Failf(t, "test failed", "challenge status = %q, confirmation = %+v", challengeRecord.Status, challengeRecord.Confirmation)
|
||||
}
|
||||
if len(deps.publisher.PublishedSnapshots()) != 1 {
|
||||
require.Failf(t, "test failed", "PublishedSnapshots() length = %d, want 1", len(deps.publisher.PublishedSnapshots()))
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteConfirmsChallengeByCreatingUser(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
deps := newConfirmDeps(t)
|
||||
if err := deps.userDirectory.QueueCreatedUserIDs(common.UserID("user-created")); err != nil {
|
||||
require.Failf(t, "test failed", "QueueCreatedUserIDs() returned error: %v", err)
|
||||
}
|
||||
if err := deps.challengeStore.Create(context.Background(), sentChallengeFixture(t, deps.hasher, "challenge-1", "new@example.com", "654321", deps.now.Add(-time.Minute), deps.now.Add(time.Minute))); err != nil {
|
||||
require.Failf(t, "test failed", "Create() returned error: %v", err)
|
||||
}
|
||||
|
||||
service := mustNewConfirmService(t, deps)
|
||||
result, err := service.Execute(context.Background(), Input{
|
||||
ChallengeID: "challenge-1",
|
||||
Code: "654321",
|
||||
ClientPublicKey: publicKeyString(),
|
||||
})
|
||||
if err != nil {
|
||||
require.Failf(t, "test failed", "Execute() returned error: %v", err)
|
||||
}
|
||||
if result.DeviceSessionID != "device-session-1" {
|
||||
require.Failf(t, "test failed", "Execute().DeviceSessionID = %q, want %q", result.DeviceSessionID, "device-session-1")
|
||||
}
|
||||
|
||||
record, err := deps.sessionStore.Get(context.Background(), common.DeviceSessionID("device-session-1"))
|
||||
if err != nil {
|
||||
require.Failf(t, "test failed", "Get() returned error: %v", err)
|
||||
}
|
||||
if record.UserID != common.UserID("user-created") {
|
||||
require.Failf(t, "test failed", "session user id = %q, want %q", record.UserID, common.UserID("user-created"))
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteConfirmsSuppressedChallenge(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
deps := newConfirmDeps(t)
|
||||
if err := deps.userDirectory.SeedExisting(common.Email("pilot@example.com"), common.UserID("user-1")); err != nil {
|
||||
require.Failf(t, "test failed", "SeedExisting() returned error: %v", err)
|
||||
}
|
||||
record := sentChallengeFixture(t, deps.hasher, "challenge-1", "pilot@example.com", "654321", deps.now.Add(-time.Minute), deps.now.Add(time.Minute))
|
||||
record.Status = challenge.StatusDeliverySuppressed
|
||||
record.DeliveryState = challenge.DeliverySuppressed
|
||||
if err := record.Validate(); err != nil {
|
||||
require.Failf(t, "test failed", "Validate() returned error: %v", err)
|
||||
}
|
||||
if err := deps.challengeStore.Create(context.Background(), record); err != nil {
|
||||
require.Failf(t, "test failed", "Create() returned error: %v", err)
|
||||
}
|
||||
|
||||
service := mustNewConfirmService(t, deps)
|
||||
result, err := service.Execute(context.Background(), Input{
|
||||
ChallengeID: "challenge-1",
|
||||
Code: "654321",
|
||||
ClientPublicKey: publicKeyString(),
|
||||
})
|
||||
if err != nil {
|
||||
require.Failf(t, "test failed", "Execute() returned error: %v", err)
|
||||
}
|
||||
if result.DeviceSessionID != "device-session-1" {
|
||||
require.Failf(t, "test failed", "Execute().DeviceSessionID = %q, want %q", result.DeviceSessionID, "device-session-1")
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteReturnsChallengeNotFound(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
service := mustNewConfirmService(t, newConfirmDeps(t))
|
||||
|
||||
_, err := service.Execute(context.Background(), Input{
|
||||
ChallengeID: "missing",
|
||||
Code: "654321",
|
||||
ClientPublicKey: publicKeyString(),
|
||||
})
|
||||
if shared.CodeOf(err) != shared.ErrorCodeChallengeNotFound {
|
||||
require.Failf(t, "test failed", "Execute() error code = %q, want %q", shared.CodeOf(err), shared.ErrorCodeChallengeNotFound)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteReturnsChallengeExpiredAndMarksExpired(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
deps := newConfirmDeps(t)
|
||||
if err := deps.challengeStore.Create(context.Background(), sentChallengeFixture(t, deps.hasher, "challenge-1", "pilot@example.com", "654321", deps.now.Add(-2*time.Minute), deps.now.Add(-time.Second))); err != nil {
|
||||
require.Failf(t, "test failed", "Create() returned error: %v", err)
|
||||
}
|
||||
|
||||
service := mustNewConfirmService(t, deps)
|
||||
|
||||
_, err := service.Execute(context.Background(), Input{
|
||||
ChallengeID: "challenge-1",
|
||||
Code: "654321",
|
||||
ClientPublicKey: publicKeyString(),
|
||||
})
|
||||
if shared.CodeOf(err) != shared.ErrorCodeChallengeExpired {
|
||||
require.Failf(t, "test failed", "Execute() error code = %q, want %q", shared.CodeOf(err), shared.ErrorCodeChallengeExpired)
|
||||
}
|
||||
|
||||
record, err := deps.challengeStore.Get(context.Background(), common.ChallengeID("challenge-1"))
|
||||
if err != nil {
|
||||
require.Failf(t, "test failed", "Get() returned error: %v", err)
|
||||
}
|
||||
if record.Status != challenge.StatusExpired {
|
||||
require.Failf(t, "test failed", "challenge status = %q, want %q", record.Status, challenge.StatusExpired)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteReturnsChallengeExpiredForConfirmedChallengeAfterRetentionWindow(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
deps := newConfirmDeps(t)
|
||||
key, err := shared.ParseClientPublicKey(publicKeyString())
|
||||
if err != nil {
|
||||
require.Failf(t, "test failed", "ParseClientPublicKey() returned error: %v", err)
|
||||
}
|
||||
record := confirmedChallengeFixture(
|
||||
t,
|
||||
deps.hasher,
|
||||
"challenge-1",
|
||||
"pilot@example.com",
|
||||
"654321",
|
||||
"device-session-1",
|
||||
key,
|
||||
deps.now.Add(-2*challenge.ConfirmedRetention),
|
||||
deps.now.Add(-time.Second),
|
||||
)
|
||||
if err := deps.challengeStore.Create(context.Background(), record); err != nil {
|
||||
require.Failf(t, "test failed", "Create() returned error: %v", err)
|
||||
}
|
||||
|
||||
service := mustNewConfirmService(t, deps)
|
||||
_, err = service.Execute(context.Background(), Input{
|
||||
ChallengeID: "challenge-1",
|
||||
Code: "654321",
|
||||
ClientPublicKey: publicKeyString(),
|
||||
})
|
||||
if shared.CodeOf(err) != shared.ErrorCodeChallengeExpired {
|
||||
require.Failf(t, "test failed", "Execute() error code = %q, want %q", shared.CodeOf(err), shared.ErrorCodeChallengeExpired)
|
||||
}
|
||||
|
||||
updated, err := deps.challengeStore.Get(context.Background(), common.ChallengeID("challenge-1"))
|
||||
if err != nil {
|
||||
require.Failf(t, "test failed", "Get() returned error: %v", err)
|
||||
}
|
||||
if updated.Status != challenge.StatusExpired {
|
||||
require.Failf(t, "test failed", "challenge status = %q, want %q", updated.Status, challenge.StatusExpired)
|
||||
}
|
||||
if updated.Confirmation != nil {
|
||||
require.Failf(t, "test failed", "Confirmation = %+v, want nil after expiration", updated.Confirmation)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteReturnsInvalidClientPublicKey(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
service := mustNewConfirmService(t, newConfirmDeps(t))
|
||||
|
||||
_, err := service.Execute(context.Background(), Input{
|
||||
ChallengeID: "challenge-1",
|
||||
Code: "654321",
|
||||
ClientPublicKey: "invalid",
|
||||
})
|
||||
if shared.CodeOf(err) != shared.ErrorCodeInvalidClientPublicKey {
|
||||
require.Failf(t, "test failed", "Execute() error code = %q, want %q", shared.CodeOf(err), shared.ErrorCodeInvalidClientPublicKey)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteInvalidCodeIncrementsAttempts(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
deps := newConfirmDeps(t)
|
||||
if err := deps.challengeStore.Create(context.Background(), sentChallengeFixture(t, deps.hasher, "challenge-1", "pilot@example.com", "654321", deps.now.Add(-time.Minute), deps.now.Add(time.Minute))); err != nil {
|
||||
require.Failf(t, "test failed", "Create() returned error: %v", err)
|
||||
}
|
||||
|
||||
service := mustNewConfirmService(t, deps)
|
||||
_, err := service.Execute(context.Background(), Input{
|
||||
ChallengeID: "challenge-1",
|
||||
Code: "000000",
|
||||
ClientPublicKey: publicKeyString(),
|
||||
})
|
||||
if shared.CodeOf(err) != shared.ErrorCodeInvalidCode {
|
||||
require.Failf(t, "test failed", "Execute() error code = %q, want %q", shared.CodeOf(err), shared.ErrorCodeInvalidCode)
|
||||
}
|
||||
|
||||
record, err := deps.challengeStore.Get(context.Background(), common.ChallengeID("challenge-1"))
|
||||
if err != nil {
|
||||
require.Failf(t, "test failed", "Get() returned error: %v", err)
|
||||
}
|
||||
if record.Attempts.Confirm != 1 {
|
||||
require.Failf(t, "test failed", "Attempts.Confirm = %d, want 1", record.Attempts.Confirm)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteFifthInvalidAttemptMarksChallengeFailed(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
deps := newConfirmDeps(t)
|
||||
record := sentChallengeFixture(t, deps.hasher, "challenge-1", "pilot@example.com", "654321", deps.now.Add(-time.Minute), deps.now.Add(time.Minute))
|
||||
record.Attempts.Confirm = 4
|
||||
if err := deps.challengeStore.Create(context.Background(), record); err != nil {
|
||||
require.Failf(t, "test failed", "Create() returned error: %v", err)
|
||||
}
|
||||
|
||||
service := mustNewConfirmService(t, deps)
|
||||
_, err := service.Execute(context.Background(), Input{
|
||||
ChallengeID: "challenge-1",
|
||||
Code: "000000",
|
||||
ClientPublicKey: publicKeyString(),
|
||||
})
|
||||
if shared.CodeOf(err) != shared.ErrorCodeInvalidCode {
|
||||
require.Failf(t, "test failed", "Execute() error code = %q, want %q", shared.CodeOf(err), shared.ErrorCodeInvalidCode)
|
||||
}
|
||||
|
||||
updated, err := deps.challengeStore.Get(context.Background(), common.ChallengeID("challenge-1"))
|
||||
if err != nil {
|
||||
require.Failf(t, "test failed", "Get() returned error: %v", err)
|
||||
}
|
||||
if updated.Status != challenge.StatusFailed {
|
||||
require.Failf(t, "test failed", "challenge status = %q, want %q", updated.Status, challenge.StatusFailed)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteDoesNotCreateSessionAfterTooManyAttempts(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
deps := newConfirmDeps(t)
|
||||
if err := deps.userDirectory.SeedExisting(common.Email("pilot@example.com"), common.UserID("user-1")); err != nil {
|
||||
require.Failf(t, "test failed", "SeedExisting() returned error: %v", err)
|
||||
}
|
||||
record := sentChallengeFixture(t, deps.hasher, "challenge-1", "pilot@example.com", "654321", deps.now.Add(-time.Minute), deps.now.Add(time.Minute))
|
||||
record.Attempts.Confirm = challenge.MaxInvalidConfirmAttempts
|
||||
record.Status = challenge.StatusFailed
|
||||
if err := record.Validate(); err != nil {
|
||||
require.Failf(t, "test failed", "Validate() returned error: %v", err)
|
||||
}
|
||||
if err := deps.challengeStore.Create(context.Background(), record); err != nil {
|
||||
require.Failf(t, "test failed", "Create() returned error: %v", err)
|
||||
}
|
||||
|
||||
service := mustNewConfirmService(t, deps)
|
||||
_, err := service.Execute(context.Background(), Input{
|
||||
ChallengeID: "challenge-1",
|
||||
Code: "654321",
|
||||
ClientPublicKey: publicKeyString(),
|
||||
})
|
||||
if shared.CodeOf(err) != shared.ErrorCodeInvalidCode {
|
||||
require.Failf(t, "test failed", "Execute() error code = %q, want %q", shared.CodeOf(err), shared.ErrorCodeInvalidCode)
|
||||
}
|
||||
|
||||
if got, err := deps.sessionStore.CountActiveByUserID(context.Background(), common.UserID("user-1")); err != nil {
|
||||
require.Failf(t, "test failed", "CountActiveByUserID() returned error: %v", err)
|
||||
} else if got != 0 {
|
||||
require.Failf(t, "test failed", "CountActiveByUserID() = %d, want 0", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteReturnsSameSessionIDForIdempotentRetryAndRepublishes(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
deps := newConfirmDeps(t)
|
||||
key, err := shared.ParseClientPublicKey(publicKeyString())
|
||||
if err != nil {
|
||||
require.Failf(t, "test failed", "ParseClientPublicKey() returned error: %v", err)
|
||||
}
|
||||
record := confirmedChallengeFixture(t, deps.hasher, "challenge-1", "pilot@example.com", "654321", "device-session-1", key, deps.now.Add(-time.Minute), deps.now.Add(time.Minute))
|
||||
if err := deps.challengeStore.Create(context.Background(), record); err != nil {
|
||||
require.Failf(t, "test failed", "Create() returned error: %v", err)
|
||||
}
|
||||
if err := deps.sessionStore.Create(context.Background(), activeSessionFixture("device-session-1", "user-1", key, deps.now.Add(-time.Minute))); err != nil {
|
||||
require.Failf(t, "test failed", "Create() returned error: %v", err)
|
||||
}
|
||||
|
||||
service := mustNewConfirmService(t, deps)
|
||||
result, err := service.Execute(context.Background(), Input{
|
||||
ChallengeID: "challenge-1",
|
||||
Code: "654321",
|
||||
ClientPublicKey: publicKeyString(),
|
||||
})
|
||||
if err != nil {
|
||||
require.Failf(t, "test failed", "Execute() returned error: %v", err)
|
||||
}
|
||||
if result.DeviceSessionID != "device-session-1" {
|
||||
require.Failf(t, "test failed", "Execute().DeviceSessionID = %q, want %q", result.DeviceSessionID, "device-session-1")
|
||||
}
|
||||
if len(deps.publisher.PublishedSnapshots()) != 1 {
|
||||
require.Failf(t, "test failed", "PublishedSnapshots() length = %d, want 1", len(deps.publisher.PublishedSnapshots()))
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteReturnsInvalidCodeForDifferentKeyDuringIdempotentRetry(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
deps := newConfirmDeps(t)
|
||||
key, err := shared.ParseClientPublicKey(publicKeyString())
|
||||
if err != nil {
|
||||
require.Failf(t, "test failed", "ParseClientPublicKey() returned error: %v", err)
|
||||
}
|
||||
record := confirmedChallengeFixture(t, deps.hasher, "challenge-1", "pilot@example.com", "654321", "device-session-1", key, deps.now.Add(-time.Minute), deps.now.Add(time.Minute))
|
||||
if err := deps.challengeStore.Create(context.Background(), record); err != nil {
|
||||
require.Failf(t, "test failed", "Create() returned error: %v", err)
|
||||
}
|
||||
if err := deps.sessionStore.Create(context.Background(), activeSessionFixture("device-session-1", "user-1", key, deps.now.Add(-time.Minute))); err != nil {
|
||||
require.Failf(t, "test failed", "Create() returned error: %v", err)
|
||||
}
|
||||
|
||||
service := mustNewConfirmService(t, deps)
|
||||
_, err = service.Execute(context.Background(), Input{
|
||||
ChallengeID: "challenge-1",
|
||||
Code: "654321",
|
||||
ClientPublicKey: alternatePublicKeyString(),
|
||||
})
|
||||
if shared.CodeOf(err) != shared.ErrorCodeInvalidCode {
|
||||
require.Failf(t, "test failed", "Execute() error code = %q, want %q", shared.CodeOf(err), shared.ErrorCodeInvalidCode)
|
||||
}
|
||||
|
||||
updated, err := deps.challengeStore.Get(context.Background(), common.ChallengeID("challenge-1"))
|
||||
if err != nil {
|
||||
require.Failf(t, "test failed", "Get() returned error: %v", err)
|
||||
}
|
||||
if updated.Attempts.Confirm != 0 {
|
||||
require.Failf(t, "test failed", "Attempts.Confirm = %d, want 0", updated.Attempts.Confirm)
|
||||
}
|
||||
if updated.Confirmation == nil {
|
||||
require.FailNow(t, "Confirmation = nil, want metadata to stay intact")
|
||||
}
|
||||
if updated.Confirmation.SessionID != common.DeviceSessionID("device-session-1") {
|
||||
require.Failf(t, "test failed", "Confirmation.SessionID = %q, want %q", updated.Confirmation.SessionID, common.DeviceSessionID("device-session-1"))
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteReturnsInvalidCodeForNonConfirmableStates(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
status challenge.Status
|
||||
deliveryState challenge.DeliveryState
|
||||
}{
|
||||
{name: "pending send", status: challenge.StatusPendingSend, deliveryState: challenge.DeliveryPending},
|
||||
{name: "failed", status: challenge.StatusFailed, deliveryState: challenge.DeliveryFailed},
|
||||
{name: "cancelled", status: challenge.StatusCancelled, deliveryState: challenge.DeliverySent},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
deps := newConfirmDeps(t)
|
||||
record := sentChallengeFixture(t, deps.hasher, "challenge-1", "pilot@example.com", "654321", deps.now.Add(-time.Minute), deps.now.Add(time.Minute))
|
||||
record.Status = tt.status
|
||||
record.DeliveryState = tt.deliveryState
|
||||
if err := record.Validate(); err != nil {
|
||||
require.Failf(t, "test failed", "Validate() returned error: %v", err)
|
||||
}
|
||||
if err := deps.challengeStore.Create(context.Background(), record); err != nil {
|
||||
require.Failf(t, "test failed", "Create() returned error: %v", err)
|
||||
}
|
||||
|
||||
service := mustNewConfirmService(t, deps)
|
||||
_, err := service.Execute(context.Background(), Input{
|
||||
ChallengeID: "challenge-1",
|
||||
Code: "654321",
|
||||
ClientPublicKey: publicKeyString(),
|
||||
})
|
||||
if shared.CodeOf(err) != shared.ErrorCodeInvalidCode {
|
||||
require.Failf(t, "test failed", "Execute() error code = %q, want %q", shared.CodeOf(err), shared.ErrorCodeInvalidCode)
|
||||
}
|
||||
|
||||
updated, err := deps.challengeStore.Get(context.Background(), common.ChallengeID("challenge-1"))
|
||||
if err != nil {
|
||||
require.Failf(t, "test failed", "Get() returned error: %v", err)
|
||||
}
|
||||
if updated.Attempts.Confirm != 0 {
|
||||
require.Failf(t, "test failed", "Attempts.Confirm = %d, want 0", updated.Attempts.Confirm)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteMarksChallengeFailedAndReturnsBlockedByPolicy(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
deps := newConfirmDeps(t)
|
||||
if err := deps.userDirectory.SeedBlockedEmail(common.Email("pilot@example.com"), userresolution.BlockReasonCode("policy_block")); err != nil {
|
||||
require.Failf(t, "test failed", "SeedBlockedEmail() returned error: %v", err)
|
||||
}
|
||||
if err := deps.challengeStore.Create(context.Background(), sentChallengeFixture(t, deps.hasher, "challenge-1", "pilot@example.com", "654321", deps.now.Add(-time.Minute), deps.now.Add(time.Minute))); err != nil {
|
||||
require.Failf(t, "test failed", "Create() returned error: %v", err)
|
||||
}
|
||||
|
||||
service := mustNewConfirmService(t, deps)
|
||||
_, err := service.Execute(context.Background(), Input{
|
||||
ChallengeID: "challenge-1",
|
||||
Code: "654321",
|
||||
ClientPublicKey: publicKeyString(),
|
||||
})
|
||||
if shared.CodeOf(err) != shared.ErrorCodeBlockedByPolicy {
|
||||
require.Failf(t, "test failed", "Execute() error code = %q, want %q", shared.CodeOf(err), shared.ErrorCodeBlockedByPolicy)
|
||||
}
|
||||
|
||||
record, err := deps.challengeStore.Get(context.Background(), common.ChallengeID("challenge-1"))
|
||||
if err != nil {
|
||||
require.Failf(t, "test failed", "Get() returned error: %v", err)
|
||||
}
|
||||
if record.Status != challenge.StatusFailed {
|
||||
require.Failf(t, "test failed", "challenge status = %q, want %q", record.Status, challenge.StatusFailed)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteReturnsSessionLimitExceededWithoutConsumingChallenge(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
deps := newConfirmDeps(t)
|
||||
if err := deps.userDirectory.SeedExisting(common.Email("pilot@example.com"), common.UserID("user-1")); err != nil {
|
||||
require.Failf(t, "test failed", "SeedExisting() returned error: %v", err)
|
||||
}
|
||||
if err := deps.challengeStore.Create(context.Background(), sentChallengeFixture(t, deps.hasher, "challenge-1", "pilot@example.com", "654321", deps.now.Add(-time.Minute), deps.now.Add(time.Minute))); err != nil {
|
||||
require.Failf(t, "test failed", "Create() returned error: %v", err)
|
||||
}
|
||||
if err := deps.sessionStore.Create(context.Background(), activeSessionFixture("device-session-existing", "user-1", mustClientPublicKey(t, publicKeyString()), deps.now.Add(-2*time.Minute))); err != nil {
|
||||
require.Failf(t, "test failed", "Create() returned error: %v", err)
|
||||
}
|
||||
limit := 1
|
||||
deps.configProvider.Config.ActiveSessionLimit = &limit
|
||||
|
||||
service := mustNewConfirmService(t, deps)
|
||||
_, err := service.Execute(context.Background(), Input{
|
||||
ChallengeID: "challenge-1",
|
||||
Code: "654321",
|
||||
ClientPublicKey: publicKeyString(),
|
||||
})
|
||||
if shared.CodeOf(err) != shared.ErrorCodeSessionLimitExceeded {
|
||||
require.Failf(t, "test failed", "Execute() error code = %q, want %q", shared.CodeOf(err), shared.ErrorCodeSessionLimitExceeded)
|
||||
}
|
||||
|
||||
record, err := deps.challengeStore.Get(context.Background(), common.ChallengeID("challenge-1"))
|
||||
if err != nil {
|
||||
require.Failf(t, "test failed", "Get() returned error: %v", err)
|
||||
}
|
||||
if record.Status != challenge.StatusSent {
|
||||
require.Failf(t, "test failed", "challenge status = %q, want %q", record.Status, challenge.StatusSent)
|
||||
}
|
||||
if record.Attempts.Confirm != 0 {
|
||||
require.Failf(t, "test failed", "Attempts.Confirm = %d, want 0", record.Attempts.Confirm)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteReturnsServiceUnavailableThenSucceedsIdempotentlyAfterPublishFailure(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
deps := newConfirmDeps(t)
|
||||
deps.publisher.Err = errors.New("publish failed")
|
||||
if err := deps.userDirectory.SeedExisting(common.Email("pilot@example.com"), common.UserID("user-1")); err != nil {
|
||||
require.Failf(t, "test failed", "SeedExisting() returned error: %v", err)
|
||||
}
|
||||
if err := deps.challengeStore.Create(context.Background(), sentChallengeFixture(t, deps.hasher, "challenge-1", "pilot@example.com", "654321", deps.now.Add(-time.Minute), deps.now.Add(time.Minute))); err != nil {
|
||||
require.Failf(t, "test failed", "Create() returned error: %v", err)
|
||||
}
|
||||
|
||||
service := mustNewConfirmService(t, deps)
|
||||
_, err := service.Execute(context.Background(), Input{
|
||||
ChallengeID: "challenge-1",
|
||||
Code: "654321",
|
||||
ClientPublicKey: publicKeyString(),
|
||||
})
|
||||
if shared.CodeOf(err) != shared.ErrorCodeServiceUnavailable {
|
||||
require.Failf(t, "test failed", "first Execute() error code = %q, want %q", shared.CodeOf(err), shared.ErrorCodeServiceUnavailable)
|
||||
}
|
||||
|
||||
deps.publisher.Err = nil
|
||||
result, err := service.Execute(context.Background(), Input{
|
||||
ChallengeID: "challenge-1",
|
||||
Code: "654321",
|
||||
ClientPublicKey: publicKeyString(),
|
||||
})
|
||||
if err != nil {
|
||||
require.Failf(t, "test failed", "second Execute() returned error: %v", err)
|
||||
}
|
||||
if result.DeviceSessionID != "device-session-1" {
|
||||
require.Failf(t, "test failed", "second Execute().DeviceSessionID = %q, want %q", result.DeviceSessionID, "device-session-1")
|
||||
}
|
||||
}
|
||||
|
||||
type confirmDeps struct {
|
||||
challengeStore *testkit.InMemoryChallengeStore
|
||||
sessionStore *testkit.InMemorySessionStore
|
||||
userDirectory *testkit.InMemoryUserDirectory
|
||||
configProvider testkit.StaticConfigProvider
|
||||
publisher *testkit.RecordingProjectionPublisher
|
||||
idGenerator *testkit.SequenceIDGenerator
|
||||
hasher testkit.DeterministicCodeHasher
|
||||
now time.Time
|
||||
}
|
||||
|
||||
func newConfirmDeps(t *testing.T) confirmDeps {
|
||||
t.Helper()
|
||||
|
||||
return confirmDeps{
|
||||
challengeStore: &testkit.InMemoryChallengeStore{},
|
||||
sessionStore: &testkit.InMemorySessionStore{},
|
||||
userDirectory: &testkit.InMemoryUserDirectory{},
|
||||
configProvider: testkit.StaticConfigProvider{},
|
||||
publisher: &testkit.RecordingProjectionPublisher{},
|
||||
idGenerator: &testkit.SequenceIDGenerator{
|
||||
DeviceSessionIDs: []common.DeviceSessionID{"device-session-1"},
|
||||
},
|
||||
hasher: testkit.DeterministicCodeHasher{},
|
||||
now: time.Unix(20, 0).UTC(),
|
||||
}
|
||||
}
|
||||
|
||||
func mustNewConfirmService(t *testing.T, deps confirmDeps) *Service {
|
||||
t.Helper()
|
||||
|
||||
service, err := New(
|
||||
deps.challengeStore,
|
||||
deps.sessionStore,
|
||||
deps.userDirectory,
|
||||
deps.configProvider,
|
||||
deps.publisher,
|
||||
deps.idGenerator,
|
||||
deps.hasher,
|
||||
testkit.FixedClock{Time: deps.now},
|
||||
)
|
||||
if err != nil {
|
||||
require.Failf(t, "test failed", "New() returned error: %v", err)
|
||||
}
|
||||
|
||||
return service
|
||||
}
|
||||
|
||||
func sentChallengeFixture(
|
||||
t *testing.T,
|
||||
hasher testkit.DeterministicCodeHasher,
|
||||
challengeID string,
|
||||
email string,
|
||||
code string,
|
||||
createdAt time.Time,
|
||||
expiresAt time.Time,
|
||||
) challenge.Challenge {
|
||||
t.Helper()
|
||||
|
||||
codeHash, err := hasher.Hash(code)
|
||||
if err != nil {
|
||||
require.Failf(t, "test failed", "Hash() returned error: %v", err)
|
||||
}
|
||||
|
||||
record := challenge.Challenge{
|
||||
ID: common.ChallengeID(challengeID),
|
||||
Email: common.Email(email),
|
||||
CodeHash: codeHash,
|
||||
Status: challenge.StatusSent,
|
||||
DeliveryState: challenge.DeliverySent,
|
||||
CreatedAt: createdAt,
|
||||
ExpiresAt: expiresAt,
|
||||
}
|
||||
if err := record.Validate(); err != nil {
|
||||
require.Failf(t, "test failed", "Validate() returned error: %v", err)
|
||||
}
|
||||
|
||||
return record
|
||||
}
|
||||
|
||||
func confirmedChallengeFixture(
|
||||
t *testing.T,
|
||||
hasher testkit.DeterministicCodeHasher,
|
||||
challengeID string,
|
||||
email string,
|
||||
code string,
|
||||
deviceSessionID string,
|
||||
clientPublicKey common.ClientPublicKey,
|
||||
createdAt time.Time,
|
||||
expiresAt time.Time,
|
||||
) challenge.Challenge {
|
||||
t.Helper()
|
||||
|
||||
record := sentChallengeFixture(t, hasher, challengeID, email, code, createdAt, expiresAt)
|
||||
record.Status = challenge.StatusConfirmedPendingExpire
|
||||
record.Confirmation = &challenge.Confirmation{
|
||||
SessionID: common.DeviceSessionID(deviceSessionID),
|
||||
ClientPublicKey: clientPublicKey,
|
||||
ConfirmedAt: createdAt.Add(time.Minute),
|
||||
}
|
||||
if err := record.Validate(); err != nil {
|
||||
require.Failf(t, "test failed", "Validate() returned error: %v", err)
|
||||
}
|
||||
|
||||
return record
|
||||
}
|
||||
|
||||
func activeSessionFixture(deviceSessionID string, userID string, clientPublicKey common.ClientPublicKey, createdAt time.Time) devicesession.Session {
|
||||
return devicesession.Session{
|
||||
ID: common.DeviceSessionID(deviceSessionID),
|
||||
UserID: common.UserID(userID),
|
||||
ClientPublicKey: clientPublicKey,
|
||||
Status: devicesession.StatusActive,
|
||||
CreatedAt: createdAt,
|
||||
}
|
||||
}
|
||||
|
||||
func mustClientPublicKey(t *testing.T, value string) common.ClientPublicKey {
|
||||
t.Helper()
|
||||
|
||||
key, err := shared.ParseClientPublicKey(value)
|
||||
if err != nil {
|
||||
require.Failf(t, "test failed", "ParseClientPublicKey() returned error: %v", err)
|
||||
}
|
||||
|
||||
return key
|
||||
}
|
||||
|
||||
func publicKeyString() string {
|
||||
return "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8="
|
||||
}
|
||||
|
||||
func alternatePublicKeyString() string {
|
||||
return "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE="
|
||||
}
|
||||
Reference in New Issue
Block a user