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,183 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"RenewCMS/internal/domain/update"
|
||||
infraUpdate "RenewCMS/internal/infrastructure/update"
|
||||
"crypto/ed25519"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
const giteaReleasesPath = "/api/v1/repos/fsylvain/RenewCMS/releases"
|
||||
|
||||
func newGiteaFeedServer(t *testing.T, status int, body string) (*httptest.Server, *[]string) {
|
||||
t.Helper()
|
||||
var paths []string
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
paths = append(paths, r.URL.Path)
|
||||
w.WriteHeader(status)
|
||||
_, _ = w.Write([]byte(body))
|
||||
}))
|
||||
t.Cleanup(server.Close)
|
||||
return server, &paths
|
||||
}
|
||||
|
||||
func TestGiteaReleaseFeed(t *testing.T) {
|
||||
list := `[
|
||||
{"tag_name":"v2.4.0-beta","html_url":"https://git.example/r/v2.4.0-beta","body":"- Four","draft":false,"prerelease":true,"published_at":"2026-08-01T10:00:00Z"},
|
||||
{"tag_name":"v9.0.0","html_url":"https://git.example/r/v9.0.0","body":"- Draft","draft":true,"prerelease":false,"published_at":"2026-09-30T10:00:00Z"},
|
||||
{"tag_name":"v2.5.0-beta","html_url":"https://git.example/r/v2.5.0-beta","body":"## New\n- **Media** library\n- Fixed [caching](http://x) bug","draft":false,"prerelease":true,"published_at":"2026-09-24T08:30:00Z"},
|
||||
{"tag_name":"v2.3.0-beta","html_url":"https://git.example/r/v2.3.0-beta","body":"","draft":false,"prerelease":true,"published_at":"2026-07-01T10:00:00Z"}
|
||||
]`
|
||||
|
||||
t.Run("requests the release list and returns the newest published release", func(t *testing.T) {
|
||||
server, paths := newGiteaFeedServer(t, http.StatusOK, list)
|
||||
|
||||
release, err := infraUpdate.NewGiteaReleaseFeed(server.URL + "/").Latest()
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Latest: %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(*paths, []string{giteaReleasesPath}) {
|
||||
t.Errorf("paths = %v", *paths)
|
||||
}
|
||||
if release.Tag != "v2.5.0-beta" || release.URL != "https://git.example/r/v2.5.0-beta" {
|
||||
t.Errorf("release = %+v", release)
|
||||
}
|
||||
if want := []string{"Media library", "Fixed caching bug"}; !reflect.DeepEqual(release.Notes.Items, want) {
|
||||
t.Errorf("items = %v", release.Notes.Items)
|
||||
}
|
||||
if want := time.Date(2026, 9, 24, 8, 30, 0, 0, time.UTC); !release.PublishedAt.Equal(want) || !release.Notes.ReleasedAt.Equal(want) {
|
||||
t.Errorf("published = %v, notes released = %v", release.PublishedAt, release.Notes.ReleasedAt)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a final release beats a beta of the same number", func(t *testing.T) {
|
||||
server, _ := newGiteaFeedServer(t, http.StatusOK, `[{"tag_name":"v2.6.0-beta","published_at":"2026-09-01T00:00:00Z"},{"tag_name":"v2.6.0","published_at":"2026-09-02T00:00:00Z"}]`)
|
||||
release, err := infraUpdate.NewGiteaReleaseFeed(server.URL).Latest()
|
||||
if err != nil || release.Tag != "v2.6.0" {
|
||||
t.Errorf("release = %+v, err = %v", release, err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("only drafts is no release", func(t *testing.T) {
|
||||
server, _ := newGiteaFeedServer(t, http.StatusOK, `[{"tag_name":"v9.0.0","draft":true}]`)
|
||||
if _, err := infraUpdate.NewGiteaReleaseFeed(server.URL).Latest(); !errors.Is(err, update.ErrNoRelease) {
|
||||
t.Errorf("err = %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a 500 is a feed error", func(t *testing.T) {
|
||||
server, _ := newGiteaFeedServer(t, http.StatusInternalServerError, "boom")
|
||||
if _, err := infraUpdate.NewGiteaReleaseFeed(server.URL).Latest(); !errors.Is(err, update.ErrFeedUnavailable) {
|
||||
t.Errorf("err = %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("invalid json is a feed error", func(t *testing.T) {
|
||||
server, _ := newGiteaFeedServer(t, http.StatusOK, "not json")
|
||||
if _, err := infraUpdate.NewGiteaReleaseFeed(server.URL).Latest(); !errors.Is(err, update.ErrFeedUnavailable) {
|
||||
t.Errorf("err = %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("an unreachable server is a feed error", func(t *testing.T) {
|
||||
server, _ := newGiteaFeedServer(t, http.StatusOK, "[]")
|
||||
url := server.URL
|
||||
server.Close()
|
||||
if _, err := infraUpdate.NewGiteaReleaseFeed(url).Latest(); !errors.Is(err, update.ErrFeedUnavailable) {
|
||||
t.Errorf("err = %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestJSONStatusStore(t *testing.T) {
|
||||
t.Run("The stored result survives a restart", func(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "data", "update-status.json")
|
||||
saved := newStoredUpdateResult()
|
||||
if err := infraUpdate.NewJSONStatusStore(path).Save(saved); err != nil {
|
||||
t.Fatalf("Save: %v", err)
|
||||
}
|
||||
|
||||
loaded, found := infraUpdate.NewJSONStatusStore(path).Load()
|
||||
|
||||
if !found {
|
||||
t.Fatal("expected a stored result")
|
||||
}
|
||||
if loaded.Latest != saved.Latest || loaded.ReleaseURL != saved.ReleaseURL || !loaded.CheckedAt.Equal(saved.CheckedAt) {
|
||||
t.Errorf("loaded = %+v", loaded)
|
||||
}
|
||||
if !reflect.DeepEqual(loaded.Notes.Items, saved.Notes.Items) {
|
||||
t.Errorf("items = %v", loaded.Notes.Items)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a second save replaces the first", func(t *testing.T) {
|
||||
store := infraUpdate.NewJSONStatusStore(filepath.Join(t.TempDir(), "update-status.json"))
|
||||
first := newStoredUpdateResult()
|
||||
second := newStoredUpdateResult()
|
||||
second.Latest = "v2.4.0-beta"
|
||||
_ = store.Save(first)
|
||||
_ = store.Save(second)
|
||||
if loaded, _ := store.Load(); loaded.Latest != "v2.4.0-beta" {
|
||||
t.Errorf("latest = %q", loaded.Latest)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a missing file reads as no result", func(t *testing.T) {
|
||||
if _, found := infraUpdate.NewJSONStatusStore(filepath.Join(t.TempDir(), "absent.json")).Load(); found {
|
||||
t.Error("a missing file must read as no result")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a corrupt file reads as no result", func(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "update-status.json")
|
||||
if err := os.WriteFile(path, []byte("{not json"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, found := infraUpdate.NewJSONStatusStore(path).Load(); found {
|
||||
t.Error("a corrupt file must read as no result")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("an unwritable location is an error", func(t *testing.T) {
|
||||
blocker := filepath.Join(t.TempDir(), "file")
|
||||
if err := os.WriteFile(blocker, nil, 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := infraUpdate.NewJSONStatusStore(filepath.Join(blocker, "update-status.json")).Save(newStoredUpdateResult()); err == nil {
|
||||
t.Error("expected an error")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestCommittedReleasePublicKeyHasTheRightShape(t *testing.T) {
|
||||
content, err := os.ReadFile(filepath.Join("..", "internal", "infrastructure", "update", "release.pub"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
decoded, err := base64.StdEncoding.DecodeString(strings.TrimSpace(string(content)))
|
||||
if err != nil || len(decoded) != ed25519.PublicKeySize {
|
||||
t.Fatalf("release.pub must be base64 of %d bytes: %v", ed25519.PublicKeySize, err)
|
||||
}
|
||||
if _, err := infraUpdate.EmbeddedPublicKey(); err != nil {
|
||||
t.Errorf("embedded key: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGiteaReleaseFeedRejectsAnOversizedBody(t *testing.T) {
|
||||
huge := `[{"tag_name":"v2.5.0","html_url":"u","body":"` + strings.Repeat("a", 5<<20) + `","draft":false,"published_at":"2026-09-24T08:30:00Z"}]`
|
||||
server, _ := newGiteaFeedServer(t, http.StatusOK, huge)
|
||||
|
||||
if _, err := infraUpdate.NewGiteaReleaseFeed(server.URL).Latest(); !errors.Is(err, update.ErrFeedUnavailable) {
|
||||
t.Fatalf("Latest error = %v, want ErrFeedUnavailable", err)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user