Files
gochat/backend/internal/handler/api/v1/crm_frontend_smoke_test.go
T
rogee aeddedf2a3 Reorganize repo: backend/, deploy/, docs/ layout + AGENTS.md
Restructure the monorepo into clear top-level directories:
- backend/: Go module root (cmd, internal, pkg, configs, migrations,
  docs/swagger, scripts, tests, go.mod, Makefile, .air.toml)
- deploy/: Docker (Dockerfile, docker-compose*), quickstart, fluentd
- docs/: project documentation + reports/ (moved from repo root)
- AGENTS.md: new AI coding-agent guide at repo root

Update all references to the new layout:
- Dockerfile: COPY backend/go.mod, COPY backend/ (context = repo root)
- docker-compose files: context ../.., dockerfile deploy/docker/Dockerfile,
  env_file ../../.env, volume mounts ../../backend:/app
- deploy/quickstart/compose.yaml: dockerfile deploy/docker/Dockerfile
- CI: working-directory: backend for go commands, file deploy/docker/Dockerfile,
  coverage path backend/coverage.out, health_check backend/scripts/
- backend/Makefile: docker target uses -f ../deploy/docker/Dockerfile ../
- README: architecture tree, quickstart, config paths updated

Move root stray scripts (rename_models.*, run_m11_tests.sh, verify_build.sh,
gorm_bool_main.go) to backend/scripts/legacy/. All moves via git mv to
preserve history. Build, vet, SQLite tests, and docker compose config verified.
2026-07-07 14:44:12 +08:00

486 lines
23 KiB
Go

