feat(tools): create_project_from_template
Generates a new repo from mathias/template-go-web via Gitea's generate API, then substitutes __PROJECT_NAME__ and __MODULE_PATH__ placeholders in six known files (best-effort, partial failure surfaced in result). Validates name regex, allowlist, template flag, and destination non-existence before generating. Adds Template field to gitea.Repo. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
156
internal/gitea/templates_test.go
Normal file
156
internal/gitea/templates_test.go
Normal file
@@ -0,0 +1,156 @@
|
||||
package gitea_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
|
||||
"gitea.d-ma.be/mathias/gitea-mcp/internal/gitea"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGenerateFromTemplate(t *testing.T) {
|
||||
var capturedBody []byte
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, "/api/v1/repos/mathias/template-go-web/generate", r.URL.Path)
|
||||
assert.Equal(t, http.MethodPost, r.Method)
|
||||
var err error
|
||||
capturedBody, err = io.ReadAll(r.Body)
|
||||
require.NoError(t, err)
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{
|
||||
"name":"new-svc",
|
||||
"full_name":"mathias/new-svc",
|
||||
"default_branch":"main",
|
||||
"description":"A new service",
|
||||
"private":true,
|
||||
"clone_url":"http://gitea.example.com/mathias/new-svc.git",
|
||||
"html_url":"http://gitea.example.com/mathias/new-svc",
|
||||
"template":false
|
||||
}`))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
c := gitea.NewClient(srv.URL, "tok")
|
||||
repo, err := c.GenerateFromTemplate(context.Background(), "mathias", "template-go-web", gitea.GenerateFromTemplateArgs{
|
||||
Owner: "mathias",
|
||||
Name: "new-svc",
|
||||
Description: "A new service",
|
||||
Private: true,
|
||||
GitContent: true,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify the captured POST body contains the expected fields.
|
||||
var payload map[string]any
|
||||
require.NoError(t, json.Unmarshal(capturedBody, &payload))
|
||||
assert.Equal(t, "mathias", payload["owner"])
|
||||
assert.Equal(t, "new-svc", payload["name"])
|
||||
assert.Equal(t, "A new service", payload["description"])
|
||||
assert.Equal(t, true, payload["private"])
|
||||
assert.Equal(t, true, payload["git_content"])
|
||||
|
||||
// Verify the decoded repo fields.
|
||||
assert.Equal(t, "new-svc", repo.Name)
|
||||
assert.Equal(t, "mathias/new-svc", repo.FullName)
|
||||
assert.Equal(t, "main", repo.DefaultBranch)
|
||||
assert.Equal(t, "A new service", repo.Description)
|
||||
assert.True(t, repo.Private)
|
||||
assert.Equal(t, "http://gitea.example.com/mathias/new-svc.git", repo.CloneURL)
|
||||
assert.Equal(t, "http://gitea.example.com/mathias/new-svc", repo.HTMLURL)
|
||||
}
|
||||
|
||||
func TestSubstituteFileApplies(t *testing.T) {
|
||||
originalContent := "module __MODULE_PATH__\n\ngo 1.22\n"
|
||||
encoded := base64.StdEncoding.EncodeToString([]byte(originalContent))
|
||||
|
||||
var capturedPutBody []byte
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
assert.Equal(t, "/api/v1/repos/mathias/new-svc/contents/go.mod", r.URL.Path)
|
||||
assert.Equal(t, "main", r.URL.Query().Get("ref"))
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"path":"go.mod","sha":"abc123","size":30,"content":"` + encoded + `","encoding":"base64"}`))
|
||||
case http.MethodPut:
|
||||
assert.Equal(t, "/api/v1/repos/mathias/new-svc/contents/go.mod", r.URL.Path)
|
||||
var err error
|
||||
capturedPutBody, err = io.ReadAll(r.Body)
|
||||
require.NoError(t, err)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte(`{"content":{"path":"go.mod","sha":"newsha","html_url":""},"commit":{"sha":"commitsha","html_url":""}}`))
|
||||
default:
|
||||
t.Errorf("unexpected method %s", r.Method)
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
}
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
c := gitea.NewClient(srv.URL, "tok")
|
||||
err := c.SubstituteFile(context.Background(), "mathias", "new-svc", "main", "go.mod", map[string]string{
|
||||
"__MODULE_PATH__": "gitea.d-ma.be/mathias/new-svc",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify the PUT body contains the substituted content.
|
||||
require.NotNil(t, capturedPutBody, "PUT should have been called")
|
||||
var payload map[string]string
|
||||
require.NoError(t, json.Unmarshal(capturedPutBody, &payload))
|
||||
|
||||
decoded, err := base64.StdEncoding.DecodeString(payload["content"])
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, string(decoded), "gitea.d-ma.be/mathias/new-svc")
|
||||
assert.NotContains(t, string(decoded), "__MODULE_PATH__")
|
||||
assert.Equal(t, "abc123", payload["sha"])
|
||||
assert.Equal(t, "Apply template substitutions", payload["message"])
|
||||
}
|
||||
|
||||
func TestSubstituteFileNoChangeSkipsWrite(t *testing.T) {
|
||||
originalContent := "module gitea.d-ma.be/mathias/existing\n\ngo 1.22\n"
|
||||
encoded := base64.StdEncoding.EncodeToString([]byte(originalContent))
|
||||
|
||||
var putCount atomic.Int32
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"path":"go.mod","sha":"abc123","size":40,"content":"` + encoded + `","encoding":"base64"}`))
|
||||
case http.MethodPut:
|
||||
putCount.Add(1)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte(`{"content":{"path":"go.mod","sha":"newsha","html_url":""},"commit":{"sha":"c","html_url":""}}`))
|
||||
}
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
c := gitea.NewClient(srv.URL, "tok")
|
||||
// Replacements that don't match anything in the content.
|
||||
err := c.SubstituteFile(context.Background(), "mathias", "new-svc", "main", "go.mod", map[string]string{
|
||||
"__MODULE_PATH__": "gitea.d-ma.be/mathias/new-svc",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int32(0), putCount.Load(), "PUT should not be called when content is unchanged")
|
||||
}
|
||||
|
||||
func TestSubstituteFileReadError(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
_, _ = w.Write([]byte(`{"message":"file not found"}`))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
c := gitea.NewClient(srv.URL, "tok")
|
||||
err := c.SubstituteFile(context.Background(), "mathias", "new-svc", "main", "go.mod", map[string]string{
|
||||
"__MODULE_PATH__": "gitea.d-ma.be/mathias/new-svc",
|
||||
})
|
||||
require.Error(t, err)
|
||||
assert.True(t, errors.Is(err, gitea.ErrNotFound), "error should wrap ErrNotFound, got: %v", err)
|
||||
}
|
||||
Reference in New Issue
Block a user