Files
go-omni/cmd/go-omni/apigen/testdata/managed/consumer_test.go

236 lines
7.9 KiB
Go

package main
import (
"bytes"
"context"
"encoding/json"
"io"
"mime/multipart"
"net"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
"time"
"example.com/managed/internal/config"
"example.com/managed/internal/handler"
"example.com/managed/internal/svc"
omni "git.yuntuo.hk/k2safe/go-omni"
"git.yuntuo.hk/k2safe/go-omni/logx"
)
func application(t *testing.T) *omni.App {
t.Helper()
cfg := config.Default()
cfg.Server.Log.Writer = io.Discard
logger, err := logx.New(cfg.Server.Log)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = logger.Close() })
deps, err := svc.New(context.Background(), logger, cfg)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = deps.Close() })
app, err := handler.NewApp(deps)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = app.Close() })
return app
}
func response(t *testing.T, app *omni.App, method, path string, body io.Reader, headers map[string]string) *httptest.ResponseRecorder {
t.Helper()
req := httptest.NewRequest(method, path, body)
for k, v := range headers {
req.Header.Set(k, v)
}
out := httptest.NewRecorder()
app.ServeHTTP(out, req)
return out
}
func assertError(t *testing.T, out *httptest.ResponseRecorder, status, code int, message, field string) {
t.Helper()
var data map[string]any
if err := json.Unmarshal(out.Body.Bytes(), &data); err != nil {
t.Fatal(out.Body.String(), err)
}
value, exists := data["data"]
if out.Code != status || data["code"] != float64(code) || data[field] != message || !exists || value != nil {
t.Fatalf("unexpected error: %d %s", out.Code, out.Body.String())
}
}
func TestGeneratedAppPolicies(t *testing.T) {
app := application(t)
assertError(t, response(t, app, "GET", "/check?kind=fixed", nil, nil), 400, 400002, "用户名格式不正确", "msg")
assertError(t, response(t, app, "GET", "/check?kind=params", nil, nil), 400, 400003, "企业名称不能超过100个字符", "msg")
assertError(t, response(t, app, "GET", "/check?kind=unknown", nil, nil), 500, 9999, "系统错误", "msg")
assertError(t, response(t, app, "GET", "/other?kind=fixed", nil, nil), 400, 400002, "用户名格式不正确", "detail")
for _, path := range []string{"/check", "/missing", "/callback"} {
assertError(t, response(t, app, "GET", path, nil, map[string]string{"X-Fail": "1"}), 400, 400002, "用户名格式不正确", "msg")
}
assertError(t, response(t, app, "GET", "/missing", nil, nil), 404, 404001, "接口不存在", "msg")
assertError(t, response(t, app, "POST", "/check", nil, nil), 405, 405001, "请求方法不支持", "msg")
out := response(t, app, "GET", "/raw", nil, nil)
if strings.TrimSpace(out.Body.String()) != `{"bytes":3}` {
t.Fatal(out.Body.String())
}
}
func TestGeneratedTransfers(t *testing.T) {
app := application(t)
var body bytes.Buffer
writer := multipart.NewWriter(&body)
file, err := writer.CreateFormFile("file", "x.txt")
if err != nil {
t.Fatal(err)
}
_, _ = file.Write([]byte("hello"))
_ = writer.Close()
out := response(t, app, "POST", "/upload", &body, map[string]string{"Content-Type": writer.FormDataContentType()})
if out.Code != 200 || !strings.Contains(out.Body.String(), `"bytes":5`) {
t.Fatalf("upload: %d %s", out.Code, out.Body.String())
}
out = response(t, app, "POST", "/upload", strings.NewReader("bad"), map[string]string{"Content-Type": "application/json"})
if out.Code != 415 {
t.Fatal(out.Code, out.Body.String())
}
// Truncated uploads must use the project's public error catalog rather
// than succeed or be mistaken for an internal failure.
out = response(t, app, "POST", "/upload", strings.NewReader("--b\r\nContent-Disposition: form-data; name=\"file\"\r\n\r\nprivate file"), map[string]string{"Content-Type": "multipart/form-data; boundary=b"})
assertError(t, out, 400, 400004, "上传内容不完整", "msg")
out = response(t, app, "POST", "/callback", strings.NewReader("{ \"exact\" : 123 }"), map[string]string{"X-Signature": "valid"})
if out.Code != 200 || out.Body.String() != "ACK:{ \"exact\" : 123 }" || out.Header().Get("Content-Type") != "text/plain" {
t.Fatal(out.Code, out.Body.String())
}
assertError(t, response(t, app, "POST", "/callback", strings.NewReader("{}"), nil), 400, 400002, "用户名格式不正确", "msg")
// Unknown length must be limited by reads too, not just Content-Length.
req := httptest.NewRequest("POST", "/callback", strings.NewReader(strings.Repeat("x", 65)))
req.ContentLength = -1
req.Header.Set("X-Signature", "valid")
out = httptest.NewRecorder()
app.ServeHTTP(out, req)
assertError(t, out, 413, 413001, "请求体太大", "msg")
before := svc.DownloadClosed.Load()
out = response(t, app, "GET", "/download", nil, nil)
if out.Body.String() != "file-content" || out.Header().Get("Content-Type") != "application/octet-stream" || !strings.Contains(out.Header().Get("Content-Disposition"), "attachment") || svc.DownloadClosed.Load() != before+1 {
t.Fatal(out.Code, out.Body.String(), out.Header())
}
}
func configFile(t *testing.T, address string, fail bool) string {
t.Helper()
path := filepath.Join(t.TempDir(), "config.json")
data, _ := json.Marshal(map[string]any{"server": map[string]any{"address": address, "transport": "${GO_OMNI_TRANSPORT:-nethttp}"}, "failInit": fail})
if err := os.WriteFile(path, data, 0600); err != nil {
t.Fatal(err)
}
return path
}
func waitReady(t *testing.T, url string, done <-chan error) {
t.Helper()
client := http.Client{Timeout: 100 * time.Millisecond}
deadline := time.Now().Add(5 * time.Second)
for time.Now().Before(deadline) {
select {
case err := <-done:
t.Fatalf("startup failed: %v", err)
default:
}
res, err := client.Get(url + "/health/ready")
if err == nil {
_ = res.Body.Close()
if res.StatusCode == 200 {
return
}
}
time.Sleep(10 * time.Millisecond)
}
t.Fatal("server did not become ready")
}
func TestGeneratedMainLifecycle(t *testing.T) {
t.Run("initializationFailure", func(t *testing.T) {
before := svc.Closed.Load()
if err := run(context.Background(), configFile(t, "127.0.0.1:0", true)); err == nil {
t.Fatal("failure hidden")
}
if svc.Closed.Load() != before+1 {
t.Fatal("initialized client leaked or closed twice")
}
})
t.Run("listenFailure", func(t *testing.T) {
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
defer listener.Close()
before := svc.Closed.Load()
if err = run(context.Background(), configFile(t, listener.Addr().String(), false)); err == nil {
t.Fatal("occupied listener accepted")
}
if svc.Closed.Load() != before+1 {
t.Fatal("listen failure cleanup")
}
})
t.Run("drainBeforeClose", func(t *testing.T) {
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
address := listener.Addr().String()
_ = listener.Close()
svc.Closed.Store(0)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
done := make(chan error, 1)
path := configFile(t, address, false)
go func() { done <- run(ctx, path) }()
base := "http://" + address
waitReady(t, base, done)
requestDone := make(chan error, 1)
go func() {
res, err := http.Get(base + "/slow")
if err == nil {
data, _ := io.ReadAll(res.Body)
_ = res.Body.Close()
if res.StatusCode != 200 || !strings.Contains(string(data), `"bytes":1`) {
err = io.ErrUnexpectedEOF
}
}
requestDone <- err
}()
select {
case <-svc.Started:
case <-time.After(3 * time.Second):
t.Fatal("request did not enter logic")
}
cancel()
time.Sleep(30 * time.Millisecond)
if svc.Closed.Load() != 0 {
t.Fatal("dependency closed before drain")
}
close(svc.Release)
select {
case err := <-requestDone:
if err != nil {
t.Fatal(err)
}
case <-time.After(3 * time.Second):
t.Fatal("request stuck")
}
select {
case err := <-done:
if err != nil {
t.Fatal(err)
}
case <-time.After(3 * time.Second):
t.Fatal("shutdown stuck")
}
if svc.Closed.Load() != 1 {
t.Fatal("client closed more/less than once", svc.Closed.Load())
}
})
}