blob: 207371596cfed88d2c5199156202c757fecd9cf4 [file] [log] [blame]
// Copyright 2024 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package github
import (
"context"
"net/http"
"reflect"
"testing"
"github.com/google/go-cmp/cmp"
"golang.org/x/oscar/internal/github"
"golang.org/x/oscar/internal/httprr"
"golang.org/x/oscar/internal/model"
"golang.org/x/oscar/internal/secret"
"golang.org/x/oscar/internal/storage"
"golang.org/x/oscar/internal/storage/timed"
"golang.org/x/oscar/internal/testutil"
)
func TestIssueSource(t *testing.T) {
ctx := context.Background()
check := testutil.Checker(t)
lg := testutil.Slogger(t)
db := storage.MemDB()
a := New(lg, db, nil, nil)
s := a.IssueSource()
p := &github.Issue{
URL: "https://api.github.com/repos/org/repo/issues/17",
HTMLURL: "htmlURL",
Number: 17,
Title: "title",
CreatedAt: "",
UpdatedAt: "",
ClosedAt: "",
Body: "body",
State: "open",
}
u := p.Updates()
check(u.SetTitle("t2"))
check(u.SetBody("b2"))
check(s.Update(ctx, p, u))
es := a.ic.Testing().Edits()
if len(es) != 1 {
t.Fatalf("got %d edits, want 1", len(es))
}
got := es[0]
want := &github.TestingEdit{
Project: "org/repo",
Issue: 17,
IssueChanges: &github.IssueChanges{Title: "t2", Body: "b2"},
}
if !reflect.DeepEqual(got, want) {
t.Errorf("got %+v, want %+v", got, want)
}
a.ic.Testing().ClearEdits()
c := &github.IssueComment{
URL: "https://api.github.com/repos/org/repo/issues/comments/3",
Body: "before",
}
u = c.Updates()
check(u.SetBody("after"))
check(s.Update(ctx, c, u))
es = a.ic.Testing().Edits()
if len(es) != 1 {
t.Fatalf("got %d edits, want 1", len(es))
}
got = es[0]
want = &github.TestingEdit{
Project: "org/repo",
Comment: 3,
IssueCommentChanges: &github.IssueCommentChanges{Body: "after"},
}
if !reflect.DeepEqual(got, want) {
t.Errorf("got %+v, want %+v", got, want)
}
}
func TestIssueWatcher(t *testing.T) {
// Verify that Adapter.IssueWatcher skips events.
ctx := context.Background()
check := testutil.Checker(t)
lg := testutil.Slogger(t)
db := storage.MemDB()
rr, err := httprr.Open("../../github/testdata/markdown.httprr", http.DefaultTransport)
check(err)
if rr.Recording() {
t.Fatal("record from internal/github, not here")
}
rr.ScrubReq(github.Scrub)
a := New(lg, db, secret.Empty(), rr.Client())
check(a.AddProject("rsc/markdown"))
check(a.Sync(ctx))
// Collect summaries of all events.
allItems := recentItemsFromEvents(a.ic.EventWatcher("ew"))
issueItems := recentItemsFromPosts(a.IssueWatcher("iw"))
checkIssueItems(t, allItems, issueItems)
}
func checkIssueItems(t *testing.T, allItems, issueItems []item) {
// The issueItems should be the subsequence of allItems that
// are issues and issue comments.
t.Helper()
var wantItems []item
sawIssue := false
sawComment := false
for _, it := range allItems {
if it.API == "/issues" {
wantItems = append(wantItems, it)
sawIssue = true
} else if it.API == "/issues/comments" {
wantItems = append(wantItems, it)
sawComment = true
}
}
if !sawIssue || !sawComment {
t.Fatal("missing at least one issue and one issue comment")
}
if diff := cmp.Diff(wantItems, issueItems); diff != "" {
t.Errorf("mismatch (-want, +got):\n%s", diff)
}
}
func TestReplaceWatcher(t *testing.T) {
// Verify that an event watcher can be safely replaced by an issue watcher
// with the same name, and vice versa.
ctx := context.Background()
check := testutil.Checker(t)
lg := testutil.Slogger(t)
db := storage.MemDB()
rr, err := httprr.Open("../../github/testdata/markdown.httprr", http.DefaultTransport)
check(err)
if rr.Recording() {
t.Fatal("record from internal/github, not here")
}
rr.ScrubReq(github.Scrub)
a := New(lg, db, secret.Empty(), rr.Client())
check(a.AddProject("rsc/markdown"))
check(a.Sync(ctx))
ew := a.ic.EventWatcher("w")
// Mark at least one issue and one comment old with an event watcher.
markedIssue := false
markedComment := false
for e := range ew.Recent() {
ew.MarkOld(e.DBTime)
switch x := e.Typed.(type) {
case *github.Issue:
if x.PullRequest == nil {
markedIssue = true
}
case *github.IssueComment:
markedComment = true
}
if markedIssue && markedComment {
break
}
}
// Collect everything else.
// Switch to an issue watcher with the same name.
iw := a.IssueWatcher("w")
issueItems := recentItemsFromPosts(iw)
if len(issueItems) == 0 {
t.Fatal("no issue items")
}
// The issue watcher should start from the same place as the event watcher.
allItems := recentItemsFromEvents(ew)
checkIssueItems(t, allItems, issueItems)
// Mark something old using the issue watcher.
for range iw.Recent() { // MarkOld must be called within Recent.
iw.MarkOld(issueItems[0].DBTime)
break
}
// The event watcher should skip that.
start := -1
for i, it := range allItems {
if it.API == "/issues" || it.API == "/issues/comments" {
// This is the event that we marked old above; recent
// events should begin just after.
start = i
break
}
}
if start < 0 {
t.Fatal("no issues or comments left")
}
if diff := cmp.Diff(allItems[start+1:], recentItemsFromEvents(ew)); diff != "" {
t.Errorf("mismatch (-want, +got):\n%s", diff)
}
}
// item is a summary of a github.Event or model.DBContent, for testing.
type item struct {
DBTime timed.DBTime
Issue int64
API string // github API, or "PR" for pull requests
ID int64
}
func recentItemsFromEvents(w *timed.Watcher[*github.Event]) []item {
var its []item
for e := range w.Recent() {
it := item{
DBTime: e.DBTime,
Issue: e.Issue,
API: e.API,
ID: e.ID,
}
if i, ok := e.Typed.(*github.Issue); ok {
it.ID = i.Number
if i.PullRequest != nil {
it.API = "PR"
}
} else if c, ok := e.Typed.(*github.IssueComment); ok {
it.ID = c.CommentID()
}
its = append(its, it)
}
return its
}
func recentItemsFromPosts(w model.Watcher[model.DBContent]) []item {
var its []item
for dp := range w.Recent() {
it := item{DBTime: dp.DBTime}
switch x := dp.Content.(type) {
case *github.Issue:
it.API = "/issues"
it.Issue = x.Number
it.ID = x.Number
case *github.IssueComment:
it.API = "/issues/comments"
it.Issue = x.Issue()
it.ID = x.CommentID()
default:
panic("bad post type")
}
its = append(its, it)
}
return its
}