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:
Snider 2026-02-21 16:11:29 +00:00
parent 9e3d15da68
commit 33319590d4
9 changed files with 840 additions and 8 deletions

View file

@ -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
View 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
View 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)
}
}

View file

@ -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
View 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
View 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)
}
}

View file

@ -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
View 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
View 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)
}
}