graphql_test.go

  1package graphql
  2
  3import (
  4	"testing"
  5
  6	"github.com/99designs/gqlgen/client"
  7	"github.com/stretchr/testify/assert"
  8	"github.com/stretchr/testify/require"
  9
 10	"github.com/git-bug/git-bug/api/graphql/models"
 11	"github.com/git-bug/git-bug/cache"
 12	"github.com/git-bug/git-bug/misc/random_bugs"
 13	"github.com/git-bug/git-bug/repository"
 14)
 15
 16func TestQueries(t *testing.T) {
 17	repo := repository.CreateGoGitTestRepo(t, false)
 18
 19	random_bugs.FillRepoWithSeed(repo, 10, 42)
 20
 21	mrc := cache.NewMultiRepoCache()
 22	_, events := mrc.RegisterDefaultRepository(repo)
 23	for event := range events {
 24		require.NoError(t, event.Err)
 25	}
 26
 27	handler := NewHandler(mrc, ServerConfig{AuthMode: "local"}, nil)
 28
 29	c := client.New(handler)
 30
 31	query := `
 32     query {
 33        repository {
 34          allBugs(first: 2) {
 35            pageInfo {
 36              endCursor
 37              hasNextPage
 38              startCursor
 39              hasPreviousPage
 40            }
 41            nodes{
 42              author {
 43                name
 44                email
 45                avatarUrl
 46              }
 47
 48              createdAt
 49              humanId
 50              id
 51              lastEdit
 52              status
 53              title
 54
 55              actors(first: 10) {
 56                pageInfo {
 57                  endCursor
 58                  hasNextPage
 59                  startCursor
 60                  hasPreviousPage
 61                }
 62                nodes {
 63                  id
 64                  humanId
 65                  name
 66                  displayName
 67                }
 68              }
 69
 70              participants(first: 10) {
 71                pageInfo {
 72                  endCursor
 73                  hasNextPage
 74                  startCursor
 75                  hasPreviousPage
 76                }
 77                nodes {
 78                  id
 79                  humanId
 80                  name
 81                  displayName
 82                }
 83              }
 84
 85              comments(first: 2) {
 86                pageInfo {
 87                  endCursor
 88                  hasNextPage
 89                  startCursor
 90                  hasPreviousPage
 91                }
 92                nodes {
 93                  files
 94                  message
 95                }
 96              }
 97
 98              operations(first: 20) {
 99                pageInfo {
100                  endCursor
101                  hasNextPage
102                  startCursor
103                  hasPreviousPage
104                }
105                nodes {
106                  author {
107                    name
108                    email
109                    avatarUrl
110                  }
111                  date
112                  ... on BugCreateOperation {
113                    title
114                    message
115                    files
116                  }
117                  ... on BugSetTitleOperation {
118                    title
119                    was
120                  }
121                  ... on BugAddCommentOperation {
122                    files
123                    message
124                  }
125                  ... on BugSetStatusOperation {
126                    status
127                  }
128                  ... on BugLabelChangeOperation {
129                    added {
130                      name
131                      color {
132                        R
133                        G
134                        B
135                      }
136                    }
137                    removed {
138                      name
139                      color {
140                        R
141                        G
142                        B
143                      }
144                    }
145                  }
146                }
147              }
148            }
149          }
150        }
151      }`
152
153	type Identity struct {
154		Id          string `json:"id"`
155		HumanId     string `json:"humanId"`
156		Name        string `json:"name"`
157		Email       string `json:"email"`
158		AvatarUrl   string `json:"avatarUrl"`
159		DisplayName string `json:"displayName"`
160	}
161
162	type Label struct {
163		Name  string
164		Color struct {
165			R, G, B int
166		}
167	}
168
169	var resp struct {
170		Repository struct {
171			AllBugs struct {
172				PageInfo models.PageInfo
173				Nodes    []struct {
174					Author    Identity
175					CreatedAt string `json:"createdAt"`
176					HumanId   string `json:"humanId"`
177					Id        string
178					LastEdit  string `json:"lastEdit"`
179					Status    string
180					Title     string
181
182					Actors struct {
183						PageInfo models.PageInfo
184						Nodes    []Identity
185					}
186
187					Participants struct {
188						PageInfo models.PageInfo
189						Nodes    []Identity
190					}
191
192					Comments struct {
193						PageInfo models.PageInfo
194						Nodes    []struct {
195							Files   []string
196							Message string
197						}
198					}
199
200					Operations struct {
201						PageInfo models.PageInfo
202						Nodes    []struct {
203							Author  Identity
204							Date    string
205							Title   string
206							Files   []string
207							Message string
208							Was     string
209							Status  string
210							Added   []Label
211							Removed []Label
212						}
213					}
214				}
215			}
216		}
217	}
218
219	err := c.Post(query, &resp)
220	assert.NoError(t, err)
221}
222
223// TestGitBrowseQueries exercises the git-browsing GraphQL fields (commit, blob,
224// tree, commits, lastCommits) against a synthetic fixture repo with the same
225// commit graph used by RepoBrowseTest:
226//
227//	c1 ── c2 ── c3   refs/heads/main
228//	       └────────  refs/heads/feature
229//	c1 ←── refs/tags/v1.0
230func TestGitBrowseQueries(t *testing.T) {
231	repo := repository.CreateGoGitTestRepo(t, false)
232	require.NoError(t, repo.LocalConfig().StoreString("init.defaultBranch", "main"))
233
234	// ── build fixture ─────────────────────────────────────────────────────────
235
236	readmeV1 := []byte("# Hello\n")
237	readmeV3 := []byte("# Hello\n\n## Updated\n")
238	mainV1 := []byte("package main\n")
239	mainV2 := []byte("package main\n\n// updated\n")
240	libV1 := []byte("package lib\n")
241	utilV1 := []byte("package util\n")
242
243	hReadmeV1, err := repo.StoreData(readmeV1)
244	require.NoError(t, err)
245	hReadmeV3, err := repo.StoreData(readmeV3)
246	require.NoError(t, err)
247	hMainV1, err := repo.StoreData(mainV1)
248	require.NoError(t, err)
249	hMainV2, err := repo.StoreData(mainV2)
250	require.NoError(t, err)
251	hLibV1, err := repo.StoreData(libV1)
252	require.NoError(t, err)
253	hUtilV1, err := repo.StoreData(utilV1)
254	require.NoError(t, err)
255
256	srcTreeV1, err := repo.StoreTree([]repository.TreeEntry{
257		{ObjectType: repository.Blob, Hash: hLibV1, Name: "lib.go"},
258	})
259	require.NoError(t, err)
260	rootTreeV1, err := repo.StoreTree([]repository.TreeEntry{
261		{ObjectType: repository.Blob, Hash: hReadmeV1, Name: "README.md"},
262		{ObjectType: repository.Blob, Hash: hMainV1, Name: "main.go"},
263		{ObjectType: repository.Tree, Hash: srcTreeV1, Name: "src"},
264	})
265	require.NoError(t, err)
266
267	srcTreeV2, err := repo.StoreTree([]repository.TreeEntry{
268		{ObjectType: repository.Blob, Hash: hLibV1, Name: "lib.go"},
269		{ObjectType: repository.Blob, Hash: hUtilV1, Name: "util.go"},
270	})
271	require.NoError(t, err)
272	rootTreeV2, err := repo.StoreTree([]repository.TreeEntry{
273		{ObjectType: repository.Blob, Hash: hReadmeV1, Name: "README.md"},
274		{ObjectType: repository.Blob, Hash: hMainV2, Name: "main.go"},
275		{ObjectType: repository.Tree, Hash: srcTreeV2, Name: "src"},
276	})
277	require.NoError(t, err)
278
279	rootTreeV3, err := repo.StoreTree([]repository.TreeEntry{
280		{ObjectType: repository.Blob, Hash: hReadmeV3, Name: "README.md"},
281		{ObjectType: repository.Blob, Hash: hMainV2, Name: "main.go"},
282		{ObjectType: repository.Tree, Hash: srcTreeV2, Name: "src"},
283	})
284	require.NoError(t, err)
285
286	c1, err := repo.StoreCommit(rootTreeV1)
287	require.NoError(t, err)
288	c2, err := repo.StoreCommit(rootTreeV2, c1)
289	require.NoError(t, err)
290	c3, err := repo.StoreCommit(rootTreeV3, c2)
291	require.NoError(t, err)
292
293	require.NoError(t, repo.UpdateRef("refs/heads/main", c3))
294	require.NoError(t, repo.UpdateRef("refs/heads/feature", c2))
295	require.NoError(t, repo.UpdateRef("refs/tags/v1.0", c1))
296
297	// ── set up GraphQL handler ─────────────────────────────────────────────────
298
299	mrc := cache.NewMultiRepoCache()
300	_, events := mrc.RegisterDefaultRepository(repo)
301	for event := range events {
302		require.NoError(t, event.Err)
303	}
304	c := client.New(NewHandler(mrc, ServerConfig{AuthMode: "local"}, nil))
305
306	// ── commit ────────────────────────────────────────────────────────────────
307
308	t.Run("commit", func(t *testing.T) {
309		var resp struct {
310			Repository struct {
311				Commit struct {
312					Hash    string
313					Parents []string
314				}
315			}
316		}
317		require.NoError(t, c.Post(`query($hash: String!) {
318			repository { commit(hash: $hash) { hash parents } }
319		}`, &resp, client.Var("hash", string(c3))))
320		got := resp.Repository.Commit
321		require.Equal(t, string(c3), got.Hash)
322		require.Equal(t, []string{string(c2)}, got.Parents)
323	})
324
325	t.Run("commit_not_found", func(t *testing.T) {
326		var resp struct {
327			Repository struct {
328				Commit *struct{ Hash string }
329			}
330		}
331		require.NoError(t, c.Post(`query {
332			repository { commit(hash: "0000000000000000000000000000000000000000") { hash } }
333		}`, &resp))
334		require.Nil(t, resp.Repository.Commit)
335	})
336
337	// ── blob ──────────────────────────────────────────────────────────────────
338
339	t.Run("blob", func(t *testing.T) {
340		var resp struct {
341			Repository struct {
342				Blob struct {
343					Hash     string
344					IsBinary bool
345					Size     int
346					Text     *string
347				}
348			}
349		}
350		require.NoError(t, c.Post(`query {
351			repository { blob(ref: "main", path: "README.md") { hash isBinary size text } }
352		}`, &resp))
353		got := resp.Repository.Blob
354		require.Equal(t, string(hReadmeV3), got.Hash)
355		require.False(t, got.IsBinary)
356		require.Equal(t, len(readmeV3), got.Size)
357		require.NotNil(t, got.Text)
358		require.Equal(t, string(readmeV3), *got.Text)
359	})
360
361	t.Run("blob_not_found", func(t *testing.T) {
362		var resp struct {
363			Repository struct {
364				Blob *struct{ Hash string }
365			}
366		}
367		require.NoError(t, c.Post(`query {
368			repository { blob(ref: "main", path: "nonexistent.go") { hash } }
369		}`, &resp))
370		require.Nil(t, resp.Repository.Blob)
371	})
372
373	// ── tree ──────────────────────────────────────────────────────────────────
374
375	t.Run("tree", func(t *testing.T) {
376		var resp struct {
377			Repository struct {
378				Tree []struct {
379					Name string
380					Type string `json:"type"`
381				}
382			}
383		}
384		require.NoError(t, c.Post(`query {
385			repository { tree(ref: "main", path: "") { name type } }
386		}`, &resp))
387		byName := make(map[string]string)
388		for _, e := range resp.Repository.Tree {
389			byName[e.Name] = e.Type
390		}
391		require.Equal(t, "BLOB", byName["README.md"])
392		require.Equal(t, "BLOB", byName["main.go"])
393		require.Equal(t, "TREE", byName["src"])
394	})
395
396	// ── commits ───────────────────────────────────────────────────────────────
397
398	t.Run("commits", func(t *testing.T) {
399		var resp struct {
400			Repository struct {
401				Commits struct {
402					TotalCount int
403					PageInfo   struct{ HasNextPage bool }
404					Nodes      []struct{ Hash string }
405				}
406			}
407		}
408		require.NoError(t, c.Post(`query {
409			repository {
410				commits(ref: "main", first: 2) {
411					totalCount pageInfo { hasNextPage } nodes { hash }
412				}
413			}
414		}`, &resp))
415		got := resp.Repository.Commits
416		require.Equal(t, 2, got.TotalCount)
417		require.True(t, got.PageInfo.HasNextPage)
418		require.Equal(t, string(c3), got.Nodes[0].Hash)
419		require.Equal(t, string(c2), got.Nodes[1].Hash)
420	})
421
422	// ── lastCommits ───────────────────────────────────────────────────────────
423
424	t.Run("lastCommits", func(t *testing.T) {
425		var resp struct {
426			Repository struct {
427				LastCommits []struct {
428					Name   string
429					Commit struct{ Hash string }
430				}
431			}
432		}
433		require.NoError(t, c.Post(`query {
434			repository {
435				lastCommits(ref: "main", names: ["README.md", "main.go"]) {
436					name commit { hash }
437				}
438			}
439		}`, &resp))
440		byName := make(map[string]string)
441		for _, lc := range resp.Repository.LastCommits {
442			byName[lc.Name] = lc.Commit.Hash
443		}
444		require.Equal(t, string(c3), byName["README.md"]) // changed in c3
445		require.Equal(t, string(c2), byName["main.go"])   // changed in c2
446	})
447}
448
449func TestBugEventsSubscription(t *testing.T) {
450	repo := repository.CreateGoGitTestRepo(t, false)
451
452	mrc := cache.NewMultiRepoCache()
453	rc, events := mrc.RegisterDefaultRepository(repo)
454	for event := range events {
455		require.NoError(t, event.Err)
456	}
457
458	h := NewHandler(mrc, ServerConfig{AuthMode: "local"}, nil)
459	c := client.New(h)
460
461	sub := c.Websocket(`subscription { bugEvents { type bug { id } } }`)
462	t.Cleanup(func() { _ = sub.Close() })
463
464	rene, err := rc.Identities().New("RenΓ© Descartes", "rene@descartes.fr")
465	require.NoError(t, err)
466	require.NoError(t, rc.SetUserIdentity(rene))
467
468	b, _, err := rc.Bugs().New("test subscription", "body")
469	require.NoError(t, err)
470
471	var resp struct {
472		BugEvents struct {
473			Type string
474			Bug  struct{ Id string }
475		}
476	}
477	require.NoError(t, sub.Next(&resp))
478	assert.Equal(t, "CREATED", resp.BugEvents.Type)
479	assert.Equal(t, b.Id().String(), resp.BugEvents.Bug.Id)
480}