From 6e1ac07b04cb3f0b2926798e76ef52880b96fbff Mon Sep 17 00:00:00 2001 From: juancwu Date: Wed, 6 May 2026 01:09:47 +0000 Subject: [PATCH] feat: sqlite: implement GetUserByUsername --- store/queries.go | 23 +++++++++++++----- store/sqlite/store.go | 13 ++++++++++ store/storetest/storetest.go | 47 ++++++++++++++++++++++++++++++++++++ 3 files changed, 77 insertions(+), 6 deletions(-) diff --git a/store/queries.go b/store/queries.go index 8423438..8cebf63 100644 --- a/store/queries.go +++ b/store/queries.go @@ -1,9 +1,10 @@ package store type Queries struct { - CreateUser string - GetUserByID string - GetUserByEmail string + CreateUser string + GetUserByID string + GetUserByEmail string + GetUserByUsername string } var CanonicalQueries Queries = Queries{ @@ -33,12 +34,22 @@ var CanonicalQueries Queries = Queries{ created_at, updated_at FROM pase_users WHERE email = ?;`, + GetUserByUsername: ` + SELECT + id, email, email_verified_at, + username, username_normalized, display_name, profile_image_url, + status, status_reason, status_changed_at, status_expires_at, + failed_login_count, last_failed_login_at, + created_at, updated_at + FROM pase_users + WHERE username_normalized = ?;`, } func (q Queries) Rebind(d Dialect) Queries { return Queries{ - CreateUser: d.Rebind(q.CreateUser), - GetUserByID: d.Rebind(q.GetUserByID), - GetUserByEmail: d.Rebind(q.GetUserByEmail), + CreateUser: d.Rebind(q.CreateUser), + GetUserByID: d.Rebind(q.GetUserByID), + GetUserByEmail: d.Rebind(q.GetUserByEmail), + GetUserByUsername: d.Rebind(q.GetUserByUsername), } } diff --git a/store/sqlite/store.go b/store/sqlite/store.go index 8677832..f4ad03c 100644 --- a/store/sqlite/store.go +++ b/store/sqlite/store.go @@ -98,6 +98,19 @@ func (s *Store) GetUserByEmail(ctx context.Context, email string) (*store.User, return u, nil } +// GetUserByUsername returns the user with the given username. +func (s *Store) GetUserByUsername(ctx context.Context, normalizedUsername string) (*store.User, error) { + row := s.db.QueryRowContext(ctx, s.q.GetUserByUsername, normalizedUsername) + u, err := s.scanUser(row) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, fmt.Errorf("pase/sqlite: get user by username: %w", store.ErrUserNotFound) + } + return nil, fmt.Errorf("pase/sqlite: get user by username: %w", err) + } + return u, nil +} + func (s *Store) scanUser(row *sql.Row) (*store.User, error) { var u store.User err := row.Scan( diff --git a/store/storetest/storetest.go b/store/storetest/storetest.go index ae68f26..4856a80 100644 --- a/store/storetest/storetest.go +++ b/store/storetest/storetest.go @@ -29,6 +29,7 @@ type SuiteStore interface { CreateUser(ctx context.Context, u *store.User) error GetUserByID(ctx context.Context, id string) (*store.User, error) GetUserByEmail(ctx context.Context, email string) (*store.User, error) + GetUserByUsername(ctx context.Context, username string) (*store.User, error) } // Factory returns a fresh, isolated Store. Each call to the factory must @@ -56,6 +57,9 @@ func RunSuite(t *testing.T, newStore Factory) { {"CreateUser_duplicateUsernameNormalized", testCreateUserDuplicateUsernameNormalized}, {"CreateUser_GetUserByEmail_roundTrip", testCreateUserGetUserByEmailRoundTrip}, {"GetUserByEmail_notFound", testGetUserByEmailNotFound}, + {"CreateUser_GetUserByUsername_roundTrip", testCreateUserGetUserByUsernameRoundTrip}, + {"GetUserByUsername_notFound", testGetUserByUsernameNotFound}, + {"GetUserByUsername_notNormalized_notFound", testGetUserByUsernameNotNormalized}, } for _, tc := range cases { @@ -167,6 +171,49 @@ func testGetUserByEmailNotFound(t *testing.T, s SuiteStore) { } } +func testCreateUserGetUserByUsernameRoundTrip(t *testing.T, s SuiteStore) { + ctx := context.Background() + + want := FixedUser() + if err := s.CreateUser(ctx, want); err != nil { + t.Fatalf("CreateUser: %v", err) + } + + got, err := s.GetUserByUsername(ctx, want.UsernameNormalized.String) + if err != nil { + t.Fatalf("GetUserByUsername (normalized username): %v", err) + } + + AssertUserEqual(t, got, want) +} + +func testGetUserByUsernameNotFound(t *testing.T, s SuiteStore) { + got, err := s.GetUserByUsername(context.Background(), "does-not-exists") + if err == nil { + t.Fatalf("expected error, got user %+v", got) + } + if !errors.Is(err, store.ErrUserNotFound) { + t.Errorf("expected ErrUserNotFound, got: %v", err) + } +} + +func testGetUserByUsernameNotNormalized(t *testing.T, s SuiteStore) { + ctx := context.Background() + + want := FixedUser() + if err := s.CreateUser(ctx, want); err != nil { + t.Fatalf("CreateUser: %v", err) + } + + got, err := s.GetUserByUsername(ctx, want.Username.String) + if err == nil { + t.Fatalf("expected error, got user %+v", got) + } + if !errors.Is(err, store.ErrUserNotFound) { + t.Errorf("expected ErrUserNotFound, got: %v", err) + } +} + // --------------------------------------------------------------------------- // Fixtures and helpers. Exported so dialect-specific tests can reuse them // for one-off cases that don't fit into the shared suite.