features_test.go
⎇
Raw
1package web
2
3import (
4 "bufio"
5 "fmt"
6 "io"
7 "mime/multipart"
8 "net"
9 "net/http"
10 "net/url"
11 "strings"
12 "sync"
13 "testing"
14
15 "forge.gpl-nas.de/physiobase/internal/config"
16)
17
18func login(t *testing.T, c *http.Client, base string) {
19 t.Helper()
20 r, err := post(c, base+"/login", url.Values{"username": {"admin"}, "password": {"geheim123"}})
21 if err != nil || r.StatusCode != http.StatusSeeOther {
22 t.Fatalf("login: %v", err)
23 }
24}
25
26func TestPrintable(t *testing.T) {
27 in := "✎ Hinweis oben\n\n## Ihre Beschwerden\n\nKnie tut weh.\n✎ nur für mich\n\n## Leer\n\n✎ nichts\n\n## Übungen\n\n- Wandsitz\n"
28 got := Printable(in)
29 if strings.Contains(got, "✎") || strings.Contains(got, "## Leer") {
30 t.Errorf("Hinweise oder leerer Abschnitt im Druck:\n%s", got)
31 }
32 if !strings.Contains(got, "## Ihre Beschwerden") || !strings.Contains(got, "- Wandsitz") {
33 t.Errorf("Inhalt fehlt:\n%s", got)
34 }
35}
36
37func TestPatientDocs(t *testing.T) {
38 ts, c := newServer(t)
39 login(t, c, ts.URL)
40 post(c, ts.URL+"/patienten", url.Values{"name": {"Doku Test"}})
41
42 upload := func(name, content string) int {
43 var buf strings.Builder
44 w := multipart.NewWriter(&buf)
45 fw, _ := w.CreateFormFile("files", name)
46 io.WriteString(fw, content)
47 w.WriteField("category", "Arztbrief / Befund")
48 w.WriteField("date", "2026-10-05")
49 w.Close()
50 req, _ := http.NewRequest(http.MethodPost, ts.URL+"/patient/1/dokumente", strings.NewReader(buf.String()))
51 req.Header.Set("Content-Type", w.FormDataContentType())
52 req.Header.Set("Sec-Fetch-Site", "same-origin")
53 r, err := c.Do(req)
54 if err != nil {
55 t.Fatal(err)
56 }
57 r.Body.Close()
58 return r.StatusCode
59 }
60 if code := upload("Arztbrief Müller.pdf", "%PDF-1.4 test"); code != http.StatusSeeOther {
61 t.Fatalf("PDF-Upload: %d", code)
62 }
63 if code := upload("boese.exe", "MZ"); code != http.StatusBadRequest {
64 t.Fatalf("exe akzeptiert: %d", code)
65 }
66 _, html := body(t, c, ts.URL+"/patient/1")
67 i := strings.Index(html, "/dokumente/1-")
68 if i < 0 || !strings.Contains(html, "Arztbrief Müller") || !strings.Contains(html, "05.10.2026") {
69 t.Fatal("Dokument nicht auf der Patientenseite")
70 }
71 link := html[i : i+strings.IndexByte(html[i:], '"')]
72 r, _ := c.Get(ts.URL + link)
73 b, _ := io.ReadAll(r.Body)
74 r.Body.Close()
75 if r.StatusCode != http.StatusOK || string(b) != "%PDF-1.4 test" || !strings.Contains(r.Header.Get("Cache-Control"), "no-store") {
76 t.Fatalf("Dokument abrufen: %d %q", r.StatusCode, r.Header.Get("Cache-Control"))
77 }
78 // Abgemeldet kein Zugriff
79 anon := &http.Client{CheckRedirect: c.CheckRedirect}
80 if r, _ := anon.Get(ts.URL + link); r.StatusCode != http.StatusSeeOther {
81 t.Fatalf("ohne Login: %d", r.StatusCode)
82 }
83 // Patient löschen entfernt auch die Datei
84 post(c, ts.URL+"/patient/1/loeschen", nil)
85 if code, _ := body(t, c, ts.URL+link); code != http.StatusNotFound {
86 t.Errorf("Dokument nach Löschen noch da: %d", code)
87 }
88}
89
90func TestSummaries(t *testing.T) {
91 ts, c := newServer(t)
92 login(t, c, ts.URL)
93 post(c, ts.URL+"/patienten", url.Values{"name": {"Sam Beispiel"}})
94 post(c, ts.URL+"/patient/1/befund", url.Values{})
95 post(c, ts.URL+"/befund/1", url.Values{
96 "title": {"Erstbefund"}, "date": {"2026-10-01"}, "f.diag": {"Patellatendinopathie"},
97 "f.pain_now": {"6"}, "f.goal_patient": {"Wieder Basketball spielen"}, "t.psfs.n": {"1"}, "t.psfs.0.activity": {"Treppe"}, "t.psfs.0.score": {"4"},
98 })
99 r, _ := post(c, ts.URL+"/patient/1/handout", url.Values{"title": {"Woche 1"}})
100 hid := strings.TrimPrefix(r.Header.Get("Location"), "/handout/")
101 post(c, ts.URL+"/handout/"+hid+"/uebung", url.Values{"entry": {"3"}}) // Wandsitz aus den Beispieldaten
102
103 r, _ = post(c, ts.URL+"/patient/1/zusammenfassung", nil)
104 loc := r.Header.Get("Location")
105 if !strings.HasPrefix(loc, "/zusammenfassung/") {
106 t.Fatalf("Gesamtzusammenfassung: %s", loc)
107 }
108 _, html := body(t, c, ts.URL+loc)
109 for _, want := range []string{"Patellatendinopathie", "Schmerz heute: 6 von 10", "Wieder Basketball spielen", "Treppe (heute 4 von 10", "Wandsitz", "Wann Sie sich melden"} {
110 if !strings.Contains(html, want) {
111 t.Errorf("Entwurf ohne %q", want)
112 }
113 }
114 _, pr := body(t, c, ts.URL+loc+"/druck")
115 if strings.Contains(pr, "✎") || !strings.Contains(pr, "Wandsitz") {
116 t.Error("Druck enthält Hinweise oder keine Übungen")
117 }
118 // Behandlungszusammenfassung
119 post(c, ts.URL+"/patient/1/verlauf", url.Values{"date": {"2026-10-08"}, "goal": {"Kniestreckung kräftigen, Hüpfen dosiert"}})
120 r, _ = post(c, ts.URL+"/verlauf/1/zusammenfassung", nil)
121 _, html = body(t, c, ts.URL+r.Header.Get("Location"))
122 if !strings.Contains(html, "Ihre Behandlung am 08.10.2026") || !strings.Contains(html, "Kniestreckung kräftigen") {
123 t.Error("Behandlungszusammenfassung unvollständig")
124 }
125 // Ohne SMTP: Senden wird verhindert, mit Begründung
126 _, html = body(t, c, ts.URL+loc)
127 if !strings.Contains(html, "nicht eingerichtet") {
128 t.Error("fehlender Hinweis auf SMTP")
129 }
130}
131
132// fakeSMTP nimmt genau eine Nachricht an (ohne TLS/Anmeldung) und gibt sie zurück.
133func fakeSMTP(t *testing.T) (string, func() string) {
134 t.Helper()
135 ln, err := net.Listen("tcp", "127.0.0.1:0")
136 if err != nil {
137 t.Fatal(err)
138 }
139 t.Cleanup(func() { ln.Close() })
140 var mu sync.Mutex
141 var got strings.Builder
142 go func() {
143 conn, err := ln.Accept()
144 if err != nil {
145 return
146 }
147 defer conn.Close()
148 rd := bufio.NewReader(conn)
149 say := func(s string) { fmt.Fprintf(conn, "%s\r\n", s) }
150 say("220 fake")
151 inData := false
152 for {
153 line, err := rd.ReadString('\n')
154 if err != nil {
155 return
156 }
157 if inData {
158 if line == ".\r\n" {
159 inData = false
160 say("250 ok")
161 continue
162 }
163 mu.Lock()
164 got.WriteString(line)
165 mu.Unlock()
166 continue
167 }
168 switch cmd := strings.ToUpper(strings.TrimSpace(line)); {
169 case strings.HasPrefix(cmd, "EHLO"), strings.HasPrefix(cmd, "HELO"):
170 say("250 fake")
171 case strings.HasPrefix(cmd, "MAIL"), strings.HasPrefix(cmd, "RCPT"):
172 mu.Lock()
173 got.WriteString(line)
174 mu.Unlock()
175 say("250 ok")
176 case cmd == "DATA":
177 inData = true
178 say("354 go")
179 case cmd == "QUIT":
180 say("221 bye")
181 return
182 default:
183 say("250 ok")
184 }
185 }
186 }()
187 return ln.Addr().String(), func() string { mu.Lock(); defer mu.Unlock(); return got.String() }
188}
189
190func TestSendSummary(t *testing.T) {
191 addr, received := fakeSMTP(t)
192 host, port, _ := net.SplitHostPort(addr)
193 var p int
194 fmt.Sscan(port, &p)
195 ts, c := newServerCfg(t, func(cfg *config.Config) {
196 cfg.SMTP = config.SMTP{Host: host, Port: p, Security: "none", From: "praxis@example.org"}
197 })
198 login(t, c, ts.URL)
199 post(c, ts.URL+"/patienten", url.Values{"name": {"Mia Mail"}, "email": {"mia@example.org"}})
200 r, _ := post(c, ts.URL+"/patient/1/zusammenfassung", nil)
201 loc := r.Header.Get("Location")
202
203 // ohne Einwilligung: kein Versand
204 r, _ = post(c, ts.URL+loc+"/senden", nil)
205 if !strings.Contains(r.Header.Get("Location"), "fehler=") {
206 t.Fatal("Versand ohne Einwilligung nicht verhindert")
207 }
208 post(c, ts.URL+"/patient/1", url.Values{"name": {"Mia Mail"}, "email": {"mia@example.org"}, "email_consent": {"1"}})
209 post(c, ts.URL+loc, url.Values{"title": {"Ihre Physiotherapie"}, "date": {"2026-11-01"}, "body": {"## Ihre Übungen\n\n- Wandsitz\n✎ geheim"}})
210 r, _ = post(c, ts.URL+loc+"/senden", nil)
211 if !strings.Contains(r.Header.Get("Location"), "gesendet=1") {
212 t.Fatalf("Versand: %s", r.Header.Get("Location"))
213 }
214 msg := received()
215 for _, want := range []string{"RCPT TO:<mia@example.org>", "Subject: Ihre Physiotherapie", "Guten Tag Mia Mail", "Wandsitz", "multipart/alternative"} {
216 if !strings.Contains(msg, want) {
217 t.Errorf("Mail ohne %q", want)
218 }
219 }
220 if strings.Contains(msg, "geheim") {
221 t.Error("✎-Hinweis wurde mitgeschickt")
222 }
223 if _, html := body(t, c, ts.URL+loc); !strings.Contains(html, "✓") {
224 t.Error("Versandprotokoll fehlt")
225 }
226 // Header-Injektion in der Adresse wird abgelehnt
227 r, _ = post(c, ts.URL+"/patient/1", url.Values{"name": {"X"}, "email": {"a@b.de\r\nBcc: evil@x.de"}})
228 if r.StatusCode != http.StatusBadRequest {
229 t.Errorf("Injektion akzeptiert: %d", r.StatusCode)
230 }
231}
232