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 | 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 | 11 | - =gitbayd admin backup --verify= also checks every release asset the |
| 10 | 12 | database names against its recorded size and sha256, and every |
| 11 | 13 | archived LFS object against its name (#259). |
internal/httpd/account_test.go
+43
| @@ -498,3 +498,46 @@ func TestAccountTokenCreateAuditOmitsToken(t *testing.T) { |
| 498 | 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). |
| 504 | func 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 | 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). |
| 42 | 42 | func (s *Server) notificationsRead(w http.ResponseWriter, r *http.Request, u store.User) { |
| 43 | | var ids []int64 |
| 43 | argv := []string{"notifications", "read", "--all"} |
| 44 | 44 | if v := r.FormValue("id"); v != "" { |
| 45 | | n, err := strconv.ParseInt(v, 10, 64) |
| 46 | | if err != nil { |
| 45 | if _, err := strconv.ParseInt(v, 10, 64); err != nil { |
| 47 | 46 | http.Error(w, "bad id", http.StatusBadRequest) |
| 48 | 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 { |
| 53 | | http.Error(w, "internal error", http.StatusInternalServerError) |
| 54 | | return |
| 51 | if _, msg, ok := s.runControl(u, argv); !ok { |
| 52 | s.setFlash(w, msg) |
| 55 | 53 | } |
| 56 | 54 | http.Redirect(w, r, "/notifications", http.StatusSeeOther) |
| 57 | 55 | } |