feat(1d): wire oapi-codegen, generate types/server/client, add 501 stubs — tsk_Kp80nvHV2yLc
- Fix openapi.yaml: quote bearerAuth description that contained unquoted `: ` plain-scalar YAML
- Add server/api/cfg-{types,server,client}.yaml — oapi-codegen v2 config for three split outputs
- Add server/api/gen/gen.go — three //go:generate directives; run `make generate`
- Generate server/api/gen/{types,server,client}.gen.go from the contract (all 15 operations)
- Add github.com/oapi-codegen/runtime v1.4.1 to go.mod (runtime types needed by generated code)
- Add server/internal/httpapi/unimplemented.go — UnimplementedServer satisfies gen.ServerInterface;
every method writes HTTP 501 + {code,message_zh,message_en} JSON body (no dep on task-1f apierr)
- Update server/cmd/server/main.go — mount /v1 API via gen.HandlerFromMuxWithBaseURL; keep /healthz
- Update server/Makefile — `generate` target now runs `go generate ./...`;
add `check-generate` CI guard (regenerate + git diff --exit-code)
- Add server/internal/httpapi/unimplemented_test.go — 19 httptest cases covering all 15 API routes
(501+body), undefined routes (404), and /healthz isolation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
// Package httpapi contains HTTP API handler stubs for the Pangolin v1 API.
|
||||
// All endpoints return HTTP 501 with a structured bilingual error body until
|
||||
// real business logic is wired in (tasks #2-#8).
|
||||
package httpapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
openapi_types "github.com/oapi-codegen/runtime/types"
|
||||
|
||||
"github.com/wangjia/pangolin/server/api/gen"
|
||||
)
|
||||
|
||||
// notImplError is the fixed JSON error body returned by every 501 stub.
|
||||
// Intentionally hand-written here (not from internal/apierr) so that this
|
||||
// package stays free of dependencies on task-1f which runs in parallel.
|
||||
var notImplError = struct {
|
||||
Code string `json:"code"`
|
||||
MessageZH string `json:"message_zh"`
|
||||
MessageEn string `json:"message_en"`
|
||||
}{
|
||||
Code: "NOT_IMPLEMENTED",
|
||||
MessageZH: "接口尚未实现",
|
||||
MessageEn: "Not implemented yet",
|
||||
}
|
||||
|
||||
// writeNotImpl writes a 501 Not Implemented response with the structured error body.
|
||||
func writeNotImpl(w http.ResponseWriter) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.WriteHeader(http.StatusNotImplemented)
|
||||
_ = json.NewEncoder(w).Encode(notImplError)
|
||||
}
|
||||
|
||||
// UnimplementedServer satisfies gen.ServerInterface and returns 501 for every
|
||||
// endpoint. Individual methods should be replaced as each module is implemented.
|
||||
type UnimplementedServer struct{}
|
||||
|
||||
// Compile-time assertion: UnimplementedServer must satisfy ServerInterface.
|
||||
var _ gen.ServerInterface = (*UnimplementedServer)(nil)
|
||||
|
||||
// ── Auth ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
func (s *UnimplementedServer) SendVerificationCode(w http.ResponseWriter, r *http.Request) {
|
||||
writeNotImpl(w)
|
||||
}
|
||||
|
||||
func (s *UnimplementedServer) Register(w http.ResponseWriter, r *http.Request) {
|
||||
writeNotImpl(w)
|
||||
}
|
||||
|
||||
func (s *UnimplementedServer) Login(w http.ResponseWriter, r *http.Request) {
|
||||
writeNotImpl(w)
|
||||
}
|
||||
|
||||
func (s *UnimplementedServer) RefreshToken(w http.ResponseWriter, r *http.Request) {
|
||||
writeNotImpl(w)
|
||||
}
|
||||
|
||||
// ── Account ──────────────────────────────────────────────────────────────────
|
||||
|
||||
func (s *UnimplementedServer) GetMe(w http.ResponseWriter, r *http.Request) {
|
||||
writeNotImpl(w)
|
||||
}
|
||||
|
||||
func (s *UnimplementedServer) ListDevices(w http.ResponseWriter, r *http.Request) {
|
||||
writeNotImpl(w)
|
||||
}
|
||||
|
||||
func (s *UnimplementedServer) DeleteDevice(w http.ResponseWriter, r *http.Request, id openapi_types.UUID) {
|
||||
writeNotImpl(w)
|
||||
}
|
||||
|
||||
// ── Commerce ─────────────────────────────────────────────────────────────────
|
||||
|
||||
func (s *UnimplementedServer) RedeemCode(w http.ResponseWriter, r *http.Request) {
|
||||
writeNotImpl(w)
|
||||
}
|
||||
|
||||
func (s *UnimplementedServer) AdsUnlock(w http.ResponseWriter, r *http.Request) {
|
||||
writeNotImpl(w)
|
||||
}
|
||||
|
||||
func (s *UnimplementedServer) ListPlans(w http.ResponseWriter, r *http.Request) {
|
||||
writeNotImpl(w)
|
||||
}
|
||||
|
||||
// ── Nodes ────────────────────────────────────────────────────────────────────
|
||||
|
||||
func (s *UnimplementedServer) ListNodes(w http.ResponseWriter, r *http.Request, params gen.ListNodesParams) {
|
||||
writeNotImpl(w)
|
||||
}
|
||||
|
||||
func (s *UnimplementedServer) ConnectNode(w http.ResponseWriter, r *http.Request, id openapi_types.UUID) {
|
||||
writeNotImpl(w)
|
||||
}
|
||||
|
||||
func (s *UnimplementedServer) DisconnectNode(w http.ResponseWriter, r *http.Request, id openapi_types.UUID) {
|
||||
writeNotImpl(w)
|
||||
}
|
||||
|
||||
// ── Usage & Notices ──────────────────────────────────────────────────────────
|
||||
|
||||
func (s *UnimplementedServer) GetUsage(w http.ResponseWriter, r *http.Request, params gen.GetUsageParams) {
|
||||
writeNotImpl(w)
|
||||
}
|
||||
|
||||
func (s *UnimplementedServer) ListNotices(w http.ResponseWriter, r *http.Request) {
|
||||
writeNotImpl(w)
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
package httpapi_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
|
||||
"github.com/wangjia/pangolin/server/api/gen"
|
||||
"github.com/wangjia/pangolin/server/internal/httpapi"
|
||||
)
|
||||
|
||||
func newTestServer() http.Handler {
|
||||
r := chi.NewRouter()
|
||||
r.Use(middleware.Recoverer)
|
||||
gen.HandlerFromMuxWithBaseURL(&httpapi.UnimplementedServer{}, r, "/v1")
|
||||
return r
|
||||
}
|
||||
|
||||
type errorBody struct {
|
||||
Code string `json:"code"`
|
||||
MessageZH string `json:"message_zh"`
|
||||
MessageEn string `json:"message_en"`
|
||||
}
|
||||
|
||||
func Test501StructuredBody(t *testing.T) {
|
||||
srv := newTestServer()
|
||||
|
||||
// All defined /v1 routes should return 501 with the structured body.
|
||||
routes := []struct {
|
||||
method string
|
||||
path string
|
||||
}{
|
||||
{"GET", "/v1/plans"},
|
||||
{"GET", "/v1/nodes"},
|
||||
{"GET", "/v1/me"},
|
||||
{"GET", "/v1/me/devices"},
|
||||
{"GET", "/v1/usage"},
|
||||
{"GET", "/v1/notices"},
|
||||
{"POST", "/v1/auth/code"},
|
||||
{"POST", "/v1/auth/register"},
|
||||
{"POST", "/v1/auth/login"},
|
||||
{"POST", "/v1/auth/refresh"},
|
||||
{"POST", "/v1/redeem"},
|
||||
{"POST", "/v1/ads/unlock"},
|
||||
{"DELETE", "/v1/me/devices/00000000-0000-0000-0000-000000000001"},
|
||||
{"POST", "/v1/nodes/00000000-0000-0000-0000-000000000001/connect"},
|
||||
{"POST", "/v1/nodes/00000000-0000-0000-0000-000000000001/disconnect"},
|
||||
}
|
||||
|
||||
for _, tc := range routes {
|
||||
t.Run(tc.method+" "+tc.path, func(t *testing.T) {
|
||||
req := httptest.NewRequest(tc.method, tc.path, nil)
|
||||
rec := httptest.NewRecorder()
|
||||
srv.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusNotImplemented {
|
||||
t.Errorf("expected 501, got %d", rec.Code)
|
||||
}
|
||||
|
||||
var body errorBody
|
||||
if err := json.NewDecoder(rec.Body).Decode(&body); err != nil {
|
||||
t.Fatalf("could not decode body: %v", err)
|
||||
}
|
||||
if body.Code != "NOT_IMPLEMENTED" {
|
||||
t.Errorf("code: want NOT_IMPLEMENTED, got %q", body.Code)
|
||||
}
|
||||
if body.MessageZH == "" {
|
||||
t.Error("message_zh is empty")
|
||||
}
|
||||
if body.MessageEn == "" {
|
||||
t.Error("message_en is empty")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test404ForUndefinedRoutes(t *testing.T) {
|
||||
srv := newTestServer()
|
||||
|
||||
paths := []string{
|
||||
"/v1/nonexistent",
|
||||
"/v2/plans",
|
||||
"/totally/wrong",
|
||||
}
|
||||
|
||||
for _, p := range paths {
|
||||
t.Run(p, func(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", p, nil)
|
||||
rec := httptest.NewRecorder()
|
||||
srv.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusNotFound {
|
||||
t.Errorf("path %s: expected 404, got %d", p, rec.Code)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHealthzNotAffected(t *testing.T) {
|
||||
r := chi.NewRouter()
|
||||
r.Get("/healthz", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte(`{"status":"ok"}`))
|
||||
})
|
||||
gen.HandlerFromMuxWithBaseURL(&httpapi.UnimplementedServer{}, r, "/v1")
|
||||
|
||||
req := httptest.NewRequest("GET", "/healthz", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
r.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Errorf("healthz: expected 200, got %d", rec.Code)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user