Files
gitea-mcp/internal/tools/repo_mirror_push_test.go
mathiasandClaude Opus 4.8 64176fe6d7
CD / Lint / Test / Vet (push) Successful in 7s
CD / Build & Import (push) Successful in 21s
CD / Deploy via GitOps (push) Has been skipped
fix(repo_mirror_push): resolve mirror credential from server env, not the payload (#49)
The mirror credential no longer has to ride the tool-call payload (which is
persisted to transcript → claudewatcher → brain → gitea history). Adds
remote_password_env: the name of a server-side env var the tool resolves at call
time, so the secret stays in the server process. An env name that resolves to
empty errors loudly rather than silently sending an empty password. Raw
remote_password still works but the schema/description now mark it DISCOURAGED.

Tests: password resolved from the env var (never in output); unset env var →
ErrValidation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 09:00:07 +02:00

121 lines
5.2 KiB
Go

package tools_test
import (
"context"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"testing"
"git.d-ma.be/mathias/gitea-mcp/internal/allowlist"
"git.d-ma.be/mathias/gitea-mcp/internal/gitea"
"git.d-ma.be/mathias/gitea-mcp/internal/tools"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// #49: remote_password_env names a server-side env var; the secret is resolved
// from the server environment and never rides the tool-call payload.
func TestRepoMirrorPushTool_PasswordFromEnv(t *testing.T) {
t.Setenv("TEST_MIRROR_PW", "env-secret")
var gotPw string
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
var m map[string]any
_ = json.Unmarshal(body, &m)
gotPw, _ = m["remote_password"].(string)
w.WriteHeader(http.StatusCreated)
_, _ = w.Write([]byte(`{"id":1,"remote_name":"m","remote_address":"a"}`))
}))
defer srv.Close()
tool := tools.NewRepoMirrorPush(gitea.NewClient(srv.URL, "tok"), allowlist.New([]string{"mathias"}))
out, err := tool.Call(context.Background(), json.RawMessage(`{
"owner":"mathias","name":"infra","action":"add",
"remote_address":"https://github.com/mathias/infra.git",
"remote_username":"mathias","remote_password_env":"TEST_MIRROR_PW"
}`))
require.NoError(t, err)
assert.Equal(t, "env-secret", gotPw, "password must be resolved from the server env var")
assert.NotContains(t, string(out), "env-secret")
}
// remote_password_env pointing at an unset var must fail loudly, not silently
// send an empty password.
func TestRepoMirrorPushTool_EnvUnsetErrors(t *testing.T) {
tool := tools.NewRepoMirrorPush(gitea.NewClient("http://unused", ""), allowlist.New([]string{"mathias"}))
_, err := tool.Call(context.Background(), json.RawMessage(`{
"owner":"mathias","name":"infra","action":"add",
"remote_address":"https://github.com/x/y.git","remote_username":"u",
"remote_password_env":"DEFINITELY_UNSET_MIRROR_VAR_XYZ"
}`))
require.Error(t, err)
assert.ErrorIs(t, err, gitea.ErrValidation)
}
func TestRepoMirrorPushTool_Add(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, http.MethodPost, r.Method)
assert.Equal(t, "/api/v1/repos/mathias/infra/push_mirrors", r.URL.Path)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
_, _ = w.Write([]byte(`{"id":1,"remote_name":"mirror-github","remote_address":"https://github.com/mathias/infra.git","interval":"8h0m0s","sync_on_commit":true}`))
}))
defer srv.Close()
tool := tools.NewRepoMirrorPush(gitea.NewClient(srv.URL, "tok"), allowlist.New([]string{"mathias"}))
out, err := tool.Call(context.Background(), json.RawMessage(`{
"owner":"mathias","name":"infra","action":"add",
"remote_address":"https://github.com/mathias/infra.git",
"remote_username":"mathias","remote_password":"secret",
"interval":"8h0m0s","sync_on_commit":true
}`))
require.NoError(t, err)
// password must never appear in output
assert.NotContains(t, string(out), "secret")
assert.Contains(t, string(out), `"remote_name":"mirror-github"`)
}
func TestRepoMirrorPushTool_List(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, http.MethodGet, r.Method)
assert.Equal(t, "/api/v1/repos/mathias/infra/push_mirrors", r.URL.Path)
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`[{"id":1,"remote_name":"mirror-github","remote_address":"https://github.com/mathias/infra.git","interval":"8h0m0s","sync_on_commit":true}]`))
}))
defer srv.Close()
tool := tools.NewRepoMirrorPush(gitea.NewClient(srv.URL, "tok"), allowlist.New([]string{"mathias"}))
out, err := tool.Call(context.Background(), json.RawMessage(`{"owner":"mathias","name":"infra","action":"list"}`))
require.NoError(t, err)
assert.Contains(t, string(out), `"remote_name":"mirror-github"`)
}
func TestRepoMirrorPushTool_Delete(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, http.MethodDelete, r.Method)
assert.Equal(t, "/api/v1/repos/mathias/infra/push_mirrors/mirror-github", r.URL.Path)
w.WriteHeader(http.StatusNoContent)
}))
defer srv.Close()
tool := tools.NewRepoMirrorPush(gitea.NewClient(srv.URL, "tok"), allowlist.New([]string{"mathias"}))
out, err := tool.Call(context.Background(), json.RawMessage(`{"owner":"mathias","name":"infra","action":"delete","mirror_name":"mirror-github"}`))
require.NoError(t, err)
assert.Contains(t, string(out), "deleted")
}
func TestRepoMirrorPushTool_DeleteRequiresMirrorName(t *testing.T) {
tool := tools.NewRepoMirrorPush(gitea.NewClient("http://unused", ""), allowlist.New([]string{"mathias"}))
_, err := tool.Call(context.Background(), json.RawMessage(`{"owner":"mathias","name":"infra","action":"delete"}`))
require.Error(t, err)
assert.Contains(t, err.Error(), "mirror_name")
}
func TestRepoMirrorPushTool_AllowlistRejects(t *testing.T) {
tool := tools.NewRepoMirrorPush(gitea.NewClient("http://unused", ""), allowlist.New([]string{"mathias"}))
_, err := tool.Call(context.Background(), json.RawMessage(`{"owner":"evil","name":"x","action":"list"}`))
require.Error(t, err)
}