* Replace shell smoke test with Go integration tests Replaces the 480-line bash smoke test with Go integration tests that are deterministic, fast, and self-contained. The new test infrastructure uses fixture data and a mock wp.org server so tests run without network dependencies on the WordPress.org API. New files: - Mock wp.org server (internal/wporg/mock_server.go) + API fixtures - Test DB helpers (internal/testutil/testdb.go) for in-memory SQLite - Integration smoke test covering full pipeline, HTTP endpoints, composer install, version pinning, and build integrity - R2 sync test using gofakes3 for S3-compat verification - Live canary test for nightly runs against real wp.org - Fixture capture script (test/capture-fixtures.sh) - Canary CI workflow (.github/workflows/canary.yml) Changes: - wporg.Client now supports configurable base URL via SetBaseURL() - S3 client uses path-style addressing (needed for R2 + test compat) - CI workflow gains integration test job (stub CSS, no Tailwind needed) - Makefile: smoke target replaced with integration target Deleted: test/smoke_test.sh, .github/workflows/smoke-test.yml Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Allow insecure HTTP for composer in integration tests The httptest server uses plain HTTP (http://127.0.0.1:...) which Composer's secure-http default blocks. Add secure-http: false to the generated composer.json config. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
78 lines
1.7 KiB
Go
78 lines
1.7 KiB
Go
package packagist
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"log/slog"
|
|
"net/http"
|
|
"sync/atomic"
|
|
"time"
|
|
)
|
|
|
|
// DownloadsCache fetches and caches total download counts from packagist.org.
|
|
type DownloadsCache struct {
|
|
value atomic.Int64
|
|
logger *slog.Logger
|
|
}
|
|
|
|
func NewDownloadsCache(logger *slog.Logger) *DownloadsCache {
|
|
c := &DownloadsCache{logger: logger}
|
|
c.value.Store(0)
|
|
c.fetch()
|
|
go c.loop()
|
|
return c
|
|
}
|
|
|
|
// NewStubCache returns a DownloadsCache that never fetches, for use in tests.
|
|
func NewStubCache() *DownloadsCache {
|
|
c := &DownloadsCache{logger: slog.Default()}
|
|
c.value.Store(0)
|
|
return c
|
|
}
|
|
|
|
func (c *DownloadsCache) Total() int64 {
|
|
return c.value.Load()
|
|
}
|
|
|
|
func (c *DownloadsCache) loop() {
|
|
ticker := time.NewTicker(1 * time.Hour)
|
|
for range ticker.C {
|
|
c.fetch()
|
|
}
|
|
}
|
|
|
|
func (c *DownloadsCache) fetch() {
|
|
total, err := fetchDownloads("roots/wordpress")
|
|
if err != nil {
|
|
c.logger.Warn("packagist downloads fetch failed", "error", err)
|
|
return
|
|
}
|
|
c.value.Store(total)
|
|
c.logger.Info("packagist downloads updated", "total", total)
|
|
}
|
|
|
|
func fetchDownloads(pkg string) (int64, error) {
|
|
resp, err := http.Get(fmt.Sprintf("https://packagist.org/packages/%s/downloads.json", pkg))
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
defer func() { _ = resp.Body.Close() }()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return 0, fmt.Errorf("packagist returned %d", resp.StatusCode)
|
|
}
|
|
|
|
var data struct {
|
|
Package struct {
|
|
Downloads struct {
|
|
Total struct {
|
|
Total int64 `json:"total"`
|
|
} `json:"total"`
|
|
} `json:"downloads"`
|
|
} `json:"package"`
|
|
}
|
|
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
|
|
return 0, err
|
|
}
|
|
return data.Package.Downloads.Total.Total, nil
|
|
}
|