packages.wenpai.net/internal/wporg/mock_server.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

92 lines
2.5 KiB
Go

package wporg
import (
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
)
// NewMockServer returns an httptest.Server that serves fixtures from fixtureDir.
// Routes:
// - GET /plugins/info/1.2/?...&request[slug]=X → testdata/plugins/X.json
// - GET /themes/info/1.2/?...&request[slug]=X → testdata/themes/X.json
//
// Returns 404 for unknown slugs. Handles both full info and last_updated-only requests.
func NewMockServer(fixtureDir string) *httptest.Server {
mux := http.NewServeMux()
mux.HandleFunc("/plugins/info/1.2/", func(w http.ResponseWriter, r *http.Request) {
serveFixture(w, r, filepath.Join(fixtureDir, "plugins"))
})
mux.HandleFunc("/themes/info/1.2/", func(w http.ResponseWriter, r *http.Request) {
serveFixture(w, r, filepath.Join(fixtureDir, "themes"))
})
return httptest.NewServer(mux)
}
func serveFixture(w http.ResponseWriter, r *http.Request, dir string) {
slug := extractSlug(r)
if slug == "" {
http.Error(w, `{"error":"missing slug"}`, http.StatusBadRequest)
return
}
data, err := os.ReadFile(filepath.Join(dir, slug+".json"))
if err != nil {
w.WriteHeader(http.StatusNotFound)
_, _ = w.Write([]byte(`{"error":"Plugin not found.","slug":"` + slug + `"}`))
return
}
// If the request only asks for last_updated, return a minimal response
if isLastUpdatedOnly(r) {
var full map[string]any
if err := json.Unmarshal(data, &full); err == nil {
minimal := map[string]any{
"last_updated": full["last_updated"],
"slug": full["slug"],
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(minimal)
return
}
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write(data)
}
func extractSlug(r *http.Request) string {
// Check query string (GET-style URLs used by our client)
q := r.URL.RawQuery
if slug := extractParam(q, "request%5Bslug%5D"); slug != "" {
return slug
}
if slug := extractParam(q, "request[slug]"); slug != "" {
return slug
}
return ""
}
func extractParam(query, key string) string {
idx := strings.Index(query, key+"=")
if idx < 0 {
return ""
}
val := query[idx+len(key)+1:]
if end := strings.IndexByte(val, '&'); end >= 0 {
val = val[:end]
}
return val
}
func isLastUpdatedOnly(r *http.Request) bool {
q := r.URL.RawQuery
// Check if versions=false is in the query (indicates a minimal last_updated-only request)
return strings.Contains(q, "versions%5D=false") || strings.Contains(q, "versions]=false")
}