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>
121 lines
5.2 KiB
Go
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)
|
|
}
|