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") } }