329 lines
9.6 KiB
Go
329 lines
9.6 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 LANGUAGE=en_US:\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{"set-locale", "LANG=it_IT.UTF-8", "LANGUAGE=en_US:"}) {
|
|
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.Language != "en_US:" || 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.Post("/api/system/locale", struct {
|
|
Lang string `json:"lang"`
|
|
Language string `json:"language"`
|
|
}{
|
|
Lang: "it_IT.UTF-8",
|
|
Language: "en_US:",
|
|
})
|
|
if resp.Code != http.StatusOK {
|
|
t.Errorf("set locale with language: 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)))
|
|
}
|