hatch-surf/internal/face/integration_test.go
Chris Anderson 5bce1bd39b
Some checks are pending
CI / go (push) Waiting to run
CI / docker (push) Waiting to run
CI / lint-go (push) Waiting to run
CI / lint-docs (push) Waiting to run
CI / check-paperclip (push) Waiting to run
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 ./...
2026-06-26 05:50:07 +02:00

281 lines
6.9 KiB
Go

//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")
}