Skip to content

feat: Admin Management Functionality #953

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
151 changes: 151 additions & 0 deletions admin_invite.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
package openai

import (
"context"
"fmt"
"net/http"
"net/url"
)

const (
adminInvitesSuffix = "/organization/invites"
)

var (
// adminInviteRoles is a list of valid roles for an Admin Invite.
adminInviteRoles = []string{"owner", "member"}
)

// AdminInvite represents an Admin Invite.
type AdminInvite struct {
Object string `json:"object"`
ID string `json:"id"`
Email string `json:"email"`
Role string `json:"role"`
Status string `json:"status"`
InvitedAt int64 `json:"invited_at"`
ExpiresAt int64 `json:"expires_at"`
AcceptedAt int64 `json:"accepted_at"`
Projects []AdminInviteProject `json:"projects"`

httpHeader
}

// AdminInviteProject represents a project associated with an Admin Invite.
type AdminInviteProject struct {
ID string `json:"id"`
Role string `json:"role"`
}

// AdminInviteList represents a list of Admin Invites.
type AdminInviteList struct {
Object string `json:"object"`
AdminInvites []AdminInvite `json:"data"`
FirstID string `json:"first_id"`
LastID string `json:"last_id"`
HasMore bool `json:"has_more"`

httpHeader
}

// AdminInviteDeleteResponse represents the response from deleting an Admin Invite.
type AdminInviteDeleteResponse struct {
ID string `json:"id"`
Object string `json:"object"`
Deleted bool `json:"deleted"`

httpHeader
}

// ListAdminInvites lists Admin Invites associated with the organization.
func (c *Client) ListAdminInvites(
ctx context.Context,
limit *int,
after *string,
) (response AdminInviteList, err error) {
urlValues := url.Values{}
if limit != nil {
urlValues.Add("limit", fmt.Sprintf("%d", *limit))
}
if after != nil {
urlValues.Add("after", *after)
}

encodedValues := ""
if len(urlValues) > 0 {
encodedValues = "?" + urlValues.Encode()
}

urlSuffix := adminInvitesSuffix + encodedValues
req, err := c.newRequest(ctx, http.MethodGet, c.fullURL(urlSuffix))
if err != nil {
return
}

Check warning on line 83 in admin_invite.go

View check run for this annotation

Codecov / codecov/patch

admin_invite.go#L82-L83

Added lines #L82 - L83 were not covered by tests

err = c.sendRequest(req, &response)
return
}

// CreateAdminInvite creates a new Admin Invite.
func (c *Client) CreateAdminInvite(
ctx context.Context,
email string,
role string,
projects *[]AdminInviteProject,
) (response AdminInvite, err error) {
// Validate the role.
if !containsSubstr(adminInviteRoles, role) {
return response, fmt.Errorf("invalid admin role: %s", role)
}

Check warning on line 99 in admin_invite.go

View check run for this annotation

Codecov / codecov/patch

admin_invite.go#L98-L99

Added lines #L98 - L99 were not covered by tests

// Create the request object.
request := struct {
Email string `json:"email"`
Role string `json:"role"`
Projects *[]AdminInviteProject `json:"projects,omitempty"`
}{
Email: email,
Role: role,
Projects: projects,
}

urlSuffix := adminInvitesSuffix
req, err := c.newRequest(ctx, http.MethodPost, c.fullURL(urlSuffix), withBody(request))
if err != nil {
return
}

Check warning on line 116 in admin_invite.go

View check run for this annotation

Codecov / codecov/patch

admin_invite.go#L115-L116

Added lines #L115 - L116 were not covered by tests

err = c.sendRequest(req, &response)

return
}

// RetrieveAdminInvite retrieves an Admin Invite.
func (c *Client) RetrieveAdminInvite(
ctx context.Context,
inviteID string,
) (response AdminInvite, err error) {
urlSuffix := fmt.Sprintf("%s/%s", adminInvitesSuffix, inviteID)
req, err := c.newRequest(ctx, http.MethodGet, c.fullURL(urlSuffix))
if err != nil {
return
}

Check warning on line 132 in admin_invite.go

View check run for this annotation

Codecov / codecov/patch

admin_invite.go#L131-L132

Added lines #L131 - L132 were not covered by tests

err = c.sendRequest(req, &response)
return
}

// DeleteAdminInvite deletes an Admin Invite.
func (c *Client) DeleteAdminInvite(
ctx context.Context,
inviteID string,
) (response AdminInviteDeleteResponse, err error) {
urlSuffix := fmt.Sprintf("%s/%s", adminInvitesSuffix, inviteID)
req, err := c.newRequest(ctx, http.MethodDelete, c.fullURL(urlSuffix))
if err != nil {
return
}

Check warning on line 147 in admin_invite.go

View check run for this annotation

Codecov / codecov/patch

admin_invite.go#L146-L147

Added lines #L146 - L147 were not covered by tests

err = c.sendRequest(req, &response)
return
}
163 changes: 163 additions & 0 deletions admin_invite_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
package openai_test

