package main
import (
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
"testing/fstest"
"time"
)
func newNoteServer(t *testing.T) (*Server, http.Handler) {
st := newSessionStore("")
s := &Server{noteSess: st.set("note"), npSess: st.set("np"), feedback: newFeedbackStore(filepath.Join(t.TempDir(), "feedback.json"))}
s.noteSess.add("ok-token", time.Hour)
private := fstest.MapFS{"modules-ai.html": {Data: []byte("
แผนงาน design")}, "secret.html": {Data: []byte("Secret")}, "notes.txt": {Data: []byte("x")}}
return s, s.noteMux(fstest.MapFS{"note.html": {Data: []byte("note page")}}, private)
}
func noteReq(h http.Handler, method, path, body string, login bool) *httptest.ResponseRecorder {
r := httptest.NewRequest(method, path, strings.NewReader(body))
if login {
r.AddCookie(&http.Cookie{Name: "wxch_note", Value: "ok-token"})
}
w := httptest.NewRecorder()
h.ServeHTTP(w, r)
return w
}
func TestDocsOnTheNoteHostNeedTheLogin(t *testing.T) {
_, mux := newNoteServer(t)
rec := noteReq(mux, "GET", "/doc/modules-ai", "", false)
if rec.Code != 302 || rec.Header().Get("Location") != "/?next=/doc/modules-ai" {
t.Fatalf("without the login the visitor goes to the login page: %d %q", rec.Code, rec.Header().Get("Location"))
}
if strings.Contains(rec.Body.String(), "design") {
t.Fatal("nothing of the page may leak before the login")
}
rec = noteReq(mux, "GET", "/doc/modules-ai", "", true)
if rec.Code != 200 || !strings.Contains(rec.Body.String(), "design") || rec.Header().Get("Cache-Control") != "no-store" {
t.Fatalf("%d %q", rec.Code, rec.Body.String())
}
for _, bad := range []string{"/doc/../note", "/doc/nope", "/doc/Modules-AI", "/doc/", "/doc/notes.txt"} {
if rec := noteReq(mux, "GET", bad, "", true); rec.Code == 200 {
t.Fatalf("%s must not be served", bad)
}
}
}
func TestTheOldAddressOnTheNotepadHostIsGone(t *testing.T) {
s := &Server{npSess: newSessionStore("").set("np")}
s.npSess.add("ok-token", time.Hour)
mux := s.notepadMux(fstest.MapFS{"notepad.html": {Data: []byte("np")}})
req := httptest.NewRequest("GET", "/doc/modules-ai", nil)
req.AddCookie(&http.Cookie{Name: "wxch_np", Value: "ok-token"})
rec := httptest.NewRecorder()
mux.ServeHTTP(rec, req)
if rec.Code != 404 {
t.Fatalf("the design notes moved to note.*, the notepad must not serve them: %d", rec.Code)
}
data, err := os.ReadFile("../../web/static/notepad.html")
if err != nil {
t.Fatal(err)
}
if strings.Contains(string(data), "/doc/") {
t.Error("the notepad page still links to the old document")
}
}
func TestNoteListsDocsByTitle(t *testing.T) {
_, mux := newNoteServer(t)
if rec := noteReq(mux, "GET", "/note/api/docs", "", false); rec.Code != 401 {
t.Fatalf("docs list without login: %d", rec.Code)
}
rec := noteReq(mux, "GET", "/note/api/docs", "", true)
if !strings.Contains(rec.Body.String(), `"Name":"modules-ai"`) || !strings.Contains(rec.Body.String(), "แผนงาน design") || strings.Contains(rec.Body.String(), "notes.txt") {
t.Errorf("%s", rec.Body.String())
}
}
func TestSuggestionsAreSavedListedAndDeleted(t *testing.T) {
s, mux := newNoteServer(t)
if rec := noteReq(mux, "POST", "/note/api/feedback", `{"doc":"modules-ai","topic":"next","text":"do 3 first"}`, false); rec.Code != 401 {
t.Fatalf("saving without a login: %d", rec.Code)
}
if rec := noteReq(mux, "POST", "/note/api/feedback", `{"doc":"modules-ai","topic":"next","text":" do 3 first "}`, true); rec.Code != 200 {
t.Fatalf("save: %d %s", rec.Code, rec.Body.String())
}
noteReq(mux, "POST", "/note/api/feedback", `{"doc":"../../x","topic":"","text":"other"}`, true)
if rec := noteReq(mux, "POST", "/note/api/feedback", `{"text":" "}`, true); rec.Code != 400 {
t.Errorf("an empty suggestion: %d", rec.Code)
}
if rec := noteReq(mux, "POST", "/note/api/feedback", `{"text":"`+strings.Repeat("ก", 4001)+`"}`, true); rec.Code != 400 {
t.Errorf("a suggestion over the limit: %d", rec.Code)
}
all := s.feedback.list("")
if len(all) != 2 || all[1].Text != "do 3 first" || all[1].Doc != "modules-ai" {
t.Fatalf("%+v", all)
}
if all[0].Doc != "general" || all[0].Topic != "general" {
t.Errorf("odd doc and topic names must be reduced to general: %+v", all[0])
}
if got := s.feedback.list("modules-ai"); len(got) != 1 {
t.Errorf("per document: %d", len(got))
}
// kept across a restart
again := newFeedbackStore(s.feedback.path)
if len(again.list("")) != 2 {
t.Error("suggestions were not kept in the file")
}
if rec := noteReq(mux, "DELETE", "/note/api/feedback?id="+all[1].ID, "", true); rec.Code != 200 {
t.Errorf("delete: %d", rec.Code)
}
if rec := noteReq(mux, "DELETE", "/note/api/feedback?id=nope", "", true); rec.Code != 404 {
t.Errorf("deleting what is not there: %d", rec.Code)
}
if len(s.feedback.list("")) != 1 {
t.Error("not deleted")
}
}
func TestTheDesignNoteHasWhatItPromises(t *testing.T) {
b, err := os.ReadFile("../../web/private/modules-ai.html")
if err != nil {
t.Fatal(err)
}
doc := string(b)
for _, want := range []string{"/note/api/feedback", "id=\"fbForm\"", "studio.wxch.org", "comfy.wxch.org", "110 GB", "38.5 GB", "id=\"next\""} {
if !strings.Contains(doc, want) {
t.Errorf("the design note lacks %q", want)
}
}
if strings.Contains(doc, "รออนุมัติ ยังไม่ได้เขียนโค้ด") {
t.Error("the old draft text is still there")
}
}