packages.wenpai.net/internal/packagist/downloads.go
Scott Walkinshaw 3604622901
Replace shell smoke test with Go integration tests (#26)
* 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>
2026-03-19 00:38:21 -04:00

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
}