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 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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))
}

Check warning on line 69 in admin_invite.go

View check run for this annotation

Codecov / codecov/patch

admin_invite.go#L68-L69

Added lines #L68 - L69 were not covered by tests
if after != nil {
urlValues.Add("after", *after)
}

Check warning on line 72 in admin_invite.go

View check run for this annotation

Codecov / codecov/patch

admin_invite.go#L71-L72

Added lines #L71 - L72 were not covered by tests

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

Check warning on line 77 in admin_invite.go

View check run for this annotation

Codecov / codecov/patch

admin_invite.go#L76-L77

Added lines #L76 - L77 were not covered by tests

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
}
147 changes: 147 additions & 0 deletions admin_invite_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
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 := "[email protected]"
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("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