2026-02-21 16:07:43 +00:00
|
|
|
package forge
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
2026-03-26 18:00:20 +00:00
|
|
|
json "github.com/goccy/go-json"
|
2026-02-21 16:07:43 +00:00
|
|
|
"net/http"
|
|
|
|
|
"net/http/httptest"
|
|
|
|
|
"testing"
|
2026-04-02 00:52:43 +00:00
|
|
|
"time"
|
2026-02-21 16:07:43 +00:00
|
|
|
|
2026-03-22 01:51:29 +00:00
|
|
|
"dappco.re/go/core/forge/types"
|
2026-02-21 16:07:43 +00:00
|
|
|
)
|
|
|
|
|
|
2026-03-26 18:00:20 +00:00
|
|
|
func TestNotificationService_List_Good(t *testing.T) {
|
2026-02-21 16:07:43 +00:00
|
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
if r.Method != http.MethodGet {
|
|
|
|
|
t.Errorf("expected GET, got %s", r.Method)
|
|
|
|
|
}
|
|
|
|
|
if r.URL.Path != "/api/v1/notifications" {
|
|
|
|
|
t.Errorf("wrong path: %s", r.URL.Path)
|
|
|
|
|
}
|
|
|
|
|
w.Header().Set("X-Total-Count", "2")
|
|
|
|
|
json.NewEncoder(w).Encode([]types.NotificationThread{
|
|
|
|
|
{ID: 1, Unread: true, Subject: &types.NotificationSubject{Title: "Issue opened"}},
|
|
|
|
|
{ID: 2, Unread: false, Subject: &types.NotificationSubject{Title: "PR merged"}},
|
|
|
|
|
})
|
|
|
|
|
}))
|
|
|
|
|
defer srv.Close()
|
|
|
|
|
|
|
|
|
|
f := NewForge(srv.URL, "tok")
|
|
|
|
|
threads, err := f.Notifications.List(context.Background())
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if len(threads) != 2 {
|
|
|
|
|
t.Fatalf("got %d threads, want 2", len(threads))
|
|
|
|
|
}
|
|
|
|
|
if threads[0].ID != 1 {
|
|
|
|
|
t.Errorf("got id=%d, want 1", threads[0].ID)
|
|
|
|
|
}
|
|
|
|
|
if threads[0].Subject.Title != "Issue opened" {
|
|
|
|
|
t.Errorf("got title=%q, want %q", threads[0].Subject.Title, "Issue opened")
|
|
|
|
|
}
|
|
|
|
|
if !threads[0].Unread {
|
|
|
|
|
t.Error("expected thread 1 to be unread")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-02 01:25:43 +00:00
|
|
|
func TestNotificationService_List_Filters(t *testing.T) {
|
|
|
|
|
since := time.Date(2026, time.April, 1, 12, 0, 0, 0, time.UTC)
|
|
|
|
|
before := time.Date(2026, time.April, 2, 12, 0, 0, 0, time.UTC)
|
|
|
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
if r.Method != http.MethodGet {
|
|
|
|
|
t.Errorf("expected GET, got %s", r.Method)
|
|
|
|
|
}
|
|
|
|
|
if r.URL.Path != "/api/v1/notifications" {
|
|
|
|
|
t.Errorf("wrong path: %s", r.URL.Path)
|
|
|
|
|
}
|
|
|
|
|
if got := r.URL.Query().Get("all"); got != "true" {
|
|
|
|
|
t.Errorf("got all=%q, want true", got)
|
|
|
|
|
}
|
|
|
|
|
if got := r.URL.Query()["status-types"]; len(got) != 2 || got[0] != "unread" || got[1] != "pinned" {
|
|
|
|
|
t.Errorf("got status-types=%v, want [unread pinned]", got)
|
|
|
|
|
}
|
|
|
|
|
if got := r.URL.Query()["subject-type"]; len(got) != 2 || got[0] != "issue" || got[1] != "pull" {
|
|
|
|
|
t.Errorf("got subject-type=%v, want [issue pull]", got)
|
|
|
|
|
}
|
|
|
|
|
if got := r.URL.Query().Get("since"); got != since.Format(time.RFC3339) {
|
|
|
|
|
t.Errorf("got since=%q, want %q", got, since.Format(time.RFC3339))
|
|
|
|
|
}
|
|
|
|
|
if got := r.URL.Query().Get("before"); got != before.Format(time.RFC3339) {
|
|
|
|
|
t.Errorf("got before=%q, want %q", got, before.Format(time.RFC3339))
|
|
|
|
|
}
|
|
|
|
|
w.Header().Set("X-Total-Count", "1")
|
|
|
|
|
json.NewEncoder(w).Encode([]types.NotificationThread{
|
|
|
|
|
{ID: 11, Unread: true, Subject: &types.NotificationSubject{Title: "Filtered"}},
|
|
|
|
|
})
|
|
|
|
|
}))
|
|
|
|
|
defer srv.Close()
|
|
|
|
|
|
|
|
|
|
f := NewForge(srv.URL, "tok")
|
|
|
|
|
threads, err := f.Notifications.List(context.Background(), NotificationListOptions{
|
|
|
|
|
All: true,
|
|
|
|
|
StatusTypes: []string{"unread", "pinned"},
|
|
|
|
|
SubjectTypes: []string{"issue", "pull"},
|
|
|
|
|
Since: &since,
|
|
|
|
|
Before: &before,
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if len(threads) != 1 || threads[0].ID != 11 {
|
|
|
|
|
t.Fatalf("got threads=%+v", threads)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-26 18:00:20 +00:00
|
|
|
func TestNotificationService_ListRepo_Good(t *testing.T) {
|
2026-02-21 16:07:43 +00:00
|
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
if r.Method != http.MethodGet {
|
|
|
|
|
t.Errorf("expected GET, got %s", r.Method)
|
|
|
|
|
}
|
|
|
|
|
if r.URL.Path != "/api/v1/repos/core/go-forge/notifications" {
|
|
|
|
|
t.Errorf("wrong path: %s", r.URL.Path)
|
|
|
|
|
}
|
|
|
|
|
w.Header().Set("X-Total-Count", "1")
|
|
|
|
|
json.NewEncoder(w).Encode([]types.NotificationThread{
|
|
|
|
|
{ID: 10, Unread: true, Subject: &types.NotificationSubject{Title: "New commit"}},
|
|
|
|
|
})
|
|
|
|
|
}))
|
|
|
|
|
defer srv.Close()
|
|
|
|
|
|
|
|
|
|
f := NewForge(srv.URL, "tok")
|
|
|
|
|
threads, err := f.Notifications.ListRepo(context.Background(), "core", "go-forge")
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if len(threads) != 1 {
|
|
|
|
|
t.Fatalf("got %d threads, want 1", len(threads))
|
|
|
|
|
}
|
|
|
|
|
if threads[0].ID != 10 {
|
|
|
|
|
t.Errorf("got id=%d, want 10", threads[0].ID)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-02 01:25:43 +00:00
|
|
|
func TestNotificationService_ListRepo_Filters(t *testing.T) {
|
|
|
|
|
since := time.Date(2026, time.April, 1, 12, 0, 0, 0, time.UTC)
|
|
|
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
if r.Method != http.MethodGet {
|
|
|
|
|
t.Errorf("expected GET, got %s", r.Method)
|
|
|
|
|
}
|
|
|
|
|
if r.URL.Path != "/api/v1/repos/core/go-forge/notifications" {
|
|
|
|
|
t.Errorf("wrong path: %s", r.URL.Path)
|
|
|
|
|
}
|
|
|
|
|
if got := r.URL.Query()["status-types"]; len(got) != 1 || got[0] != "read" {
|
|
|
|
|
t.Errorf("got status-types=%v, want [read]", got)
|
|
|
|
|
}
|
|
|
|
|
if got := r.URL.Query()["subject-type"]; len(got) != 1 || got[0] != "repository" {
|
|
|
|
|
t.Errorf("got subject-type=%v, want [repository]", got)
|
|
|
|
|
}
|
|
|
|
|
if got := r.URL.Query().Get("since"); got != since.Format(time.RFC3339) {
|
|
|
|
|
t.Errorf("got since=%q, want %q", got, since.Format(time.RFC3339))
|
|
|
|
|
}
|
|
|
|
|
w.Header().Set("X-Total-Count", "1")
|
|
|
|
|
json.NewEncoder(w).Encode([]types.NotificationThread{
|
|
|
|
|
{ID: 12, Unread: false, Subject: &types.NotificationSubject{Title: "Repo filtered"}},
|
|
|
|
|
})
|
|
|
|
|
}))
|
|
|
|
|
defer srv.Close()
|
|
|
|
|
|
|
|
|
|
f := NewForge(srv.URL, "tok")
|
|
|
|
|
threads, err := f.Notifications.ListRepo(context.Background(), "core", "go-forge", NotificationListOptions{
|
|
|
|
|
StatusTypes: []string{"read"},
|
|
|
|
|
SubjectTypes: []string{"repository"},
|
|
|
|
|
Since: &since,
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if len(threads) != 1 || threads[0].ID != 12 {
|
|
|
|
|
t.Fatalf("got threads=%+v", threads)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-01 23:42:31 +00:00
|
|
|
func TestNotificationService_NewAvailable_Good(t *testing.T) {
|
|
|
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
if r.Method != http.MethodGet {
|
|
|
|
|
t.Errorf("expected GET, got %s", r.Method)
|
|
|
|
|
}
|
|
|
|
|
if r.URL.Path != "/api/v1/notifications/new" {
|
|
|
|
|
t.Errorf("wrong path: %s", r.URL.Path)
|
|
|
|
|
}
|
|
|
|
|
json.NewEncoder(w).Encode(types.NotificationCount{New: 3})
|
|
|
|
|
}))
|
|
|
|
|
defer srv.Close()
|
|
|
|
|
|
|
|
|
|
f := NewForge(srv.URL, "tok")
|
|
|
|
|
count, err := f.Notifications.NewAvailable(context.Background())
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if count.New != 3 {
|
|
|
|
|
t.Fatalf("got new=%d, want 3", count.New)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-26 18:00:20 +00:00
|
|
|
func TestNotificationService_GetThread_Good(t *testing.T) {
|
2026-02-21 16:07:43 +00:00
|
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
if r.Method != http.MethodGet {
|
|
|
|
|
t.Errorf("expected GET, got %s", r.Method)
|
|
|
|
|
}
|
|
|
|
|
if r.URL.Path != "/api/v1/notifications/threads/42" {
|
|
|
|
|
t.Errorf("wrong path: %s", r.URL.Path)
|
|
|
|
|
}
|
|
|
|
|
json.NewEncoder(w).Encode(types.NotificationThread{
|
|
|
|
|
ID: 42,
|
|
|
|
|
Unread: true,
|
|
|
|
|
Subject: &types.NotificationSubject{
|
|
|
|
|
Title: "Build failed",
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
}))
|
|
|
|
|
defer srv.Close()
|
|
|
|
|
|
|
|
|
|
f := NewForge(srv.URL, "tok")
|
|
|
|
|
thread, err := f.Notifications.GetThread(context.Background(), 42)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if thread.ID != 42 {
|
|
|
|
|
t.Errorf("got id=%d, want 42", thread.ID)
|
|
|
|
|
}
|
|
|
|
|
if thread.Subject.Title != "Build failed" {
|
|
|
|
|
t.Errorf("got title=%q, want %q", thread.Subject.Title, "Build failed")
|
|
|
|
|
}
|
|
|
|
|
if !thread.Unread {
|
|
|
|
|
t.Error("expected thread to be unread")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-02 03:03:01 +00:00
|
|
|
func TestNotificationService_MarkNotifications_Good(t *testing.T) {
|
|
|
|
|
lastReadAt := time.Date(2026, time.April, 2, 15, 4, 5, 0, time.UTC)
|
|
|
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
if r.Method != http.MethodPut {
|
|
|
|
|
t.Errorf("expected PUT, got %s", r.Method)
|
|
|
|
|
}
|
|
|
|
|
if r.URL.Path != "/api/v1/notifications" {
|
|
|
|
|
t.Errorf("wrong path: %s", r.URL.Path)
|
|
|
|
|
}
|
|
|
|
|
if got := r.URL.Query().Get("all"); got != "true" {
|
|
|
|
|
t.Errorf("got all=%q, want true", got)
|
|
|
|
|
}
|
|
|
|
|
if got := r.URL.Query()["status-types"]; len(got) != 2 || got[0] != "unread" || got[1] != "pinned" {
|
|
|
|
|
t.Errorf("got status-types=%v, want [unread pinned]", got)
|
|
|
|
|
}
|
|
|
|
|
if got := r.URL.Query().Get("to-status"); got != "read" {
|
|
|
|
|
t.Errorf("got to-status=%q, want read", got)
|
|
|
|
|
}
|
|
|
|
|
if got := r.URL.Query().Get("last_read_at"); got != lastReadAt.Format(time.RFC3339) {
|
|
|
|
|
t.Errorf("got last_read_at=%q, want %q", got, lastReadAt.Format(time.RFC3339))
|
|
|
|
|
}
|
|
|
|
|
w.WriteHeader(http.StatusResetContent)
|
|
|
|
|
json.NewEncoder(w).Encode([]types.NotificationThread{
|
|
|
|
|
{ID: 21, Unread: false, Subject: &types.NotificationSubject{Title: "Release notes"}},
|
|
|
|
|
{ID: 22, Unread: false, Subject: &types.NotificationSubject{Title: "Issue triaged"}},
|
|
|
|
|
})
|
|
|
|
|
}))
|
|
|
|
|
defer srv.Close()
|
|
|
|
|
|
|
|
|
|
f := NewForge(srv.URL, "tok")
|
|
|
|
|
threads, err := f.Notifications.MarkNotifications(context.Background(), &NotificationMarkOptions{
|
|
|
|
|
All: true,
|
|
|
|
|
StatusTypes: []string{"unread", "pinned"},
|
|
|
|
|
ToStatus: "read",
|
|
|
|
|
LastReadAt: &lastReadAt,
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if len(threads) != 2 {
|
|
|
|
|
t.Fatalf("got %d threads, want 2", len(threads))
|
|
|
|
|
}
|
|
|
|
|
if threads[0].ID != 21 || threads[1].ID != 22 {
|
|
|
|
|
t.Fatalf("got ids=%d,%d want 21,22", threads[0].ID, threads[1].ID)
|
|
|
|
|
}
|
|
|
|
|
if threads[0].Subject.Title != "Release notes" {
|
|
|
|
|
t.Errorf("got title=%q, want %q", threads[0].Subject.Title, "Release notes")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-26 18:00:20 +00:00
|
|
|
func TestNotificationService_MarkRead_Good(t *testing.T) {
|
2026-02-21 16:07:43 +00:00
|
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
if r.Method != http.MethodPut {
|
|
|
|
|
t.Errorf("expected PUT, got %s", r.Method)
|
|
|
|
|
}
|
|
|
|
|
if r.URL.Path != "/api/v1/notifications" {
|
|
|
|
|
t.Errorf("wrong path: %s", r.URL.Path)
|
|
|
|
|
}
|
|
|
|
|
w.WriteHeader(http.StatusResetContent)
|
|
|
|
|
}))
|
|
|
|
|
defer srv.Close()
|
|
|
|
|
|
|
|
|
|
f := NewForge(srv.URL, "tok")
|
|
|
|
|
err := f.Notifications.MarkRead(context.Background())
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-26 18:00:20 +00:00
|
|
|
func TestNotificationService_MarkThreadRead_Good(t *testing.T) {
|
2026-02-21 16:07:43 +00:00
|
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
if r.Method != http.MethodPatch {
|
|
|
|
|
t.Errorf("expected PATCH, got %s", r.Method)
|
|
|
|
|
}
|
|
|
|
|
if r.URL.Path != "/api/v1/notifications/threads/42" {
|
|
|
|
|
t.Errorf("wrong path: %s", r.URL.Path)
|
|
|
|
|
}
|
|
|
|
|
w.WriteHeader(http.StatusResetContent)
|
|
|
|
|
}))
|
|
|
|
|
defer srv.Close()
|
|
|
|
|
|
|
|
|
|
f := NewForge(srv.URL, "tok")
|
|
|
|
|
err := f.Notifications.MarkThreadRead(context.Background(), 42)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-02 00:52:43 +00:00
|
|
|
func TestNotificationService_MarkRepoNotifications_Good(t *testing.T) {
|
|
|
|
|
lastReadAt := time.Date(2026, time.April, 2, 15, 4, 5, 0, time.UTC)
|
|
|
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
if r.Method != http.MethodPut {
|
|
|
|
|
t.Errorf("expected PUT, got %s", r.Method)
|
|
|
|
|
}
|
|
|
|
|
if r.URL.Path != "/api/v1/repos/core/go-forge/notifications" {
|
|
|
|
|
t.Errorf("wrong path: %s", r.URL.Path)
|
|
|
|
|
}
|
|
|
|
|
if got := r.URL.Query()["status-types"]; len(got) != 2 || got[0] != "unread" || got[1] != "pinned" {
|
|
|
|
|
t.Errorf("got status-types=%v, want [unread pinned]", got)
|
|
|
|
|
}
|
|
|
|
|
if got := r.URL.Query().Get("all"); got != "true" {
|
|
|
|
|
t.Errorf("got all=%q, want true", got)
|
|
|
|
|
}
|
|
|
|
|
if got := r.URL.Query().Get("to-status"); got != "read" {
|
|
|
|
|
t.Errorf("got to-status=%q, want read", got)
|
|
|
|
|
}
|
|
|
|
|
if got := r.URL.Query().Get("last_read_at"); got != lastReadAt.Format(time.RFC3339) {
|
|
|
|
|
t.Errorf("got last_read_at=%q, want %q", got, lastReadAt.Format(time.RFC3339))
|
|
|
|
|
}
|
|
|
|
|
w.WriteHeader(http.StatusResetContent)
|
|
|
|
|
json.NewEncoder(w).Encode([]types.NotificationThread{
|
|
|
|
|
{ID: 7, Unread: false, Subject: &types.NotificationSubject{Title: "Pinned release"}},
|
|
|
|
|
{ID: 8, Unread: false, Subject: &types.NotificationSubject{Title: "New docs"}},
|
|
|
|
|
})
|
|
|
|
|
}))
|
|
|
|
|
defer srv.Close()
|
|
|
|
|
|
|
|
|
|
f := NewForge(srv.URL, "tok")
|
|
|
|
|
threads, err := f.Notifications.MarkRepoNotifications(context.Background(), "core", "go-forge", &NotificationRepoMarkOptions{
|
|
|
|
|
All: true,
|
|
|
|
|
StatusTypes: []string{"unread", "pinned"},
|
|
|
|
|
ToStatus: "read",
|
|
|
|
|
LastReadAt: &lastReadAt,
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if len(threads) != 2 {
|
|
|
|
|
t.Fatalf("got %d threads, want 2", len(threads))
|
|
|
|
|
}
|
|
|
|
|
if threads[0].ID != 7 || threads[1].ID != 8 {
|
|
|
|
|
t.Fatalf("got ids=%d,%d want 7,8", threads[0].ID, threads[1].ID)
|
|
|
|
|
}
|
|
|
|
|
if threads[0].Subject.Title != "Pinned release" {
|
|
|
|
|
t.Errorf("got title=%q, want %q", threads[0].Subject.Title, "Pinned release")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-26 18:00:20 +00:00
|
|
|
func TestNotificationService_NotFound_Bad(t *testing.T) {
|
2026-02-21 16:07:43 +00:00
|
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
w.WriteHeader(http.StatusNotFound)
|
|
|
|
|
json.NewEncoder(w).Encode(map[string]string{"message": "thread not found"})
|
|
|
|
|
}))
|
|
|
|
|
defer srv.Close()
|
|
|
|
|
|
|
|
|
|
f := NewForge(srv.URL, "tok")
|
|
|
|
|
_, err := f.Notifications.GetThread(context.Background(), 9999)
|
|
|
|
|
if err == nil {
|
|
|
|
|
t.Fatal("expected error, got nil")
|
|
|
|
|
}
|
|
|
|
|
if !IsNotFound(err) {
|
|
|
|
|
t.Errorf("expected not-found error, got %v", err)
|
|
|
|
|
}
|
|
|
|
|
}
|