Files
2026-10-06 22:25:23 +02:00

242 lines
8.1 KiB
Go

package test
import (
"RenewCMS/internal/domain/update"
"RenewCMS/internal/infrastructure/useCases"
"errors"
"reflect"
"testing"
"time"
)
type fakeReleaseFeed struct {
release update.Release
err error
calls int
}
var _ update.ReleaseFeed = &fakeReleaseFeed{}
func (f *fakeReleaseFeed) Latest() (update.Release, error) {
f.calls++
if f.err != nil {
return update.Release{}, f.err
}
return f.release, nil
}
type fakeUpdateStore struct {
result update.CheckResult
found bool
saveErr error
saves int
}
var _ update.StatusStore = &fakeUpdateStore{}
func (f *fakeUpdateStore) Load() (update.CheckResult, bool) {
return f.result, f.found
}
func (f *fakeUpdateStore) Save(result update.CheckResult) error {
f.saves++
if f.saveErr != nil {
return f.saveErr
}
f.result = result
f.found = true
return nil
}
var updateTestCheckedAt = time.Date(2026, 9, 30, 9, 41, 0, 0, time.UTC)
func newStoredUpdateResult() update.CheckResult {
return update.CheckResult{
Latest: "v2.3.0-beta",
ReleaseURL: "https://git.example/fsylvain/RenewCMS/releases/tag/v2.3.0-beta",
Notes: update.ReleaseNotes{
Version: "v2.3.0-beta",
ReleasedAt: time.Date(2026, 9, 24, 8, 0, 0, 0, time.UTC),
Items: []string{"Media library"},
},
CheckedAt: updateTestCheckedAt,
}
}
func TestVersionComparison(t *testing.T) {
cases := []struct {
current, latest string
available bool
}{
{"v2.2.0-beta", "v2.3.0-beta", true},
{"v2.5.0-beta", "v2.5.0", true},
{"v2.5.0", "v2.5.0-beta", false},
{"v2.9.0", "v2.10.0", true},
{"v2.5.0-beta", "v2.5.0-beta", false},
{"v2.5.0", "v2.4.9", false},
{"dev", "v9.9.9", false},
{"v2.5.0-beta.2", "v2.5.0-beta.10", true},
{"v2.5.0-alpha", "v2.5.0-beta", true},
{"2.0.0-beta", "v2.1.0-beta", true},
{"v2.5", "v2.5.0", false},
{"v2.2.0", "garbage", false},
{"v1.0.0-1", "v1.0.0-alpha", true},
{"v1.0.0-alpha", "v1.0.0-1", false},
{"v1.0.0-beta", "v1.0.0-beta.1", true},
{"v1.0.0-beta.1", "v1.0.0-beta", false},
{"v1.0.0-beta.2", "v1.0.0-beta.1", false},
{"v1.0.0-beta", "v1.0.0-alpha", false},
{"v1.0.0", "v1.0.0", false},
{"v1.1.0", "v1.0.9", false},
}
for _, c := range cases {
t.Run(c.current+" to "+c.latest, func(t *testing.T) {
if got := update.IsNewer(c.current, c.latest); got != c.available {
t.Errorf("IsNewer(%q, %q) = %v, want %v", c.current, c.latest, got, c.available)
}
})
}
}
func TestParseVersionRejectsGarbage(t *testing.T) {
for _, raw := range []string{"", "dev", "v", "v1.x.0", "v1.2.3-", "v-1.0.0"} {
if _, err := update.ParseVersion(raw); !errors.Is(err, update.ErrInvalidVersion) {
t.Errorf("ParseVersion(%q) error = %v, want ErrInvalidVersion", raw, err)
}
}
}
func TestNewestReleaseIsChosenRegardlessOfOrder(t *testing.T) {
releases := []update.Release{{Tag: "v2.4.0-beta"}, {Tag: "v2.5.0-beta"}, {Tag: "v2.3.0-beta"}}
newest, err := update.Newest(releases)
if err != nil || newest.Tag != "v2.5.0-beta" {
t.Fatalf("Newest = %q, %v, want v2.5.0-beta", newest.Tag, err)
}
}
func TestNewestReleaseOfNothingIsAnError(t *testing.T) {
if _, err := update.Newest(nil); !errors.Is(err, update.ErrNoRelease) {
t.Errorf("error = %v, want ErrNoRelease", err)
}
if _, err := update.Newest([]update.Release{{Tag: "nightly"}}); !errors.Is(err, update.ErrNoRelease) {
t.Errorf("error = %v, want ErrNoRelease for an unparsable tag", err)
}
}
func TestReleaseNotesBecomeAPlainTextList(t *testing.T) {
published := time.Date(2026, 9, 24, 8, 0, 0, 0, time.UTC)
body := "## What's new\n- **Media** library\n- Fixed [caching](http://x) bug\n\n* Faster start"
notes := update.BuildNotes("v2.3.0-beta", published, body)
want := []string{"Media library", "Fixed caching bug", "Faster start"}
if !reflect.DeepEqual(notes.Items, want) {
t.Errorf("items = %v, want %v", notes.Items, want)
}
if !notes.ReleasedAt.Equal(published) || notes.Version != "v2.3.0-beta" {
t.Errorf("notes = %+v", notes)
}
}
func TestReleaseNotesWithoutBulletsHaveNoItems(t *testing.T) {
notes := update.BuildNotes("v1.0.0", time.Time{}, "Just an intro.\n\n**Group**\n")
if notes.Items == nil || len(notes.Items) != 0 {
t.Errorf("items = %#v, want an empty non-nil list", notes.Items)
}
}
func TestReleaseNotesStripInlineMarkdown(t *testing.T) {
notes := update.BuildNotes("v1.0.0", time.Time{}, "- Run `task build` and *enjoy* __it__")
if want := []string{"Run task build and enjoy it"}; !reflect.DeepEqual(notes.Items, want) {
t.Errorf("items = %v, want %v", notes.Items, want)
}
}
func TestGetUpdateStatusUseCase(t *testing.T) {
t.Run("never checked", func(t *testing.T) {
status := newUpdateStatusUseCase(&fakeUpdateStore{}, &fakeJobStore{}, &fakeHelperDetector{}, "v2.2.0-beta").GetStatus(true)
if status.Current != "v2.2.0-beta" || status.Latest != "" || status.Available {
t.Errorf("status = %+v", status)
}
if status.ReleaseURL != nil || status.Notes != nil || status.CheckedAt != nil || status.Progress != nil || status.Error != nil || status.StartedAt != nil {
t.Errorf("never-checked values must be nil: %+v", status)
}
if status.State != update.StateIdle || status.CanApply {
t.Errorf("state = %q, can_apply = %v", status.State, status.CanApply)
}
})
t.Run("stored result", func(t *testing.T) {
store := &fakeUpdateStore{result: newStoredUpdateResult(), found: true}
status := newUpdateStatusUseCase(store, &fakeJobStore{}, &fakeHelperDetector{}, "v2.2.0-beta").GetStatus(true)
if status.Latest != "v2.3.0-beta" || !status.Available || status.CanApply || status.State != update.StateIdle {
t.Errorf("status = %+v", status)
}
if status.ReleaseURL == nil || *status.ReleaseURL != store.result.ReleaseURL {
t.Errorf("release_url = %v", status.ReleaseURL)
}
if status.CheckedAt == nil || !status.CheckedAt.Equal(updateTestCheckedAt) {
t.Errorf("checked_at = %v", status.CheckedAt)
}
})
t.Run("dev build never reports an update", func(t *testing.T) {
store := &fakeUpdateStore{result: newStoredUpdateResult(), found: true}
if newUpdateStatusUseCase(store, &fakeJobStore{}, &fakeHelperDetector{}, "dev").GetStatus(true).Available {
t.Error("a dev build must not report an update")
}
})
}
func TestCheckForUpdateUseCase(t *testing.T) {
newRelease := update.Release{Tag: "v2.3.0-beta", URL: "https://git.example/r/v2.3.0-beta", PublishedAt: updateTestCheckedAt}
now := func() time.Time { return updateTestCheckedAt }
t.Run("stores the result and reports it", func(t *testing.T) {
store := &fakeUpdateStore{}
feed := &fakeReleaseFeed{release: newRelease}
status, err := useCases.NewCheckForUpdateUseCase(feed, store, "v2.2.0-beta", now, discardRecorder).Check(testCtx)
if err != nil || !status.Available || status.Latest != "v2.3.0-beta" {
t.Fatalf("status = %+v, err = %v", status, err)
}
if store.result.Latest != "v2.3.0-beta" || store.result.ReleaseURL != newRelease.URL || !store.result.CheckedAt.Equal(updateTestCheckedAt) {
t.Errorf("stored = %+v", store.result)
}
})
t.Run("nothing newer", func(t *testing.T) {
feed := &fakeReleaseFeed{release: update.Release{Tag: "v2.2.0-beta"}}
status, err := useCases.NewCheckForUpdateUseCase(feed, &fakeUpdateStore{}, "v2.2.0-beta", now, discardRecorder).Check(testCtx)
if err != nil || status.Available {
t.Errorf("status = %+v, err = %v", status, err)
}
})
t.Run("feed failure keeps the previous result", func(t *testing.T) {
store := &fakeUpdateStore{result: newStoredUpdateResult(), found: true}
feed := &fakeReleaseFeed{err: update.ErrFeedUnavailable}
_, err := useCases.NewCheckForUpdateUseCase(feed, store, "v2.2.0-beta", now, discardRecorder).Check(testCtx)
if !errors.Is(err, update.ErrFeedUnavailable) {
t.Errorf("err = %v", err)
}
if store.saves != 0 || store.result.Latest != "v2.3.0-beta" {
t.Errorf("store changed: %+v saves %d", store.result, store.saves)
}
})
t.Run("store failure is returned", func(t *testing.T) {
store := &fakeUpdateStore{saveErr: errRepositoryFailure}
feed := &fakeReleaseFeed{release: newRelease}
if _, err := useCases.NewCheckForUpdateUseCase(feed, store, "v2.2.0-beta", now, discardRecorder).Check(testCtx); !errors.Is(err, errRepositoryFailure) {
t.Errorf("err = %v", err)
}
})
}