Files
gochat/internal/handler/api/v1/inbox_handler_parity_test.go
T

303 lines
14 KiB
Go

package v1
import (
"bytes"
"encoding/json"
"fmt"
"mime/multipart"
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
"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"
)
func TestInboxHandler_ChatwootSerializerParity(t *testing.T) {
gin.SetMode(gin.TestMode)
db, err := gorm.Open(sqlite.Open("file:inbox_handler_parity?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.Inbox{}, &model.WorkingHour{}))
account := &model.Account{Name: "Inbox Parity", Locale: "en", Active: true}
require.NoError(t, db.Create(account).Error)
widget := &model.Inbox{
AccountID: account.ID,
Name: "Website",
ChannelType: "web_widget",
ChannelID: 11,
Enabled: true,
AvatarURL: "https://example.com/avatar.png",
GreetingEnabled: true,
GreetingMessage: "Welcome",
EnableEmailCollect: true,
EnableAutoAssignment: true,
AllowMessagesAfterResolved: true,
SenderNameType: "friendly_name",
BusinessName: "Example Co",
Timezone: "UTC",
ChannelConfig: `{"website_token":"web-token","hmac_token":"hmac-token","widget_color":"#1f93ff","website_url":"https://example.com","welcome_title":"Hi","welcome_tagline":"We reply fast","reply_time":"in_a_few_minutes","pre_chat_form_enabled":true,"pre_chat_form_options":{"fields":[{"name":"email"}]},"continuity_via_email":true}`,
}
require.NoError(t, db.Create(widget).Error)
apiInbox := &model.Inbox{
AccountID: account.ID,
Name: "API",
ChannelType: "api",
ChannelID: 12,
Enabled: true,
WebhookURL: "https://example.com/hook",
Secret: "api-secret",
ChannelConfig: `{"identifier":"api-identifier","hmac_token":"api-hmac","additional_attributes":{"source":"frontend"}}`,
}
require.NoError(t, db.Create(apiInbox).Error)
router := setupInboxParityRouter(db)
list := inboxParityRequest(t, router, http.MethodGet, fmt.Sprintf("/api/v1/accounts/%d/inboxes/", account.ID), nil)
require.Equal(t, http.StatusOK, list.Code, list.Body.String())
listData := inboxParityObject(t, list)
require.NotContains(t, listData, "inboxes")
payload := listData["payload"].([]any)
require.Len(t, payload, 2)
first := payload[0].(map[string]any)
require.Equal(t, "Channel::Api", first["channel_type"])
require.Equal(t, "api-secret", first["secret"])
require.Equal(t, "api-identifier", first["inbox_identifier"])
show := inboxParityRequest(t, router, http.MethodGet, fmt.Sprintf("/api/v1/accounts/%d/inboxes/%d", account.ID, widget.ID), nil)
require.Equal(t, http.StatusOK, show.Code, show.Body.String())
showData := inboxParityObject(t, show)
require.NotContains(t, showData, "payload")
require.Equal(t, "Channel::WebWidget", showData["channel_type"])
require.Equal(t, "web-token", showData["website_token"])
require.Equal(t, "hmac-token", showData["hmac_token"])
require.Equal(t, "#1f93ff", showData["widget_color"])
require.Equal(t, "https://example.com", showData["website_url"])
require.Equal(t, true, showData["pre_chat_form_enabled"])
require.IsType(t, []any{}, showData["working_hours"])
update := inboxParityRequest(t, router, http.MethodPatch, fmt.Sprintf("/api/v1/accounts/%d/inboxes/%d", account.ID, widget.ID), map[string]any{
"name": "Website Updated",
"enable_auto_assignment": false,
})
require.Equal(t, http.StatusOK, update.Code, update.Body.String())
updateData := inboxParityObject(t, update)
require.Equal(t, "Website Updated", updateData["name"])
require.Equal(t, false, updateData["enable_auto_assignment"])
deleteAvatar := inboxParityRequest(t, router, http.MethodDelete, fmt.Sprintf("/api/v1/accounts/%d/inboxes/%d/avatar", account.ID, widget.ID), nil)
require.Equal(t, http.StatusOK, deleteAvatar.Code, deleteAvatar.Body.String())
require.Empty(t, deleteAvatar.Body.String())
destroy := inboxParityRequest(t, router, http.MethodDelete, fmt.Sprintf("/api/v1/accounts/%d/inboxes/%d", account.ID, apiInbox.ID), nil)
require.Equal(t, http.StatusOK, destroy.Code, destroy.Body.String())
require.Equal(t, "Your inbox deletion request will be processed in some time.", inboxParityObject(t, destroy)["message"])
}
func TestInboxHandler_ChatwootCreateUpdateRequestBinding(t *testing.T) {
gin.SetMode(gin.TestMode)
db, err := gorm.Open(sqlite.Open("file:inbox_handler_binding?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.Inbox{}, &model.WorkingHour{}))
account := &model.Account{Name: "Inbox Binding", Locale: "en", Active: true}
require.NoError(t, db.Create(account).Error)
router := setupInboxParityRouter(db)
websiteCreate := inboxParityMultipartRequest(t, router, http.MethodPost, fmt.Sprintf("/api/v1/accounts/%d/inboxes/", account.ID), map[string][]string{
"name": {"Website"},
"greeting_enabled": {"true"},
"greeting_message": {"Hello from Chatwoot"},
"enable_email_collect": {"false"},
"allow_messages_after_resolved": {"true"},
"sender_name_type": {"business_name"},
"business_name": {"Acme Support"},
"channel[type]": {"web_widget"},
"channel[website_url]": {"https://acme.example"},
"channel[widget_color]": {"#f97316"},
"channel[welcome_title]": {"Hi there"},
"channel[welcome_tagline]": {"We reply quickly"},
"channel[selected_feature_flags][]": {"attachments", "emoji_picker"},
})
require.Equal(t, http.StatusOK, websiteCreate.Code, websiteCreate.Body.String())
websiteData := inboxParityObject(t, websiteCreate)
require.Equal(t, "Channel::WebWidget", websiteData["channel_type"])
require.Equal(t, "https://acme.example", websiteData["website_url"])
require.Equal(t, "#f97316", websiteData["widget_color"])
require.Equal(t, "Hi there", websiteData["welcome_title"])
require.NotEmpty(t, websiteData["website_token"])
require.NotEmpty(t, websiteData["hmac_token"])
require.Equal(t, false, websiteData["enable_email_collect"])
require.Equal(t, "business_name", websiteData["sender_name_type"])
require.Equal(t, "Acme Support", websiteData["business_name"])
require.ElementsMatch(t, []any{"attachments", "emoji_picker"}, websiteData["selected_feature_flags"].([]any))
apiCreate := inboxParityRequest(t, router, http.MethodPost, fmt.Sprintf("/api/v1/accounts/%d/inboxes/", account.ID), map[string]any{
"name": "API",
"channel": map[string]any{
"type": "api",
"webhook_url": "https://hooks.example/chatwoot",
},
})
require.Equal(t, http.StatusOK, apiCreate.Code, apiCreate.Body.String())
apiData := inboxParityObject(t, apiCreate)
require.Equal(t, "Channel::Api", apiData["channel_type"])
require.Equal(t, "https://hooks.example/chatwoot", apiData["webhook_url"])
require.NotEmpty(t, apiData["secret"])
require.NotEmpty(t, apiData["hmac_token"])
require.NotEmpty(t, apiData["inbox_identifier"])
telegramCreate := inboxParityRequest(t, router, http.MethodPost, fmt.Sprintf("/api/v1/accounts/%d/inboxes/", account.ID), map[string]any{
"channel": map[string]any{
"type": "telegram",
"bot_token": "123:telegram-token",
},
})
require.Equal(t, http.StatusOK, telegramCreate.Code, telegramCreate.Body.String())
telegramData := inboxParityObject(t, telegramCreate)
require.Equal(t, "Telegram", telegramData["name"])
require.Equal(t, "Channel::Telegram", telegramData["channel_type"])
websiteID := uint(websiteData["id"].(float64))
update := inboxParityMultipartRequest(t, router, http.MethodPatch, fmt.Sprintf("/api/v1/accounts/%d/inboxes/%d", account.ID, websiteID), map[string][]string{
"name": {"Website Updated"},
"enable_auto_assignment": {"true"},
"csat_survey_enabled": {"true"},
"csat_config[display_type]": {"emoji"},
"csat_config[message]": {"Rate this conversation"},
"csat_config[survey_rules][operator]": {"contains"},
"csat_config[survey_rules][values][]": {"vip", "priority"},
"channel[website_url]": {"https://updated.example"},
"channel[welcome_tagline]": {"Updated tagline"},
})
require.Equal(t, http.StatusOK, update.Code, update.Body.String())
updateData := inboxParityObject(t, update)
require.Equal(t, "Website Updated", updateData["name"])
require.Equal(t, true, updateData["enable_auto_assignment"])
require.Equal(t, true, updateData["csat_survey_enabled"])
require.Equal(t, "https://updated.example", updateData["website_url"])
require.Equal(t, "Updated tagline", updateData["welcome_tagline"])
csatConfig := updateData["csat_config"].(map[string]any)
require.Equal(t, "Rate this conversation", csatConfig["message"])
require.Equal(t, "Please rate us", csatConfig["button_text"])
require.ElementsMatch(t, []any{"vip", "priority"}, csatConfig["survey_rules"].(map[string]any)["values"].([]any))
workingHoursUpdate := inboxParityRequest(t, router, http.MethodPut, fmt.Sprintf("/api/v1/accounts/%d/inboxes/%d", account.ID, websiteID), map[string]any{
"working_hours_enabled": true,
"out_of_office_message": "We are away",
"timezone": "Asia/Shanghai",
"working_hours": []map[string]any{
{"day_of_week": 0, "closed_all_day": true, "open_hour": "", "open_minutes": "", "close_hour": "", "close_minutes": "", "open_all_day": false},
{"day_of_week": 1, "closed_all_day": false, "open_hour": "9", "open_minutes": "30", "close_hour": "18", "close_minutes": "0", "open_all_day": false},
{"day_of_week": 2, "closed_all_day": false, "open_hour": "", "open_minutes": "", "close_hour": "", "close_minutes": "", "open_all_day": true},
{"day_of_week": 3, "closed_all_day": true, "open_hour": "", "open_minutes": "", "close_hour": "", "close_minutes": "", "open_all_day": false},
{"day_of_week": 4, "closed_all_day": true, "open_hour": "", "open_minutes": "", "close_hour": "", "close_minutes": "", "open_all_day": false},
{"day_of_week": 5, "closed_all_day": true, "open_hour": "", "open_minutes": "", "close_hour": "", "close_minutes": "", "open_all_day": false},
{"day_of_week": 6, "closed_all_day": true, "open_hour": "", "open_minutes": "", "close_hour": "", "close_minutes": "", "open_all_day": false},
},
"channel": map[string]any{},
})
require.Equal(t, http.StatusOK, workingHoursUpdate.Code, workingHoursUpdate.Body.String())
workingHoursData := inboxParityObject(t, workingHoursUpdate)
require.Equal(t, true, workingHoursData["working_hours_enabled"])
require.Equal(t, "We are away", workingHoursData["out_of_office_message"])
require.Equal(t, "Asia/Shanghai", workingHoursData["timezone"])
workingHours := workingHoursData["working_hours"].([]any)
require.Len(t, workingHours, 7)
monday := workingHours[1].(map[string]any)
require.Equal(t, float64(1), monday["day_of_week"])
require.Equal(t, float64(9), monday["open_hour"])
require.Equal(t, float64(30), monday["open_minutes"])
tuesday := workingHours[2].(map[string]any)
require.Equal(t, true, tuesday["open_all_day"])
require.Equal(t, float64(0), tuesday["open_hour"])
require.Equal(t, float64(23), tuesday["close_hour"])
showUpdated := inboxParityRequest(t, router, http.MethodGet, fmt.Sprintf("/api/v1/accounts/%d/inboxes/%d", account.ID, websiteID), nil)
require.Equal(t, http.StatusOK, showUpdated.Code, showUpdated.Body.String())
require.Len(t, inboxParityObject(t, showUpdated)["working_hours"].([]any), 7)
}
func setupInboxParityRouter(db *gorm.DB) *gin.Engine {
inboxSvc := service.NewInboxService(repository.NewInboxRepo(db), nil, nil, nil, nil, nil, nil)
handler := NewInboxHandler(inboxSvc)
router := gin.New()
inboxes := router.Group("/api/v1/accounts/:id/inboxes")
{
inboxes.GET("/", handler.List)
inboxes.GET("/:inbox_id", handler.Get)
inboxes.POST("/", handler.Create)
inboxes.PUT("/:inbox_id", handler.Update)
inboxes.PATCH("/:inbox_id", handler.Update)
inboxes.DELETE("/:inbox_id", handler.Delete)
inboxes.DELETE("/:inbox_id/avatar", handler.DeleteAvatar)
}
return router
}
func inboxParityRequest(t *testing.T, router *gin.Engine, method string, path string, body any) *httptest.ResponseRecorder {
t.Helper()
var reader *bytes.Reader
if body == nil {
reader = bytes.NewReader(nil)
} else {
payload, err := json.Marshal(body)
require.NoError(t, err)
reader = bytes.NewReader(payload)
}
req := httptest.NewRequest(method, path, reader)
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
return w
}
func inboxParityMultipartRequest(t *testing.T, router *gin.Engine, method string, path string, fields map[string][]string) *httptest.ResponseRecorder {
t.Helper()
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
for key, values := range fields {
for _, value := range values {
require.NoError(t, writer.WriteField(key, value))
}
}
require.NoError(t, writer.Close())
req := httptest.NewRequest(method, path, body)
req.Header.Set("Content-Type", writer.FormDataContentType())
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
return w
}
func inboxParityObject(t *testing.T, response *httptest.ResponseRecorder) map[string]any {
t.Helper()
var data map[string]any
require.NoError(t, json.Unmarshal(response.Body.Bytes(), &data), response.Body.String())
return data
}