package service import ( "bytes" "context" "encoding/json" "io" "net/http" "net/url" "strings" "testing" "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" ) func setupShopifyIntegrationServiceTestDB(t *testing.T) *gorm.DB { t.Helper() db, err := gorm.Open(sqlite.Open("file::memory:"), &gorm.Config{ Logger: logger.Default.LogMode(logger.Silent), }) require.NoError(t, err, "failed to open SQLite test database") require.NoError(t, db.AutoMigrate( &model.Account{}, &model.Contact{}, &model.IntegrationHook{}, &model.IntegrationApp{}, ), "failed to auto-migrate models") t.Cleanup(func() { sqlDB, _ := db.DB() sqlDB.Close() }) return db } func setupShopifyIntegrationService(t *testing.T) (*ShopifyIntegrationService, *gorm.DB) { t.Helper() db := setupShopifyIntegrationServiceTestDB(t) hookRepo := repository.NewIntegrationHookRepo(db) svc := NewShopifyIntegrationService(hookRepo) svc.client = &shopifyAPIClient{apiVersion: "2025-01", httpClient: fakeShopifyHTTPClient(t, false)} return svc, db } func seedShopifyAccount(db *gorm.DB, t *testing.T) uint { t.Helper() account := &model.Account{Name: "Test Account"} require.NoError(t, db.Create(account).Error, "failed to seed account") return account.ID } func seedShopifyContact(db *gorm.DB, t *testing.T, accountID uint, email, phone string) *model.Contact { t.Helper() contact := &model.Contact{AccountID: accountID, Name: "Shopper", Email: email, PhoneNumber: phone} require.NoError(t, db.Create(contact).Error) return contact } type shopifyRoundTripFunc func(*http.Request) (*http.Response, error) func (f shopifyRoundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) { return f(req) } func fakeShopifyHTTPClient(t *testing.T, emptyCustomers bool) *http.Client { t.Helper() return &http.Client{Transport: shopifyRoundTripFunc(func(req *http.Request) (*http.Response, error) { assert.Equal(t, "shpat_abc123", req.Header.Get("X-Shopify-Access-Token")) switch req.URL.Path { case "/admin/api/2025-01/customers/search.json": assert.Equal(t, "email:test@example.com OR phone:+1234567890", req.URL.Query().Get("query")) assert.Equal(t, "id,email,phone", req.URL.Query().Get("fields")) if emptyCustomers { return shopifyHTTPResponse(http.StatusOK, `{"customers":[]}`), nil } return shopifyHTTPResponse(http.StatusOK, `{"customers":[{"id":"123","email":"test@example.com","phone":"+1234567890"}]}`), nil case "/admin/api/2025-01/orders.json": assert.Equal(t, "123", req.URL.Query().Get("customer_id")) assert.Equal(t, "any", req.URL.Query().Get("status")) assert.Equal(t, "id,email,created_at,total_price,currency,fulfillment_status,financial_status", req.URL.Query().Get("fields")) return shopifyHTTPResponse(http.StatusOK, `{"orders":[{"id":"456","email":"test@example.com","created_at":"2026-06-06T00:00:00Z","total_price":"100.00","currency":"USD","fulfillment_status":"fulfilled","financial_status":"paid"}]}`), nil default: return shopifyHTTPResponse(http.StatusNotFound, `{"error":"not found"}`), nil } })} } func shopifyHTTPResponse(status int, body string) *http.Response { return &http.Response{StatusCode: status, Header: http.Header{"Content-Type": []string{"application/json"}}, Body: io.NopCloser(bytes.NewBufferString(body))} } // ======================================== // ShopifyIntegrationService — Auth tests // ======================================== func TestShopifyIntegrationService_Auth_CreateNew(t *testing.T) { svc, db := setupShopifyIntegrationService(t) accountID := seedShopifyAccount(db, t) ctx := context.Background() req := CreateShopifyAuthRequest{ ShopDomain: "my-store.myshopify.com", AccessToken: "shpat_abc123", } hook, err := svc.Auth(ctx, accountID, req) require.NoError(t, err, "Auth should succeed for new integration") require.NotNil(t, hook, "Auth should return a hook") assert.Equal(t, accountID, hook.AccountID, "hook should belong to the account") assert.Equal(t, model.HookTypeShopify, hook.HookType, "hook type should be shopify") assert.Equal(t, model.HookStatusActive, hook.Status, "hook status should be active") assert.Equal(t, "https://my-store.myshopify.com/admin/api/webhooks.json", hook.URL, "hook URL should be constructed from shop domain") // Verify settings were stored correctly var settings model.ShopifySettings require.NoError(t, json.Unmarshal(hook.Settings, &settings), "settings should be valid JSON") assert.Equal(t, "my-store.myshopify.com", settings.ShopDomain, "settings shop_domain should match request") assert.Equal(t, "shpat_abc123", settings.AccessToken, "settings access_token should match request") // Verify the hook was persisted hooks, err := svc.hookRepo.FindByAccountAndType(ctx, accountID, model.HookTypeShopify) require.NoError(t, err) assert.Len(t, hooks, 1, "one Shopify hook should exist for the account") } func TestShopifyIntegrationService_BuildAuthRedirect_ChatwootPayload(t *testing.T) { svc, db := setupShopifyIntegrationService(t) accountID := seedShopifyAccount(db, t) t.Setenv("FRONTEND_URL", "https://app.example.test/") t.Setenv("SHOPIFY_CLIENT_ID", "shopify-client") t.Setenv("SHOPIFY_CLIENT_SECRET", "shopify-secret") result, err := svc.BuildAuthRedirect(context.Background(), accountID, CreateShopifyAuthRequest{ShopDomain: "my-store.myshopify.com"}) require.NoError(t, err) require.NotNil(t, result) require.True(t, strings.HasPrefix(result.RedirectURL, "https://my-store.myshopify.com/admin/oauth/authorize?")) parsed, err := url.Parse(result.RedirectURL) require.NoError(t, err) query := parsed.Query() assert.Equal(t, "shopify-client", query.Get("client_id")) assert.Equal(t, "read_customers,read_orders,read_fulfillments", query.Get("scope")) assert.Equal(t, "https://app.example.test/shopify/callback", query.Get("redirect_uri")) assert.NotEmpty(t, query.Get("state")) } func TestShopifyIntegrationService_Auth_UpdateExisting(t *testing.T) { svc, db := setupShopifyIntegrationService(t) accountID := seedShopifyAccount(db, t) ctx := context.Background() // Create initial integration initialReq := CreateShopifyAuthRequest{ ShopDomain: "old-store.myshopify.com", AccessToken: "shpat_old_token", } initialHook, err := svc.Auth(ctx, accountID, initialReq) require.NoError(t, err, "initial Auth should succeed") require.NotNil(t, initialHook) // Update with new credentials updateReq := CreateShopifyAuthRequest{ ShopDomain: "new-store.myshopify.com", AccessToken: "shpat_new_token", } updatedHook, err := svc.Auth(ctx, accountID, updateReq) require.NoError(t, err, "Auth update should succeed") require.NotNil(t, updatedHook) // Should return the same hook (updated, not a new one) assert.Equal(t, initialHook.ID, updatedHook.ID, "update should return the same hook ID") assert.Equal(t, "https://new-store.myshopify.com/admin/api/webhooks.json", updatedHook.URL, "URL should reflect updated shop domain") // Verify updated settings var settings model.ShopifySettings require.NoError(t, json.Unmarshal(updatedHook.Settings, &settings), "settings should be valid JSON") assert.Equal(t, "new-store.myshopify.com", settings.ShopDomain, "shop_domain should be updated") assert.Equal(t, "shpat_new_token", settings.AccessToken, "access_token should be updated") // Verify only one hook exists (no duplicates) hooks, err := svc.hookRepo.FindByAccountAndType(ctx, accountID, model.HookTypeShopify) require.NoError(t, err) assert.Len(t, hooks, 1, "still only one Shopify hook should exist after update") } // ======================================== // ShopifyIntegrationService — Delete tests // ======================================== func TestShopifyIntegrationService_Delete_Success(t *testing.T) { svc, db := setupShopifyIntegrationService(t) accountID := seedShopifyAccount(db, t) ctx := context.Background() // Create an integration first req := CreateShopifyAuthRequest{ ShopDomain: "my-store.myshopify.com", AccessToken: "shpat_abc123", } _, err := svc.Auth(ctx, accountID, req) require.NoError(t, err, "Auth should succeed before delete") // Verify hook exists before delete hooks, err := svc.hookRepo.FindByAccountAndType(ctx, accountID, model.HookTypeShopify) require.NoError(t, err) assert.Len(t, hooks, 1, "hook should exist before delete") // Delete the integration err = svc.Delete(ctx, accountID) require.NoError(t, err, "Delete should succeed") // Verify hook no longer exists hooks, err = svc.hookRepo.FindByAccountAndType(ctx, accountID, model.HookTypeShopify) require.NoError(t, err) assert.Len(t, hooks, 0, "no Shopify hooks should remain after delete") } func TestShopifyIntegrationService_Delete_NotFound(t *testing.T) { svc, db := setupShopifyIntegrationService(t) accountID := seedShopifyAccount(db, t) ctx := context.Background() // Delete without any integration configured err := svc.Delete(ctx, accountID) assert.Error(t, err, "Delete should fail when no Shopify integration exists") assert.Contains(t, err.Error(), "not found", "error should indicate integration not found") } func TestShopifyIntegrationService_GetOrders_ChatwootPayload(t *testing.T) { svc, db := setupShopifyIntegrationService(t) accountID := seedShopifyAccount(db, t) contact := seedShopifyContact(db, t, accountID, "test@example.com", "+1234567890") ctx := context.Background() _, err := svc.Auth(ctx, accountID, CreateShopifyAuthRequest{ShopDomain: "test-store.myshopify.com", AccessToken: "shpat_abc123"}) require.NoError(t, err) orders, err := svc.GetOrders(ctx, accountID, contact.ID) require.NoError(t, err) require.Len(t, orders, 1) assert.Equal(t, "456", orders[0]["id"]) assert.Equal(t, "https://test-store.myshopify.com/admin/orders/456", orders[0]["admin_url"]) } func TestShopifyIntegrationService_GetOrders_ContactInformationMissing(t *testing.T) { svc, db := setupShopifyIntegrationService(t) accountID := seedShopifyAccount(db, t) contact := seedShopifyContact(db, t, accountID, "", "") ctx := context.Background() _, err := svc.Auth(ctx, accountID, CreateShopifyAuthRequest{ShopDomain: "test-store.myshopify.com", AccessToken: "shpat_abc123"}) require.NoError(t, err) orders, err := svc.GetOrders(ctx, accountID, contact.ID) assert.Error(t, err) assert.Nil(t, orders) assert.Contains(t, err.Error(), "Contact information missing") } func TestShopifyIntegrationService_GetOrders_EmptyCustomers(t *testing.T) { svc, db := setupShopifyIntegrationService(t) svc.client = &shopifyAPIClient{apiVersion: "2025-01", httpClient: fakeShopifyHTTPClient(t, true)} accountID := seedShopifyAccount(db, t) contact := seedShopifyContact(db, t, accountID, "test@example.com", "+1234567890") ctx := context.Background() _, err := svc.Auth(ctx, accountID, CreateShopifyAuthRequest{ShopDomain: "test-store.myshopify.com", AccessToken: "shpat_abc123"}) require.NoError(t, err) orders, err := svc.GetOrders(ctx, accountID, contact.ID) require.NoError(t, err) assert.Empty(t, orders) }