feat: sqlite: implement GetUserByUsername

This commit is contained in:
juancwu 2026-05-06 01:09:47 +00:00
commit 6e1ac07b04
3 changed files with 77 additions and 6 deletions

View file

@ -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),
}
}

View file

@ -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(

View file

@ -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.