feat: LabelService, WebhookService, ContentService
Add three new services covering labels, webhooks, and file content operations. LabelService handles repo and org labels without Resource embedding due to heterogeneous paths. WebhookService embeds Resource for standard CRUD on repo hooks plus action methods for test delivery and org hooks. ContentService provides file CRUD and raw file retrieval. Adds GetRaw method to Client for non-JSON responses. Co-Authored-By: Virgil <virgil@lethean.io> Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
fd0343e47e
commit
de76399608
9 changed files with 838 additions and 6 deletions
33
client.go
33
client.go
|
|
@ -105,6 +105,39 @@ func (c *Client) DeleteWithBody(ctx context.Context, path string, body any) erro
|
|||
return c.do(ctx, http.MethodDelete, path, body, 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) {
|
||||
url := c.baseURL + path
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("forge: create request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", "token "+c.token)
|
||||
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 GET %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
|
||||
}
|
||||
|
||||
func (c *Client) do(ctx context.Context, method, path string, body, out any) error {
|
||||
url := c.baseURL + path
|
||||
|
||||
|
|
|
|||
60
contents.go
Normal file
60
contents.go
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
package forge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"forge.lthn.ai/core/go-forge/types"
|
||||
)
|
||||
|
||||
// ContentService handles file read/write operations via the Forgejo API.
|
||||
// No Resource embedding — paths vary by operation.
|
||||
type ContentService struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
func newContentService(c *Client) *ContentService {
|
||||
return &ContentService{client: c}
|
||||
}
|
||||
|
||||
// GetFile returns metadata and content for a file in a repository.
|
||||
func (s *ContentService) GetFile(ctx context.Context, owner, repo, filepath string) (*types.ContentsResponse, error) {
|
||||
path := fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", owner, repo, filepath)
|
||||
var out types.ContentsResponse
|
||||
if err := s.client.Get(ctx, path, &out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
// CreateFile creates a new file in a repository.
|
||||
func (s *ContentService) CreateFile(ctx context.Context, owner, repo, filepath string, opts *types.CreateFileOptions) (*types.FileResponse, error) {
|
||||
path := fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", owner, repo, filepath)
|
||||
var out types.FileResponse
|
||||
if err := s.client.Post(ctx, path, opts, &out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
// UpdateFile updates an existing file in a repository.
|
||||
func (s *ContentService) UpdateFile(ctx context.Context, owner, repo, filepath string, opts *types.UpdateFileOptions) (*types.FileResponse, error) {
|
||||
path := fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", owner, repo, filepath)
|
||||
var out types.FileResponse
|
||||
if err := s.client.Put(ctx, path, opts, &out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
// DeleteFile deletes a file from a repository. Uses DELETE with a JSON body.
|
||||
func (s *ContentService) DeleteFile(ctx context.Context, owner, repo, filepath string, opts *types.DeleteFileOptions) error {
|
||||
path := fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", owner, repo, filepath)
|
||||
return s.client.DeleteWithBody(ctx, path, opts)
|
||||
}
|
||||
|
||||
// GetRawFile returns the raw file content as bytes.
|
||||
func (s *ContentService) GetRawFile(ctx context.Context, owner, repo, filepath string) ([]byte, error) {
|
||||
path := fmt.Sprintf("/api/v1/repos/%s/%s/raw/%s", owner, repo, filepath)
|
||||
return s.client.GetRaw(ctx, path)
|
||||
}
|
||||
227
contents_test.go
Normal file
227
contents_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 TestContentService_Good_GetFile(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/contents/README.md" {
|
||||
t.Errorf("wrong path: %s", r.URL.Path)
|
||||
}
|
||||
json.NewEncoder(w).Encode(types.ContentsResponse{
|
||||
Name: "README.md",
|
||||
Path: "README.md",
|
||||
Type: "file",
|
||||
Encoding: "base64",
|
||||
Content: "IyBnby1mb3JnZQ==",
|
||||
SHA: "abc123",
|
||||
Size: 12,
|
||||
})
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
f := NewForge(srv.URL, "tok")
|
||||
file, err := f.Contents.GetFile(context.Background(), "core", "go-forge", "README.md")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if file.Name != "README.md" {
|
||||
t.Errorf("got name=%q, want %q", file.Name, "README.md")
|
||||
}
|
||||
if file.Type != "file" {
|
||||
t.Errorf("got type=%q, want %q", file.Type, "file")
|
||||
}
|
||||
if file.SHA != "abc123" {
|
||||
t.Errorf("got sha=%q, want %q", file.SHA, "abc123")
|
||||
}
|
||||
if file.Content != "IyBnby1mb3JnZQ==" {
|
||||
t.Errorf("got content=%q, want %q", file.Content, "IyBnby1mb3JnZQ==")
|
||||
}
|
||||
}
|
||||
|
||||
func TestContentService_Good_CreateFile(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/contents/docs/new.md" {
|
||||
t.Errorf("wrong path: %s", r.URL.Path)
|
||||
}
|
||||
var opts types.CreateFileOptions
|
||||
if err := json.NewDecoder(r.Body).Decode(&opts); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if opts.ContentBase64 != "bmV3IGZpbGU=" {
|
||||
t.Errorf("got content=%q, want %q", opts.ContentBase64, "bmV3IGZpbGU=")
|
||||
}
|
||||
json.NewEncoder(w).Encode(types.FileResponse{
|
||||
Content: &types.ContentsResponse{
|
||||
Name: "new.md",
|
||||
Path: "docs/new.md",
|
||||
Type: "file",
|
||||
SHA: "def456",
|
||||
},
|
||||
Commit: &types.FileCommitResponse{
|
||||
SHA: "commit789",
|
||||
Message: "create docs/new.md",
|
||||
},
|
||||
})
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
f := NewForge(srv.URL, "tok")
|
||||
resp, err := f.Contents.CreateFile(context.Background(), "core", "go-forge", "docs/new.md", &types.CreateFileOptions{
|
||||
ContentBase64: "bmV3IGZpbGU=",
|
||||
Message: "create docs/new.md",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if resp.Content.Name != "new.md" {
|
||||
t.Errorf("got name=%q, want %q", resp.Content.Name, "new.md")
|
||||
}
|
||||
if resp.Content.SHA != "def456" {
|
||||
t.Errorf("got sha=%q, want %q", resp.Content.SHA, "def456")
|
||||
}
|
||||
if resp.Commit.Message != "create docs/new.md" {
|
||||
t.Errorf("got commit message=%q, want %q", resp.Commit.Message, "create docs/new.md")
|
||||
}
|
||||
}
|
||||
|
||||
func TestContentService_Good_UpdateFile(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPut {
|
||||
t.Errorf("expected PUT, got %s", r.Method)
|
||||
}
|
||||
if r.URL.Path != "/api/v1/repos/core/go-forge/contents/README.md" {
|
||||
t.Errorf("wrong path: %s", r.URL.Path)
|
||||
}
|
||||
var opts types.UpdateFileOptions
|
||||
if err := json.NewDecoder(r.Body).Decode(&opts); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if opts.SHA != "abc123" {
|
||||
t.Errorf("got sha=%q, want %q", opts.SHA, "abc123")
|
||||
}
|
||||
json.NewEncoder(w).Encode(types.FileResponse{
|
||||
Content: &types.ContentsResponse{
|
||||
Name: "README.md",
|
||||
Path: "README.md",
|
||||
Type: "file",
|
||||
SHA: "updated456",
|
||||
},
|
||||
})
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
f := NewForge(srv.URL, "tok")
|
||||
resp, err := f.Contents.UpdateFile(context.Background(), "core", "go-forge", "README.md", &types.UpdateFileOptions{
|
||||
ContentBase64: "dXBkYXRlZA==",
|
||||
SHA: "abc123",
|
||||
Message: "update README",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if resp.Content.SHA != "updated456" {
|
||||
t.Errorf("got sha=%q, want %q", resp.Content.SHA, "updated456")
|
||||
}
|
||||
}
|
||||
|
||||
func TestContentService_Good_DeleteFile(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/contents/old.txt" {
|
||||
t.Errorf("wrong path: %s", r.URL.Path)
|
||||
}
|
||||
var opts types.DeleteFileOptions
|
||||
if err := json.NewDecoder(r.Body).Decode(&opts); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if opts.SHA != "sha123" {
|
||||
t.Errorf("got sha=%q, want %q", opts.SHA, "sha123")
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(types.FileDeleteResponse{})
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
f := NewForge(srv.URL, "tok")
|
||||
err := f.Contents.DeleteFile(context.Background(), "core", "go-forge", "old.txt", &types.DeleteFileOptions{
|
||||
SHA: "sha123",
|
||||
Message: "remove old file",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestContentService_Good_GetRawFile(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/raw/README.md" {
|
||||
t.Errorf("wrong path: %s", r.URL.Path)
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
w.Write([]byte("# go-forge\n\nA Go client for Forgejo."))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
f := NewForge(srv.URL, "tok")
|
||||
data, err := f.Contents.GetRawFile(context.Background(), "core", "go-forge", "README.md")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := "# go-forge\n\nA Go client for Forgejo."
|
||||
if string(data) != want {
|
||||
t.Errorf("got %q, want %q", string(data), want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestContentService_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": "file not found"})
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
f := NewForge(srv.URL, "tok")
|
||||
_, err := f.Contents.GetFile(context.Background(), "core", "go-forge", "nonexistent.md")
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
}
|
||||
if !IsNotFound(err) {
|
||||
t.Errorf("expected not-found error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestContentService_Bad_GetRawNotFound(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": "file not found"})
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
f := NewForge(srv.URL, "tok")
|
||||
_, err := f.Contents.GetRawFile(context.Background(), "core", "go-forge", "nonexistent.md")
|
||||
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
|
|
@ -36,12 +36,12 @@ func NewForge(url, token string, opts ...Option) *Forge {
|
|||
f.Admin = newAdminService(c)
|
||||
f.Branches = newBranchService(c)
|
||||
f.Releases = newReleaseService(c)
|
||||
f.Labels = &LabelService{}
|
||||
f.Webhooks = &WebhookService{}
|
||||
f.Labels = newLabelService(c)
|
||||
f.Webhooks = newWebhookService(c)
|
||||
f.Notifications = &NotificationService{}
|
||||
f.Packages = &PackageService{}
|
||||
f.Actions = &ActionsService{}
|
||||
f.Contents = &ContentService{}
|
||||
f.Contents = newContentService(c)
|
||||
f.Wiki = &WikiService{}
|
||||
f.Misc = &MiscService{}
|
||||
return f
|
||||
|
|
|
|||
76
labels.go
Normal file
76
labels.go
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
package forge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"forge.lthn.ai/core/go-forge/types"
|
||||
)
|
||||
|
||||
// LabelService handles repository labels, organisation labels, and issue labels.
|
||||
// No Resource embedding — paths are heterogeneous.
|
||||
type LabelService struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
func newLabelService(c *Client) *LabelService {
|
||||
return &LabelService{client: c}
|
||||
}
|
||||
|
||||
// ListRepoLabels returns all labels for a repository.
|
||||
func (s *LabelService) ListRepoLabels(ctx context.Context, owner, repo string) ([]types.Label, error) {
|
||||
path := fmt.Sprintf("/api/v1/repos/%s/%s/labels", owner, repo)
|
||||
return ListAll[types.Label](ctx, s.client, path, nil)
|
||||
}
|
||||
|
||||
// GetRepoLabel returns a single label by ID.
|
||||
func (s *LabelService) GetRepoLabel(ctx context.Context, owner, repo string, id int64) (*types.Label, error) {
|
||||
path := fmt.Sprintf("/api/v1/repos/%s/%s/labels/%d", owner, repo, id)
|
||||
var out types.Label
|
||||
if err := s.client.Get(ctx, path, &out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
// CreateRepoLabel creates a new label in a repository.
|
||||
func (s *LabelService) CreateRepoLabel(ctx context.Context, owner, repo string, opts *types.CreateLabelOption) (*types.Label, error) {
|
||||
path := fmt.Sprintf("/api/v1/repos/%s/%s/labels", owner, repo)
|
||||
var out types.Label
|
||||
if err := s.client.Post(ctx, path, opts, &out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
// EditRepoLabel updates an existing label in a repository.
|
||||
func (s *LabelService) EditRepoLabel(ctx context.Context, owner, repo string, id int64, opts *types.EditLabelOption) (*types.Label, error) {
|
||||
path := fmt.Sprintf("/api/v1/repos/%s/%s/labels/%d", owner, repo, id)
|
||||
var out types.Label
|
||||
if err := s.client.Patch(ctx, path, opts, &out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
// DeleteRepoLabel deletes a label from a repository.
|
||||
func (s *LabelService) DeleteRepoLabel(ctx context.Context, owner, repo string, id int64) error {
|
||||
path := fmt.Sprintf("/api/v1/repos/%s/%s/labels/%d", owner, repo, id)
|
||||
return s.client.Delete(ctx, path)
|
||||
}
|
||||
|
||||
// ListOrgLabels returns all labels for an organisation.
|
||||
func (s *LabelService) ListOrgLabels(ctx context.Context, org string) ([]types.Label, error) {
|
||||
path := fmt.Sprintf("/api/v1/orgs/%s/labels", org)
|
||||
return ListAll[types.Label](ctx, s.client, path, nil)
|
||||
}
|
||||
|
||||
// CreateOrgLabel creates a new label in an organisation.
|
||||
func (s *LabelService) CreateOrgLabel(ctx context.Context, org string, opts *types.CreateLabelOption) (*types.Label, error) {
|
||||
path := fmt.Sprintf("/api/v1/orgs/%s/labels", org)
|
||||
var out types.Label
|
||||
if err := s.client.Post(ctx, path, opts, &out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &out, nil
|
||||
}
|
||||
232
labels_test.go
Normal file
232
labels_test.go
Normal file
|
|
@ -0,0 +1,232 @@
|
|||
package forge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"forge.lthn.ai/core/go-forge/types"
|
||||
)
|
||||
|
||||
func TestLabelService_Good_ListRepoLabels(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/labels" {
|
||||
t.Errorf("wrong path: %s", r.URL.Path)
|
||||
}
|
||||
w.Header().Set("X-Total-Count", "2")
|
||||
json.NewEncoder(w).Encode([]types.Label{
|
||||
{ID: 1, Name: "bug", Color: "#d73a4a"},
|
||||
{ID: 2, Name: "feature", Color: "#0075ca"},
|
||||
})
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
f := NewForge(srv.URL, "tok")
|
||||
labels, err := f.Labels.ListRepoLabels(context.Background(), "core", "go-forge")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(labels) != 2 {
|
||||
t.Errorf("got %d labels, want 2", len(labels))
|
||||
}
|
||||
if labels[0].Name != "bug" {
|
||||
t.Errorf("got name=%q, want %q", labels[0].Name, "bug")
|
||||
}
|
||||
if labels[1].Color != "#0075ca" {
|
||||
t.Errorf("got colour=%q, want %q", labels[1].Color, "#0075ca")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLabelService_Good_CreateRepoLabel(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/labels" {
|
||||
t.Errorf("wrong path: %s", r.URL.Path)
|
||||
}
|
||||
var opts types.CreateLabelOption
|
||||
if err := json.NewDecoder(r.Body).Decode(&opts); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if opts.Name != "enhancement" {
|
||||
t.Errorf("got name=%q, want %q", opts.Name, "enhancement")
|
||||
}
|
||||
if opts.Color != "#a2eeef" {
|
||||
t.Errorf("got colour=%q, want %q", opts.Color, "#a2eeef")
|
||||
}
|
||||
json.NewEncoder(w).Encode(types.Label{
|
||||
ID: 3,
|
||||
Name: opts.Name,
|
||||
Color: opts.Color,
|
||||
})
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
f := NewForge(srv.URL, "tok")
|
||||
label, err := f.Labels.CreateRepoLabel(context.Background(), "core", "go-forge", &types.CreateLabelOption{
|
||||
Name: "enhancement",
|
||||
Color: "#a2eeef",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if label.ID != 3 {
|
||||
t.Errorf("got id=%d, want 3", label.ID)
|
||||
}
|
||||
if label.Name != "enhancement" {
|
||||
t.Errorf("got name=%q, want %q", label.Name, "enhancement")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLabelService_Good_GetRepoLabel(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/labels/1" {
|
||||
t.Errorf("wrong path: %s", r.URL.Path)
|
||||
}
|
||||
json.NewEncoder(w).Encode(types.Label{ID: 1, Name: "bug", Color: "#d73a4a"})
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
f := NewForge(srv.URL, "tok")
|
||||
label, err := f.Labels.GetRepoLabel(context.Background(), "core", "go-forge", 1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if label.Name != "bug" {
|
||||
t.Errorf("got name=%q, want %q", label.Name, "bug")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLabelService_Good_EditRepoLabel(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/labels/1" {
|
||||
t.Errorf("wrong path: %s", r.URL.Path)
|
||||
}
|
||||
var opts types.EditLabelOption
|
||||
if err := json.NewDecoder(r.Body).Decode(&opts); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
json.NewEncoder(w).Encode(types.Label{ID: 1, Name: opts.Name, Color: opts.Color})
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
f := NewForge(srv.URL, "tok")
|
||||
label, err := f.Labels.EditRepoLabel(context.Background(), "core", "go-forge", 1, &types.EditLabelOption{
|
||||
Name: "critical-bug",
|
||||
Color: "#ff0000",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if label.Name != "critical-bug" {
|
||||
t.Errorf("got name=%q, want %q", label.Name, "critical-bug")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLabelService_Good_DeleteRepoLabel(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/labels/1" {
|
||||
t.Errorf("wrong path: %s", r.URL.Path)
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
f := NewForge(srv.URL, "tok")
|
||||
err := f.Labels.DeleteRepoLabel(context.Background(), "core", "go-forge", 1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLabelService_Good_ListOrgLabels(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/orgs/myorg/labels" {
|
||||
t.Errorf("wrong path: %s", r.URL.Path)
|
||||
}
|
||||
w.Header().Set("X-Total-Count", "1")
|
||||
json.NewEncoder(w).Encode([]types.Label{
|
||||
{ID: 10, Name: "org-wide", Color: "#333333"},
|
||||
})
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
f := NewForge(srv.URL, "tok")
|
||||
labels, err := f.Labels.ListOrgLabels(context.Background(), "myorg")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(labels) != 1 {
|
||||
t.Errorf("got %d labels, want 1", len(labels))
|
||||
}
|
||||
if labels[0].Name != "org-wide" {
|
||||
t.Errorf("got name=%q, want %q", labels[0].Name, "org-wide")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLabelService_Good_CreateOrgLabel(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/orgs/myorg/labels" {
|
||||
t.Errorf("wrong path: %s", r.URL.Path)
|
||||
}
|
||||
var opts types.CreateLabelOption
|
||||
if err := json.NewDecoder(r.Body).Decode(&opts); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
json.NewEncoder(w).Encode(types.Label{ID: 11, Name: opts.Name, Color: opts.Color})
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
f := NewForge(srv.URL, "tok")
|
||||
label, err := f.Labels.CreateOrgLabel(context.Background(), "myorg", &types.CreateLabelOption{
|
||||
Name: "priority",
|
||||
Color: "#e4e669",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if label.ID != 11 {
|
||||
t.Errorf("got id=%d, want 11", label.ID)
|
||||
}
|
||||
if label.Name != "priority" {
|
||||
t.Errorf("got name=%q, want %q", label.Name, "priority")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLabelService_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": "label not found"})
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
f := NewForge(srv.URL, "tok")
|
||||
_, err := f.Labels.GetRepoLabel(context.Background(), "core", "go-forge", 999)
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
}
|
||||
if !IsNotFound(err) {
|
||||
t.Errorf("expected not-found error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
|
@ -2,11 +2,8 @@ package forge
|
|||
|
||||
// Stub service types — replaced as each service is implemented.
|
||||
|
||||
type LabelService struct{}
|
||||
type WebhookService struct{}
|
||||
type NotificationService struct{}
|
||||
type PackageService struct{}
|
||||
type ActionsService struct{}
|
||||
type ContentService struct{}
|
||||
type WikiService struct{}
|
||||
type MiscService struct{}
|
||||
|
|
|
|||
34
webhooks.go
Normal file
34
webhooks.go
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
package forge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"forge.lthn.ai/core/go-forge/types"
|
||||
)
|
||||
|
||||
// WebhookService handles webhook (hook) operations within a repository.
|
||||
// Embeds Resource for standard CRUD on /api/v1/repos/{owner}/{repo}/hooks/{id}.
|
||||
type WebhookService struct {
|
||||
Resource[types.Hook, types.CreateHookOption, types.EditHookOption]
|
||||
}
|
||||
|
||||
func newWebhookService(c *Client) *WebhookService {
|
||||
return &WebhookService{
|
||||
Resource: *NewResource[types.Hook, types.CreateHookOption, types.EditHookOption](
|
||||
c, "/api/v1/repos/{owner}/{repo}/hooks/{id}",
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
// TestHook triggers a test delivery for a webhook.
|
||||
func (s *WebhookService) TestHook(ctx context.Context, owner, repo string, id int64) error {
|
||||
path := fmt.Sprintf("/api/v1/repos/%s/%s/hooks/%d/tests", owner, repo, id)
|
||||
return s.client.Post(ctx, path, nil, nil)
|
||||
}
|
||||
|
||||
// ListOrgHooks returns all webhooks for an organisation.
|
||||
func (s *WebhookService) ListOrgHooks(ctx context.Context, org string) ([]types.Hook, error) {
|
||||
path := fmt.Sprintf("/api/v1/orgs/%s/hooks", org)
|
||||
return ListAll[types.Hook](ctx, s.client, path, nil)
|
||||
}
|
||||
173
webhooks_test.go
Normal file
173
webhooks_test.go
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
package forge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"forge.lthn.ai/core/go-forge/types"
|
||||
)
|
||||
|
||||
func TestWebhookService_Good_List(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)
|
||||
}
|
||||
w.Header().Set("X-Total-Count", "2")
|
||||
json.NewEncoder(w).Encode([]types.Hook{
|
||||
{ID: 1, Type: "forgejo", Active: true, URL: "https://example.com/hook1"},
|
||||
{ID: 2, Type: "forgejo", Active: false, URL: "https://example.com/hook2"},
|
||||
})
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
f := NewForge(srv.URL, "tok")
|
||||
result, err := f.Webhooks.List(context.Background(), Params{"owner": "core", "repo": "go-forge"}, DefaultList)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(result.Items) != 2 {
|
||||
t.Errorf("got %d items, want 2", len(result.Items))
|
||||
}
|
||||
if result.Items[0].URL != "https://example.com/hook1" {
|
||||
t.Errorf("got url=%q, want %q", result.Items[0].URL, "https://example.com/hook1")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebhookService_Good_Get(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/hooks/1" {
|
||||
t.Errorf("wrong path: %s", r.URL.Path)
|
||||
}
|
||||
json.NewEncoder(w).Encode(types.Hook{
|
||||
ID: 1,
|
||||
Type: "forgejo",
|
||||
Active: true,
|
||||
URL: "https://example.com/hook1",
|
||||
Events: []string{"push", "pull_request"},
|
||||
})
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
f := NewForge(srv.URL, "tok")
|
||||
hook, err := f.Webhooks.Get(context.Background(), Params{"owner": "core", "repo": "go-forge", "id": "1"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if hook.ID != 1 {
|
||||
t.Errorf("got id=%d, want 1", hook.ID)
|
||||
}
|
||||
if hook.URL != "https://example.com/hook1" {
|
||||
t.Errorf("got url=%q, want %q", hook.URL, "https://example.com/hook1")
|
||||
}
|
||||
if len(hook.Events) != 2 {
|
||||
t.Errorf("got %d events, want 2", len(hook.Events))
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebhookService_Good_Create(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)
|
||||
}
|
||||
var opts types.CreateHookOption
|
||||
if err := json.NewDecoder(r.Body).Decode(&opts); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if opts.Type != "forgejo" {
|
||||
t.Errorf("got type=%q, want %q", opts.Type, "forgejo")
|
||||
}
|
||||
json.NewEncoder(w).Encode(types.Hook{
|
||||
ID: 3,
|
||||
Type: opts.Type,
|
||||
Active: opts.Active,
|
||||
Events: opts.Events,
|
||||
})
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
f := NewForge(srv.URL, "tok")
|
||||
hook, err := f.Webhooks.Create(context.Background(), Params{"owner": "core", "repo": "go-forge"}, &types.CreateHookOption{
|
||||
Type: "forgejo",
|
||||
Active: true,
|
||||
Events: []string{"push"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if hook.ID != 3 {
|
||||
t.Errorf("got id=%d, want 3", hook.ID)
|
||||
}
|
||||
if hook.Type != "forgejo" {
|
||||
t.Errorf("got type=%q, want %q", hook.Type, "forgejo")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebhookService_Good_TestHook(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/hooks/1/tests" {
|
||||
t.Errorf("wrong path: %s", r.URL.Path)
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
f := NewForge(srv.URL, "tok")
|
||||
err := f.Webhooks.TestHook(context.Background(), "core", "go-forge", 1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebhookService_Good_ListOrgHooks(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/orgs/myorg/hooks" {
|
||||
t.Errorf("wrong path: %s", r.URL.Path)
|
||||
}
|
||||
w.Header().Set("X-Total-Count", "1")
|
||||
json.NewEncoder(w).Encode([]types.Hook{
|
||||
{ID: 10, Type: "forgejo", Active: true},
|
||||
})
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
f := NewForge(srv.URL, "tok")
|
||||
hooks, err := f.Webhooks.ListOrgHooks(context.Background(), "myorg")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(hooks) != 1 {
|
||||
t.Errorf("got %d hooks, want 1", len(hooks))
|
||||
}
|
||||
if hooks[0].ID != 10 {
|
||||
t.Errorf("got id=%d, want 10", hooks[0].ID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebhookService_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": "hook not found"})
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
f := NewForge(srv.URL, "tok")
|
||||
_, err := f.Webhooks.Get(context.Background(), Params{"owner": "core", "repo": "go-forge", "id": "999"})
|
||||
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