Files
wehub-resource-sync 75f3dd141c
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CI and Release / lint-frontend (push) Has been cancelled
CI and Release / dockerfile-scan (push) Has been cancelled
CI and Release / test-frontend (push) Has been cancelled
CI and Release / lint-verification-agent (push) Has been cancelled
CI and Release / test-verification-agent (push) Has been cancelled
CI and Release / e2e-verification-agent (push) Has been cancelled
CI and Release / test-backend (push) Has been cancelled
CI and Release / build-dev-image (push) Has been cancelled
CI and Release / push-dev-image (push) Has been cancelled
CI and Release / build-image (push) Has been cancelled
CI and Release / build-verification-image (push) Has been cancelled
CI and Release / determine-version (push) Has been cancelled
CI and Release / push-image (push) Has been cancelled
CI and Release / push-verification-image (12) (push) Has been cancelled
CI and Release / lint-backend (push) Has been cancelled
CI and Release / push-verification-image (17) (push) Has been cancelled
CI and Release / push-verification-image (18) (push) Has been cancelled
CI and Release / release (push) Has been cancelled
CI and Release / publish-helm-chart (push) Has been cancelled
CI and Release / push-verification-image (13) (push) Has been cancelled
CI and Release / push-verification-image (14) (push) Has been cancelled
CI and Release / push-verification-image (15) (push) Has been cancelled
CI and Release / push-verification-image (16) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:11:07 +08:00

369 lines
9.7 KiB
Go

