gitlink-cli/shortcuts/member/member_test.go

447 lines
14 KiB
Go

package member
import (
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"reflect"
"testing"
"github.com/gitlink-org/gitlink-cli/internal/client"
"github.com/gitlink-org/gitlink-cli/shortcuts/common"
)
func TestMemberList(t *testing.T) {
server := newMemberTestServer(t, func(w http.ResponseWriter, r *http.Request) {
assertRequest(t, r, "GET", "/owner/repo/collaborators.json")
writeJSON(t, w, map[string]interface{}{"total_count": 1, "members": []interface{}{}})
})
defer server.Close()
if err := runMemberShortcut(t, server, "list", nil); err != nil {
t.Fatalf("list shortcut failed: %v", err)
}
}
func TestMemberAdd(t *testing.T) {
var payload map[string]interface{}
server := newMemberTestServer(t, func(w http.ResponseWriter, r *http.Request) {
assertRequest(t, r, "POST", "/owner/repo/collaborators.json")
payload = decodeJSON(t, r)
writeJSON(t, w, map[string]interface{}{"status": 0, "message": "success"})
})
defer server.Close()
if err := runMemberShortcut(t, server, "add", map[string]string{"user-id": "101"}); err != nil {
t.Fatalf("add shortcut failed: %v", err)
}
assertNumber(t, payload["user_id"], 101)
}
func TestMemberBatchAdd(t *testing.T) {
var seen []int
server := newMemberTestServer(t, func(w http.ResponseWriter, r *http.Request) {
assertRequest(t, r, "POST", "/owner/repo/collaborators.json")
payload := decodeJSON(t, r)
seen = append(seen, int(payload["user_id"].(float64)))
writeJSON(t, w, map[string]interface{}{"status": 0, "message": "success"})
})
defer server.Close()
csvPath := writeTempCSV(t, "user_id\n102\n103\n")
err := runMemberShortcut(t, server, "batch-add", map[string]string{
"user-ids": "101,102",
"from": csvPath,
})
if err != nil {
t.Fatalf("batch-add shortcut failed: %v", err)
}
want := []int{101, 102, 103}
if !reflect.DeepEqual(seen, want) {
t.Fatalf("batch-add user IDs = %v, want %v", seen, want)
}
}
func TestMemberBatchAddDryRunDoesNotCallAPI(t *testing.T) {
server := newMemberTestServer(t, func(w http.ResponseWriter, r *http.Request) {
t.Fatalf("dry-run should not call API, got: %s %s", r.Method, r.URL.Path)
})
defer server.Close()
err := runMemberShortcut(t, server, "batch-add", map[string]string{
"user-ids": "101,102",
"dry-run": "true",
})
if err != nil {
t.Fatalf("batch-add dry-run failed: %v", err)
}
}
func TestMemberBatchAddReturnsErrorWhenAnyRequestFails(t *testing.T) {
server := newMemberTestServer(t, func(w http.ResponseWriter, r *http.Request) {
assertRequest(t, r, "POST", "/owner/repo/collaborators.json")
payload := decodeJSON(t, r)
if int(payload["user_id"].(float64)) == 102 {
http.Error(w, "member add failed", http.StatusBadRequest)
return
}
writeJSON(t, w, map[string]interface{}{"status": 0, "message": "success"})
})
defer server.Close()
err := runMemberShortcut(t, server, "batch-add", map[string]string{
"user-ids": "101,102",
})
if err == nil {
t.Fatal("expected batch-add to return an error when one request fails")
}
}
func TestMemberRemove(t *testing.T) {
var payload map[string]interface{}
server := newMemberTestServer(t, func(w http.ResponseWriter, r *http.Request) {
assertRequest(t, r, "DELETE", "/owner/repo/collaborators/remove.json")
payload = decodeJSON(t, r)
writeJSON(t, w, map[string]interface{}{"status": 0, "message": "success"})
})
defer server.Close()
if err := runMemberShortcut(t, server, "remove", map[string]string{"user-id": "101"}); err != nil {
t.Fatalf("remove shortcut failed: %v", err)
}
assertNumber(t, payload["user_id"], 101)
}
func TestMemberRole(t *testing.T) {
var payload map[string]interface{}
server := newMemberTestServer(t, func(w http.ResponseWriter, r *http.Request) {
assertRequest(t, r, "PUT", "/owner/repo/collaborators/change_role.json")
payload = decodeJSON(t, r)
writeJSON(t, w, map[string]interface{}{"status": 0, "message": "success"})
})
defer server.Close()
err := runMemberShortcut(t, server, "role", map[string]string{
"user-id": "101",
"role": "developer",
})
if err != nil {
t.Fatalf("role shortcut failed: %v", err)
}
assertNumber(t, payload["user_id"], 101)
if payload["role"] != "Developer" {
t.Fatalf("role = %v, want Developer", payload["role"])
}
}
func TestMemberInviteLink(t *testing.T) {
server := newMemberTestServer(t, func(w http.ResponseWriter, r *http.Request) {
assertRequest(t, r, "GET", "/owner/repo/project_invite_links/current_link.json")
if r.URL.Query().Get("role") != "developer" {
t.Fatalf("role query = %q, want developer", r.URL.Query().Get("role"))
}
if r.URL.Query().Get("is_apply") != "false" {
t.Fatalf("is_apply query = %q, want false", r.URL.Query().Get("is_apply"))
}
writeJSON(t, w, map[string]interface{}{"sign": "abc"})
})
defer server.Close()
err := runMemberShortcut(t, server, "invite-link", map[string]string{
"role": "developer",
"apply": "false",
})
if err != nil {
t.Fatalf("invite-link shortcut failed: %v", err)
}
}
func TestMemberInviteInfo(t *testing.T) {
server := newMemberTestServer(t, func(w http.ResponseWriter, r *http.Request) {
assertRequest(t, r, "GET", "/owner/repo/project_invite_links/show_link.json")
if r.URL.Query().Get("invite_sign") != "abc" {
t.Fatalf("invite_sign query = %q, want abc", r.URL.Query().Get("invite_sign"))
}
writeJSON(t, w, map[string]interface{}{"sign": "abc"})
})
defer server.Close()
if err := runMemberShortcut(t, server, "invite-info", map[string]string{"sign": "abc"}); err != nil {
t.Fatalf("invite-info shortcut failed: %v", err)
}
}
func TestMemberAcceptInvite(t *testing.T) {
server := newMemberTestServer(t, func(w http.ResponseWriter, r *http.Request) {
assertRequest(t, r, "POST", "/owner/repo/project_invite_links/redirect_link.json")
if r.URL.Query().Get("invite_sign") != "abc" {
t.Fatalf("invite_sign query = %q, want abc", r.URL.Query().Get("invite_sign"))
}
writeJSON(t, w, map[string]interface{}{"status": 0, "message": "success"})
})
defer server.Close()
if err := runMemberShortcut(t, server, "accept-invite", map[string]string{"sign": "abc"}); err != nil {
t.Fatalf("accept-invite shortcut failed: %v", err)
}
}
func TestMemberApplicationsUsesExplicitUserAndPagination(t *testing.T) {
server := newMemberTestServer(t, func(w http.ResponseWriter, r *http.Request) {
assertRequest(t, r, "GET", "/users/Mengz/applied_projects.json")
if r.URL.Query().Get("page") != "2" {
t.Fatalf("page query = %q, want 2", r.URL.Query().Get("page"))
}
if r.URL.Query().Get("per_page") != "50" {
t.Fatalf("per_page query = %q, want 50", r.URL.Query().Get("per_page"))
}
writeJSON(t, w, map[string]interface{}{"total_count": 1, "applied_projects": []interface{}{}})
})
defer server.Close()
err := runMemberShortcut(t, server, "applications", map[string]string{
"user": "Mengz",
"page": "2",
"per-page": "50",
})
if err != nil {
t.Fatalf("applications shortcut failed: %v", err)
}
}
func TestMemberApplicationsDefaultsToCurrentUserWhenOwnerMissing(t *testing.T) {
var paths []string
server := newMemberTestServer(t, func(w http.ResponseWriter, r *http.Request) {
paths = append(paths, r.URL.Path)
switch r.URL.Path {
case "/users/me.json":
assertRequest(t, r, "GET", "/users/me.json")
writeJSON(t, w, map[string]interface{}{"login": "Mengz"})
case "/users/Mengz/applied_projects.json":
assertRequest(t, r, "GET", "/users/Mengz/applied_projects.json")
writeJSON(t, w, map[string]interface{}{"total_count": 0, "applied_projects": []interface{}{}})
default:
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
}
})
defer server.Close()
if err := runMemberShortcutWithOwner(t, server, "", "applications", nil); err != nil {
t.Fatalf("applications shortcut failed: %v", err)
}
want := []string{"/users/me.json", "/users/Mengz/applied_projects.json"}
if !reflect.DeepEqual(paths, want) {
t.Fatalf("request paths = %v, want %v", paths, want)
}
}
func TestMemberAcceptApplication(t *testing.T) {
server := newMemberTestServer(t, func(w http.ResponseWriter, r *http.Request) {
assertRequest(t, r, "POST", "/users/Mengz/applied_projects/42/accept.json")
writeJSON(t, w, map[string]interface{}{"id": 42, "status": "accepted"})
})
defer server.Close()
err := runMemberShortcut(t, server, "accept-application", map[string]string{
"user": "Mengz",
"id": "42",
})
if err != nil {
t.Fatalf("accept-application shortcut failed: %v", err)
}
}
func TestMemberRefuseApplicationDryRunDoesNotCallAPI(t *testing.T) {
server := newMemberTestServer(t, func(w http.ResponseWriter, r *http.Request) {
t.Fatalf("dry-run should not call API, got: %s %s", r.Method, r.URL.Path)
})
defer server.Close()
err := runMemberShortcut(t, server, "refuse-application", map[string]string{
"user": "Mengz",
"id": "43",
"dry-run": "true",
})
if err != nil {
t.Fatalf("refuse-application dry-run failed: %v", err)
}
}
func TestMemberApplicationRejectsInvalidIDBeforeAPI(t *testing.T) {
server := newMemberTestServer(t, func(w http.ResponseWriter, r *http.Request) {
t.Fatalf("invalid id should not call API, got: %s %s", r.Method, r.URL.Path)
})
defer server.Close()
err := runMemberShortcut(t, server, "accept-application", map[string]string{
"user": "Mengz",
"id": "0",
})
if err == nil {
t.Fatal("expected invalid id to return an error")
}
}
func TestMemberApply(t *testing.T) {
var payload map[string]interface{}
server := newMemberTestServer(t, func(w http.ResponseWriter, r *http.Request) {
assertRequest(t, r, "POST", "/applied_projects.json")
payload = decodeJSON(t, r)
writeJSON(t, w, map[string]interface{}{"status": 0, "message": "success"})
})
defer server.Close()
err := runMemberShortcut(t, server, "apply", map[string]string{
"code": "invite-code",
"role": "reporter",
})
if err != nil {
t.Fatalf("apply shortcut failed: %v", err)
}
appliedProject, ok := payload["applied_project"].(map[string]interface{})
if !ok {
t.Fatalf("applied_project payload = %v, want object", payload["applied_project"])
}
if appliedProject["code"] != "invite-code" {
t.Fatalf("code = %v, want invite-code", appliedProject["code"])
}
if appliedProject["role"] != "reporter" {
t.Fatalf("role = %v, want reporter", appliedProject["role"])
}
}
func TestMemberApplyDryRunDoesNotCallAPI(t *testing.T) {
server := newMemberTestServer(t, func(w http.ResponseWriter, r *http.Request) {
t.Fatalf("dry-run should not call API, got: %s %s", r.Method, r.URL.Path)
})
defer server.Close()
err := runMemberShortcut(t, server, "apply", map[string]string{
"code": "invite-code",
"role": "developer",
"dry-run": "true",
})
if err != nil {
t.Fatalf("apply dry-run failed: %v", err)
}
}
func TestMemberApplyRejectsInvalidRoleBeforeAPI(t *testing.T) {
server := newMemberTestServer(t, func(w http.ResponseWriter, r *http.Request) {
t.Fatalf("invalid role should not call API, got: %s %s", r.Method, r.URL.Path)
})
defer server.Close()
err := runMemberShortcut(t, server, "apply", map[string]string{
"code": "invite-code",
"role": "owner",
})
if err == nil {
t.Fatal("expected invalid role to return an error")
}
}
func TestCollectUserIDs(t *testing.T) {
csvPath := writeTempCSV(t, "name,id\nfirst,102\nsecond,103\n")
got, err := collectUserIDs("101,102", csvPath)
if err != nil {
t.Fatalf("collectUserIDs returned error: %v", err)
}
want := []int{101, 102, 103}
if !reflect.DeepEqual(got, want) {
t.Fatalf("collectUserIDs() = %v, want %v", got, want)
}
}
func TestNormalizeRoleRejectsInvalidRole(t *testing.T) {
if _, err := normalizeRole("owner"); err == nil {
t.Fatal("expected invalid role to return an error")
}
}
func runMemberShortcut(t *testing.T, server *httptest.Server, name string, args map[string]string) error {
t.Helper()
return runMemberShortcutWithOwner(t, server, "owner", name, args)
}
func runMemberShortcutWithOwner(t *testing.T, server *httptest.Server, owner, name string, args map[string]string) error {
t.Helper()
shortcut := findMemberShortcut(t, name)
ctx := &common.RuntimeContext{
Client: &client.Client{
HTTP: server.Client(),
BaseURL: server.URL,
},
Owner: owner,
Repo: "repo",
Format: "json",
Args: args,
}
if ctx.Args == nil {
ctx.Args = map[string]string{}
}
return shortcut.Run(ctx)
}
func findMemberShortcut(t *testing.T, name string) *common.Shortcut {
t.Helper()
for _, shortcut := range Shortcuts() {
if shortcut.Name == name {
return shortcut
}
}
t.Fatalf("shortcut %q not found", name)
return nil
}
func newMemberTestServer(t *testing.T, handler http.HandlerFunc) *httptest.Server {
t.Helper()
return httptest.NewServer(handler)
}
func assertRequest(t *testing.T, r *http.Request, method, path string) {
t.Helper()
if r.Method != method || r.URL.Path != path {
t.Fatalf("got request %s %s, want %s %s", r.Method, r.URL.Path, method, path)
}
}
func decodeJSON(t *testing.T, r *http.Request) map[string]interface{} {
t.Helper()
var payload map[string]interface{}
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
t.Fatalf("failed to decode request body: %v", err)
}
return payload
}
func writeJSON(t *testing.T, w http.ResponseWriter, payload interface{}) {
t.Helper()
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(payload); err != nil {
t.Fatalf("failed to write response: %v", err)
}
}
func assertNumber(t *testing.T, got interface{}, want int) {
t.Helper()
value, ok := got.(float64)
if !ok {
t.Fatalf("got %v (%T), want JSON number", got, got)
}
if int(value) != want {
t.Fatalf("got %v, want %d", got, want)
}
}
func writeTempCSV(t *testing.T, content string) string {
t.Helper()
path := filepath.Join(t.TempDir(), "members.csv")
if err := os.WriteFile(path, []byte(content), 0o600); err != nil {
t.Fatalf("write temp csv: %v", err)
}
return path
}