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"))) registerUsers(api, nil) // 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) } }