A live token-scope probe against mathias/ai-sessions revealed gitea's contents API uses POST to create and PUT (sha required) to update — the first impl always PUT'd, so creating a new summary 422'd "[SHA]: Required". The httptest mock had the same wrong assumption. Pick the method by whether the file exists (GET sha). Token confirmed contents:write (push:true) by the probe. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
184 lines
6.4 KiB
Go
184 lines
6.4 KiB
Go
package gitea_test
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/mathiasbq/hyperguild/ingestion/internal/gitea"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
const testToken = "super-secret-token-value"
|
|
|
|
func TestNewNilWhenUnconfigured(t *testing.T) {
|
|
assert.Nil(t, gitea.New("", testToken))
|
|
assert.Nil(t, gitea.New("https://git.example", ""))
|
|
}
|
|
|
|
func TestCreateIssueForcesOwnerAndAuth(t *testing.T) {
|
|
var gotPath, gotAuth, gotBody string
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
gotPath = r.URL.Path
|
|
gotAuth = r.Header.Get("Authorization")
|
|
b, _ := io.ReadAll(r.Body)
|
|
gotBody = string(b)
|
|
assert.Equal(t, http.MethodPost, r.Method)
|
|
w.WriteHeader(http.StatusCreated)
|
|
_ = json.NewEncoder(w).Encode(map[string]any{"number": 42, "html_url": "https://git.d-ma.be/mathias/hyperguild/issues/42"})
|
|
}))
|
|
defer srv.Close()
|
|
|
|
c := gitea.New(srv.URL, testToken)
|
|
require.NotNil(t, c)
|
|
ref, err := c.CreateIssue(context.Background(), "hyperguild", "Do the thing", "details")
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, "/api/v1/repos/mathias/hyperguild/issues", gotPath, "owner forced to mathias")
|
|
assert.Equal(t, "token "+testToken, gotAuth)
|
|
assert.Contains(t, gotBody, "Do the thing")
|
|
assert.Equal(t, "hyperguild", ref.Repo)
|
|
assert.Equal(t, 42, ref.Number)
|
|
assert.Contains(t, ref.URL, "/issues/42")
|
|
}
|
|
|
|
func TestCommentIssue(t *testing.T) {
|
|
var gotPath string
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
gotPath = r.URL.Path
|
|
w.WriteHeader(http.StatusCreated)
|
|
_ = json.NewEncoder(w).Encode(map[string]any{"html_url": "https://git/c/1"})
|
|
}))
|
|
defer srv.Close()
|
|
|
|
ref, err := gitea.New(srv.URL, testToken).CommentIssue(context.Background(), "hyperguild", 7, "a comment")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "/api/v1/repos/mathias/hyperguild/issues/7/comments", gotPath)
|
|
assert.Equal(t, 7, ref.Number)
|
|
}
|
|
|
|
func TestCloseIssueWithComment(t *testing.T) {
|
|
var paths []string
|
|
var states []string
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
paths = append(paths, r.Method+" "+r.URL.Path)
|
|
if r.Method == http.MethodPatch {
|
|
var body map[string]any
|
|
b, _ := io.ReadAll(r.Body)
|
|
_ = json.Unmarshal(b, &body)
|
|
states = append(states, body["state"].(string))
|
|
}
|
|
w.WriteHeader(http.StatusOK)
|
|
_ = json.NewEncoder(w).Encode(map[string]any{"number": 9, "html_url": "https://git/i/9"})
|
|
}))
|
|
defer srv.Close()
|
|
|
|
ref, err := gitea.New(srv.URL, testToken).CloseIssue(context.Background(), "hyperguild", 9, "closing because done")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 9, ref.Number)
|
|
// Comment posted first, then state PATCHed to closed.
|
|
assert.Contains(t, paths, "POST /api/v1/repos/mathias/hyperguild/issues/9/comments")
|
|
assert.Contains(t, paths, "PATCH /api/v1/repos/mathias/hyperguild/issues/9")
|
|
assert.Equal(t, []string{"closed"}, states)
|
|
}
|
|
|
|
func TestCloseIssueNoComment(t *testing.T) {
|
|
var commented bool
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if strings.HasSuffix(r.URL.Path, "/comments") {
|
|
commented = true
|
|
}
|
|
w.WriteHeader(http.StatusOK)
|
|
_ = json.NewEncoder(w).Encode(map[string]any{"number": 3, "html_url": "https://git/i/3"})
|
|
}))
|
|
defer srv.Close()
|
|
|
|
_, err := gitea.New(srv.URL, testToken).CloseIssue(context.Background(), "hyperguild", 3, "")
|
|
require.NoError(t, err)
|
|
assert.False(t, commented, "empty comment ⇒ no comment POST")
|
|
}
|
|
|
|
func TestErrorPathDoesNotLeakToken(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
_, _ = w.Write([]byte("boom"))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
_, err := gitea.New(srv.URL, testToken).CreateIssue(context.Background(), "hyperguild", "t", "b")
|
|
require.Error(t, err)
|
|
assert.NotContains(t, err.Error(), testToken, "token must never appear in an error message")
|
|
assert.Contains(t, err.Error(), "500")
|
|
}
|
|
|
|
func TestWriteFileCreatesNewFile(t *testing.T) {
|
|
var getPath, postPath, postBody string
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.Method {
|
|
case http.MethodGet:
|
|
getPath = r.URL.Path
|
|
w.WriteHeader(http.StatusNotFound) // file does not exist yet
|
|
case http.MethodPost: // gitea contents API: POST = create
|
|
postPath = r.URL.Path
|
|
b, _ := io.ReadAll(r.Body)
|
|
postBody = string(b)
|
|
w.WriteHeader(http.StatusCreated)
|
|
_ = json.NewEncoder(w).Encode(map[string]any{"content": map[string]any{"html_url": "https://git/x"}})
|
|
default:
|
|
t.Errorf("create must POST, got %s", r.Method)
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
err := gitea.New(srv.URL, testToken).WriteFile(context.Background(),
|
|
"ai-sessions", "summaries/claude-code/2026-06/2026-06-23-x-abcd1234.md", "# Summary\n\nbody\n")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "/api/v1/repos/mathias/ai-sessions/contents/summaries/claude-code/2026-06/2026-06-23-x-abcd1234.md", getPath)
|
|
assert.Equal(t, getPath, postPath)
|
|
// base64 of the content, no sha on create.
|
|
assert.Contains(t, postBody, "IyBTdW1tYXJ5") // base64("# Summary")
|
|
assert.NotContains(t, postBody, `"sha"`)
|
|
}
|
|
|
|
func TestWriteFileUpdatesExisting(t *testing.T) {
|
|
var putBody string
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.Method {
|
|
case http.MethodGet:
|
|
w.WriteHeader(http.StatusOK)
|
|
_ = json.NewEncoder(w).Encode(map[string]any{"sha": "deadbeef"})
|
|
case http.MethodPut:
|
|
b, _ := io.ReadAll(r.Body)
|
|
putBody = string(b)
|
|
w.WriteHeader(http.StatusOK)
|
|
_ = json.NewEncoder(w).Encode(map[string]any{"content": map[string]any{"html_url": "https://git/x"}})
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
err := gitea.New(srv.URL, testToken).WriteFile(context.Background(), "ai-sessions", "p/x.md", "new")
|
|
require.NoError(t, err)
|
|
assert.Contains(t, putBody, `"sha":"deadbeef"`, "existing file → update with sha")
|
|
}
|
|
|
|
func TestWriteFileErrorNoTokenLeak(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method == http.MethodGet {
|
|
w.WriteHeader(http.StatusNotFound)
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusUnprocessableEntity)
|
|
_, _ = w.Write([]byte("bad"))
|
|
}))
|
|
defer srv.Close()
|
|
err := gitea.New(srv.URL, testToken).WriteFile(context.Background(), "ai-sessions", "p/x.md", "x")
|
|
require.Error(t, err)
|
|
assert.NotContains(t, err.Error(), testToken)
|
|
assert.Contains(t, err.Error(), "422")
|
|
}
|