Files
NixMsg/sdk/go/itest_harness_test.go
T

298 lines
7.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}