feat(custom-domains): expose cloudflare challenge

This commit is contained in:
2026-06-06 09:09:01 +08:00
parent 361e3b8311
commit c4b92b7a18
6 changed files with 128 additions and 8 deletions
+1
View File
@@ -28,6 +28,7 @@ var criticalRoutes = []route{
{Method: "GET", Path: "/.well-known/assetlinks.json", Controller: "android_app#assetlinks", Source: "routes.rb:657"},
{Method: "GET", Path: "/.well-known/apple-app-site-association", Controller: "apple_app#site_association", Source: "routes.rb:658"},
{Method: "GET", Path: "/.well-known/microsoft-identity-association.json", Controller: "microsoft#identity_association", Source: "routes.rb:659"},
{Method: "GET", Path: "/.well-known/cf-custom-hostname-challenge/:id", Controller: "custom_domains#verify", Source: "routes.rb:660"},
{Method: "POST", Path: "/api/v1/accounts/", Controller: "api/v1/accounts#create", Source: "routes.rb:47"},
{Method: "GET", Path: "/api/v1/accounts/:account_id", Controller: "api/v1/accounts#show", Source: "routes.rb:47"},
+8 -6
View File
@@ -49,14 +49,14 @@ Hermes task landing checklist:
## Current Baseline
- Current tracking checkpoint: 2026-06-06 after this implementation checkpoint, prepared as `feat(well-known): expose app association files`.
- Latest implementation checkpoint: this checkpoint, prepared as `feat(well-known): expose app association files`.
- Current tracking checkpoint: 2026-06-06 after this implementation checkpoint, prepared as `feat(custom-domains): expose cloudflare challenge`.
- Latest implementation checkpoint: this checkpoint, prepared as `feat(custom-domains): expose cloudflare challenge`.
- Latest documentation/tooling checkpoint: `docs: land full parity tracking plan`; this document is now the active follow-up plan and supersedes `.hermes/plans/*`.
- Worktree status at this implementation checkpoint: P2/P3 external service verification route drift is now implemented for Chatwoot `AndroidAppController#assetlinks`, `AppleAppController#site_association`, `MicrosoftController#identity_association`, and `routes.rb:657-659`. GoChat registers `/.well-known/assetlinks.json`, `/.well-known/apple-app-site-association`, and `/.well-known/microsoft-identity-association.json`; the payloads use `ANDROID_BUNDLE_ID`, `ANDROID_SHA256_CERT_FINGERPRINT`, `IOS_APP_ID`, and `AZURE_APP_ID` like the reference views/controllers. P2/P3 dashboard deep-link route drift is in Review. P3.8/P6.6l public help-center sitemap drift is in Review. P6.6g public article lists, P6.6h public categories, P6.6i public portal show, P6.6j public portal search, P6.6k public article show/markdown/tracking, P3.5 company avatar form/pagination drift, P3.11 account integrations apps/hooks API, P3.10 account webhook API, P3.9 account agent-bot API, P6.8 contact outbound voice call, P3.7d enterprise help-center article translation, P6.5 inbox agent-bot member actions, P6 message retry parity, P6 conversation custom-attribute response parity, and P3.7a-P3.7c help-center payload parity remain in Review. The fresh placeholder audit found remaining `chatwootParityStub` routes only in webhook handler-not-injected fallbacks, not in the reused dashboard account/contact/conversation/message/inbox critical path. P3.6 custom filters/custom attribute definitions and label CRUD are already in Review. B11.1a-B11.3e Captain/Copilot slices remain in Review; B12.1/B12.2/B12.3 smoke harnesses remain in Review; P5 durable job work through P5.13b is in Review. Next active implementation slice is another Phase 6 placeholder burn-down item or a new Phase 2/3 drift slice from fresh reference/smoke evidence.
- Worktree status at this implementation checkpoint: P2/P3 external service verification route drift is now implemented for Chatwoot `AndroidAppController#assetlinks`, `AppleAppController#site_association`, `MicrosoftController#identity_association`, `CustomDomainsController#verify`, and `routes.rb:657-660`. GoChat registers `/.well-known/assetlinks.json`, `/.well-known/apple-app-site-association`, `/.well-known/microsoft-identity-association.json`, and `/.well-known/cf-custom-hostname-challenge/:id`; the app association payloads use `ANDROID_BUNDLE_ID`, `ANDROID_SHA256_CERT_FINGERPRINT`, `IOS_APP_ID`, and `AZURE_APP_ID`, while the Cloudflare challenge route resolves `request.host` to `portals.custom_domain` and returns `ssl_settings.cf_verification_body` only when `cf_verification_id` matches. P2/P3 dashboard deep-link route drift is in Review. P3.8/P6.6l public help-center sitemap drift is in Review. P6.6g public article lists, P6.6h public categories, P6.6i public portal show, P6.6j public portal search, P6.6k public article show/markdown/tracking, P3.5 company avatar form/pagination drift, P3.11 account integrations apps/hooks API, P3.10 account webhook API, P3.9 account agent-bot API, P6.8 contact outbound voice call, P3.7d enterprise help-center article translation, P6.5 inbox agent-bot member actions, P6 message retry parity, P6 conversation custom-attribute response parity, and P3.7a-P3.7c help-center payload parity remain in Review. The fresh placeholder audit found remaining `chatwootParityStub` routes only in webhook handler-not-injected fallbacks, not in the reused dashboard account/contact/conversation/message/inbox critical path. P3.6 custom filters/custom attribute definitions and label CRUD are already in Review. B11.1a-B11.3e Captain/Copilot slices remain in Review; B12.1/B12.2/B12.3 smoke harnesses remain in Review; P5 durable job work through P5.13b is in Review. Next active implementation slice is another Phase 6 placeholder burn-down item or a new Phase 2/3 drift slice from fresh reference/smoke evidence.
- `go test ./...` passes.
- Route dump succeeds with `TOTAL: 886` after adding the `.well-known` app association routes.
- Route dump succeeds with `TOTAL: 887` after adding the `.well-known` Cloudflare custom hostname challenge route.
- Route parity artifacts now exist under `docs/parity/` and are generated by `cmd/route_parity`.
- Tracked frontend-critical route audit covers 347 Chatwoot routes: 338 exact, 0 method-compatible, 9 parameter-compatible, 0 missing. The 9 parameter-compatible routes are Gin-internal parameter-name differences for nested AgentCapacityPolicy users/inbox limits plus the public article `.md`/`.png` suffixes served through the same external article route dispatcher.
- Tracked frontend-critical route audit covers 348 Chatwoot routes: 339 exact, 0 method-compatible, 9 parameter-compatible, 0 missing. The 9 parameter-compatible routes are Gin-internal parameter-name differences for nested AgentCapacityPolicy users/inbox limits plus the public article `.md`/`.png` suffixes served through the same external article route dispatcher.
- `/api/v1/widget` stubs are burned down and public inbox/contact/conversation/message core flows are backed by real handlers.
- Handler test stability fixes are committed into the baseline before feature parity work continues.
- `.codegraph/` is generated indexing output and is not part of tracked product code.
@@ -138,7 +138,7 @@ This table is the shortest authoritative handoff view. If an older lower section
| Priority | Workstream | Current state | Next checkpoint | Commit close rule |
| --- | --- | --- | --- | --- |
| 1 | Phase 2/3 drift | Tracked route parity is 0 missing for the current 347-route critical set; dashboard `/app` shell routes from `routes.rb:19-20`, `.well-known` app association routes from `routes.rb:657-659`, help-center portal/category/article routes from `routes.rb:385-404`, public help-center portal/sitemap/article/category/search/article-detail routes from `routes.rb:590-601`, enterprise contact outbound voice call from `routes.rb:216`, account agent-bot routes from `routes.rb:94-97`, account webhook routes from `routes.rb:342`, and account integration app/hook routes from `routes.rb:345-348` are now explicitly tracked. Notification list/action serializers, user notification-settings raw payloads, campaigns raw payload/display-id routes, Devise password reset/confirmation payloads, CRM shared attachment payloads plus fixed 100-row attachment pagination, account/settings payloads, assignable-agent payloads, agent index full-list behavior, agent create/update/delete defaults/errors/scope, account agent-bot route/payload/mutation behavior, account webhook payload/mutation behavior, integration app/hook payload behavior, label CRUD payloads, custom filters, custom attribute definitions, contact outbound voice calls, help-center portal/category/article payloads, dashboard app shell route behavior, app association JSON payloads, public widget popular-article lists, public help-center category list/show payloads, public portal show/default-locale payloads, public portal search payloads, public article show/markdown/tracking routes, and public sitemap XML now match the inspected Chatwoot contract. | Run the next evidence-backed route/controller/serializer drift audit. | Regenerate parity artifacts when routes change and add endpoint-family fixture tests. |
| 1 | Phase 2/3 drift | Tracked route parity is 0 missing for the current 348-route critical set; dashboard `/app` shell routes from `routes.rb:19-20`, `.well-known` app association and custom-domain challenge routes from `routes.rb:657-660`, help-center portal/category/article routes from `routes.rb:385-404`, public help-center portal/sitemap/article/category/search/article-detail routes from `routes.rb:590-601`, enterprise contact outbound voice call from `routes.rb:216`, account agent-bot routes from `routes.rb:94-97`, account webhook routes from `routes.rb:342`, and account integration app/hook routes from `routes.rb:345-348` are now explicitly tracked. Notification list/action serializers, user notification-settings raw payloads, campaigns raw payload/display-id routes, Devise password reset/confirmation payloads, CRM shared attachment payloads plus fixed 100-row attachment pagination, account/settings payloads, assignable-agent payloads, agent index full-list behavior, agent create/update/delete defaults/errors/scope, account agent-bot route/payload/mutation behavior, account webhook payload/mutation behavior, integration app/hook payload behavior, label CRUD payloads, custom filters, custom attribute definitions, contact outbound voice calls, help-center portal/category/article payloads, dashboard app shell route behavior, app association JSON payloads, Cloudflare custom hostname verification, public widget popular-article lists, public help-center category list/show payloads, public portal show/default-locale payloads, public portal search payloads, public article show/markdown/tracking routes, and public sitemap XML now match the inspected Chatwoot contract. | Run the next evidence-backed route/controller/serializer drift audit. | Regenerate parity artifacts when routes change and add endpoint-family fixture tests. |
| 2 | Phase 6 placeholder audit | Widget/public/webhook critical placeholders are burned down; inbox WhatsApp health/register-webhook and sync-template drift are closed; fresh `rg` audit shows only webhook nil-handler fallbacks still call `chatwootParityStub`; dashboard conversation transcript/custom-attribute response drift and message retry status drift are closed. | Continue targeted account/contact/conversation/message/inbox drift from reference/smoke evidence. | `rg` placeholder audit is recorded and no reused-frontend blocker is ownerless. |
| 3 | P3.9 account agent-bot API | Implemented for the reused dashboard AgentBots settings route with no-trailing-slash routes, PATCH update, raw Jbuilder-style payloads, account mutation scope, system-bot show/list visibility, empty `200 OK` delete, and full reset/avatar action payloads. | Keep in Review; reopen only if live settings smoke exposes avatar upload storage or administrator-secret gating drift. | Focused AgentBot handler tests, service/router focused tests, route dump/parity regeneration, full `go test ./...`, and `git diff --check` passed. |
| 4 | P3.10 account webhooks API | Implemented for the reused dashboard Webhooks settings route with PATCH update, Chatwoot `{ payload }` list/mutation serializers, nested `{ webhook: ... }` bodies, generated secret, account-scoped mutations, URL/subscription validation, optional inbox serialization, and empty `200 OK` delete. | Keep in Review; reopen only if live settings smoke exposes audit writer or delivery-signature drift beyond the existing delivery service boundary. | Focused webhook handler/service/router tests, migration test, route dump/parity regeneration, full `go test ./...`, and `git diff --check` passed. |
@@ -207,6 +207,7 @@ This ledger records the committed parity checkpoints that future slices should b
| Commit | Scope | Verification summary | Follow-up state |
| --- | --- | --- | --- |
| `feat(custom-domains): expose cloudflare challenge` | Advances Phase 2/3 public custom-domain parity by matching Chatwoot enterprise `CustomDomainsController#verify`, Cloudflare SSL setting keys from `Cloudflare::BaseCloudflareZoneService`, and `routes.rb:660`. GoChat now registers `/.well-known/cf-custom-hostname-challenge/:id`, resolves the request host against `portals.custom_domain`, returns `Domain not found` for unknown domains, returns `Challenge ID not found` for mismatched `ssl_settings.cf_verification_id`, and serves the stored `cf_verification_body` as plain text on success. | `go test ./internal/router -run 'CustomDomain\|WellKnown\|RegisterRoutes' -count=1`; `go test ./cmd/route_parity -count=1`; `go run ./cmd/dump_routes > docs/parity/gochat_routes.txt`; `go run ./cmd/route_parity`; `go test ./...`; `git diff --check`. Route dump is `TOTAL: 887`; tracked route parity is `339 exact, 0 method-compatible, 9 parameter-compatible, 0 missing out of 348`. | Cloudflare custom hostname verification moves to Review for current reference evidence; continue Phase 2/3 drift audit, Phase 6 placeholder audit, or B12 live smoke. |
| `feat(well-known): expose app association files` | Advances Phase 2/3 public route parity by matching Chatwoot `AndroidAppController#assetlinks`, `AppleAppController#site_association`, `MicrosoftController#identity_association`, `assetlinks.json.erb`, `site_association.html.erb`, `identity_association.json.jbuilder`, and `routes.rb:657-659`. GoChat now registers `/.well-known/assetlinks.json`, `/.well-known/apple-app-site-association`, and `/.well-known/microsoft-identity-association.json`; Android payloads use `ANDROID_BUNDLE_ID` and `ANDROID_SHA256_CERT_FINGERPRINT`, Apple payloads use `IOS_APP_ID` plus `/app/accounts/*/conversations/*`, and Microsoft payloads use `AZURE_APP_ID`. | `go test ./internal/router -run 'WellKnown\|RegisterRoutes' -count=1`; `go test ./cmd/route_parity -count=1`; `go run ./cmd/dump_routes > docs/parity/gochat_routes.txt`; `go run ./cmd/route_parity`; `go test ./...`; `git diff --check`. Route dump is `TOTAL: 886`; tracked route parity is `338 exact, 0 method-compatible, 9 parameter-compatible, 0 missing out of 347`. | `.well-known` association routes move to Review for current reference/spec evidence; continue Phase 2/3 drift audit, Phase 6 placeholder audit, or B12 live smoke. |
| `feat(dashboard): expose app shell routes` | Advances Phase 2/3 dashboard route parity by matching Chatwoot `DashboardController#index`, `routes.rb:19-20`, `layouts/vueapp.html.erb`, and mailer/push helper paths that resolve to `/app/accounts/:account_id/conversations/:display_id`. GoChat now registers `/app` and `/app/*params`, returns a Chatwoot-style HTML dashboard shell with `window.chatwootConfig`, `window.globalConfig`, and the `#app` mount for browser requests, and returns Chatwoot's `406 { error: "Please use API routes instead of dashboard routes for JSON requests" }` for JSON dashboard requests. | `go test ./internal/router -run 'DashboardIndex\|RegisterRoutes' -count=1`; `go test ./cmd/route_parity -count=1`; `go run ./cmd/dump_routes > docs/parity/gochat_routes.txt`; `go run ./cmd/route_parity`; `go test ./...`; `git diff --check`. Route dump is `TOTAL: 883`; tracked route parity is `335 exact, 0 method-compatible, 9 parameter-compatible, 0 missing out of 344`. | Dashboard deep-link routes move to Review for current mailer/push evidence; continue Phase 2/3 drift audit, Phase 6 placeholder audit, or B12 live smoke. |
| `docs: land full parity tracking plan` | Documentation-only checkpoint requested before continuing implementation. It confirms the worktree was already clean at `efceb22`, makes the Hermes search and automation/macro/CSAT task families explicit in the master tracker, and records the current implementation baseline plus next executable checkpoints in one place. | `git diff --check`. No Go code changed and no route artifacts changed. | Continue Phase 2/3 drift audit, Phase 6 placeholder audit, or B12 live smoke from this tracker; `.hermes/plans/*` are source notes only. |
@@ -2349,3 +2350,4 @@ Verification milestone gates:
- 2026-06-06: Documentation checkpoint prepared as `docs: land full parity tracking plan`; worktree was clean at `efceb22 feat(help-center): expose public sitemap`, so this docs-only checkpoint lands the remaining Hermes task-family checklist directly into the master tracker. The plan now maps search, indexing, automation, macros, CSAT, enterprise paid features, and B12 reused-frontend smoke to concrete owner sections, current states, and reopen triggers. Verification for this docs-only checkpoint: `git diff --check`; continue Phase 2/3 drift audit, Phase 6 placeholder audit, or B12 live smoke.
- 2026-06-06: Dashboard app-shell checkpoint prepared as `feat(dashboard): expose app shell routes`; audited Chatwoot `DashboardController#index`, `routes.rb:19-20`, `layouts/vueapp.html.erb`, and mailer/push helper paths that point to `/app/accounts/:account_id/conversations/:display_id`. GoChat now exposes `/app` and `/app/*params`, serves a dashboard HTML shell with `window.chatwootConfig`, `window.globalConfig`, and the `#app` mount, rejects JSON dashboard requests with Chatwoot's `406` error, and tracks the two app shell routes in route parity. Focused router tests, route-parity tests, route dump/parity regeneration, full `go test ./...`, and `git diff --check` passed. Continue Phase 2/3 drift audit, Phase 6 placeholder audit, or B12 live smoke.
- 2026-06-06: `.well-known` app association checkpoint prepared as `feat(well-known): expose app association files`; audited Chatwoot `AndroidAppController#assetlinks`, `AppleAppController#site_association`, `MicrosoftController#identity_association`, their views/specs, and `routes.rb:657-659`. GoChat now exposes `/.well-known/assetlinks.json`, `/.well-known/apple-app-site-association`, and `/.well-known/microsoft-identity-association.json` with env-backed Android package/fingerprint, iOS app ID, app conversation deep-link path, and Azure application ID payloads. Focused router tests, route-parity tests, route dump/parity regeneration, full `go test ./...`, and `git diff --check` passed. Continue Phase 2/3 drift audit, Phase 6 placeholder audit, or B12 live smoke.
- 2026-06-06: Cloudflare custom-domain checkpoint prepared as `feat(custom-domains): expose cloudflare challenge`; audited Chatwoot enterprise `CustomDomainsController#verify`, `Cloudflare::BaseCloudflareZoneService`, and `routes.rb:660`. GoChat now exposes `/.well-known/cf-custom-hostname-challenge/:id`, resolves `request.host` against `portals.custom_domain`, checks `ssl_settings.cf_verification_id`, returns Chatwoot plain-text 404 bodies for unknown domains and mismatched challenges, and serves `ssl_settings.cf_verification_body` on success. Focused router tests, route-parity tests, route dump/parity regeneration, full `go test ./...`, and `git diff --check` passed. Continue Phase 2/3 drift audit, Phase 6 placeholder audit, or B12 live smoke.
+2 -1
View File
@@ -116,6 +116,7 @@ DELETE /platform/api/v1/installation_configs/:id
DELETE /platform/api/v1/users/:id
GET /.well-known/apple-app-site-association
GET /.well-known/assetlinks.json
GET /.well-known/cf-custom-hostname-challenge/:id
GET /.well-known/microsoft-identity-association.json
GET /api/v1/accounts/
GET /api/v1/accounts/:account_id
@@ -884,4 +885,4 @@ PUT /public/api/v1/csat_survey/:id
PUT /public/api/v1/inboxes/:inbox_id/contacts/:contact_id
PUT /public/api/v1/inboxes/:inbox_id/contacts/:contact_id/conversations/:conversation_id/messages/:message_id
PUT /widget/direct_uploads/:upload_uuid
TOTAL: 886
TOTAL: 887
+2 -1
View File
@@ -7,7 +7,7 @@ Generated from:
This report covers tracked frontend-critical Chatwoot routes from `reference/chatwoot/config/routes.rb`, including API v1 account routes, Captain/Copilot, assignment policies, widget/public APIs, and API v2 reports. Ruby is not installed in the workspace, so Chatwoot routes are sourced from static route declarations instead of `bin/rails routes`.
Summary: 338 exact, 0 method-compatible, 9 parameter-compatible, 0 missing out of 347 tracked critical routes.
Summary: 339 exact, 0 method-compatible, 9 parameter-compatible, 0 missing out of 348 tracked critical routes.
## Missing Critical Routes
@@ -86,6 +86,7 @@ These routes exist with equivalent method and path shape but different parameter
| DELETE | `/api/v1/widget/labels/:label_id` | `/api/v1/widget/labels/:label_id` | `api/v1/widget/labels#destroy` | `routes.rb:464` | exact |
| GET | `/.well-known/apple-app-site-association` | `/.well-known/apple-app-site-association` | `apple_app#site_association` | `routes.rb:658` | exact |
| GET | `/.well-known/assetlinks.json` | `/.well-known/assetlinks.json` | `android_app#assetlinks` | `routes.rb:657` | exact |
| GET | `/.well-known/cf-custom-hostname-challenge/:id` | `/.well-known/cf-custom-hostname-challenge/:id` | `custom_domains#verify` | `routes.rb:660` | exact |
| GET | `/.well-known/microsoft-identity-association.json` | `/.well-known/microsoft-identity-association.json` | `microsoft#identity_association` | `routes.rb:659` | exact |
| GET | `/api/v1/accounts/:account_id` | `/api/v1/accounts/:account_id` | `api/v1/accounts#show` | `routes.rb:47` | exact |
| GET | `/api/v1/accounts/:account_id/agent_bots` | `/api/v1/accounts/:account_id/agent_bots` | `api/v1/accounts/agent_bots#index` | `routes.rb:94` | exact |
+55
View File
@@ -2,7 +2,9 @@ package router
import (
"encoding/json"
"errors"
"html"
"net"
"net/http"
"os"
"strings"
@@ -21,6 +23,7 @@ import (
"github.com/gochat/gochat/internal/handler/widget"
ws "github.com/gochat/gochat/internal/handler/ws"
"github.com/gochat/gochat/internal/middleware"
"github.com/gochat/gochat/internal/model"
wspkg "github.com/gochat/gochat/internal/ws"
"gorm.io/gorm"
@@ -199,6 +202,7 @@ func RegisterRoutes(
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)
engine.GET("/.well-known/cf-custom-hostname-challenge/:id", customDomainChallenge(db))
// Dashboard shell routes used by Chatwoot mailer and push deep links.
// Reference: Chatwoot routes.rb `get '/app'`, `get '/app/*params'` -> DashboardController#index.
@@ -2074,3 +2078,54 @@ func microsoftIdentityAssociation(c *gin.Context) {
},
})
}
func customDomainChallenge(db *gorm.DB) gin.HandlerFunc {
return func(c *gin.Context) {
if db == nil {
c.String(http.StatusNotFound, "Domain not found")
return
}
var portal model.Portal
domain := requestHost(c.Request)
err := db.WithContext(c.Request.Context()).
Select("id, custom_domain, ssl_settings").
Where("custom_domain = ?", domain).
First(&portal).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
c.String(http.StatusNotFound, "Domain not found")
return
}
if err != nil {
c.String(http.StatusInternalServerError, "Internal server error")
return
}
settings := map[string]any{}
if len(portal.SSLSettings) > 0 {
_ = json.Unmarshal(portal.SSLSettings, &settings)
}
if sslSettingString(settings, "cf_verification_id") != c.Param("id") {
c.String(http.StatusNotFound, "Challenge ID not found")
return
}
c.String(http.StatusOK, sslSettingString(settings, "cf_verification_body"))
}
}
func sslSettingString(settings map[string]any, key string) string {
value, _ := settings[key].(string)
return value
}
func requestHost(req *http.Request) string {
host := strings.TrimSpace(req.Host)
if host == "" && req.URL != nil {
host = strings.TrimSpace(req.URL.Host)
}
if stripped, _, err := net.SplitHostPort(host); err == nil {
return stripped
}
return strings.Trim(host, "[]")
}
+60
View File
@@ -10,6 +10,10 @@ import (
"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) {
@@ -38,6 +42,7 @@ func TestRegisterRoutesBootsWithChatwootParityConflictGroups(t *testing.T) {
"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",
@@ -73,6 +78,39 @@ func TestRegisterRoutesBootsWithChatwootParityConflictGroups(t *testing.T) {
}
}
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")
@@ -167,3 +205,25 @@ func performGet(engine *gin.Engine, path string) *httptest.ResponseRecorder {
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
}