package face import ( "context" "fmt" "sync" "time" ) // Client defines the interface for face detection, indexing, and search operations type Client interface { // CreateCollection creates a new face collection for a space CreateCollection(ctx context.Context, collectionName string) error // DeleteCollection deletes a face collection DeleteCollection(ctx context.Context, collectionName string) error // DetectFaces detects faces in an image DetectFaces(ctx context.Context, imageBytes []byte) ([]FaceDetection, error) // IndexFace indexes a face in a collection IndexFace(ctx context.Context, imageBytes []byte, photoID string) (*FaceDetection, error) // SearchFacesByImage searches for similar faces in the collection SearchFacesByImage(ctx context.Context, imageBytes []byte, maxFaces int, threshold float64) ([]FaceMatch, error) } // Compile-time check that RekognitionClient implements Client var _ Client = (*RekognitionClient)(nil) // MockClient is a mock implementation of the Client interface for testing type MockClient struct { mu sync.RWMutex collections map[string]*CollectionInfo faces map[string][]FaceIndex detections []FaceDetection searchResults []FaceMatch // Mock configuration DetectFacesFunc func(ctx context.Context, imageBytes []byte) ([]FaceDetection, error) IndexFaceFunc func(ctx context.Context, imageBytes []byte, photoID string) (*FaceDetection, error) SearchFacesFunc func(ctx context.Context, imageBytes []byte, maxFaces int, threshold float64) ([]FaceMatch, error) } // FaceIndex represents an indexed face (internal to mock) type FaceIndex struct { FaceID string `json:"faceId"` ExternalImageID string `json:"externalImageId"` Confidence float64 `json:"confidence"` BoundingBox BoundingBox `json:"boundingBox"` IndexedAt time.Time `json:"indexedAt"` } // NewMockClient creates a new mock client for testing func NewMockClient() *MockClient { return &MockClient{ collections: make(map[string]*CollectionInfo), faces: make(map[string][]FaceIndex), } } // CreateCollection creates a mock collection func (m *MockClient) CreateCollection(ctx context.Context, collectionName string) error { m.mu.Lock() defer m.mu.Unlock() m.collections[collectionName] = &CollectionInfo{ CollectionID: collectionName, FaceCount: 0, CreationDate: time.Now(), LastUpdated: time.Now(), } return nil } // DeleteCollection deletes a mock collection func (m *MockClient) DeleteCollection(ctx context.Context, collectionName string) error { m.mu.Lock() defer m.mu.Unlock() delete(m.collections, collectionName) delete(m.faces, collectionName) return nil } // DetectFaces returns mock face detections func (m *MockClient) DetectFaces(ctx context.Context, imageBytes []byte) ([]FaceDetection, error) { if m.DetectFacesFunc != nil { return m.DetectFacesFunc(ctx, imageBytes) } m.mu.RLock() defer m.mu.RUnlock() // Return mock detections if len(m.detections) > 0 { return m.detections, nil } // Default: return one face return []FaceDetection{ { Confidence: 99.5, BoundingBox: BoundingBox{ Left: 0.1, Top: 0.1, Width: 0.3, Height: 0.3, }, }, }, nil } // IndexFace indexes a mock face func (m *MockClient) IndexFace(ctx context.Context, imageBytes []byte, photoID string) (*FaceDetection, error) { if m.IndexFaceFunc != nil { return m.IndexFaceFunc(ctx, imageBytes, photoID) } m.mu.Lock() defer m.mu.Unlock() detection := &FaceDetection{ Confidence: 99.5, BoundingBox: BoundingBox{ Left: 0.1, Top: 0.1, Width: 0.3, Height: 0.3, }, } // Store in mock database faceIndex := FaceIndex{ FaceID: fmt.Sprintf("face-%s-%d", photoID, time.Now().UnixNano()), ExternalImageID: photoID, Confidence: 99.5, BoundingBox: detection.BoundingBox, IndexedAt: time.Now(), } m.faces["hatch-default"] = append(m.faces["hatch-default"], faceIndex) return detection, nil } // SearchFacesByImage returns mock search results func (m *MockClient) SearchFacesByImage(ctx context.Context, imageBytes []byte, maxFaces int, threshold float64) ([]FaceMatch, error) { if m.SearchFacesFunc != nil { return m.SearchFacesFunc(ctx, imageBytes, maxFaces, threshold) } m.mu.RLock() defer m.mu.RUnlock() // Return mock matches if len(m.searchResults) > 0 { if maxFaces > 0 && len(m.searchResults) > maxFaces { return m.searchResults[:maxFaces], nil } return m.searchResults, nil } // Default: return one match return []FaceMatch{ { FaceID: "face-mock-1", ExternalImageID: "photo-1", Similarity: 95.0, BoundingBox: BoundingBox{ Left: 0.1, Top: 0.1, Width: 0.3, Height: 0.3, }, }, }, nil } // SetDetections configures mock face detections func (m *MockClient) SetDetections(detections []FaceDetection) { m.mu.Lock() defer m.mu.Unlock() m.detections = detections } // SetSearchResults configures mock search results func (m *MockClient) SetSearchResults(results []FaceMatch) { m.mu.Lock() defer m.mu.Unlock() m.searchResults = results }