155 lines
5.5 KiB
Go
155 lines
5.5 KiB
Go
package services
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
|
|
"nadir/internal/oscmd"
|
|
|
|
"github.com/danielgtaylor/huma/v2"
|
|
"github.com/danielgtaylor/huma/v2/adapters/humago"
|
|
"github.com/danielgtaylor/huma/v2/humatest"
|
|
)
|
|
|
|
func TestServicesHandlers(t *testing.T) {
|
|
mux := http.NewServeMux()
|
|
api := humatest.Wrap(t, humago.New(mux, huma.DefaultConfig("Test", "1.0.0")))
|
|
|
|
// Set up allowlisted log files for testing the file source
|
|
logFiles := map[string][]string{
|
|
"nginx.service": {filepath.Join(t.TempDir(), "nginx-error.log")},
|
|
}
|
|
// Create the dummy log file
|
|
errLogPath := logFiles["nginx.service"][0]
|
|
if err := os.WriteFile(errLogPath, []byte("file log line 1\nfile log line 2\n"), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
m := New(logFiles)
|
|
m.Register(api)
|
|
|
|
// 1. Test GET /api/services (list services)
|
|
oscmd.SetMock("systemctl", func(args []string) oscmd.MockCommand {
|
|
if reflect.DeepEqual(args, []string{"list-units", "--type=service", "--all", "-o", "json", "--no-pager"}) {
|
|
units := []ServiceUnit{
|
|
{Unit: "sshd.service", Load: "loaded", Active: "active", Sub: "running", Description: "OpenSSH"},
|
|
}
|
|
data, _ := json.Marshal(units)
|
|
return oscmd.MockCommand{Stdout: string(data) + "\n", ExitCode: 0}
|
|
}
|
|
if reflect.DeepEqual(args, []string{"show", "-p", "Id", "-p", "Description", "-p", "LoadState", "-p", "ActiveState", "-p", "SubState", "-p", "UnitFileState", "--", "sshd.service"}) {
|
|
showOut := "Id=sshd.service\nDescription=OpenSSH\nLoadState=loaded\nActiveState=active\nSubState=running\nUnitFileState=enabled\n"
|
|
return oscmd.MockCommand{Stdout: showOut, ExitCode: 0}
|
|
}
|
|
if reflect.DeepEqual(args, []string{"start", "--", "sshd.service"}) {
|
|
return oscmd.MockCommand{ExitCode: 0}
|
|
}
|
|
return oscmd.MockCommand{ExitCode: 1}
|
|
})
|
|
defer oscmd.ClearMocks()
|
|
|
|
resp := api.Get("/api/services")
|
|
if resp.Code != http.StatusOK {
|
|
t.Errorf("list services: got %d, want %d", resp.Code, http.StatusOK)
|
|
}
|
|
var listRes ListServicesOutput
|
|
if err := json.Unmarshal(resp.Body.Bytes(), &listRes.Body); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(listRes.Body.Services) != 1 || listRes.Body.Services[0].Unit != "sshd.service" {
|
|
t.Errorf("list services output: %+v", listRes.Body)
|
|
}
|
|
|
|
// 2. Test GET /api/services/{unit} (get service status)
|
|
resp = api.Get("/api/services/sshd.service")
|
|
if resp.Code != http.StatusOK {
|
|
t.Errorf("get service status: got %d, want %d", resp.Code, http.StatusOK)
|
|
}
|
|
var getRes GetServiceOutput
|
|
if err := json.Unmarshal(resp.Body.Bytes(), &getRes.Body); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if getRes.Body.Unit != "sshd.service" || getRes.Body.ActiveState != "active" {
|
|
t.Errorf("get service output: %+v", getRes.Body)
|
|
}
|
|
|
|
// 3. Test POST /api/services/{unit}/start
|
|
resp = api.Post("/api/services/sshd.service/start", struct{}{})
|
|
if resp.Code != http.StatusOK {
|
|
t.Errorf("start service: got %d, want %d", resp.Code, http.StatusOK)
|
|
}
|
|
|
|
// 4. Test GET /api/services/{unit}/logs (journal source)
|
|
oscmd.SetMock("journalctl", func(args []string) oscmd.MockCommand {
|
|
if strings.Contains(strings.Join(args, " "), "-f") {
|
|
// Streaming mock
|
|
lines := []string{
|
|
`{"MESSAGE":"streaming line 1","PRIORITY":"6","__REALTIME_TIMESTAMP":"1718873704000000"}`,
|
|
}
|
|
return oscmd.MockCommand{Lines: lines, DelayMs: 1, ExitCode: 0}
|
|
}
|
|
// Regular snapshot mock
|
|
lines := []string{
|
|
`{"MESSAGE":"journal line 1","PRIORITY":"6","__REALTIME_TIMESTAMP":"1718873704000000"}`,
|
|
}
|
|
return oscmd.MockCommand{Stdout: strings.Join(lines, "\n") + "\n", ExitCode: 0}
|
|
})
|
|
|
|
resp = api.Get("/api/services/sshd.service/logs")
|
|
if resp.Code != http.StatusOK {
|
|
t.Errorf("get journal logs: got %d, want %d", resp.Code, http.StatusOK)
|
|
}
|
|
var logsRes LogsOutput
|
|
if err := json.Unmarshal(resp.Body.Bytes(), &logsRes.Body); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(logsRes.Body.Entries) != 1 || logsRes.Body.Entries[0].Message != "journal line 1" {
|
|
t.Errorf("journal logs output: %+v", logsRes.Body)
|
|
}
|
|
|
|
// 5. Test GET /api/services/{unit}/logs (file source)
|
|
oscmd.SetMock("tail", func(args []string) oscmd.MockCommand {
|
|
if strings.Contains(strings.Join(args, " "), "-F") {
|
|
// Streaming mock
|
|
return oscmd.MockCommand{Lines: []string{"stream file line 1"}, DelayMs: 1, ExitCode: 0}
|
|
}
|
|
return oscmd.MockCommand{Stdout: "file log line 1\nfile log line 2\n", ExitCode: 0}
|
|
})
|
|
|
|
resp = api.Get("/api/services/nginx.service/logs?source=file&path=" + errLogPath)
|
|
if resp.Code != http.StatusOK {
|
|
t.Errorf("get file logs: got %d, want %d", resp.Code, http.StatusOK)
|
|
}
|
|
if err := json.Unmarshal(resp.Body.Bytes(), &logsRes.Body); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(logsRes.Body.Entries) != 2 || logsRes.Body.Entries[0].Message != "file log line 1" {
|
|
t.Errorf("file logs output: %+v", logsRes.Body)
|
|
}
|
|
|
|
// 6. Test GET /api/services/{unit}/logs/stream (journal stream)
|
|
resp = api.Get("/api/services/sshd.service/logs/stream")
|
|
if resp.Code != http.StatusOK {
|
|
t.Errorf("stream journal logs: got %d, want %d", resp.Code, http.StatusOK)
|
|
}
|
|
bodyStr := resp.Body.String()
|
|
if !strings.Contains(bodyStr, "streaming line 1") {
|
|
t.Errorf("stream journal logs missing message, got: %q", bodyStr)
|
|
}
|
|
|
|
// 7. Test GET /api/services/{unit}/logs/stream (file stream)
|
|
resp = api.Get("/api/services/nginx.service/logs/stream?source=file&path=" + errLogPath)
|
|
if resp.Code != http.StatusOK {
|
|
t.Errorf("stream file logs: got %d, want %d", resp.Code, http.StatusOK)
|
|
}
|
|
bodyStr = resp.Body.String()
|
|
if !strings.Contains(bodyStr, "stream file line 1") {
|
|
t.Errorf("stream file logs missing message, got: %q", bodyStr)
|
|
}
|
|
}
|