package main import ( "bytes" "context" "io" "net/http" "net/http/cookiejar" "os" "os/exec" "path/filepath" "runtime" "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 TestBackupMissingDB(t *testing.T) { dir := t.TempDir() path := writeTestConfig(t, dir) t.Setenv("NIXMSG_CONFIG", path) out := filepath.Join(dir, "copy.db") err := cmdBackup([]string{"--out", out}) if err == nil || !strings.Contains(err.Error(), "database not found") { t.Fatalf("want database not found, got %v", err) } if _, statErr := os.Stat(filepath.Join(dir, store.DBFileName)); !os.IsNotExist(statErr) { t.Fatalf("backup must not create %s: %v", store.DBFileName, statErr) } } func TestAdminSetPasswordClearsSessions(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) }() addr := waitListenAddr(t, dir, 15*time.Second) base := "http://" + addr jar, err := cookiejar.New(nil) if err != nil { t.Fatal(err) } client := &http.Client{Jar: jar, Timeout: 10 * time.Second} loginBody := `{"username":"admin","password":"test-admin-password-xx"}` resp, err := client.Post(base+"/api/admin/login", "application/json", strings.NewReader(loginBody)) if err != nil { cancel() <-errCh t.Fatal(err) } _, _ = io.ReadAll(resp.Body) _ = resp.Body.Close() if resp.StatusCode != http.StatusOK { cancel() <-errCh t.Fatalf("login status=%d", resp.StatusCode) } me, err := client.Get(base + "/api/admin/me") if err != nil { cancel() <-errCh t.Fatal(err) } _ = me.Body.Close() if me.StatusCode != http.StatusOK { cancel() <-errCh t.Fatalf("me before set-password status=%d", me.StatusCode) } if err := cmdAdminSetPassword([]string{"--password", "long-enough-password"}); err != nil { cancel() <-errCh t.Fatal(err) } me2, err := client.Get(base + "/api/admin/me") if err != nil { cancel() <-errCh t.Fatal(err) } body, _ := io.ReadAll(me2.Body) _ = me2.Body.Close() cancel() <-errCh if me2.StatusCode != http.StatusUnauthorized { t.Fatalf("want 401 after set-password, got %d body=%s", me2.StatusCode, body) } } 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) } } func TestCmdVersionPrintsInjected(t *testing.T) { old := Version Version = "d05-injected" defer func() { Version = old }() r, w, err := os.Pipe() if err != nil { t.Fatal(err) } oldOut := os.Stdout os.Stdout = w cmdVersion(nil) _ = w.Close() os.Stdout = oldOut var buf bytes.Buffer _, _ = buf.ReadFrom(r) _ = r.Close() if !strings.Contains(buf.String(), "d05-injected") { t.Fatalf("version output=%q", buf.String()) } } func TestGoBuildInjectsVersion(t *testing.T) { _, thisFile, _, ok := runtime.Caller(0) if !ok { t.Fatal("runtime.Caller") } pkgDir := filepath.Dir(thisFile) out := filepath.Join(t.TempDir(), "nixmsg-d05-version.exe") build := exec.Command("go", "build", "-ldflags", "-X main.Version=d05-ldflags", "-o", out) build.Dir = pkgDir build.Env = append(os.Environ(), "CGO_ENABLED=0") if b, err := build.CombinedOutput(); err != nil { t.Fatalf("go build: %v\n%s", err, b) } run := exec.Command(out, "version") got, err := run.CombinedOutput() if err != nil { t.Fatalf("version: %v\n%s", err, got) } if !strings.Contains(string(got), "d05-ldflags") { t.Fatalf("version output=%q", got) } }