diff --git a/internal/user/service/service.go b/internal/user/service/service.go new file mode 100644 index 0000000..22bed69 --- /dev/null +++ b/internal/user/service/service.go @@ -0,0 +1,113 @@ +package service + +import ( + "context" + "errors" + "log/slog" + + "gitea.michaelthomson.dev/mthomson/habits/internal/user/repository" + "github.com/gofrs/uuid/v5" +) + +var ( + ErrNotFound error = errors.New("user cannot be found") +) + +type User struct { + Id uuid.UUID + Email string + HashedPassword string +} + +func NewUser(id uuid.UUID, email string, hashedPassword string) User { + return User{Id: id, Email: email, HashedPassword: hashedPassword} +} + +func UserFromUserRow(userRow repository.UserRow) User { + return User{Id: userRow.Id, Email: userRow.Email, HashedPassword: userRow.HashedPassword} +} + +func UserRowFromUser(user User) repository.UserRow { + return repository.UserRow{Id: user.Id, Email: user.Email, HashedPassword: user.HashedPassword} +} + +func (t User) Equal(user User) bool { + return t.Id == user.Id && t.Email == user.Email && t.HashedPassword == user.HashedPassword +} + +type UserRepository interface { + Create(ctx context.Context, user repository.UserRow) (repository.UserRow, error) + GetById(ctx context.Context, id uuid.UUID) (repository.UserRow, error) + Update(ctx context.Context, user repository.UserRow) error + Delete(ctx context.Context, id uuid.UUID) error +} + +type UserService struct { + logger *slog.Logger + repo UserRepository +} + +func NewUserService(logger *slog.Logger, userRepo UserRepository) *UserService { + return &UserService{ + logger: logger, + repo: userRepo, + } +} + +func (s *UserService) GetUser(ctx context.Context, id uuid.UUID) (User, error) { + user, err := s.repo.GetById(ctx, id) + + if err != nil { + if err == repository.ErrNotFound { + return User{}, ErrNotFound + } + + s.logger.ErrorContext(ctx, err.Error()) + return User{}, err + } + + return UserFromUserRow(user), err +} + +func (s *UserService) CreateUser(ctx context.Context, user User) (User, error) { + userRow := UserRowFromUser(user) + + newUserRow, err := s.repo.Create(ctx, userRow) + + if err != nil { + s.logger.ErrorContext(ctx, err.Error()) + return User{}, err + } + + return UserFromUserRow(newUserRow), err +} + +func (s *UserService) DeleteUser(ctx context.Context, id uuid.UUID) error { + err := s.repo.Delete(ctx, id) + + if err == repository.ErrNotFound { + return ErrNotFound + } + + if err != nil { + s.logger.ErrorContext(ctx, err.Error()) + } + + return err +} + +func (s *UserService) UpdateUser(ctx context.Context, user User) error { + userRow := UserRowFromUser(user) + + err := s.repo.Update(ctx, userRow) + + if err == repository.ErrNotFound { + return ErrNotFound + } + + if err != nil { + s.logger.ErrorContext(ctx, err.Error()) + } + + return err +} diff --git a/internal/user/service/service_test.go b/internal/user/service/service_test.go new file mode 100644 index 0000000..4e5ccbc --- /dev/null +++ b/internal/user/service/service_test.go @@ -0,0 +1,155 @@ +package service_test + +import ( + "context" + "log/slog" + "testing" + + "gitea.michaelthomson.dev/mthomson/habits/internal/test" + "gitea.michaelthomson.dev/mthomson/habits/internal/user/repository" + "gitea.michaelthomson.dev/mthomson/habits/internal/user/service" + "github.com/gofrs/uuid/v5" +) + +func TestCreateUser(t *testing.T) { + t.Parallel() + ctx := context.Background() + logger := slog.Default() + tdb := test.NewTestDatabase(t) + defer tdb.TearDown() + r := repository.NewUserRepository(logger, tdb.Db) + + userService := service.NewUserService(logger, r) + + t.Run("Create user", func(t *testing.T) { + uuid := NewUUID(t) + user := service.NewUser(uuid, "test@test.com", "supersecurehash") + + _, err := userService.CreateUser(ctx, user) + + AssertNoError(t, err) + }) +} + +func TestGetUser(t *testing.T) { + t.Parallel() + ctx := context.Background() + logger := slog.Default() + tdb := test.NewTestDatabase(t) + defer tdb.TearDown() + r := repository.NewUserRepository(logger, tdb.Db) + uuid := NewUUID(t) + + row := repository.UserRow{Id: uuid, Email: "test@test.com", HashedPassword: "supersecurehash"} + _, err := r.Create(ctx, row) + AssertNoError(t, err); + + userService := service.NewUserService(logger, r) + + t.Run("Get exisiting user", func(t *testing.T) { + _, err := userService.GetUser(ctx, uuid) + + AssertNoError(t, err) + }) + + t.Run("Get non-existant user", func(t *testing.T) { + _, err := userService.GetUser(ctx, NewUUID(t)) + + AssertErrors(t, err, service.ErrNotFound) + }) +} + +func TestDeleteUser(t *testing.T) { + t.Parallel() + ctx := context.Background() + logger := slog.Default() + tdb := test.NewTestDatabase(t) + defer tdb.TearDown() + r := repository.NewUserRepository(logger, tdb.Db) + uuid := NewUUID(t) + + row := repository.UserRow{Id: uuid, Email: "test@test.com", HashedPassword: "supersecurehash"} + _, err := r.Create(ctx, row) + AssertNoError(t, err); + + userService := service.NewUserService(logger, r) + + t.Run("Delete exisiting user", func(t *testing.T) { + err := userService.DeleteUser(ctx, uuid) + + AssertNoError(t, err) + }) + + t.Run("Delete non-existant user", func(t *testing.T) { + err := userService.DeleteUser(ctx, uuid) + + AssertErrors(t, err, service.ErrNotFound) + }) +} + +func TestUpdateUser(t *testing.T) { + t.Parallel() + ctx := context.Background() + logger := slog.Default() + tdb := test.NewTestDatabase(t) + defer tdb.TearDown() + r := repository.NewUserRepository(logger, tdb.Db) + uuid := NewUUID(t) + + row := repository.UserRow{Id: uuid, Email: "test@test.com", HashedPassword: "supersecurehash"} + _, err := r.Create(ctx, row) + AssertNoError(t, err); + + userService := service.NewUserService(logger, r) + + t.Run("Update exisiting user", func(t *testing.T) { + user := service.User{uuid, "new@email.com", "supersecurehash"} + + err := userService.UpdateUser(ctx, user) + + AssertNoError(t, err) + + newUser, err := userService.GetUser(ctx, uuid) + + AssertNoError(t, err) + + AssertUsers(t, newUser, user) + }) + + t.Run("Update non-existant user", func(t *testing.T) { + user := service.User{NewUUID(t), "new@email.com", "supersecurehash"} + + err := userService.UpdateUser(ctx, user) + + AssertErrors(t, err, service.ErrNotFound) + }) +} + +func AssertErrors(t testing.TB, got, want error) { + t.Helper() + if got != want { + t.Errorf("got error: %v, want error: %v", want, got) + } +} + +func AssertNoError(t testing.TB, err error) { + t.Helper() + if err != nil { + t.Errorf("expected no error, got %v", err) + } +} + +func AssertUsers(t testing.TB, got, want service.User) { + if !got.Equal(want) { + t.Errorf("got %+v want %+v", got, want) + } +} + +func NewUUID(t testing.TB) uuid.UUID { + t.Helper() + uuid, err := uuid.NewV4() + if err != nil { + t.Errorf("error generation uuid: %v", err) + } + return uuid +}