// Mock Databasus API server for the verification agent e2e: serves version
// checks, the verification-agent binary download, the capacity heartbeat, and
// the job protocol (claim / backup-stream / report) with fault-injection
// controls. Stdlib only — built standalone via `go build main.go`.
package main
import (
"encoding/json"
"log"
"net/http"
"os"
"sync"
"time"
)
type server struct {
mu sync.RWMutex
version string
binaryPath string
heartbeats int
lastBody map[string]any
// job protocol state
claimJob map[string]any
backupFixturePath string
reports []map[string]any
abortIDs []string
streamFailRemain int
tearStreamOnce bool
reportGone bool
}
func main() {
s := &server{version: "v1.0.0", binaryPath: "/artifacts/agent-v1"}
mux := http.NewServeMux()
mux.HandleFunc("GET /api/v1/system/version", s.handleVersion)
mux.HandleFunc("GET /api/v1/system/verification-agent", s.handleDownload)
mux.HandleFunc("POST /api/v1/agent/verification/{agentId}/heartbeat", s.handleHeartbeat)
mux.HandleFunc("POST /api/v1/agent/verifications/{agentId}/claim", s.handleClaim)
mux.HandleFunc("GET /api/v1/agent/verifications/{agentId}/{id}/backup-stream", s.handleBackupStream)
mux.HandleFunc("POST /api/v1/agent/verifications/{agentId}/{id}/report", s.handleReport)
mux.HandleFunc("POST /mock/set-version", s.handleSetVersion)
mux.HandleFunc("POST /mock/set-binary-path", s.handleSetBinaryPath)
mux.HandleFunc("GET /mock/heartbeats", s.handleHeartbeatStatus)
mux.HandleFunc("POST /mock/set-claim", s.handleSetClaim)
mux.HandleFunc("POST /mock/set-backup-fixture", s.handleSetBackupFixture)
mux.HandleFunc("POST /mock/set-abort", s.handleSetAbort)
mux.HandleFunc("POST /mock/set-stream-fail", s.handleSetStreamFail)
mux.HandleFunc("POST /mock/set-report-gone", s.handleSetReportGone)
mux.HandleFunc("GET /mock/reports", s.handleGetReports)
mux.HandleFunc("POST /mock/reset", s.handleReset)
mux.HandleFunc("GET /health", s.handleHealth)
addr := ":4050"
log.Printf("mock server starting on %s", addr)
if err := http.ListenAndServe(addr, mux); err != nil {
log.Fatalf("server failed: %v", err)
}
}
func (s *server) handleVersion(w http.ResponseWriter, _ *http.Request) {
s.mu.RLock()
version := s.version
s.mu.RUnlock()
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]string{"version": version})
}
func (s *server) handleDownload(w http.ResponseWriter, r *http.Request) {
s.mu.RLock()
path := s.binaryPath
version := s.version
s.mu.RUnlock()
log.Printf("GET verification-agent (arch=%s) -> %s", r.URL.Query().Get("arch"), path)
w.Header().Set("X-Databasus-Version", version)
http.ServeFile(w, r, path)
}
func (s *server) handleHeartbeat(w http.ResponseWriter, r *http.Request) {
var body map[string]any
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
s.mu.Lock()
s.heartbeats++
s.lastBody = body
count := s.heartbeats
abortIDs := append([]string{}, s.abortIDs...)
s.mu.Unlock()
log.Printf("POST heartbeat (agent=%s) -> count=%d", r.PathValue("agentId"), count)
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]any{
"lastSeenAt": time.Now().UTC().Format(time.RFC3339),
"abortVerificationIds": abortIDs,
})
}
func (s *server) handleClaim(w http.ResponseWriter, r *http.Request) {
s.mu.Lock()
job := s.claimJob
s.claimJob = nil // one-shot: a single verification per test run
s.mu.Unlock()
if job == nil {
w.WriteHeader(http.StatusNoContent)
return
}
log.Printf("POST claim (agent=%s) -> assigning %v", r.PathValue("agentId"), job["verificationId"])
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(job)
}
func (s *server) handleBackupStream(w http.ResponseWriter, r *http.Request) {
s.mu.Lock()
failRemain := s.streamFailRemain
if failRemain > 0 {
s.streamFailRemain--
}
tear := s.tearStreamOnce
s.tearStreamOnce = false
fixture := s.backupFixturePath
s.mu.Unlock()
if failRemain > 0 {
log.Printf("GET backup-stream -> injected 503 (remaining=%d)", failRemain-1)
w.WriteHeader(http.StatusServiceUnavailable)
return
}
if tear {
log.Printf("GET backup-stream -> tearing connection mid-body")
w.Header().Set("Content-Type", "application/octet-stream")
_, _ = w.Write([]byte("PARTIAL"))
if flusher, ok := w.(http.Flusher); ok {
flusher.Flush()
}
if hj, ok := w.(http.Hijacker); ok {
conn, _, err := hj.Hijack()
if err == nil {
_ = conn.Close()
}
}
return
}
log.Printf("GET backup-stream (id=%s) -> %s", r.PathValue("id"), fixture)
w.Header().Set("Content-Type", "application/octet-stream")
http.ServeFile(w, r, fixture)
}
func (s *server) handleReport(w http.ResponseWriter, r *http.Request) {
var body map[string]any
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
body["verificationId"] = r.PathValue("id")
s.mu.Lock()
gone := s.reportGone
if !gone {
s.reports = append(s.reports, body)
}
s.mu.Unlock()
if gone {
log.Printf("POST report (id=%s) -> 410 gone", r.PathValue("id"))
w.WriteHeader(http.StatusGone)
_, _ = w.Write([]byte(`{"reason":"gone"}`))
return
}
log.Printf("POST report (id=%s) -> status=%v exit=%v", r.PathValue("id"), body["status"], body["pgRestoreExitCode"])
w.WriteHeader(http.StatusNoContent)
}
func (s *server) handleSetVersion(w http.ResponseWriter, r *http.Request) {
var body struct {
Version string `json:"version"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
s.mu.Lock()
s.version = body.Version
s.mu.Unlock()
log.Printf("POST /mock/set-version -> %s", body.Version)
w.WriteHeader(http.StatusOK)
}
func (s *server) handleSetBinaryPath(w http.ResponseWriter, r *http.Request) {
var body struct {
BinaryPath string `json:"binaryPath"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
s.mu.Lock()
s.binaryPath = body.BinaryPath
s.mu.Unlock()
log.Printf("POST /mock/set-binary-path -> %s", body.BinaryPath)
w.WriteHeader(http.StatusOK)
}
func (s *server) handleSetClaim(w http.ResponseWriter, r *http.Request) {
var job map[string]any
if err := json.NewDecoder(r.Body).Decode(&job); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
s.mu.Lock()
s.claimJob = job
s.mu.Unlock()
log.Printf("POST /mock/set-claim -> %v", job["verificationId"])
w.WriteHeader(http.StatusOK)
}
func (s *server) handleSetBackupFixture(w http.ResponseWriter, r *http.Request) {
var body struct {
Path string `json:"path"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if _, err := os.Stat(body.Path); err != nil {
http.Error(w, "fixture not found: "+err.Error(), http.StatusBadRequest)
return
}
s.mu.Lock()
s.backupFixturePath = body.Path
s.mu.Unlock()
log.Printf("POST /mock/set-backup-fixture -> %s", body.Path)
w.WriteHeader(http.StatusOK)
}
func (s *server) handleSetAbort(w http.ResponseWriter, r *http.Request) {
var body struct {
IDs []string `json:"ids"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
s.mu.Lock()
s.abortIDs = body.IDs
s.mu.Unlock()
log.Printf("POST /mock/set-abort -> %v", body.IDs)
w.WriteHeader(http.StatusOK)
}
func (s *server) handleSetStreamFail(w http.ResponseWriter, r *http.Request) {
var body struct {
Count int `json:"count"`
TearStreamOnce bool `json:"tearStreamOnce"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
s.mu.Lock()
s.streamFailRemain = body.Count
s.tearStreamOnce = body.TearStreamOnce
s.mu.Unlock()
log.Printf("POST /mock/set-stream-fail -> count=%d tear=%v", body.Count, body.TearStreamOnce)
w.WriteHeader(http.StatusOK)
}
func (s *server) handleSetReportGone(w http.ResponseWriter, r *http.Request) {
var body struct {
Gone bool `json:"gone"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
s.mu.Lock()
s.reportGone = body.Gone
s.mu.Unlock()
log.Printf("POST /mock/set-report-gone -> %v", body.Gone)
w.WriteHeader(http.StatusOK)
}
func (s *server) handleGetReports(w http.ResponseWriter, _ *http.Request) {
s.mu.RLock()
defer s.mu.RUnlock()
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]any{
"count": len(s.reports),
"reports": s.reports,
})
}
func (s *server) handleHeartbeatStatus(w http.ResponseWriter, _ *http.Request) {
s.mu.RLock()
defer s.mu.RUnlock()
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]any{
"count": s.heartbeats,
"last": s.lastBody,
})
}
// handleReset wipes per-test state (reports, claim, abort/stream/report-gone
// flags, fixture path). Version + binary path are left alone — pin them via
// /mock/set-version + /mock/set-binary-path. Each restore-style e2e test
// should call this at the top to avoid seeing prior-test artifacts.
func (s *server) handleReset(w http.ResponseWriter, _ *http.Request) {
s.mu.Lock()
s.reports = nil
s.claimJob = nil
s.backupFixturePath = ""
s.abortIDs = nil
s.streamFailRemain = 0
s.tearStreamOnce = false
s.reportGone = false
s.heartbeats = 0
s.lastBody = nil
s.mu.Unlock()
log.Printf("POST /mock/reset -> state wiped")
w.WriteHeader(http.StatusOK)
}
func (s *server) handleHealth(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("ok"))
}