package routers import ( "FamilyHub/src/api/services" "FamilyHub/src/domain" "bytes" "context" "database/sql" "encoding/json" "errors" "net/http" "net/http/httptest" "strings" "testing" "time" "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func int64Ptr(v int64) *int64 { return &v } func stringPtr(v string) *string { return &v } type familyServiceMock struct { createFn func(ctx context.Context, req domain.CreateFamilyRequest) (*domain.Family, error) getByIDFn func(ctx context.Context, id int64) (*domain.Family, error) updateFn func(ctx context.Context, id int64, req domain.UpdateFamilyRequest) (*domain.Family, error) deleteFn func(ctx context.Context, id int64) error } func (m *familyServiceMock) Create(ctx context.Context, req domain.CreateFamilyRequest) (*domain.Family, error) { if m.createFn != nil { return m.createFn(ctx, req) } return nil, errors.New("mock create is not configured") } func (m *familyServiceMock) GetByID(ctx context.Context, id int64) (*domain.Family, error) { if m.getByIDFn != nil { return m.getByIDFn(ctx, id) } return nil, errors.New("mock getByID is not configured") } func (m *familyServiceMock) Update(ctx context.Context, id int64, req domain.UpdateFamilyRequest) (*domain.Family, error) { if m.updateFn != nil { return m.updateFn(ctx, id, req) } return nil, errors.New("mock update is not configured") } func (m *familyServiceMock) 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 setupFamiliesRouter(mock services.FamilyService) *gin.Engine { gin.SetMode(gin.TestMode) r := gin.New() apiV1 := r.Group("/api/v1") router := NewFamiliesRouter(mock) router.RegisterRoutes(apiV1) return r } func sampleFamily() *domain.Family { return &domain.Family{ ID: 7, Name: "Belan", OwnerID: 10, TelegramChatID: int64Ptr(12345), TelegramChatName: stringPtr("Family Chat"), CreatedAt: time.Date(2026, time.January, 21, 10, 0, 0, 0, time.UTC), UpdatedAt: time.Date(2026, time.January, 21, 11, 0, 0, 0, time.UTC), } } func TestFamiliesRouter_Create(t *testing.T) { t.Run("bad request on malformed body", func(t *testing.T) { r := setupFamiliesRouter(&familyServiceMock{}) req := httptest.NewRequest(http.MethodPost, "/api/v1/families", bytes.NewBufferString(`{"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("internal error", func(t *testing.T) { r := setupFamiliesRouter(&familyServiceMock{createFn: func(ctx context.Context, req domain.CreateFamilyRequest) (*domain.Family, error) { return nil, errors.New("db unavailable") }}) req := httptest.NewRequest(http.MethodPost, "/api/v1/families", bytes.NewBufferString(`{"name":"Belan","owner_id":10,"telegram_chat_id":12345,"telegram_chat_name":"Family Chat"}`)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() r.ServeHTTP(w, req) require.Equal(t, http.StatusInternalServerError, w.Code) assert.Contains(t, w.Body.String(), "internal server error") }) t.Run("created", func(t *testing.T) { expected := sampleFamily() r := setupFamiliesRouter(&familyServiceMock{createFn: func(ctx context.Context, req domain.CreateFamilyRequest) (*domain.Family, error) { assert.Equal(t, "Belan", req.Name) assert.Nil(t, req.TelegramChatID) assert.Nil(t, req.TelegramChatName) return expected, nil }}) req := httptest.NewRequest(http.MethodPost, "/api/v1/families", bytes.NewBufferString(`{"name":"Belan","owner_id":10}`)) 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(), "Belan") }) } func TestFamiliesRouter_GetByID(t *testing.T) { t.Run("bad request on invalid id", func(t *testing.T) { r := setupFamiliesRouter(&familyServiceMock{}) req := httptest.NewRequest(http.MethodGet, "/api/v1/families/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 on service error", func(t *testing.T) { r := setupFamiliesRouter(&familyServiceMock{getByIDFn: func(ctx context.Context, id int64) (*domain.Family, error) { return nil, services.ErrFamilyNotFound }}) req := httptest.NewRequest(http.MethodGet, "/api/v1/families/7", nil) w := httptest.NewRecorder() r.ServeHTTP(w, req) require.Equal(t, http.StatusNotFound, w.Code) assert.Contains(t, w.Body.String(), services.ErrFamilyNotFound.Error()) }) t.Run("not found on sql no rows", func(t *testing.T) { r := setupFamiliesRouter(&familyServiceMock{getByIDFn: func(ctx context.Context, id int64) (*domain.Family, error) { return nil, sql.ErrNoRows }}) req := httptest.NewRequest(http.MethodGet, "/api/v1/families/7", nil) w := httptest.NewRecorder() r.ServeHTTP(w, req) require.Equal(t, http.StatusNotFound, w.Code) assert.Contains(t, w.Body.String(), "family not found") }) t.Run("ok", func(t *testing.T) { expected := sampleFamily() r := setupFamiliesRouter(&familyServiceMock{getByIDFn: func(ctx context.Context, id int64) (*domain.Family, error) { assert.Equal(t, int64(7), id) return expected, nil }}) req := httptest.NewRequest(http.MethodGet, "/api/v1/families/7", nil) w := httptest.NewRecorder() r.ServeHTTP(w, req) require.Equal(t, http.StatusOK, w.Code) assert.Contains(t, w.Body.String(), "Belan") }) } func TestFamiliesRouter_Update(t *testing.T) { t.Run("bad request on invalid id", func(t *testing.T) { r := setupFamiliesRouter(&familyServiceMock{}) req := httptest.NewRequest(http.MethodPatch, "/api/v1/families/abc", bytes.NewBufferString(`{"name":"Belan"}`)) 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 := setupFamiliesRouter(&familyServiceMock{}) req := httptest.NewRequest(http.MethodPatch, "/api/v1/families/7", bytes.NewBufferString(`{"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 missing name", func(t *testing.T) { r := setupFamiliesRouter(&familyServiceMock{}) req := httptest.NewRequest(http.MethodPatch, "/api/v1/families/7", bytes.NewBufferString(`{"telegram_chat_name":"Updated"}`)) 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(), "name is required") }) t.Run("not found", func(t *testing.T) { r := setupFamiliesRouter(&familyServiceMock{updateFn: func(ctx context.Context, id int64, req domain.UpdateFamilyRequest) (*domain.Family, error) { return nil, services.ErrFamilyNotFound }}) name := "Belan Updated" req := httptest.NewRequest(http.MethodPatch, "/api/v1/families/7", bytes.NewBufferString(`{"name":"`+name+`","telegram_chat_name":"Updated"}`)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() r.ServeHTTP(w, req) require.Equal(t, http.StatusNotFound, w.Code) assert.Contains(t, w.Body.String(), services.ErrFamilyNotFound.Error()) }) t.Run("ok", func(t *testing.T) { expected := sampleFamily() updatedName := "Belan Updated" expected.Name = updatedName r := setupFamiliesRouter(&familyServiceMock{updateFn: func(ctx context.Context, id int64, req domain.UpdateFamilyRequest) (*domain.Family, error) { assert.Equal(t, int64(7), id) require.NotNil(t, req.Name) assert.Equal(t, updatedName, *req.Name) require.NotNil(t, req.TelegramChatName) assert.Equal(t, "Updated", *req.TelegramChatName) return expected, nil }}) req := httptest.NewRequest(http.MethodPatch, "/api/v1/families/7", bytes.NewBufferString(`{"name":"`+updatedName+`","telegram_chat_name":"Updated"}`)) 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(), updatedName) }) } func TestFamiliesRouter_Delete(t *testing.T) { t.Run("bad request on invalid id", func(t *testing.T) { r := setupFamiliesRouter(&familyServiceMock{}) req := httptest.NewRequest(http.MethodDelete, "/api/v1/families/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 := setupFamiliesRouter(&familyServiceMock{deleteFn: func(ctx context.Context, id int64) error { return sql.ErrNoRows }}) req := httptest.NewRequest(http.MethodDelete, "/api/v1/families/7", nil) w := httptest.NewRecorder() r.ServeHTTP(w, req) require.Equal(t, http.StatusNotFound, w.Code) assert.Contains(t, w.Body.String(), "family not found") }) t.Run("no content", func(t *testing.T) { called := false r := setupFamiliesRouter(&familyServiceMock{deleteFn: func(ctx context.Context, id int64) error { called = true assert.Equal(t, int64(7), id) return nil }}) req := httptest.NewRequest(http.MethodDelete, "/api/v1/families/7", 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 TestFamiliesRouter_Create_ResponseShape(t *testing.T) { expected := sampleFamily() r := setupFamiliesRouter(&familyServiceMock{createFn: func(ctx context.Context, req domain.CreateFamilyRequest) (*domain.Family, error) { return expected, nil }}) req := httptest.NewRequest(http.MethodPost, "/api/v1/families", bytes.NewBufferString(`{"name":"Belan","owner_id":10,"telegram_chat_id":12345,"telegram_chat_name":"Family Chat"}`)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() r.ServeHTTP(w, req) require.Equal(t, http.StatusCreated, w.Code) var resp domain.FamilyResponse err := json.Unmarshal(w.Body.Bytes(), &resp) require.NoError(t, err) assert.Equal(t, expected.ID, resp.ID) assert.Equal(t, expected.Name, resp.Name) assert.Equal(t, expected.OwnerID, resp.OwnerID) assert.Equal(t, expected.TelegramChatID, resp.TelegramChatID) assert.Equal(t, expected.TelegramChatName, resp.TelegramChatName) assert.Equal(t, expected.CreatedAt.Format(time.RFC3339), resp.CreatedAt) assert.Equal(t, expected.UpdatedAt.Format(time.RFC3339), resp.UpdatedAt) }