package face import ( "context" "testing" "time" ) // MockPhotoRepository is a mock implementation of PhotoRepository for testing type MockPhotoRepository struct { photos map[string]*PhotoInfo embeddings []*FaceEmbeddingInfo } func NewMockPhotoRepository() *MockPhotoRepository { return &MockPhotoRepository{ photos: make(map[string]*PhotoInfo), } } func (m *MockPhotoRepository) GetPhoto(ctx context.Context, id string) (*PhotoInfo, error) { if photo, ok := m.photos[id]; ok { return photo, nil } return &PhotoInfo{ ID: id, SpaceID: "test-space", URL: "https://example.com/photo.jpg", }, nil } func (m *MockPhotoRepository) CreateFaceEmbedding(ctx context.Context, embedding *FaceEmbeddingInfo) (*FaceEmbeddingInfo, error) { m.embeddings = append(m.embeddings, embedding) return embedding, nil } func (m *MockPhotoRepository) AddPhoto(photo *PhotoInfo) { m.photos[photo.ID] = photo } // Test helper functions func setupTestService() (*Service, *MockClient, *MockPhotoRepository, *MemoryCache) { mockClient := NewMockClient() mockRepo := NewMockPhotoRepository() cache := NewMemoryCache() config := DefaultConfig() service := NewService(mockClient, mockRepo, cache, config) return service, mockClient, mockRepo, cache } // Tests func TestService_IndexPhoto_Success(t *testing.T) { service, mockClient, mockRepo, _ := setupTestService() ctx := context.Background() // Test data photoID := "test-photo-1" imageBytes := []byte("test-image-data") // Add photo to mock repo mockRepo.AddPhoto(&PhotoInfo{ ID: photoID, SpaceID: "test-space-1", }) // Execute err := service.IndexPhoto(ctx, photoID, imageBytes) // Assert if err != nil { t.Errorf("Expected no error, got: %v", err) } _ = mockClient // Client was called internally } func TestService_IndexPhoto_AlreadyIndexed(t *testing.T) { service, _, _, cache := setupTestService() ctx := context.Background() // Test data photoID := "test-photo-1" imageBytes := []byte("test-image-data") // Pre-populate cache cache.Set(ctx, "indexed:"+photoID, "true", 24*time.Hour) // Execute err := service.IndexPhoto(ctx, photoID, imageBytes) // Assert if err != nil { t.Errorf("Expected no error, got: %v", err) } } func TestService_SearchFaces_Success(t *testing.T) { service, _, mockRepo, _ := setupTestService() ctx := context.Background() // Test data spaceID := "test-space-1" imageBytes := []byte("test-image-data") limit := 10 // Add photo to mock repo mockRepo.AddPhoto(&PhotoInfo{ ID: "photo-1", SpaceID: spaceID, URL: "https://example.com/photo-1.jpg", }) // Execute result, err := service.SearchFaces(ctx, spaceID, imageBytes, limit) // Assert if err != nil { t.Errorf("Expected no error, got: %v", err) } if result == nil { t.Fatal("Expected result, got nil") } if len(result.Matches) == 0 { t.Error("Expected at least one match") } if result.CacheHit { t.Error("Expected cache hit to be false") } } func TestService_SearchFaces_CacheHit(t *testing.T) { service, _, _, cache := setupTestService() ctx := context.Background() // Test data spaceID := "test-space-1" imageBytes := []byte("test-image-data") limit := 10 // Pre-populate cache with a result cachedResult := &FaceSearchResult{ Matches: []FaceMatch{ { FaceID: "cached-face-1", Similarity: 95.0, }, }, PhotoCount: 1, } cache.SetJSON(ctx, "search:"+spaceID+":"+computeHash(imageBytes), cachedResult, 5*time.Minute) // Execute result, err := service.SearchFaces(ctx, spaceID, imageBytes, limit) // Assert if err != nil { t.Errorf("Expected no error, got: %v", err) } if result == nil { t.Fatal("Expected result, got nil") } if !result.CacheHit { t.Error("Expected cache hit to be true") } if result.PhotoCount != 1 { t.Errorf("Expected 1 photo, got %d", result.PhotoCount) } } func TestService_CreateCollection(t *testing.T) { service, _, _, _ := setupTestService() ctx := context.Background() // Execute err := service.CreateCollection(ctx, "test-collection") // Assert if err != nil { t.Errorf("Expected no error, got: %v", err) } } func TestService_DeleteCollection(t *testing.T) { service, _, _, _ := setupTestService() ctx := context.Background() // Execute err := service.DeleteCollection(ctx, "test-collection") // Assert if err != nil { t.Errorf("Expected no error, got: %v", err) } } func TestConfig_Default(t *testing.T) { config := DefaultConfig() if config.Region != "us-east-1" { t.Errorf("Expected region us-east-1, got %s", config.Region) } if config.CollectionID != "hatch-faces" { t.Errorf("Expected collection ID hatch-faces, got %s", config.CollectionID) } if config.MaxFaces != 10 { t.Errorf("Expected max faces 10, got %d", config.MaxFaces) } if config.Threshold != 80.0 { t.Errorf("Expected threshold 80.0, got %f", config.Threshold) } if config.SearchCacheTTL != 5*time.Minute { t.Errorf("Expected search cache TTL 5m, got %v", config.SearchCacheTTL) } } func TestConfig_Validate(t *testing.T) { // Valid config config := DefaultConfig() if err := config.Validate(); err != nil { t.Errorf("Expected valid config, got error: %v", err) } // Invalid: missing region invalidConfig := DefaultConfig() invalidConfig.Region = "" if err := invalidConfig.Validate(); err == nil { t.Error("Expected error for missing region") } // Invalid: missing collection ID invalidConfig2 := DefaultConfig() invalidConfig2.CollectionID = "" if err := invalidConfig2.Validate(); err == nil { t.Error("Expected error for missing collection ID") } } func TestCacheKeys(t *testing.T) { keys := CacheKeys{} if keys.Search("space1", "hash1") != "search:space1:hash1" { t.Error("Search key mismatch") } if keys.Photo("photo1") != "photo:photo1" { t.Error("Photo key mismatch") } if keys.Face("face1") != "face:face1" { t.Error("Face key mismatch") } if keys.Indexed("photo1") != "indexed:photo1" { t.Error("Indexed key mismatch") } } func TestValidateImage(t *testing.T) { service, _, _, _ := setupTestService() // Test with valid JPEG header (need at least 100 bytes) validJPEG := make([]byte, 200) validJPEG[0] = 0xFF validJPEG[1] = 0xD8 validJPEG[2] = 0xFF validJPEG[3] = 0xE0 if err := service.ValidateImage(validJPEG); err != nil { t.Errorf("Expected no error for valid JPEG, got: %v", err) } // Test with valid PNG header (need at least 100 bytes) validPNG := make([]byte, 200) validPNG[0] = 0x89 validPNG[1] = 0x50 validPNG[2] = 0x4E validPNG[3] = 0x47 if err := service.ValidateImage(validPNG); err != nil { t.Errorf("Expected no error for valid PNG, got: %v", err) } // Test with too large image largeImage := make([]byte, 11*1024*1024) // 11MB if err := service.ValidateImage(largeImage); err != ErrImageTooLarge { t.Errorf("Expected ErrImageTooLarge, got: %v", err) } // Test with too small image smallImage := make([]byte, 10) if err := service.ValidateImage(smallImage); err != ErrImageTooSmall { t.Errorf("Expected ErrImageTooSmall, got: %v", err) } // Test with invalid format (large enough but wrong header) invalidImage := make([]byte, 200) if err := service.ValidateImage(invalidImage); err != ErrInvalidImageFormat { t.Errorf("Expected ErrInvalidImageFormat, got: %v", err) } } func TestComputeHash(t *testing.T) { data1 := []byte("test data for hashing") data2 := []byte("different data for hashing") hash1 := computeHash(data1) hash2 := computeHash(data2) if hash1 == hash2 { t.Error("Expected different hashes for different data") } // Same data should produce same hash hash1Again := computeHash(data1) if hash1 != hash1Again { t.Error("Expected same hash for same data") } }