test: Go/JS SDK 接入清单集成测与 README 示例
This commit is contained in:
@@ -0,0 +1,297 @@
|
||||
package nixmsg_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/cookiejar"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// 集成测试启动器:在仓库根目录编译 nixmsg,临时目录 + 127.0.0.1:0 + admin init。
|
||||
// 不依赖根模块 harness 包(sdk/go 是独立模块,从本目录起测时 go.mod 会挡住 Binary 找根)。
|
||||
|
||||
type itestServer struct {
|
||||
BinPath string
|
||||
ConfigPath string
|
||||
DataDir string
|
||||
Addr string
|
||||
HTTPBase string
|
||||
AdminHTTPBase string
|
||||
AdminPassword string
|
||||
MQTTWS string
|
||||
cmd *exec.Cmd
|
||||
}
|
||||
|
||||
var (
|
||||
itestBinOnce sync.Once
|
||||
itestBinPath string
|
||||
itestBinErr error
|
||||
)
|
||||
|
||||
func repoRoot(t *testing.T) string {
|
||||
t.Helper()
|
||||
dir, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for {
|
||||
nix := filepath.Join(dir, "cmd", "nixmsg")
|
||||
mod := filepath.Join(dir, "go.mod")
|
||||
if st, e := os.Stat(nix); e == nil && st.IsDir() {
|
||||
if b, e2 := os.ReadFile(mod); e2 == nil && bytes.Contains(b, []byte("module git.asio.asia/nixevol/NixMsg\n")) {
|
||||
return dir
|
||||
}
|
||||
}
|
||||
parent := filepath.Dir(dir)
|
||||
if parent == dir {
|
||||
t.Fatal("找不到仓库根(含 cmd/nixmsg)")
|
||||
}
|
||||
dir = parent
|
||||
}
|
||||
}
|
||||
|
||||
func itestBinary(t *testing.T) string {
|
||||
t.Helper()
|
||||
itestBinOnce.Do(func() {
|
||||
root := ""
|
||||
dir, err := os.Getwd()
|
||||
if err != nil {
|
||||
itestBinErr = err
|
||||
return
|
||||
}
|
||||
for {
|
||||
nix := filepath.Join(dir, "cmd", "nixmsg")
|
||||
mod := filepath.Join(dir, "go.mod")
|
||||
if st, e := os.Stat(nix); e == nil && st.IsDir() {
|
||||
if b, e2 := os.ReadFile(mod); e2 == nil && bytes.Contains(b, []byte("module git.asio.asia/nixevol/NixMsg\n")) {
|
||||
root = dir
|
||||
break
|
||||
}
|
||||
}
|
||||
parent := filepath.Dir(dir)
|
||||
if parent == dir {
|
||||
itestBinErr = fmt.Errorf("repo root not found")
|
||||
return
|
||||
}
|
||||
dir = parent
|
||||
}
|
||||
tmp, err := os.MkdirTemp("", "nixmsg-sdk-go-bin-*")
|
||||
if err != nil {
|
||||
itestBinErr = err
|
||||
return
|
||||
}
|
||||
name := "nixmsg"
|
||||
if runtime.GOOS == "windows" {
|
||||
name += ".exe"
|
||||
}
|
||||
out := filepath.Join(tmp, name)
|
||||
cmd := exec.Command("go", "build", "-o", out, "./cmd/nixmsg")
|
||||
cmd.Dir = root
|
||||
cmd.Env = append(os.Environ(), "CGO_ENABLED=0")
|
||||
if b, e := cmd.CombinedOutput(); e != nil {
|
||||
itestBinErr = fmt.Errorf("build nixmsg: %w\n%s", e, b)
|
||||
return
|
||||
}
|
||||
itestBinPath = out
|
||||
})
|
||||
if itestBinErr != nil {
|
||||
t.Fatal(itestBinErr)
|
||||
}
|
||||
return itestBinPath
|
||||
}
|
||||
|
||||
func startITestServer(t *testing.T) *itestServer {
|
||||
t.Helper()
|
||||
bin := itestBinary(t)
|
||||
dataDir, err := os.MkdirTemp("", "nixmsg-sdk-go-itest-*")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cfgPath := filepath.Join(dataDir, "config.yaml")
|
||||
cfg := fmt.Sprintf("listen: %q\ndata_dir: %q\n", "127.0.0.1:0", filepath.ToSlash(dataDir))
|
||||
if err := os.WriteFile(cfgPath, []byte(cfg), 0o644); err != nil {
|
||||
_ = os.RemoveAll(dataDir)
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
initCmd := exec.Command(bin, "admin", "init")
|
||||
initCmd.Env = append(os.Environ(), "NIXMSG_CONFIG="+cfgPath)
|
||||
initOut, initErr := initCmd.CombinedOutput()
|
||||
if initErr != nil {
|
||||
_ = os.RemoveAll(dataDir)
|
||||
t.Fatalf("admin init: %v\n%s", initErr, initOut)
|
||||
}
|
||||
pass := parseAdminPassword(string(initOut))
|
||||
if pass == "" {
|
||||
_ = os.RemoveAll(dataDir)
|
||||
t.Fatalf("admin init 未打印密码:\n%s", initOut)
|
||||
}
|
||||
|
||||
cmd := exec.Command(bin, "serve")
|
||||
cmd.Env = append(os.Environ(), "NIXMSG_CONFIG="+cfgPath)
|
||||
cmd.Stdout = os.Stderr
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Start(); err != nil {
|
||||
_ = os.RemoveAll(dataDir)
|
||||
t.Fatal(err)
|
||||
}
|
||||
s := &itestServer{
|
||||
BinPath: bin,
|
||||
ConfigPath: cfgPath,
|
||||
DataDir: dataDir,
|
||||
AdminPassword: pass,
|
||||
cmd: cmd,
|
||||
}
|
||||
addr, err := waitAddrFile(filepath.Join(dataDir, "listen.addr"), 15*time.Second)
|
||||
if err != nil {
|
||||
_ = s.Stop()
|
||||
t.Fatalf("wait listen.addr: %v", err)
|
||||
}
|
||||
s.Addr = addr
|
||||
s.HTTPBase = "http://" + addr
|
||||
s.AdminHTTPBase = s.HTTPBase
|
||||
s.MQTTWS = "ws://" + addr + "/mqtt"
|
||||
t.Cleanup(func() { _ = s.Stop() })
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *itestServer) Stop() error {
|
||||
if s == nil || s.cmd == nil {
|
||||
return nil
|
||||
}
|
||||
if s.cmd.Process != nil {
|
||||
_ = s.cmd.Process.Kill()
|
||||
_, _ = s.cmd.Process.Wait()
|
||||
}
|
||||
s.cmd = nil
|
||||
if s.DataDir != "" {
|
||||
return os.RemoveAll(s.DataDir)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseAdminPassword(out string) string {
|
||||
for _, line := range strings.Split(out, "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
lower := strings.ToLower(line)
|
||||
if strings.HasPrefix(lower, "admin password:") {
|
||||
return strings.TrimSpace(line[len("admin password:"):])
|
||||
}
|
||||
if strings.HasPrefix(lower, "password:") {
|
||||
return strings.TrimSpace(line[len("password:"):])
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func waitAddrFile(path string, timeout time.Duration) (string, error) {
|
||||
deadline := time.Now().Add(timeout)
|
||||
var last error
|
||||
for time.Now().Before(deadline) {
|
||||
b, err := os.ReadFile(path)
|
||||
if err == nil {
|
||||
addr := strings.TrimSpace(string(b))
|
||||
if addr != "" {
|
||||
return addr, nil
|
||||
}
|
||||
last = fmt.Errorf("empty addr")
|
||||
} else {
|
||||
last = err
|
||||
}
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
}
|
||||
return "", last
|
||||
}
|
||||
|
||||
type adminHTTP struct {
|
||||
base string
|
||||
hc *http.Client
|
||||
}
|
||||
|
||||
func newAdminHTTP(t *testing.T, base, password string) *adminHTTP {
|
||||
t.Helper()
|
||||
jar, err := cookiejar.New(nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
a := &adminHTTP{
|
||||
base: strings.TrimRight(base, "/"),
|
||||
hc: &http.Client{Timeout: 30 * time.Second, Jar: jar},
|
||||
}
|
||||
body, _ := json.Marshal(map[string]string{"username": "admin", "password": password})
|
||||
res, err := a.do(http.MethodPost, "/api/admin/login", body, "application/json")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusOK {
|
||||
raw, _ := io.ReadAll(res.Body)
|
||||
t.Fatalf("admin login %d %s", res.StatusCode, raw)
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
func (a *adminHTTP) do(method, path string, body []byte, ct string) (*http.Response, error) {
|
||||
var rdr io.Reader
|
||||
if body != nil {
|
||||
rdr = bytes.NewReader(body)
|
||||
}
|
||||
req, err := http.NewRequest(method, a.base+path, rdr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if ct != "" {
|
||||
req.Header.Set("Content-Type", ct)
|
||||
}
|
||||
switch strings.ToUpper(method) {
|
||||
case http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete:
|
||||
req.Header.Set("X-Nixmsg-Request", "1")
|
||||
}
|
||||
return a.hc.Do(req)
|
||||
}
|
||||
|
||||
func (a *adminHTTP) putRegistration(t *testing.T, enabled bool, code string) {
|
||||
t.Helper()
|
||||
payload := map[string]any{"enabled": enabled}
|
||||
if code != "" {
|
||||
payload["code"] = code
|
||||
}
|
||||
raw, _ := json.Marshal(payload)
|
||||
res, err := a.do(http.MethodPut, "/api/admin/registration", raw, "application/json")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
b, _ := io.ReadAll(res.Body)
|
||||
if res.StatusCode != http.StatusOK {
|
||||
t.Fatalf("put registration %d %s", res.StatusCode, b)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *adminHTTP) kick(t *testing.T, endpointID string) {
|
||||
t.Helper()
|
||||
res, err := a.do(http.MethodPost, "/api/admin/endpoints/"+endpointID+"/kick", []byte("{}"), "application/json")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusOK {
|
||||
b, _ := io.ReadAll(res.Body)
|
||||
t.Fatalf("kick %s: %d %s", endpointID, res.StatusCode, b)
|
||||
}
|
||||
}
|
||||
|
||||
func delay0() *time.Duration {
|
||||
d := time.Duration(0)
|
||||
return &d
|
||||
}
|
||||
Reference in New Issue
Block a user