Files
gochat/internal/router/router_test.go
T

230 lines
8.0 KiB
Go

package router
import (
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/gin-gonic/gin"
"github.com/gochat/gochat/internal/config"
"github.com/gochat/gochat/internal/middleware"
"github.com/gochat/gochat/internal/model"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
func TestRegisterRoutesBootsWithChatwootParityConflictGroups(t *testing.T) {
gin.SetMode(gin.TestMode)
engine := gin.New()
RegisterRoutes(
engine,
nil,
nil,
nil,
&Handlers{},
nil,
nil,
&config.JWTConfig{},
middleware.CORSConfig{},
nil,
)
routes := map[string]bool{}
for _, route := range engine.Routes() {
routes[route.Method+" "+route.Path] = true
}
expected := []string{
"GET /.well-known/assetlinks.json",
"GET /.well-known/apple-app-site-association",
"GET /.well-known/microsoft-identity-association.json",
"GET /.well-known/cf-custom-hostname-challenge/:id",
"GET /app",
"GET /app/*params",
"GET /api/v1/accounts/:account_id/captain/assistants/tools",
"GET /api/v1/accounts/:account_id/captain/assistants/:assistant_id",
"GET /api/v1/widget/conversations",
"GET /api/v1/widget/conversations/toggle_status",
"PUT /public/api/v1/inboxes/:inbox_id/contacts/:contact_id/conversations/:conversation_id/messages/:message_id",
"GET /hc/:slug",
"GET /hc/:slug/sitemap.xml",
"GET /hc/:slug/:locale",
"GET /hc/:slug/:locale/search",
"GET /hc/:slug/:locale/articles.json",
"GET /hc/:slug/:locale/categories.json",
"GET /hc/:slug/:locale/categories/:category_slug",
"GET /hc/:slug/articles/:article_slug",
"GET /api/v2/accounts/:account_id/reports/summary",
"GET /api/v2/accounts/:account_id/live_reports/grouped_conversation_metrics",
"GET /webhooks/twitter",
"POST /webhooks/twitter",
"POST /webhooks/telegram/:bot_token",
"POST /webhooks/line/:line_channel_id",
"POST /webhooks/sms/:phone_number",
"GET /webhooks/whatsapp/:phone_number",
"POST /webhooks/whatsapp/:phone_number",
"POST /webhooks/tiktok",
"POST /webhooks/shopify",
}
for _, key := range expected {
if !routes[key] {
t.Fatalf("expected route %s to be registered", key)
}
}
}
func TestCustomDomainChallengeMatchesChatwootVerification(t *testing.T) {
gin.SetMode(gin.TestMode)
db := setupRouterPortalDB(t)
portal := &model.Portal{
AccountID: 1,
Name: "Help Center",
Slug: "help-center",
CustomDomain: "help.example.com",
SSLSettings: json.RawMessage(`{"cf_verification_id":"challenge-token","cf_verification_body":"cloudflare-body"}`),
}
if err := db.Create(portal).Error; err != nil {
t.Fatalf("failed to create portal: %v", err)
}
engine := gin.New()
engine.GET("/.well-known/cf-custom-hostname-challenge/:id", customDomainChallenge(db))
matched := performHostGet(engine, "/.well-known/cf-custom-hostname-challenge/challenge-token", "help.example.com:3000")
if matched.Code != http.StatusOK || matched.Body.String() != "cloudflare-body" {
t.Fatalf("expected matching challenge body, got %d %q", matched.Code, matched.Body.String())
}
missingDomain := performHostGet(engine, "/.well-known/cf-custom-hostname-challenge/challenge-token", "missing.example.com")
if missingDomain.Code != http.StatusNotFound || missingDomain.Body.String() != "Domain not found" {
t.Fatalf("expected domain 404, got %d %q", missingDomain.Code, missingDomain.Body.String())
}
wrongChallenge := performHostGet(engine, "/.well-known/cf-custom-hostname-challenge/wrong-token", "help.example.com")
if wrongChallenge.Code != http.StatusNotFound || wrongChallenge.Body.String() != "Challenge ID not found" {
t.Fatalf("expected challenge 404, got %d %q", wrongChallenge.Code, wrongChallenge.Body.String())
}
}
func TestWellKnownRoutesServeMobileAssociationPayloads(t *testing.T) {
gin.SetMode(gin.TestMode)
t.Setenv("ANDROID_BUNDLE_ID", "com.example.gochat")
t.Setenv("ANDROID_SHA256_CERT_FINGERPRINT", "AA:BB:CC")
t.Setenv("IOS_APP_ID", "TEAMID.com.example.gochat")
t.Setenv("AZURE_APP_ID", "azure-application-client-id")
engine := gin.New()
engine.GET("/.well-known/assetlinks.json", androidAssetlinks)
engine.GET("/.well-known/apple-app-site-association", appleAppSiteAssociation)
engine.GET("/.well-known/microsoft-identity-association.json", microsoftIdentityAssociation)
assetlinks := performGet(engine, "/.well-known/assetlinks.json")
if assetlinks.Code != http.StatusOK {
t.Fatalf("expected assetlinks 200, got %d", assetlinks.Code)
}
var androidPayload []map[string]any
if err := json.Unmarshal(assetlinks.Body.Bytes(), &androidPayload); err != nil {
t.Fatalf("invalid android assetlinks JSON: %v", err)
}
if androidPayload[0]["target"].(map[string]any)["package_name"] != "com.example.gochat" {
t.Fatalf("expected android package name, got %s", assetlinks.Body.String())
}
apple := performGet(engine, "/.well-known/apple-app-site-association")
if apple.Code != http.StatusOK {
t.Fatalf("expected apple association 200, got %d", apple.Code)
}
if !strings.Contains(apple.Body.String(), `"appID":"TEAMID.com.example.gochat"`) ||
!strings.Contains(apple.Body.String(), `/app/accounts/*/conversations/*`) {
t.Fatalf("expected apple app association payload, got %s", apple.Body.String())
}
microsoft := performGet(engine, "/.well-known/microsoft-identity-association.json")
if microsoft.Code != http.StatusOK {
t.Fatalf("expected microsoft association 200, got %d", microsoft.Code)
}
if !strings.Contains(microsoft.Body.String(), `"applicationId":"azure-application-client-id"`) {
t.Fatalf("expected microsoft application ID, got %s", microsoft.Body.String())
}
}
func TestDashboardIndexServesChatwootShell(t *testing.T) {
gin.SetMode(gin.TestMode)
t.Setenv("INSTALLATION_NAME", "GoChat Test")
t.Setenv("FRONTEND_URL", "https://app.example.test/")
t.Setenv("HELPCENTER_URL", "https://help.example.test/")
engine := gin.New()
engine.GET("/app", dashboardIndex)
engine.GET("/app/*params", dashboardIndex)
recorder := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodGet, "/app/accounts/1/conversations/42", nil)
req.Header.Set("Accept", "text/html")
engine.ServeHTTP(recorder, req)
if recorder.Code != http.StatusOK {
t.Fatalf("expected 200, got %d", recorder.Code)
}
body := recorder.Body.String()
if !strings.Contains(body, `<div id="app"></div>`) {
t.Fatalf("expected dashboard app mount in response: %s", body)
}
if !strings.Contains(body, `"hostURL":"https://app.example.test"`) {
t.Fatalf("expected frontend URL in chatwoot config: %s", body)
}
}
func TestDashboardIndexRejectsJSONLikeChatwoot(t *testing.T) {
gin.SetMode(gin.TestMode)
engine := gin.New()
engine.GET("/app", dashboardIndex)
engine.GET("/app/*params", dashboardIndex)
recorder := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodGet, "/app/accounts/1/conversations/42", nil)
req.Header.Set("Accept", "application/json")
engine.ServeHTTP(recorder, req)
if recorder.Code != http.StatusNotAcceptable {
t.Fatalf("expected 406, got %d", recorder.Code)
}
if !strings.Contains(recorder.Body.String(), "Please use API routes instead of dashboard routes for JSON requests") {
t.Fatalf("expected Chatwoot dashboard JSON error, got %s", recorder.Body.String())
}
}
func performGet(engine *gin.Engine, path string) *httptest.ResponseRecorder {
recorder := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodGet, path, nil)
engine.ServeHTTP(recorder, req)
return recorder
}
func performHostGet(engine *gin.Engine, path string, host string) *httptest.ResponseRecorder {
recorder := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodGet, path, nil)
req.Host = host
engine.ServeHTTP(recorder, req)
return recorder
}
func setupRouterPortalDB(t *testing.T) *gorm.DB {
t.Helper()
db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=private"), &gorm.Config{
Logger: logger.Default.LogMode(logger.Silent),
})
if err != nil {
t.Fatalf("failed to open sqlite: %v", err)
}
if err := db.AutoMigrate(&model.Portal{}); err != nil {
t.Fatalf("failed to migrate portal: %v", err)
}
return db
}