94 lines
3.8 KiB
Go
94 lines
3.8 KiB
Go
package handler
|
|
|
|
import (
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestNewChatPageHandlerValidatesPublicAppID(t *testing.T) {
|
|
for _, appID := range []string{"", "app/other", "<script>", " app"} {
|
|
if _, err := NewChatPageHandler(ChatPageOptions{AppID: appID}); err == nil {
|
|
t.Fatalf("NewChatPageHandler(%q) error = nil", appID)
|
|
}
|
|
}
|
|
if _, err := NewChatPageHandler(ChatPageOptions{AppID: "fire-safety-public-app"}); err != nil {
|
|
t.Fatalf("NewChatPageHandler(valid) error = %v", err)
|
|
}
|
|
}
|
|
|
|
func TestChatPageServesSafeHTMLAndAssets(t *testing.T) {
|
|
handler, err := NewChatPageHandler(ChatPageOptions{AppID: "fire-safety-public-app"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
tests := []struct {
|
|
path string
|
|
contentType string
|
|
contains []string
|
|
}{
|
|
{path: "/chat/", contentType: "text/html", contains: []string{`data-app-id="fire-safety-public-app"`, `src="/chat/app.js"`, `aria-live="polite"`, `method="post"`, `maxlength="4096"`, `<noscript>`}},
|
|
{path: "/chat/app.css", contentType: "text/css", contains: []string{"@media (max-width: 680px)", ":focus-visible"}},
|
|
{path: "/chat/app.js", contentType: "text/javascript", contains: []string{`"xtoken": authToken`, `"Accept": "text/event-stream"`, `credentials: "omit"`, "output.session_id", "paragraph.textContent = text", "committedSessionID = \"\"", "payload.error"}},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.path, func(t *testing.T) {
|
|
request := httptest.NewRequest(http.MethodGet, tt.path, nil)
|
|
response := httptest.NewRecorder()
|
|
handler.ServeHTTP(response, request)
|
|
result := response.Result()
|
|
body, readErr := io.ReadAll(result.Body)
|
|
if readErr != nil {
|
|
t.Fatal(readErr)
|
|
}
|
|
if result.StatusCode != http.StatusOK || !strings.HasPrefix(result.Header.Get("Content-Type"), tt.contentType) {
|
|
t.Fatalf("status=%d content-type=%q body=%s", result.StatusCode, result.Header.Get("Content-Type"), body)
|
|
}
|
|
for _, expected := range tt.contains {
|
|
if !strings.Contains(string(body), expected) {
|
|
t.Fatalf("body missing %q", expected)
|
|
}
|
|
}
|
|
for _, header := range []string{"Content-Security-Policy", "Cross-Origin-Resource-Policy", "Referrer-Policy", "Permissions-Policy", "X-Content-Type-Options", "X-Frame-Options"} {
|
|
if result.Header.Get(header) == "" {
|
|
t.Fatalf("security header %s is empty", header)
|
|
}
|
|
}
|
|
if csp := result.Header.Get("Content-Security-Policy"); !strings.Contains(csp, "form-action 'none'") || !strings.Contains(csp, "object-src 'none'") {
|
|
t.Fatalf("CSP does not prohibit native form/object submission: %q", csp)
|
|
}
|
|
if strings.Contains(string(body), "xtoken=") || strings.Contains(string(body), "localStorage") || strings.Contains(string(body), "sessionStorage") {
|
|
t.Fatalf("asset contains forbidden credential persistence: %s", body)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestChatPageRestrictsMethodsAndPaths(t *testing.T) {
|
|
handler, err := NewChatPageHandler(ChatPageOptions{AppID: "fire-safety-public-app"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
post := httptest.NewRecorder()
|
|
handler.ServeHTTP(post, httptest.NewRequest(http.MethodPost, "/chat/", nil))
|
|
if post.Code != http.StatusMethodNotAllowed || post.Header().Get("Allow") != "GET, HEAD" {
|
|
t.Fatalf("POST status=%d allow=%q", post.Code, post.Header().Get("Allow"))
|
|
}
|
|
|
|
missing := httptest.NewRecorder()
|
|
handler.ServeHTTP(missing, httptest.NewRequest(http.MethodGet, "/chat/unknown", nil))
|
|
if missing.Code != http.StatusNotFound {
|
|
t.Fatalf("unknown path status=%d", missing.Code)
|
|
}
|
|
|
|
head := httptest.NewRecorder()
|
|
handler.ServeHTTP(head, httptest.NewRequest(http.MethodHead, "/chat/app.js", nil))
|
|
if head.Code != http.StatusOK || head.Body.Len() != 0 || head.Header().Get("Content-Length") == "" {
|
|
t.Fatalf("HEAD status=%d len=%d content-length=%q", head.Code, head.Body.Len(), head.Header().Get("Content-Length"))
|
|
}
|
|
}
|