249 lines
7.8 KiB
Go
249 lines
7.8 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
tea "github.com/charmbracelet/bubbletea"
|
|
)
|
|
|
|
type switchService struct {
|
|
prs []PullRequest
|
|
details PRDetails
|
|
err error
|
|
}
|
|
|
|
type countingCachedService struct {
|
|
liveDetailsCalls int
|
|
cachedDetailsCalls int
|
|
}
|
|
|
|
func (s *countingCachedService) ListPullRequests(context.Context, string, string, int, bool) ([]PullRequest, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func (s *countingCachedService) GetPullRequest(context.Context, string, string, int) (PRDetails, error) {
|
|
return PRDetails{}, nil
|
|
}
|
|
|
|
func (s *countingCachedService) LivePullRequests(context.Context, string, string, int, bool) ([]PullRequest, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func (s *countingCachedService) LivePullRequest(context.Context, string, string, int) (PRDetails, error) {
|
|
s.liveDetailsCalls++
|
|
return PRDetails{}, nil
|
|
}
|
|
|
|
func (s *countingCachedService) CachedPullRequests(string, string, int, bool) ([]PullRequest, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func (s *countingCachedService) CachedPullRequest(string, string, int) (PRDetails, error) {
|
|
s.cachedDetailsCalls++
|
|
return PRDetails{}, nil
|
|
}
|
|
|
|
func (s *switchService) ListPullRequests(context.Context, string, string, int, bool) ([]PullRequest, error) {
|
|
return s.prs, s.err
|
|
}
|
|
|
|
func (s *switchService) GetPullRequest(context.Context, string, string, int) (PRDetails, error) {
|
|
return s.details, s.err
|
|
}
|
|
|
|
func TestCachedServiceFallsBackToRecentReadData(t *testing.T) {
|
|
remote := &switchService{
|
|
prs: []PullRequest{{ID: "pr", Owner: "o", Repository: "r", Number: 1}},
|
|
details: PRDetails{PullRequest: PullRequest{ID: "pr", Owner: "o", Repository: "r", Number: 1}},
|
|
}
|
|
service := NewCachedGitHubService(remote, t.TempDir(), time.Hour)
|
|
if _, err := service.ListPullRequests(context.Background(), "o", "r", 50, false); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := service.GetPullRequest(context.Background(), "o", "r", 1); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
remote.err = errors.New("offline")
|
|
prs, err := service.ListPullRequests(context.Background(), "o", "r", 50, false)
|
|
if err != nil || len(prs) != 1 || !prs[0].FromCache || prs[0].CachedAt.IsZero() {
|
|
t.Fatalf("cached PR list = %#v, error = %v", prs, err)
|
|
}
|
|
details, err := service.GetPullRequest(context.Background(), "o", "r", 1)
|
|
if err != nil || !details.FromCache || details.CachedAt.IsZero() {
|
|
t.Fatalf("cached details = %#v, error = %v", details, err)
|
|
}
|
|
}
|
|
|
|
func TestReadStateSurvivesRestartWithUnreadComment(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "state.json")
|
|
settings := defaultAppSettings()
|
|
settings.ReadState = loadReadState(path)
|
|
m := NewAppWithSettings(nil, "o", "r", false, 50, time.Second, settings)
|
|
initial := PRDetails{
|
|
PullRequest: PullRequest{ID: "pr"},
|
|
Threads: []ReviewThread{{ID: "thread", Comments: []ReviewComment{{ID: "old"}}}},
|
|
}
|
|
m.trackThreadUpdates(initial)
|
|
updated := initial
|
|
updated.Threads[0].Comments = append(updated.Threads[0].Comments, ReviewComment{ID: "new"})
|
|
m.trackThreadUpdates(updated)
|
|
if !m.unreadThreads["thread"] {
|
|
t.Fatal("new comment was not unread before restart")
|
|
}
|
|
|
|
settings.ReadState = loadReadState(path)
|
|
restarted := NewAppWithSettings(nil, "o", "r", false, 50, time.Second, settings)
|
|
restarted.trackThreadUpdates(updated)
|
|
if !restarted.unreadThreads["thread"] {
|
|
t.Fatal("unread comment was lost across restart")
|
|
}
|
|
}
|
|
|
|
func TestCorruptReadStateIsReportedAndRecoveredEmpty(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "state.json")
|
|
if err := os.WriteFile(path, []byte("{broken"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
store := loadReadState(path)
|
|
if store.loadErr == nil || len(store.Data) != 0 {
|
|
t.Fatalf("corrupt state recovery = error %v data %#v", store.loadErr, store.Data)
|
|
}
|
|
}
|
|
|
|
func TestCachedPickerSnapshotStaysVisibleWhileLiveRefreshContinues(t *testing.T) {
|
|
m := NewApp(nil, "", "", false, 50, time.Second)
|
|
m.loading = true
|
|
cachedAt := time.Now().Add(-time.Hour)
|
|
updated, _ := m.Update(prsLoadedMsg{
|
|
cached: true,
|
|
prs: []PullRequest{{
|
|
ID: "cached", Number: 1, FromCache: true, CachedAt: cachedAt,
|
|
}},
|
|
})
|
|
m = updated.(App)
|
|
if len(m.prs) != 1 || !m.prs[0].FromCache || !m.loading {
|
|
t.Fatalf("cached snapshot was not shown during refresh: %#v", m)
|
|
}
|
|
|
|
updated, _ = m.Update(prsLoadedMsg{prs: []PullRequest{{ID: "live", Number: 2}}})
|
|
m = updated.(App)
|
|
if len(m.prs) != 1 || m.prs[0].ID != "live" || m.loading {
|
|
t.Fatalf("live response did not replace cached snapshot: %#v", m)
|
|
}
|
|
}
|
|
|
|
func TestRoutineRefreshDoesNotReplayCachedDetails(t *testing.T) {
|
|
service := &countingCachedService{}
|
|
m := NewApp(service, "o", "r", false, 50, time.Second)
|
|
pr := PullRequest{Owner: "o", Repository: "r", Number: 1}
|
|
|
|
if msg := m.loadDetails(pr, false)(); msg == nil {
|
|
t.Fatal("live refresh returned no message")
|
|
}
|
|
if service.liveDetailsCalls != 1 || service.cachedDetailsCalls != 0 {
|
|
t.Fatalf("routine refresh calls: live=%d cached=%d", service.liveDetailsCalls, service.cachedDetailsCalls)
|
|
}
|
|
|
|
msg := m.loadDetails(pr, true)()
|
|
batch, ok := msg.(tea.BatchMsg)
|
|
if !ok {
|
|
t.Fatalf("initial load command returned %T, want tea.BatchMsg", msg)
|
|
}
|
|
for _, command := range batch {
|
|
_ = command()
|
|
}
|
|
if service.liveDetailsCalls != 2 || service.cachedDetailsCalls != 1 {
|
|
t.Fatalf("initial load calls: live=%d cached=%d", service.liveDetailsCalls, service.cachedDetailsCalls)
|
|
}
|
|
}
|
|
|
|
func TestCacheDoesNotRewriteUnchangedContent(t *testing.T) {
|
|
remote := &switchService{details: PRDetails{
|
|
PullRequest: PullRequest{ID: "pr", Owner: "o", Repository: "r", Number: 1, Title: "same"},
|
|
}}
|
|
service := NewCachedGitHubService(remote, t.TempDir(), time.Hour)
|
|
if _, err := service.LivePullRequest(context.Background(), "o", "r", 1); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
path := service.file(service.pullRequestKey("o", "r", 1))
|
|
before, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
time.Sleep(2 * time.Millisecond)
|
|
if _, err := service.LivePullRequest(context.Background(), "o", "r", 1); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
after, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !bytes.Equal(before, after) {
|
|
t.Fatal("unchanged cache content was rewritten")
|
|
}
|
|
|
|
remote.details.Title = "changed"
|
|
if _, err := service.LivePullRequest(context.Background(), "o", "r", 1); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
changed, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if bytes.Equal(after, changed) {
|
|
t.Fatal("changed cache content was not persisted")
|
|
}
|
|
}
|
|
|
|
func TestCachePrunesOldestEntriesAtConfiguredBound(t *testing.T) {
|
|
remote := &switchService{}
|
|
service := NewCachedGitHubService(remote, t.TempDir(), time.Hour, 2)
|
|
for number := 1; number <= 3; number++ {
|
|
remote.details = PRDetails{PullRequest: PullRequest{
|
|
ID: "pr-" + fmtInt(number), Owner: "o", Repository: "r", Number: number,
|
|
}}
|
|
if _, err := service.LivePullRequest(context.Background(), "o", "r", number); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
time.Sleep(time.Millisecond)
|
|
}
|
|
entries, err := os.ReadDir(service.dir)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(entries) != 2 {
|
|
t.Fatalf("cache entries = %d, want 2", len(entries))
|
|
}
|
|
if _, err := service.CachedPullRequest("o", "r", 1); !errors.Is(err, os.ErrNotExist) {
|
|
t.Fatalf("oldest cache entry error = %v, want not exist", err)
|
|
}
|
|
}
|
|
|
|
func TestCacheRejectsUnknownSchemaVersion(t *testing.T) {
|
|
service := NewCachedGitHubService(&switchService{}, t.TempDir(), time.Hour)
|
|
path := service.file(service.pullRequestKey("o", "r", 1))
|
|
envelope := map[string]any{
|
|
"version": 999, "saved_at": time.Now(),
|
|
"value": PRDetails{PullRequest: PullRequest{ID: "pr"}},
|
|
}
|
|
data, err := json.Marshal(envelope)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(path, data, 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := service.CachedPullRequest("o", "r", 1); err == nil ||
|
|
!strings.Contains(err.Error(), "unsupported cache schema") {
|
|
t.Fatalf("schema error = %v", err)
|
|
}
|
|
}
|