e2e/pagination_test.go

4effb29e572777a68b56b21da1b1733fb6219e89
gitbay/e2e/pagination_test.go history · blame · raw

196 lines · 6694 bytes

  1package e2e
  2
  3import (
  4	"encoding/json"
  5	"fmt"
  6	"os"
  7	"path/filepath"
  8	"testing"
  9)
 10
 11// pageOf unmarshals a paged --json response.
 12type pageOf[T any] struct {
 13	Data struct {
 14		Items []T    `json:"items"`
 15		Next  string `json:"next"`
 16	} `json:"data"`
 17}
 18
 19func decodePage[T any](t *testing.T, out string) ([]T, string) {
 20	t.Helper()
 21	var p pageOf[T]
 22	if err := json.Unmarshal([]byte(out), &p); err != nil {
 23		t.Fatalf("bad json: %v\n%s", err, out)
 24	}
 25	return p.Data.Items, p.Data.Next
 26}
 27
 28// Cursor pagination on the list commands: opaque cursors, stable pages,
 29// and the bare-array shape untouched when the flags are absent.
 30func TestCursorPagination(t *testing.T) {
 31	inst := startInstance(t)
 32	aliceKey := inst.newKey(t, "alice")
 33	inst.admin(t, "admin", "user", "create", "alice", "--key", aliceKey+".pub")
 34
 35	if _, errOut, code := inst.ssh(t, aliceKey, "", "repo", "create", "alice/app"); code != 0 {
 36		t.Fatalf("repo create: %s", errOut)
 37	}
 38	for _, name := range []string{"'one'", "'two'", "'three'"} {
 39		if _, _, code := inst.ssh(t, aliceKey, "", "issue", "create", "alice/app", "--title", name); code != 0 {
 40			t.Fatal("issue create failed")
 41		}
 42	}
 43
 44	// Bare list: still a plain array.
 45	out, _, code := inst.ssh(t, aliceKey, "", "issue", "list", "alice/app", "--json")
 46	if code != 0 {
 47		t.Fatal("issue list failed")
 48	}
 49	var bare struct {
 50		Data []struct {
 51			Number int64 `json:"number"`
 52		} `json:"data"`
 53	}
 54	if err := json.Unmarshal([]byte(out), &bare); err != nil || len(bare.Data) != 3 {
 55		t.Fatalf("bare issue list: %v\n%s", err, out)
 56	}
 57
 58	type numbered struct {
 59		Number int64 `json:"number"`
 60	}
 61	// Page 1: newest two, with a cursor onward.
 62	out, _, code = inst.ssh(t, aliceKey, "", "issue", "list", "alice/app", "--limit", "2", "--json")
 63	if code != 0 {
 64		t.Fatal("paged issue list failed")
 65	}
 66	items, next := decodePage[numbered](t, out)
 67	if len(items) != 2 || items[0].Number != 3 || items[1].Number != 2 || next == "" {
 68		t.Fatalf("page 1: %+v next=%q", items, next)
 69	}
 70	// Page 2: the remainder, no cursor.
 71	out, _, code = inst.ssh(t, aliceKey, "", "issue", "list", "alice/app", "--limit", "2", "--cursor", next, "--json")
 72	if code != 0 {
 73		t.Fatal("cursor follow failed")
 74	}
 75	items, next = decodePage[numbered](t, out)
 76	if len(items) != 1 || items[0].Number != 1 || next != "" {
 77		t.Fatalf("page 2: %+v next=%q", items, next)
 78	}
 79
 80	// A cursor from one command is refused by another.
 81	out, _, code = inst.ssh(t, aliceKey, "", "issue", "list", "alice/app", "--limit", "2", "--json")
 82	items, next = decodePage[numbered](t, out)
 83	if _, _, code := inst.ssh(t, aliceKey, "", "mr", "list", "alice/app", "--cursor", next); code != 2 {
 84		t.Fatal("foreign cursor accepted")
 85	}
 86	if _, _, code := inst.ssh(t, aliceKey, "", "issue", "list", "alice/app", "--cursor", "garbage!"); code != 2 {
 87		t.Fatal("garbage cursor accepted")
 88	}
 89	if _, _, code := inst.ssh(t, aliceKey, "", "issue", "list", "alice/app", "--limit", "0"); code != 2 {
 90		t.Fatal("limit 0 accepted")
 91	}
 92
 93	// MR pagination pages by number the same way.
 94	work := t.TempDir()
 95	env := inst.gitEnv(aliceKey)
 96	mustGit(t, work, env, "clone", inst.sshURL("alice/app"), "w")
 97	dir := filepath.Join(work, "w")
 98	os.WriteFile(filepath.Join(dir, "a.txt"), []byte("a\n"), 0o644)
 99	mustGit(t, dir, env, "checkout", "-q", "-b", "main")
100	mustGit(t, dir, env, "add", ".")
101	mustGit(t, dir, env, "commit", "-q", "-m", "base")
102	mustGit(t, dir, env, "push", "-q", "origin", "main")
103	for i := 1; i <= 2; i++ {
104		branch := fmt.Sprintf("feat%d", i)
105		mustGit(t, dir, env, "checkout", "-q", "-b", branch, "main")
106		os.WriteFile(filepath.Join(dir, "a.txt"), []byte(fmt.Sprintf("a\n%d\n", i)), 0o644)
107		mustGit(t, dir, env, "add", ".")
108		mustGit(t, dir, env, "commit", "-q", "-m", branch)
109		mustGit(t, dir, env, "push", "-q", "origin", branch)
110		if _, _, code := inst.ssh(t, aliceKey, "", "mr", "create", "alice/app",
111			"--source", branch, "--target", "main", "--title", branch); code != 0 {
112			t.Fatal("mr create failed")
113		}
114	}
115	out, _, code = inst.ssh(t, aliceKey, "", "mr", "list", "alice/app", "--limit", "1", "--json")
116	if code != 0 {
117		t.Fatal("paged mr list failed")
118	}
119	items, next = decodePage[numbered](t, out)
120	if len(items) != 1 || items[0].Number != 2 || next == "" {
121		t.Fatalf("mr page 1: %+v next=%q", items, next)
122	}
123	out, _, code = inst.ssh(t, aliceKey, "", "mr", "list", "alice/app", "--limit", "1", "--cursor", next, "--json")
124	items, next = decodePage[numbered](t, out)
125	if len(items) != 1 || items[0].Number != 1 || next != "" {
126		t.Fatalf("mr page 2: %+v next=%q", items, next)
127	}
128
129	// Repo pagination pages by path.
130	for _, name := range []string{"alice/butter", "alice/cheese"} {
131		if _, _, code := inst.ssh(t, aliceKey, "", "repo", "create", name); code != 0 {
132			t.Fatal("repo create failed")
133		}
134	}
135	type pathed struct {
136		Path string `json:"path"`
137	}
138	out, _, code = inst.ssh(t, aliceKey, "", "repo", "list", "--limit", "2", "--json")
139	if code != 0 {
140		t.Fatal("paged repo list failed")
141	}
142	rItems, next := decodePage[pathed](t, out)
143	if len(rItems) != 2 || rItems[0].Path != "alice/app" || rItems[1].Path != "alice/butter" || next == "" {
144		t.Fatalf("repo page 1: %+v next=%q", rItems, next)
145	}
146	out, _, code = inst.ssh(t, aliceKey, "", "repo", "list", "--limit", "2", "--cursor", next, "--json")
147	rItems, next = decodePage[pathed](t, out)
148	if len(rItems) != 1 || rItems[0].Path != "alice/cheese" || next != "" {
149		t.Fatalf("repo page 2: %+v next=%q", rItems, next)
150	}
151
152	// The feed pages by event id, newest first.
153	type feedRow struct {
154		Kind string `json:"kind"`
155		Repo string `json:"repo"`
156	}
157	out, _, code = inst.ssh(t, aliceKey, "", "feed", "--limit", "2", "--json")
158	if code != 0 {
159		t.Fatal("paged feed failed")
160	}
161	fItems, next := decodePage[feedRow](t, out)
162	if len(fItems) != 2 || next == "" {
163		t.Fatalf("feed page 1: %+v next=%q", fItems, next)
164	}
165	if fItems[0].Kind != "mr.created" || fItems[0].Repo != "alice/app" {
166		t.Fatalf("feed head: %+v", fItems[0])
167	}
168	seen := len(fItems)
169	for next != "" {
170		out, _, code = inst.ssh(t, aliceKey, "", "feed", "--limit", "2", "--cursor", next, "--json")
171		if code != 0 {
172			t.Fatal("feed follow failed")
173		}
174		fItems, next = decodePage[feedRow](t, out)
175		seen += len(fItems)
176		if seen > 20 {
177			t.Fatal("feed cursor loop")
178		}
179	}
180	// 3 issues + 2 MRs created above.
181	if seen != 5 {
182		t.Fatalf("feed walked %d events", seen)
183	}
184
185	// Bare feed: plain array, most recent first.
186	out, _, code = inst.ssh(t, aliceKey, "", "feed", "--json")
187	if code != 0 {
188		t.Fatal("feed failed")
189	}
190	var bareFeed struct {
191		Data []feedRow `json:"data"`
192	}
193	if err := json.Unmarshal([]byte(out), &bareFeed); err != nil || len(bareFeed.Data) != 5 {
194		t.Fatalf("bare feed: %v\n%s", err, out)
195	}
196}