import (
"context"
"encoding/json"
"fmt"
"net/http"
"testing"

"github.com/sashabaranov/go-openai"
"github.com/sashabaranov/go-openai/internal/test/checks"
)

func TestAdminInvite(t *testing.T) {
adminInviteObject := "organization.invite"
adminInviteID := "invite-abc-123"
adminInviteEmail := "invite@openai.com"
adminInviteRole := "owner"
adminInviteStatus := "pending"

adminInviteInvitedAt := int64(1711471533)
adminInviteExpiresAt := int64(1711471533)
adminInviteAcceptedAt := int64(1711471533)
adminInviteProjects := []openai.AdminInviteProject{
{
ID: "project-id",
Role: "owner",
},
}

client, server, teardown := setupOpenAITestServer()
defer teardown()

server.RegisterHandler(
"/v1/organization/invites",
func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
resBytes, _ := json.Marshal(openai.AdminInviteList{
Object: "list",
AdminInvites: []openai.AdminInvite{
{
Object: adminInviteObject,
ID: adminInviteID,
Email: adminInviteEmail,
Role: adminInviteRole,
Status: adminInviteStatus,
InvitedAt: adminInviteInvitedAt,
ExpiresAt: adminInviteExpiresAt,
AcceptedAt: adminInviteAcceptedAt,
Projects: adminInviteProjects,
},
},
FirstID: "first_id",
LastID: "last_id",
HasMore: false,
})
fmt.Fprintln(w, string(resBytes))

case http.MethodPost:
resBytes, _ := json.Marshal(openai.AdminInvite{
Object: adminInviteObject,
ID: adminInviteID,
Email: adminInviteEmail,
Role: adminInviteRole,
Status: adminInviteStatus,
InvitedAt: adminInviteInvitedAt,
ExpiresAt: adminInviteExpiresAt,
AcceptedAt: adminInviteAcceptedAt,
Projects: adminInviteProjects,
})
fmt.Fprintln(w, string(resBytes))
}
},
)

server.RegisterHandler(
"/v1/organization/invites/"+adminInviteID,
func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodDelete:
resBytes, _ := json.Marshal(openai.AdminInviteDeleteResponse{
ID: adminInviteID,
Object: adminInviteObject,
Deleted: true,
})
fmt.Fprintln(w, string(resBytes))

case http.MethodGet:
resBytes, _ := json.Marshal(openai.AdminInvite{
Object: adminInviteObject,
ID: adminInviteID,
Email: adminInviteEmail,
Role: adminInviteRole,
Status: adminInviteStatus,
InvitedAt: adminInviteInvitedAt,
ExpiresAt: adminInviteExpiresAt,
AcceptedAt: adminInviteAcceptedAt,
Projects: adminInviteProjects,
})
fmt.Fprintln(w, string(resBytes))
}
},
)

ctx := context.Background()

t.Run("ListAdminInvites", func(t *testing.T) {
adminInvites, err := client.ListAdminInvites(ctx, nil, nil)
checks.NoError(t, err, "ListAdminInvites error")

if len(adminInvites.AdminInvites) != 1 {
t.Fatalf("expected 1 admin invite, got %d", len(adminInvites.AdminInvites))
}

if adminInvites.AdminInvites[0].ID != adminInviteID {
t.Errorf("expected admin invite ID %s, got %s", adminInviteID, adminInvites.AdminInvites[0].ID)
}
})

t.Run("ListAdminInvitesFilter", func(t *testing.T) {
limit := 10
after := "after-id"

adminInvites, err := client.ListAdminInvites(ctx, &limit, &after)
checks.NoError(t, err, "ListAdminInvites error")

if len(adminInvites.AdminInvites) != 1 {
t.Fatalf("expected 1 admin invite, got %d", len(adminInvites.AdminInvites))
}

if adminInvites.AdminInvites[0].ID != adminInviteID {
t.Errorf("expected admin invite ID %s, got %s", adminInviteID, adminInvites.AdminInvites[0].ID)
}
})

t.Run("CreateAdminInvite", func(t *testing.T) {
adminInvite, err := client.CreateAdminInvite(ctx, adminInviteEmail, adminInviteRole, &adminInviteProjects)
checks.NoError(t, err, "CreateAdminInvite error")

if adminInvite.ID != adminInviteID {
t.Errorf("expected admin invite ID %s, got %s", adminInviteID, adminInvite.ID)
}
})

t.Run("RetrieveAdminInvite", func(t *testing.T) {
adminInvite, err := client.RetrieveAdminInvite(ctx, adminInviteID)
checks.NoError(t, err, "RetrieveAdminInvite error")

if adminInvite.ID != adminInviteID {
t.Errorf("expected admin invite ID %s, got %s", adminInviteID, adminInvite.ID)
}
})

t.Run("DeleteAdminInvite", func(t *testing.T) {
adminInviteDeleteResponse, err := client.DeleteAdminInvite(ctx, adminInviteID)
checks.NoError(t, err, "DeleteAdminInvite error")

if !adminInviteDeleteResponse.Deleted {
t.Errorf("expected admin invite to be deleted, got not deleted")
}
})
}
Loading
Loading