283 lines
11 KiB
Go
283 lines
11 KiB
Go
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)
|
|
}
|