From 6bcd8b65d28da6d1f38f1a3a85db0faf1052f352 Mon Sep 17 00:00:00 2001 From: wyxfzgg <3132758001@qq.com> Date: Wed, 3 Jun 2026 11:39:55 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=BB=BA=20wiki=20=E6=A8=A1?= =?UTF-8?q?=E5=9D=97=EF=BC=8C=E6=B7=BB=E5=8A=A0=205=20=E6=9D=A1=20Wiki=20?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E5=91=BD=E4=BB=A4=20(pages/get/create/update?= =?UTF-8?q?/delete)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 shortcuts/wiki/wiki.go: 5 条 Shortcut 命令 - 新增 shortcuts/wiki/wiki_test.go: 9 个测试函数,覆盖正常/异常/缺少参数场景 - 修改 shortcuts/register.go: 注册 wiki 模块 关联 Issue: #13 --- shortcuts/register.go | 4 + shortcuts/wiki/wiki.go | 129 +++++++++++++++++ shortcuts/wiki/wiki_test.go | 281 ++++++++++++++++++++++++++++++++++++ 3 files changed, 414 insertions(+) create mode 100644 shortcuts/wiki/wiki.go create mode 100644 shortcuts/wiki/wiki_test.go diff --git a/shortcuts/register.go b/shortcuts/register.go index b8e7d46..63b387f 100644 --- a/shortcuts/register.go +++ b/shortcuts/register.go @@ -22,6 +22,8 @@ import ( "github.com/gitlink-org/gitlink-cli/shortcuts/search" "github.com/gitlink-org/gitlink-cli/shortcuts/user" "github.com/gitlink-org/gitlink-cli/shortcuts/webhook" + "github.com/gitlink-org/gitlink-cli/shortcuts/webhook" + "github.com/gitlink-org/gitlink-cli/shortcuts/wiki" "github.com/gitlink-org/gitlink-cli/shortcuts/workflow" ) @@ -49,6 +51,7 @@ func RegisterAll(root *cobra.Command, translators ...*i18n.Translator) { "ci": ci.Shortcuts(tr), "compare": compare.Shortcuts(), "webhook": webhook.Shortcuts(tr), + "wiki": wiki.Shortcuts(), "workflow": workflow.Shortcuts(), } @@ -70,6 +73,7 @@ func RegisterAll(root *cobra.Command, translators ...*i18n.Translator) { "ci": tr.T("cmd.ci.short"), "compare": "Compare branches, tags, or commits", "webhook": tr.T("cmd.webhook.short"), + "wiki": "Wiki page operations", "workflow": "AI agent workflow analysis", } diff --git a/shortcuts/wiki/wiki.go b/shortcuts/wiki/wiki.go new file mode 100644 index 0000000..870f51a --- /dev/null +++ b/shortcuts/wiki/wiki.go @@ -0,0 +1,129 @@ +package wiki + +import ( + "fmt" + + "github.com/gitlink-org/gitlink-cli/shortcuts/common" +) + +// Shortcuts returns wiki management shortcuts for GitLink. +// +// The wiki domain provides commands for listing, viewing, creating, +// updating, and deleting wiki pages within a repository. +func Shortcuts() []*common.Shortcut { + return []*common.Shortcut{ + { + Name: "pages", + Description: "列出 Wiki 页面", + Run: func(ctx *common.RuntimeContext) error { + if err := ctx.ResolveOwnerRepo(); err != nil { + return err + } + env, err := ctx.CallAPI("GET", "/api/wiki/wikiPages", nil) + if err != nil { + return err + } + return ctx.Output(env) + }, + }, + { + Name: "get", + Description: "获取 Wiki 页面内容", + Flags: []common.Flag{ + {Name: "id", Short: "i", Usage: "Wiki 页面 ID", Required: true}, + }, + Run: func(ctx *common.RuntimeContext) error { + id, err := ctx.RequireArg("id") + if err != nil { + return err + } + path := fmt.Sprintf("/api/wiki/getWiki?id=%s", id) + env, err := ctx.CallAPI("GET", path, nil) + if err != nil { + return err + } + return ctx.Output(env) + }, + }, + { + Name: "create", + Description: "创建 Wiki 页面", + Flags: []common.Flag{ + {Name: "title", Short: "t", Usage: "页面标题", Required: true}, + {Name: "content", Short: "c", Usage: "页面内容(Markdown)", Required: true}, + {Name: "project", Usage: "项目 ID"}, + }, + Run: func(ctx *common.RuntimeContext) error { + if err := ctx.ResolveOwnerRepo(); err != nil { + return err + } + title, err := ctx.RequireArg("title") + if err != nil { + return err + } + content, err := ctx.RequireArg("content") + if err != nil { + return err + } + body := map[string]interface{}{ + "title": title, + "content": content, + } + if project := ctx.Arg("project"); project != "" { + body["project_id"] = project + } + env, err := ctx.CallAPI("POST", "/api/wiki/createWiki", body) + if err != nil { + return err + } + return ctx.Output(env) + }, + }, + { + Name: "update", + Description: "更新 Wiki 页面", + Flags: []common.Flag{ + {Name: "id", Short: "i", Usage: "Wiki 页面 ID", Required: true}, + {Name: "title", Short: "t", Usage: "新标题"}, + {Name: "content", Short: "c", Usage: "新内容(Markdown)"}, + }, + Run: func(ctx *common.RuntimeContext) error { + id, err := ctx.RequireArg("id") + if err != nil { + return err + } + body := map[string]interface{}{"id": id} + if t := ctx.Arg("title"); t != "" { + body["title"] = t + } + if c := ctx.Arg("content"); c != "" { + body["content"] = c + } + env, err := ctx.CallAPI("PUT", "/api/wiki/updateWiki", body) + if err != nil { + return err + } + return ctx.Output(env) + }, + }, + { + Name: "delete", + Description: "删除 Wiki 页面", + Flags: []common.Flag{ + {Name: "id", Short: "i", Usage: "Wiki 页面 ID", Required: true}, + }, + Run: func(ctx *common.RuntimeContext) error { + id, err := ctx.RequireArg("id") + if err != nil { + return err + } + body := map[string]interface{}{"id": id} + env, err := ctx.CallAPI("POST", "/api/wiki/deleteWiki", body) + if err != nil { + return err + } + return ctx.Output(env) + }, + }, + } +} diff --git a/shortcuts/wiki/wiki_test.go b/shortcuts/wiki/wiki_test.go new file mode 100644 index 0000000..7712815 --- /dev/null +++ b/shortcuts/wiki/wiki_test.go @@ -0,0 +1,281 @@ +package wiki + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/gitlink-org/gitlink-cli/internal/client" + "github.com/gitlink-org/gitlink-cli/shortcuts/common" +) + +func TestWikiPages(t *testing.T) { + tests := []struct { + name string + mockStatus int + mockBody string + wantErr bool + errContains string + }{ + {"正常返回", 200, `{"wikiPages": []}`, false, ""}, + {"API 404", 404, `{"error": "not found"}`, true, "404"}, + {"返回 HTML", 200, `Login`, true, "HTML"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != "GET" { + t.Errorf("expected GET, got %s", r.Method) + } + w.WriteHeader(tt.mockStatus) + w.Write([]byte(tt.mockBody)) + })) + defer server.Close() + + shortcut := findWikiShortcut(t, "pages") + ctx := &common.RuntimeContext{ + Client: &client.Client{HTTP: server.Client(), BaseURL: server.URL}, + Owner: "test", Repo: "test", Format: "json", + Args: map[string]string{}, + } + err := shortcut.Run(ctx) + + if tt.wantErr && err == nil { + t.Fatal("期望错误但为 nil") + } + if !tt.wantErr && err != nil { + t.Fatalf("不期望错误: %v", err) + } + if tt.wantErr && tt.errContains != "" && err != nil { + if !strings.Contains(err.Error(), tt.errContains) { + t.Errorf("错误应包含 %q: %s", tt.errContains, err.Error()) + } + } + }) + } +} + +func TestWikiGet(t *testing.T) { + tests := []struct { + name string + args map[string]string + mockStatus int + mockBody string + wantErr bool + errContains string + }{ + {"正常获取", map[string]string{"id": "42"}, 200, `{"id": 42, "title": "Home"}`, false, ""}, + {"缺少 id", map[string]string{}, 200, `{}`, true, ""}, + {"API 404", map[string]string{"id": "999"}, 404, `{"error": "not found"}`, true, "404"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if !strings.Contains(r.URL.Path, "/api/wiki/getWiki") { + t.Errorf("expected path containing /api/wiki/getWiki, got %s", r.URL.Path) + } + w.WriteHeader(tt.mockStatus) + w.Write([]byte(tt.mockBody)) + })) + defer server.Close() + + shortcut := findWikiShortcut(t, "get") + ctx := &common.RuntimeContext{ + Client: &client.Client{HTTP: server.Client(), BaseURL: server.URL}, + Owner: "test", Repo: "test", Format: "json", + Args: tt.args, + } + err := shortcut.Run(ctx) + + if tt.wantErr && err == nil { + t.Fatal("期望错误但为 nil") + } + if !tt.wantErr && err != nil { + t.Fatalf("不期望错误: %v", err) + } + }) + } +} + +func TestWikiCreate(t *testing.T) { + var payload map[string]interface{} + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" { + t.Errorf("expected POST, got %s", r.Method) + } + if !strings.Contains(r.URL.Path, "/api/wiki/createWiki") { + t.Errorf("expected path containing /api/wiki/createWiki, got %s", r.URL.Path) + } + payload = decodeWikiJSON(t, r) + w.WriteHeader(200) + w.Write([]byte(`{"status": 0, "message": "success"}`)) + })) + defer server.Close() + + shortcut := findWikiShortcut(t, "create") + ctx := &common.RuntimeContext{ + Client: &client.Client{HTTP: server.Client(), BaseURL: server.URL}, + Owner: "test", Repo: "test", Format: "json", + Args: map[string]string{ + "title": "Getting Started", + "content": "# Hello\nWelcome to the wiki", + }, + } + if err := shortcut.Run(ctx); err != nil { + t.Fatalf("create shortcut failed: %v", err) + } + if payload["title"] != "Getting Started" { + t.Errorf("expected title 'Getting Started', got %v", payload["title"]) + } + if payload["content"] != "# Hello\nWelcome to the wiki" { + t.Errorf("unexpected content: %v", payload["content"]) + } +} + +func TestWikiCreateWithProject(t *testing.T) { + var payload map[string]interface{} + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + payload = decodeWikiJSON(t, r) + w.WriteHeader(200) + w.Write([]byte(`{"status": 0}`)) + })) + defer server.Close() + + shortcut := findWikiShortcut(t, "create") + ctx := &common.RuntimeContext{ + Client: &client.Client{HTTP: server.Client(), BaseURL: server.URL}, + Owner: "test", Repo: "test", Format: "json", + Args: map[string]string{ + "title": "Test", + "content": "Body", + "project": "123", + }, + } + if err := shortcut.Run(ctx); err != nil { + t.Fatalf("create shortcut failed: %v", err) + } + if payload["project_id"] != "123" { + t.Errorf("expected project_id '123', got %v", payload["project_id"]) + } +} + +func TestWikiCreateMissingTitle(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Fatal("should not call API when --title is missing") + })) + defer server.Close() + + shortcut := findWikiShortcut(t, "create") + ctx := &common.RuntimeContext{ + Client: &client.Client{HTTP: server.Client(), BaseURL: server.URL}, + Owner: "test", Repo: "test", Format: "json", + Args: map[string]string{"content": "only content"}, + } + if err := shortcut.Run(ctx); err == nil { + t.Fatal("expected error when --title is missing") + } +} + +func TestWikiUpdate(t *testing.T) { + var payload map[string]interface{} + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != "PUT" { + t.Errorf("expected PUT, got %s", r.Method) + } + if !strings.Contains(r.URL.Path, "/api/wiki/updateWiki") { + t.Errorf("expected path containing /api/wiki/updateWiki, got %s", r.URL.Path) + } + payload = decodeWikiJSON(t, r) + w.WriteHeader(200) + w.Write([]byte(`{"status": 0, "message": "success"}`)) + })) + defer server.Close() + + shortcut := findWikiShortcut(t, "update") + ctx := &common.RuntimeContext{ + Client: &client.Client{HTTP: server.Client(), BaseURL: server.URL}, + Owner: "test", Repo: "test", Format: "json", + Args: map[string]string{ + "id": "42", + "title": "Updated Title", + "content": "Updated content", + }, + } + if err := shortcut.Run(ctx); err != nil { + t.Fatalf("update shortcut failed: %v", err) + } + if payload["id"] != "42" { + t.Errorf("expected id '42', got %v", payload["id"]) + } + if payload["title"] != "Updated Title" { + t.Errorf("expected title 'Updated Title', got %v", payload["title"]) + } +} + +func TestWikiDelete(t *testing.T) { + var payload map[string]interface{} + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" { + t.Errorf("expected POST, got %s", r.Method) + } + if !strings.Contains(r.URL.Path, "/api/wiki/deleteWiki") { + t.Errorf("expected path containing /api/wiki/deleteWiki, got %s", r.URL.Path) + } + payload = decodeWikiJSON(t, r) + w.WriteHeader(200) + w.Write([]byte(`{"status": 0, "message": "success"}`)) + })) + defer server.Close() + + shortcut := findWikiShortcut(t, "delete") + ctx := &common.RuntimeContext{ + Client: &client.Client{HTTP: server.Client(), BaseURL: server.URL}, + Owner: "test", Repo: "test", Format: "json", + Args: map[string]string{"id": "42"}, + } + if err := shortcut.Run(ctx); err != nil { + t.Fatalf("delete shortcut failed: %v", err) + } + if payload["id"] != "42" { + t.Errorf("expected id '42', got %v", payload["id"]) + } +} + +func TestWikiDeleteMissingId(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Fatal("should not call API when --id is missing") + })) + defer server.Close() + + shortcut := findWikiShortcut(t, "delete") + ctx := &common.RuntimeContext{ + Client: &client.Client{HTTP: server.Client(), BaseURL: server.URL}, + Owner: "test", Repo: "test", Format: "json", + Args: map[string]string{}, + } + if err := shortcut.Run(ctx); err == nil { + t.Fatal("expected error when --id is missing") + } +} + +func findWikiShortcut(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 decodeWikiJSON(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 +} -- 2.34.1