package v1
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/gin-gonic/gin"
"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"
"github.com/gochat/gochat/internal/service"
)
func TestChatwootFrontendCRMSmoke(t *testing.T) {
gin.SetMode(gin.TestMode)
db, err := gorm.Open(sqlite.Open("file:crm_frontend_smoke?mode=memory&cache=shared"), &gorm.Config{
Logger: logger.Default.LogMode(logger.Silent),
})
require.NoError(t, err)
t.Cleanup(func() {
sqlDB, dbErr := db.DB()
if dbErr == nil {
_ = sqlDB.Close()
}
})
require.NoError(t, db.AutoMigrate(
&model.Account{},
&model.User{},
&model.AccountUser{},
&model.Tag{},
&model.Contact{},
&model.ContactLabel{},
&model.ContactInbox{},
&model.Inbox{},
&model.Company{},
&model.CompanyNote{},
&model.Note{},
&model.Conversation{},
&model.Message{},
&model.Attachment{},
))
account := &model.Account{Name: "CRM Smoke", Locale: "en", Active: true, Status: "active"}
require.NoError(t, db.Create(account).Error)
user := &model.User{Name: "Smoke Agent", Email: "smoke-agent@example.com", AccountID: account.ID}
require.NoError(t, db.Create(user).Error)
require.NoError(t, db.Create(&model.AccountUser{AccountID: account.ID, UserID: user.ID, Role: "administrator"}).Error)
inbox := &model.Inbox{AccountID: account.ID, Name: "Website", ChannelType: "web_widget", ChannelID: 1, Enabled: true}
require.NoError(t, db.Create(inbox).Error)
router := buildCRMFrontendSmokeRouter(t, db, account.ID, user.ID)
contactID := smokeCreateContact(t, router, account.ID, inbox.ID)
smokeAssertContactListAndSearch(t, router, account.ID, contactID)
smokeAssertContactShowUpdateLabelsAndNestedData(t, router, db, account.ID, user.ID, contactID, inbox.ID)
smokeAssertCompanyFlows(t, router, db, account.ID, contactID)
}
func buildCRMFrontendSmokeRouter(t *testing.T, db *gorm.DB, accountID uint, userID uint) *gin.Engine {
t.Helper()
contactRepo := repository.NewContactRepo(db)
contactInboxRepo := repository.NewContactInboxRepo(db)
noteRepo := repository.NewNoteRepo(db)
contactNoteRepo := repository.NewContactNoteRepo(db)
conversationRepo := repository.NewConversationRepo(db)
companyRepo := repository.NewCompanyRepo(db)
contactInboxSvc := service.NewContactInboxService(contactInboxRepo)
contactSvc := service.NewContactService(contactRepo, contactInboxSvc, noteRepo)
mergeSvc := service.NewContactMergeService(repository.NewContactMergeRepo(db), db)
contactNoteSvc := service.NewContactNoteService(contactRepo, contactNoteRepo)
conversationSvc := service.NewConversationService(conversationRepo, nil, nil, nil, nil, nil, nil)
companySvc := service.NewCompanyService(companyRepo, contactRepo, conversationRepo)
contactHandler := NewContactHandler(contactSvc, contactInboxSvc, mergeSvc, contactNoteSvc, conversationSvc)
companyHandler := NewCompanyHandler(companySvc)
router := gin.New()
router.Use(gin.Recovery())
router.Use(func(c *gin.Context) {
c.Set("account_id", accountID)
c.Set("user_id", userID)
c.Next()
})
contacts := router.Group("/api/v1/accounts/:id/contacts")
{
contacts.GET("/", contactHandler.List)
contacts.POST("/", contactHandler.Create)
contacts.GET("/search", contactHandler.Search)
contacts.GET("/:contact_id", contactHandler.Get)
contacts.PATCH("/:contact_id", contactHandler.Update)
contacts.POST("/:contact_id/destroy_custom_attributes", contactHandler.DeleteCustomAttributes)
contacts.DELETE("/:contact_id/avatar", contactHandler.DeleteAvatar)
contacts.GET("/:contact_id/labels", contactHandler.ListLabels)
contacts.POST("/:contact_id/labels", contactHandler.UpdateLabels)
contacts.GET("/:contact_id/contactable_inboxes", contactHandler.ContactableInboxes)
contacts.GET("/:contact_id/conversations", contactHandler.ListConversations)
contacts.GET("/:contact_id/notes", contactHandler.ListNotes)
contacts.POST("/:contact_id/notes", contactHandler.CreateNote)
}
companies := router.Group("/api/v1/accounts/:id/companies")
{
companies.GET("/", companyHandler.List)
companies.POST("/", companyHandler.Create)
companies.GET("/search", companyHandler.Search)
companies.GET("/:company_id", companyHandler.Get)
companies.PATCH("/:company_id", companyHandler.Update)
companies.POST("/:company_id/destroy_custom_attributes", companyHandler.DestroyCustomAttributes)
companies.DELETE("/:company_id/avatar", companyHandler.DeleteAvatar)
companies.GET("/:company_id/contacts", companyHandler.ListContacts)
companies.GET("/:company_id/contacts/search", companyHandler.SearchContacts)
companies.POST("/:company_id/contacts", companyHandler.AddContact)
companies.DELETE("/:company_id/contacts/:contact_id", companyHandler.RemoveContact)
companies.GET("/:company_id/conversations", companyHandler.ListConversations)
companies.GET("/:company_id/notes", companyHandler.ListNotes)
companies.POST("/:company_id/notes", companyHandler.CreateNote)
}
return router
}
func smokeCreateContact(t *testing.T, router *gin.Engine, accountID uint, inboxID uint) uint {
t.Helper()
body := map[string]any{
"name": "Jane Frontend",
"email": "jane.frontend@example.com",
"phone": "+15550101",
"identifier": "frontend-jane",
"inbox_id": inboxID,
"source_id": "frontend-source",
}
resp := smokeJSONRequest(t, router, http.MethodPost, fmt.Sprintf("/api/v1/accounts/%d/contacts/", accountID), body)
require.Equal(t, http.StatusOK, resp.Code, resp.Body.String())
data := smokeDecodeObject(t, resp)
payload := smokeObject(t, data, "payload")
contact := smokeObject(t, payload, "contact")
smokeAssertContactFixtureShape(t, contact, false)
require.Equal(t, "Jane Frontend", contact["name"])
require.Equal(t, "jane.frontend@example.com", contact["email"])
contactInbox := smokeObject(t, payload, "contact_inbox")
require.Equal(t, "frontend-source", contactInbox["source_id"])
smokeAssertInboxSlimFixtureShape(t, smokeObject(t, contactInbox, "inbox"))
return smokeUint(t, contact["id"])
}
func smokeAssertContactListAndSearch(t *testing.T, router *gin.Engine, accountID uint, contactID uint) {
t.Helper()
listURL := fmt.Sprintf("/api/v1/accounts/%d/contacts/?include_contact_inboxes=false&page=1&sort=name", accountID)
resp := smokeJSONRequest(t, router, http.MethodGet, listURL, nil)
require.Equal(t, http.StatusOK, resp.Code, resp.Body.String())
data := smokeDecodeObject(t, resp)
meta := smokeObject(t, data, "meta")
require.Equal(t, float64(1), meta["count"])
require.Equal(t, float64(1), meta["current_page"])
require.NotContains(t, meta, "total_count")
contacts := smokeArray(t, data, "payload")
require.Len(t, contacts, 1)
contact := contacts[0].(map[string]any)
smokeAssertContactFixtureShape(t, contact, false)
require.Equal(t, float64(contactID), contact["id"])
require.NotContains(t, contact, "contact_inboxes")
emptySearch := smokeJSONRequest(t, router, http.MethodGet, fmt.Sprintf("/api/v1/accounts/%d/contacts/search?include_contact_inboxes=false&page=1&sort=name&q=", accountID), nil)
require.Equal(t, http.StatusUnprocessableEntity, emptySearch.Code, emptySearch.Body.String())
require.Equal(t, "Specify search string with parameter q", smokeDecodeObject(t, emptySearch)["error"])
searchURL := fmt.Sprintf("/api/v1/accounts/%d/contacts/search?include_contact_inboxes=false&page=1&sort=name&q=Jane", accountID)
search := smokeJSONRequest(t, router, http.MethodGet, searchURL, nil)
require.Equal(t, http.StatusOK, search.Code, search.Body.String())
searchData := smokeDecodeObject(t, search)
require.Equal(t, float64(1), smokeObject(t, searchData, "meta")["count"])
require.Len(t, smokeArray(t, searchData, "payload"), 1)
}
func smokeAssertContactShowUpdateLabelsAndNestedData(t *testing.T, router *gin.Engine, db *gorm.DB, accountID, userID, contactID, inboxID uint) {
t.Helper()
showURL := fmt.Sprintf("/api/v1/accounts/%d/contacts/%d?include_contact_inboxes=false", accountID, contactID)
show := smokeJSONRequest(t, router, http.MethodGet, showURL, nil)
require.Equal(t, http.StatusOK, show.Code, show.Body.String())
showPayload := smokeObject(t, smokeDecodeObject(t, show), "payload")
smokeAssertContactFixtureShape(t, showPayload, false)
require.Equal(t, "Jane Frontend", showPayload["name"])
require.NotContains(t, showPayload, "contact_inboxes")
updateBody := map[string]any{"name": "Jane Updated", "custom_attributes": map[string]any{"plan": "pro", "tier": "gold"}}
update := smokeJSONRequest(t, router, http.MethodPatch, showURL, updateBody)
require.Equal(t, http.StatusOK, update.Code, update.Body.String())
updated := smokeObject(t, smokeDecodeObject(t, update), "payload")
smokeAssertContactFixtureShape(t, updated, false)
require.Equal(t, "Jane Updated", updated["name"])
require.Equal(t, "pro", smokeObject(t, updated, "custom_attributes")["plan"])
destroyAttrs := smokeJSONRequest(t, router, http.MethodPost, fmt.Sprintf("/api/v1/accounts/%d/contacts/%d/destroy_custom_attributes", accountID, contactID), map[string]any{"custom_attributes": []string{"tier"}})
require.Equal(t, http.StatusOK, destroyAttrs.Code, destroyAttrs.Body.String())
require.NotContains(t, smokeObject(t, smokeObject(t, smokeDecodeObject(t, destroyAttrs), "payload"), "custom_attributes"), "tier")
labelsURL := fmt.Sprintf("/api/v1/accounts/%d/contacts/%d/labels", accountID, contactID)
labels := smokeJSONRequest(t, router, http.MethodPost, labelsURL, map[string]any{"labels": []string{"customer-support", "vip"}})
require.Equal(t, http.StatusOK, labels.Code, labels.Body.String())
require.Equal(t, []any{"customer-support", "vip"}, smokeArray(t, smokeDecodeObject(t, labels), "payload"))
labelsGet := smokeJSONRequest(t, router, http.MethodGet, labelsURL, nil)
require.Equal(t, http.StatusOK, labelsGet.Code, labelsGet.Body.String())
require.Equal(t, []any{"customer-support", "vip"}, smokeArray(t, smokeDecodeObject(t, labelsGet), "payload"))
filteredURL := fmt.Sprintf("/api/v1/accounts/%d/contacts/?include_contact_inboxes=false&page=1&sort=name&labels[]=customer-support", accountID)
filtered := smokeJSONRequest(t, router, http.MethodGet, filteredURL, nil)
require.Equal(t, http.StatusOK, filtered.Code, filtered.Body.String())
require.Len(t, smokeArray(t, smokeDecodeObject(t, filtered), "payload"), 1)
contactable := smokeJSONRequest(t, router, http.MethodGet, fmt.Sprintf("/api/v1/accounts/%d/contacts/%d/contactable_inboxes", accountID, contactID), nil)
require.Equal(t, http.StatusOK, contactable.Code, contactable.Body.String())
contactablePayload := smokeArray(t, smokeDecodeObject(t, contactable), "payload")
require.Len(t, contactablePayload, 1)
smokeAssertInboxSlimFixtureShape(t, smokeObject(t, contactablePayload[0].(map[string]any), "inbox"))
note := smokeJSONRequest(t, router, http.MethodPost, fmt.Sprintf("/api/v1/accounts/%d/contacts/%d/notes", accountID, contactID), map[string]any{"note": map[string]any{"content": "Frontend note"}})
require.Equal(t, http.StatusOK, note.Code, note.Body.String())
notePayload := smokeDecodeObject(t, note)
smokeAssertContactNoteFixtureShape(t, notePayload)
require.Equal(t, "Frontend note", notePayload["content"])
notes := smokeJSONRequest(t, router, http.MethodGet, fmt.Sprintf("/api/v1/accounts/%d/contacts/%d/notes", accountID, contactID), nil)
require.Equal(t, http.StatusOK, notes.Code, notes.Body.String())
notesPayload := smokeDecodeArray(t, notes)
require.Len(t, notesPayload, 1)
smokeAssertContactNoteFixtureShape(t, notesPayload[0].(map[string]any))
createConversationForSmoke(t, db, accountID, userID, contactID, inboxID)
conversations := smokeJSONRequest(t, router, http.MethodGet, fmt.Sprintf("/api/v1/accounts/%d/contacts/%d/conversations", accountID, contactID), nil)
require.Equal(t, http.StatusOK, conversations.Code, conversations.Body.String())
payload := smokeArray(t, smokeDecodeObject(t, conversations), "payload")
require.Len(t, payload, 1)
conversation := payload[0].(map[string]any)
require.Equal(t, float64(accountID), conversation["account_id"])
require.NotEmpty(t, smokeObject(t, conversation, "meta"))
}
func smokeAssertCompanyFlows(t *testing.T, router *gin.Engine, db *gorm.DB, accountID uint, contactID uint) {
t.Helper()
companyCreate := smokeJSONRequest(t, router, http.MethodPost, fmt.Sprintf("/api/v1/accounts/%d/companies/", accountID), map[string]any{
"company": map[string]any{"name": "Acme Frontend", "domain": "acme.example", "custom_attributes": map[string]any{"plan": "enterprise", "region": "apac"}},
})
require.Equal(t, http.StatusOK, companyCreate.Code, companyCreate.Body.String())
createdCompany := smokeObject(t, smokeDecodeObject(t, companyCreate), "payload")
smokeAssertCompanyFixtureShape(t, createdCompany)
companyID := smokeUint(t, createdCompany["id"])
companyList := smokeJSONRequest(t, router, http.MethodGet, fmt.Sprintf("/api/v1/accounts/%d/companies/?page=1&sort=name", accountID), nil)
require.Equal(t, http.StatusOK, companyList.Code, companyList.Body.String())
companyListData := smokeDecodeObject(t, companyList)
companyMeta := smokeObject(t, companyListData, "meta")
require.Equal(t, float64(1), companyMeta["total_count"])
require.Equal(t, float64(1), companyMeta["page"])
listedCompanies := smokeArray(t, companyListData, "payload")
require.Len(t, listedCompanies, 1)
smokeAssertCompanyFixtureShape(t, listedCompanies[0].(map[string]any))
companySearch := smokeJSONRequest(t, router, http.MethodGet, fmt.Sprintf("/api/v1/accounts/%d/companies/search?q=Acme&page=1&sort=name", accountID), nil)
require.Equal(t, http.StatusOK, companySearch.Code, companySearch.Body.String())
require.Len(t, smokeArray(t, smokeDecodeObject(t, companySearch), "payload"), 1)
emptyCompanySearch := smokeJSONRequest(t, router, http.MethodGet, fmt.Sprintf("/api/v1/accounts/%d/companies/search?q=&page=1&sort=name", accountID), nil)
require.Equal(t, http.StatusUnprocessableEntity, emptyCompanySearch.Code, emptyCompanySearch.Body.String())
companyShowURL := fmt.Sprintf("/api/v1/accounts/%d/companies/%d", accountID, companyID)
companyShow := smokeJSONRequest(t, router, http.MethodGet, companyShowURL, nil)
require.Equal(t, http.StatusOK, companyShow.Code, companyShow.Body.String())
shownCompany := smokeObject(t, smokeDecodeObject(t, companyShow), "payload")
smokeAssertCompanyFixtureShape(t, shownCompany)
require.Equal(t, "Acme Frontend", shownCompany["name"])
companyUpdate := smokeJSONRequest(t, router, http.MethodPatch, companyShowURL, map[string]any{"company": map[string]any{"name": "Acme Updated", "custom_attributes": map[string]any{"segment": "platinum"}}})
require.Equal(t, http.StatusOK, companyUpdate.Code, companyUpdate.Body.String())
updatedCompany := smokeObject(t, smokeDecodeObject(t, companyUpdate), "payload")
smokeAssertCompanyFixtureShape(t, updatedCompany)
require.Equal(t, "Acme Updated", updatedCompany["name"])
destroyCompanyAttrs := smokeJSONRequest(t, router, http.MethodPost, fmt.Sprintf("/api/v1/accounts/%d/companies/%d/destroy_custom_attributes", accountID, companyID), map[string]any{"custom_attributes": []string{"region"}})
require.Equal(t, http.StatusOK, destroyCompanyAttrs.Code, destroyCompanyAttrs.Body.String())
require.NotContains(t, smokeObject(t, smokeObject(t, smokeDecodeObject(t, destroyCompanyAttrs), "payload"), "custom_attributes"), "region")
attach := smokeJSONRequest(t, router, http.MethodPost, fmt.Sprintf("/api/v1/accounts/%d/companies/%d/contacts", accountID, companyID), map[string]any{"contact_id": contactID})
require.Equal(t, http.StatusOK, attach.Code, attach.Body.String())
attachedContact := smokeObject(t, smokeDecodeObject(t, attach), "payload")
smokeAssertCompanyContactFixtureShape(t, attachedContact)
require.True(t, attachedContact["linked_to_current_company"].(bool))
companyContacts := smokeJSONRequest(t, router, http.MethodGet, fmt.Sprintf("/api/v1/accounts/%d/companies/%d/contacts?page=1", accountID, companyID), nil)
require.Equal(t, http.StatusOK, companyContacts.Code, companyContacts.Body.String())
companyContactsData := smokeDecodeObject(t, companyContacts)
require.Equal(t, float64(1), smokeObject(t, companyContactsData, "meta")["total_count"])
companyContactPayload := smokeArray(t, companyContactsData, "payload")
require.Len(t, companyContactPayload, 1)
smokeAssertCompanyContactFixtureShape(t, companyContactPayload[0].(map[string]any))
candidate := &model.Contact{AccountID: accountID, Name: "Search Candidate", Email: "candidate@example.com"}
require.NoError(t, db.Create(candidate).Error)
searchCandidates := smokeJSONRequest(t, router, http.MethodGet, fmt.Sprintf("/api/v1/accounts/%d/companies/%d/contacts/search?q=Candidate&page=1", accountID, companyID), nil)
require.Equal(t, http.StatusOK, searchCandidates.Code, searchCandidates.Body.String())
require.Len(t, smokeArray(t, smokeDecodeObject(t, searchCandidates), "payload"), 1)
companyNote := smokeJSONRequest(t, router, http.MethodPost, fmt.Sprintf("/api/v1/accounts/%d/companies/%d/notes", accountID, companyID), map[string]any{"content": "Company frontend note"})
require.Equal(t, http.StatusOK, companyNote.Code, companyNote.Body.String())
companyNotePayload := smokeObject(t, smokeDecodeObject(t, companyNote), "payload")
smokeAssertCompanyNoteFixtureShape(t, companyNotePayload)
require.Equal(t, "Company frontend note", companyNotePayload["content"])
companyNotes := smokeJSONRequest(t, router, http.MethodGet, fmt.Sprintf("/api/v1/accounts/%d/companies/%d/notes", accountID, companyID), nil)
require.Equal(t, http.StatusOK, companyNotes.Code, companyNotes.Body.String())
companyNotesPayload := smokeArray(t, smokeDecodeObject(t, companyNotes), "payload")
require.Len(t, companyNotesPayload, 1)
smokeAssertCompanyNoteFixtureShape(t, companyNotesPayload[0].(map[string]any))
companyConversations := smokeJSONRequest(t, router, http.MethodGet, fmt.Sprintf("/api/v1/accounts/%d/companies/%d/conversations", accountID, companyID), nil)
require.Equal(t, http.StatusOK, companyConversations.Code, companyConversations.Body.String())
require.Len(t, smokeArray(t, smokeDecodeObject(t, companyConversations), "payload"), 1)
}
func createConversationForSmoke(t *testing.T, db *gorm.DB, accountID, userID, contactID, inboxID uint) {
t.Helper()
// This test uses direct persistence for conversation state because the CRM frontend
// reads nested conversation endpoints after inbox/message flows have already created it.
now := time.Now().Unix()
displayID := uint(1)
conversation := &model.Conversation{
DisplayID: &displayID,
AccountID: accountID,
InboxID: inboxID,
ContactID: contactID,
Status: "open",
Priority: "low",
ChannelType: "Channel::WebWidget",
Channel: "web_widget",
LastActivityAt: &now,
LastMessageAt: &now,
}
require.NoError(t, db.Create(conversation).Error)
require.NoError(t, db.Create(&model.Message{
ConversationID: conversation.ID,
AccountID: accountID,
InboxID: inboxID,
SenderID: &userID,
SenderType: "User",
Content: "Hello from the frontend smoke",
ContentType: "text",
MessageType: "outgoing",
Status: "sent",
ContentAttributes: datatypes.JSON([]byte(`{}`)),
}).Error)
}
func smokeJSONRequest(t *testing.T, router *gin.Engine, method string, path string, body any) *httptest.ResponseRecorder {
t.Helper()
var reader *bytes.Reader
if body != nil {
payload, err := json.Marshal(body)
require.NoError(t, err)
reader = bytes.NewReader(payload)
} else {
reader = bytes.NewReader(nil)
}
req := httptest.NewRequest(method, path, reader)
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
return w
}
func smokeDecodeObject(t *testing.T, resp *httptest.ResponseRecorder) map[string]any {
t.Helper()
var data map[string]any
require.NoError(t, json.Unmarshal(resp.Body.Bytes(), &data), resp.Body.String())
return data
}
func smokeDecodeArray(t *testing.T, resp *httptest.ResponseRecorder) []any {
t.Helper()
var data []any
require.NoError(t, json.Unmarshal(resp.Body.Bytes(), &data), resp.Body.String())
return data
}
func smokeAssertContactFixtureShape(t *testing.T, contact map[string]any, includeInboxes bool) {
t.Helper()
for _, key := range []string{"additional_attributes", "availability_status", "blocked", "custom_attributes", "email", "id", "identifier", "name", "phone_number", "thumbnail"} {
require.Contains(t, contact, key, "contact payload should expose %s", key)
}
require.IsType(t, map[string]any{}, smokeObject(t, contact, "additional_attributes"))
require.IsType(t, map[string]any{}, smokeObject(t, contact, "custom_attributes"))
require.IsType(t, "", contact["availability_status"])
require.IsType(t, false, contact["blocked"])
if includeInboxes {
require.Contains(t, contact, "contact_inboxes")
}
}
func smokeAssertCompanyFixtureShape(t *testing.T, company map[string]any) {
t.Helper()
for _, key := range []string{"additional_attributes", "avatar_url", "contacts_count", "created_at", "custom_attributes", "description", "domain", "id", "last_activity_at", "name", "updated_at"} {
require.Contains(t, company, key, "company payload should expose %s", key)
}
require.IsType(t, map[string]any{}, smokeObject(t, company, "additional_attributes"))
require.IsType(t, map[string]any{}, smokeObject(t, company, "custom_attributes"))
}
func smokeAssertCompanyContactFixtureShape(t *testing.T, contact map[string]any) {
t.Helper()
smokeAssertContactFixtureShape(t, contact, false)
require.Contains(t, contact, "company")
require.Contains(t, contact, "company_id")
require.Contains(t, contact, "linked_to_current_company")
require.IsType(t, false, contact["linked_to_current_company"])
}
func smokeAssertInboxSlimFixtureShape(t *testing.T, inbox map[string]any) {
t.Helper()
for _, key := range []string{"avatar_url", "channel_id", "channel_type", "id", "name", "provider"} {
require.Contains(t, inbox, key, "inbox payload should expose %s", key)
}
}
func smokeAssertContactNoteFixtureShape(t *testing.T, note map[string]any) {
t.Helper()
for _, key := range []string{"account_id", "contact_id", "content", "created_at", "id", "updated_at", "user_id"} {
require.Contains(t, note, key, "contact note payload should expose %s", key)
}
}
func smokeAssertCompanyNoteFixtureShape(t *testing.T, note map[string]any) {
t.Helper()
for _, key := range []string{"company_id", "content", "created_at", "id", "updated_at", "user_id"} {
require.Contains(t, note, key, "company note payload should expose %s", key)
}
}
func smokeObject(t *testing.T, data map[string]any, key string) map[string]any {
t.Helper()
value, ok := data[key].(map[string]any)
require.Truef(t, ok, "%s should be an object in %#v", key, data)
return value
}
func smokeArray(t *testing.T, data map[string]any, key string) []any {
t.Helper()
value, ok := data[key].([]any)
require.Truef(t, ok, "%s should be an array in %#v", key, data)
return value
}
func smokeUint(t *testing.T, value any) uint {
t.Helper()
switch v := value.(type) {
case float64:
return uint(v)
case uint:
return v
default:
t.Fatalf("unexpected uint-compatible value %#v", value)
return 0
}
}