feat: authsession service
This commit is contained in:
@@ -0,0 +1,309 @@
|
||||
package testkit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"galaxy/authsession/internal/domain/common"
|
||||
"galaxy/authsession/internal/domain/userresolution"
|
||||
"galaxy/authsession/internal/ports"
|
||||
)
|
||||
|
||||
type userDirectoryEntry struct {
|
||||
UserID common.UserID
|
||||
BlockReasonCode userresolution.BlockReasonCode
|
||||
}
|
||||
|
||||
// InMemoryUserDirectory is a deterministic map-backed UserDirectory double
|
||||
// suitable for service tests.
|
||||
type InMemoryUserDirectory struct {
|
||||
mu sync.Mutex
|
||||
byEmail map[common.Email]userDirectoryEntry
|
||||
emailByUserID map[common.UserID]common.Email
|
||||
createdUserIDs []common.UserID
|
||||
nextUserNumber int
|
||||
}
|
||||
|
||||
// ResolveByEmail returns the current resolution state for email without
|
||||
// creating a new user.
|
||||
func (d *InMemoryUserDirectory) ResolveByEmail(ctx context.Context, email common.Email) (userresolution.Result, error) {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return userresolution.Result{}, err
|
||||
}
|
||||
if err := email.Validate(); err != nil {
|
||||
return userresolution.Result{}, fmt.Errorf("resolve by email: %w", err)
|
||||
}
|
||||
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
result, err := d.resolveLocked(email)
|
||||
if err != nil {
|
||||
return userresolution.Result{}, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// ExistsByUserID reports whether userID currently identifies a stored user
|
||||
// record.
|
||||
func (d *InMemoryUserDirectory) ExistsByUserID(ctx context.Context, userID common.UserID) (bool, error) {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if err := userID.Validate(); err != nil {
|
||||
return false, fmt.Errorf("exists by user id: %w", err)
|
||||
}
|
||||
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
_, ok := d.emailByUserID[userID]
|
||||
return ok, nil
|
||||
}
|
||||
|
||||
// EnsureUserByEmail returns an existing user for email, creates a new user
|
||||
// when registration is allowed, or reports a blocked outcome.
|
||||
func (d *InMemoryUserDirectory) EnsureUserByEmail(ctx context.Context, email common.Email) (ports.EnsureUserResult, error) {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return ports.EnsureUserResult{}, err
|
||||
}
|
||||
if err := email.Validate(); err != nil {
|
||||
return ports.EnsureUserResult{}, fmt.Errorf("ensure user by email: %w", err)
|
||||
}
|
||||
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
if d.byEmail == nil {
|
||||
d.byEmail = make(map[common.Email]userDirectoryEntry)
|
||||
}
|
||||
if d.emailByUserID == nil {
|
||||
d.emailByUserID = make(map[common.UserID]common.Email)
|
||||
}
|
||||
|
||||
entry, ok := d.byEmail[email]
|
||||
if ok {
|
||||
if !entry.BlockReasonCode.IsZero() {
|
||||
result := ports.EnsureUserResult{
|
||||
Outcome: ports.EnsureUserOutcomeBlocked,
|
||||
BlockReasonCode: entry.BlockReasonCode,
|
||||
}
|
||||
return result, result.Validate()
|
||||
}
|
||||
|
||||
result := ports.EnsureUserResult{
|
||||
Outcome: ports.EnsureUserOutcomeExisting,
|
||||
UserID: entry.UserID,
|
||||
}
|
||||
return result, result.Validate()
|
||||
}
|
||||
|
||||
userID, err := d.nextCreatedUserIDLocked()
|
||||
if err != nil {
|
||||
return ports.EnsureUserResult{}, err
|
||||
}
|
||||
d.byEmail[email] = userDirectoryEntry{UserID: userID}
|
||||
d.emailByUserID[userID] = email
|
||||
|
||||
result := ports.EnsureUserResult{
|
||||
Outcome: ports.EnsureUserOutcomeCreated,
|
||||
UserID: userID,
|
||||
}
|
||||
return result, result.Validate()
|
||||
}
|
||||
|
||||
// BlockByUserID applies a block state to the user identified by input.UserID.
|
||||
func (d *InMemoryUserDirectory) BlockByUserID(ctx context.Context, input ports.BlockUserByIDInput) (ports.BlockUserResult, error) {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return ports.BlockUserResult{}, err
|
||||
}
|
||||
if err := input.Validate(); err != nil {
|
||||
return ports.BlockUserResult{}, fmt.Errorf("block by user id: %w", err)
|
||||
}
|
||||
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
email, ok := d.emailByUserID[input.UserID]
|
||||
if !ok {
|
||||
return ports.BlockUserResult{}, fmt.Errorf("block by user id %q: %w", input.UserID, ports.ErrNotFound)
|
||||
}
|
||||
entry := d.byEmail[email]
|
||||
if !entry.BlockReasonCode.IsZero() {
|
||||
result := ports.BlockUserResult{
|
||||
Outcome: ports.BlockUserOutcomeAlreadyBlocked,
|
||||
UserID: input.UserID,
|
||||
}
|
||||
return result, result.Validate()
|
||||
}
|
||||
|
||||
entry.BlockReasonCode = input.ReasonCode
|
||||
d.byEmail[email] = entry
|
||||
|
||||
result := ports.BlockUserResult{
|
||||
Outcome: ports.BlockUserOutcomeBlocked,
|
||||
UserID: input.UserID,
|
||||
}
|
||||
return result, result.Validate()
|
||||
}
|
||||
|
||||
// BlockByEmail applies a block state to input.Email even when no user record
|
||||
// currently exists for that e-mail address.
|
||||
func (d *InMemoryUserDirectory) BlockByEmail(ctx context.Context, input ports.BlockUserByEmailInput) (ports.BlockUserResult, error) {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return ports.BlockUserResult{}, err
|
||||
}
|
||||
if err := input.Validate(); err != nil {
|
||||
return ports.BlockUserResult{}, fmt.Errorf("block by email: %w", err)
|
||||
}
|
||||
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
if d.byEmail == nil {
|
||||
d.byEmail = make(map[common.Email]userDirectoryEntry)
|
||||
}
|
||||
if d.emailByUserID == nil {
|
||||
d.emailByUserID = make(map[common.UserID]common.Email)
|
||||
}
|
||||
|
||||
entry := d.byEmail[input.Email]
|
||||
if !entry.BlockReasonCode.IsZero() {
|
||||
result := ports.BlockUserResult{
|
||||
Outcome: ports.BlockUserOutcomeAlreadyBlocked,
|
||||
UserID: entry.UserID,
|
||||
}
|
||||
return result, result.Validate()
|
||||
}
|
||||
|
||||
entry.BlockReasonCode = input.ReasonCode
|
||||
d.byEmail[input.Email] = entry
|
||||
if !entry.UserID.IsZero() {
|
||||
d.emailByUserID[entry.UserID] = input.Email
|
||||
}
|
||||
|
||||
result := ports.BlockUserResult{
|
||||
Outcome: ports.BlockUserOutcomeBlocked,
|
||||
UserID: entry.UserID,
|
||||
}
|
||||
return result, result.Validate()
|
||||
}
|
||||
|
||||
// SeedExisting preloads one existing unblocked user record for service tests.
|
||||
func (d *InMemoryUserDirectory) SeedExisting(email common.Email, userID common.UserID) error {
|
||||
if err := email.Validate(); err != nil {
|
||||
return fmt.Errorf("seed existing email: %w", err)
|
||||
}
|
||||
if err := userID.Validate(); err != nil {
|
||||
return fmt.Errorf("seed existing user id: %w", err)
|
||||
}
|
||||
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
if d.byEmail == nil {
|
||||
d.byEmail = make(map[common.Email]userDirectoryEntry)
|
||||
}
|
||||
if d.emailByUserID == nil {
|
||||
d.emailByUserID = make(map[common.UserID]common.Email)
|
||||
}
|
||||
|
||||
d.byEmail[email] = userDirectoryEntry{UserID: userID}
|
||||
d.emailByUserID[userID] = email
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SeedBlockedEmail preloads one blocked e-mail address that does not
|
||||
// necessarily belong to an existing user record.
|
||||
func (d *InMemoryUserDirectory) SeedBlockedEmail(email common.Email, reasonCode userresolution.BlockReasonCode) error {
|
||||
if err := email.Validate(); err != nil {
|
||||
return fmt.Errorf("seed blocked email: %w", err)
|
||||
}
|
||||
if err := reasonCode.Validate(); err != nil {
|
||||
return fmt.Errorf("seed blocked email reason code: %w", err)
|
||||
}
|
||||
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
if d.byEmail == nil {
|
||||
d.byEmail = make(map[common.Email]userDirectoryEntry)
|
||||
}
|
||||
|
||||
d.byEmail[email] = userDirectoryEntry{BlockReasonCode: reasonCode}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SeedBlockedUser preloads one blocked existing user record for service tests.
|
||||
func (d *InMemoryUserDirectory) SeedBlockedUser(email common.Email, userID common.UserID, reasonCode userresolution.BlockReasonCode) error {
|
||||
if err := d.SeedExisting(email, userID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
entry := d.byEmail[email]
|
||||
entry.BlockReasonCode = reasonCode
|
||||
d.byEmail[email] = entry
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// QueueCreatedUserIDs appends deterministic user identifiers that
|
||||
// EnsureUserByEmail will consume before falling back to generated ids.
|
||||
func (d *InMemoryUserDirectory) QueueCreatedUserIDs(userIDs ...common.UserID) error {
|
||||
for index, userID := range userIDs {
|
||||
if err := userID.Validate(); err != nil {
|
||||
return fmt.Errorf("queue created user id %d: %w", index, err)
|
||||
}
|
||||
}
|
||||
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
d.createdUserIDs = append(d.createdUserIDs, userIDs...)
|
||||
return nil
|
||||
}
|
||||
|
||||
var _ ports.UserDirectory = (*InMemoryUserDirectory)(nil)
|
||||
|
||||
func (d *InMemoryUserDirectory) resolveLocked(email common.Email) (userresolution.Result, error) {
|
||||
entry, ok := d.byEmail[email]
|
||||
if !ok {
|
||||
result := userresolution.Result{Kind: userresolution.KindCreatable}
|
||||
return result, result.Validate()
|
||||
}
|
||||
if !entry.BlockReasonCode.IsZero() {
|
||||
result := userresolution.Result{
|
||||
Kind: userresolution.KindBlocked,
|
||||
BlockReasonCode: entry.BlockReasonCode,
|
||||
}
|
||||
return result, result.Validate()
|
||||
}
|
||||
|
||||
result := userresolution.Result{
|
||||
Kind: userresolution.KindExisting,
|
||||
UserID: entry.UserID,
|
||||
}
|
||||
return result, result.Validate()
|
||||
}
|
||||
|
||||
func (d *InMemoryUserDirectory) nextCreatedUserIDLocked() (common.UserID, error) {
|
||||
if len(d.createdUserIDs) > 0 {
|
||||
userID := d.createdUserIDs[0]
|
||||
d.createdUserIDs = d.createdUserIDs[1:]
|
||||
return userID, nil
|
||||
}
|
||||
|
||||
d.nextUserNumber++
|
||||
userID := common.UserID(fmt.Sprintf("user-%d", d.nextUserNumber))
|
||||
if err := userID.Validate(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return userID, nil
|
||||
}
|
||||
Reference in New Issue
Block a user