Phase 2.4: Testing & Deployment
- Wire face service into main.go with FACE_SERVICE_ENABLED toggle - Add integration tests for AWS Rekognition (build tag: integration) - Add performance benchmarks for face operations (build tag: performance) - Fix PhotoRepository interface to use store types directly - Remove duplicate PhotoInfo/FaceEmbeddingInfo types from face package - Add go.mod replace directive for local module resolution Integration tests: go test -tags=integration ./internal/face/... Performance tests: go test -tags=performance -bench=. ./internal/face/... Standard tests: go test ./...
This commit is contained in:
parent
a63d2dbf62
commit
5bce1bd39b
@ -10,6 +10,8 @@ import (
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/config"
|
||||
"github.com/elfoundation/hatch/internal/face"
|
||||
"github.com/elfoundation/hatch/internal/handler"
|
||||
"github.com/elfoundation/hatch/internal/store"
|
||||
"github.com/go-chi/chi/v5"
|
||||
@ -33,7 +35,32 @@ func main() {
|
||||
}
|
||||
defer repo.Close()
|
||||
|
||||
h := handler.New(repo)
|
||||
// Initialize face service if enabled
|
||||
var faceService *face.Service
|
||||
if os.Getenv("FACE_SERVICE_ENABLED") == "true" {
|
||||
cfg, err := config.LoadDefaultConfig(context.Background())
|
||||
if err != nil {
|
||||
log.Printf("warning: failed to load AWS config: %v", err)
|
||||
} else {
|
||||
collectionID := os.Getenv("REKOGNITION_COLLECTION")
|
||||
if collectionID == "" {
|
||||
collectionID = "hatch-faces"
|
||||
}
|
||||
rekognitionClient := face.NewRekognitionClient(cfg, collectionID)
|
||||
faceConfig := face.DefaultConfig()
|
||||
faceCache := face.NewMemoryCache()
|
||||
faceService = face.NewService(rekognitionClient, repo, faceCache, faceConfig)
|
||||
log.Println("face search service initialized")
|
||||
}
|
||||
}
|
||||
|
||||
// Create handler with or without face service
|
||||
var h *handler.Handler
|
||||
if faceService != nil {
|
||||
h = handler.NewWithFaceService(repo, faceService)
|
||||
} else {
|
||||
h = handler.New(repo)
|
||||
}
|
||||
h.Debug = os.Getenv("HATCH_DEBUG") != ""
|
||||
r := chi.NewRouter()
|
||||
|
||||
|
||||
2
go.mod
2
go.mod
@ -44,3 +44,5 @@ require (
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
)
|
||||
|
||||
replace github.com/elfoundation/hatch => .
|
||||
|
||||
281
internal/face/integration_test.go
Normal file
281
internal/face/integration_test.go
Normal file
@ -0,0 +1,281 @@
|
||||
//go:build integration
|
||||
|
||||
package face
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TestIntegration_DetectFaces tests face detection with real AWS Rekognition
|
||||
func TestIntegration_DetectFaces(t *testing.T) {
|
||||
if os.Getenv("AWS_REGION") == "" {
|
||||
t.Skip("AWS_REGION not set, skipping integration test")
|
||||
}
|
||||
|
||||
cfg, err := loadAWSConfig()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to load AWS config: %v", err)
|
||||
}
|
||||
|
||||
collectionID := os.Getenv("REKOGNITION_COLLECTION")
|
||||
if collectionID == "" {
|
||||
collectionID = "hatch-faces-test"
|
||||
}
|
||||
|
||||
client := NewRekognitionClient(cfg, collectionID)
|
||||
ctx := context.Background()
|
||||
|
||||
// Load test image
|
||||
imageBytes, err := loadTestImage("test/fixtures/images/face.jpg")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to load test image: %v", err)
|
||||
}
|
||||
|
||||
// Detect faces
|
||||
detections, err := client.DetectFaces(ctx, imageBytes)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to detect faces: %v", err)
|
||||
}
|
||||
|
||||
if len(detections) == 0 {
|
||||
t.Error("expected at least one face detection")
|
||||
}
|
||||
|
||||
// Verify confidence > 99%
|
||||
for _, detection := range detections {
|
||||
if detection.Confidence < 99.0 {
|
||||
t.Errorf("expected confidence > 99%%, got %f", detection.Confidence)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestIntegration_IndexFace tests face indexing with real AWS Rekognition
|
||||
func TestIntegration_IndexFace(t *testing.T) {
|
||||
if os.Getenv("AWS_REGION") == "" {
|
||||
t.Skip("AWS_REGION not set, skipping integration test")
|
||||
}
|
||||
|
||||
cfg, err := loadAWSConfig()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to load AWS config: %v", err)
|
||||
}
|
||||
|
||||
collectionID := os.Getenv("REKOGNITION_COLLECTION")
|
||||
if collectionID == "" {
|
||||
collectionID = "hatch-faces-test"
|
||||
}
|
||||
|
||||
client := NewRekognitionClient(cfg, collectionID)
|
||||
ctx := context.Background()
|
||||
|
||||
// Create collection
|
||||
if err := client.CreateCollection(ctx, collectionID); err != nil {
|
||||
t.Fatalf("failed to create collection: %v", err)
|
||||
}
|
||||
defer client.DeleteCollection(ctx, collectionID)
|
||||
|
||||
// Load test image
|
||||
imageBytes, err := loadTestImage("test/fixtures/images/face.jpg")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to load test image: %v", err)
|
||||
}
|
||||
|
||||
// Index face
|
||||
photoID := "test-photo-" + time.Now().Format("20060102150405")
|
||||
detection, err := client.IndexFace(ctx, imageBytes, photoID)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to index face: %v", err)
|
||||
}
|
||||
|
||||
if detection == nil {
|
||||
t.Error("expected face detection, got nil")
|
||||
}
|
||||
|
||||
if detection.Confidence < 99.0 {
|
||||
t.Errorf("expected confidence > 99%%, got %f", detection.Confidence)
|
||||
}
|
||||
}
|
||||
|
||||
// TestIntegration_SearchFaces tests face search with real AWS Rekognition
|
||||
func TestIntegration_SearchFaces(t *testing.T) {
|
||||
if os.Getenv("AWS_REGION") == "" {
|
||||
t.Skip("AWS_REGION not set, skipping integration test")
|
||||
}
|
||||
|
||||
cfg, err := loadAWSConfig()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to load AWS config: %v", err)
|
||||
}
|
||||
|
||||
collectionID := os.Getenv("REKOGNITION_COLLECTION")
|
||||
if collectionID == "" {
|
||||
collectionID = "hatch-faces-test"
|
||||
}
|
||||
|
||||
client := NewRekognitionClient(cfg, collectionID)
|
||||
ctx := context.Background()
|
||||
|
||||
// Create collection
|
||||
if err := client.CreateCollection(ctx, collectionID); err != nil {
|
||||
t.Fatalf("failed to create collection: %v", err)
|
||||
}
|
||||
defer client.DeleteCollection(ctx, collectionID)
|
||||
|
||||
// Load and index test image
|
||||
imageBytes, err := loadTestImage("test/fixtures/images/face.jpg")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to load test image: %v", err)
|
||||
}
|
||||
|
||||
photoID := "test-photo-" + time.Now().Format("20060102150405")
|
||||
_, err = client.IndexFace(ctx, imageBytes, photoID)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to index face: %v", err)
|
||||
}
|
||||
|
||||
// Search for the indexed face
|
||||
matches, err := client.SearchFacesByImage(ctx, imageBytes, 10, 80.0)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to search faces: %v", err)
|
||||
}
|
||||
|
||||
if len(matches) == 0 {
|
||||
t.Error("expected at least one match")
|
||||
}
|
||||
|
||||
// Verify similarity > 80%
|
||||
for _, match := range matches {
|
||||
if match.Similarity < 80.0 {
|
||||
t.Errorf("expected similarity > 80%%, got %f", match.Similarity)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestIntegration_EndToEnd tests complete face detection → index → search flow
|
||||
func TestIntegration_EndToEnd(t *testing.T) {
|
||||
if os.Getenv("AWS_REGION") == "" {
|
||||
t.Skip("AWS_REGION not set, skipping integration test")
|
||||
}
|
||||
|
||||
startTime := time.Now()
|
||||
|
||||
cfg, err := loadAWSConfig()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to load AWS config: %v", err)
|
||||
}
|
||||
|
||||
collectionID := os.Getenv("REKOGNITION_COLLECTION")
|
||||
if collectionID == "" {
|
||||
collectionID = "hatch-faces-test"
|
||||
}
|
||||
|
||||
client := NewRekognitionClient(cfg, collectionID)
|
||||
ctx := context.Background()
|
||||
|
||||
// Create collection
|
||||
if err := client.CreateCollection(ctx, collectionID); err != nil {
|
||||
t.Fatalf("failed to create collection: %v", err)
|
||||
}
|
||||
defer client.DeleteCollection(ctx, collectionID)
|
||||
|
||||
// Load test image
|
||||
imageBytes, err := loadTestImage("test/fixtures/images/face.jpg")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to load test image: %v", err)
|
||||
}
|
||||
|
||||
// Step 1: Detect faces
|
||||
detections, err := client.DetectFaces(ctx, imageBytes)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to detect faces: %v", err)
|
||||
}
|
||||
|
||||
if len(detections) == 0 {
|
||||
t.Fatal("no faces detected")
|
||||
}
|
||||
|
||||
// Step 2: Index face
|
||||
photoID := "e2e-test-" + time.Now().Format("20060102150405")
|
||||
_, err = client.IndexFace(ctx, imageBytes, photoID)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to index face: %v", err)
|
||||
}
|
||||
|
||||
// Step 3: Search for the indexed face
|
||||
matches, err := client.SearchFacesByImage(ctx, imageBytes, 10, 80.0)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to search faces: %v", err)
|
||||
}
|
||||
|
||||
if len(matches) == 0 {
|
||||
t.Error("expected at least one match")
|
||||
}
|
||||
|
||||
// Verify performance < 5 seconds
|
||||
elapsed := time.Since(startTime)
|
||||
if elapsed > 5*time.Second {
|
||||
t.Errorf("end-to-end latency exceeded 5 seconds: %v", elapsed)
|
||||
}
|
||||
|
||||
t.Logf("End-to-end latency: %v", elapsed)
|
||||
}
|
||||
|
||||
// TestIntegration_ErrorHandling tests error handling with invalid inputs
|
||||
func TestIntegration_ErrorHandling(t *testing.T) {
|
||||
if os.Getenv("AWS_REGION") == "" {
|
||||
t.Skip("AWS_REGION not set, skipping integration test")
|
||||
}
|
||||
|
||||
cfg, err := loadAWSConfig()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to load AWS config: %v", err)
|
||||
}
|
||||
|
||||
collectionID := os.Getenv("REKOGNITION_COLLECTION")
|
||||
if collectionID == "" {
|
||||
collectionID = "hatch-faces-test"
|
||||
}
|
||||
|
||||
client := NewRekognitionClient(cfg, collectionID)
|
||||
ctx := context.Background()
|
||||
|
||||
// Test with empty image data
|
||||
_, err = client.DetectFaces(ctx, []byte{})
|
||||
if err == nil {
|
||||
t.Error("expected error for empty image data")
|
||||
}
|
||||
|
||||
// Test with invalid image data
|
||||
_, err = client.DetectFaces(ctx, []byte("invalid image data"))
|
||||
if err == nil {
|
||||
t.Error("expected error for invalid image data")
|
||||
}
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
|
||||
func loadAWSConfig() (aws.Config, error) {
|
||||
return config.LoadDefaultConfig(context.Background())
|
||||
}
|
||||
|
||||
func loadTestImage(path string) ([]byte, error) {
|
||||
// Try multiple locations
|
||||
locations := []string{
|
||||
path,
|
||||
"test/fixtures/images/face.jpg",
|
||||
"testdata/face.jpg",
|
||||
"../test/fixtures/images/face.jpg",
|
||||
}
|
||||
|
||||
for _, loc := range locations {
|
||||
data, err := os.ReadFile(loc)
|
||||
if err == nil {
|
||||
return data, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("test image not found in any of the expected locations")
|
||||
}
|
||||
225
internal/face/performance_test.go
Normal file
225
internal/face/performance_test.go
Normal file
@ -0,0 +1,225 @@
|
||||
//go:build performance
|
||||
|
||||
package face
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// BenchmarkFaceDetection benchmarks face detection performance
|
||||
func BenchmarkFaceDetection(b *testing.B) {
|
||||
if os.Getenv("AWS_REGION") == "" {
|
||||
b.Skip("AWS_REGION not set, skipping performance test")
|
||||
}
|
||||
|
||||
cfg, err := loadAWSConfig()
|
||||
if err != nil {
|
||||
b.Fatalf("failed to load AWS config: %v", err)
|
||||
}
|
||||
|
||||
collectionID := os.Getenv("REKOGNITION_COLLECTION")
|
||||
if collectionID == "" {
|
||||
collectionID = "hatch-faces-perf"
|
||||
}
|
||||
|
||||
client := NewRekognitionClient(cfg, collectionID)
|
||||
ctx := context.Background()
|
||||
|
||||
// Load test image
|
||||
imageBytes, err := loadTestImage("test/fixtures/images/face.jpg")
|
||||
if err != nil {
|
||||
b.Fatalf("failed to load test image: %v", err)
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := client.DetectFaces(ctx, imageBytes)
|
||||
if err != nil {
|
||||
b.Fatalf("failed to detect faces: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkFaceIndexing benchmarks face indexing performance
|
||||
func BenchmarkFaceIndexing(b *testing.B) {
|
||||
if os.Getenv("AWS_REGION") == "" {
|
||||
b.Skip("AWS_REGION not set, skipping performance test")
|
||||
}
|
||||
|
||||
cfg, err := loadAWSConfig()
|
||||
if err != nil {
|
||||
b.Fatalf("failed to load AWS config: %v", err)
|
||||
}
|
||||
|
||||
collectionID := os.Getenv("REKOGNITION_COLLECTION")
|
||||
if collectionID == "" {
|
||||
collectionID = "hatch-faces-perf"
|
||||
}
|
||||
|
||||
client := NewRekognitionClient(cfg, collectionID)
|
||||
ctx := context.Background()
|
||||
|
||||
// Create collection
|
||||
if err := client.CreateCollection(ctx, collectionID); err != nil {
|
||||
b.Fatalf("failed to create collection: %v", err)
|
||||
}
|
||||
defer client.DeleteCollection(ctx, collectionID)
|
||||
|
||||
// Load test image
|
||||
imageBytes, err := loadTestImage("test/fixtures/images/face.jpg")
|
||||
if err != nil {
|
||||
b.Fatalf("failed to load test image: %v", err)
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
photoID := fmt.Sprintf("perf-test-%d", time.Now().UnixNano())
|
||||
_, err := client.IndexFace(ctx, imageBytes, photoID)
|
||||
if err != nil {
|
||||
b.Fatalf("failed to index face: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkFaceSearch benchmarks face search performance
|
||||
func BenchmarkFaceSearch(b *testing.B) {
|
||||
if os.Getenv("AWS_REGION") == "" {
|
||||
b.Skip("AWS_REGION not set, skipping performance test")
|
||||
}
|
||||
|
||||
cfg, err := loadAWSConfig()
|
||||
if err != nil {
|
||||
b.Fatalf("failed to load AWS config: %v", err)
|
||||
}
|
||||
|
||||
collectionID := os.Getenv("REKOGNITION_COLLECTION")
|
||||
if collectionID == "" {
|
||||
collectionID = "hatch-faces-perf"
|
||||
}
|
||||
|
||||
client := NewRekognitionClient(cfg, collectionID)
|
||||
ctx := context.Background()
|
||||
|
||||
// Create collection
|
||||
if err := client.CreateCollection(ctx, collectionID); err != nil {
|
||||
b.Fatalf("failed to create collection: %v", err)
|
||||
}
|
||||
defer client.DeleteCollection(ctx, collectionID)
|
||||
|
||||
// Load and index test image
|
||||
imageBytes, err := loadTestImage("test/fixtures/images/face.jpg")
|
||||
if err != nil {
|
||||
b.Fatalf("failed to load test image: %v", err)
|
||||
}
|
||||
|
||||
photoID := "perf-search-" + time.Now().Format("20060102150405")
|
||||
_, err = client.IndexFace(ctx, imageBytes, photoID)
|
||||
if err != nil {
|
||||
b.Fatalf("failed to index face: %v", err)
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := client.SearchFacesByImage(ctx, imageBytes, 10, 80.0)
|
||||
if err != nil {
|
||||
b.Fatalf("failed to search faces: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestPerformanceTargets validates performance targets
|
||||
func TestPerformanceTargets(t *testing.T) {
|
||||
if os.Getenv("AWS_REGION") == "" {
|
||||
t.Skip("AWS_REGION not set, skipping performance test")
|
||||
}
|
||||
|
||||
cfg, err := loadAWSConfig()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to load AWS config: %v", err)
|
||||
}
|
||||
|
||||
collectionID := os.Getenv("REKOGNITION_COLLECTION")
|
||||
if collectionID == "" {
|
||||
collectionID = "hatch-faces-perf"
|
||||
}
|
||||
|
||||
client := NewRekognitionClient(cfg, collectionID)
|
||||
ctx := context.Background()
|
||||
|
||||
// Create collection
|
||||
if err := client.CreateCollection(ctx, collectionID); err != nil {
|
||||
t.Fatalf("failed to create collection: %v", err)
|
||||
}
|
||||
defer client.DeleteCollection(ctx, collectionID)
|
||||
|
||||
// Load test image
|
||||
imageBytes, err := loadTestImage("test/fixtures/images/face.jpg")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to load test image: %v", err)
|
||||
}
|
||||
|
||||
// Test face detection latency
|
||||
start := time.Now()
|
||||
_, err = client.DetectFaces(ctx, imageBytes)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to detect faces: %v", err)
|
||||
}
|
||||
detectionLatency := time.Since(start)
|
||||
if detectionLatency > 2*time.Second {
|
||||
t.Errorf("face detection latency exceeded 2 seconds: %v", detectionLatency)
|
||||
}
|
||||
t.Logf("Face detection latency: %v", detectionLatency)
|
||||
|
||||
// Test face indexing latency
|
||||
start = time.Now()
|
||||
photoID := "perf-target-" + time.Now().Format("20060102150405")
|
||||
_, err = client.IndexFace(ctx, imageBytes, photoID)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to index face: %v", err)
|
||||
}
|
||||
indexingLatency := time.Since(start)
|
||||
if indexingLatency > 3*time.Second {
|
||||
t.Errorf("face indexing latency exceeded 3 seconds: %v", indexingLatency)
|
||||
}
|
||||
t.Logf("Face indexing latency: %v", indexingLatency)
|
||||
|
||||
// Test face search latency
|
||||
start = time.Now()
|
||||
_, err = client.SearchFacesByImage(ctx, imageBytes, 10, 80.0)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to search faces: %v", err)
|
||||
}
|
||||
searchLatency := time.Since(start)
|
||||
if searchLatency > 2*time.Second {
|
||||
t.Errorf("face search latency exceeded 2 seconds: %v", searchLatency)
|
||||
}
|
||||
t.Logf("Face search latency: %v", searchLatency)
|
||||
|
||||
// Test end-to-end latency
|
||||
start = time.Now()
|
||||
// Detect
|
||||
_, err = client.DetectFaces(ctx, imageBytes)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to detect faces: %v", err)
|
||||
}
|
||||
// Index
|
||||
photoID = "perf-e2e-" + time.Now().Format("20060102150405")
|
||||
_, err = client.IndexFace(ctx, imageBytes, photoID)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to index face: %v", err)
|
||||
}
|
||||
// Search
|
||||
_, err = client.SearchFacesByImage(ctx, imageBytes, 10, 80.0)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to search faces: %v", err)
|
||||
}
|
||||
e2eLatency := time.Since(start)
|
||||
if e2eLatency > 5*time.Second {
|
||||
t.Errorf("end-to-end latency exceeded 5 seconds: %v", e2eLatency)
|
||||
}
|
||||
t.Logf("End-to-end latency: %v", e2eLatency)
|
||||
}
|
||||
@ -6,31 +6,14 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/elfoundation/hatch/internal/store"
|
||||
)
|
||||
|
||||
// PhotoRepository defines the photo operations needed by the face service
|
||||
type PhotoRepository interface {
|
||||
GetPhoto(ctx context.Context, id string) (*PhotoInfo, error)
|
||||
CreateFaceEmbedding(ctx context.Context, embedding *FaceEmbeddingInfo) (*FaceEmbeddingInfo, error)
|
||||
}
|
||||
|
||||
// PhotoInfo represents photo metadata needed by the face service
|
||||
type PhotoInfo struct {
|
||||
ID string `json:"id"`
|
||||
SpaceID string `json:"spaceId"`
|
||||
URL string `json:"url"`
|
||||
ThumbnailURL string `json:"thumbnailUrl"`
|
||||
FaceCount int `json:"faceCount"`
|
||||
}
|
||||
|
||||
// FaceEmbeddingInfo represents a face embedding record
|
||||
type FaceEmbeddingInfo struct {
|
||||
ID string `json:"id"`
|
||||
PhotoID string `json:"photoId"`
|
||||
SpaceID string `json:"spaceId"`
|
||||
Embedding []float32 `json:"embedding"`
|
||||
BoundingBox string `json:"boundingBox"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
GetPhoto(ctx context.Context, id string) (*store.Photo, error)
|
||||
CreateFaceEmbedding(ctx context.Context, embedding *store.FaceEmbedding) (*store.FaceEmbedding, error)
|
||||
}
|
||||
|
||||
// Service provides face detection, indexing, and search operations
|
||||
@ -119,7 +102,7 @@ func (s *Service) IndexPhoto(ctx context.Context, photoID string, imageBytes []b
|
||||
}
|
||||
|
||||
// Store in database
|
||||
embedding := &FaceEmbeddingInfo{
|
||||
embedding := &store.FaceEmbedding{
|
||||
PhotoID: photoID,
|
||||
SpaceID: photo.SpaceID,
|
||||
Embedding: []float32{}, // Rekognition handles embeddings internally
|
||||
|
||||
@ -4,37 +4,39 @@ import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/elfoundation/hatch/internal/store"
|
||||
)
|
||||
|
||||
// MockPhotoRepository is a mock implementation of PhotoRepository for testing
|
||||
type MockPhotoRepository struct {
|
||||
photos map[string]*PhotoInfo
|
||||
embeddings []*FaceEmbeddingInfo
|
||||
photos map[string]*store.Photo
|
||||
embeddings []*store.FaceEmbedding
|
||||
}
|
||||
|
||||
func NewMockPhotoRepository() *MockPhotoRepository {
|
||||
return &MockPhotoRepository{
|
||||
photos: make(map[string]*PhotoInfo),
|
||||
photos: make(map[string]*store.Photo),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MockPhotoRepository) GetPhoto(ctx context.Context, id string) (*PhotoInfo, error) {
|
||||
func (m *MockPhotoRepository) GetPhoto(ctx context.Context, id string) (*store.Photo, error) {
|
||||
if photo, ok := m.photos[id]; ok {
|
||||
return photo, nil
|
||||
}
|
||||
return &PhotoInfo{
|
||||
return &store.Photo{
|
||||
ID: id,
|
||||
SpaceID: "test-space",
|
||||
URL: "https://example.com/photo.jpg",
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *MockPhotoRepository) CreateFaceEmbedding(ctx context.Context, embedding *FaceEmbeddingInfo) (*FaceEmbeddingInfo, error) {
|
||||
func (m *MockPhotoRepository) CreateFaceEmbedding(ctx context.Context, embedding *store.FaceEmbedding) (*store.FaceEmbedding, error) {
|
||||
m.embeddings = append(m.embeddings, embedding)
|
||||
return embedding, nil
|
||||
}
|
||||
|
||||
func (m *MockPhotoRepository) AddPhoto(photo *PhotoInfo) {
|
||||
func (m *MockPhotoRepository) AddPhoto(photo *store.Photo) {
|
||||
m.photos[photo.ID] = photo
|
||||
}
|
||||
|
||||
@ -61,7 +63,7 @@ func TestService_IndexPhoto_Success(t *testing.T) {
|
||||
imageBytes := []byte("test-image-data")
|
||||
|
||||
// Add photo to mock repo
|
||||
mockRepo.AddPhoto(&PhotoInfo{
|
||||
mockRepo.AddPhoto(&store.Photo{
|
||||
ID: photoID,
|
||||
SpaceID: "test-space-1",
|
||||
})
|
||||
@ -106,7 +108,7 @@ func TestService_SearchFaces_Success(t *testing.T) {
|
||||
limit := 10
|
||||
|
||||
// Add photo to mock repo
|
||||
mockRepo.AddPhoto(&PhotoInfo{
|
||||
mockRepo.AddPhoto(&store.Photo{
|
||||
ID: "photo-1",
|
||||
SpaceID: spaceID,
|
||||
URL: "https://example.com/photo-1.jpg",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user