| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227 |
- package repository
- import (
- "context"
- "database/sql"
- "fmt"
- "sync"
- "testing"
- "time"
- dbent "github.com/Wei-Shaw/sub2api/ent"
- "github.com/Wei-Shaw/sub2api/ent/enttest"
- "github.com/Wei-Shaw/sub2api/internal/service"
- "github.com/stretchr/testify/require"
- "entgo.io/ent/dialect"
- entsql "entgo.io/ent/dialect/sql"
- _ "modernc.org/sqlite"
- )
- func newUserEntRepo(t *testing.T) (*userRepository, *dbent.Client) {
- t.Helper()
- db, err := sql.Open("sqlite", fmt.Sprintf("file:%s?mode=memory&cache=shared&_fk=1", t.Name()))
- require.NoError(t, err)
- t.Cleanup(func() { _ = db.Close() })
- db.SetMaxOpenConns(10)
- _, err = db.Exec("PRAGMA foreign_keys = ON")
- require.NoError(t, err)
- drv := entsql.OpenDB(dialect.SQLite, db)
- client := enttest.NewClient(t, enttest.WithOptions(dbent.Driver(drv)))
- t.Cleanup(func() { _ = client.Close() })
- return newUserRepositoryWithSQL(client, db), client
- }
- func TestUserRepositoryGetByEmailNormalizesLegacySpacingAndCase(t *testing.T) {
- repo, _ := newUserEntRepo(t)
- ctx := context.Background()
- err := repo.Create(ctx, &service.User{
- Email: " Legacy@Example.com ",
- Username: "legacy-user",
- PasswordHash: "hash",
- Role: service.RoleUser,
- Status: service.StatusActive,
- })
- require.NoError(t, err)
- got, err := repo.GetByEmail(ctx, "legacy@example.com")
- require.NoError(t, err)
- require.Equal(t, " Legacy@Example.com ", got.Email)
- }
- func TestUserRepositoryExistsByEmailNormalizesLegacySpacingAndCase(t *testing.T) {
- repo, _ := newUserEntRepo(t)
- ctx := context.Background()
- err := repo.Create(ctx, &service.User{
- Email: " Legacy@Example.com ",
- Username: "legacy-user",
- PasswordHash: "hash",
- Role: service.RoleUser,
- Status: service.StatusActive,
- })
- require.NoError(t, err)
- exists, err := repo.ExistsByEmail(ctx, " LEGACY@example.com ")
- require.NoError(t, err)
- require.True(t, exists)
- }
- func TestUserRepositoryCreateRejectsNormalizedEmailDuplicate(t *testing.T) {
- repo, _ := newUserEntRepo(t)
- ctx := context.Background()
- err := repo.Create(ctx, &service.User{
- Email: " Existing@Example.com ",
- Username: "existing-user",
- PasswordHash: "hash",
- Role: service.RoleUser,
- Status: service.StatusActive,
- })
- require.NoError(t, err)
- err = repo.Create(ctx, &service.User{
- Email: "existing@example.com",
- Username: "duplicate-user",
- PasswordHash: "hash",
- Role: service.RoleUser,
- Status: service.StatusActive,
- })
- require.ErrorIs(t, err, service.ErrEmailExists)
- }
- func TestUserRepositoryUpdateRejectsNormalizedEmailDuplicate(t *testing.T) {
- repo, _ := newUserEntRepo(t)
- ctx := context.Background()
- first := &service.User{
- Email: " Existing@Example.com ",
- Username: "existing-user",
- PasswordHash: "hash",
- Role: service.RoleUser,
- Status: service.StatusActive,
- }
- require.NoError(t, repo.Create(ctx, first))
- second := &service.User{
- Email: "second@example.com",
- Username: "second-user",
- PasswordHash: "hash",
- Role: service.RoleUser,
- Status: service.StatusActive,
- }
- require.NoError(t, repo.Create(ctx, second))
- second.Email = " existing@example.com "
- err := repo.Update(ctx, second, service.UserUpdateFields{Email: true})
- require.ErrorIs(t, err, service.ErrEmailExists)
- }
- func TestUserRepositoryGetByEmailReportsNormalizedEmailConflict(t *testing.T) {
- repo, client := newUserEntRepo(t)
- ctx := context.Background()
- _, err := client.User.Create().
- SetEmail("Conflict@Example.com").
- SetUsername("conflict-user-1").
- SetPasswordHash("hash").
- SetRole(service.RoleUser).
- SetStatus(service.StatusActive).
- Save(ctx)
- require.NoError(t, err)
- _, err = client.User.Create().
- SetEmail(" conflict@example.com ").
- SetUsername("conflict-user-2").
- SetPasswordHash("hash").
- SetRole(service.RoleUser).
- SetStatus(service.StatusActive).
- Save(ctx)
- require.NoError(t, err)
- _, err = repo.GetByEmail(ctx, "conflict@example.com")
- require.Error(t, err)
- require.ErrorContains(t, err, "normalized email lookup matched multiple users")
- }
- func TestUserRepositoryCreateSerializesNormalizedEmailConflictsUnderConcurrency(t *testing.T) {
- repo, client := newUserEntRepo(t)
- ctx := context.Background()
- firstCreateStarted := make(chan struct{})
- releaseFirstCreate := make(chan struct{})
- var firstCreate sync.Once
- client.User.Use(func(next dbent.Mutator) dbent.Mutator {
- return dbent.MutateFunc(func(ctx context.Context, m dbent.Mutation) (dbent.Value, error) {
- blocked := false
- if m.Op().Is(dbent.OpCreate) {
- firstCreate.Do(func() {
- blocked = true
- close(firstCreateStarted)
- })
- }
- if blocked {
- <-releaseFirstCreate
- }
- return next.Mutate(ctx, m)
- })
- })
- type createResult struct {
- err error
- }
- results := make(chan createResult, 2)
- go func() {
- results <- createResult{err: repo.Create(ctx, &service.User{
- Email: " Race@Example.com ",
- Username: "race-user-1",
- PasswordHash: "hash",
- Role: service.RoleUser,
- Status: service.StatusActive,
- })}
- }()
- <-firstCreateStarted
- go func() {
- results <- createResult{err: repo.Create(ctx, &service.User{
- Email: "race@example.com",
- Username: "race-user-2",
- PasswordHash: "hash",
- Role: service.RoleUser,
- Status: service.StatusActive,
- })}
- }()
- time.Sleep(100 * time.Millisecond)
- close(releaseFirstCreate)
- first := <-results
- second := <-results
- errors := []error{first.err, second.err}
- successes := 0
- conflicts := 0
- for _, err := range errors {
- switch err {
- case nil:
- successes++
- case service.ErrEmailExists:
- conflicts++
- default:
- t.Fatalf("unexpected create error: %v", err)
- }
- }
- require.Equal(t, 1, successes)
- require.Equal(t, 1, conflicts)
- count, err := client.User.Query().Where(userEmailLookupPredicate("race@example.com")).Count(ctx)
- require.NoError(t, err)
- require.Equal(t, 1, count)
- }
|