236 lines
7.9 KiB
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())
|
|
}
|
|
})
|
|
}
|