package routers import ( "FamilyHub/src/api/services" "FamilyHub/src/domain" "bytes" "context" "encoding/json" "errors" "net/http" "net/http/httptest" "strconv" "strings" "testing" "time" "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) type userServiceMock struct { createFn func(ctx context.Context, req domain.CreateUserRequest) (*domain.UserModel, error) getByIDFn func(ctx context.Context, id int64) (*domain.UserModel, error) getByTelegramIDFn func(ctx context.Context, telegramID int64) (*domain.UserModel, error) updateFn func(ctx context.Context, id int64, req domain.UpdateUserRequest) (*domain.UserModel, error) deleteFn func(ctx context.Context, id int64) error } func (m *userServiceMock) Create(ctx context.Context, req domain.CreateUserRequest) (*domain.UserModel, error) { if m.createFn != nil { return m.createFn(ctx, req) } return nil, errors.New("mock create is not configured") } func (m *userServiceMock) GetByID(ctx context.Context, id int64) (*domain.UserModel, error) { if m.getByIDFn != nil { return m.getByIDFn(ctx, id) } return nil, errors.New("mock getByID is not configured") } func (m *userServiceMock) GetByTelegramID(ctx context.Context, telegramID int64) (*domain.UserModel, error) { if m.getByTelegramIDFn != nil { return m.getByTelegramIDFn(ctx, telegramID) } return nil, errors.New("mock getByTelegramID is not configured") } func (m *userServiceMock) Update(ctx context.Context, id int64, req domain.UpdateUserRequest) (*domain.UserModel, error) { if m.updateFn != nil { return m.updateFn(ctx, id, req) } return nil, errors.New("mock update is not configured") } func (m *userServiceMock) Delete(ctx context.Context, id int64) error { if m.deleteFn != nil { return m.deleteFn(ctx, id) } return errors.New("mock delete is not configured") } func setupUsersRouter(mock services.UserService) *gin.Engine { gin.SetMode(gin.TestMode) r := gin.New() apiV1 := r.Group("/api/v1") router := NewUsersRouter(mock) router.RegisterRoutes(apiV1) return r } func sampleUser() *domain.UserModel { username := "john" firstName := "John" lastName := "Doe" languageCode := "en" return &domain.UserModel{ ID: 10, TelegramID: 100500, Username: &username, FirstName: &firstName, LastName: &lastName, LanguageCode: &languageCode, CreatedAt: time.Date(2026, time.January, 21, 10, 11, 12, 0, time.UTC), UpdatedAt: time.Date(2026, time.January, 21, 12, 11, 12, 0, time.UTC), } } func TestUsersRouter_CreateUser(t *testing.T) { t.Run("bad request on malformed body", func(t *testing.T) { r := setupUsersRouter(&userServiceMock{}) req := httptest.NewRequest(http.MethodPost, "/api/v1/users", bytes.NewBufferString(`{"telegram_id":`)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() r.ServeHTTP(w, req) require.Equal(t, http.StatusBadRequest, w.Code) assert.Contains(t, w.Body.String(), "message") }) t.Run("bad request on domain validation error", func(t *testing.T) { r := setupUsersRouter(&userServiceMock{createFn: func(ctx context.Context, req domain.CreateUserRequest) (*domain.UserModel, error) { return nil, services.ErrTelegramIDMissing }}) req := httptest.NewRequest(http.MethodPost, "/api/v1/users", bytes.NewBufferString(`{"telegram_id":1,"first_name":"A"}`)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() r.ServeHTTP(w, req) require.Equal(t, http.StatusBadRequest, w.Code) assert.Contains(t, w.Body.String(), services.ErrTelegramIDMissing.Error()) }) t.Run("created", func(t *testing.T) { expected := sampleUser() r := setupUsersRouter(&userServiceMock{createFn: func(ctx context.Context, req domain.CreateUserRequest) (*domain.UserModel, error) { assert.Equal(t, int64(100500), req.TelegramID) require.NotNil(t, req.FirstName) assert.Equal(t, "John", *req.FirstName) return expected, nil }}) req := httptest.NewRequest(http.MethodPost, "/api/v1/users", bytes.NewBufferString(`{"telegram_id":100500,"first_name":"John"}`)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() r.ServeHTTP(w, req) require.Equal(t, http.StatusCreated, w.Code) assert.Contains(t, w.Body.String(), "100500") assert.Contains(t, w.Body.String(), "John") }) } func TestUsersRouter_GetByID(t *testing.T) { t.Run("bad request on invalid id", func(t *testing.T) { r := setupUsersRouter(&userServiceMock{}) req := httptest.NewRequest(http.MethodGet, "/api/v1/users/abc", nil) w := httptest.NewRecorder() r.ServeHTTP(w, req) require.Equal(t, http.StatusBadRequest, w.Code) assert.Contains(t, w.Body.String(), "invalid id") }) t.Run("not found", func(t *testing.T) { r := setupUsersRouter(&userServiceMock{getByIDFn: func(ctx context.Context, id int64) (*domain.UserModel, error) { return nil, services.ErrUserNotFound }}) req := httptest.NewRequest(http.MethodGet, "/api/v1/users/1", nil) w := httptest.NewRecorder() r.ServeHTTP(w, req) require.Equal(t, http.StatusNotFound, w.Code) assert.Contains(t, w.Body.String(), services.ErrUserNotFound.Error()) }) t.Run("ok", func(t *testing.T) { expected := sampleUser() r := setupUsersRouter(&userServiceMock{getByIDFn: func(ctx context.Context, id int64) (*domain.UserModel, error) { assert.Equal(t, int64(10), id) return expected, nil }}) req := httptest.NewRequest(http.MethodGet, "/api/v1/users/10", nil) w := httptest.NewRecorder() r.ServeHTTP(w, req) require.Equal(t, http.StatusOK, w.Code) assert.Contains(t, w.Body.String(), "100500") }) } func TestUsersRouter_GetByTelegramID(t *testing.T) { t.Run("bad request on invalid telegram id", func(t *testing.T) { r := setupUsersRouter(&userServiceMock{}) req := httptest.NewRequest(http.MethodGet, "/api/v1/users/by-telegram/abc", nil) w := httptest.NewRecorder() r.ServeHTTP(w, req) require.Equal(t, http.StatusBadRequest, w.Code) assert.Contains(t, w.Body.String(), "invalid telegram id") }) t.Run("ok", func(t *testing.T) { expected := sampleUser() r := setupUsersRouter(&userServiceMock{getByTelegramIDFn: func(ctx context.Context, telegramID int64) (*domain.UserModel, error) { assert.Equal(t, int64(100500), telegramID) return expected, nil }}) req := httptest.NewRequest(http.MethodGet, "/api/v1/users/by-telegram/100500", nil) w := httptest.NewRecorder() r.ServeHTTP(w, req) require.Equal(t, http.StatusOK, w.Code) assert.Contains(t, w.Body.String(), "John") }) } func TestUsersRouter_Update(t *testing.T) { t.Run("bad request on invalid id", func(t *testing.T) { r := setupUsersRouter(&userServiceMock{}) req := httptest.NewRequest(http.MethodPatch, "/api/v1/users/abc", bytes.NewBufferString(`{"first_name":"John"}`)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() r.ServeHTTP(w, req) require.Equal(t, http.StatusBadRequest, w.Code) assert.Contains(t, w.Body.String(), "invalid id") }) t.Run("bad request on malformed body", func(t *testing.T) { r := setupUsersRouter(&userServiceMock{}) req := httptest.NewRequest(http.MethodPatch, "/api/v1/users/10", bytes.NewBufferString(`{"first_name":`)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() r.ServeHTTP(w, req) require.Equal(t, http.StatusBadRequest, w.Code) assert.Contains(t, w.Body.String(), "message") }) t.Run("bad request on invalid patch", func(t *testing.T) { r := setupUsersRouter(&userServiceMock{updateFn: func(ctx context.Context, id int64, req domain.UpdateUserRequest) (*domain.UserModel, error) { return nil, services.ErrInvalidPatch }}) req := httptest.NewRequest(http.MethodPatch, "/api/v1/users/10", bytes.NewBufferString(`{"first_name":"John"}`)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() r.ServeHTTP(w, req) require.Equal(t, http.StatusBadRequest, w.Code) assert.Contains(t, w.Body.String(), services.ErrInvalidPatch.Error()) }) t.Run("ok", func(t *testing.T) { expected := sampleUser() r := setupUsersRouter(&userServiceMock{updateFn: func(ctx context.Context, id int64, req domain.UpdateUserRequest) (*domain.UserModel, error) { assert.Equal(t, int64(10), id) require.NotNil(t, req.FirstName) assert.Equal(t, "John", *req.FirstName) return expected, nil }}) req := httptest.NewRequest(http.MethodPatch, "/api/v1/users/10", bytes.NewBufferString(`{"first_name":"John"}`)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() r.ServeHTTP(w, req) require.Equal(t, http.StatusOK, w.Code) assert.Contains(t, w.Body.String(), "100500") }) } func TestUsersRouter_Delete(t *testing.T) { t.Run("bad request on invalid id", func(t *testing.T) { r := setupUsersRouter(&userServiceMock{}) req := httptest.NewRequest(http.MethodDelete, "/api/v1/users/abc", nil) w := httptest.NewRecorder() r.ServeHTTP(w, req) require.Equal(t, http.StatusBadRequest, w.Code) assert.Contains(t, w.Body.String(), "invalid id") }) t.Run("not found", func(t *testing.T) { r := setupUsersRouter(&userServiceMock{deleteFn: func(ctx context.Context, id int64) error { return services.ErrUserNotFound }}) req := httptest.NewRequest(http.MethodDelete, "/api/v1/users/1", nil) w := httptest.NewRecorder() r.ServeHTTP(w, req) require.Equal(t, http.StatusNotFound, w.Code) assert.Contains(t, w.Body.String(), services.ErrUserNotFound.Error()) }) t.Run("no content", func(t *testing.T) { called := false r := setupUsersRouter(&userServiceMock{deleteFn: func(ctx context.Context, id int64) error { called = true assert.Equal(t, int64(10), id) return nil }}) req := httptest.NewRequest(http.MethodDelete, "/api/v1/users/10", nil) w := httptest.NewRecorder() r.ServeHTTP(w, req) require.Equal(t, http.StatusNoContent, w.Code) assert.True(t, called) assert.Empty(t, strings.TrimSpace(w.Body.String())) }) } func TestUsersRouter_CreateUser_ResponseShape(t *testing.T) { expected := sampleUser() r := setupUsersRouter(&userServiceMock{createFn: func(ctx context.Context, req domain.CreateUserRequest) (*domain.UserModel, error) { return expected, nil }}) req := httptest.NewRequest(http.MethodPost, "/api/v1/users", bytes.NewBufferString(`{"telegram_id":100500,"first_name":"John"}`)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() r.ServeHTTP(w, req) require.Equal(t, http.StatusCreated, w.Code) var resp domain.UserResponse err := json.Unmarshal(w.Body.Bytes(), &resp) require.NoError(t, err) assert.Equal(t, expected.ID, resp.ID) assert.Equal(t, expected.TelegramID, resp.TelegramID) assert.Equal(t, expected.FirstName, resp.FirstName) assert.Equal(t, expected.CreatedAt.Format(time.RFC3339), resp.CreatedAt) assert.Equal(t, expected.UpdatedAt.Format(time.RFC3339), resp.UpdatedAt) } func TestUsersRouter_GetByID_UsesPathID(t *testing.T) { r := setupUsersRouter(&userServiceMock{getByIDFn: func(ctx context.Context, id int64) (*domain.UserModel, error) { assert.Equal(t, int64(42), id) u := sampleUser() u.ID = id return u, nil }}) req := httptest.NewRequest(http.MethodGet, "/api/v1/users/42", nil) w := httptest.NewRecorder() r.ServeHTTP(w, req) require.Equal(t, http.StatusOK, w.Code) assert.Contains(t, w.Body.String(), strconv.FormatInt(42, 10)) }