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