feat: add HTTP migration bridges and extensible API binding
This commit is contained in:
117
cmd/go-omni/apigen/testdata/compatibility/consumer_test.go
vendored
Normal file
117
cmd/go-omni/apigen/testdata/compatibility/consumer_test.go
vendored
Normal file
@@ -0,0 +1,117 @@
|
||||
package compatibility_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"example.com/compatibility/internal/handler"
|
||||
"example.com/compatibility/internal/servicecontext"
|
||||
omni "git.yuntuo.hk/k2safe/go-omni"
|
||||
"git.yuntuo.hk/k2safe/go-omni/logx"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGeneratedCompatibility(t *testing.T) {
|
||||
logger, err := logx.New(logx.Config{Writer: io.Discard})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer logger.Close()
|
||||
deps, err := servicecontext.New(context.Background(), logger)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer deps.Close()
|
||||
app := omni.New()
|
||||
defer app.Close()
|
||||
if err = handler.RegisterRoutes(app, deps); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
send := func(method, path, body, contentType string) *httptest.ResponseRecorder {
|
||||
r := httptest.NewRequest(method, path, strings.NewReader(body))
|
||||
r.Header.Set("Content-Type", contentType)
|
||||
w := httptest.NewRecorder()
|
||||
app.ServeHTTP(w, r)
|
||||
return w
|
||||
}
|
||||
for _, id := range []string{`9223372036854775807`, `"9223372036854775807"`} {
|
||||
w := send("POST", "/echo", `{"id":`+id+`,"data":{"n":9223372036854775807}}`, "application/json")
|
||||
if w.Code != 200 || !strings.Contains(w.Body.String(), `"id":"9223372036854775807"`) || !strings.Contains(w.Body.String(), `"n":9223372036854775807`) {
|
||||
t.Fatalf("scalar/dynamic: %d %s", w.Code, w.Body)
|
||||
}
|
||||
}
|
||||
for _, body := range []string{`{"id":true,"data":null}`, `{"id":9223372036854775808,"data":null}`, `{"id":1,"data":{"x":1,"x":2}}`, `{"id":1,"data":null,"extra":1}`} {
|
||||
if w := send("POST", "/echo", body, "application/json"); w.Code != 400 {
|
||||
t.Fatalf("accepted %s: %d", body, w.Code)
|
||||
}
|
||||
}
|
||||
payload := `{"id":1,"data":"` + strings.Repeat("x", (1<<20)+1) + `"}`
|
||||
if w := send("POST", "/echo", payload, "application/json"); w.Code != 200 {
|
||||
t.Fatalf("route limit not passed to Binder: %d", w.Code)
|
||||
}
|
||||
if w := send("POST", "/echo", `{"id":1,"data":"`+strings.Repeat("x", 2<<20)+`"}`, "application/json"); w.Code != 413 {
|
||||
t.Fatalf("large JSON: %d", w.Code)
|
||||
}
|
||||
if w := send("GET", "/items/9223372036854775807", "", ""); w.Code != 200 || !strings.Contains(w.Body.String(), `"id":"9223372036854775807"`) {
|
||||
t.Fatalf("text scalar: %d %s", w.Code, w.Body)
|
||||
}
|
||||
if w := send("POST", "/custom", `{"value":"hello"}`, "application/json"); w.Code != 200 || !strings.Contains(w.Body.String(), "HELLO") {
|
||||
t.Fatalf("custom binder: %d %s", w.Code, w.Body)
|
||||
}
|
||||
raw := `{ "event": 1, "id": "original bytes" }`
|
||||
mac := hmac.New(sha256.New, []byte("fixture-only-key"))
|
||||
mac.Write([]byte(raw))
|
||||
r := httptest.NewRequest("POST", "/callback", strings.NewReader(raw))
|
||||
r.Header.Set("X-Signature", hex.EncodeToString(mac.Sum(nil)))
|
||||
w := httptest.NewRecorder()
|
||||
app.ServeHTTP(w, r)
|
||||
if w.Code != 200 || w.Body.String() != "accepted" {
|
||||
t.Fatalf("raw callback: %d %s", w.Code, w.Body)
|
||||
}
|
||||
r = httptest.NewRequest("POST", "/callback", strings.NewReader(strings.Repeat("x", 129)))
|
||||
r.ContentLength = -1
|
||||
w = httptest.NewRecorder()
|
||||
app.ServeHTTP(w, r)
|
||||
if w.Code != 413 {
|
||||
t.Fatalf("chunked body limit: %d %s", w.Code, w.Body)
|
||||
}
|
||||
temp := t.TempDir()
|
||||
t.Setenv("TMPDIR", temp)
|
||||
var form bytes.Buffer
|
||||
writer := multipart.NewWriter(&form)
|
||||
part, err := writer.CreateFormFile("file", "sample.bin")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
part.Write(bytes.Repeat([]byte("x"), 8192))
|
||||
writer.Close()
|
||||
w = send("POST", "/upload", form.String(), writer.FormDataContentType())
|
||||
if w.Code != 200 || !strings.Contains(w.Body.String(), `"bytes":8192`) {
|
||||
t.Fatalf("upload: %d %s", w.Code, w.Body)
|
||||
}
|
||||
entries, err := os.ReadDir(temp)
|
||||
if err != nil || len(entries) != 0 {
|
||||
t.Fatalf("multipart temp files leaked: %v %v", entries, err)
|
||||
}
|
||||
w = send("GET", "/download", "", "")
|
||||
if w.Code != 200 || w.Body.Len() != 128<<10 || w.Header().Get("Content-Disposition") == "" {
|
||||
t.Fatalf("download wrapped: %d %d", w.Code, w.Body.Len())
|
||||
}
|
||||
var doc map[string]any
|
||||
if err = json.Unmarshal([]byte(handler.OpenAPIDocument), &doc); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
paths := doc["paths"].(map[string]any)
|
||||
media := paths["/download"].(map[string]any)["get"].(map[string]any)["responses"].(map[string]any)["200"].(map[string]any)["content"].(map[string]any)
|
||||
if media["application/octet-stream"] == nil || media["application/json"] != nil {
|
||||
t.Fatal("download schema is JSON")
|
||||
}
|
||||
}
|
||||
16
cmd/go-omni/apigen/testdata/compatibility/download.go
vendored
Normal file
16
cmd/go-omni/apigen/testdata/compatibility/download.go
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
package files
|
||||
|
||||
import (
|
||||
"git.yuntuo.hk/k2safe/go-omni/httpx"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (h *Handler) Download(c *httpx.Context) error {
|
||||
return httpx.WrapHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/octet-stream")
|
||||
w.Header().Set("Content-Disposition", `attachment; filename="demo.bin"`)
|
||||
_, _ = io.Copy(w, strings.NewReader(strings.Repeat("x", 128<<10)))
|
||||
}))(c)
|
||||
}
|
||||
16
cmd/go-omni/apigen/testdata/compatibility/echo.go
vendored
Normal file
16
cmd/go-omni/apigen/testdata/compatibility/echo.go
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
package demo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"example.com/compatibility/internal/types"
|
||||
)
|
||||
|
||||
func (s *Service) Echo(ctx context.Context, req *types.EchoReq) (*types.EchoResp, error) {
|
||||
return &types.EchoResp{ID: req.ID, Data: req.Data}, nil
|
||||
}
|
||||
func (s *Service) Item(ctx context.Context, req *types.ItemReq) (*types.EchoResp, error) {
|
||||
return &types.EchoResp{ID: req.ID, Data: map[string]any{"found": true}}, nil
|
||||
}
|
||||
func (s *Service) Custom(ctx context.Context, req *types.CustomReq) (*types.CustomResp, error) {
|
||||
return &types.CustomResp{Value: req.Value}, nil
|
||||
}
|
||||
58
cmd/go-omni/apigen/testdata/compatibility/hooks.go
vendored
Normal file
58
cmd/go-omni/apigen/testdata/compatibility/hooks.go
vendored
Normal file
@@ -0,0 +1,58 @@
|
||||
package demo
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"example.com/compatibility/internal/types"
|
||||
apierrors "git.yuntuo.hk/k2safe/go-omni/errors"
|
||||
"git.yuntuo.hk/k2safe/go-omni/httpx"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (h *Handler) bindCustom(c *httpx.Context, req *types.CustomReq) error {
|
||||
decoder := json.NewDecoder(c.Request().Body)
|
||||
decoder.DisallowUnknownFields()
|
||||
if err := decoder.Decode(req); err != nil {
|
||||
return apierrors.BadRequest("INVALID_REQUEST", "invalid request").WithCause(err)
|
||||
}
|
||||
var extra any
|
||||
if err := decoder.Decode(&extra); err != io.EOF {
|
||||
return apierrors.BadRequest("INVALID_REQUEST", "one JSON value required").WithCause(err)
|
||||
}
|
||||
req.Value = strings.ToUpper(req.Value)
|
||||
return nil
|
||||
}
|
||||
func (h *Handler) Upload(c *httpx.Context) error {
|
||||
// The small memory threshold deliberately exercises disk spill and cleanup.
|
||||
if err := c.Request().ParseMultipartForm(8); err != nil {
|
||||
return err
|
||||
}
|
||||
file, _, err := c.Request().FormFile("file")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
count, err := io.Copy(io.Discard, file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.OK(map[string]any{"bytes": count})
|
||||
}
|
||||
func (h *Handler) Callback(c *httpx.Context) error {
|
||||
raw, err := io.ReadAll(c.Request().Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// This fixed key belongs only to the test fixture. Applications inject secrets.
|
||||
mac := hmac.New(sha256.New, []byte("fixture-only-key"))
|
||||
mac.Write(raw)
|
||||
signature, err := hex.DecodeString(c.Header("X-Signature"))
|
||||
if err != nil || !hmac.Equal(signature, mac.Sum(nil)) {
|
||||
return c.Text(http.StatusUnauthorized, "invalid signature")
|
||||
}
|
||||
return c.Text(http.StatusOK, "accepted")
|
||||
}
|
||||
Reference in New Issue
Block a user