forked from Gitlink/gitlink-cli
664 lines
19 KiB
Go
664 lines
19 KiB
Go
package wiki
|
||
|
||
import (
|
||
"encoding/base64"
|
||
"encoding/json"
|
||
"errors"
|
||
"net/http"
|
||
"net/http/httptest"
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
"sync"
|
||
"testing"
|
||
|
||
"github.com/gitlink-org/gitlink-cli/internal/client"
|
||
clierrors "github.com/gitlink-org/gitlink-cli/internal/errors"
|
||
"github.com/gitlink-org/gitlink-cli/internal/output"
|
||
"github.com/gitlink-org/gitlink-cli/shortcuts/common"
|
||
)
|
||
|
||
func resetProjectIDCache() {
|
||
projectIDCache = sync.Map{}
|
||
}
|
||
|
||
func newMockServer(t *testing.T, handler http.HandlerFunc) *httptest.Server {
|
||
t.Helper()
|
||
return httptest.NewServer(handler)
|
||
}
|
||
|
||
func writeJSON(t *testing.T, w http.ResponseWriter, v interface{}) {
|
||
t.Helper()
|
||
w.Header().Set("Content-Type", "application/json")
|
||
if err := json.NewEncoder(w).Encode(v); err != nil {
|
||
t.Fatalf("writeJSON: %v", err)
|
||
}
|
||
}
|
||
|
||
// ---- unwrapGatewayResponse tests ----
|
||
|
||
func TestUnwrapGatewayResponse_Success(t *testing.T) {
|
||
env := output.SuccessEnvelope(map[string]interface{}{
|
||
"code": float64(200),
|
||
"data": map[string]interface{}{"id": float64(1)},
|
||
}, nil)
|
||
|
||
result, err := unwrapGatewayResponse(env)
|
||
if err != nil {
|
||
t.Fatalf("unexpected error: %v", err)
|
||
}
|
||
data, ok := result.Data.(map[string]interface{})
|
||
if !ok {
|
||
t.Fatal("expected map data")
|
||
}
|
||
if data["id"] != float64(1) {
|
||
t.Fatalf("got %v, want 1", data["id"])
|
||
}
|
||
}
|
||
|
||
func TestUnwrapGatewayResponse_Code201(t *testing.T) {
|
||
env := output.SuccessEnvelope(map[string]interface{}{
|
||
"code": float64(201),
|
||
"data": "ok",
|
||
}, nil)
|
||
|
||
result, err := unwrapGatewayResponse(env)
|
||
if err != nil {
|
||
t.Fatalf("unexpected error: %v", err)
|
||
}
|
||
if result.Data != "ok" {
|
||
t.Fatalf("got %v, want ok", result.Data)
|
||
}
|
||
}
|
||
|
||
func TestUnwrapGatewayResponse_BusinessError(t *testing.T) {
|
||
env := output.SuccessEnvelope(map[string]interface{}{
|
||
"code": float64(500),
|
||
"msg": "内部错误",
|
||
}, nil)
|
||
|
||
_, err := unwrapGatewayResponse(env)
|
||
if err == nil {
|
||
t.Fatal("expected error, got nil")
|
||
}
|
||
// 必须是 *clierrors.CLIError,否则 TryPrintError 无法按 envelope 输出
|
||
var cliErr *clierrors.CLIError
|
||
if !errors.As(err, &cliErr) {
|
||
t.Fatalf("expected *clierrors.CLIError, got %T: %v", err, err)
|
||
}
|
||
if cliErr.Kind != clierrors.KindServer {
|
||
t.Errorf("Kind = %q, want %q", cliErr.Kind, clierrors.KindServer)
|
||
}
|
||
if cliErr.Message != "内部错误" {
|
||
t.Errorf("Message = %q, want %q", cliErr.Message, "内部错误")
|
||
}
|
||
}
|
||
|
||
func TestUnwrapGatewayResponse_NoDataField(t *testing.T) {
|
||
env := output.SuccessEnvelope(map[string]interface{}{
|
||
"code": float64(200),
|
||
}, nil)
|
||
|
||
result, err := unwrapGatewayResponse(env)
|
||
if err != nil {
|
||
t.Fatalf("unexpected error: %v", err)
|
||
}
|
||
// Should return original envelope since no "data" field to extract
|
||
if result != env {
|
||
t.Fatal("expected original envelope when no data field")
|
||
}
|
||
}
|
||
|
||
func TestUnwrapGatewayResponse_NonMapData(t *testing.T) {
|
||
env := output.SuccessEnvelope("plain text", nil)
|
||
|
||
result, err := unwrapGatewayResponse(env)
|
||
if err != nil {
|
||
t.Fatalf("unexpected error: %v", err)
|
||
}
|
||
if result.Data != "plain text" {
|
||
t.Fatalf("got %v, want plain text", result.Data)
|
||
}
|
||
}
|
||
|
||
// ---- resolveProjectID tests ----
|
||
|
||
func TestResolveProjectID_Success(t *testing.T) {
|
||
resetProjectIDCache()
|
||
server := newMockServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||
if r.URL.Path == "/owner1/repo1/detail.json" {
|
||
writeJSON(t, w, map[string]interface{}{"project_id": float64(123)})
|
||
return
|
||
}
|
||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||
})
|
||
defer server.Close()
|
||
|
||
ctx := &common.RuntimeContext{
|
||
Client: &client.Client{HTTP: server.Client(), BaseURL: server.URL},
|
||
Owner: "owner1",
|
||
Repo: "repo1",
|
||
}
|
||
pid, err := resolveProjectID(ctx)
|
||
if err != nil {
|
||
t.Fatalf("unexpected error: %v", err)
|
||
}
|
||
if pid != "123" {
|
||
t.Fatalf("got %q, want %q", pid, "123")
|
||
}
|
||
}
|
||
|
||
func TestResolveProjectID_Float64(t *testing.T) {
|
||
resetProjectIDCache()
|
||
server := newMockServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||
if r.URL.Path == "/owner2/repo2/detail.json" {
|
||
writeJSON(t, w, map[string]interface{}{"project_id": float64(456)})
|
||
return
|
||
}
|
||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||
})
|
||
defer server.Close()
|
||
|
||
ctx := &common.RuntimeContext{
|
||
Client: &client.Client{HTTP: server.Client(), BaseURL: server.URL},
|
||
Owner: "owner2",
|
||
Repo: "repo2",
|
||
}
|
||
pid, err := resolveProjectID(ctx)
|
||
if err != nil {
|
||
t.Fatalf("unexpected error: %v", err)
|
||
}
|
||
if pid != "456" {
|
||
t.Fatalf("got %q, want %q", pid, "456")
|
||
}
|
||
}
|
||
|
||
func TestResolveProjectID_CacheHit(t *testing.T) {
|
||
resetProjectIDCache()
|
||
callCount := 0
|
||
server := newMockServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||
callCount++
|
||
if r.URL.Path == "/owner3/repo3/detail.json" {
|
||
writeJSON(t, w, map[string]interface{}{"project_id": float64(789)})
|
||
return
|
||
}
|
||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||
})
|
||
defer server.Close()
|
||
|
||
ctx := &common.RuntimeContext{
|
||
Client: &client.Client{HTTP: server.Client(), BaseURL: server.URL},
|
||
Owner: "owner3",
|
||
Repo: "repo3",
|
||
}
|
||
|
||
pid1, err := resolveProjectID(ctx)
|
||
if err != nil {
|
||
t.Fatalf("first call: %v", err)
|
||
}
|
||
if pid1 != "789" {
|
||
t.Fatalf("first call: got %q, want %q", pid1, "789")
|
||
}
|
||
|
||
pid2, err := resolveProjectID(ctx)
|
||
if err != nil {
|
||
t.Fatalf("second call: %v", err)
|
||
}
|
||
if pid2 != "789" {
|
||
t.Fatalf("second call: got %q, want %q", pid2, "789")
|
||
}
|
||
|
||
if callCount != 1 {
|
||
t.Fatalf("API called %d times, want 1 (cache miss)", callCount)
|
||
}
|
||
}
|
||
|
||
func TestResolveProjectID_MissingProjectID(t *testing.T) {
|
||
resetProjectIDCache()
|
||
server := newMockServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||
writeJSON(t, w, map[string]interface{}{"name": "no-project-id"})
|
||
})
|
||
defer server.Close()
|
||
|
||
ctx := &common.RuntimeContext{
|
||
Client: &client.Client{HTTP: server.Client(), BaseURL: server.URL},
|
||
Owner: "owner4",
|
||
Repo: "repo4",
|
||
}
|
||
_, err := resolveProjectID(ctx)
|
||
if err == nil {
|
||
t.Fatal("expected error, got nil")
|
||
}
|
||
}
|
||
|
||
func TestResolveProjectID_APIError(t *testing.T) {
|
||
resetProjectIDCache()
|
||
server := newMockServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||
w.WriteHeader(http.StatusInternalServerError)
|
||
})
|
||
defer server.Close()
|
||
|
||
ctx := &common.RuntimeContext{
|
||
Client: &client.Client{HTTP: server.Client(), BaseURL: server.URL},
|
||
Owner: "owner5",
|
||
Repo: "repo5",
|
||
}
|
||
_, err := resolveProjectID(ctx)
|
||
if err == nil {
|
||
t.Fatal("expected error, got nil")
|
||
}
|
||
}
|
||
|
||
// ---- callWikiAPI HTTP request path tests ----
|
||
|
||
func TestCallWikiAPI_Success(t *testing.T) {
|
||
resetProjectIDCache()
|
||
var receivedPath, receivedMethod string
|
||
var receivedBody []byte
|
||
server := newMockServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||
receivedPath = r.URL.Path
|
||
receivedMethod = r.Method
|
||
if r.Body != nil {
|
||
buf := make([]byte, 1024)
|
||
n, _ := r.Body.Read(buf)
|
||
receivedBody = buf[:n]
|
||
}
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.Write([]byte(`{"code":200,"msg":"ok","data":{"id":42}}`))
|
||
})
|
||
defer server.Close()
|
||
|
||
ctx := &common.RuntimeContext{
|
||
Client: &client.Client{HTTP: server.Client(), BaseURL: server.URL},
|
||
GatewayBaseURL: server.URL,
|
||
GatewayHTTPClient: server.Client(),
|
||
}
|
||
|
||
env, err := callWikiAPI(ctx, "POST", "/wiki/open/test", map[string]string{"foo": "bar"})
|
||
if err != nil {
|
||
t.Fatalf("unexpected error: %v", err)
|
||
}
|
||
if receivedMethod != "POST" {
|
||
t.Errorf("method = %q, want POST", receivedMethod)
|
||
}
|
||
if receivedPath != "/wiki/open/test" {
|
||
t.Errorf("path = %q, want /wiki/open/test", receivedPath)
|
||
}
|
||
if !strings.Contains(string(receivedBody), `"foo"`) {
|
||
t.Errorf("body should contain foo: %s", string(receivedBody))
|
||
}
|
||
data, ok := env.Data.(map[string]interface{})
|
||
if !ok {
|
||
t.Fatalf("expected map data, got %T", env.Data)
|
||
}
|
||
if data["id"] != float64(42) {
|
||
t.Errorf("data[id] = %v, want 42", data["id"])
|
||
}
|
||
}
|
||
|
||
func TestCallWikiAPI_BusinessError(t *testing.T) {
|
||
resetProjectIDCache()
|
||
server := newMockServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.Write([]byte(`{"code":400,"msg":"bad request","data":null}`))
|
||
})
|
||
defer server.Close()
|
||
|
||
ctx := &common.RuntimeContext{
|
||
Client: &client.Client{HTTP: server.Client(), BaseURL: server.URL},
|
||
GatewayBaseURL: server.URL,
|
||
GatewayHTTPClient: server.Client(),
|
||
}
|
||
_, err := callWikiAPI(ctx, "GET", "/test", nil)
|
||
if err == nil {
|
||
t.Fatal("expected error, got nil")
|
||
}
|
||
// 必须是 *clierrors.CLIError,code=400 对应 KindForbidden(401/403)或 KindServer
|
||
// (其他非 200/404 错误),这里 400 走 KindServer 分支
|
||
var cliErr *clierrors.CLIError
|
||
if !errors.As(err, &cliErr) {
|
||
t.Fatalf("expected *clierrors.CLIError, got %T: %v", err, err)
|
||
}
|
||
if cliErr.Message != "bad request" {
|
||
t.Errorf("Message = %q, want %q", cliErr.Message, "bad request")
|
||
}
|
||
if cliErr.Suggestion == "" {
|
||
t.Errorf("Suggestion should not be empty (helps user recover)")
|
||
}
|
||
}
|
||
|
||
func TestCallWikiAPI_GatewayHTTPError(t *testing.T) {
|
||
resetProjectIDCache()
|
||
server := newMockServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||
w.WriteHeader(http.StatusBadGateway)
|
||
w.Write([]byte(`upstream error`))
|
||
})
|
||
defer server.Close()
|
||
|
||
ctx := &common.RuntimeContext{
|
||
Client: &client.Client{HTTP: server.Client(), BaseURL: server.URL},
|
||
GatewayBaseURL: server.URL,
|
||
GatewayHTTPClient: server.Client(),
|
||
}
|
||
_, err := callWikiAPI(ctx, "GET", "/test", nil)
|
||
if err == nil {
|
||
t.Fatal("expected error on 502")
|
||
}
|
||
}
|
||
|
||
func TestCallWikiAPI_SkipsJSONSuffix(t *testing.T) {
|
||
// Verifies the SkipJSONSuffix path is correctly taken for gateway:
|
||
// the URL should NOT have a .json appended.
|
||
resetProjectIDCache()
|
||
var receivedPath string
|
||
server := newMockServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||
receivedPath = r.URL.Path
|
||
w.Write([]byte(`{"code":200,"data":{}}`))
|
||
})
|
||
defer server.Close()
|
||
|
||
ctx := &common.RuntimeContext{
|
||
Client: &client.Client{HTTP: server.Client(), BaseURL: server.URL},
|
||
GatewayBaseURL: server.URL,
|
||
GatewayHTTPClient: server.Client(),
|
||
}
|
||
_, err := callWikiAPI(ctx, "GET", "/wiki/open/list", nil)
|
||
if err != nil {
|
||
t.Fatalf("unexpected error: %v", err)
|
||
}
|
||
if receivedPath != "/wiki/open/list" {
|
||
t.Errorf("path = %q, want /wiki/open/list (no .json suffix)", receivedPath)
|
||
}
|
||
if strings.HasSuffix(receivedPath, ".json") {
|
||
t.Errorf("path %q should NOT have .json suffix (gateway expects no suffix)", receivedPath)
|
||
}
|
||
}
|
||
|
||
func TestCallWikiAPI_ConnectionRefused(t *testing.T) {
|
||
resetProjectIDCache()
|
||
// Use an unbound port to simulate connection failure
|
||
ctx := &common.RuntimeContext{
|
||
Client: &client.Client{HTTP: &http.Client{}, BaseURL: "http://127.0.0.1:1"},
|
||
GatewayBaseURL: "http://127.0.0.1:1",
|
||
GatewayHTTPClient: &http.Client{},
|
||
}
|
||
_, err := callWikiAPI(ctx, "GET", "/test", nil)
|
||
if err == nil {
|
||
t.Fatal("expected connection error")
|
||
}
|
||
}
|
||
|
||
// ---- runLint / check rules tests ----
|
||
|
||
func TestIsCheckEnabled_Empty(t *testing.T) {
|
||
if !isCheckEnabled("", "any") {
|
||
t.Error("empty filter should enable all checks")
|
||
}
|
||
if !isCheckEnabled("empty,headings", "empty") {
|
||
t.Error("should enable 'empty' in filter list")
|
||
}
|
||
if isCheckEnabled("headings", "empty") {
|
||
t.Error("should not enable 'empty' when not in filter list")
|
||
}
|
||
if !isCheckEnabled(" empty , headings ", "empty") {
|
||
t.Error("should trim whitespace")
|
||
}
|
||
}
|
||
|
||
func TestCheckEmpty(t *testing.T) {
|
||
issues := checkEmpty("p1", "")
|
||
if len(issues) != 1 || issues[0].Level != "error" || issues[0].Check != "empty" {
|
||
t.Errorf("expected 1 error-level 'empty' issue, got %+v", issues)
|
||
}
|
||
if issues := checkEmpty("p1", "some content"); issues != nil {
|
||
t.Errorf("non-empty content should not produce issues, got %+v", issues)
|
||
}
|
||
if issues := checkEmpty("p1", " \n\t "); len(issues) != 1 {
|
||
t.Errorf("whitespace-only content should be empty, got %+v", issues)
|
||
}
|
||
}
|
||
|
||
func TestCheckHeading(t *testing.T) {
|
||
// missing H1
|
||
if issues := checkHeading("p1", "Some text without heading"); len(issues) != 1 {
|
||
t.Errorf("expected 1 missing-heading issue, got %+v", issues)
|
||
}
|
||
// has H1
|
||
if issues := checkHeading("p1", "# Title\nbody"); issues != nil {
|
||
t.Errorf("H1 should not produce issues, got %+v", issues)
|
||
}
|
||
// empty content skipped
|
||
if issues := checkHeading("p1", ""); issues != nil {
|
||
t.Errorf("empty content should be skipped, got %+v", issues)
|
||
}
|
||
// whitespace prefix
|
||
if issues := checkHeading("p1", " \n# Real Title"); issues != nil {
|
||
t.Errorf("H1 after whitespace should not produce issues, got %+v", issues)
|
||
}
|
||
}
|
||
|
||
func TestCheckShort(t *testing.T) {
|
||
if issues := checkShort("p1", ""); issues != nil {
|
||
t.Errorf("empty content should be skipped, got %+v", issues)
|
||
}
|
||
if issues := checkShort("p1", "short"); len(issues) != 1 {
|
||
t.Errorf("expected 1 short issue, got %+v", issues)
|
||
}
|
||
long := strings.Repeat("a", 100)
|
||
if issues := checkShort("p1", long); issues != nil {
|
||
t.Errorf("long content should not produce issues, got %+v", issues)
|
||
}
|
||
// exactly 49 chars triggers
|
||
if issues := checkShort("p1", strings.Repeat("a", 49)); len(issues) != 1 {
|
||
t.Errorf("49-char content should be 'short', got %+v", issues)
|
||
}
|
||
}
|
||
|
||
func TestCheckDeadLinks(t *testing.T) {
|
||
known := map[string]bool{"Home": true, "Guide": true}
|
||
|
||
// All known: no issues
|
||
if issues := checkDeadLinks("p1", "[Home](Home) and [Guide](Guide)", known); issues != nil {
|
||
t.Errorf("all-known should not produce issues, got %+v", issues)
|
||
}
|
||
// Unknown link
|
||
issues := checkDeadLinks("p1", "[Unknown](Unknown)", known)
|
||
if len(issues) != 1 || issues[0].Check != "links" {
|
||
t.Errorf("expected 1 dead link issue, got %+v", issues)
|
||
}
|
||
// External links skipped
|
||
if issues := checkDeadLinks("p1", "[ext](https://example.com)", known); issues != nil {
|
||
t.Errorf("external links should be skipped, got %+v", issues)
|
||
}
|
||
// Anchor links skipped
|
||
if issues := checkDeadLinks("p1", "[anchor](#section)", known); issues != nil {
|
||
t.Errorf("anchor links should be skipped, got %+v", issues)
|
||
}
|
||
// Mixed
|
||
issues = checkDeadLinks("p1", "[Home](Home) and [Bad](BadPage)", known)
|
||
if len(issues) != 1 {
|
||
t.Errorf("expected 1 dead link in mixed, got %+v", issues)
|
||
}
|
||
// Empty content
|
||
if issues := checkDeadLinks("p1", "", known); issues != nil {
|
||
t.Errorf("empty content should not produce issues, got %+v", issues)
|
||
}
|
||
}
|
||
|
||
func TestCheckImages(t *testing.T) {
|
||
// Mock image server
|
||
imgServer := newMockServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||
if r.Method == "HEAD" {
|
||
w.WriteHeader(http.StatusOK)
|
||
return
|
||
}
|
||
w.WriteHeader(http.StatusOK)
|
||
})
|
||
defer imgServer.Close()
|
||
|
||
brokenServer := newMockServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||
w.WriteHeader(http.StatusNotFound)
|
||
})
|
||
defer brokenServer.Close()
|
||
|
||
httpClient := imgServer.Client()
|
||
// Valid image (200)
|
||
if issues := checkImages("p1", "", httpClient); issues != nil {
|
||
t.Errorf("200 image should not produce issues, got %+v", issues)
|
||
}
|
||
// Broken image (404)
|
||
issues := checkImages("p1", "", httpClient)
|
||
if len(issues) != 1 {
|
||
t.Errorf("expected 1 broken image issue, got %+v", issues)
|
||
}
|
||
// No images
|
||
if issues := checkImages("p1", "no images here", httpClient); issues != nil {
|
||
t.Errorf("no images should not produce issues, got %+v", issues)
|
||
}
|
||
// Malformed HTTP URL (regex matches https?:// but http.NewRequest fails to parse)
|
||
if issues := checkImages("p1", "", httpClient); len(issues) != 1 {
|
||
t.Errorf("expected 1 invalid-URL issue, got %+v", issues)
|
||
}
|
||
}
|
||
|
||
func TestRunLint_Integration(t *testing.T) {
|
||
resetProjectIDCache()
|
||
|
||
// Mock main API (project detail) and gateway (wiki list + get)
|
||
mainServer := newMockServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||
if strings.HasSuffix(r.URL.Path, "/detail.json") {
|
||
writeJSON(t, w, map[string]interface{}{"project_id": float64(999)})
|
||
return
|
||
}
|
||
t.Errorf("unexpected main API call: %s %s", r.Method, r.URL.Path)
|
||
})
|
||
defer mainServer.Close()
|
||
|
||
// Page content (base64 encoded)
|
||
goodContent := base64.StdEncoding.EncodeToString([]byte("# Good Page\n\n" + strings.Repeat("This is a well-formed page with enough content to pass the short check. ", 3)))
|
||
|
||
wikiServer := newMockServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||
switch r.URL.Path {
|
||
case "/wiki/open/wikiPages":
|
||
writeJSON(t, w, map[string]interface{}{
|
||
"code": 200,
|
||
"data": []map[string]interface{}{
|
||
{"title": "Good", "sub_url": "Good"},
|
||
{"title": "Empty", "sub_url": "Empty"},
|
||
{"title": "_Sidebar", "sub_url": "_Sidebar"}, // system page - skipped
|
||
},
|
||
})
|
||
case "/wiki/open/getWiki":
|
||
pageName := r.URL.Query().Get("pageName")
|
||
var content string
|
||
if pageName == "Empty" {
|
||
content = "" // empty page
|
||
} else {
|
||
content = goodContent
|
||
}
|
||
writeJSON(t, w, map[string]interface{}{
|
||
"code": 200,
|
||
"data": map[string]interface{}{"content_base64": content},
|
||
})
|
||
default:
|
||
t.Errorf("unexpected wiki path: %s", r.URL.Path)
|
||
}
|
||
})
|
||
defer wikiServer.Close()
|
||
|
||
ctx := &common.RuntimeContext{
|
||
Client: &client.Client{HTTP: mainServer.Client(), BaseURL: mainServer.URL},
|
||
Owner: "owner1",
|
||
Repo: "repo1",
|
||
Format: "json",
|
||
GatewayBaseURL: wikiServer.URL,
|
||
GatewayHTTPClient: wikiServer.Client(),
|
||
}
|
||
|
||
if err := runLint(ctx); err != nil {
|
||
t.Fatalf("runLint: %v", err)
|
||
}
|
||
// _Sidebar is skipped, so TotalPages=2 (Good, Empty)
|
||
// Empty page produces 1 "empty" error
|
||
// We can't directly inspect the output envelope, but if no error, the function ran end-to-end
|
||
}
|
||
|
||
// ---- resolveContent tests ----
|
||
|
||
func TestResolveContent_FromArg(t *testing.T) {
|
||
ctx := &common.RuntimeContext{
|
||
Args: map[string]string{"content": "inline content"},
|
||
}
|
||
got, err := resolveContent(ctx)
|
||
if err != nil {
|
||
t.Fatalf("unexpected error: %v", err)
|
||
}
|
||
if got != "inline content" {
|
||
t.Errorf("got %q, want %q", got, "inline content")
|
||
}
|
||
}
|
||
|
||
func TestResolveContent_FromFile(t *testing.T) {
|
||
tmpDir := t.TempDir()
|
||
path := filepath.Join(tmpDir, "wiki.md")
|
||
want := "# Title\n\nBody content from file"
|
||
if err := os.WriteFile(path, []byte(want), 0600); err != nil {
|
||
t.Fatalf("setup: %v", err)
|
||
}
|
||
ctx := &common.RuntimeContext{
|
||
Args: map[string]string{"file": path},
|
||
}
|
||
got, err := resolveContent(ctx)
|
||
if err != nil {
|
||
t.Fatalf("unexpected error: %v", err)
|
||
}
|
||
if got != want {
|
||
t.Errorf("got %q, want %q", got, want)
|
||
}
|
||
}
|
||
|
||
func TestResolveContent_ArgTakesPrecedence(t *testing.T) {
|
||
// When both --content and --file are set, --content wins
|
||
tmpDir := t.TempDir()
|
||
path := filepath.Join(tmpDir, "wiki.md")
|
||
if err := os.WriteFile(path, []byte("from file"), 0600); err != nil {
|
||
t.Fatalf("setup: %v", err)
|
||
}
|
||
ctx := &common.RuntimeContext{
|
||
Args: map[string]string{
|
||
"content": "from arg",
|
||
"file": path,
|
||
},
|
||
}
|
||
got, err := resolveContent(ctx)
|
||
if err != nil {
|
||
t.Fatalf("unexpected error: %v", err)
|
||
}
|
||
if got != "from arg" {
|
||
t.Errorf("arg should take precedence; got %q", got)
|
||
}
|
||
}
|
||
|
||
func TestResolveContent_Missing(t *testing.T) {
|
||
ctx := &common.RuntimeContext{
|
||
Args: map[string]string{},
|
||
}
|
||
_, err := resolveContent(ctx)
|
||
if err == nil {
|
||
t.Fatal("expected error when neither --content nor --file is provided")
|
||
}
|
||
if !strings.Contains(err.Error(), "required") {
|
||
t.Errorf("err = %q, want to mention 'required'", err.Error())
|
||
}
|
||
}
|
||
|
||
func TestResolveContent_FileNotFound(t *testing.T) {
|
||
ctx := &common.RuntimeContext{
|
||
Args: map[string]string{"file": "/nonexistent/path/to/wiki.md"},
|
||
}
|
||
_, err := resolveContent(ctx)
|
||
if err == nil {
|
||
t.Fatal("expected error for nonexistent file")
|
||
}
|
||
}
|