feat: signed releases, release feed, notification (read-only)
This commit is contained in:
1 parent
a49eec909c
commit
834493a4f5
49 files changed
+4191
-18
No files matched your search
@@ -0,0 +1,241 @@
|
||||
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 := useCases.NewGetUpdateStatusUseCase(&fakeUpdateStore{}, "v2.2.0-beta").GetStatus()
|
||||
|
||||
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 := useCases.NewGetUpdateStatusUseCase(store, "v2.2.0-beta").GetStatus()
|
||||
|
||||
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 useCases.NewGetUpdateStatusUseCase(store, "dev").GetStatus().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).Check()
|
||||
|
||||
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).Check()
|
||||
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).Check()
|
||||
|
||||
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).Check(); !errors.Is(err, errRepositoryFailure) {
|
||||
t.Errorf("err = %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user