Files
RenewCMS/test/update_feed_test.go

184 lines
6.9 KiB
Go

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)
}
}