// 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 } }