package v1 import ( "bytes" "context" "encoding/json" "fmt" "net/http" "net/http/httptest" "testing" "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "gorm.io/driver/sqlite" "gorm.io/gorm" "gorm.io/gorm/logger" "github.com/gochat/gochat/internal/model" "github.com/gochat/gochat/internal/repository" "github.com/gochat/gochat/internal/service" ) // marshalNestedDef wraps body under "custom_attribute_definition" key // to match Chatwoot params.require(:custom_attribute_definition) format. func marshalNestedDef(body interface{}) []byte { wrapped := map[string]interface{}{"custom_attribute_definition": body} b, _ := json.Marshal(wrapped) return b } // ========== Test Setup ========== func setupCustomAttrDefHandlerTestDB(t *testing.T) *gorm.DB { t.Helper() db, err := gorm.Open(sqlite.Open("file::memory:"), &gorm.Config{ Logger: logger.Default.LogMode(logger.Silent), }) if err != nil { t.Fatalf("failed to open SQLite test database: %v", err) } if err := db.AutoMigrate( &model.Account{}, &model.User{}, &model.CustomAttributeDefinition{}, ); err != nil { t.Fatalf("failed to auto-migrate models: %v", err) } t.Cleanup(func() { sqlDB, _ := db.DB() sqlDB.Close() }) return db } func setupCustomAttrDefHandlerTest(t *testing.T) (*gin.Engine, *CustomAttributeDefinitionHandler, *gorm.DB, uint) { t.Helper() gin.SetMode(gin.TestMode) db := setupCustomAttrDefHandlerTestDB(t) repo := repository.NewCustomAttributeDefinitionRepo(db) svc := service.NewCustomAttributeDefinitionService(repo) handler := NewCustomAttributeDefinitionHandler(svc) r := gin.New() account := createHandlerTestAccount(t, db) return r, handler, db, account.ID } func createHandlerTestAccount(t *testing.T, db *gorm.DB) *model.Account { t.Helper() account := &model.Account{Name: "Test Account"} if err := db.Create(account).Error; err != nil { t.Fatalf("failed to create test account: %v", err) } return account } func seedCustomAttrDef(t *testing.T, db *gorm.DB, accountID uint, name, displayName, attrType, attrModel string) *model.CustomAttributeDefinition { t.Helper() repo := repository.NewCustomAttributeDefinitionRepo(db) svc := service.NewCustomAttributeDefinitionService(repo) def, err := svc.Create(context.Background(), accountID, &service.CreateCustomAttributeDefinitionRequest{ AttributeKey: name, AttributeDisplayName: displayName, AttributeDisplayType: attrType, AttributeModel: attrModel, }) require.NoError(t, err) return def } func fmtUint(id uint) string { return fmt.Sprintf("%d", id) } // ========== List ========== func TestCustomAttributeDefinitionHandler_List(t *testing.T) { r, handler, db, accountID := setupCustomAttrDefHandlerTest(t) seedCustomAttrDef(t, db, accountID, "custom_priority_score", "Custom Priority Score", "number", "conversation") seedCustomAttrDef(t, db, accountID, "source_link", "Source Link", "link", "conversation") r.GET("/api/v1/accounts/:account_id/custom_attribute_definitions", handler.List) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", fmt.Sprintf("/api/v1/accounts/%d/custom_attribute_definitions", accountID), nil) r.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) var resp map[string]interface{} err := json.Unmarshal(w.Body.Bytes(), &resp) require.NoError(t, err) assert.True(t, resp["success"].(bool)) data := resp["data"].([]interface{}) assert.Len(t, data, 2) } func TestCustomAttributeDefinitionHandler_List_FilterByModel(t *testing.T) { r, handler, db, accountID := setupCustomAttrDefHandlerTest(t) seedCustomAttrDef(t, db, accountID, "conv_attr", "Conv Attr", "text", "conversation") seedCustomAttrDef(t, db, accountID, "contact_attr", "Contact Attr", "text", "contact") r.GET("/api/v1/accounts/:account_id/custom_attribute_definitions", handler.List) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", fmt.Sprintf("/api/v1/accounts/%d/custom_attribute_definitions?attribute_model=conversation", accountID), nil) r.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) var resp map[string]interface{} err := json.Unmarshal(w.Body.Bytes(), &resp) require.NoError(t, err) data := resp["data"].([]interface{}) assert.Len(t, data, 1) } func TestCustomAttributeDefinitionHandler_List_InvalidAccountID(t *testing.T) { r, handler, _, _ := setupCustomAttrDefHandlerTest(t) r.GET("/api/v1/accounts/:account_id/custom_attribute_definitions", handler.List) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/api/v1/accounts/invalid/custom_attribute_definitions", nil) r.ServeHTTP(w, req) assert.Equal(t, http.StatusBadRequest, w.Code) } // ========== Get ========== func TestCustomAttributeDefinitionHandler_Get(t *testing.T) { r, handler, db, accountID := setupCustomAttrDefHandlerTest(t) def := seedCustomAttrDef(t, db, accountID, "custom_priority_score", "Custom Priority Score", "number", "conversation") r.GET("/api/v1/accounts/:account_id/custom_attribute_definitions/:id", handler.Get) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", fmt.Sprintf("/api/v1/accounts/%d/custom_attribute_definitions/%d", accountID, def.ID), nil) r.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) var resp map[string]interface{} err := json.Unmarshal(w.Body.Bytes(), &resp) require.NoError(t, err) assert.True(t, resp["success"].(bool)) } func TestCustomAttributeDefinitionHandler_Get_NotFound(t *testing.T) { r, handler, _, accountID := setupCustomAttrDefHandlerTest(t) r.GET("/api/v1/accounts/:account_id/custom_attribute_definitions/:id", handler.Get) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", fmt.Sprintf("/api/v1/accounts/%d/custom_attribute_definitions/9999", accountID), nil) r.ServeHTTP(w, req) assert.Equal(t, http.StatusNotFound, w.Code) } func TestCustomAttributeDefinitionHandler_Get_InvalidID(t *testing.T) { r, handler, _, accountID := setupCustomAttrDefHandlerTest(t) r.GET("/api/v1/accounts/:account_id/custom_attribute_definitions/:id", handler.Get) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", fmt.Sprintf("/api/v1/accounts/%d/custom_attribute_definitions/invalid", accountID), nil) r.ServeHTTP(w, req) assert.Equal(t, http.StatusBadRequest, w.Code) } // ========== Create ========== func TestCustomAttributeDefinitionHandler_Create(t *testing.T) { r, handler, _, accountID := setupCustomAttrDefHandlerTest(t) r.POST("/api/v1/accounts/:account_id/custom_attribute_definitions", handler.Create) body := service.CreateCustomAttributeDefinitionRequest{ AttributeKey: "custom_priority_score", AttributeDisplayName: "Custom Priority Score", AttributeDisplayType: "number", AttributeModel: "conversation", } bodyBytes := marshalNestedDef(body) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", fmt.Sprintf("/api/v1/accounts/%d/custom_attribute_definitions", accountID), bytes.NewReader(bodyBytes)) req.Header.Set("Content-Type", "application/json") r.ServeHTTP(w, req) assert.Equal(t, http.StatusCreated, w.Code) var resp map[string]interface{} err := json.Unmarshal(w.Body.Bytes(), &resp) require.NoError(t, err) assert.True(t, resp["success"].(bool)) data := resp["data"].(map[string]interface{}) assert.Equal(t, "custom_priority_score", data["attribute_key"]) } func TestCustomAttributeDefinitionHandler_Create_InvalidType(t *testing.T) { r, handler, _, accountID := setupCustomAttrDefHandlerTest(t) r.POST("/api/v1/accounts/:account_id/custom_attribute_definitions", handler.Create) body := service.CreateCustomAttributeDefinitionRequest{ AttributeKey: "custom_priority_score", AttributeDisplayName: "Custom Priority Score", AttributeDisplayType: "invalid_type", AttributeModel: "conversation", } bodyBytes := marshalNestedDef(body) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", fmt.Sprintf("/api/v1/accounts/%d/custom_attribute_definitions", accountID), bytes.NewReader(bodyBytes)) req.Header.Set("Content-Type", "application/json") r.ServeHTTP(w, req) assert.Equal(t, http.StatusBadRequest, w.Code) } func TestCustomAttributeDefinitionHandler_Create_InvalidAccountID(t *testing.T) { r, handler, _, _ := setupCustomAttrDefHandlerTest(t) r.POST("/api/v1/accounts/:account_id/custom_attribute_definitions", handler.Create) body := service.CreateCustomAttributeDefinitionRequest{ AttributeKey: "custom_priority_score", AttributeDisplayName: "Custom Priority Score", AttributeDisplayType: "number", AttributeModel: "conversation", } bodyBytes := marshalNestedDef(body) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/api/v1/accounts/invalid/custom_attribute_definitions", bytes.NewReader(bodyBytes)) req.Header.Set("Content-Type", "application/json") r.ServeHTTP(w, req) assert.Equal(t, http.StatusBadRequest, w.Code) } func TestCustomAttributeDefinitionHandler_Create_EmptyBody(t *testing.T) { r, handler, _, accountID := setupCustomAttrDefHandlerTest(t) r.POST("/api/v1/accounts/:account_id/custom_attribute_definitions", handler.Create) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", fmt.Sprintf("/api/v1/accounts/%d/custom_attribute_definitions", accountID), bytes.NewReader([]byte("{}"))) req.Header.Set("Content-Type", "application/json") r.ServeHTTP(w, req) assert.Equal(t, http.StatusBadRequest, w.Code) } // ========== Update ========== func TestCustomAttributeDefinitionHandler_Update(t *testing.T) { r, handler, db, accountID := setupCustomAttrDefHandlerTest(t) def := seedCustomAttrDef(t, db, accountID, "custom_priority_score", "Custom Priority Score", "number", "conversation") r.PUT("/api/v1/accounts/:account_id/custom_attribute_definitions/:id", handler.Update) body := service.UpdateCustomAttributeDefinitionRequest{ AttributeDisplayName: "Priority Level", AttributeDescription: "Updated description", } bodyBytes := marshalNestedDef(body) w := httptest.NewRecorder() req, _ := http.NewRequest("PUT", fmt.Sprintf("/api/v1/accounts/%d/custom_attribute_definitions/%d", accountID, def.ID), bytes.NewReader(bodyBytes)) req.Header.Set("Content-Type", "application/json") r.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) var resp map[string]interface{} err := json.Unmarshal(w.Body.Bytes(), &resp) require.NoError(t, err) data := resp["data"].(map[string]interface{}) assert.Equal(t, "Priority Level", data["attribute_display_name"]) } func TestCustomAttributeDefinitionHandler_Update_NotFound(t *testing.T) { r, handler, _, accountID := setupCustomAttrDefHandlerTest(t) r.PUT("/api/v1/accounts/:account_id/custom_attribute_definitions/:id", handler.Update) body := service.UpdateCustomAttributeDefinitionRequest{ AttributeDisplayName: "New Name", } bodyBytes := marshalNestedDef(body) w := httptest.NewRecorder() req, _ := http.NewRequest("PUT", fmt.Sprintf("/api/v1/accounts/%d/custom_attribute_definitions/9999", accountID), bytes.NewReader(bodyBytes)) req.Header.Set("Content-Type", "application/json") r.ServeHTTP(w, req) assert.Equal(t, http.StatusNotFound, w.Code) } func TestCustomAttributeDefinitionHandler_Update_InvalidID(t *testing.T) { r, handler, _, accountID := setupCustomAttrDefHandlerTest(t) r.PUT("/api/v1/accounts/:account_id/custom_attribute_definitions/:id", handler.Update) body := service.UpdateCustomAttributeDefinitionRequest{ AttributeDisplayName: "New Name", } bodyBytes := marshalNestedDef(body) w := httptest.NewRecorder() req, _ := http.NewRequest("PUT", fmt.Sprintf("/api/v1/accounts/%d/custom_attribute_definitions/invalid", accountID), bytes.NewReader(bodyBytes)) req.Header.Set("Content-Type", "application/json") r.ServeHTTP(w, req) assert.Equal(t, http.StatusBadRequest, w.Code) } // ========== Delete ========== func TestCustomAttributeDefinitionHandler_Delete(t *testing.T) { r, handler, db, accountID := setupCustomAttrDefHandlerTest(t) def := seedCustomAttrDef(t, db, accountID, "custom_priority_score", "Custom Priority Score", "number", "conversation") r.DELETE("/api/v1/accounts/:account_id/custom_attribute_definitions/:id", handler.Delete) w := httptest.NewRecorder() req, _ := http.NewRequest("DELETE", fmt.Sprintf("/api/v1/accounts/%d/custom_attribute_definitions/%d", accountID, def.ID), nil) r.ServeHTTP(w, req) assert.Equal(t, http.StatusNoContent, w.Code) } func TestCustomAttributeDefinitionHandler_Delete_NotFound(t *testing.T) { r, handler, _, accountID := setupCustomAttrDefHandlerTest(t) r.DELETE("/api/v1/accounts/:account_id/custom_attribute_definitions/:id", handler.Delete) w := httptest.NewRecorder() req, _ := http.NewRequest("DELETE", fmt.Sprintf("/api/v1/accounts/%d/custom_attribute_definitions/9999", accountID), nil) r.ServeHTTP(w, req) assert.Equal(t, http.StatusNotFound, w.Code) } func TestCustomAttributeDefinitionHandler_Delete_InvalidAccountID(t *testing.T) { r, handler, _, _ := setupCustomAttrDefHandlerTest(t) r.DELETE("/api/v1/accounts/:account_id/custom_attribute_definitions/:id", handler.Delete) w := httptest.NewRecorder() req, _ := http.NewRequest("DELETE", "/api/v1/accounts/invalid/custom_attribute_definitions/1", nil) r.ServeHTTP(w, req) assert.Equal(t, http.StatusBadRequest, w.Code) }