Extend the chat router HTML-injection bridge so tenants see a clean chat + conversation-history experience instead of the full agent cockpit. hideChatAdministration now hides the rail buttons and panels for logs, insights, memory, skills, workspaces, todos, tasks and profiles (alongside the existing Kanban), and bridgeCSS hides the same [data-panel] targets pre-JS to avoid a flash. Also hide the redundant legacy settings model dropdown (#settingsModel) via bridgeCSS while keeping the composer routing chip. Chat, history/sessions, new-chat, composer and the model chip are untouched. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
519 lines
20 KiB
Go
519 lines
20 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestSubjectsReceiveStableIsolatedSlots(t *testing.T) {
|
|
statePath := filepath.Join(t.TempDir(), "tenants.json")
|
|
router, err := newTenantRouter(statePath, 2, func(slot int) string { return "" })
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
first, err := router.slotFor("keycloak-subject-a")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
again, _ := router.slotFor("keycloak-subject-a")
|
|
second, err := router.slotFor("keycloak-subject-b")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if first != again || first == second {
|
|
t.Fatalf("unexpected slots: first=%d again=%d second=%d", first, again, second)
|
|
}
|
|
if _, err := router.slotFor("keycloak-subject-c"); err == nil {
|
|
t.Fatal("expected the fixed private tenant pool to report capacity")
|
|
}
|
|
reloaded, err := newTenantRouter(statePath, 2, func(slot int) string { return "" })
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
persisted, _ := reloaded.slotFor("keycloak-subject-a")
|
|
if persisted != first {
|
|
t.Fatalf("assignment changed after reload: %d != %d", persisted, first)
|
|
}
|
|
}
|
|
|
|
func TestRouterBlocksAdministrationAndRequiresIdentity(t *testing.T) {
|
|
router, err := newTenantRouter(filepath.Join(t.TempDir(), "state.json"), 1, func(slot int) string { return "http://127.0.0.1" })
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, test := range []struct {
|
|
method string
|
|
path string
|
|
user string
|
|
want int
|
|
}{
|
|
{http.MethodGet, "/", "", http.StatusUnauthorized},
|
|
{http.MethodPost, "/api/providers", "subject", http.StatusForbidden},
|
|
{http.MethodPost, "/api/commands/exec", "subject", http.StatusForbidden},
|
|
{http.MethodGet, "/api/dashboard/config", "subject", http.StatusForbidden},
|
|
{http.MethodGet, "/api/env", "subject", http.StatusForbidden},
|
|
} {
|
|
request := httptest.NewRequest(test.method, test.path, nil)
|
|
request.Header.Set("X-Forwarded-User", test.user)
|
|
response := httptest.NewRecorder()
|
|
router.ServeHTTP(response, request)
|
|
if response.Code != test.want {
|
|
t.Fatalf("%s %s: got %d, want %d", test.method, test.path, response.Code, test.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRouterRetiresStaleServiceWorkerWithoutIdentity(t *testing.T) {
|
|
router, err := newTenantRouter(filepath.Join(t.TempDir(), "state.json"), 1, func(slot int) string { return "" })
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
request := httptest.NewRequest(http.MethodGet, "/sw.js?v=exp-v0.52.181", nil)
|
|
response := httptest.NewRecorder()
|
|
router.ServeHTTP(response, request)
|
|
if response.Code != http.StatusOK {
|
|
t.Fatalf("got status %d", response.Code)
|
|
}
|
|
if response.Header().Get("Service-Worker-Allowed") != "/" {
|
|
t.Fatal("service worker retirement response does not cover the origin")
|
|
}
|
|
if !strings.Contains(response.Header().Get("Cache-Control"), "no-store") {
|
|
t.Fatal("service worker retirement response is cacheable")
|
|
}
|
|
if !strings.Contains(response.Header().Get("Clear-Site-Data"), "storage") {
|
|
t.Fatal("service worker retirement response does not clear stale storage")
|
|
}
|
|
if !strings.Contains(response.Body.String(), "registration.unregister") {
|
|
t.Fatal("service worker retirement script does not unregister itself")
|
|
}
|
|
}
|
|
|
|
func TestRouterProxiesWebUIAndAddsTelegramShortcut(t *testing.T) {
|
|
backend := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
|
if request.Header.Get("X-Forwarded-User") != "" || request.Header.Get("X-Auth-Request-User") != "" {
|
|
t.Fatal("external identity header leaked to tenant backend")
|
|
}
|
|
if request.Header.Get(trustedTenantHeader) != "slot-0" {
|
|
t.Fatalf("trusted tenant identity was not asserted: %q", request.Header.Get(trustedTenantHeader))
|
|
}
|
|
cookies := request.Cookies()
|
|
if len(cookies) != 2 || cookies[0].Name != tenantSessionCookie || cookies[1].Name != tenantProfileCookie {
|
|
t.Fatalf("unexpected backend cookies: %#v", cookies)
|
|
}
|
|
writer.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
_, _ = io.WriteString(writer, "<html><head></head><body>Hermes WebUI</body></html>")
|
|
}))
|
|
defer backend.Close()
|
|
router, err := newTenantRouter(filepath.Join(t.TempDir(), "state.json"), 1, func(slot int) string { return backend.URL })
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
request := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
request.Header.Set("X-Forwarded-User", "subject")
|
|
request.Header.Set("X-Auth-Request-User", "subject")
|
|
request.Header.Set("Cookie", "_oauth2_proxy=secret; "+tenantSessionCookie+"=session; "+tenantProfileCookie+"=default")
|
|
response := httptest.NewRecorder()
|
|
router.ServeHTTP(response, request)
|
|
if response.Code != http.StatusOK {
|
|
t.Fatalf("got status %d", response.Code)
|
|
}
|
|
if !strings.Contains(response.Body.String(), "hermes-chat-bridge.js") || !strings.Contains(response.Body.String(), "hermes-chat-bridge.css") {
|
|
t.Fatal("Telegram shortcut assets were not injected")
|
|
}
|
|
if !strings.Contains(response.Body.String(), "hermes-session-continuity.js") {
|
|
t.Fatal("session continuity fallback was not injected")
|
|
}
|
|
assetRequest := httptest.NewRequest(http.MethodGet, "/hermes-chat-bridge.js", nil)
|
|
assetRequest.Header.Set("X-Forwarded-User", "subject")
|
|
assetResponse := httptest.NewRecorder()
|
|
router.ServeHTTP(assetResponse, assetRequest)
|
|
asset := assetResponse.Body.String()
|
|
if !strings.Contains(asset, "hermes-files-sidebar") || !strings.Contains(asset, "hermes-telegram-sidebar") {
|
|
t.Fatal("Files and Telegram were not integrated into the existing sidebar")
|
|
}
|
|
if !strings.Contains(asset, "document.querySelector('.rail')") || !strings.Contains(asset, "data-tooltip', 'Telegram") {
|
|
t.Fatal("Telegram was not integrated as a native WebUI navigation action")
|
|
}
|
|
if strings.Contains(asset, "if (!files) return") {
|
|
t.Fatal("Telegram navigation still depends on the removed legacy Files link")
|
|
}
|
|
if strings.Contains(asset, "position:fixed") || strings.Contains(asset, "hermes-chat-tools") {
|
|
t.Fatal("legacy floating chat controls remain in the mobile bridge")
|
|
}
|
|
}
|
|
|
|
func TestSessionContinuityAssetHasAccessiblePollFallback(t *testing.T) {
|
|
router, err := newTenantRouter(filepath.Join(t.TempDir(), "state.json"), 1, func(int) string { return "" })
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
request := httptest.NewRequest(http.MethodGet, "/hermes-session-continuity.js", nil)
|
|
request.Header.Set("X-Forwarded-User", "subject")
|
|
response := httptest.NewRecorder()
|
|
router.ServeHTTP(response, request)
|
|
if response.Code != http.StatusOK {
|
|
t.Fatalf("got status %d", response.Code)
|
|
}
|
|
asset := response.Body.String()
|
|
for _, expected := range []string{
|
|
"aria-live", "aria-busy", "/api/sessions/", "/messages?limit=24&hermes_fallback=1",
|
|
"Latest stored activity", "no renderable messages",
|
|
"Session updates disconnected", "oauth2/start?rd=", "payload.session_id",
|
|
"session.ended_at == null", "latest.observed", "addEventListener('online'",
|
|
"addEventListener('pageshow'", "schedule(document.hidden", "fetch(",
|
|
} {
|
|
if !strings.Contains(asset, expected) {
|
|
t.Fatalf("session fallback omitted %q", expected)
|
|
}
|
|
}
|
|
for _, forbidden := range []string{
|
|
"location.replace", "location.reload", "history.replaceState", "/api/session?", "WebSocket",
|
|
} {
|
|
if strings.Contains(asset, forbidden) {
|
|
t.Fatalf("session fallback interferes with native continuity via %q", forbidden)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSessionContinuityPollIsBoundedByRouter(t *testing.T) {
|
|
backend := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, _ *http.Request) {
|
|
messages := make([]map[string]int, 30)
|
|
for index := range messages {
|
|
messages[index] = map[string]int{"index": index}
|
|
}
|
|
writer.Header().Set("Content-Type", "application/json")
|
|
_ = json.NewEncoder(writer).Encode(map[string]any{
|
|
"session_id": "resolved", "messages": messages,
|
|
})
|
|
}))
|
|
defer backend.Close()
|
|
router, err := newTenantRouter(filepath.Join(t.TempDir(), "state.json"), 1, func(int) string { return backend.URL })
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
request := httptest.NewRequest(
|
|
http.MethodGet,
|
|
"/api/sessions/root/messages?limit=24&hermes_fallback=1",
|
|
nil,
|
|
)
|
|
request.Header.Set("X-Forwarded-User", "subject")
|
|
response := httptest.NewRecorder()
|
|
router.ServeHTTP(response, request)
|
|
if response.Code != http.StatusOK {
|
|
t.Fatalf("got status %d", response.Code)
|
|
}
|
|
var payload sessionSnapshot
|
|
if err := json.NewDecoder(response.Body).Decode(&payload); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(payload.Messages) != sessionSnapshotItems || payload.TotalMessages != 30 {
|
|
t.Fatalf("router returned unbounded fallback: %#v", payload)
|
|
}
|
|
}
|
|
|
|
func TestTelegramPageExplainsOneTimeOperatorSetup(t *testing.T) {
|
|
router, err := newTenantRouter(filepath.Join(t.TempDir(), "state.json"), 1, func(slot int) string { return "" })
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
request := httptest.NewRequest(http.MethodGet, "/telegram", nil)
|
|
request.Header.Set("X-Forwarded-User", "subject")
|
|
response := httptest.NewRecorder()
|
|
router.ServeHTTP(response, request)
|
|
if response.Code != http.StatusOK {
|
|
t.Fatalf("got status %d", response.Code)
|
|
}
|
|
body := response.Body.String()
|
|
for _, expected := range []string{
|
|
"https://t.me/BotFather",
|
|
"https://secret.bstein.dev",
|
|
"vault login -method=oidc",
|
|
"vault kv patch -mount=kv atlas/hermes/chat-telegram",
|
|
"bot_token",
|
|
"relay_key",
|
|
"each Keycloak user",
|
|
} {
|
|
if !strings.Contains(body, expected) {
|
|
t.Fatalf("Telegram operator setup omitted %q", expected)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRouterRedirectsNativeLoginToSafeChatDestination(t *testing.T) {
|
|
router, err := newTenantRouter(filepath.Join(t.TempDir(), "state.json"), 1, func(slot int) string { return "" })
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, test := range []struct {
|
|
path string
|
|
want string
|
|
}{
|
|
{"/login?next=%2Fsession%2F9048e2a574d1", "/session/9048e2a574d1"},
|
|
{"/login?next=%2Fchat%3Fresume%3Dabc", "/chat?resume=abc"},
|
|
{"/login?next=https%3A%2F%2Fevil.example", "/"},
|
|
{"/login?next=%2F%2Fevil.example", "/"},
|
|
{"/login?next=%2Flogin", "/"},
|
|
{"/login?next=%2Flogin%2Fagain", "/"},
|
|
} {
|
|
request := httptest.NewRequest(http.MethodGet, test.path, nil)
|
|
request.Header.Set("X-Forwarded-User", "subject")
|
|
response := httptest.NewRecorder()
|
|
router.ServeHTTP(response, request)
|
|
if response.Code != http.StatusFound {
|
|
t.Fatalf("%s: got status %d", test.path, response.Code)
|
|
}
|
|
if location := response.Header().Get("Location"); location != test.want {
|
|
t.Fatalf("%s: got redirect %q, want %q", test.path, location, test.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRouterAllowsTenantScopedPersonalization(t *testing.T) {
|
|
backend := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
|
writer.WriteHeader(http.StatusNoContent)
|
|
}))
|
|
defer backend.Close()
|
|
router, err := newTenantRouter(filepath.Join(t.TempDir(), "state.json"), 1, func(slot int) string { return backend.URL })
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, path := range []string{
|
|
"/api/memory",
|
|
"/api/memory/write",
|
|
"/api/skills",
|
|
"/api/skills/save",
|
|
"/api/profiles",
|
|
"/api/profile/create",
|
|
} {
|
|
request := httptest.NewRequest(http.MethodGet, path, nil)
|
|
request.Header.Set("X-Forwarded-User", "subject")
|
|
response := httptest.NewRecorder()
|
|
router.ServeHTTP(response, request)
|
|
if response.Code != http.StatusNoContent {
|
|
t.Fatalf("GET %s: got %d, want %d", path, response.Code, http.StatusNoContent)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRouterAllowsPrivateWorkspaceReadsButBlocksRegistration(t *testing.T) {
|
|
backend := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
|
writer.WriteHeader(http.StatusNoContent)
|
|
}))
|
|
defer backend.Close()
|
|
router, err := newTenantRouter(filepath.Join(t.TempDir(), "state.json"), 1, func(slot int) string { return backend.URL })
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
for _, path := range []string{
|
|
"/api/workspaces",
|
|
"/api/workspaces/suggest",
|
|
"/api/file",
|
|
"/api/file/raw",
|
|
"/api/folder/download",
|
|
"/api/logs",
|
|
"/api/rollback/list",
|
|
"/api/rollback/diff",
|
|
} {
|
|
request := httptest.NewRequest(http.MethodGet, path, nil)
|
|
request.Header.Set("X-Forwarded-User", "subject")
|
|
response := httptest.NewRecorder()
|
|
router.ServeHTTP(response, request)
|
|
if response.Code != http.StatusNoContent {
|
|
t.Fatalf("GET %s: got %d, want %d", path, response.Code, http.StatusNoContent)
|
|
}
|
|
}
|
|
|
|
request := httptest.NewRequest(http.MethodPost, "/api/workspaces/add", strings.NewReader(`{"path":"/opt/data"}`))
|
|
request.Header.Set("X-Forwarded-User", "subject")
|
|
response := httptest.NewRecorder()
|
|
router.ServeHTTP(response, request)
|
|
if response.Code != http.StatusForbidden {
|
|
t.Fatalf("workspace registration got %d, want %d", response.Code, http.StatusForbidden)
|
|
}
|
|
}
|
|
|
|
func TestPrivateFileBrowserIsTenantAuthenticatedAndHidesKanban(t *testing.T) {
|
|
router, err := newTenantRouter(filepath.Join(t.TempDir(), "state.json"), 1, func(slot int) string { return "" })
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
unauthenticated := httptest.NewRequest(http.MethodGet, "/private-files", nil)
|
|
unauthenticatedResponse := httptest.NewRecorder()
|
|
router.ServeHTTP(unauthenticatedResponse, unauthenticated)
|
|
if unauthenticatedResponse.Code != http.StatusUnauthorized {
|
|
t.Fatalf("unauthenticated browser got %d", unauthenticatedResponse.Code)
|
|
}
|
|
|
|
request := httptest.NewRequest(http.MethodGet, "/private-files?session_id=session-1", nil)
|
|
request.Header.Set("X-Forwarded-User", "subject")
|
|
response := httptest.NewRecorder()
|
|
router.ServeHTTP(response, request)
|
|
if response.Code != http.StatusOK {
|
|
t.Fatalf("got %d", response.Code)
|
|
}
|
|
if !strings.Contains(response.Body.String(), "Private files") || !strings.Contains(response.Body.String(), "data-files-page") {
|
|
t.Fatal("private browser page was not rendered")
|
|
}
|
|
|
|
assetRequest := httptest.NewRequest(http.MethodGet, "/hermes-chat-bridge.js", nil)
|
|
assetRequest.Header.Set("X-Forwarded-User", "subject")
|
|
assetResponse := httptest.NewRecorder()
|
|
router.ServeHTTP(assetResponse, assetRequest)
|
|
if !strings.Contains(assetResponse.Body.String(), "endpoint('list'") || !strings.Contains(assetResponse.Body.String(), `'[data-panel="' + panel + '"]'`) {
|
|
t.Fatal("file browsing or chat-only navigation policy is missing")
|
|
}
|
|
}
|
|
|
|
func TestChatBridgeHidesOnlyNonChatCockpitChrome(t *testing.T) {
|
|
router, err := newTenantRouter(filepath.Join(t.TempDir(), "state.json"), 1, func(slot int) string { return "" })
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
asset := func(path string) string {
|
|
request := httptest.NewRequest(http.MethodGet, path, nil)
|
|
request.Header.Set("X-Forwarded-User", "subject")
|
|
response := httptest.NewRecorder()
|
|
router.ServeHTTP(response, request)
|
|
if response.Code != http.StatusOK {
|
|
t.Fatalf("%s got status %d", path, response.Code)
|
|
}
|
|
return response.Body.String()
|
|
}
|
|
js := asset("/hermes-chat-bridge.js")
|
|
css := asset("/hermes-chat-bridge.css")
|
|
|
|
// The whole cockpit (Kanban plus every non-chat rail panel) is declutter-hidden
|
|
// both pre-JS (CSS, no flash) and by the runtime .hidden reinforcement.
|
|
for _, panel := range []string{"kanban", "logs", "insights", "memory", "skills", "workspaces", "todos", "tasks", "profiles"} {
|
|
if !strings.Contains(css, `[data-panel="`+panel+`"]`) {
|
|
t.Fatalf("bridge CSS does not hide cockpit panel %q", panel)
|
|
}
|
|
if !strings.Contains(js, `'`+panel+`'`) {
|
|
t.Fatalf("bridge JS does not mark cockpit panel %q hidden", panel)
|
|
}
|
|
}
|
|
if !strings.Contains(css, "display:none") || !strings.Contains(css, "#settingsModel") {
|
|
t.Fatal("bridge CSS must hide the cockpit panels and the redundant settings model selector")
|
|
}
|
|
|
|
// The clean chat experience (chat surface, history, composer, model chip)
|
|
// must never be part of the hide set.
|
|
for _, keep := range []string{`[data-panel="chat"]`, `[data-panel="history"]`, `[data-panel="sessions"]`, `[data-panel="composer"]`, "composerRoutingChip", "composerRoutingLabel"} {
|
|
if strings.Contains(css, keep) || strings.Contains(js, keep) {
|
|
t.Fatalf("declutter wrongly targets a core chat control: %q", keep)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestWebUIModelAndReasoningOverridesAreProxied(t *testing.T) {
|
|
backend := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
|
if request.Method != http.MethodPost {
|
|
t.Fatalf("got method %s", request.Method)
|
|
}
|
|
writer.WriteHeader(http.StatusNoContent)
|
|
}))
|
|
defer backend.Close()
|
|
router, err := newTenantRouter(filepath.Join(t.TempDir(), "state.json"), 1, func(slot int) string { return backend.URL })
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, path := range []string{"/api/model/set", "/api/reasoning"} {
|
|
request := httptest.NewRequest(http.MethodPost, path, strings.NewReader(`{}`))
|
|
request.Header.Set("X-Forwarded-User", "subject")
|
|
response := httptest.NewRecorder()
|
|
router.ServeHTTP(response, request)
|
|
if response.Code != http.StatusNoContent {
|
|
t.Fatalf("POST %s: got %d", path, response.Code)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTelegramLinkUsesHashedStateAndExpires(t *testing.T) {
|
|
statePath := filepath.Join(t.TempDir(), "state.json")
|
|
router, err := newTenantRouter(statePath, 1, func(slot int) string { return "" })
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
now := time.Date(2026, 8, 8, 12, 0, 0, 0, time.UTC)
|
|
router.now = func() time.Time { return now }
|
|
code, expires, err := router.createLink("raw-keycloak-subject")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if expires.Sub(now) != 10*time.Minute {
|
|
t.Fatalf("unexpected expiry: %s", expires)
|
|
}
|
|
if _, err := router.consumeLink("123456789", code); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, linked := router.telegramSlot("123456789"); !linked {
|
|
t.Fatal("Telegram identity was not linked")
|
|
}
|
|
content, err := os.ReadFile(statePath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if strings.Contains(string(content), "raw-keycloak-subject") || strings.Contains(string(content), "123456789") || strings.Contains(string(content), code) {
|
|
t.Fatal("raw identity or one-time code was persisted")
|
|
}
|
|
secondCode, _, err := router.createLink("raw-keycloak-subject")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
router.now = func() time.Time { return now.Add(11 * time.Minute) }
|
|
if _, err := router.consumeLink("987654321", secondCode); err == nil {
|
|
t.Fatal("expected expired link code to be rejected")
|
|
}
|
|
}
|
|
|
|
func TestTelegramWebActionsRequireExplicitSameOriginHeader(t *testing.T) {
|
|
router, err := newTenantRouter(filepath.Join(t.TempDir(), "state.json"), 1, func(slot int) string { return "" })
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
router.telegram = &telegramBot{}
|
|
request := httptest.NewRequest(http.MethodPost, "/api/telegram/link", strings.NewReader(`{}`))
|
|
request.Header.Set("X-Forwarded-User", "subject")
|
|
request.Header.Set("Content-Type", "application/json")
|
|
response := httptest.NewRecorder()
|
|
router.ServeHTTP(response, request)
|
|
if response.Code != http.StatusForbidden {
|
|
t.Fatalf("got %d", response.Code)
|
|
}
|
|
router.telegram.setStatus("BsteinAtlasHermesBot", true, "")
|
|
request = httptest.NewRequest(http.MethodPost, "/api/telegram/link", strings.NewReader(`{}`))
|
|
request.Header.Set("X-Forwarded-User", "subject")
|
|
request.Header.Set("Content-Type", "application/json")
|
|
request.Header.Set("X-Hermes-Action", "telegram-link")
|
|
response = httptest.NewRecorder()
|
|
router.ServeHTTP(response, request)
|
|
if response.Code != http.StatusOK {
|
|
t.Fatalf("got %d: %s", response.Code, response.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestTelegramWebRejectsLinkWhileConfiguredBotIsNotReady(t *testing.T) {
|
|
router, err := newTenantRouter(filepath.Join(t.TempDir(), "state.json"), 1, func(slot int) string { return "" })
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
router.telegram = &telegramBot{}
|
|
request := httptest.NewRequest(http.MethodPost, "/api/telegram/link", strings.NewReader(`{}`))
|
|
request.Header.Set("X-Forwarded-User", "subject")
|
|
request.Header.Set("Content-Type", "application/json")
|
|
request.Header.Set("X-Hermes-Action", "telegram-link")
|
|
response := httptest.NewRecorder()
|
|
router.ServeHTTP(response, request)
|
|
if response.Code != http.StatusServiceUnavailable {
|
|
t.Fatalf("got %d: %s", response.Code, response.Body.String())
|
|
}
|
|
}
|