Files
gitea-tea/internal/app/notifications_test.go
T
bircni 85034c524f fix(notification): read the inbox by default and stream watch as NDJSON
notification commands required a repository unless --mine was given.
They now act on the account's inbox (D17); -R, TEA_REPO or the new
--this-repo limit them to one repository, and --mine is a hidden no-op.

notification list is a table with ID, TYPE, REPOSITORY, SUBJECT ("#17
Improve greeting", bold when unread), STATE and UPDATED, a "Showing N of
M" line and an empty message on a terminal; piped rows add the status
and the URL. The ID links to the subject. JSON gains the subject number.

notification view shows the subject, its state and the latest comment,
then marks an unread notification as read. mark-read, mark-unread, pin
and unpin report each change ("Pinned notification 12
(alice/project#17)"), and --all confirms on a terminal and reports the
count. notification watch --json printed one array when it exited; it
now prints each change as one JSON object per line, prints a line per
change on a terminal, and runs until Ctrl-C there instead of stopping
after a minute.

Status and type filters complete and validate their values, notification
IDs complete from the inbox, and every command has examples.

Signed-off-by: bircni <bircni@icloud.com>
2026-10-11 23:50:46 +02:00

141 lines
7.2 KiB
Go

// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package app
import (
"context"
"net/http"
"sync/atomic"
"testing"
"time"
"github.com/stretchr/testify/require"
)
func TestNotificationBoundsShareClockAndReachServer(t *testing.T) {
now := time.Date(2028, 2, 1, 12, 0, 0, 0, time.UTC)
calls := 0
server := newAuthenticatedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
calls++
require.Equal(t, "/api/v1/notifications", r.URL.Path)
require.Equal(t, "2028-01-31T12:00:00Z", r.URL.Query().Get("since"))
require.Equal(t, "2028-02-01T11:00:00Z", r.URL.Query().Get("before"))
require.Equal(t, []string{"unread", "pinned"}, r.URL.Query()["status-types"])
_, _ = w.Write([]byte(`[]`))
}))
defer server.Close()
deps, out, _ := serverDeps(t, server.URL)
reads := 0
deps.Now = func() time.Time { reads++; return now }
err := Execute(context.Background(), deps, []string{"notification", "list", "--mine", "--since=24h", "--before=1h", "--json"})
require.NoError(t, err)
require.Equal(t, 1, reads)
require.Equal(t, 1, calls)
require.JSONEq(t, `[]`, out.String())
}
// notificationThreads is a page of two notification threads as Gitea sends them.
const notificationThreads = `[
{"id":12,"unread":true,"repository":{"full_name":"alice/project"},"updated_at":"2026-10-11T18:00:00Z",
"subject":{"title":"Improve greeting","type":"Pull","state":"open","html_url":"https://gitea.test/alice/project/pulls/17","url":"https://gitea.test/api/v1/repos/alice/project/issues/17"}},
{"id":3,"unread":false,"pinned":true,"repository":{"full_name":"alice/project"},"updated_at":"2026-10-11T17:00:00Z",
"subject":{"title":"Improve error messages","type":"Issue","state":"closed","html_url":"https://gitea.test/alice/project/issues/8","url":"https://gitea.test/api/v1/repos/alice/project/issues/8"}}
]`
func TestNotificationsListTheInboxByDefault(t *testing.T) {
var path atomic.Value
server := newAuthenticatedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
path.Store(r.URL.Path)
w.Header().Set("X-Total-Count", "2")
_, _ = w.Write([]byte(notificationThreads))
}))
defer server.Close()
for _, tc := range []struct {
args []string
git map[string]string
path string
}{
{nil, nil, "/api/v1/notifications"},
{nil, map[string]string{"remote": "origin", "remote get-url origin": server.URL + "/alice/project.git"}, "/api/v1/notifications"},
{[]string{"-R", "alice/project"}, nil, "/api/v1/repos/alice/project/notifications"},
{[]string{"--this-repo"}, map[string]string{"remote": "origin", "remote get-url origin": server.URL + "/bob/fork.git"}, "/api/v1/repos/bob/fork/notifications"},
} {
deps, _, _ := serverDeps(t, server.URL)
deps.Git = &fakeGit{results: tc.git}
require.NoError(t, Execute(context.Background(), deps, append([]string{"notification", "list"}, tc.args...)), tc.args)
require.Equal(t, tc.path, path.Load(), tc.args)
}
deps, out, _ := serverDeps(t, server.URL)
deps.Streams.OutTTY, deps.Streams.Width = true, 100
deps.Now = func() time.Time { return time.Date(2026, 10, 11, 20, 0, 0, 0, time.UTC) }
require.NoError(t, Execute(context.Background(), deps, []string{"notification", "list"}))
require.Equal(t, `Showing 2 of 2 unread or pinned notifications in your inbox
ID TYPE REPOSITORY SUBJECT STATE UPDATED
12 pull alice/project #17 Improve greeting open about 2 hours ago
3 issue alice/project #8 Improve error messages closed about 3 hours ago
`, stripLinks(withoutANSI(out.String())))
deps, out, _ = serverDeps(t, server.URL)
require.NoError(t, Execute(context.Background(), deps, []string{"notification", "list"}))
require.Equal(t, "12\tpull\talice/project\t#17 Improve greeting\topen\tunread\t2026-10-11T18:00:00Z\thttps://gitea.test/alice/project/pulls/17\n"+
"3\tissue\talice/project\t#8 Improve error messages\tclosed\tpinned\t2026-10-11T17:00:00Z\thttps://gitea.test/alice/project/issues/8\n", out.String())
}
func TestNotificationWatchStreamsOneJSONObjectPerLine(t *testing.T) {
var polls atomic.Int32
server := newAuthenticatedServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
if polls.Add(1) == 1 {
_, _ = w.Write([]byte(`[{"id":1,"subject":{"title":"First"}}]`))
return
}
_, _ = w.Write([]byte(`[{"id":1,"subject":{"title":"First"}},{"id":2,"subject":{"title":"Second"}}]`))
}))
defer server.Close()
deps, out, _ := serverDeps(t, server.URL)
require.NoError(t, Execute(context.Background(), deps, []string{"notification", "watch", "--json", "id,title", "--interval", "20ms", "--timeout", "500ms"}))
require.Equal(t, "{\"id\":1,\"title\":\"First\"}\n{\"id\":2,\"title\":\"Second\"}\n", out.String())
}
func TestNotificationMutationsReportWhatChanged(t *testing.T) {
var marks []string
server := newAuthenticatedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == http.MethodPatch && r.URL.Path == "/api/v1/notifications/threads/12":
marks = append(marks, r.URL.Query().Get("to-status"))
_, _ = w.Write([]byte(`{"id":12,"repository":{"full_name":"alice/project"},"subject":{"type":"Pull","url":"https://gitea.test/api/v1/repos/alice/project/issues/17"}}`))
case r.Method == http.MethodGet && r.URL.Path == "/api/v1/notifications/threads/12":
_, _ = w.Write([]byte(`{"id":12,"unread":true,"repository":{"full_name":"alice/project"},"subject":{"title":"Improve greeting","type":"Pull","state":"open","html_url":"https://gitea.test/alice/project/pulls/17","url":"https://gitea.test/api/v1/repos/alice/project/issues/17","latest_comment_url":"https://gitea.test/api/v1/repos/alice/project/issues/comments/9"}}`))
case r.URL.Path == "/api/v1/repos/alice/project/issues/comments/9":
_, _ = w.Write([]byte(`{"id":9,"body":"Looks good to me","user":{"login":"bob"}}`))
case r.Method == http.MethodPut && r.URL.Path == "/api/v1/notifications":
marks = append(marks, "all "+r.URL.Query().Get("to-status"))
_, _ = w.Write([]byte(`[{"id":12},{"id":3}]`))
default:
t.Errorf("unexpected %s %s", r.Method, r.URL.Path)
}
}))
defer server.Close()
deps, out, diagnostics := serverDeps(t, server.URL)
deps.Streams.OutTTY = true
for _, tc := range []struct {
args []string
want string
}{
{[]string{"pin", "12"}, "✓ Pinned notification 12 (alice/project#17)\n"},
{[]string{"unpin", "#12"}, "✓ Unpinned notification 12 (alice/project#17)\n"},
{[]string{"mark-read", "12"}, "✓ Marked notification 12 (alice/project#17) as read\n"},
{[]string{"mark-read", "--all", "--yes"}, "✓ Marked 2 notifications as read\n"},
} {
diagnostics.Reset()
require.NoError(t, Execute(context.Background(), deps, append([]string{"notification"}, tc.args...)), tc.args)
require.Equal(t, tc.want, withoutANSI(diagnostics.String()), tc.args)
}
require.Equal(t, []string{"pinned", "read", "read", "all read"}, marks)
marks = nil
out.Reset()
require.NoError(t, Execute(context.Background(), deps, []string{"notification", "view", "12"}))
require.Contains(t, withoutANSI(out.String()), "Latest comment: bob\n\n Looks good to me\n\nView this pull request on Gitea: https://gitea.test/alice/project/pulls/17\n")
require.Equal(t, []string{"read"}, marks, "viewing marks an unread notification as read")
}