package service import ( "context" "encoding/json" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "gorm.io/datatypes" "gorm.io/driver/sqlite" "gorm.io/gorm" "gorm.io/gorm/logger" "github.com/gochat/gochat/internal/model" "github.com/gochat/gochat/internal/repository" ) // ========== Test Setup ========== func setupCustomAttrValueServiceTestDB(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{}, &model.Contact{}, &model.ContactInbox{}, &model.Inbox{}, &model.Conversation{}, &model.Message{}, ); err != nil { t.Fatalf("failed to auto-migrate models: %v", err) } t.Cleanup(func() { sqlDB, _ := db.DB() sqlDB.Close() }) return db } func setupCustomAttrValueService(t *testing.T) (*CustomAttributeValueService, uint, *gorm.DB) { t.Helper() db := setupCustomAttrValueServiceTestDB(t) defRepo := repository.NewCustomAttributeDefinitionRepo(db) convRepo := repository.NewConversationRepo(db) contactRepo := repository.NewContactRepo(db) svc := NewCustomAttributeValueService(defRepo, convRepo, contactRepo) account := createTestAccount(t, db) return svc, account.ID, db } // Helper: create a conversation attribute definition func createConvAttrDef(t *testing.T, svc *CustomAttributeValueService, accountID uint, attrType string) { t.Helper() defRepo := svc.defRepo def := &model.CustomAttributeDefinition{ AccountID: accountID, AttributeName: "conv_" + attrType, AttributeDisplayName: "Conv " + attrType, AttributeType: attrType, AttributeModel: "conversation", } require.NoError(t, defRepo.Create(context.Background(), def)) } // Helper: create a contact attribute definition func createContactAttrDef(t *testing.T, svc *CustomAttributeValueService, accountID uint, attrType string) { t.Helper() defRepo := svc.defRepo def := &model.CustomAttributeDefinition{ AccountID: accountID, AttributeName: "contact_" + attrType, AttributeDisplayName: "Contact " + attrType, AttributeType: attrType, AttributeModel: "contact", } require.NoError(t, defRepo.Create(context.Background(), def)) } // Helper: create a minimal conversation (local version to avoid clash with service_test_helper.go) func createTestConversationForAttr(t *testing.T, db *gorm.DB, accountID uint) *model.Conversation { t.Helper() inbox := &model.Inbox{AccountID: accountID, Name: "Test Inbox", ChannelType: "web_widget"} require.NoError(t, db.Create(inbox).Error) contact := &model.Contact{AccountID: accountID, Name: "Test Contact"} require.NoError(t, db.Create(contact).Error) conv := &model.Conversation{AccountID: accountID, InboxID: inbox.ID, ContactID: contact.ID, Status: "open"} require.NoError(t, db.Create(conv).Error) return conv } // Helper: create a minimal contact func createTestContactForAttr(t *testing.T, db *gorm.DB, accountID uint) *model.Contact { t.Helper() contact := &model.Contact{AccountID: accountID, Name: "Attr Test Contact"} require.NoError(t, db.Create(contact).Error) return contact } // ========== validateValueType tests ========== func TestValidateValueType_Text(t *testing.T) { assert.NoError(t, validateValueType("text", "hello")) assert.Error(t, validateValueType("text", 42)) assert.Error(t, validateValueType("text", true)) } func TestValidateValueType_Link(t *testing.T) { assert.NoError(t, validateValueType("link", "https://example.com")) assert.Error(t, validateValueType("link", 42)) } func TestValidateValueType_Number(t *testing.T) { assert.NoError(t, validateValueType("number", float64(3.14))) assert.NoError(t, validateValueType("number", int(42))) assert.NoError(t, validateValueType("number", int64(100))) assert.NoError(t, validateValueType("number", int32(5))) assert.NoError(t, validateValueType("number", float32(1.5))) assert.Error(t, validateValueType("number", "not a number")) assert.Error(t, validateValueType("number", true)) } func TestValidateValueType_Date(t *testing.T) { assert.NoError(t, validateValueType("date", "2024-01-01")) assert.Error(t, validateValueType("date", 42)) assert.Error(t, validateValueType("date", true)) } func TestValidateValueType_Checkbox(t *testing.T) { assert.NoError(t, validateValueType("checkbox", true)) assert.NoError(t, validateValueType("checkbox", false)) assert.Error(t, validateValueType("checkbox", "yes")) assert.Error(t, validateValueType("checkbox", 1)) } func TestValidateValueType_List(t *testing.T) { // List accepts any value assert.NoError(t, validateValueType("list", "anything")) assert.NoError(t, validateValueType("list", 42)) assert.NoError(t, validateValueType("list", true)) assert.NoError(t, validateValueType("list", []string{"a", "b"})) } func TestValidateValueType_UnknownType(t *testing.T) { // Unknown types are accepted assert.NoError(t, validateValueType("unknown", "anything")) } // ========== mergeIntoJSON tests ========== func TestMergeIntoJSON_NewKey(t *testing.T) { result, err := mergeIntoJSON(nil, "priority", "high") require.NoError(t, err) var data map[string]interface{} require.NoError(t, json.Unmarshal(result, &data)) assert.Equal(t, "high", data["priority"]) } func TestMergeIntoJSON_ExistingJSON(t *testing.T) { existing := datatypes.JSON(`{"status":"open","source":"web"}`) result, err := mergeIntoJSON(existing, "priority", "high") require.NoError(t, err) var data map[string]interface{} require.NoError(t, json.Unmarshal(result, &data)) assert.Equal(t, "high", data["priority"]) assert.Equal(t, "open", data["status"]) assert.Equal(t, "web", data["source"]) } func TestMergeIntoJSON_OverwriteExistingKey(t *testing.T) { existing := datatypes.JSON(`{"priority":"low"}`) result, err := mergeIntoJSON(existing, "priority", "high") require.NoError(t, err) var data map[string]interface{} require.NoError(t, json.Unmarshal(result, &data)) assert.Equal(t, "high", data["priority"]) } func TestMergeIntoJSON_EmptyJSON(t *testing.T) { result, err := mergeIntoJSON(datatypes.JSON(``), "key", "val") require.NoError(t, err) var data map[string]interface{} require.NoError(t, json.Unmarshal(result, &data)) assert.Equal(t, "val", data["key"]) } // ========== removeFromJSON tests ========== func TestRemoveFromJSON_ExistingKey(t *testing.T) { existing := datatypes.JSON(`{"status":"open","priority":"high"}`) result, err := removeFromJSON(existing, "priority") require.NoError(t, err) var data map[string]interface{} require.NoError(t, json.Unmarshal(result, &data)) assert.Equal(t, "open", data["status"]) assert.NotContains(t, data, "priority") } func TestRemoveFromJSON_NonexistentKey(t *testing.T) { existing := datatypes.JSON(`{"status":"open"}`) result, err := removeFromJSON(existing, "nonexistent") require.NoError(t, err) var data map[string]interface{} require.NoError(t, json.Unmarshal(result, &data)) assert.Equal(t, "open", data["status"]) } func TestRemoveFromJSON_NilOrEmpty(t *testing.T) { result, err := removeFromJSON(nil, "key") require.NoError(t, err) assert.Equal(t, datatypes.JSON("{}"), result) result, err = removeFromJSON(datatypes.JSON(``), "key") require.NoError(t, err) assert.Equal(t, datatypes.JSON("{}"), result) } func TestRemoveFromJSON_AllKeys(t *testing.T) { existing := datatypes.JSON(`{"a":1,"b":2}`) result, err := removeFromJSON(existing, "a") require.NoError(t, err) result, err = removeFromJSON(result, "b") require.NoError(t, err) var data map[string]interface{} require.NoError(t, json.Unmarshal(result, &data)) assert.Empty(t, data) } // ========== SetConversationAttributeValue ========== func TestCustomAttributeValueService_SetConversationAttributeValue(t *testing.T) { svc, accountID, db := setupCustomAttrValueService(t) // Create a conversation attribute definition (text type) def := &model.CustomAttributeDefinition{ AccountID: accountID, AttributeName: "conv_priority", AttributeDisplayName: "Conv Priority", AttributeType: "text", AttributeModel: "conversation", } require.NoError(t, svc.defRepo.Create(context.Background(), def)) conv := createTestConversationForAttr(t, db, accountID) req := &SetAttributeValueRequest{ AttributeName: "conv_priority", Value: "high", } result, err := svc.SetConversationAttributeValue(context.Background(), accountID, conv.ID, req) require.NoError(t, err) var attrs map[string]interface{} require.NoError(t, json.Unmarshal(result.CustomAttributes, &attrs)) assert.Equal(t, "high", attrs["conv_priority"]) } func TestCustomAttributeValueService_SetConversationAttributeValue_WrongType(t *testing.T) { svc, accountID, db := setupCustomAttrValueService(t) // Create a number-type attribute definition def := &model.CustomAttributeDefinition{ AccountID: accountID, AttributeName: "conv_score", AttributeDisplayName: "Conv Score", AttributeType: "number", AttributeModel: "conversation", } require.NoError(t, svc.defRepo.Create(context.Background(), def)) conv := createTestConversationForAttr(t, db, accountID) req := &SetAttributeValueRequest{ AttributeName: "conv_score", Value: "not_a_number", // wrong type } _, err := svc.SetConversationAttributeValue(context.Background(), accountID, conv.ID, req) assert.Error(t, err, "should fail when value type doesn't match attribute type") } func TestCustomAttributeValueService_SetConversationAttributeValue_DefNotFound(t *testing.T) { svc, accountID, db := setupCustomAttrValueService(t) conv := createTestConversationForAttr(t, db, accountID) req := &SetAttributeValueRequest{ AttributeName: "nonexistent_attr", Value: "val", } _, err := svc.SetConversationAttributeValue(context.Background(), accountID, conv.ID, req) assert.Error(t, err, "should fail when attribute definition doesn't exist") } func TestCustomAttributeValueService_SetConversationAttributeValue_ConvNotFound(t *testing.T) { svc, accountID, _ := setupCustomAttrValueService(t) def := &model.CustomAttributeDefinition{ AccountID: accountID, AttributeName: "conv_priority", AttributeDisplayName: "Conv Priority", AttributeType: "text", AttributeModel: "conversation", } require.NoError(t, svc.defRepo.Create(context.Background(), def)) req := &SetAttributeValueRequest{ AttributeName: "conv_priority", Value: "high", } _, err := svc.SetConversationAttributeValue(context.Background(), accountID, 9999, req) assert.Error(t, err, "should fail when conversation doesn't exist") } // ========== RemoveConversationAttributeValue ========== func TestCustomAttributeValueService_RemoveConversationAttributeValue(t *testing.T) { svc, accountID, db := setupCustomAttrValueService(t) def := &model.CustomAttributeDefinition{ AccountID: accountID, AttributeName: "conv_priority", AttributeDisplayName: "Conv Priority", AttributeType: "text", AttributeModel: "conversation", } require.NoError(t, svc.defRepo.Create(context.Background(), def)) conv := createTestConversationForAttr(t, db, accountID) // First set the value setReq := &SetAttributeValueRequest{ AttributeName: "conv_priority", Value: "high", } _, err := svc.SetConversationAttributeValue(context.Background(), accountID, conv.ID, setReq) require.NoError(t, err) // Then remove it result, err := svc.RemoveConversationAttributeValue(context.Background(), accountID, conv.ID, "conv_priority") require.NoError(t, err) var attrs map[string]interface{} require.NoError(t, json.Unmarshal(result.CustomAttributes, &attrs)) assert.NotContains(t, attrs, "conv_priority") } func TestCustomAttributeValueService_RemoveConversationAttributeValue_DefNotFound(t *testing.T) { svc, accountID, db := setupCustomAttrValueService(t) conv := createTestConversationForAttr(t, db, accountID) _, err := svc.RemoveConversationAttributeValue(context.Background(), accountID, conv.ID, "nonexistent") assert.Error(t, err) } // ========== SetContactAttributeValue ========== func TestCustomAttributeValueService_SetContactAttributeValue(t *testing.T) { svc, accountID, db := setupCustomAttrValueService(t) def := &model.CustomAttributeDefinition{ AccountID: accountID, AttributeName: "contact_level", AttributeDisplayName: "Contact Level", AttributeType: "text", AttributeModel: "contact", } require.NoError(t, svc.defRepo.Create(context.Background(), def)) contact := createTestContactForAttr(t, db, accountID) req := &SetAttributeValueRequest{ AttributeName: "contact_level", Value: "VIP", } result, err := svc.SetContactAttributeValue(context.Background(), accountID, contact.ID, req) require.NoError(t, err) var attrs map[string]interface{} require.NoError(t, json.Unmarshal(result.CustomAttributes, &attrs)) assert.Equal(t, "VIP", attrs["contact_level"]) } func TestCustomAttributeValueService_SetContactAttributeValue_WrongType(t *testing.T) { svc, accountID, db := setupCustomAttrValueService(t) def := &model.CustomAttributeDefinition{ AccountID: accountID, AttributeName: "contact_score", AttributeDisplayName: "Contact Score", AttributeType: "number", AttributeModel: "contact", } require.NoError(t, svc.defRepo.Create(context.Background(), def)) contact := createTestContactForAttr(t, db, accountID) req := &SetAttributeValueRequest{ AttributeName: "contact_score", Value: "not_a_number", } _, err := svc.SetContactAttributeValue(context.Background(), accountID, contact.ID, req) assert.Error(t, err) } func TestCustomAttributeValueService_SetContactAttributeValue_DefNotFound(t *testing.T) { svc, accountID, db := setupCustomAttrValueService(t) contact := createTestContactForAttr(t, db, accountID) req := &SetAttributeValueRequest{ AttributeName: "nonexistent", Value: "val", } _, err := svc.SetContactAttributeValue(context.Background(), accountID, contact.ID, req) assert.Error(t, err) } // ========== RemoveContactAttributeValue ========== func TestCustomAttributeValueService_RemoveContactAttributeValue(t *testing.T) { svc, accountID, db := setupCustomAttrValueService(t) def := &model.CustomAttributeDefinition{ AccountID: accountID, AttributeName: "contact_level", AttributeDisplayName: "Contact Level", AttributeType: "text", AttributeModel: "contact", } require.NoError(t, svc.defRepo.Create(context.Background(), def)) contact := createTestContactForAttr(t, db, accountID) // Set value first setReq := &SetAttributeValueRequest{ AttributeName: "contact_level", Value: "VIP", } _, err := svc.SetContactAttributeValue(context.Background(), accountID, contact.ID, setReq) require.NoError(t, err) // Remove it result, err := svc.RemoveContactAttributeValue(context.Background(), accountID, contact.ID, "contact_level") require.NoError(t, err) var attrs map[string]interface{} require.NoError(t, json.Unmarshal(result.CustomAttributes, &attrs)) assert.NotContains(t, attrs, "contact_level") } func TestCustomAttributeValueService_RemoveContactAttributeValue_DefNotFound(t *testing.T) { svc, accountID, db := setupCustomAttrValueService(t) contact := createTestContactForAttr(t, db, accountID) _, err := svc.RemoveContactAttributeValue(context.Background(), accountID, contact.ID, "nonexistent") assert.Error(t, err) }