feat: WikiService, MiscService, CommitService
Add three new services completing the final service layer: - WikiService: CRUD operations for repository wiki pages - MiscService: markdown rendering, licence/gitignore templates, nodeinfo, version - CommitService: commit statuses (list, create, combined) and git notes - PostRaw method on Client for endpoints returning raw text (e.g. /markdown) - Remove services_stub.go (all stubs now replaced with real implementations) - Wire Commits field into Forge struct Co-Authored-By: Virgil <virgil@lethean.io> Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
9e3d15da68
commit
33319590d4
9 changed files with 840 additions and 8 deletions
44
client.go
44
client.go
|
|
@ -105,6 +105,50 @@ func (c *Client) DeleteWithBody(ctx context.Context, path string, body any) erro
|
|||
return c.do(ctx, http.MethodDelete, path, body, nil)
|
||||
}
|
||||
|
||||
// PostRaw performs a POST request with a JSON body and returns the raw
|
||||
// response body as bytes instead of JSON-decoding. Useful for endpoints
|
||||
// such as /markdown that return raw HTML text.
|
||||
func (c *Client) PostRaw(ctx context.Context, path string, body any) ([]byte, error) {
|
||||
url := c.baseURL + path
|
||||
|
||||
var bodyReader io.Reader
|
||||
if body != nil {
|
||||
data, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("forge: marshal body: %w", err)
|
||||
}
|
||||
bodyReader = bytes.NewReader(data)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bodyReader)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("forge: create request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", "token "+c.token)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
if c.userAgent != "" {
|
||||
req.Header.Set("User-Agent", c.userAgent)
|
||||
}
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("forge: request POST %s: %w", path, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode >= 400 {
|
||||
return nil, c.parseError(resp, path)
|
||||
}
|
||||
|
||||
data, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("forge: read response body: %w", err)
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// GetRaw performs a GET request and returns the raw response body as bytes
|
||||
// instead of JSON-decoding. Useful for endpoints that return raw file content.
|
||||
func (c *Client) GetRaw(ctx context.Context, path string) ([]byte, error) {
|
||||
|
|
|
|||
59
commits.go
Normal file
59
commits.go
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
package forge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"forge.lthn.ai/core/go-forge/types"
|
||||
)
|
||||
|
||||
// CommitService handles commit-related operations such as commit statuses
|
||||
// and git notes.
|
||||
// No Resource embedding — heterogeneous endpoints across status and note paths.
|
||||
type CommitService struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
func newCommitService(c *Client) *CommitService {
|
||||
return &CommitService{client: c}
|
||||
}
|
||||
|
||||
// GetCombinedStatus returns the combined status for a given ref (branch, tag, or SHA).
|
||||
func (s *CommitService) GetCombinedStatus(ctx context.Context, owner, repo, ref string) (*types.CombinedStatus, error) {
|
||||
path := fmt.Sprintf("/api/v1/repos/%s/%s/statuses/%s", owner, repo, ref)
|
||||
var out types.CombinedStatus
|
||||
if err := s.client.Get(ctx, path, &out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
// ListStatuses returns all commit statuses for a given ref.
|
||||
func (s *CommitService) ListStatuses(ctx context.Context, owner, repo, ref string) ([]types.CommitStatus, error) {
|
||||
path := fmt.Sprintf("/api/v1/repos/%s/%s/commits/%s/statuses", owner, repo, ref)
|
||||
var out []types.CommitStatus
|
||||
if err := s.client.Get(ctx, path, &out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// CreateStatus creates a new commit status for the given SHA.
|
||||
func (s *CommitService) CreateStatus(ctx context.Context, owner, repo, sha string, opts *types.CreateStatusOption) (*types.CommitStatus, error) {
|
||||
path := fmt.Sprintf("/api/v1/repos/%s/%s/statuses/%s", owner, repo, sha)
|
||||
var out types.CommitStatus
|
||||
if err := s.client.Post(ctx, path, opts, &out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
// GetNote returns the git note for a given commit SHA.
|
||||
func (s *CommitService) GetNote(ctx context.Context, owner, repo, sha string) (*types.Note, error) {
|
||||
path := fmt.Sprintf("/api/v1/repos/%s/%s/git/notes/%s", owner, repo, sha)
|
||||
var out types.Note
|
||||
if err := s.client.Get(ctx, path, &out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &out, nil
|
||||
}
|
||||
167
commits_test.go
Normal file
167
commits_test.go
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
package forge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"forge.lthn.ai/core/go-forge/types"
|
||||
)
|
||||
|
||||
func TestCommitService_Good_ListStatuses(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
t.Errorf("expected GET, got %s", r.Method)
|
||||
}
|
||||
if r.URL.Path != "/api/v1/repos/core/go-forge/commits/abc123/statuses" {
|
||||
t.Errorf("wrong path: %s", r.URL.Path)
|
||||
}
|
||||
json.NewEncoder(w).Encode([]types.CommitStatus{
|
||||
{ID: 1, Context: "ci/build", Description: "Build passed"},
|
||||
{ID: 2, Context: "ci/test", Description: "Tests passed"},
|
||||
})
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
f := NewForge(srv.URL, "tok")
|
||||
statuses, err := f.Commits.ListStatuses(context.Background(), "core", "go-forge", "abc123")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(statuses) != 2 {
|
||||
t.Fatalf("got %d statuses, want 2", len(statuses))
|
||||
}
|
||||
if statuses[0].Context != "ci/build" {
|
||||
t.Errorf("got context=%q, want %q", statuses[0].Context, "ci/build")
|
||||
}
|
||||
if statuses[1].Context != "ci/test" {
|
||||
t.Errorf("got context=%q, want %q", statuses[1].Context, "ci/test")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommitService_Good_CreateStatus(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
t.Errorf("expected POST, got %s", r.Method)
|
||||
}
|
||||
if r.URL.Path != "/api/v1/repos/core/go-forge/statuses/abc123" {
|
||||
t.Errorf("wrong path: %s", r.URL.Path)
|
||||
}
|
||||
var opts types.CreateStatusOption
|
||||
if err := json.NewDecoder(r.Body).Decode(&opts); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if opts.Context != "ci/build" {
|
||||
t.Errorf("got context=%q, want %q", opts.Context, "ci/build")
|
||||
}
|
||||
if opts.Description != "Build passed" {
|
||||
t.Errorf("got description=%q, want %q", opts.Description, "Build passed")
|
||||
}
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
json.NewEncoder(w).Encode(types.CommitStatus{
|
||||
ID: 1,
|
||||
Context: "ci/build",
|
||||
Description: "Build passed",
|
||||
})
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
f := NewForge(srv.URL, "tok")
|
||||
status, err := f.Commits.CreateStatus(context.Background(), "core", "go-forge", "abc123", &types.CreateStatusOption{
|
||||
Context: "ci/build",
|
||||
Description: "Build passed",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if status.Context != "ci/build" {
|
||||
t.Errorf("got context=%q, want %q", status.Context, "ci/build")
|
||||
}
|
||||
if status.ID != 1 {
|
||||
t.Errorf("got id=%d, want 1", status.ID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommitService_Good_GetNote(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
t.Errorf("expected GET, got %s", r.Method)
|
||||
}
|
||||
if r.URL.Path != "/api/v1/repos/core/go-forge/git/notes/abc123" {
|
||||
t.Errorf("wrong path: %s", r.URL.Path)
|
||||
}
|
||||
json.NewEncoder(w).Encode(types.Note{
|
||||
Message: "reviewed and approved",
|
||||
Commit: &types.Commit{
|
||||
SHA: "abc123",
|
||||
},
|
||||
})
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
f := NewForge(srv.URL, "tok")
|
||||
note, err := f.Commits.GetNote(context.Background(), "core", "go-forge", "abc123")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if note.Message != "reviewed and approved" {
|
||||
t.Errorf("got message=%q, want %q", note.Message, "reviewed and approved")
|
||||
}
|
||||
if note.Commit.SHA != "abc123" {
|
||||
t.Errorf("got commit sha=%q, want %q", note.Commit.SHA, "abc123")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommitService_Good_GetCombinedStatus(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
t.Errorf("expected GET, got %s", r.Method)
|
||||
}
|
||||
if r.URL.Path != "/api/v1/repos/core/go-forge/statuses/main" {
|
||||
t.Errorf("wrong path: %s", r.URL.Path)
|
||||
}
|
||||
json.NewEncoder(w).Encode(types.CombinedStatus{
|
||||
SHA: "abc123",
|
||||
TotalCount: 2,
|
||||
Statuses: []*types.CommitStatus{
|
||||
{ID: 1, Context: "ci/build"},
|
||||
{ID: 2, Context: "ci/test"},
|
||||
},
|
||||
})
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
f := NewForge(srv.URL, "tok")
|
||||
cs, err := f.Commits.GetCombinedStatus(context.Background(), "core", "go-forge", "main")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if cs.SHA != "abc123" {
|
||||
t.Errorf("got sha=%q, want %q", cs.SHA, "abc123")
|
||||
}
|
||||
if cs.TotalCount != 2 {
|
||||
t.Errorf("got total_count=%d, want 2", cs.TotalCount)
|
||||
}
|
||||
if len(cs.Statuses) != 2 {
|
||||
t.Fatalf("got %d statuses, want 2", len(cs.Statuses))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommitService_Bad_NotFound(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
json.NewEncoder(w).Encode(map[string]string{"message": "not found"})
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
f := NewForge(srv.URL, "tok")
|
||||
_, err := f.Commits.GetNote(context.Background(), "core", "go-forge", "nonexistent")
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
}
|
||||
if !IsNotFound(err) {
|
||||
t.Errorf("expected not-found error, got %v", err)
|
||||
}
|
||||
}
|
||||
6
forge.go
6
forge.go
|
|
@ -21,6 +21,7 @@ type Forge struct {
|
|||
Contents *ContentService
|
||||
Wiki *WikiService
|
||||
Misc *MiscService
|
||||
Commits *CommitService
|
||||
}
|
||||
|
||||
// NewForge creates a new Forge client.
|
||||
|
|
@ -42,8 +43,9 @@ func NewForge(url, token string, opts ...Option) *Forge {
|
|||
f.Packages = newPackageService(c)
|
||||
f.Actions = newActionsService(c)
|
||||
f.Contents = newContentService(c)
|
||||
f.Wiki = &WikiService{}
|
||||
f.Misc = &MiscService{}
|
||||
f.Wiki = newWikiService(c)
|
||||
f.Misc = newMiscService(c)
|
||||
f.Commits = newCommitService(c)
|
||||
return f
|
||||
}
|
||||
|
||||
|
|
|
|||
87
misc.go
Normal file
87
misc.go
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
package forge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"forge.lthn.ai/core/go-forge/types"
|
||||
)
|
||||
|
||||
// MiscService handles miscellaneous Forgejo API endpoints such as
|
||||
// markdown rendering, licence templates, gitignore templates, and
|
||||
// server metadata.
|
||||
// No Resource embedding — heterogeneous read-only endpoints.
|
||||
type MiscService struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
func newMiscService(c *Client) *MiscService {
|
||||
return &MiscService{client: c}
|
||||
}
|
||||
|
||||
// RenderMarkdown renders markdown text to HTML. The response is raw HTML
|
||||
// text, not JSON.
|
||||
func (s *MiscService) RenderMarkdown(ctx context.Context, text, mode string) (string, error) {
|
||||
body := types.MarkdownOption{Text: text, Mode: mode}
|
||||
data, err := s.client.PostRaw(ctx, "/api/v1/markdown", body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
// ListLicenses returns all available licence templates.
|
||||
func (s *MiscService) ListLicenses(ctx context.Context) ([]types.LicensesTemplateListEntry, error) {
|
||||
var out []types.LicensesTemplateListEntry
|
||||
if err := s.client.Get(ctx, "/api/v1/licenses", &out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// GetLicense returns a single licence template by name.
|
||||
func (s *MiscService) GetLicense(ctx context.Context, name string) (*types.LicenseTemplateInfo, error) {
|
||||
path := fmt.Sprintf("/api/v1/licenses/%s", name)
|
||||
var out types.LicenseTemplateInfo
|
||||
if err := s.client.Get(ctx, path, &out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
// ListGitignoreTemplates returns all available gitignore template names.
|
||||
func (s *MiscService) ListGitignoreTemplates(ctx context.Context) ([]string, error) {
|
||||
var out []string
|
||||
if err := s.client.Get(ctx, "/api/v1/gitignore/templates", &out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// GetGitignoreTemplate returns a single gitignore template by name.
|
||||
func (s *MiscService) GetGitignoreTemplate(ctx context.Context, name string) (*types.GitignoreTemplateInfo, error) {
|
||||
path := fmt.Sprintf("/api/v1/gitignore/templates/%s", name)
|
||||
var out types.GitignoreTemplateInfo
|
||||
if err := s.client.Get(ctx, path, &out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
// GetNodeInfo returns the NodeInfo metadata for the Forgejo instance.
|
||||
func (s *MiscService) GetNodeInfo(ctx context.Context) (*types.NodeInfo, error) {
|
||||
var out types.NodeInfo
|
||||
if err := s.client.Get(ctx, "/api/v1/nodeinfo", &out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
// GetVersion returns the server version.
|
||||
func (s *MiscService) GetVersion(ctx context.Context) (*types.ServerVersion, error) {
|
||||
var out types.ServerVersion
|
||||
if err := s.client.Get(ctx, "/api/v1/version", &out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &out, nil
|
||||
}
|
||||
227
misc_test.go
Normal file
227
misc_test.go
Normal file
|
|
@ -0,0 +1,227 @@
|
|||
package forge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"forge.lthn.ai/core/go-forge/types"
|
||||
)
|
||||
|
||||
func TestMiscService_Good_RenderMarkdown(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
t.Errorf("expected POST, got %s", r.Method)
|
||||
}
|
||||
if r.URL.Path != "/api/v1/markdown" {
|
||||
t.Errorf("wrong path: %s", r.URL.Path)
|
||||
}
|
||||
var opts types.MarkdownOption
|
||||
if err := json.NewDecoder(r.Body).Decode(&opts); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if opts.Text != "# Hello" {
|
||||
t.Errorf("got text=%q, want %q", opts.Text, "# Hello")
|
||||
}
|
||||
if opts.Mode != "gfm" {
|
||||
t.Errorf("got mode=%q, want %q", opts.Mode, "gfm")
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.Write([]byte("<h1>Hello</h1>\n"))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
f := NewForge(srv.URL, "tok")
|
||||
html, err := f.Misc.RenderMarkdown(context.Background(), "# Hello", "gfm")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := "<h1>Hello</h1>\n"
|
||||
if html != want {
|
||||
t.Errorf("got %q, want %q", html, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMiscService_Good_GetVersion(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
t.Errorf("expected GET, got %s", r.Method)
|
||||
}
|
||||
if r.URL.Path != "/api/v1/version" {
|
||||
t.Errorf("wrong path: %s", r.URL.Path)
|
||||
}
|
||||
json.NewEncoder(w).Encode(types.ServerVersion{
|
||||
Version: "1.21.0",
|
||||
})
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
f := NewForge(srv.URL, "tok")
|
||||
ver, err := f.Misc.GetVersion(context.Background())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if ver.Version != "1.21.0" {
|
||||
t.Errorf("got version=%q, want %q", ver.Version, "1.21.0")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMiscService_Good_ListLicenses(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
t.Errorf("expected GET, got %s", r.Method)
|
||||
}
|
||||
if r.URL.Path != "/api/v1/licenses" {
|
||||
t.Errorf("wrong path: %s", r.URL.Path)
|
||||
}
|
||||
json.NewEncoder(w).Encode([]types.LicensesTemplateListEntry{
|
||||
{Key: "mit", Name: "MIT License"},
|
||||
{Key: "gpl-3.0", Name: "GNU General Public License v3.0"},
|
||||
})
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
f := NewForge(srv.URL, "tok")
|
||||
licenses, err := f.Misc.ListLicenses(context.Background())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(licenses) != 2 {
|
||||
t.Fatalf("got %d licenses, want 2", len(licenses))
|
||||
}
|
||||
if licenses[0].Key != "mit" {
|
||||
t.Errorf("got key=%q, want %q", licenses[0].Key, "mit")
|
||||
}
|
||||
if licenses[1].Key != "gpl-3.0" {
|
||||
t.Errorf("got key=%q, want %q", licenses[1].Key, "gpl-3.0")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMiscService_Good_GetLicense(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
t.Errorf("expected GET, got %s", r.Method)
|
||||
}
|
||||
if r.URL.Path != "/api/v1/licenses/mit" {
|
||||
t.Errorf("wrong path: %s", r.URL.Path)
|
||||
}
|
||||
json.NewEncoder(w).Encode(types.LicenseTemplateInfo{
|
||||
Key: "mit",
|
||||
Name: "MIT License",
|
||||
Body: "MIT License body text...",
|
||||
})
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
f := NewForge(srv.URL, "tok")
|
||||
lic, err := f.Misc.GetLicense(context.Background(), "mit")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if lic.Key != "mit" {
|
||||
t.Errorf("got key=%q, want %q", lic.Key, "mit")
|
||||
}
|
||||
if lic.Name != "MIT License" {
|
||||
t.Errorf("got name=%q, want %q", lic.Name, "MIT License")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMiscService_Good_ListGitignoreTemplates(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
t.Errorf("expected GET, got %s", r.Method)
|
||||
}
|
||||
if r.URL.Path != "/api/v1/gitignore/templates" {
|
||||
t.Errorf("wrong path: %s", r.URL.Path)
|
||||
}
|
||||
json.NewEncoder(w).Encode([]string{"Go", "Python", "Node"})
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
f := NewForge(srv.URL, "tok")
|
||||
names, err := f.Misc.ListGitignoreTemplates(context.Background())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(names) != 3 {
|
||||
t.Fatalf("got %d templates, want 3", len(names))
|
||||
}
|
||||
if names[0] != "Go" {
|
||||
t.Errorf("got [0]=%q, want %q", names[0], "Go")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMiscService_Good_GetGitignoreTemplate(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
t.Errorf("expected GET, got %s", r.Method)
|
||||
}
|
||||
if r.URL.Path != "/api/v1/gitignore/templates/Go" {
|
||||
t.Errorf("wrong path: %s", r.URL.Path)
|
||||
}
|
||||
json.NewEncoder(w).Encode(types.GitignoreTemplateInfo{
|
||||
Name: "Go",
|
||||
Source: "*.exe\n*.test\n/vendor/",
|
||||
})
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
f := NewForge(srv.URL, "tok")
|
||||
tmpl, err := f.Misc.GetGitignoreTemplate(context.Background(), "Go")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if tmpl.Name != "Go" {
|
||||
t.Errorf("got name=%q, want %q", tmpl.Name, "Go")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMiscService_Good_GetNodeInfo(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
t.Errorf("expected GET, got %s", r.Method)
|
||||
}
|
||||
if r.URL.Path != "/api/v1/nodeinfo" {
|
||||
t.Errorf("wrong path: %s", r.URL.Path)
|
||||
}
|
||||
json.NewEncoder(w).Encode(types.NodeInfo{
|
||||
Version: "2.1",
|
||||
Software: &types.NodeInfoSoftware{
|
||||
Name: "forgejo",
|
||||
Version: "1.21.0",
|
||||
},
|
||||
})
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
f := NewForge(srv.URL, "tok")
|
||||
info, err := f.Misc.GetNodeInfo(context.Background())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if info.Version != "2.1" {
|
||||
t.Errorf("got version=%q, want %q", info.Version, "2.1")
|
||||
}
|
||||
if info.Software.Name != "forgejo" {
|
||||
t.Errorf("got software name=%q, want %q", info.Software.Name, "forgejo")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMiscService_Bad_NotFound(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
json.NewEncoder(w).Encode(map[string]string{"message": "not found"})
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
f := NewForge(srv.URL, "tok")
|
||||
_, err := f.Misc.GetLicense(context.Background(), "nonexistent")
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
}
|
||||
if !IsNotFound(err) {
|
||||
t.Errorf("expected not-found error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
package forge
|
||||
|
||||
// Stub service types — replaced as each service is implemented.
|
||||
|
||||
type WikiService struct{}
|
||||
type MiscService struct{}
|
||||
64
wiki.go
Normal file
64
wiki.go
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
package forge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"forge.lthn.ai/core/go-forge/types"
|
||||
)
|
||||
|
||||
// WikiService handles wiki page operations for a repository.
|
||||
// No Resource embedding — custom endpoints for wiki CRUD.
|
||||
type WikiService struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
func newWikiService(c *Client) *WikiService {
|
||||
return &WikiService{client: c}
|
||||
}
|
||||
|
||||
// ListPages returns all wiki page metadata for a repository.
|
||||
func (s *WikiService) ListPages(ctx context.Context, owner, repo string) ([]types.WikiPageMetaData, error) {
|
||||
path := fmt.Sprintf("/api/v1/repos/%s/%s/wiki/pages", owner, repo)
|
||||
var out []types.WikiPageMetaData
|
||||
if err := s.client.Get(ctx, path, &out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// GetPage returns a single wiki page by name.
|
||||
func (s *WikiService) GetPage(ctx context.Context, owner, repo, pageName string) (*types.WikiPage, error) {
|
||||
path := fmt.Sprintf("/api/v1/repos/%s/%s/wiki/page/%s", owner, repo, pageName)
|
||||
var out types.WikiPage
|
||||
if err := s.client.Get(ctx, path, &out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
// CreatePage creates a new wiki page.
|
||||
func (s *WikiService) CreatePage(ctx context.Context, owner, repo string, opts *types.CreateWikiPageOptions) (*types.WikiPage, error) {
|
||||
path := fmt.Sprintf("/api/v1/repos/%s/%s/wiki/new", owner, repo)
|
||||
var out types.WikiPage
|
||||
if err := s.client.Post(ctx, path, opts, &out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
// EditPage updates an existing wiki page.
|
||||
func (s *WikiService) EditPage(ctx context.Context, owner, repo, pageName string, opts *types.CreateWikiPageOptions) (*types.WikiPage, error) {
|
||||
path := fmt.Sprintf("/api/v1/repos/%s/%s/wiki/page/%s", owner, repo, pageName)
|
||||
var out types.WikiPage
|
||||
if err := s.client.Patch(ctx, path, opts, &out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
// DeletePage removes a wiki page.
|
||||
func (s *WikiService) DeletePage(ctx context.Context, owner, repo, pageName string) error {
|
||||
path := fmt.Sprintf("/api/v1/repos/%s/%s/wiki/page/%s", owner, repo, pageName)
|
||||
return s.client.Delete(ctx, path)
|
||||
}
|
||||
188
wiki_test.go
Normal file
188
wiki_test.go
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
package forge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"forge.lthn.ai/core/go-forge/types"
|
||||
)
|
||||
|
||||
func TestWikiService_Good_ListPages(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
t.Errorf("expected GET, got %s", r.Method)
|
||||
}
|
||||
if r.URL.Path != "/api/v1/repos/core/go-forge/wiki/pages" {
|
||||
t.Errorf("wrong path: %s", r.URL.Path)
|
||||
}
|
||||
json.NewEncoder(w).Encode([]types.WikiPageMetaData{
|
||||
{Title: "Home", SubURL: "Home"},
|
||||
{Title: "Setup", SubURL: "Setup"},
|
||||
})
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
f := NewForge(srv.URL, "tok")
|
||||
pages, err := f.Wiki.ListPages(context.Background(), "core", "go-forge")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(pages) != 2 {
|
||||
t.Fatalf("got %d pages, want 2", len(pages))
|
||||
}
|
||||
if pages[0].Title != "Home" {
|
||||
t.Errorf("got title=%q, want %q", pages[0].Title, "Home")
|
||||
}
|
||||
if pages[1].Title != "Setup" {
|
||||
t.Errorf("got title=%q, want %q", pages[1].Title, "Setup")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWikiService_Good_GetPage(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
t.Errorf("expected GET, got %s", r.Method)
|
||||
}
|
||||
if r.URL.Path != "/api/v1/repos/core/go-forge/wiki/page/Home" {
|
||||
t.Errorf("wrong path: %s", r.URL.Path)
|
||||
}
|
||||
json.NewEncoder(w).Encode(types.WikiPage{
|
||||
Title: "Home",
|
||||
ContentBase64: "IyBXZWxjb21l",
|
||||
SubURL: "Home",
|
||||
CommitCount: 3,
|
||||
})
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
f := NewForge(srv.URL, "tok")
|
||||
page, err := f.Wiki.GetPage(context.Background(), "core", "go-forge", "Home")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if page.Title != "Home" {
|
||||
t.Errorf("got title=%q, want %q", page.Title, "Home")
|
||||
}
|
||||
if page.ContentBase64 != "IyBXZWxjb21l" {
|
||||
t.Errorf("got content=%q, want %q", page.ContentBase64, "IyBXZWxjb21l")
|
||||
}
|
||||
if page.CommitCount != 3 {
|
||||
t.Errorf("got commit_count=%d, want 3", page.CommitCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWikiService_Good_CreatePage(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
t.Errorf("expected POST, got %s", r.Method)
|
||||
}
|
||||
if r.URL.Path != "/api/v1/repos/core/go-forge/wiki/new" {
|
||||
t.Errorf("wrong path: %s", r.URL.Path)
|
||||
}
|
||||
var opts types.CreateWikiPageOptions
|
||||
if err := json.NewDecoder(r.Body).Decode(&opts); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if opts.Title != "Install" {
|
||||
t.Errorf("got title=%q, want %q", opts.Title, "Install")
|
||||
}
|
||||
if opts.ContentBase64 != "IyBJbnN0YWxs" {
|
||||
t.Errorf("got content=%q, want %q", opts.ContentBase64, "IyBJbnN0YWxs")
|
||||
}
|
||||
json.NewEncoder(w).Encode(types.WikiPage{
|
||||
Title: "Install",
|
||||
ContentBase64: "IyBJbnN0YWxs",
|
||||
SubURL: "Install",
|
||||
CommitCount: 1,
|
||||
})
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
f := NewForge(srv.URL, "tok")
|
||||
page, err := f.Wiki.CreatePage(context.Background(), "core", "go-forge", &types.CreateWikiPageOptions{
|
||||
Title: "Install",
|
||||
ContentBase64: "IyBJbnN0YWxs",
|
||||
Message: "create install page",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if page.Title != "Install" {
|
||||
t.Errorf("got title=%q, want %q", page.Title, "Install")
|
||||
}
|
||||
if page.CommitCount != 1 {
|
||||
t.Errorf("got commit_count=%d, want 1", page.CommitCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWikiService_Good_EditPage(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPatch {
|
||||
t.Errorf("expected PATCH, got %s", r.Method)
|
||||
}
|
||||
if r.URL.Path != "/api/v1/repos/core/go-forge/wiki/page/Home" {
|
||||
t.Errorf("wrong path: %s", r.URL.Path)
|
||||
}
|
||||
var opts types.CreateWikiPageOptions
|
||||
if err := json.NewDecoder(r.Body).Decode(&opts); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
json.NewEncoder(w).Encode(types.WikiPage{
|
||||
Title: "Home",
|
||||
ContentBase64: "dXBkYXRlZA==",
|
||||
CommitCount: 4,
|
||||
})
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
f := NewForge(srv.URL, "tok")
|
||||
page, err := f.Wiki.EditPage(context.Background(), "core", "go-forge", "Home", &types.CreateWikiPageOptions{
|
||||
ContentBase64: "dXBkYXRlZA==",
|
||||
Message: "update home page",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if page.ContentBase64 != "dXBkYXRlZA==" {
|
||||
t.Errorf("got content=%q, want %q", page.ContentBase64, "dXBkYXRlZA==")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWikiService_Good_DeletePage(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodDelete {
|
||||
t.Errorf("expected DELETE, got %s", r.Method)
|
||||
}
|
||||
if r.URL.Path != "/api/v1/repos/core/go-forge/wiki/page/Old" {
|
||||
t.Errorf("wrong path: %s", r.URL.Path)
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
f := NewForge(srv.URL, "tok")
|
||||
err := f.Wiki.DeletePage(context.Background(), "core", "go-forge", "Old")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWikiService_Bad_NotFound(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
json.NewEncoder(w).Encode(map[string]string{"message": "page not found"})
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
f := NewForge(srv.URL, "tok")
|
||||
_, err := f.Wiki.GetPage(context.Background(), "core", "go-forge", "nonexistent")
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
}
|
||||
if !IsNotFound(err) {
|
||||
t.Errorf("expected not-found error, got %v", err)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue