forgejo-mcp/operation/repo/file_test.go
Christoph Görn 593eeb832f
fix: 🐛 base64-encode content in create_file and update_file (#72)
The Forgejo API requires file content to be base64-encoded, but the MCP
tools were passing plain text through, causing "illegal base64 data"
errors. Now the server encodes content automatically so AI models can
pass plain text directly.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 23:45:14 +01:00

134 lines
3.6 KiB
Go

package repo
import (
"context"
"encoding/base64"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"testing"
"codeberg.org/goern/forgejo-mcp/v2/pkg/forgejo"
forgejo_sdk "codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2"
"github.com/mark3labs/mcp-go/mcp"
)
// apiFileRequest mirrors the Forgejo API request body for create/update file.
type apiFileRequest struct {
Content string `json:"content"`
}
func newCallToolRequest(args map[string]interface{}) mcp.CallToolRequest {
return mcp.CallToolRequest{
Params: mcp.CallToolParams{
Arguments: args,
},
}
}
// setupMockServer creates an httptest server that captures the request body
// and returns a minimal valid FileResponse. It returns the server and a
// pointer to the captured request body bytes.
func setupMockServer(t *testing.T) (*httptest.Server, *[]byte) {
t.Helper()
var captured []byte
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
if err != nil {
t.Fatalf("reading request body: %v", err)
}
captured = body
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(map[string]interface{}{
"content": map[string]interface{}{
"name": "test.txt",
"path": "test.txt",
"sha": "abc123",
},
})
}))
client, err := forgejo_sdk.NewClient(srv.URL, forgejo_sdk.SetForgejoVersion("7.0.0"))
if err != nil {
t.Fatalf("creating test client: %v", err)
}
forgejo.SetClientForTesting(client)
return srv, &captured
}
func TestCreateFileFn_Base64EncodesContent(t *testing.T) {
srv, captured := setupMockServer(t)
defer srv.Close()
plainText := "Hello, World!\nThis is a test file."
req := newCallToolRequest(map[string]interface{}{
"owner": "testowner",
"repo": "testrepo",
"filePath": "test.txt",
"content": plainText,
"message": "add test file",
"branch_name": "main",
})
result, err := CreateFileFn(context.Background(), req)
if err != nil {
t.Fatalf("CreateFileFn returned error: %v", err)
}
if result.IsError {
t.Fatalf("CreateFileFn returned tool error")
}
var body apiFileRequest
if err := json.Unmarshal(*captured, &body); err != nil {
t.Fatalf("unmarshaling captured body: %v", err)
}
expected := base64.StdEncoding.EncodeToString([]byte(plainText))
if body.Content != expected {
decoded, _ := base64.StdEncoding.DecodeString(body.Content)
t.Errorf("content sent to API is not correctly base64-encoded\n got decoded: %q\n want: %q", string(decoded), plainText)
}
}
func TestUpdateFileFn_Base64EncodesContent(t *testing.T) {
srv, captured := setupMockServer(t)
defer srv.Close()
plainText := "Updated content with special chars: <>&\"\n\ttabs too"
req := newCallToolRequest(map[string]interface{}{
"owner": "testowner",
"repo": "testrepo",
"filePath": "test.txt",
"content": plainText,
"message": "update test file",
"branch_name": "main",
"sha": "abc123",
})
result, err := UpdateFileFn(context.Background(), req)
if err != nil {
t.Fatalf("UpdateFileFn returned error: %v", err)
}
if result.IsError {
t.Fatalf("UpdateFileFn returned tool error")
}
var body apiFileRequest
if err := json.Unmarshal(*captured, &body); err != nil {
t.Fatalf("unmarshaling captured body: %v", err)
}
expected := base64.StdEncoding.EncodeToString([]byte(plainText))
if body.Content != expected {
decoded, _ := base64.StdEncoding.DecodeString(body.Content)
t.Errorf("content sent to API is not correctly base64-encoded\n got decoded: %q\n want: %q", string(decoded), plainText)
}
}