feat: OrgService, TeamService, UserService
Co-Authored-By: Virgil <virgil@lethean.io> Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
f8c6090227
commit
df75d3281d
8 changed files with 441 additions and 8 deletions
8
forge.go
8
forge.go
|
|
@ -30,11 +30,9 @@ func NewForge(url, token string, opts ...Option) *Forge {
|
|||
f.Repos = newRepoService(c)
|
||||
f.Issues = newIssueService(c)
|
||||
f.Pulls = newPullService(c)
|
||||
// Other services initialised in their respective tasks.
|
||||
// Stub them here so tests compile:
|
||||
f.Orgs = &OrgService{}
|
||||
f.Users = &UserService{}
|
||||
f.Teams = &TeamService{}
|
||||
f.Orgs = newOrgService(c)
|
||||
f.Users = newUserService(c)
|
||||
f.Teams = newTeamService(c)
|
||||
f.Admin = &AdminService{}
|
||||
f.Branches = &BranchService{}
|
||||
f.Releases = &ReleaseService{}
|
||||
|
|
|
|||
50
orgs.go
Normal file
50
orgs.go
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
package forge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"forge.lthn.ai/core/go-forge/types"
|
||||
)
|
||||
|
||||
// OrgService handles organisation operations.
|
||||
type OrgService struct {
|
||||
Resource[types.Organization, types.CreateOrgOption, types.EditOrgOption]
|
||||
}
|
||||
|
||||
func newOrgService(c *Client) *OrgService {
|
||||
return &OrgService{
|
||||
Resource: *NewResource[types.Organization, types.CreateOrgOption, types.EditOrgOption](
|
||||
c, "/api/v1/orgs/{org}",
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
// ListMembers returns all members of an organisation.
|
||||
func (s *OrgService) ListMembers(ctx context.Context, org string) ([]types.User, error) {
|
||||
path := fmt.Sprintf("/api/v1/orgs/%s/members", org)
|
||||
return ListAll[types.User](ctx, s.client, path, nil)
|
||||
}
|
||||
|
||||
// AddMember adds a user to an organisation.
|
||||
func (s *OrgService) AddMember(ctx context.Context, org, username string) error {
|
||||
path := fmt.Sprintf("/api/v1/orgs/%s/members/%s", org, username)
|
||||
return s.client.Put(ctx, path, nil, nil)
|
||||
}
|
||||
|
||||
// RemoveMember removes a user from an organisation.
|
||||
func (s *OrgService) RemoveMember(ctx context.Context, org, username string) error {
|
||||
path := fmt.Sprintf("/api/v1/orgs/%s/members/%s", org, username)
|
||||
return s.client.Delete(ctx, path)
|
||||
}
|
||||
|
||||
// ListUserOrgs returns all organisations for a user.
|
||||
func (s *OrgService) ListUserOrgs(ctx context.Context, username string) ([]types.Organization, error) {
|
||||
path := fmt.Sprintf("/api/v1/users/%s/orgs", username)
|
||||
return ListAll[types.Organization](ctx, s.client, path, nil)
|
||||
}
|
||||
|
||||
// ListMyOrgs returns all organisations for the authenticated user.
|
||||
func (s *OrgService) ListMyOrgs(ctx context.Context) ([]types.Organization, error) {
|
||||
return ListAll[types.Organization](ctx, s.client, "/api/v1/user/orgs", nil)
|
||||
}
|
||||
88
orgs_test.go
Normal file
88
orgs_test.go
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
package forge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"forge.lthn.ai/core/go-forge/types"
|
||||
)
|
||||
|
||||
func TestOrgService_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.Organization{
|
||||
{ID: 1, Name: "core"},
|
||||
{ID: 2, Name: "labs"},
|
||||
})
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
f := NewForge(srv.URL, "tok")
|
||||
result, err := f.Orgs.ListAll(context.Background(), nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(result) != 2 {
|
||||
t.Errorf("got %d items, want 2", len(result))
|
||||
}
|
||||
if result[0].Name != "core" {
|
||||
t.Errorf("got name=%q, want %q", result[0].Name, "core")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOrgService_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/orgs/core" {
|
||||
t.Errorf("wrong path: %s", r.URL.Path)
|
||||
}
|
||||
json.NewEncoder(w).Encode(types.Organization{ID: 1, Name: "core"})
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
f := NewForge(srv.URL, "tok")
|
||||
org, err := f.Orgs.Get(context.Background(), Params{"org": "core"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if org.Name != "core" {
|
||||
t.Errorf("got name=%q, want %q", org.Name, "core")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOrgService_Good_ListMembers(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/core/members" {
|
||||
t.Errorf("wrong path: %s", r.URL.Path)
|
||||
}
|
||||
w.Header().Set("X-Total-Count", "2")
|
||||
json.NewEncoder(w).Encode([]types.User{
|
||||
{ID: 1, UserName: "alice"},
|
||||
{ID: 2, UserName: "bob"},
|
||||
})
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
f := NewForge(srv.URL, "tok")
|
||||
members, err := f.Orgs.ListMembers(context.Background(), "core")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(members) != 2 {
|
||||
t.Errorf("got %d members, want 2", len(members))
|
||||
}
|
||||
if members[0].UserName != "alice" {
|
||||
t.Errorf("got username=%q, want %q", members[0].UserName, "alice")
|
||||
}
|
||||
}
|
||||
|
|
@ -2,9 +2,6 @@ package forge
|
|||
|
||||
// Stub service types — replaced as each service is implemented.
|
||||
|
||||
type OrgService struct{}
|
||||
type UserService struct{}
|
||||
type TeamService struct{}
|
||||
type AdminService struct{}
|
||||
type BranchService struct{}
|
||||
type ReleaseService struct{}
|
||||
|
|
|
|||
63
teams.go
Normal file
63
teams.go
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
package forge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"forge.lthn.ai/core/go-forge/types"
|
||||
)
|
||||
|
||||
// TeamService handles team operations.
|
||||
type TeamService struct {
|
||||
Resource[types.Team, types.CreateTeamOption, types.EditTeamOption]
|
||||
}
|
||||
|
||||
func newTeamService(c *Client) *TeamService {
|
||||
return &TeamService{
|
||||
Resource: *NewResource[types.Team, types.CreateTeamOption, types.EditTeamOption](
|
||||
c, "/api/v1/teams/{id}",
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
// ListMembers returns all members of a team.
|
||||
func (s *TeamService) ListMembers(ctx context.Context, teamID int64) ([]types.User, error) {
|
||||
path := fmt.Sprintf("/api/v1/teams/%d/members", teamID)
|
||||
return ListAll[types.User](ctx, s.client, path, nil)
|
||||
}
|
||||
|
||||
// AddMember adds a user to a team.
|
||||
func (s *TeamService) AddMember(ctx context.Context, teamID int64, username string) error {
|
||||
path := fmt.Sprintf("/api/v1/teams/%d/members/%s", teamID, username)
|
||||
return s.client.Put(ctx, path, nil, nil)
|
||||
}
|
||||
|
||||
// RemoveMember removes a user from a team.
|
||||
func (s *TeamService) RemoveMember(ctx context.Context, teamID int64, username string) error {
|
||||
path := fmt.Sprintf("/api/v1/teams/%d/members/%s", teamID, username)
|
||||
return s.client.Delete(ctx, path)
|
||||
}
|
||||
|
||||
// ListRepos returns all repositories managed by a team.
|
||||
func (s *TeamService) ListRepos(ctx context.Context, teamID int64) ([]types.Repository, error) {
|
||||
path := fmt.Sprintf("/api/v1/teams/%d/repos", teamID)
|
||||
return ListAll[types.Repository](ctx, s.client, path, nil)
|
||||
}
|
||||
|
||||
// AddRepo adds a repository to a team.
|
||||
func (s *TeamService) AddRepo(ctx context.Context, teamID int64, org, repo string) error {
|
||||
path := fmt.Sprintf("/api/v1/teams/%d/repos/%s/%s", teamID, org, repo)
|
||||
return s.client.Put(ctx, path, nil, nil)
|
||||
}
|
||||
|
||||
// RemoveRepo removes a repository from a team.
|
||||
func (s *TeamService) RemoveRepo(ctx context.Context, teamID int64, org, repo string) error {
|
||||
path := fmt.Sprintf("/api/v1/teams/%d/repos/%s/%s", teamID, org, repo)
|
||||
return s.client.Delete(ctx, path)
|
||||
}
|
||||
|
||||
// ListOrgTeams returns all teams in an organisation.
|
||||
func (s *TeamService) ListOrgTeams(ctx context.Context, org string) ([]types.Team, error) {
|
||||
path := fmt.Sprintf("/api/v1/orgs/%s/teams", org)
|
||||
return ListAll[types.Team](ctx, s.client, path, nil)
|
||||
}
|
||||
81
teams_test.go
Normal file
81
teams_test.go
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
package forge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"forge.lthn.ai/core/go-forge/types"
|
||||
)
|
||||
|
||||
func TestTeamService_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/teams/42" {
|
||||
t.Errorf("wrong path: %s", r.URL.Path)
|
||||
}
|
||||
json.NewEncoder(w).Encode(types.Team{ID: 42, Name: "developers"})
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
f := NewForge(srv.URL, "tok")
|
||||
team, err := f.Teams.Get(context.Background(), Params{"id": "42"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if team.Name != "developers" {
|
||||
t.Errorf("got name=%q, want %q", team.Name, "developers")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTeamService_Good_ListMembers(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/teams/42/members" {
|
||||
t.Errorf("wrong path: %s", r.URL.Path)
|
||||
}
|
||||
w.Header().Set("X-Total-Count", "2")
|
||||
json.NewEncoder(w).Encode([]types.User{
|
||||
{ID: 1, UserName: "alice"},
|
||||
{ID: 2, UserName: "bob"},
|
||||
})
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
f := NewForge(srv.URL, "tok")
|
||||
members, err := f.Teams.ListMembers(context.Background(), 42)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(members) != 2 {
|
||||
t.Errorf("got %d members, want 2", len(members))
|
||||
}
|
||||
if members[0].UserName != "alice" {
|
||||
t.Errorf("got username=%q, want %q", members[0].UserName, "alice")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTeamService_Good_AddMember(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/teams/42/members/alice" {
|
||||
t.Errorf("wrong path: %s", r.URL.Path)
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
f := NewForge(srv.URL, "tok")
|
||||
err := f.Teams.AddMember(context.Background(), 42, "alice")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
72
users.go
Normal file
72
users.go
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
package forge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"forge.lthn.ai/core/go-forge/types"
|
||||
)
|
||||
|
||||
// UserService handles user operations.
|
||||
type UserService struct {
|
||||
Resource[types.User, struct{}, struct{}]
|
||||
}
|
||||
|
||||
func newUserService(c *Client) *UserService {
|
||||
return &UserService{
|
||||
Resource: *NewResource[types.User, struct{}, struct{}](
|
||||
c, "/api/v1/users/{username}",
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
// GetCurrent returns the authenticated user.
|
||||
func (s *UserService) GetCurrent(ctx context.Context) (*types.User, error) {
|
||||
var out types.User
|
||||
if err := s.client.Get(ctx, "/api/v1/user", &out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
// ListFollowers returns all followers of a user.
|
||||
func (s *UserService) ListFollowers(ctx context.Context, username string) ([]types.User, error) {
|
||||
path := fmt.Sprintf("/api/v1/users/%s/followers", username)
|
||||
return ListAll[types.User](ctx, s.client, path, nil)
|
||||
}
|
||||
|
||||
// ListFollowing returns all users that a user is following.
|
||||
func (s *UserService) ListFollowing(ctx context.Context, username string) ([]types.User, error) {
|
||||
path := fmt.Sprintf("/api/v1/users/%s/following", username)
|
||||
return ListAll[types.User](ctx, s.client, path, nil)
|
||||
}
|
||||
|
||||
// Follow follows a user as the authenticated user.
|
||||
func (s *UserService) Follow(ctx context.Context, username string) error {
|
||||
path := fmt.Sprintf("/api/v1/user/following/%s", username)
|
||||
return s.client.Put(ctx, path, nil, nil)
|
||||
}
|
||||
|
||||
// Unfollow unfollows a user as the authenticated user.
|
||||
func (s *UserService) Unfollow(ctx context.Context, username string) error {
|
||||
path := fmt.Sprintf("/api/v1/user/following/%s", username)
|
||||
return s.client.Delete(ctx, path)
|
||||
}
|
||||
|
||||
// ListStarred returns all repositories starred by a user.
|
||||
func (s *UserService) ListStarred(ctx context.Context, username string) ([]types.Repository, error) {
|
||||
path := fmt.Sprintf("/api/v1/users/%s/starred", username)
|
||||
return ListAll[types.Repository](ctx, s.client, path, nil)
|
||||
}
|
||||
|
||||
// Star stars a repository as the authenticated user.
|
||||
func (s *UserService) Star(ctx context.Context, owner, repo string) error {
|
||||
path := fmt.Sprintf("/api/v1/user/starred/%s/%s", owner, repo)
|
||||
return s.client.Put(ctx, path, nil, nil)
|
||||
}
|
||||
|
||||
// Unstar unstars a repository as the authenticated user.
|
||||
func (s *UserService) Unstar(ctx context.Context, owner, repo string) error {
|
||||
path := fmt.Sprintf("/api/v1/user/starred/%s/%s", owner, repo)
|
||||
return s.client.Delete(ctx, path)
|
||||
}
|
||||
84
users_test.go
Normal file
84
users_test.go
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
package forge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"forge.lthn.ai/core/go-forge/types"
|
||||
)
|
||||
|
||||
func TestUserService_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/users/alice" {
|
||||
t.Errorf("wrong path: %s", r.URL.Path)
|
||||
}
|
||||
json.NewEncoder(w).Encode(types.User{ID: 1, UserName: "alice"})
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
f := NewForge(srv.URL, "tok")
|
||||
user, err := f.Users.Get(context.Background(), Params{"username": "alice"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if user.UserName != "alice" {
|
||||
t.Errorf("got username=%q, want %q", user.UserName, "alice")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserService_Good_GetCurrent(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/user" {
|
||||
t.Errorf("wrong path: %s", r.URL.Path)
|
||||
}
|
||||
json.NewEncoder(w).Encode(types.User{ID: 1, UserName: "me"})
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
f := NewForge(srv.URL, "tok")
|
||||
user, err := f.Users.GetCurrent(context.Background())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if user.UserName != "me" {
|
||||
t.Errorf("got username=%q, want %q", user.UserName, "me")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserService_Good_ListFollowers(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/users/alice/followers" {
|
||||
t.Errorf("wrong path: %s", r.URL.Path)
|
||||
}
|
||||
w.Header().Set("X-Total-Count", "2")
|
||||
json.NewEncoder(w).Encode([]types.User{
|
||||
{ID: 2, UserName: "bob"},
|
||||
{ID: 3, UserName: "charlie"},
|
||||
})
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
f := NewForge(srv.URL, "tok")
|
||||
followers, err := f.Users.ListFollowers(context.Background(), "alice")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(followers) != 2 {
|
||||
t.Errorf("got %d followers, want 2", len(followers))
|
||||
}
|
||||
if followers[0].UserName != "bob" {
|
||||
t.Errorf("got username=%q, want %q", followers[0].UserName, "bob")
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue