Initial commit of Descrybe v2 without local scratch artifacts.
Drop one-shot tmp/axe scripts and agent i18n scratch so the Gitea tree is deployable.
This commit is contained in:
@@ -0,0 +1,330 @@
|
||||
// Command mock-woo serves minimal WooCommerce REST API v3 fixtures for local
|
||||
// live sync proofs (Test Connection, product batch push, orders/reviews pull).
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// go run ./cmd/mock-woo -addr 127.0.0.1:19090
|
||||
// go run ./cmd/mock-woo -addr 127.0.0.1:19090 -key ck_mock -secret cs_mock
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
const apiPrefix = "/wp-json/wc/v3"
|
||||
|
||||
type server struct {
|
||||
key string
|
||||
secret string
|
||||
mu sync.Mutex
|
||||
nextID atomic.Int64
|
||||
bySKU map[string]product
|
||||
logReq atomic.Int64
|
||||
}
|
||||
|
||||
type product struct {
|
||||
ID int `json:"id"`
|
||||
SKU string `json:"sku"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type orderBilling struct {
|
||||
Email string `json:"email"`
|
||||
FirstName string `json:"first_name"`
|
||||
LastName string `json:"last_name"`
|
||||
}
|
||||
|
||||
type orderLine struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
ProductID int `json:"product_id"`
|
||||
Quantity int `json:"quantity"`
|
||||
Total string `json:"total"`
|
||||
SKU string `json:"sku"`
|
||||
MetaData []any `json:"meta_data"`
|
||||
}
|
||||
|
||||
type order struct {
|
||||
ID int `json:"id"`
|
||||
Status string `json:"status"`
|
||||
Currency string `json:"currency"`
|
||||
Total string `json:"total"`
|
||||
CustomerID int `json:"customer_id"`
|
||||
DateCreated string `json:"date_created"`
|
||||
DateCreatedGMT string `json:"date_created_gmt"`
|
||||
Billing orderBilling `json:"billing"`
|
||||
LineItems []orderLine `json:"line_items"`
|
||||
}
|
||||
|
||||
type review struct {
|
||||
ID int `json:"id"`
|
||||
ProductID int `json:"product_id"`
|
||||
Status string `json:"status"`
|
||||
Reviewer string `json:"reviewer"`
|
||||
ReviewerEmail string `json:"reviewer_email"`
|
||||
Review string `json:"review"`
|
||||
Rating int `json:"rating"`
|
||||
DateCreated string `json:"date_created"`
|
||||
DateCreatedGMT string `json:"date_created_gmt"`
|
||||
ProductName string `json:"product_name"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
addr := flag.String("addr", "127.0.0.1:19090", "listen address")
|
||||
key := flag.String("key", envOr("MOCK_WOO_KEY", "ck_mock_local"), "consumer key")
|
||||
secret := flag.String("secret", envOr("MOCK_WOO_SECRET", "cs_mock_local"), "consumer secret")
|
||||
flag.Parse()
|
||||
|
||||
s := &server{
|
||||
key: strings.TrimSpace(*key),
|
||||
secret: strings.TrimSpace(*secret),
|
||||
bySKU: map[string]product{},
|
||||
}
|
||||
s.nextID.Store(1000)
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/", s.handle)
|
||||
mux.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"status":"ok","service":"mock-woo"}`))
|
||||
})
|
||||
|
||||
log.Printf("mock-woo listening on http://%s key=%s", *addr, s.key)
|
||||
log.Printf("WC base: http://%s%s", *addr, apiPrefix)
|
||||
if err := http.ListenAndServe(*addr, mux); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func envOr(k, def string) string {
|
||||
if v := strings.TrimSpace(os.Getenv(k)); v != "" {
|
||||
return v
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
func (s *server) handle(w http.ResponseWriter, r *http.Request) {
|
||||
s.logReq.Add(1)
|
||||
if !strings.HasPrefix(r.URL.Path, apiPrefix) {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
if !s.authOK(r) {
|
||||
w.Header().Set("WWW-Authenticate", `Basic realm="WooCommerce"`)
|
||||
http.Error(w, `{"code":"woocommerce_rest_cannot_view","message":"unauthorized"}`, http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
path := strings.TrimPrefix(r.URL.Path, apiPrefix)
|
||||
path = strings.TrimSuffix(path, "/")
|
||||
switch {
|
||||
case r.Method == http.MethodGet && path == "/products":
|
||||
s.handleListProducts(w, r)
|
||||
case r.Method == http.MethodPost && path == "/products/batch":
|
||||
s.handleBatchProducts(w, r)
|
||||
case r.Method == http.MethodGet && path == "/products/categories":
|
||||
s.writeJSON(w, []map[string]any{
|
||||
{"id": 10, "name": "Demo Electronics", "slug": "demo-electronics"},
|
||||
{"id": 11, "name": "Accessories", "slug": "accessories"},
|
||||
})
|
||||
case r.Method == http.MethodGet && path == "/products/attributes":
|
||||
s.writeJSON(w, []map[string]any{
|
||||
{"id": 20, "name": "Color", "slug": "pa_color"},
|
||||
{"id": 21, "name": "Size", "slug": "pa_size"},
|
||||
})
|
||||
case r.Method == http.MethodGet && path == "/orders":
|
||||
s.handleOrders(w, r)
|
||||
case r.Method == http.MethodGet && path == "/products/reviews":
|
||||
s.handleReviews(w, r)
|
||||
default:
|
||||
http.Error(w, `{"code":"rest_no_route","message":"no route"}`, http.StatusNotFound)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *server) authOK(r *http.Request) bool {
|
||||
h := r.Header.Get("Authorization")
|
||||
if !strings.HasPrefix(h, "Basic ") {
|
||||
return false
|
||||
}
|
||||
raw, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(h, "Basic "))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
parts := strings.SplitN(string(raw), ":", 2)
|
||||
if len(parts) != 2 {
|
||||
return false
|
||||
}
|
||||
return parts[0] == s.key && parts[1] == s.secret
|
||||
}
|
||||
|
||||
func (s *server) handleListProducts(w http.ResponseWriter, r *http.Request) {
|
||||
sku := strings.TrimSpace(r.URL.Query().Get("sku"))
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
out := make([]product, 0)
|
||||
if sku != "" {
|
||||
if p, ok := s.bySKU[sku]; ok {
|
||||
out = append(out, p)
|
||||
}
|
||||
} else {
|
||||
for _, p := range s.bySKU {
|
||||
out = append(out, p)
|
||||
if len(out) >= 1 {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
s.writeJSON(w, out)
|
||||
}
|
||||
|
||||
func (s *server) handleBatchProducts(w http.ResponseWriter, r *http.Request) {
|
||||
body, err := io.ReadAll(io.LimitReader(r.Body, 8<<20))
|
||||
if err != nil {
|
||||
http.Error(w, `{"message":"read body"}`, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
var req struct {
|
||||
Create []map[string]any `json:"create"`
|
||||
Update []map[string]any `json:"update"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &req); err != nil {
|
||||
http.Error(w, `{"message":"invalid json"}`, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
created := make([]product, 0, len(req.Create))
|
||||
updated := make([]product, 0, len(req.Update))
|
||||
for _, item := range req.Create {
|
||||
p := s.upsertFromPayload(item, 0)
|
||||
created = append(created, p)
|
||||
}
|
||||
for _, item := range req.Update {
|
||||
id := intFromAny(item["id"])
|
||||
p := s.upsertFromPayload(item, id)
|
||||
updated = append(updated, p)
|
||||
}
|
||||
s.writeJSON(w, map[string]any{"create": created, "update": updated})
|
||||
}
|
||||
|
||||
func (s *server) upsertFromPayload(item map[string]any, preferID int) product {
|
||||
sku, _ := item["sku"].(string)
|
||||
name, _ := item["name"].(string)
|
||||
if name == "" {
|
||||
name = "Product"
|
||||
}
|
||||
id := preferID
|
||||
if id <= 0 {
|
||||
if existing, ok := s.bySKU[sku]; ok && sku != "" {
|
||||
id = existing.ID
|
||||
} else {
|
||||
id = int(s.nextID.Add(1))
|
||||
}
|
||||
}
|
||||
p := product{ID: id, SKU: sku, Name: name}
|
||||
if sku != "" {
|
||||
s.bySKU[sku] = p
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
func (s *server) handleOrders(w http.ResponseWriter, r *http.Request) {
|
||||
page, _ := strconv.Atoi(r.URL.Query().Get("page"))
|
||||
if page <= 0 {
|
||||
page = 1
|
||||
}
|
||||
if page > 1 {
|
||||
s.writeJSON(w, []order{})
|
||||
return
|
||||
}
|
||||
now := time.Now().UTC().Format("2006-01-02T15:04:05")
|
||||
orders := []order{
|
||||
{
|
||||
ID: 5001, Status: "completed", Currency: "EUR", Total: "499.00", CustomerID: 1,
|
||||
DateCreated: now, DateCreatedGMT: now,
|
||||
Billing: orderBilling{Email: "anna.buyer@example.com", FirstName: "Anna", LastName: "Buyer"},
|
||||
LineItems: []orderLine{{
|
||||
ID: 1, Name: "Mock 4K TV", ProductID: 90001, Quantity: 1, Total: "499.00", SKU: "MOCK-WOO-TV",
|
||||
MetaData: []any{map[string]any{"key": "categories", "value": []any{"Demo Electronics"}}},
|
||||
}},
|
||||
},
|
||||
{
|
||||
ID: 5002, Status: "completed", Currency: "EUR", Total: "149.00", CustomerID: 2,
|
||||
DateCreated: now, DateCreatedGMT: now,
|
||||
Billing: orderBilling{Email: "ben.buyer@example.com", FirstName: "Ben", LastName: "Buyer"},
|
||||
LineItems: []orderLine{{
|
||||
ID: 2, Name: "Mock Soundbar", ProductID: 90002, Quantity: 1, Total: "149.00", SKU: "MOCK-WOO-SOUND",
|
||||
MetaData: []any{map[string]any{"key": "categories", "value": []any{"Demo Electronics"}}},
|
||||
}},
|
||||
},
|
||||
{
|
||||
ID: 5003, Status: "processing", Currency: "EUR", Total: "29.00", CustomerID: 3,
|
||||
DateCreated: now, DateCreatedGMT: now,
|
||||
Billing: orderBilling{Email: "cara.buyer@example.com", FirstName: "Cara", LastName: "Buyer"},
|
||||
LineItems: []orderLine{{
|
||||
ID: 3, Name: "Mock Cable", ProductID: 90010, Quantity: 1, Total: "29.00", SKU: "MOCK-WOO-CABLE",
|
||||
MetaData: []any{map[string]any{"key": "categories", "value": []any{"Accessories"}}},
|
||||
}},
|
||||
},
|
||||
}
|
||||
s.writeJSON(w, orders)
|
||||
}
|
||||
|
||||
func (s *server) handleReviews(w http.ResponseWriter, r *http.Request) {
|
||||
page, _ := strconv.Atoi(r.URL.Query().Get("page"))
|
||||
if page <= 0 {
|
||||
page = 1
|
||||
}
|
||||
if page > 1 {
|
||||
s.writeJSON(w, []review{})
|
||||
return
|
||||
}
|
||||
now := time.Now().UTC().Format("2006-01-02T15:04:05")
|
||||
s.writeJSON(w, []review{
|
||||
{
|
||||
ID: 7001, ProductID: 90001, Status: "approved", Reviewer: "Anna Buyer",
|
||||
ReviewerEmail: "anna.buyer@example.com", Review: "Great mock TV.", Rating: 5,
|
||||
DateCreated: now, DateCreatedGMT: now, ProductName: "Mock 4K TV",
|
||||
},
|
||||
{
|
||||
ID: 7002, ProductID: 90002, Status: "approved", Reviewer: "Ben Buyer",
|
||||
ReviewerEmail: "ben.buyer@example.com", Review: "Solid soundbar for demos.", Rating: 4,
|
||||
DateCreated: now, DateCreatedGMT: now, ProductName: "Mock Soundbar",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (s *server) writeJSON(w http.ResponseWriter, v any) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
enc := json.NewEncoder(w)
|
||||
if err := enc.Encode(v); err != nil {
|
||||
log.Printf("encode: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func intFromAny(v any) int {
|
||||
switch t := v.(type) {
|
||||
case float64:
|
||||
return int(t)
|
||||
case int:
|
||||
return t
|
||||
case json.Number:
|
||||
i, _ := t.Int64()
|
||||
return int(i)
|
||||
case string:
|
||||
i, _ := strconv.Atoi(t)
|
||||
return i
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user