2026-06-22 16:06:57 +02:00
|
|
|
package users
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"encoding/json"
|
|
|
|
|
"net/http"
|
|
|
|
|
"os"
|
|
|
|
|
"path/filepath"
|
|
|
|
|
"reflect"
|
|
|
|
|
"testing"
|
|
|
|
|
|
|
|
|
|
"nadir/internal/oscmd"
|
|
|
|
|
|
|
|
|
|
"github.com/danielgtaylor/huma/v2"
|
|
|
|
|
"github.com/danielgtaylor/huma/v2/adapters/humago"
|
|
|
|
|
"github.com/danielgtaylor/huma/v2/humatest"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func TestUsersHandlers(t *testing.T) {
|
|
|
|
|
tempPasswd := filepath.Join(t.TempDir(), "passwd")
|
|
|
|
|
initialContent := "root:x:0:0:root:/root:/bin/bash\nalice:x:1000:1000:Alice Smith:/home/alice:/bin/bash\n"
|
|
|
|
|
if err := os.WriteFile(tempPasswd, []byte(initialContent), 0644); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
oldPasswd := passwdPath
|
|
|
|
|
passwdPath = tempPasswd
|
|
|
|
|
defer func() { passwdPath = oldPasswd }()
|
|
|
|
|
|
|
|
|
|
mux := http.NewServeMux()
|
|
|
|
|
api := humatest.Wrap(t, humago.New(mux, huma.DefaultConfig("Test", "1.0.0")))
|
2026-06-24 17:29:45 +02:00
|
|
|
registerUsers(api, nil)
|
2026-06-22 16:06:57 +02:00
|
|
|
|
|
|
|
|
// 1. Test GET /api/users
|
|
|
|
|
resp := api.Get("/api/users")
|
|
|
|
|
if resp.Code != http.StatusOK {
|
|
|
|
|
t.Errorf("list users: got %d, want %d", resp.Code, http.StatusOK)
|
|
|
|
|
}
|
|
|
|
|
var listRes ListUsersOutput
|
|
|
|
|
if err := json.Unmarshal(resp.Body.Bytes(), &listRes.Body); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if len(listRes.Body.Users) != 2 {
|
|
|
|
|
t.Errorf("got %d users, want 2", len(listRes.Body.Users))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 2. Test GET /api/users/{username}
|
|
|
|
|
resp = api.Get("/api/users/alice")
|
|
|
|
|
if resp.Code != http.StatusOK {
|
|
|
|
|
t.Errorf("get user: got %d, want %d", resp.Code, http.StatusOK)
|
|
|
|
|
}
|
|
|
|
|
var getRes GetUserOutput
|
|
|
|
|
if err := json.Unmarshal(resp.Body.Bytes(), &getRes.Body); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if getRes.Body.Username != "alice" || getRes.Body.UID != 1000 {
|
|
|
|
|
t.Errorf("get user: got %+v", getRes.Body)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
resp = api.Get("/api/users/bob")
|
|
|
|
|
if resp.Code != http.StatusNotFound {
|
|
|
|
|
t.Errorf("get non-existent user: got %d, want %d", resp.Code, http.StatusNotFound)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 3. Test POST /api/users
|
|
|
|
|
oscmd.SetMock("useradd", func(args []string) oscmd.MockCommand {
|
|
|
|
|
wantArgs := []string{"-m", "-c", "Bob Jones", "-s", "/bin/sh", "--", "bob"}
|
|
|
|
|
if !reflect.DeepEqual(args, wantArgs) {
|
|
|
|
|
t.Errorf("useradd args: got %v, want %v", args, wantArgs)
|
|
|
|
|
}
|
|
|
|
|
bobContent := initialContent + "bob:x:1001:1001:Bob Jones:/home/bob:/bin/sh\n"
|
|
|
|
|
os.WriteFile(tempPasswd, []byte(bobContent), 0644)
|
|
|
|
|
return oscmd.MockCommand{ExitCode: 0}
|
|
|
|
|
})
|
|
|
|
|
defer oscmd.ClearMocks()
|
|
|
|
|
|
|
|
|
|
resp = api.Post("/api/users", struct {
|
|
|
|
|
Username string `json:"username"`
|
|
|
|
|
Comment string `json:"comment"`
|
|
|
|
|
Shell string `json:"shell"`
|
|
|
|
|
CreateHome bool `json:"create_home"`
|
|
|
|
|
}{
|
|
|
|
|
Username: "bob",
|
|
|
|
|
Comment: "Bob Jones",
|
|
|
|
|
Shell: "/bin/sh",
|
|
|
|
|
CreateHome: true,
|
|
|
|
|
})
|
|
|
|
|
if resp.Code != http.StatusOK {
|
|
|
|
|
t.Errorf("create user: got %d, want %d", resp.Code, http.StatusOK)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 4. Test DELETE /api/users/{username}
|
|
|
|
|
oscmd.SetMock("userdel", func(args []string) oscmd.MockCommand {
|
|
|
|
|
wantArgs := []string{"-r", "--", "bob"}
|
|
|
|
|
if !reflect.DeepEqual(args, wantArgs) {
|
|
|
|
|
t.Errorf("userdel args: got %v, want %v", args, wantArgs)
|
|
|
|
|
}
|
|
|
|
|
os.WriteFile(tempPasswd, []byte(initialContent), 0644)
|
|
|
|
|
return oscmd.MockCommand{ExitCode: 0}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
resp = api.Delete("/api/users/bob?remove_home=true")
|
|
|
|
|
if resp.Code != http.StatusOK {
|
|
|
|
|
t.Errorf("delete user: got %d, want %d", resp.Code, http.StatusOK)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 5. Test POST /api/users/{username}/password
|
|
|
|
|
oscmd.SetMock("chpasswd", func(args []string) oscmd.MockCommand {
|
|
|
|
|
return oscmd.MockCommand{ExitCode: 0}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
resp = api.Post("/api/users/alice/password", struct {
|
|
|
|
|
Password string `json:"password"`
|
|
|
|
|
}{
|
|
|
|
|
Password: "newsecretpwd",
|
|
|
|
|
})
|
|
|
|
|
if resp.Code != http.StatusOK {
|
|
|
|
|
t.Errorf("set password: got %d, want %d", resp.Code, http.StatusOK)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 6. Test PUT /api/users/{username}/groups
|
|
|
|
|
oscmd.SetMock("usermod", func(args []string) oscmd.MockCommand {
|
|
|
|
|
wantArgs := []string{"-G", "wheel,dev", "--", "alice"}
|
|
|
|
|
if !reflect.DeepEqual(args, wantArgs) {
|
|
|
|
|
t.Errorf("usermod args: got %v, want %v", args, wantArgs)
|
|
|
|
|
}
|
|
|
|
|
return oscmd.MockCommand{ExitCode: 0}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
oscmd.SetMock("id", func(args []string) oscmd.MockCommand {
|
|
|
|
|
wantArgs := []string{"-nG", "alice"}
|
|
|
|
|
if !reflect.DeepEqual(args, wantArgs) {
|
|
|
|
|
t.Errorf("id args: got %v, want %v", args, wantArgs)
|
|
|
|
|
}
|
|
|
|
|
return oscmd.MockCommand{Stdout: "alice wheel dev\n", ExitCode: 0}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
resp = api.Put("/api/users/alice/groups", struct {
|
|
|
|
|
Groups []string `json:"groups"`
|
|
|
|
|
}{
|
|
|
|
|
Groups: []string{"wheel", "dev"},
|
|
|
|
|
})
|
|
|
|
|
if resp.Code != http.StatusOK {
|
|
|
|
|
t.Errorf("set groups: got %d, want %d", resp.Code, http.StatusOK)
|
|
|
|
|
}
|
|
|
|
|
}
|