refactor: unit testable units and cleaner dep injection
This commit is contained in:
@@ -4,9 +4,9 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/flowy-live/llink/internal/network"
|
||||
"github.com/flowy-live/llink/internal/utils"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
@@ -58,14 +58,14 @@ type Service interface {
|
||||
}
|
||||
|
||||
type serviceImpl struct {
|
||||
repo repository
|
||||
networkSvc network.Service
|
||||
repo repository
|
||||
networkMembershipChecker NetworkMembershipChecker
|
||||
}
|
||||
|
||||
func NewService(pool *pgxpool.Pool, networkSvc network.Service) Service {
|
||||
func NewService(pool *pgxpool.Pool, networkReader NetworkMembershipChecker) Service {
|
||||
return &serviceImpl{
|
||||
repo: newRepository(pool),
|
||||
networkSvc: networkSvc,
|
||||
repo: newRepository(pool),
|
||||
networkMembershipChecker: networkReader,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,6 +152,7 @@ func (s *serviceImpl) Create(ctx context.Context, input CreateInput, requesterEm
|
||||
}
|
||||
|
||||
// Custom visibility requires at least one member and must be a stream
|
||||
var customMembers []string
|
||||
if input.Visibility == VisibilityCustom {
|
||||
if input.Type != TypeStream {
|
||||
return nil, ErrNotAContainer
|
||||
@@ -159,6 +160,30 @@ func (s *serviceImpl) Create(ctx context.Context, input CreateInput, requesterEm
|
||||
if len(input.Members) == 0 {
|
||||
return nil, ErrMembersRequired
|
||||
}
|
||||
|
||||
// Validate every supplied member against the network checker before touching the DB.
|
||||
customMembers = make([]string, 0, len(input.Members)+1)
|
||||
customMembers = append(customMembers, requesterEmail)
|
||||
seen := map[string]bool{requesterEmail: true}
|
||||
for _, email := range input.Members {
|
||||
normalized, err := utils.NormalizeEmail(email)
|
||||
if err != nil {
|
||||
return nil, errors.Join(ErrInvalidData, err)
|
||||
}
|
||||
if seen[normalized] {
|
||||
continue
|
||||
}
|
||||
seen[normalized] = true
|
||||
|
||||
isMember, err := s.networkMembershipChecker.IsMember(ctx, input.NetworkID, normalized)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !isMember {
|
||||
return nil, fmt.Errorf("%w: %s", ErrInvalidMember, normalized)
|
||||
}
|
||||
customMembers = append(customMembers, normalized)
|
||||
}
|
||||
}
|
||||
|
||||
// Network membership is verified by handler - we only check particle visibility
|
||||
@@ -206,22 +231,9 @@ func (s *serviceImpl) Create(ctx context.Context, input CreateInput, requesterEm
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Add members if custom visibility (only streams for MVP)
|
||||
if input.Visibility == VisibilityCustom && len(input.Members) > 0 {
|
||||
normalizedEmails := make([]string, 0, len(input.Members)+1)
|
||||
// Always include the creator
|
||||
normalizedEmails = append(normalizedEmails, requesterEmail)
|
||||
for _, email := range input.Members {
|
||||
normalized, err := utils.NormalizeEmail(email)
|
||||
if err != nil {
|
||||
continue // Skip invalid emails
|
||||
}
|
||||
if normalized == requesterEmail {
|
||||
continue // Already added
|
||||
}
|
||||
normalizedEmails = append(normalizedEmails, normalized)
|
||||
}
|
||||
if err := s.repo.addMembers(ctx, created.ID, normalizedEmails); err != nil {
|
||||
// Add the pre-validated member list for custom visibility.
|
||||
if len(customMembers) > 0 {
|
||||
if err := s.repo.addMembers(ctx, created.ID, customMembers); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
@@ -614,20 +626,28 @@ func (s *serviceImpl) AddMembers(ctx context.Context, id string, emails []string
|
||||
return ErrNotAContainer
|
||||
}
|
||||
|
||||
// Validate and normalize emails, check network membership
|
||||
// Validate and normalize emails, checking network membership upfront.
|
||||
// Strict: a normalize failure, checker error, or non-member aborts the
|
||||
// whole operation before any rows are written.
|
||||
normalizedEmails := make([]string, 0, len(emails))
|
||||
seen := make(map[string]bool, len(emails))
|
||||
for _, email := range emails {
|
||||
normalized, err := utils.NormalizeEmail(email)
|
||||
if err != nil {
|
||||
return errors.Join(ErrInvalidData, err)
|
||||
}
|
||||
if seen[normalized] {
|
||||
continue
|
||||
}
|
||||
seen[normalized] = true
|
||||
|
||||
// Root stream - check network membership
|
||||
isMember, err := s.networkSvc.IsMember(ctx, p.NetworkID, normalized)
|
||||
if err != nil || !isMember {
|
||||
continue
|
||||
isMember, err := s.networkMembershipChecker.IsMember(ctx, p.NetworkID, normalized)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !isMember {
|
||||
return fmt.Errorf("%w: %s", ErrInvalidMember, normalized)
|
||||
}
|
||||
|
||||
normalizedEmails = append(normalizedEmails, normalized)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user