refactor: update api and client to reference humanIds
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user