feat: 实现配置校验与运维命令及优雅停机
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"git.asio.asia/nixevol/NixMsg/internal/auth"
|
||||
"git.asio.asia/nixevol/NixMsg/internal/config"
|
||||
"git.asio.asia/nixevol/NixMsg/internal/store"
|
||||
)
|
||||
|
||||
const (
|
||||
adminPasswordLen = 20
|
||||
minAdminPasswordLen = 12
|
||||
adminPasswordChars = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789"
|
||||
)
|
||||
|
||||
func cmdAdmin(args []string) error {
|
||||
if len(args) < 1 {
|
||||
return errors.New("usage: nixmsg admin <init|set-password>")
|
||||
}
|
||||
switch args[0] {
|
||||
case "init":
|
||||
return cmdAdminInit(args[1:])
|
||||
case "set-password":
|
||||
return cmdAdminSetPassword(args[1:])
|
||||
default:
|
||||
return fmt.Errorf("unknown admin command: %s", args[0])
|
||||
}
|
||||
}
|
||||
|
||||
func cmdAdminInit(_ []string) error {
|
||||
cfg, err := loadAndValidateConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ctx := context.Background()
|
||||
db, err := store.Open(cfg.DataDir, cfg.SQLiteSynchronous)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = db.Close() }()
|
||||
|
||||
ok, err := store.HasAdminPassword(ctx, db.Write)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if ok {
|
||||
return errors.New("admin already initialized; use admin set-password")
|
||||
}
|
||||
|
||||
password, err := generateAdminPassword(adminPasswordLen)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
phc, err := auth.HashPassword(password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := store.SetAdminPasswordHash(ctx, db.Write, phc); err != nil {
|
||||
return err
|
||||
}
|
||||
// 密码只打印到终端一次,不进日志。
|
||||
fmt.Printf("admin password: %s\n", password)
|
||||
return nil
|
||||
}
|
||||
|
||||
func cmdAdminSetPassword(args []string) error {
|
||||
password, err := parseSetPasswordArgs(args, os.Stdin)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if vErr := validateAdminPassword(password); vErr != nil {
|
||||
return vErr
|
||||
}
|
||||
cfg, err := loadAndValidateConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ctx := context.Background()
|
||||
db, err := store.Open(cfg.DataDir, cfg.SQLiteSynchronous)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = db.Close() }()
|
||||
|
||||
phc, err := auth.HashPassword(password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := store.SetAdminPasswordHash(ctx, db.Write, phc); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintln(os.Stderr, "admin password updated")
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseSetPasswordArgs(args []string, stdin io.Reader) (string, error) {
|
||||
var password string
|
||||
for i := 0; i < len(args); i++ {
|
||||
switch args[i] {
|
||||
case "--password":
|
||||
if i+1 >= len(args) {
|
||||
return "", errors.New("usage: nixmsg admin set-password --password <password>")
|
||||
}
|
||||
password = args[i+1]
|
||||
i++
|
||||
default:
|
||||
if strings.HasPrefix(args[i], "-") {
|
||||
return "", fmt.Errorf("unknown flag: %s", args[i])
|
||||
}
|
||||
if password != "" {
|
||||
return "", errors.New("usage: nixmsg admin set-password --password <password>")
|
||||
}
|
||||
password = args[i]
|
||||
}
|
||||
}
|
||||
if password == "" {
|
||||
b, err := io.ReadAll(io.LimitReader(stdin, 4096))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
password = strings.TrimSpace(string(b))
|
||||
}
|
||||
if password == "" {
|
||||
return "", errors.New("password required (pass --password or stdin)")
|
||||
}
|
||||
return password, nil
|
||||
}
|
||||
|
||||
func validateAdminPassword(password string) error {
|
||||
if len(password) < minAdminPasswordLen {
|
||||
return fmt.Errorf("admin password must be at least %d characters", minAdminPasswordLen)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func generateAdminPassword(n int) (string, error) {
|
||||
buf := make([]byte, n)
|
||||
charset := []byte(adminPasswordChars)
|
||||
for i := 0; i < n; {
|
||||
var b [1]byte
|
||||
if _, err := rand.Read(b[:]); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if int(b[0]) >= 256-(256%len(charset)) {
|
||||
continue
|
||||
}
|
||||
buf[i] = charset[int(b[0])%len(charset)]
|
||||
i++
|
||||
}
|
||||
return string(buf), nil
|
||||
}
|
||||
|
||||
func loadAndValidateConfig() (config.Config, error) {
|
||||
cfgPath := config.PathFromEnv()
|
||||
cfg, err := config.Load(cfgPath)
|
||||
if err != nil {
|
||||
return config.Config{}, err
|
||||
}
|
||||
if err := cfg.Validate(); err != nil {
|
||||
return config.Config{}, err
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"git.asio.asia/nixevol/NixMsg/internal/store"
|
||||
)
|
||||
|
||||
func cmdBackup(args []string) error {
|
||||
outPath, err := parseBackupArgs(args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cfg, err := loadAndValidateConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if dir := filepath.Dir(outPath); dir != "" && dir != "." {
|
||||
if mkErr := os.MkdirAll(dir, 0o755); mkErr != nil {
|
||||
return fmt.Errorf("mkdir backup dir: %w", mkErr)
|
||||
}
|
||||
}
|
||||
absOut, err := filepath.Abs(outPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ctx := context.Background()
|
||||
// 对运行中的库:单独打开写连接执行 VACUUM INTO(可与 serve 并存,WAL 下安全)。
|
||||
write, err := store.OpenWriter(cfg.DataDir, cfg.SQLiteSynchronous)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = write.Close() }()
|
||||
if err := store.VacuumInto(ctx, write, filepath.ToSlash(absOut)); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "backup written to %s\n", absOut)
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseBackupArgs(args []string) (string, error) {
|
||||
var out string
|
||||
for i := 0; i < len(args); i++ {
|
||||
switch args[i] {
|
||||
case "--out":
|
||||
if i+1 >= len(args) {
|
||||
return "", errors.New("usage: nixmsg backup --out <file.db>")
|
||||
}
|
||||
out = args[i+1]
|
||||
i++
|
||||
default:
|
||||
return "", fmt.Errorf("unknown argument: %s", args[i])
|
||||
}
|
||||
}
|
||||
if out == "" {
|
||||
return "", errors.New("usage: nixmsg backup --out <file.db>")
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"git.asio.asia/nixevol/NixMsg/internal/config"
|
||||
)
|
||||
|
||||
func cmdCheckConfig(args []string) error {
|
||||
path := config.PathFromEnv()
|
||||
if len(args) > 0 {
|
||||
path = args[0]
|
||||
}
|
||||
cfg, err := config.Load(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := cfg.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintln(os.Stderr, "config ok")
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.asio.asia/nixevol/NixMsg/internal/store"
|
||||
)
|
||||
|
||||
func TestCheckConfigRejectsLargeBody(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "bad.yaml")
|
||||
body := "listen: \":0\"\ndata_dir: \"" + filepath.ToSlash(dir) + "\"\nlimits:\n max_body_bytes: 262145\n"
|
||||
if err := os.WriteFile(path, []byte(body), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Setenv("NIXMSG_CONFIG", path)
|
||||
err := cmdCheckConfig(nil)
|
||||
if err == nil || !strings.Contains(err.Error(), "max_body_bytes") {
|
||||
t.Fatalf("want max_body_bytes error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckConfigOK(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := writeTestConfig(t, dir)
|
||||
t.Setenv("NIXMSG_CONFIG", path)
|
||||
if err := cmdCheckConfig(nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdminInitOnceAndSetPassword(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := writeTestConfig(t, dir)
|
||||
t.Setenv("NIXMSG_CONFIG", path)
|
||||
|
||||
var out bytes.Buffer
|
||||
old := os.Stdout
|
||||
r, w, err := os.Pipe()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
os.Stdout = w
|
||||
errInit := cmdAdminInit(nil)
|
||||
_ = w.Close()
|
||||
os.Stdout = old
|
||||
_, _ = out.ReadFrom(r)
|
||||
_ = r.Close()
|
||||
if errInit != nil {
|
||||
t.Fatal(errInit)
|
||||
}
|
||||
text := out.String()
|
||||
if !strings.Contains(text, "admin password:") {
|
||||
t.Fatalf("password not printed: %q", text)
|
||||
}
|
||||
pass := strings.TrimSpace(strings.TrimPrefix(strings.TrimSpace(text), "admin password:"))
|
||||
if len(pass) != 20 {
|
||||
t.Fatalf("password len=%d value=%q", len(pass), pass)
|
||||
}
|
||||
|
||||
if err2 := cmdAdminInit(nil); err2 == nil || !strings.Contains(err2.Error(), "already initialized") {
|
||||
t.Fatalf("want already initialized, got %v", err2)
|
||||
}
|
||||
|
||||
if err2 := cmdAdminSetPassword([]string{"--password", "short"}); err2 == nil {
|
||||
t.Fatal("expected short password error")
|
||||
}
|
||||
if err2 := cmdAdminSetPassword([]string{"--password", "long-enough-password"}); err2 != nil {
|
||||
t.Fatal(err2)
|
||||
}
|
||||
|
||||
db, openErr := store.Open(dir, "FULL")
|
||||
if openErr != nil {
|
||||
t.Fatal(openErr)
|
||||
}
|
||||
defer func() { _ = db.Close() }()
|
||||
ok, hasErr := store.HasAdminPassword(context.Background(), db.Write)
|
||||
if hasErr != nil || !ok {
|
||||
t.Fatalf("has admin: ok=%v err=%v", ok, hasErr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBackupVacuumInto(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := writeTestConfig(t, dir)
|
||||
t.Setenv("NIXMSG_CONFIG", path)
|
||||
initAdminForTest(t, dir)
|
||||
|
||||
out := filepath.Join(dir, "backup", "copy.db")
|
||||
if err := os.MkdirAll(filepath.Dir(out), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := cmdBackup([]string{"--out", out}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
st, err := os.Stat(out)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if st.Size() == 0 {
|
||||
t.Fatal("backup empty")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHealthcheck(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
cfgPath := writeTestConfig(t, dir)
|
||||
initAdminForTest(t, dir)
|
||||
t.Setenv("NIXMSG_CONFIG", cfgPath)
|
||||
|
||||
cfg, err := loadAndValidateConfig()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
errCh := make(chan error, 1)
|
||||
go func() { errCh <- runServe(ctx, cfg) }()
|
||||
|
||||
deadline := time.Now().Add(10 * time.Second)
|
||||
for time.Now().Before(deadline) {
|
||||
if b, readErr := os.ReadFile(filepath.Join(dir, "listen.addr")); readErr == nil && strings.TrimSpace(string(b)) != "" {
|
||||
break
|
||||
}
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
}
|
||||
|
||||
if err := cmdHealthcheck(nil); err != nil {
|
||||
cancel()
|
||||
<-errCh
|
||||
t.Fatal(err)
|
||||
}
|
||||
cancel()
|
||||
<-errCh
|
||||
}
|
||||
|
||||
func TestParseSetPasswordArgs(t *testing.T) {
|
||||
t.Parallel()
|
||||
pass, err := parseSetPasswordArgs([]string{"--password", "abcdefghijkl"}, nil)
|
||||
if err != nil || pass != "abcdefghijkl" {
|
||||
t.Fatalf("pass=%q err=%v", pass, err)
|
||||
}
|
||||
pass, err = parseSetPasswordArgs([]string{"twelvechars!!"}, nil)
|
||||
if err != nil || pass != "twelvechars!!" {
|
||||
t.Fatalf("pass=%q err=%v", pass, err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func cmdHealthcheck(_ []string) error {
|
||||
cfg, err := loadAndValidateConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
addrBytes, err := os.ReadFile(filepath.Join(cfg.DataDir, "listen.addr"))
|
||||
if err != nil {
|
||||
return fmt.Errorf("read listen.addr: %w", err)
|
||||
}
|
||||
addr := strings.TrimSpace(string(addrBytes))
|
||||
if addr == "" {
|
||||
return fmt.Errorf("listen.addr is empty")
|
||||
}
|
||||
url := "http://" + addr + "/healthz"
|
||||
client := &http.Client{Timeout: 5 * time.Second}
|
||||
resp, err := client.Get(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
body, _ := io.ReadAll(io.LimitReader(resp.Body, 1024))
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("healthz status %d: %s", resp.StatusCode, strings.TrimSpace(string(body)))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+16
-4
@@ -10,16 +10,28 @@ func main() {
|
||||
fmt.Fprintln(os.Stderr, "usage: nixmsg <command>")
|
||||
os.Exit(2)
|
||||
}
|
||||
var err error
|
||||
switch os.Args[1] {
|
||||
case "version":
|
||||
cmdVersion(os.Args[2:])
|
||||
case "serve":
|
||||
if err := cmdServe(os.Args[2:]); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
err = cmdServe(os.Args[2:])
|
||||
// P-WIRE-BEGIN
|
||||
case "admin":
|
||||
err = cmdAdmin(os.Args[2:])
|
||||
case "backup":
|
||||
err = cmdBackup(os.Args[2:])
|
||||
case "check-config":
|
||||
err = cmdCheckConfig(os.Args[2:])
|
||||
case "healthcheck":
|
||||
err = cmdHealthcheck(os.Args[2:])
|
||||
// P-WIRE-END
|
||||
default:
|
||||
fmt.Fprintf(os.Stderr, "unknown command: %s\n", os.Args[1])
|
||||
os.Exit(2)
|
||||
}
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
+55
-1
@@ -4,11 +4,13 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
@@ -23,6 +25,12 @@ func cmdServe(_ []string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// P-WIRE-BEGIN
|
||||
if err := cfg.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
setupJSONLogger(cfg.Log)
|
||||
// P-WIRE-END
|
||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||
defer stop()
|
||||
return runServe(ctx, cfg)
|
||||
@@ -41,6 +49,16 @@ func runServe(ctx context.Context, cfg config.Config) error {
|
||||
}
|
||||
defer func() { _ = db.Close() }()
|
||||
|
||||
// P-WIRE-BEGIN
|
||||
ok, err := store.HasAdminPassword(ctx, db.Write)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !ok {
|
||||
return errors.New("admin password not initialized; run: nixmsg admin init")
|
||||
}
|
||||
// P-WIRE-END
|
||||
|
||||
// 启动恢复入口已挂上(假实现为空操作);M 线替换 message.Service 后生效。
|
||||
if recoverErr := deps.Messages.RecoverOnStart(ctx); recoverErr != nil {
|
||||
return fmt.Errorf("message recover: %w", recoverErr)
|
||||
@@ -62,6 +80,17 @@ func runServe(ctx context.Context, cfg config.Config) error {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte("ok"))
|
||||
})
|
||||
// P-WIRE-BEGIN
|
||||
mux.HandleFunc("GET /readyz", func(w http.ResponseWriter, r *http.Request) {
|
||||
if readyErr := db.Ready(r.Context()); readyErr != nil {
|
||||
slog.Error("readyz failed", "err", readyErr)
|
||||
http.Error(w, "not ready", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte("ok"))
|
||||
})
|
||||
// P-WIRE-END
|
||||
// 确保前端资源被链接进二进制;完整静态托管由后续任务完善。
|
||||
_ = web.Dist()
|
||||
|
||||
@@ -87,9 +116,17 @@ func runServe(ctx context.Context, cfg config.Config) error {
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
// P-WIRE-BEGIN
|
||||
// 先停止接受新连接,再等写队列最多 10 秒,然后断开并退出。
|
||||
shutdownCtx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
_ = srv.Shutdown(shutdownCtx)
|
||||
drainCtx, drainCancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer drainCancel()
|
||||
if drainErr := db.Queue.Drain(drainCtx); drainErr != nil && !errors.Is(drainErr, context.DeadlineExceeded) {
|
||||
slog.Error("write queue drain", "err", drainErr)
|
||||
}
|
||||
// P-WIRE-END
|
||||
serveErr := <-errCh
|
||||
if serveErr != nil && !errors.Is(serveErr, http.ErrServerClosed) {
|
||||
return serveErr
|
||||
@@ -107,3 +144,20 @@ func writeListenAddr(dataDir, addr string) error {
|
||||
path := filepath.Join(dataDir, "listen.addr")
|
||||
return os.WriteFile(path, []byte(addr+"\n"), 0o644)
|
||||
}
|
||||
|
||||
// P-WIRE-BEGIN
|
||||
func setupJSONLogger(cfg config.LogConfig) {
|
||||
level := slog.LevelInfo
|
||||
switch strings.ToLower(strings.TrimSpace(cfg.Level)) {
|
||||
case "debug":
|
||||
level = slog.LevelDebug
|
||||
case "warn", "warning":
|
||||
level = slog.LevelWarn
|
||||
case "error":
|
||||
level = slog.LevelError
|
||||
}
|
||||
h := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: level})
|
||||
slog.SetDefault(slog.New(h))
|
||||
}
|
||||
|
||||
// P-WIRE-END
|
||||
|
||||
@@ -10,23 +10,51 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.asio.asia/nixevol/NixMsg/internal/auth"
|
||||
"git.asio.asia/nixevol/NixMsg/internal/config"
|
||||
"git.asio.asia/nixevol/NixMsg/internal/store"
|
||||
)
|
||||
|
||||
func writeTestConfig(t *testing.T, dataDir string) string {
|
||||
t.Helper()
|
||||
cfgPath := filepath.Join(dataDir, "config.yaml")
|
||||
cfgYAML := []byte("listen: \"127.0.0.1:0\"\ndata_dir: \"" + filepath.ToSlash(dataDir) + "\"\nlog:\n level: error\n")
|
||||
if err := os.WriteFile(cfgPath, cfgYAML, 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return cfgPath
|
||||
}
|
||||
|
||||
func initAdminForTest(t *testing.T, dataDir string) {
|
||||
t.Helper()
|
||||
db, err := store.Open(dataDir, "FULL")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() { _ = db.Close() }()
|
||||
phc, err := auth.HashPassword("test-admin-password-xx")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := store.SetAdminPasswordHash(context.Background(), db.Write, phc); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServeHealthzAndListenAddr(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dataDir := t.TempDir()
|
||||
cfgPath := filepath.Join(dataDir, "config.yaml")
|
||||
cfgYAML := []byte("listen: \"127.0.0.1:0\"\ndata_dir: \"" + filepath.ToSlash(dataDir) + "\"\n")
|
||||
if err := os.WriteFile(cfgPath, cfgYAML, 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cfgPath := writeTestConfig(t, dataDir)
|
||||
initAdminForTest(t, dataDir)
|
||||
|
||||
cfg, err := config.Load(cfgPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if vErr := cfg.Validate(); vErr != nil {
|
||||
t.Fatal(vErr)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
@@ -37,7 +65,7 @@ func TestServeHealthzAndListenAddr(t *testing.T) {
|
||||
}()
|
||||
|
||||
var addr string
|
||||
deadline := time.Now().Add(5 * time.Second)
|
||||
deadline := time.Now().Add(10 * time.Second)
|
||||
for time.Now().Before(deadline) {
|
||||
b, readErr := os.ReadFile(filepath.Join(dataDir, "listen.addr"))
|
||||
if readErr == nil {
|
||||
@@ -65,6 +93,15 @@ func TestServeHealthzAndListenAddr(t *testing.T) {
|
||||
t.Fatalf("unexpected body: %q", body)
|
||||
}
|
||||
|
||||
ready, err := http.Get("http://" + addr + "/readyz")
|
||||
if err != nil {
|
||||
t.Fatalf("readyz: %v", err)
|
||||
}
|
||||
defer func() { _ = ready.Body.Close() }()
|
||||
if ready.StatusCode != http.StatusOK {
|
||||
t.Fatalf("readyz status=%d", ready.StatusCode)
|
||||
}
|
||||
|
||||
if _, err := os.Stat(filepath.Join(dataDir, "nixmsg.db")); err != nil {
|
||||
t.Fatalf("db missing: %v", err)
|
||||
}
|
||||
@@ -75,7 +112,23 @@ func TestServeHealthzAndListenAddr(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("serve exit: %v", err)
|
||||
}
|
||||
case <-time.After(5 * time.Second):
|
||||
case <-time.After(10 * time.Second):
|
||||
t.Fatal("serve did not stop")
|
||||
}
|
||||
}
|
||||
|
||||
func TestServeRejectsWithoutAdmin(t *testing.T) {
|
||||
t.Parallel()
|
||||
dataDir := t.TempDir()
|
||||
cfgPath := writeTestConfig(t, dataDir)
|
||||
cfg, err := config.Load(cfgPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
err = runServe(ctx, cfg)
|
||||
if err == nil || !strings.Contains(err.Error(), "admin password") {
|
||||
t.Fatalf("want admin init error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user