From 93ebcf440696441da9b7c4fa613dbf89e5b5f8a1 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 23 Jul 2026 19:02:03 +0300 Subject: [PATCH] Initial commit: wb-mock skeleton --- .gitignore | 3 ++ AGENTS.md | 60 +++++++++++++++++++++++++++++++++++ Dockerfile | 11 +++++++ Makefile | 25 +++++++++++++++ cmd/server/main.go | 38 ++++++++++++++++++++++ go.mod | 3 ++ internal/config/config.go | 22 +++++++++++++ internal/handlers/handlers.go | 22 +++++++++++++ internal/middleware/auth.go | 33 +++++++++++++++++++ 9 files changed, 217 insertions(+) create mode 100644 .gitignore create mode 100644 AGENTS.md create mode 100644 Dockerfile create mode 100644 Makefile create mode 100644 cmd/server/main.go create mode 100644 go.mod create mode 100644 internal/config/config.go create mode 100644 internal/handlers/handlers.go create mode 100644 internal/middleware/auth.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..87a5eef --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +wb-mock +*.log +.DS_Store diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..d9aeb13 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,60 @@ +# AGENTS.md — WB Mock Server — Project Context + +> **Read this first.** Append what you did at the end under **Session Log**. + +## 1. What this is + +Mock server emulating Wildberries Seller API for integration testing. Returns hardcoded fixture data. + +| Layer | Tech | +|-------|------| +| Language | Go 1.25, `net/http` stdlib | +| Deployment | Docker + systemd on ioffe | +| Auth | Optional Api-Key validation (WB Authorization header) | +| Config | Env vars: `PORT`, `WB_API_KEY` | + +## 2. Endpoints (TBD by agent) + +| Method | Path | Description | +|--------|------|-------------| +| GET | `/health` | Health check | + +## 3. Test Data + +| Entity | Count | +|--------|-------| +| Products | TBD | +| Stocks | TBD | +| Prices | TBD | + +## 4. Deploy + +```bash +cd /Users/rinat/projects/wb-mock +CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o wb-mock ./cmd/server +scp wb-mock ioffe:/opt/wb-mock/wb-mock +ssh ioffe "sudo systemctl restart wb-mock" +curl https://wb-mock.px.kinrin.ru/health # expect {"status":"ok"} +``` + +## 5. Key Commands + +```bash +make build # binary +make test # go test -race ./... +make docker # Docker build +go run ./cmd/server # local on :8080 +``` + +## 6. Wildberries API Auth + +WB Seller API uses `Authorization: ` header (no Client-Id). + +## 7. Reference + +Ozon mock sibling project: `/Users/rinat/projects/ozon-mock` — same architecture, 20 endpoints, 31 tests. +Use it as a reference for patterns: config loading, fixture generation, handler structure, filter logic. + +## 8. Session Log + +> Append your summary below. Latest first. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e2bcc7a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,11 @@ +FROM alpine:3.20 + +RUN apk --no-cache add ca-certificates tzdata + +COPY wb-mock /usr/local/bin/wb-mock + +EXPOSE 8080 + +ENV PORT=8080 + +CMD ["/usr/local/bin/wb-mock"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..1af4545 --- /dev/null +++ b/Makefile @@ -0,0 +1,25 @@ +.PHONY: build test docker run clean vet lint + +build: + CGO_ENABLED=0 go build -o wb-mock ./cmd/server + +test: + go test -v -race ./... + +vet: + go vet ./... + +lint: + go vet ./... + +run: + go run ./cmd/server + +docker: + CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o wb-mock ./cmd/server + docker build -t wb-mock . + +clean: + rm -f wb-mock + +all: vet test build diff --git a/cmd/server/main.go b/cmd/server/main.go new file mode 100644 index 0000000..1dbf054 --- /dev/null +++ b/cmd/server/main.go @@ -0,0 +1,38 @@ +package main + +import ( + "log" + "net/http" + + "github.com/rinat/wb-mock/internal/config" + "github.com/rinat/wb-mock/internal/middleware" +) + +func main() { + cfg := config.Load() + + mux := http.NewServeMux() + + auth := func(h http.HandlerFunc) http.HandlerFunc { + return middleware.Auth(cfg.APIKey, h) + } + + // Health + mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(`{"status":"ok"}`)) + }) + + _ = auth + + // 404 handler + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusNotFound) + w.Write([]byte(`{"code":404,"message":"not found"}`)) + }) + + addr := ":" + cfg.Port + log.Printf("Starting wb-mock on %s", addr) + log.Fatal(http.ListenAndServe(addr, mux)) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..371ed0d --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/rinat/wb-mock + +go 1.25.0 diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..c3548db --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,22 @@ +package config + +import "os" + +type Config struct { + Port string + APIKey string +} + +func Load() *Config { + return &Config{ + Port: getEnv("PORT", "8080"), + APIKey: os.Getenv("WB_API_KEY"), + } +} + +func getEnv(key, defaultVal string) string { + if val := os.Getenv(key); val != "" { + return val + } + return defaultVal +} diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go new file mode 100644 index 0000000..678b825 --- /dev/null +++ b/internal/handlers/handlers.go @@ -0,0 +1,22 @@ +package handlers + +import ( + "encoding/json" + "log" + "net/http" +) + +func writeJSON(w http.ResponseWriter, status int, v any) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + json.NewEncoder(w).Encode(v) +} + +func decodeBody(r *http.Request, v any) error { + defer r.Body.Close() + return json.NewDecoder(r.Body).Decode(v) +} + +func logRequest(r *http.Request) { + log.Printf("[wb-mock] %s %s", r.Method, r.URL.Path) +} diff --git a/internal/middleware/auth.go b/internal/middleware/auth.go new file mode 100644 index 0000000..c2c94ca --- /dev/null +++ b/internal/middleware/auth.go @@ -0,0 +1,33 @@ +package middleware + +import ( + "encoding/json" + "net/http" +) + +func Auth(expectedAPIKey string, next http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + apiKey := r.Header.Get("Authorization") + + if apiKey == "" { + writeError(w, 401, "missing credentials: Authorization header is required") + return + } + + if expectedAPIKey != "" && apiKey != expectedAPIKey { + writeError(w, 403, "invalid api key") + return + } + + next(w, r) + } +} + +func writeError(w http.ResponseWriter, code int, msg string) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(code) + json.NewEncoder(w).Encode(map[string]any{ + "code": code, + "message": msg, + }) +}