Files
mesh-catalog/modules/mesh-mail-web/cmd/mail-page-dev/main_test.go
T
jochen d8144b704d issue 1859: run the mail page locally with invented mail (1859.5)
So the operator can click through the page on their own machine before it is delivered: cmd/mail-page-dev
serves internal/web's real handlers on 127.0.0.1 alone, over invented mail in two accounts and an in-memory
mail reading that fills every section, with an always-signed-in stand-in; nothing reaches the bus, a
database, Keycloak or a mail server. -now fixes today. The README says how to run it and what is a stand-in.
2026-10-22 20:14:24 +02:00

148 lines
5.6 KiB
Go

package main
// Issue 1859/AC8: the local run serves the real page on 127.0.0.1 alone, with invented mail and an in-memory
// mail reading — no bus, store or Keycloak; every section of the list has something in it, and a task ticked
// is under handled today, then back once undone.
import (
"io"
"net/http"
"net/url"
"os"
"path/filepath"
"reflect"
"regexp"
"strings"
"testing"
"time"
)
func fetch(t *testing.T, c *http.Client, method, u string, form url.Values) (int, string) {
t.Helper()
var resp *http.Response
var err error
if method == http.MethodPost {
resp, err = c.PostForm(u, form)
} else {
resp, err = c.Get(u)
}
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
return resp.StatusCode, string(body)
}
// section is the part of the page from one section's opening to the next's.
func section(t *testing.T, body, id string) string {
t.Helper()
i := strings.Index(body, `id="`+id+`"`)
if i < 0 {
t.Fatalf("the page has no section %s", id)
}
rest := body[i:]
if j := strings.Index(rest[1:], "<section "); j >= 0 {
return rest[:j+1]
}
return rest
}
var tokenField = regexp.MustCompile(`name="csrf_token" value="([^"]+)"`)
// 1859/AC8.
func TestTheLocalRunServesThePageWithEverySectionAndItsForms(t *testing.T) {
if _, err := start("0.0.0.0:0", time.Time{}); err == nil {
t.Fatal("the local run listened beyond 127.0.0.1")
}
thursday := time.Date(2026, 10, 22, 9, 0, 0, 0, brussels)
run, err := start("127.0.0.1:0", thursday)
if err != nil {
t.Fatal(err)
}
defer run.Close()
if !strings.HasPrefix(run.URL, "http://127.0.0.1:") {
t.Fatalf("serves at %s", run.URL)
}
c := &http.Client{Timeout: 5 * time.Second}
code, body := fetch(t, c, http.MethodGet, run.URL, nil)
if code != http.StatusOK {
t.Fatalf("/: status %d:\n%s", code, body)
}
for _, id := range []string{"greeting", "today", "this-week", "later", "you-promised", "waiting-on-others", "on-its-way",
"good-to-know", "read-later", "handled-today", "scam-warnings"} {
if s := section(t, body, id); id != "greeting" && !strings.Contains(s, `<li class="tk`) {
t.Fatalf("the section %s is empty:\n%s", id, s)
}
}
if !strings.Contains(body, "The mail signal told you of 2 mails today") || !strings.Contains(body, "digest@lists.example") {
t.Fatalf("the greeting or the nameless sender is missing:\n%s", body)
}
m := tokenField.FindStringSubmatch(body)
if m == nil {
t.Fatal("the page carries no form token")
}
token := m[1]
tick := url.Values{"id": {"1"}, "item": {"task 1"}, "mode": {"everything"}, "csrf_token": {token}}
tick.Set("act", "handle")
if code, body = fetch(t, c, http.MethodPost, run.URL+"item", tick); code != http.StatusOK {
t.Fatalf("ticking: status %d:\n%s", code, body)
}
if !strings.Contains(section(t, body, "handled-today"), "Sign the Harbor offer") || strings.Contains(section(t, body, "today"), "Sign the Harbor offer") {
t.Fatalf("the ticked task is not under handled today alone:\n%s", body)
}
tick.Set("act", "unhandle")
if code, body = fetch(t, c, http.MethodPost, run.URL+"item", tick); code != http.StatusOK {
t.Fatalf("undoing: status %d:\n%s", code, body)
}
if !strings.Contains(section(t, body, "today"), "Sign the Harbor offer") || strings.Contains(section(t, body, "handled-today"), "Sign the Harbor offer") {
t.Fatalf("the undone task is not back under today:\n%s", body)
}
// The other pages and forms the operator clicks: the pane and a correction, Work, All mail, write and send.
if code, body = fetch(t, c, http.MethodGet, run.URL+"read?id=3&mode=work", nil); code != http.StatusOK || !strings.Contains(body, "Hostco renewal") {
t.Fatalf("the reading pane: status %d:\n%s", code, body)
}
if code, body = fetch(t, c, http.MethodPost, run.URL+"correct", url.Values{"id": {"3"}, "mode": {"work"}, "not-a-task": {"2"}, "csrf_token": {token}}); code != http.StatusOK ||
strings.Contains(section(t, body, "pane"), "Forward the Hostco invoice") {
t.Fatalf("not a task: status %d:\n%s", code, body)
}
if code, body = fetch(t, c, http.MethodPost, run.URL+"mode", url.Values{"mode": {"work"}, "csrf_token": {token}}); code != http.StatusOK ||
strings.Contains(body, "Pay the Telco bill") || !strings.Contains(body, "Sign the Harbor offer") {
t.Fatalf("Work: status %d, or it shows the personal account:\n%s", code, body)
}
for _, path := range []string{"all", "folder?account=work&folder=INBOX", "search?query=Harbor", "mail?account=work&folder=INBOX&uid=101", "write", "static/app.css"} {
if code, _ := fetch(t, c, http.MethodGet, run.URL+path, nil); code != http.StatusOK {
t.Fatalf("/%s: status %d", path, code)
}
}
if code, body = fetch(t, c, http.MethodPost, run.URL+"send", url.Values{"account": {"work"}, "to": {"sara.claes@harbor.example"},
"subject": {"s"}, "body": {"b"}, "csrf_token": {token}}); code != http.StatusOK || !strings.Contains(body, "waiting") {
t.Fatalf("a send: status %d:\n%s", code, body)
}
}
// 1859/AC8: every seat the page reaches is a stand-in, so nothing falls back to the bus.
func TestTheLocalRunGivesEverySeat(t *testing.T) {
b := reflect.ValueOf(*newBundle(newWorld(time.Now(), time.Now)))
for i := 0; i < b.NumField(); i++ {
f := b.Field(i)
if f.Kind() == reflect.Func && f.IsNil() {
t.Errorf("the local run gives no %s: the page would ask the bus", b.Type().Field(i).Name)
}
}
}
// The local run is never part of the module's build: the manifest builds cmd/mesh-mail-web alone.
func TestTheManifestDoesNotBuildTheLocalRun(t *testing.T) {
raw, err := os.ReadFile(filepath.Join("..", "..", "module.json"))
if err != nil {
t.Fatal(err)
}
if strings.Contains(string(raw), "mail-page-dev") {
t.Fatal("module.json names mail-page-dev")
}
}