package web
import (
"encoding/json"
"fmt"
"io/fs"
"net/http"
"net/http/httptest"
"net/url"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"testing"
"time"
"github.com/google/uuid"
"github.com/gramanas/blogspace/internal/config"
"github.com/gramanas/blogspace/internal/i18n"
"github.com/gramanas/blogspace/internal/store"
)
func TestHostname(t *testing.T) {
cases := map[string]string{"Example.com:8080": "example.com", "a.example.com": "a.example.com", "example.com.": "example.com", "[::1]:80": "::1"}
for in, want := range cases {
if got := hostname(in); got != want {
t.Errorf("hostname(%q) = %q, want %q", in, got, want)
}
}
}
// Host routing that does not need the database: unknown hosts and reserved/nested subdomains 404 before any lookup.
func TestHostRoutingWithoutDB(t *testing.T) {
cfg := &config.Config{BaseDomain: "example.com", JWTSecret: []byte("x"), MaxUploadBytes: 1 << 20}
s := NewServer(cfg, nil)
for _, host := range []string{"evil.com", "a.b.example.com", "www.blog.example.com", "static.example.com", "notexample.com"} {
rec := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/", nil)
req.Host = host
s.ServeHTTP(rec, req)
if rec.Code != http.StatusNotFound {
t.Errorf("host %q: got %d, want 404", host, rec.Code)
}
}
// root domain (and www) reach the management mux: /webadmin renders without DB access
for _, host := range []string{"example.com", "www.example.com", "example.com:8080"} {
rec := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/webadmin", nil)
req.Host = host
s.ServeHTTP(rec, req)
if rec.Code != http.StatusOK || !strings.Contains(rec.Body.String(), "Log in") {
t.Errorf("host %q /webadmin: got %d", host, rec.Code)
}
}
}
// On the root domain the literal management routes must win over the blog's /{page} wildcards.
// "blog_" + subdomain must fit a 63-char Postgres database name.
func TestSubdomainLength(t *testing.T) {
if !subdomainRe.MatchString(strings.Repeat("a", 58)) || subdomainRe.MatchString(strings.Repeat("a", 59)) {
t.Error("subdomains must be at most 58 chars")
}
}
func TestRootRoutePrecedence(t *testing.T) {
cfg := &config.Config{BaseDomain: "example.com", JWTSecret: []byte("x"), MaxUploadBytes: 1 << 20}
s := NewServer(cfg, nil)
for path, want := range map[string]int{"/webadmin": 200, "/healthz": 200, "/dashboard": 303, "/admin/": 303, "/b/alice/posts": 303, "/b/alice/announcements": 303, "/b/alice/layout": 303, "/b/alice/media/6ba7b810-9dad-11d1-80b4-00c04fd430c8": 303, "/b/alice/files": 303, "/account/password": 303} {
rec := httptest.NewRecorder()
req := httptest.NewRequest("GET", path, nil)
req.Host = "example.com"
s.ServeHTTP(rec, req)
if rec.Code != want {
t.Errorf("%s: got %d, want %d", path, rec.Code, want)
}
}
for _, slug := range []string{"webadmin", "admin", "b", "media", "feed.xml", "favicon.ico", "tag", "search"} {
if !reservedPageSlugs[slug] {
t.Errorf("page slug %q should be reserved", slug)
}
}
}
func TestThemeNormalizeAndCSS(t *testing.T) {
th := ParseTheme([]byte(`{"bg_color":"red;}body{display:none","font":"comic","bg_image":"../etc","logo":"../etc","link_hover":"blue","heading_font":"wingdings","favicon":"x","logo_size":"huge","left_width":5,"right_width":90,"header_rule":"neon\">"
if got := renderBody(store.FormatHTML, raw); got != raw {
t.Errorf("html must pass through untouched: %q", got)
}
if got := renderBody(store.FormatMarkdown, raw); !strings.Contains(got, "x") || strings.Contains(got, ""))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
rec := httptest.NewRecorder()
s.handlePreview(rec, req)
if ct := rec.Header().Get("Content-Type"); ct != "application/json" {
t.Errorf("content type %q", ct)
}
var out map[string]string
if err := json.Unmarshal(rec.Body.Bytes(), &out); err != nil {
t.Fatal(err)
}
if h := out["html"]; !strings.Contains(h, "x") || strings.Contains(h, "