Phase 6b/7b: dashboard codes editor, decode events, human-readable briefs (AUD-115)
- New /codes dashboard page (behind the session login, linked in the top bar):
lists every mapping by kind with Add, inline Edit+Save and Delete. Sam can put
real teacher surnames in without a rebuild (FR-17).
- Decode timetable EVENTS through the same map as periods, so an ensemble reads
'JAZZ ENSEMBLE - Ensemble Room - Mr KWO' instead of raw codes.
- teacherName splits comma- or space-separated codes ('HHL, OFG').
- SeedCodes is a one-time bootstrap while the table is empty, so a correction or
a delete is never overwritten or restored.
- Tests: decode Label/Event, the /codes page (save, refuse, csrf, delete, auth),
store re-seed behaviour. go test ./internal/... passes.
This commit is contained in:
@@ -875,3 +875,91 @@ func TestMorningEndpointDryRun(t *testing.T) {
|
||||
t.Errorf("a dry run must not record deliveries: %+v", rows)
|
||||
}
|
||||
}
|
||||
|
||||
// TestCodesPageEditsDecodeMap proves FR-17 in the dashboard: Sam can see the
|
||||
// decode map, correct a teacher initial to a surname, and remove a mapping.
|
||||
// A corrected or deleted mapping must stick, not reappear from the seed.
|
||||
func TestCodesPageEditsDecodeMap(t *testing.T) {
|
||||
ts, st := newTestServer(t)
|
||||
client := login(t, ts)
|
||||
|
||||
page := getBody(t, client, ts.URL+"/codes")
|
||||
if !strings.Contains(page, "KNI") || !strings.Contains(page, "Teachers") {
|
||||
t.Fatalf("codes page does not list the seeded teacher codes")
|
||||
}
|
||||
token := csrfToken(t, page)
|
||||
|
||||
// Correct an initial to a surname.
|
||||
resp, err := client.PostForm(ts.URL+"/codes", url.Values{
|
||||
"csrf": {token}, "kind": {"teacher"}, "code": {"KNI"}, "name": {"Mr Knight"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("save: %v", err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusSeeOther {
|
||||
t.Fatalf("save status = %d, want 303", resp.StatusCode)
|
||||
}
|
||||
codes, err := st.Codes(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("codes: %v", err)
|
||||
}
|
||||
if codes.Teacher["KNI"] != "Mr Knight" {
|
||||
t.Errorf("teacher KNI = %q, want Mr Knight", codes.Teacher["KNI"])
|
||||
}
|
||||
|
||||
// An empty name is refused and the mapping is unchanged.
|
||||
resp, err = client.PostForm(ts.URL+"/codes", url.Values{
|
||||
"csrf": {token}, "kind": {"teacher"}, "code": {"KNI"}, "name": {""},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("bad save: %v", err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
if loc := resp.Header.Get("Location"); !strings.Contains(loc, "level=error") {
|
||||
t.Errorf("bad save Location = %q, want an error flash", loc)
|
||||
}
|
||||
if codes, _ := st.Codes(context.Background()); codes.Teacher["KNI"] != "Mr Knight" {
|
||||
t.Errorf("the refused save changed the mapping to %q", codes.Teacher["KNI"])
|
||||
}
|
||||
|
||||
// A bad CSRF token is refused.
|
||||
resp, err = client.PostForm(ts.URL+"/codes", url.Values{
|
||||
"csrf": {"wrong"}, "kind": {"teacher"}, "code": {"KNI"}, "name": {"Mr Nope"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("csrf save: %v", err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusForbidden {
|
||||
t.Errorf("bad csrf status = %d, want 403", resp.StatusCode)
|
||||
}
|
||||
|
||||
// Delete returns the code to raw, and it stays deleted.
|
||||
resp, err = client.PostForm(ts.URL+"/codes/delete", url.Values{
|
||||
"csrf": {token}, "kind": {"teacher"}, "code": {"KNI"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("delete: %v", err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusSeeOther {
|
||||
t.Fatalf("delete status = %d, want 303", resp.StatusCode)
|
||||
}
|
||||
if codes, _ := st.Codes(context.Background()); codes.Teacher["KNI"] != "" {
|
||||
t.Errorf("teacher KNI still mapped after delete: %q", codes.Teacher["KNI"])
|
||||
}
|
||||
|
||||
// The page is behind the session login.
|
||||
anon := &http.Client{CheckRedirect: func(*http.Request, []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
}}
|
||||
anonResp, err := anon.Get(ts.URL + "/codes")
|
||||
if err != nil {
|
||||
t.Fatalf("anon get: %v", err)
|
||||
}
|
||||
anonResp.Body.Close()
|
||||
if anonResp.StatusCode != http.StatusSeeOther {
|
||||
t.Errorf("anonymous /codes = %d, want a redirect to login", anonResp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user