e2e/pagination_test.go

v1.37.0
gitbay/e2e/pagination_test.go history · blame · raw

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