Files
nadir-agent/internal/modules/system/system_handler_test.go
T
urania ac196e720b
build-and-release / release (push) Successful in 2m38s
fix: add locale
2026-06-22 21:58:01 +02:00

315 lines
9.1 KiB
Go

package system
import (
"encoding/json"
"net/http"
"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 TestSystemHandlers(t *testing.T) {
mux := http.NewServeMux()
api := humatest.Wrap(t, humago.New(mux, huma.DefaultConfig("Test", "1.0.0")))
registerHostname(api)
registerTimedate(api)
registerLocale(api)
registerPower(api)
registerInfo(api)
// Mock uname for GET /api/system/info
oscmd.SetMock("uname", func(args []string) oscmd.MockCommand {
if reflect.DeepEqual(args, []string{"-r"}) {
return oscmd.MockCommand{Stdout: "6.9.1-1-test\n", ExitCode: 0}
}
if reflect.DeepEqual(args, []string{"-m"}) {
return oscmd.MockCommand{Stdout: "x86_64\n", ExitCode: 0}
}
return oscmd.MockCommand{ExitCode: 1}
})
defer oscmd.ClearMocks()
// 1. Test GET /api/system/info
resp := api.Get("/api/system/info")
if resp.Code != http.StatusOK {
t.Errorf("get info: got %d, want %d", resp.Code, http.StatusOK)
}
// 2. Test GET & POST /api/system/hostname
oscmd.SetMock("hostnamectl", func(args []string) oscmd.MockCommand {
if reflect.DeepEqual(args, []string{"hostname"}) {
return oscmd.MockCommand{Stdout: "server01\n", ExitCode: 0}
}
if reflect.DeepEqual(args, []string{"set-hostname", "--", "server02"}) {
return oscmd.MockCommand{ExitCode: 0}
}
return oscmd.MockCommand{ExitCode: 1}
})
resp = api.Get("/api/system/hostname")
if resp.Code != http.StatusOK {
t.Errorf("get hostname: got %d, want %d", resp.Code, http.StatusOK)
}
var hostnameRes GetHostnameOutput
if err := json.Unmarshal(resp.Body.Bytes(), &hostnameRes.Body); err != nil {
t.Fatal(err)
}
if hostnameRes.Body.Hostname != "server01" {
t.Errorf("got hostname %q, want %q", hostnameRes.Body.Hostname, "server01")
}
resp = api.Post("/api/system/hostname", struct {
Hostname string `json:"hostname"`
}{
Hostname: "server02",
})
if resp.Code != http.StatusOK {
t.Errorf("set hostname: got %d, want %d", resp.Code, http.StatusOK)
}
// 3. Test GET & POST /api/system/time
oscmd.SetMock("timedatectl", func(args []string) oscmd.MockCommand {
if reflect.DeepEqual(args, []string{"show"}) {
showOut := "Timezone=Europe/Rome\nLocalRTC=no\nNTP=yes\nNTPSynchronized=yes\nCanNTP=yes\n"
return oscmd.MockCommand{Stdout: showOut, ExitCode: 0}
}
if reflect.DeepEqual(args, []string{"list-timezones"}) {
return oscmd.MockCommand{Stdout: "Europe/Rome\nUTC\n", ExitCode: 0}
}
if reflect.DeepEqual(args, []string{"set-timezone", "Europe/Rome"}) {
return oscmd.MockCommand{ExitCode: 0}
}
if reflect.DeepEqual(args, []string{"set-ntp", "true"}) {
return oscmd.MockCommand{ExitCode: 0}
}
if len(args) == 2 && args[0] == "set-time" {
return oscmd.MockCommand{ExitCode: 0}
}
return oscmd.MockCommand{ExitCode: 1}
})
resp = api.Get("/api/system/time")
if resp.Code != http.StatusOK {
t.Errorf("get time: got %d, want %d", resp.Code, http.StatusOK)
}
var timeRes GetTimeOutput
if err := json.Unmarshal(resp.Body.Bytes(), &timeRes.Body); err != nil {
t.Fatal(err)
}
if timeRes.Body.Timezone != "Europe/Rome" || !timeRes.Body.NTP {
t.Errorf("got time settings: %+v", timeRes.Body)
}
resp = api.Get("/api/system/timezones")
if resp.Code != http.StatusOK {
t.Errorf("list timezones: got %d, want %d", resp.Code, http.StatusOK)
}
resp = api.Post("/api/system/timezone", struct {
Timezone string `json:"timezone"`
}{
Timezone: "Europe/Rome",
})
if resp.Code != http.StatusOK {
t.Errorf("set timezone: got %d, want %d", resp.Code, http.StatusOK)
}
resp = api.Post("/api/system/ntp", struct {
Enabled bool `json:"enabled"`
}{
Enabled: true,
})
if resp.Code != http.StatusOK {
t.Errorf("set ntp: got %d, want %d", resp.Code, http.StatusOK)
}
resp = api.Post("/api/system/time", struct {
Time string `json:"time"`
}{
Time: "2026-06-20T12:00:00Z",
})
if resp.Code != http.StatusOK {
t.Errorf("set time: got %d, want %d", resp.Code, http.StatusOK)
}
// 4. Test GET & POST /api/system/locale
oscmd.SetMock("localectl", func(args []string) oscmd.MockCommand {
if reflect.DeepEqual(args, []string{"status"}) {
statusOut := " System Locale: LANG=it_IT.UTF-8\n VC Keymap: it\n X11 Layout: it\n"
return oscmd.MockCommand{Stdout: statusOut, ExitCode: 0}
}
if reflect.DeepEqual(args, []string{"list-locales"}) {
return oscmd.MockCommand{Stdout: "it_IT.UTF-8\nen_US.UTF-8\n", ExitCode: 0}
}
if reflect.DeepEqual(args, []string{"set-locale", "LANG=it_IT.UTF-8"}) {
return oscmd.MockCommand{ExitCode: 0}
}
if reflect.DeepEqual(args, []string{"list-keymaps"}) {
return oscmd.MockCommand{Stdout: "it\nus\n", ExitCode: 0}
}
if reflect.DeepEqual(args, []string{"set-keymap", "it"}) {
return oscmd.MockCommand{ExitCode: 0}
}
return oscmd.MockCommand{ExitCode: 1}
})
resp = api.Get("/api/system/locale")
if resp.Code != http.StatusOK {
t.Errorf("get locale: got %d, want %d", resp.Code, http.StatusOK)
}
var localeRes GetLocaleOutput
if err := json.Unmarshal(resp.Body.Bytes(), &localeRes.Body); err != nil {
t.Fatal(err)
}
if localeRes.Body.Lang != "it_IT.UTF-8" || localeRes.Body.VCKeymap != "it" {
t.Errorf("got locale status: %+v", localeRes.Body)
}
resp = api.Get("/api/system/locales")
if resp.Code != http.StatusOK {
t.Errorf("list locales: got %d, want %d", resp.Code, http.StatusOK)
}
resp = api.Post("/api/system/locale", struct {
Lang string `json:"lang"`
}{
Lang: "it_IT.UTF-8",
})
if resp.Code != http.StatusOK {
t.Errorf("set locale: got %d, want %d", resp.Code, http.StatusOK)
}
resp = api.Get("/api/system/keymaps")
if resp.Code != http.StatusOK {
t.Errorf("list keymaps: got %d, want %d", resp.Code, http.StatusOK)
}
resp = api.Post("/api/system/keymap", struct {
Keymap string `json:"keymap"`
}{
Keymap: "it",
})
if resp.Code != http.StatusOK {
t.Errorf("set keymap: got %d, want %d", resp.Code, http.StatusOK)
}
// 4b. Test POST /api/system/locale/generate (validation & idempotent)
// Empty locale → 422 (huma validates non-empty before handler runs)
resp = api.Post("/api/system/locale/generate", struct {
Locale string `json:"locale"`
}{
Locale: "",
})
if resp.Code != http.StatusBadRequest && resp.Code != http.StatusUnprocessableEntity {
t.Errorf("generate empty locale: got %d, want 400 or 422", resp.Code)
}
// Invalid format → 400
resp = api.Post("/api/system/locale/generate", struct {
Locale string `json:"locale"`
}{
Locale: "not-a-locale!!",
})
if resp.Code != http.StatusBadRequest {
t.Errorf("generate invalid locale: got %d, want %d", resp.Code, http.StatusBadRequest)
}
// Already generated (it_IT.UTF-8 is in list-locales mock) → 200 (idempotent)
resp = api.Post("/api/system/locale/generate", struct {
Locale string `json:"locale"`
}{
Locale: "it_IT.UTF-8",
})
if resp.Code != http.StatusOK {
t.Errorf("generate existing locale (idempotent): got %d, want %d", resp.Code, http.StatusOK)
}
// 5. Test POST /api/system/reboot and /api/system/poweroff
oscmd.SetMock("shutdown", func(args []string) oscmd.MockCommand {
if reflect.DeepEqual(args, []string{"-r", "now"}) || reflect.DeepEqual(args, []string{"-h", "now"}) {
return oscmd.MockCommand{ExitCode: 0}
}
return oscmd.MockCommand{ExitCode: 1}
})
resp = api.Post("/api/system/reboot", struct {
When string `json:"when"`
}{
When: "now",
})
if resp.Code != http.StatusOK {
t.Errorf("reboot: got %d, want %d", resp.Code, http.StatusOK)
}
resp = api.Post("/api/system/poweroff", struct {
When string `json:"when"`
}{
When: "now",
})
if resp.Code != http.StatusOK {
t.Errorf("poweroff: got %d, want %d", resp.Code, http.StatusOK)
}
}
func TestUncommentLocaleGen(t *testing.T) {
const sampleLocaleGen = `# This file lists locales that you wish to have built.
#
# en_US.UTF-8 UTF-8
# fr_FR.UTF-8 UTF-8
# de_DE.UTF-8 UTF-8
it_IT.UTF-8 UTF-8
`
tests := []struct {
name string
locale string
wantFound bool
wantSubstr string // substring that should appear uncommented
}{
{
name: "uncomment commented locale",
locale: "fr_FR.UTF-8",
wantFound: true,
wantSubstr: "\nfr_FR.UTF-8 UTF-8\n",
},
{
name: "already uncommented",
locale: "it_IT.UTF-8",
wantFound: true,
},
{
name: "locale not in file",
locale: "ja_JP.UTF-8",
wantFound: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, found := uncommentLocaleGen(sampleLocaleGen, tt.locale)
if found != tt.wantFound {
t.Errorf("found = %v, want %v", found, tt.wantFound)
}
if tt.wantSubstr != "" && !contains(result, tt.wantSubstr) {
t.Errorf("result does not contain %q:\n%s", tt.wantSubstr, result)
}
// The commented versions of OTHER locales should remain commented.
if tt.locale == "fr_FR.UTF-8" && !contains(result, "# en_US.UTF-8 UTF-8") {
t.Errorf("other locales should stay commented:\n%s", result)
}
})
}
}
func contains(s, substr string) bool {
return len(s) >= len(substr) && (s == substr || len(substr) == 0 ||
(len(s) > 0 && strings.Contains(s, substr)))
}