212 lines
5.8 KiB
Go
212 lines
5.8 KiB
Go
package watch
|
|
|
|
import (
|
|
"bufio"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func newTestServer(t *testing.T, dir string) (*Server, *httptest.Server) {
|
|
t.Helper()
|
|
srv, err := NewServer(dir)
|
|
if err != nil {
|
|
t.Fatalf("NewServer() error = %v", err)
|
|
}
|
|
t.Cleanup(func() { srv.Close() })
|
|
|
|
handler, err := srv.Handler()
|
|
if err != nil {
|
|
t.Fatalf("Handler() error = %v", err)
|
|
}
|
|
httpSrv := httptest.NewServer(handler)
|
|
t.Cleanup(httpSrv.Close)
|
|
|
|
return srv, httpSrv
|
|
}
|
|
|
|
func TestHandler(t *testing.T) {
|
|
dir := t.TempDir()
|
|
if err := os.WriteFile(filepath.Join(dir, "010-intro.md"), []byte("# Hallo\n"), 0o644); err != nil {
|
|
t.Fatalf("WriteFile: %v", err)
|
|
}
|
|
|
|
_, httpSrv := newTestServer(t, dir)
|
|
|
|
tests := []struct {
|
|
name string
|
|
path string
|
|
wantStatus int
|
|
wantContains string
|
|
}{
|
|
{name: "index renders slides", path: "/", wantStatus: http.StatusOK, wantContains: "<h1>Hallo</h1>"},
|
|
{name: "index includes live-reload script", path: "/", wantStatus: http.StatusOK, wantContains: `<script src="vendor/live-reload.js">`},
|
|
{name: "style.css served", path: "/vendor/style.css", wantStatus: http.StatusOK, wantContains: ":root"},
|
|
{name: "chroma.css served", path: "/vendor/chroma.css", wantStatus: http.StatusOK, wantContains: ".chroma"},
|
|
{name: "nav.js served", path: "/vendor/nav.js", wantStatus: http.StatusOK, wantContains: "mermaid.run"},
|
|
{name: "live-reload.js served", path: "/vendor/live-reload.js", wantStatus: http.StatusOK, wantContains: "EventSource"},
|
|
{name: "vendor mermaid served", path: "/vendor/mermaid.min.js", wantStatus: http.StatusOK},
|
|
{name: "unknown path 404s", path: "/does-not-exist", wantStatus: http.StatusNotFound},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
resp, err := http.Get(httpSrv.URL + tt.path)
|
|
if err != nil {
|
|
t.Fatalf("GET %s: %v", tt.path, err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != tt.wantStatus {
|
|
t.Errorf("status = %d, want %d", resp.StatusCode, tt.wantStatus)
|
|
}
|
|
if tt.wantContains != "" {
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
t.Fatalf("reading body: %v", err)
|
|
}
|
|
if !strings.Contains(string(body), tt.wantContains) {
|
|
t.Errorf("body does not contain %q: %s", tt.wantContains, body)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestHandler_ServesUserAssets(t *testing.T) {
|
|
dir := t.TempDir()
|
|
if err := os.WriteFile(filepath.Join(dir, "010-intro.md"), []byte("# Hallo\n"), 0o644); err != nil {
|
|
t.Fatalf("WriteFile: %v", err)
|
|
}
|
|
if err := os.MkdirAll(filepath.Join(dir, "img"), 0o755); err != nil {
|
|
t.Fatalf("MkdirAll: %v", err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(dir, "img", "foto.png"), []byte("fake-png-bytes"), 0o644); err != nil {
|
|
t.Fatalf("WriteFile: %v", err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(dir, "notes.md"), []byte("geheim"), 0o644); err != nil {
|
|
t.Fatalf("WriteFile: %v", err)
|
|
}
|
|
|
|
_, httpSrv := newTestServer(t, dir)
|
|
|
|
tests := []struct {
|
|
name string
|
|
path string
|
|
wantStatus int
|
|
}{
|
|
{name: "user image served", path: "/img/foto.png", wantStatus: http.StatusOK},
|
|
{name: "markdown file not served", path: "/notes.md", wantStatus: http.StatusNotFound},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
resp, err := http.Get(httpSrv.URL + tt.path)
|
|
if err != nil {
|
|
t.Fatalf("GET %s: %v", tt.path, err)
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != tt.wantStatus {
|
|
t.Errorf("status = %d, want %d", resp.StatusCode, tt.wantStatus)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestServer_ReloadsOnFileChange(t *testing.T) {
|
|
dir := t.TempDir()
|
|
slidePath := filepath.Join(dir, "010-intro.md")
|
|
if err := os.WriteFile(slidePath, []byte("# First\n"), 0o644); err != nil {
|
|
t.Fatalf("WriteFile: %v", err)
|
|
}
|
|
|
|
_, httpSrv := newTestServer(t, dir)
|
|
|
|
get := func() string {
|
|
resp, err := http.Get(httpSrv.URL + "/")
|
|
if err != nil {
|
|
t.Fatalf("GET /: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
t.Fatalf("reading body: %v", err)
|
|
}
|
|
return string(body)
|
|
}
|
|
|
|
if !strings.Contains(get(), "<h1>First</h1>") {
|
|
t.Fatal("expected first version of slide content")
|
|
}
|
|
|
|
if err := os.WriteFile(slidePath, []byte("# Second\n"), 0o644); err != nil {
|
|
t.Fatalf("WriteFile: %v", err)
|
|
}
|
|
|
|
deadline := time.Now().Add(5 * time.Second)
|
|
for {
|
|
if strings.Contains(get(), "<h1>Second</h1>") {
|
|
break
|
|
}
|
|
if time.Now().After(deadline) {
|
|
t.Fatal("timed out waiting for in-memory page to reflect file change")
|
|
}
|
|
time.Sleep(20 * time.Millisecond)
|
|
}
|
|
}
|
|
|
|
func TestServer_NotifiesSSEClientsOnChange(t *testing.T) {
|
|
dir := t.TempDir()
|
|
slidePath := filepath.Join(dir, "010-intro.md")
|
|
if err := os.WriteFile(slidePath, []byte("# First\n"), 0o644); err != nil {
|
|
t.Fatalf("WriteFile: %v", err)
|
|
}
|
|
|
|
_, httpSrv := newTestServer(t, dir)
|
|
|
|
req, err := http.NewRequest(http.MethodGet, httpSrv.URL+"/events", nil)
|
|
if err != nil {
|
|
t.Fatalf("NewRequest: %v", err)
|
|
}
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
t.Fatalf("GET /events: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if ct := resp.Header.Get("Content-Type"); ct != "text/event-stream" {
|
|
t.Fatalf("Content-Type = %q, want text/event-stream", ct)
|
|
}
|
|
|
|
if err := os.WriteFile(slidePath, []byte("# Second\n"), 0o644); err != nil {
|
|
t.Fatalf("WriteFile: %v", err)
|
|
}
|
|
|
|
type result struct {
|
|
line string
|
|
err error
|
|
}
|
|
lineCh := make(chan result, 1)
|
|
go func() {
|
|
reader := bufio.NewReader(resp.Body)
|
|
line, err := reader.ReadString('\n')
|
|
lineCh <- result{line, err}
|
|
}()
|
|
|
|
select {
|
|
case res := <-lineCh:
|
|
if res.err != nil {
|
|
t.Fatalf("reading SSE stream: %v", res.err)
|
|
}
|
|
if !strings.Contains(res.line, "reload") {
|
|
t.Errorf("SSE event = %q, want it to mention reload", res.line)
|
|
}
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatal("timed out waiting for SSE change notification")
|
|
}
|
|
}
|