refactor: unit testable units and cleaner dep injection

This commit is contained in:
Arjun Patel
2026-04-27 16:07:28 -07:00
parent 013c453dd3
commit 48d6d5cb07
26 changed files with 761 additions and 617 deletions
+48 -28
View File
@@ -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)
}