refactor: update api and client to reference humanIds

This commit is contained in:
talksik
2026-03-24 19:08:51 -07:00
parent 3bf3f16be7
commit 57a1cbc696
24 changed files with 666 additions and 416 deletions
+8 -2
View File
@@ -5,9 +5,15 @@ import "time"
type Network struct {
ID string
Name string
AdminEmail string
MemberEmails []string
AdminHumanId string
MemberHumanIds []string
OpenStreamCapacity int
OpenStreamCount int
CreatedAt time.Time
}
type Invitation struct {
NetworkID string
Email string
CreatedAt time.Time
}
+91 -79
View File
@@ -26,18 +26,21 @@ func newNetworkID() (networkID, error) {
var errCapacityExceeded = errors.New("capacity exceeded")
type repository interface {
create(ctx context.Context, name, adminEmail string) (*Network, error)
create(ctx context.Context, name, adminHumanId string) (*Network, error)
getByID(ctx context.Context, id string) (*Network, error)
updateName(ctx context.Context, id, name string) error
delete(ctx context.Context, id string) error
addMember(ctx context.Context, networkID, email string) error
removeMember(ctx context.Context, networkID, email string) error
getMemberEmails(ctx context.Context, networkID string) ([]string, error)
getNetworksForEmail(ctx context.Context, email string) ([]*Network, error)
isMember(ctx context.Context, networkID, email string) (bool, error)
setOpenStreamCapacity(ctx context.Context, id string, capacity int) error
incrementOpenStreamCount(ctx context.Context, id string) error
decrementOpenStreamCount(ctx context.Context, id string) error
addMember(ctx context.Context, networkID, humanId string) error
removeMember(ctx context.Context, networkID, humanId string) error
getMemberHumanIds(ctx context.Context, networkID string) ([]string, error)
getNetworksForHuman(ctx context.Context, humanId string) ([]*Network, error)
isMember(ctx context.Context, networkID, humanId string) (bool, error)
// Invitations
createInvitation(ctx context.Context, networkID, email string) error
getInvitationsByEmail(ctx context.Context, email string) ([]*Invitation, error)
getInvitationsByNetwork(ctx context.Context, networkID string) ([]*Invitation, error)
deleteInvitation(ctx context.Context, networkID, email string) error
}
type repositoryImpl struct {
@@ -48,7 +51,7 @@ func newRepository(pool *pgxpool.Pool) repository {
return &repositoryImpl{pool: pool}
}
func (r *repositoryImpl) create(ctx context.Context, name, adminEmail string) (*Network, error) {
func (r *repositoryImpl) create(ctx context.Context, name, adminHumanId string) (*Network, error) {
id, err := newNetworkID()
if err != nil {
return nil, err
@@ -56,24 +59,24 @@ func (r *repositoryImpl) create(ctx context.Context, name, adminEmail string) (*
var n Network
err = r.pool.QueryRow(ctx,
`INSERT INTO networks (id, name, admin_email) VALUES ($1, $2, $3)
RETURNING id, name, admin_email, open_stream_capacity, open_stream_count, created_at`,
id.String(), name, adminEmail,
).Scan(&n.ID, &n.Name, &n.AdminEmail, &n.OpenStreamCapacity, &n.OpenStreamCount, &n.CreatedAt)
`INSERT INTO networks (id, name, admin_human_id) VALUES ($1, $2, $3)
RETURNING id, name, admin_human_id, open_stream_capacity, open_stream_count, created_at`,
id.String(), name, adminHumanId,
).Scan(&n.ID, &n.Name, &n.AdminHumanId, &n.OpenStreamCapacity, &n.OpenStreamCount, &n.CreatedAt)
if err != nil {
return nil, err
}
n.MemberEmails = []string{}
n.MemberHumanIds = []string{}
return &n, nil
}
func (r *repositoryImpl) getByID(ctx context.Context, id string) (*Network, error) {
var n Network
err := r.pool.QueryRow(ctx,
`SELECT id, name, admin_email, open_stream_capacity, open_stream_count, created_at FROM networks WHERE id = $1`,
`SELECT id, name, admin_human_id, open_stream_capacity, open_stream_count, created_at FROM networks WHERE id = $1`,
id,
).Scan(&n.ID, &n.Name, &n.AdminEmail, &n.OpenStreamCapacity, &n.OpenStreamCount, &n.CreatedAt)
).Scan(&n.ID, &n.Name, &n.AdminHumanId, &n.OpenStreamCapacity, &n.OpenStreamCount, &n.CreatedAt)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, errNotFound
@@ -81,7 +84,7 @@ func (r *repositoryImpl) getByID(ctx context.Context, id string) (*Network, erro
return nil, err
}
n.MemberEmails, err = r.getMemberEmails(ctx, id)
n.MemberHumanIds, err = r.getMemberHumanIds(ctx, id)
if err != nil {
return nil, err
}
@@ -114,26 +117,26 @@ func (r *repositoryImpl) delete(ctx context.Context, id string) error {
return nil
}
func (r *repositoryImpl) addMember(ctx context.Context, networkID, email string) error {
func (r *repositoryImpl) addMember(ctx context.Context, networkID, humanId string) error {
_, err := r.pool.Exec(ctx,
`INSERT INTO network_members (network_id, email) VALUES ($1, $2)
ON CONFLICT (network_id, email) DO NOTHING`,
networkID, email,
`INSERT INTO network_members (network_id, human_id) VALUES ($1, $2)
ON CONFLICT (network_id, human_id) DO NOTHING`,
networkID, humanId,
)
return err
}
func (r *repositoryImpl) removeMember(ctx context.Context, networkID, email string) error {
func (r *repositoryImpl) removeMember(ctx context.Context, networkID, humanId string) error {
_, err := r.pool.Exec(ctx,
`DELETE FROM network_members WHERE network_id = $1 AND email = $2`,
networkID, email,
`DELETE FROM network_members WHERE network_id = $1 AND human_id = $2`,
networkID, humanId,
)
return err
}
func (r *repositoryImpl) getMemberEmails(ctx context.Context, networkID string) ([]string, error) {
func (r *repositoryImpl) getMemberHumanIds(ctx context.Context, networkID string) ([]string, error) {
rows, err := r.pool.Query(ctx,
`SELECT email FROM network_members WHERE network_id = $1`,
`SELECT human_id FROM network_members WHERE network_id = $1`,
networkID,
)
if err != nil {
@@ -141,24 +144,24 @@ func (r *repositoryImpl) getMemberEmails(ctx context.Context, networkID string)
}
defer rows.Close()
var emails []string
var humanIds []string
for rows.Next() {
var email string
if err := rows.Scan(&email); err != nil {
var humanId string
if err := rows.Scan(&humanId); err != nil {
return nil, err
}
emails = append(emails, email)
humanIds = append(humanIds, humanId)
}
return emails, rows.Err()
return humanIds, rows.Err()
}
func (r *repositoryImpl) getNetworksForEmail(ctx context.Context, email string) ([]*Network, error) {
func (r *repositoryImpl) getNetworksForHuman(ctx context.Context, humanId string) ([]*Network, error) {
rows, err := r.pool.Query(ctx,
`SELECT n.id, n.name, n.admin_email, n.open_stream_capacity, n.open_stream_count, n.created_at
`SELECT n.id, n.name, n.admin_human_id, n.open_stream_capacity, n.open_stream_count, n.created_at
FROM networks n
WHERE n.admin_email = $1
OR EXISTS (SELECT 1 FROM network_members nm WHERE nm.network_id = n.id AND nm.email = $1)`,
email,
WHERE n.admin_human_id = $1
OR EXISTS (SELECT 1 FROM network_members nm WHERE nm.network_id = n.id AND nm.human_id = $1)`,
humanId,
)
if err != nil {
return nil, err
@@ -168,7 +171,7 @@ func (r *repositoryImpl) getNetworksForEmail(ctx context.Context, email string)
var networks []*Network
for rows.Next() {
var n Network
if err := rows.Scan(&n.ID, &n.Name, &n.AdminEmail, &n.OpenStreamCapacity, &n.OpenStreamCount, &n.CreatedAt); err != nil {
if err := rows.Scan(&n.ID, &n.Name, &n.AdminHumanId, &n.OpenStreamCapacity, &n.OpenStreamCount, &n.CreatedAt); err != nil {
return nil, err
}
networks = append(networks, &n)
@@ -178,7 +181,7 @@ func (r *repositoryImpl) getNetworksForEmail(ctx context.Context, email string)
}
for _, n := range networks {
n.MemberEmails, err = r.getMemberEmails(ctx, n.ID)
n.MemberHumanIds, err = r.getMemberHumanIds(ctx, n.ID)
if err != nil {
return nil, err
}
@@ -187,66 +190,75 @@ func (r *repositoryImpl) getNetworksForEmail(ctx context.Context, email string)
return networks, nil
}
func (r *repositoryImpl) isMember(ctx context.Context, networkID, email string) (bool, error) {
func (r *repositoryImpl) isMember(ctx context.Context, networkID, humanId string) (bool, error) {
var isMember bool
err := r.pool.QueryRow(ctx, `
SELECT EXISTS(
SELECT 1 FROM networks n
LEFT JOIN network_members nm ON nm.network_id = n.id AND nm.email = $2
WHERE n.id = $1 AND (n.admin_email = $2 OR nm.email IS NOT NULL)
LEFT JOIN network_members nm ON nm.network_id = n.id AND nm.human_id = $2
WHERE n.id = $1 AND (n.admin_human_id = $2 OR nm.human_id IS NOT NULL)
)
`, networkID, email).Scan(&isMember)
`, networkID, humanId).Scan(&isMember)
return isMember, err
}
func (r *repositoryImpl) setOpenStreamCapacity(ctx context.Context, id string, capacity int) error {
result, err := r.pool.Exec(ctx,
`UPDATE networks SET open_stream_capacity = $1 WHERE id = $2`,
capacity, id,
// Invitation methods
func (r *repositoryImpl) createInvitation(ctx context.Context, networkID, email string) error {
_, err := r.pool.Exec(ctx,
`INSERT INTO network_invitations (network_id, email) VALUES ($1, $2)
ON CONFLICT (network_id, email) DO NOTHING`,
networkID, email,
)
if err != nil {
return err
}
if result.RowsAffected() == 0 {
return errNotFound
}
return nil
return err
}
func (r *repositoryImpl) incrementOpenStreamCount(ctx context.Context, id string) error {
result, err := r.pool.Exec(ctx,
`UPDATE networks SET open_stream_count = open_stream_count + 1
WHERE id = $1 AND open_stream_count < open_stream_capacity`,
id,
func (r *repositoryImpl) getInvitationsByEmail(ctx context.Context, email string) ([]*Invitation, error) {
rows, err := r.pool.Query(ctx,
`SELECT network_id, email, created_at FROM network_invitations WHERE email = $1`,
email,
)
if err != nil {
return err
return nil, err
}
if result.RowsAffected() == 0 {
// Check if network exists vs capacity exceeded
var exists bool
err := r.pool.QueryRow(ctx, `SELECT EXISTS(SELECT 1 FROM networks WHERE id = $1)`, id).Scan(&exists)
if err != nil {
return err
defer rows.Close()
var invitations []*Invitation
for rows.Next() {
var inv Invitation
if err := rows.Scan(&inv.NetworkID, &inv.Email, &inv.CreatedAt); err != nil {
return nil, err
}
if !exists {
return errNotFound
}
return errCapacityExceeded
invitations = append(invitations, &inv)
}
return nil
return invitations, rows.Err()
}
func (r *repositoryImpl) decrementOpenStreamCount(ctx context.Context, id string) error {
result, err := r.pool.Exec(ctx,
`UPDATE networks SET open_stream_count = GREATEST(0, open_stream_count - 1) WHERE id = $1`,
id,
func (r *repositoryImpl) getInvitationsByNetwork(ctx context.Context, networkID string) ([]*Invitation, error) {
rows, err := r.pool.Query(ctx,
`SELECT network_id, email, created_at FROM network_invitations WHERE network_id = $1`,
networkID,
)
if err != nil {
return err
return nil, err
}
if result.RowsAffected() == 0 {
return errNotFound
defer rows.Close()
var invitations []*Invitation
for rows.Next() {
var inv Invitation
if err := rows.Scan(&inv.NetworkID, &inv.Email, &inv.CreatedAt); err != nil {
return nil, err
}
invitations = append(invitations, &inv)
}
return nil
return invitations, rows.Err()
}
func (r *repositoryImpl) deleteInvitation(ctx context.Context, networkID, email string) error {
_, err := r.pool.Exec(ctx,
`DELETE FROM network_invitations WHERE network_id = $1 AND email = $2`,
networkID, email,
)
return err
}
+76 -65
View File
@@ -15,23 +15,23 @@ var ErrInvalidName = errors.New("name cannot be empty")
var ErrCapacityExceeded = errors.New("active stream capacity exceeded")
type Service interface {
// Create creates a network and adds adminEmail as the first member. Returns ErrInvalidName if name is empty.
Create(ctx context.Context, name, adminEmail string) (*Network, error)
// Create creates a network and adds adminHumanId as the first member. Returns ErrInvalidName if name is empty.
Create(ctx context.Context, name, adminHumanId string) (*Network, error)
// GetByID returns ErrNotFound if network doesn't exist.
GetByID(ctx context.Context, id string) (*Network, error)
// SetName returns ErrNotFound or ErrInvalidName.
SetName(ctx context.Context, id, name string) error
AddMembers(ctx context.Context, networkID string, emails []string) error
RemoveMember(ctx context.Context, networkID, email string) error
ListForEmail(ctx context.Context, email string) ([]*Network, error)
IsMember(ctx context.Context, networkID, email string) (bool, error)
AddMembers(ctx context.Context, networkID string, humanIds []string) error
RemoveMember(ctx context.Context, networkID, humanId string) error
ListForHuman(ctx context.Context, humanId string) ([]*Network, error)
IsMember(ctx context.Context, networkID, humanId string) (bool, error)
// SetOpenStreamCapacity sets the max open streams for a network. Returns ErrNotFound.
SetOpenStreamCapacity(ctx context.Context, networkID string, capacity int) error
// IncrementOpenStreamCount returns ErrNotFound or ErrCapacityExceeded.
IncrementOpenStreamCount(ctx context.Context, networkID string) error
// DecrementOpenStreamCount returns ErrNotFound.
DecrementOpenStreamCount(ctx context.Context, networkID string) error
// Invitations (email-based, for users who haven't registered yet)
InviteByEmail(ctx context.Context, networkID string, emails []string) error
ListInvitationsForEmail(ctx context.Context, email string) ([]*Invitation, error)
ListInvitationsForNetwork(ctx context.Context, networkID string) ([]*Invitation, error)
AcceptInvitation(ctx context.Context, networkID, email, humanId string) error
RevokeInvitation(ctx context.Context, networkID, email string) error
}
type serviceImpl struct {
@@ -42,23 +42,18 @@ func NewService(pool *pgxpool.Pool) Service {
return &serviceImpl{repo: newRepository(pool)}
}
func (s *serviceImpl) Create(ctx context.Context, name, adminEmail string) (*Network, error) {
func (s *serviceImpl) Create(ctx context.Context, name, adminHumanId string) (*Network, error) {
name = strings.TrimSpace(name)
if name == "" {
return nil, ErrInvalidName
}
adminEmail, err := utils.NormalizeEmail(adminEmail)
network, err := s.repo.create(ctx, name, adminHumanId)
if err != nil {
return nil, err
}
network, err := s.repo.create(ctx, name, adminEmail)
if err != nil {
return nil, err
}
err = s.AddMembers(ctx, network.ID, []string{adminEmail})
err = s.AddMembers(ctx, network.ID, []string{adminHumanId})
if err != nil {
return nil, err
}
@@ -87,69 +82,85 @@ func (s *serviceImpl) SetName(ctx context.Context, id, name string) error {
return err
}
func (s *serviceImpl) AddMembers(ctx context.Context, networkID string, emails []string) error {
for _, email := range emails {
normalized, err := utils.NormalizeEmail(email)
if err != nil {
return fmt.Errorf("invalid email: %w", err)
func (s *serviceImpl) AddMembers(ctx context.Context, networkID string, humanIds []string) error {
for _, humanId := range humanIds {
if humanId == "" {
return fmt.Errorf("invalid humanId")
}
if err := s.repo.addMember(ctx, networkID, normalized); err != nil {
if err := s.repo.addMember(ctx, networkID, humanId); err != nil {
return err
}
}
return nil
}
func (s *serviceImpl) RemoveMember(ctx context.Context, networkID, email string) error {
email, err := utils.NormalizeEmail(email)
if err != nil {
return fmt.Errorf("invalid email: %w", err)
func (s *serviceImpl) RemoveMember(ctx context.Context, networkID, humanId string) error {
if humanId == "" {
return fmt.Errorf("invalid humanId")
}
return s.repo.removeMember(ctx, networkID, email)
return s.repo.removeMember(ctx, networkID, humanId)
}
func (s *serviceImpl) ListForEmail(ctx context.Context, email string) ([]*Network, error) {
email, err := utils.NormalizeEmail(email)
func (s *serviceImpl) ListForHuman(ctx context.Context, humanId string) ([]*Network, error) {
if humanId == "" {
return nil, fmt.Errorf("invalid humanId")
}
return s.repo.getNetworksForHuman(ctx, humanId)
}
func (s *serviceImpl) IsMember(ctx context.Context, networkID, humanId string) (bool, error) {
if humanId == "" {
return false, fmt.Errorf("invalid humanId")
}
return s.repo.isMember(ctx, networkID, humanId)
}
// Invitation methods
func (s *serviceImpl) InviteByEmail(ctx context.Context, networkID string, emails []string) error {
for _, email := range emails {
normalized, err := utils.NormalizeEmail(email)
if err != nil {
return fmt.Errorf("invalid email %q: %w", email, err)
}
if err := s.repo.createInvitation(ctx, networkID, normalized); err != nil {
return err
}
}
return nil
}
func (s *serviceImpl) ListInvitationsForEmail(ctx context.Context, email string) ([]*Invitation, error) {
normalized, err := utils.NormalizeEmail(email)
if err != nil {
return nil, fmt.Errorf("invalid email: %w", err)
}
return s.repo.getNetworksForEmail(ctx, email)
return s.repo.getInvitationsByEmail(ctx, normalized)
}
func (s *serviceImpl) IsMember(ctx context.Context, networkID, email string) (bool, error) {
email, err := utils.NormalizeEmail(email)
func (s *serviceImpl) ListInvitationsForNetwork(ctx context.Context, networkID string) ([]*Invitation, error) {
return s.repo.getInvitationsByNetwork(ctx, networkID)
}
func (s *serviceImpl) AcceptInvitation(ctx context.Context, networkID, email, humanId string) error {
normalized, err := utils.NormalizeEmail(email)
if err != nil {
return false, err
return fmt.Errorf("invalid email: %w", err)
}
return s.repo.isMember(ctx, networkID, email)
if humanId == "" {
return fmt.Errorf("invalid humanId")
}
if err := s.repo.deleteInvitation(ctx, networkID, normalized); err != nil {
return err
}
return s.repo.addMember(ctx, networkID, humanId)
}
func (s *serviceImpl) SetOpenStreamCapacity(ctx context.Context, networkID string, capacity int) error {
if capacity < 0 {
capacity = 0
func (s *serviceImpl) RevokeInvitation(ctx context.Context, networkID, email string) error {
normalized, err := utils.NormalizeEmail(email)
if err != nil {
return fmt.Errorf("invalid email: %w", err)
}
err := s.repo.setOpenStreamCapacity(ctx, networkID, capacity)
if errors.Is(err, errNotFound) {
return ErrNotFound
}
return err
}
func (s *serviceImpl) IncrementOpenStreamCount(ctx context.Context, networkID string) error {
err := s.repo.incrementOpenStreamCount(ctx, networkID)
if errors.Is(err, errNotFound) {
return ErrNotFound
}
if errors.Is(err, errCapacityExceeded) {
return ErrCapacityExceeded
}
return err
}
func (s *serviceImpl) DecrementOpenStreamCount(ctx context.Context, networkID string) error {
err := s.repo.decrementOpenStreamCount(ctx, networkID)
if errors.Is(err, errNotFound) {
return ErrNotFound
}
return err
return s.repo.deleteInvitation(ctx, networkID, normalized)
}
+83 -16
View File
@@ -25,12 +25,17 @@ func TestNetworkService(t *testing.T) {
ctx := context.Background()
svc := network.NewService(dbPool)
adminHumanId := "human_admin123"
member1HumanId := "human_member1abc"
member2HumanId := "human_member2def"
strangerHumanId := "human_stranger789"
// Test Create
createdNetwork, err := svc.Create(ctx, "Test Network", "[email protected]")
createdNetwork, err := svc.Create(ctx, "Test Network", adminHumanId)
assert.NoError(t, err)
assert.NotEmpty(t, createdNetwork.ID)
assert.Equal(t, "Test Network", createdNetwork.Name)
assert.Equal(t, "[email protected]", createdNetwork.AdminEmail)
assert.Equal(t, adminHumanId, createdNetwork.AdminHumanId)
assert.NotZero(t, createdNetwork.CreatedAt)
// Test GetByID
@@ -38,7 +43,7 @@ func TestNetworkService(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, createdNetwork.ID, foundNetwork.ID)
assert.Equal(t, createdNetwork.Name, foundNetwork.Name)
assert.Equal(t, createdNetwork.AdminEmail, foundNetwork.AdminEmail)
assert.Equal(t, createdNetwork.AdminHumanId, foundNetwork.AdminHumanId)
// Test GetByID with non-existent id
_, err = svc.GetByID(ctx, "network_nonexistent")
@@ -60,45 +65,45 @@ func TestNetworkService(t *testing.T) {
assert.ErrorIs(t, err, network.ErrNotFound)
// Test AddMembers
err = svc.AddMembers(ctx, createdNetwork.ID, []string{"[email protected]", "[email protected]"})
err = svc.AddMembers(ctx, createdNetwork.ID, []string{member1HumanId, member2HumanId})
assert.NoError(t, err)
// Test ListForEmail - should find network for admin
networks, err := svc.ListForEmail(ctx, "[email protected]")
// Test ListForHuman - should find network for admin
networks, err := svc.ListForHuman(ctx, adminHumanId)
assert.NoError(t, err)
assert.Len(t, networks, 1)
assert.Equal(t, createdNetwork.ID, networks[0].ID)
// Test ListForEmail - should find network for member
networks, err = svc.ListForEmail(ctx, "[email protected]")
// Test ListForHuman - should find network for member
networks, err = svc.ListForHuman(ctx, member1HumanId)
assert.NoError(t, err)
assert.Len(t, networks, 1)
assert.Equal(t, createdNetwork.ID, networks[0].ID)
// Test ListForEmail - should return empty for non-member
networks, err = svc.ListForEmail(ctx, "[email protected]")
// Test ListForHuman - should return empty for non-member
networks, err = svc.ListForHuman(ctx, strangerHumanId)
assert.NoError(t, err)
assert.Len(t, networks, 0)
// Test RemoveMember
err = svc.RemoveMember(ctx, createdNetwork.ID, "[email protected]")
err = svc.RemoveMember(ctx, createdNetwork.ID, member1HumanId)
assert.NoError(t, err)
// Verify member was removed
networks, err = svc.ListForEmail(ctx, "[email protected]")
networks, err = svc.ListForHuman(ctx, member1HumanId)
assert.NoError(t, err)
assert.Len(t, networks, 0)
// member2 should still have access
networks, err = svc.ListForEmail(ctx, "[email protected]")
networks, err = svc.ListForHuman(ctx, member2HumanId)
assert.NoError(t, err)
assert.Len(t, networks, 1)
// Create another network and verify ListForEmail returns multiple
network2, err := svc.Create(ctx, "Second Network", "[email protected]")
// Create another network and verify ListForHuman returns multiple
network2, err := svc.Create(ctx, "Second Network", member2HumanId)
assert.NoError(t, err)
networks, err = svc.ListForEmail(ctx, "[email protected]")
networks, err = svc.ListForHuman(ctx, member2HumanId)
assert.NoError(t, err)
assert.Len(t, networks, 2)
@@ -107,3 +112,65 @@ func TestNetworkService(t *testing.T) {
assert.Contains(t, networkIDs, createdNetwork.ID)
assert.Contains(t, networkIDs, network2.ID)
}
func TestNetworkInvitations(t *testing.T) {
ctx := context.Background()
svc := network.NewService(dbPool)
adminHumanId := "human_invtest_admin"
inviteeEmail := "[email protected]"
inviteeHumanId := "human_invitee123"
// Create a network
net, err := svc.Create(ctx, "Invitation Test Network", adminHumanId)
assert.NoError(t, err)
// Invite by email
err = svc.InviteByEmail(ctx, net.ID, []string{inviteeEmail})
assert.NoError(t, err)
// List invitations for email
invitations, err := svc.ListInvitationsForEmail(ctx, inviteeEmail)
assert.NoError(t, err)
assert.Len(t, invitations, 1)
assert.Equal(t, net.ID, invitations[0].NetworkID)
assert.Equal(t, inviteeEmail, invitations[0].Email)
// List invitations for network
invitations, err = svc.ListInvitationsForNetwork(ctx, net.ID)
assert.NoError(t, err)
assert.Len(t, invitations, 1)
// Duplicate invite is idempotent
err = svc.InviteByEmail(ctx, net.ID, []string{inviteeEmail})
assert.NoError(t, err)
invitations, err = svc.ListInvitationsForNetwork(ctx, net.ID)
assert.NoError(t, err)
assert.Len(t, invitations, 1)
// Accept invitation
err = svc.AcceptInvitation(ctx, net.ID, inviteeEmail, inviteeHumanId)
assert.NoError(t, err)
// Invitation should be removed
invitations, err = svc.ListInvitationsForEmail(ctx, inviteeEmail)
assert.NoError(t, err)
assert.Len(t, invitations, 0)
// Human should now be a member
isMember, err := svc.IsMember(ctx, net.ID, inviteeHumanId)
assert.NoError(t, err)
assert.True(t, isMember)
// Test revoke invitation
revokeEmail := "[email protected]"
err = svc.InviteByEmail(ctx, net.ID, []string{revokeEmail})
assert.NoError(t, err)
err = svc.RevokeInvitation(ctx, net.ID, revokeEmail)
assert.NoError(t, err)
invitations, err = svc.ListInvitationsForEmail(ctx, revokeEmail)
assert.NoError(t, err)
assert.Len(t, invitations, 0)
}