Commit abdeafa214

abdeafa2143636d262e1c9e77efbb2ed0ab3e9f3

parent: 921868375e

Verified · cmc

cmc <hello@cleberg.net> · 2026-09-29 04:41 UTC

web: mark-read dispatches notifications read

Ref #261

Layout: unified · split

CHANGELOG.org +2
@@ -6,6 +6,8 @@ anything beyond "replace the binary and restart" is needed.
6 6
7* Unreleased 7* Unreleased
8 8
9- Marking notifications read from the web dispatches =notifications
10 read=, so it counts against the write budget and is audited (#261).
9- =gitbayd admin backup --verify= also checks every release asset the 11- =gitbayd admin backup --verify= also checks every release asset the
10 database names against its recorded size and sha256, and every 12 database names against its recorded size and sha256, and every
11 archived LFS object against its name (#259). 13 archived LFS object against its name (#259).
internal/httpd/account_test.go +43
@@ -498,3 +498,46 @@ func TestAccountTokenCreateAuditOmitsToken(t *testing.T) {
498 t.Fatal("no cmd token create audit row") 498 t.Fatal("no cmd token create audit row")
499 } 499 }
500} 500}
501
502// Marking notices read goes through notifications read, so it carries the
503// audit trail and write budget of the CLI command (#261).
504func TestNotificationsReadDispatches(t *testing.T) {
505 st, err := store.Open(":memory:")
506 if err != nil {
507 t.Fatal(err)
508 }
509 defer st.Close()
510 if err := st.MigrateUp(); err != nil {
511 t.Fatal(err)
512 }
513 uid, err := st.CreateUser("alice", false)
514 if err != nil {
515 t.Fatal(err)
516 }
517 u := store.User{ID: uid, Username: "alice"}
518 repoID, err := st.CreateRepo("user", uid, "app", "public")
519 if err != nil {
520 t.Fatal(err)
521 }
522 for i := 0; i < 2; i++ {
523 if err := st.AddNotice(uid, repoID, "issue", "bob", "s", "alice/app/issues/1"); err != nil {
524 t.Fatal(err)
525 }
526 }
527 s := New(config.Default(), st, nil)
528
529 notices, _ := st.Inbox(uid, true, 10, 0)
530 req := httptest.NewRequest("POST", "/notifications/read", strings.NewReader("id="+strconv.FormatInt(notices[0].ID, 10)))
531 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
532 s.notificationsRead(httptest.NewRecorder(), req, u)
533 if n := st.UnreadNotices(uid); n != 1 {
534 t.Fatalf("unread after one id = %d, want 1", n)
535 }
536 assertAudited(t, st, "cmd notifications read")
537
538 req = httptest.NewRequest("POST", "/notifications/read", nil)
539 s.notificationsRead(httptest.NewRecorder(), req, u)
540 if n := st.UnreadNotices(uid); n != 0 {
541 t.Fatalf("unread after all = %d, want 0", n)
542 }
543}
internal/httpd/notifyweb.go +6 −8
@@ -38,20 +38,18 @@ func (s *Server) notifications(w http.ResponseWriter, r *http.Request, u store.U
38} 38}
39 39
40// notificationsRead marks one notice read, or the whole inbox when no id 40// notificationsRead marks one notice read, or the whole inbox when no id
41// is given, then returns to the list. 41// is given, through notifications read, then returns to the list (#261).
42func (s *Server) notificationsRead(w http.ResponseWriter, r *http.Request, u store.User) { 42func (s *Server) notificationsRead(w http.ResponseWriter, r *http.Request, u store.User) {
43 var ids []int64 43 argv := []string{"notifications", "read", "--all"}
44 if v := r.FormValue("id"); v != "" { 44 if v := r.FormValue("id"); v != "" {
45 n, err := strconv.ParseInt(v, 10, 64) 45 if _, err := strconv.ParseInt(v, 10, 64); err != nil {
46 if err != nil {
47 http.Error(w, "bad id", http.StatusBadRequest) 46 http.Error(w, "bad id", http.StatusBadRequest)
48 return 47 return
49 } 48 }
50 ids = append(ids, n) 49 argv = []string{"notifications", "read", v}
51 } 50 }
52 if _, err := s.st.MarkNoticesRead(u.ID, ids); err != nil { 51 if _, msg, ok := s.runControl(u, argv); !ok {
53 http.Error(w, "internal error", http.StatusInternalServerError) 52 s.setFlash(w, msg)
54 return
55 } 53 }
56 http.Redirect(w, r, "/notifications", http.StatusSeeOther) 54 http.Redirect(w, r, "/notifications", http.StatusSeeOther)
57} 55}