207 lines
4.6 KiB
Go
207 lines
4.6 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestServerHealthViaSubprocess(t *testing.T) {
|
|
dir := t.TempDir()
|
|
port := freePort(t)
|
|
cfgPath := writeServerConfig(t, dir, port)
|
|
|
|
bin := buildServerBinary(t)
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
cmd := exec.CommandContext(ctx, bin, "-config", cfgPath)
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
if err := cmd.Start(); err != nil {
|
|
t.Fatalf("start server: %v", err)
|
|
}
|
|
t.Cleanup(func() {
|
|
cancel()
|
|
_ = cmd.Wait()
|
|
})
|
|
|
|
base := fmt.Sprintf("http://127.0.0.1:%d", port)
|
|
waitForHealth(t, base+"/api/v1/health", 15*time.Second)
|
|
|
|
resp, err := http.Get(base + "/api/v1/health")
|
|
if err != nil {
|
|
t.Fatalf("health GET: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("health status: got %d want 200", resp.StatusCode)
|
|
}
|
|
|
|
var body struct {
|
|
Status string `json:"status"`
|
|
Service string `json:"service"`
|
|
Version string `json:"version"`
|
|
}
|
|
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
|
|
t.Fatalf("decode health: %v", err)
|
|
}
|
|
if body.Status != "ok" {
|
|
t.Fatalf("status: got %q want ok", body.Status)
|
|
}
|
|
if body.Service != "forge-mesh-server" {
|
|
t.Fatalf("service: got %q", body.Service)
|
|
}
|
|
if body.Version == "" {
|
|
t.Fatal("expected version in health response")
|
|
}
|
|
}
|
|
|
|
func TestServerUsesConfigDataDirAndPort(t *testing.T) {
|
|
dir := t.TempDir()
|
|
port := freePort(t)
|
|
cfgPath := writeServerConfig(t, dir, port)
|
|
|
|
cfgData, err := os.ReadFile(cfgPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !containsAll(string(cfgData), dir, fmt.Sprintf("127.0.0.1:%d", port)) {
|
|
t.Fatalf("config missing data_dir or listen_addr: %s", cfgData)
|
|
}
|
|
|
|
dbPath := filepath.Join(dir, "forge-mesh.db")
|
|
artifacts := filepath.Join(dir, "artifacts")
|
|
|
|
bin := buildServerBinary(t)
|
|
ctx, cancel := context.WithTimeout(context.Background(), 12*time.Second)
|
|
defer cancel()
|
|
|
|
cmd := exec.CommandContext(ctx, bin, "-config", cfgPath)
|
|
if err := cmd.Start(); err != nil {
|
|
t.Fatalf("start: %v", err)
|
|
}
|
|
defer func() {
|
|
cancel()
|
|
_ = cmd.Wait()
|
|
}()
|
|
|
|
waitForHealth(t, fmt.Sprintf("http://127.0.0.1:%d/api/v1/health", port), 12*time.Second)
|
|
|
|
for _, path := range []string{dbPath, artifacts, filepath.Join(dir, "signing.key")} {
|
|
if _, err := os.Stat(path); err != nil {
|
|
t.Fatalf("expected server to create %s: %v", path, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func buildServerBinary(t *testing.T) string {
|
|
t.Helper()
|
|
out := filepath.Join(t.TempDir(), "forge-mesh-server")
|
|
cmd := exec.Command("go", "build", "-o", out, "./cmd/server")
|
|
cmd.Dir = repoRoot(t)
|
|
if outBytes, err := cmd.CombinedOutput(); err != nil {
|
|
t.Fatalf("build server: %v\n%s", err, outBytes)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func writeServerConfig(t *testing.T, dir string, port int) string {
|
|
t.Helper()
|
|
cfgPath := filepath.Join(dir, "config.json")
|
|
content := fmt.Sprintf(`{
|
|
"listen_addr": "127.0.0.1:%d",
|
|
"data_dir": %q,
|
|
"database_path": %q,
|
|
"auth": {
|
|
"basic_username": "admin",
|
|
"basic_password": "changeme",
|
|
"fleet_secret": "test-fleet-secret"
|
|
},
|
|
"forge": {
|
|
"signing_key_path": %q,
|
|
"artifacts_dir": %q
|
|
}
|
|
}`, port, dir, filepath.Join(dir, "forge-mesh.db"),
|
|
filepath.Join(dir, "signing.key"), filepath.Join(dir, "artifacts"))
|
|
if err := os.WriteFile(cfgPath, []byte(content), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return cfgPath
|
|
}
|
|
|
|
func freePort(t *testing.T) int {
|
|
t.Helper()
|
|
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer ln.Close()
|
|
return ln.Addr().(*net.TCPAddr).Port
|
|
}
|
|
|
|
func waitForHealth(t *testing.T, url string, timeout time.Duration) {
|
|
t.Helper()
|
|
deadline := time.Now().Add(timeout)
|
|
for time.Now().Before(deadline) {
|
|
resp, err := http.Get(url)
|
|
if err == nil {
|
|
resp.Body.Close()
|
|
if resp.StatusCode == http.StatusOK {
|
|
return
|
|
}
|
|
}
|
|
time.Sleep(200 * time.Millisecond)
|
|
}
|
|
t.Fatalf("server did not become healthy at %s within %s", url, timeout)
|
|
}
|
|
|
|
func repoRoot(t *testing.T) string {
|
|
t.Helper()
|
|
wd, err := os.Getwd()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for {
|
|
if _, err := os.Stat(filepath.Join(wd, "go.mod")); err == nil {
|
|
return wd
|
|
}
|
|
parent := filepath.Dir(wd)
|
|
if parent == wd {
|
|
t.Fatal("go.mod not found")
|
|
}
|
|
wd = parent
|
|
}
|
|
}
|
|
|
|
func containsAll(s string, parts ...string) bool {
|
|
for _, p := range parts {
|
|
if !contains(s, p) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func contains(s, sub string) bool {
|
|
return len(sub) == 0 || (len(s) >= len(sub) && indexOf(s, sub) >= 0)
|
|
}
|
|
|
|
func indexOf(s, sub string) int {
|
|
for i := 0; i+len(sub) <= len(s); i++ {
|
|
if s[i:i+len(sub)] == sub {
|
|
return i
|
|
}
|
|
}
|
|
return -1
|
|
}
|