diff --git a/cmd/route_parity/main.go b/cmd/route_parity/main.go index 45f19919..8e0110c1 100644 --- a/cmd/route_parity/main.go +++ b/cmd/route_parity/main.go @@ -25,6 +25,9 @@ type sourceDecl struct { var criticalRoutes = []route{ {Method: "GET", Path: "/app", Controller: "dashboard#index", Source: "routes.rb:19"}, {Method: "GET", Path: "/app/*params", Controller: "dashboard#index", Source: "routes.rb:20"}, + {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: "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"}, diff --git a/docs/CHATWOOT_PARITY_DEVELOPMENT_PLAN.md b/docs/CHATWOOT_PARITY_DEVELOPMENT_PLAN.md index 43a5fe9c..09bf6c6c 100644 --- a/docs/CHATWOOT_PARITY_DEVELOPMENT_PLAN.md +++ b/docs/CHATWOOT_PARITY_DEVELOPMENT_PLAN.md @@ -49,14 +49,14 @@ Hermes task landing checklist: ## Current Baseline -- Current tracking checkpoint: 2026-06-06 after this implementation checkpoint, prepared as `feat(dashboard): expose app shell routes`. -- Latest implementation checkpoint: this checkpoint, prepared as `feat(dashboard): expose app shell routes`. +- 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`. - 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 dashboard deep-link route drift is now implemented for Chatwoot `DashboardController#index`, `routes.rb:19-20`, and the mailer/push helper paths that rely on `/app/*params`. GoChat registers `/app` and `/app/*params`, serves a Chatwoot-style dashboard HTML shell for browser requests, and returns Chatwoot's `406 { error }` response for JSON dashboard requests. 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`, 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. - `go test ./...` passes. -- Route dump succeeds with `TOTAL: 883` after adding the dashboard `/app` shell routes. +- Route dump succeeds with `TOTAL: 886` after adding the `.well-known` app association routes. - Route parity artifacts now exist under `docs/parity/` and are generated by `cmd/route_parity`. -- Tracked frontend-critical route audit covers 344 Chatwoot routes: 335 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 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. - `/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 344-route critical set; dashboard `/app` shell routes from `routes.rb:19-20`, 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, 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 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. | | 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(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. | | `feat(help-center): expose public sitemap` | Advances P3.8/P6.6 public help-center parity by matching `Public::Api::V1::PortalsController#sitemap`, `sitemap.xml.erb`, and `routes.rb:591`. GoChat now registers `/hc/:slug/sitemap.xml`, resolves only non-archived portals by slug, emits sitemap XML for published portal articles only, uses custom domain URLs with Chatwoot-style `https://` prefixing when no protocol is present, and writes public article links plus ISO date `lastmod` values. | `go test ./internal/handler/api/v1 -run PortalHandler -count=1`; `go test ./internal/router -run Router -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: 881`; tracked route parity is `333 exact, 0 method-compatible, 9 parameter-compatible, 0 missing out of 342`. | P3.8/P6.6l moves to Review for current public sitemap evidence; continue Phase 2/3 drift audit, Phase 6 placeholder audit, or B12 live smoke. | @@ -2347,3 +2348,4 @@ Verification milestone gates: - 2026-06-06: P3.8/P6.6l public sitemap checkpoint prepared as `feat(help-center): expose public sitemap`; audited Chatwoot public portal sitemap action, `sitemap.xml.erb`, and route `591`. GoChat now exposes `/hc/:slug/sitemap.xml`, rejects archived portals, returns sitemap XML for published articles only, uses portal custom domains with `https://` prefixing when no protocol is present, and emits public article links plus ISO `lastmod` dates. Focused portal/router/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: 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. diff --git a/docs/parity/gochat_routes.txt b/docs/parity/gochat_routes.txt index 4850285a..3c2f9713 100644 --- a/docs/parity/gochat_routes.txt +++ b/docs/parity/gochat_routes.txt @@ -114,6 +114,9 @@ DELETE /platform/api/v1/apps/:id/permissibles/:permissible_id DELETE /platform/api/v1/banners/:id 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/microsoft-identity-association.json GET /api/v1/accounts/ GET /api/v1/accounts/:account_id GET /api/v1/accounts/:account_id/agent_bot_inboxes/ @@ -881,4 +884,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: 883 +TOTAL: 886 diff --git a/docs/parity/route_parity.md b/docs/parity/route_parity.md index aaa5a515..08eaf285 100644 --- a/docs/parity/route_parity.md +++ b/docs/parity/route_parity.md @@ -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: 335 exact, 0 method-compatible, 9 parameter-compatible, 0 missing out of 344 tracked critical routes. +Summary: 338 exact, 0 method-compatible, 9 parameter-compatible, 0 missing out of 347 tracked critical routes. ## Missing Critical Routes @@ -84,6 +84,9 @@ These routes exist with equivalent method and path shape but different parameter | DELETE | `/api/v1/accounts/:account_id/teams/:team_id/team_members/` | `/api/v1/accounts/:account_id/teams/:team_id/team_members/` | `api/v1/accounts/teams/team_members#destroy` | `routes.rb:299` | exact | | DELETE | `/api/v1/accounts/:account_id/webhooks/:webhook_id` | `/api/v1/accounts/:account_id/webhooks/:webhook_id` | `api/v1/accounts/webhooks#destroy` | `routes.rb:342` | exact | | 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/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 | | GET | `/api/v1/accounts/:account_id/agent_bots/:agent_bot_id` | `/api/v1/accounts/:account_id/agent_bots/:agent_bot_id` | `api/v1/accounts/agent_bots#show` | `routes.rb:94` | exact | diff --git a/internal/router/router.go b/internal/router/router.go index ab2f38e2..a96f1648 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -194,6 +194,12 @@ func RegisterRoutes( // Swagger UI — interactive API documentation engine.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) + // External service verification routes. + // Reference: Chatwoot routes.rb `.well-known/*` controllers. + 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) + // Dashboard shell routes used by Chatwoot mailer and push deep links. // Reference: Chatwoot routes.rb `get '/app'`, `get '/app/*params'` -> DashboardController#index. engine.GET("/app", dashboardIndex) @@ -2033,3 +2039,38 @@ func dashboardJSON(value any) string { } return string(b) } + +func androidAssetlinks(c *gin.Context) { + c.JSON(http.StatusOK, []gin.H{ + { + "relation": []string{"delegate_permission/common.handle_all_urls"}, + "target": gin.H{ + "namespace": "android_app", + "package_name": os.Getenv("ANDROID_BUNDLE_ID"), + "sha256_cert_fingerprints": []string{os.Getenv("ANDROID_SHA256_CERT_FINGERPRINT")}, + }, + }, + }) +} + +func appleAppSiteAssociation(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{ + "applinks": gin.H{ + "apps": []string{}, + "details": []gin.H{ + { + "appID": os.Getenv("IOS_APP_ID"), + "paths": []string{"/app/accounts/*/conversations/*"}, + }, + }, + }, + }) +} + +func microsoftIdentityAssociation(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{ + "associatedApplications": []gin.H{ + {"applicationId": os.Getenv("AZURE_APP_ID")}, + }, + }) +} diff --git a/internal/router/router_test.go b/internal/router/router_test.go index 814a8dc4..30f26ae0 100644 --- a/internal/router/router_test.go +++ b/internal/router/router_test.go @@ -1,6 +1,7 @@ package router import ( + "encoding/json" "net/http" "net/http/httptest" "strings" @@ -34,6 +35,9 @@ func TestRegisterRoutesBootsWithChatwootParityConflictGroups(t *testing.T) { } expected := []string{ + "GET /.well-known/assetlinks.json", + "GET /.well-known/apple-app-site-association", + "GET /.well-known/microsoft-identity-association.json", "GET /app", "GET /app/*params", "GET /api/v1/accounts/:account_id/captain/assistants/tools", @@ -69,6 +73,48 @@ func TestRegisterRoutesBootsWithChatwootParityConflictGroups(t *testing.T) { } } +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") @@ -114,3 +160,10 @@ func TestDashboardIndexRejectsJSONLikeChatwoot(t *testing.T) { 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 +}