// Copyright 2025 Cloudbase Solutions SRL // // Licensed under the Apache License, Version 2.0 (the "License"); you may // not use this file except in compliance with the License. You may obtain // a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the // License for the specific language governing permissions and limitations // under the License. package sql import ( "bytes" "context" "crypto/sha256" "encoding/hex" "fmt" "io" "testing" "github.com/stretchr/testify/suite" runnerErrors "github.com/cloudbase/garm-provider-common/errors" dbCommon "github.com/cloudbase/garm/database/common" "github.com/cloudbase/garm/database/watcher" garmTesting "github.com/cloudbase/garm/internal/testing" "github.com/cloudbase/garm/params" ) type FileStoreTestFixtures struct { FileObjects []params.FileObject } type FileStoreTestSuite struct { suite.Suite Store dbCommon.Store ctx context.Context adminCtx context.Context Fixtures *FileStoreTestFixtures } func (s *FileStoreTestSuite) TearDownTest() { watcher.CloseWatcher() } func (s *FileStoreTestSuite) SetupTest() { ctx := context.Background() watcher.InitWatcher(ctx) db, err := NewSQLDatabase(context.Background(), garmTesting.GetTestSqliteDBConfig(s.T())) if err != nil { s.FailNow(fmt.Sprintf("failed to create db connection: %s", err)) } s.Store = db adminCtx := garmTesting.ImpersonateAdminContext(context.Background(), db, s.T()) s.adminCtx = adminCtx s.ctx = adminCtx // Create test file objects fileObjects := []params.FileObject{} // File 1: Small text file with tags content1 := []byte("Hello, World! This is test file 1.") param := params.CreateFileObjectParams{ Name: "test-file-1.txt", Size: int64(len(content1)), Tags: []string{"test", "text"}, } fileObj1, err := s.Store.CreateFileObject(s.ctx, param, bytes.NewReader(content1)) if err != nil { s.FailNow(fmt.Sprintf("failed to create test file 1: %s", err)) } fileObjects = append(fileObjects, fileObj1) // File 2: Binary-like content with different tags content2 := []byte{0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00} // PNG header-like param = params.CreateFileObjectParams{ Name: "test-image.png", Size: int64(len(content2)), Tags: []string{"image", "binary"}, } fileObj2, err := s.Store.CreateFileObject(s.ctx, param, bytes.NewReader(content2)) if err != nil { s.FailNow(fmt.Sprintf("failed to create test file 2: %s", err)) } fileObjects = append(fileObjects, fileObj2) // File 3: No tags content3 := []byte("File without tags.") param = params.CreateFileObjectParams{ Name: "no-tags.txt", Size: int64(len(content3)), Tags: []string{}, } fileObj3, err := s.Store.CreateFileObject(s.ctx, param, bytes.NewReader(content3)) if err != nil { s.FailNow(fmt.Sprintf("failed to create test file 3: %s", err)) } fileObjects = append(fileObjects, fileObj3) s.Fixtures = &FileStoreTestFixtures{ FileObjects: fileObjects, } } func (s *FileStoreTestSuite) TestCreateFileObject() { content := []byte("New test file content") tags := []string{"new", "test"} param := params.CreateFileObjectParams{ Name: "new-file.txt", Size: int64(len(content)), Tags: tags, } fileObj, err := s.Store.CreateFileObject(s.ctx, param, bytes.NewReader(content)) s.Require().Nil(err) s.Require().NotZero(fileObj.ID) s.Require().Equal("new-file.txt", fileObj.Name) s.Require().Equal(int64(len(content)), fileObj.Size) s.Require().ElementsMatch(tags, fileObj.Tags) s.Require().NotEmpty(fileObj.SHA256) s.Require().NotEmpty(fileObj.FileType) // Verify SHA256 is correct expectedHash := sha256.Sum256(content) expectedHashStr := hex.EncodeToString(expectedHash[:]) s.Require().Equal(expectedHashStr, fileObj.SHA256) } func (s *FileStoreTestSuite) TestCreateFileObjectEmpty() { content := []byte{} param := params.CreateFileObjectParams{ Name: "empty-file.txt", Size: 0, Tags: []string{}, } fileObj, err := s.Store.CreateFileObject(s.ctx, param, bytes.NewReader(content)) s.Require().Nil(err) s.Require().NotZero(fileObj.ID) s.Require().Equal("empty-file.txt", fileObj.Name) s.Require().Equal(int64(0), fileObj.Size) } func (s *FileStoreTestSuite) TestGetFileObject() { fileObj, err := s.Store.GetFileObject(s.ctx, s.Fixtures.FileObjects[0].ID) s.Require().Nil(err) s.Require().Equal(s.Fixtures.FileObjects[0].ID, fileObj.ID) s.Require().Equal(s.Fixtures.FileObjects[0].Name, fileObj.Name) s.Require().Equal(s.Fixtures.FileObjects[0].Size, fileObj.Size) s.Require().Equal(s.Fixtures.FileObjects[0].SHA256, fileObj.SHA256) s.Require().ElementsMatch(s.Fixtures.FileObjects[0].Tags, fileObj.Tags) } func (s *FileStoreTestSuite) TestGetFileObjectNotFound() { _, err := s.Store.GetFileObject(s.ctx, 99999) s.Require().NotNil(err) s.Require().ErrorIs(err, runnerErrors.ErrNotFound) } func (s *FileStoreTestSuite) TestOpenFileObjectContent() { // Create a file with known content content := []byte("Test content for reading") param := params.CreateFileObjectParams{ Name: "read-test.txt", Size: int64(len(content)), Tags: []string{"read"}, } fileObj, err := s.Store.CreateFileObject(s.ctx, param, bytes.NewReader(content)) s.Require().Nil(err) // Open and read the content reader, err := s.Store.OpenFileObjectContent(s.ctx, fileObj.ID) s.Require().Nil(err) s.Require().NotNil(reader) defer reader.Close() readContent, err := io.ReadAll(reader) s.Require().Nil(err) s.Require().Equal(content, readContent) } func (s *FileStoreTestSuite) TestOpenFileObjectContentNotFound() { _, err := s.Store.OpenFileObjectContent(s.ctx, 99999) s.Require().NotNil(err) } func (s *FileStoreTestSuite) TestListFileObjects() { result, err := s.Store.ListFileObjects(s.ctx, 1, 10) s.Require().Nil(err) s.Require().GreaterOrEqual(len(result.Results), len(s.Fixtures.FileObjects)) s.Require().Equal(uint64(1), result.CurrentPage) s.Require().GreaterOrEqual(result.Pages, uint64(1)) s.Require().GreaterOrEqual(result.TotalCount, uint64(len(s.Fixtures.FileObjects))) // First page should not have previous page s.Require().Nil(result.PreviousPage) // If there are more pages, next page should be set if result.Pages > 1 { s.Require().NotNil(result.NextPage) s.Require().Equal(uint64(2), *result.NextPage) } } func (s *FileStoreTestSuite) TestListFileObjectsPagination() { // Create more files to test pagination for i := 0; i < 5; i++ { content := []byte(fmt.Sprintf("File %d", i)) param := params.CreateFileObjectParams{ Name: fmt.Sprintf("page-test-%d.txt", i), Size: int64(len(content)), Tags: []string{"pagination"}, } _, err := s.Store.CreateFileObject(s.ctx, param, bytes.NewReader(content)) s.Require().Nil(err) } // Test first page with page size of 2 page1, err := s.Store.ListFileObjects(s.ctx, 1, 2) s.Require().Nil(err) s.Require().Equal(2, len(page1.Results)) s.Require().Equal(uint64(1), page1.CurrentPage) s.Require().GreaterOrEqual(page1.TotalCount, uint64(5)) s.Require().Nil(page1.PreviousPage, "First page should not have previous page") s.Require().NotNil(page1.NextPage, "First page should have next page") s.Require().Equal(uint64(2), *page1.NextPage) // Test second page page2, err := s.Store.ListFileObjects(s.ctx, 2, 2) s.Require().Nil(err) s.Require().Equal(2, len(page2.Results)) s.Require().Equal(uint64(2), page2.CurrentPage) s.Require().Equal(page1.Pages, page2.Pages) s.Require().Equal(page1.TotalCount, page2.TotalCount) s.Require().NotNil(page2.PreviousPage, "Second page should have previous page") s.Require().Equal(uint64(1), *page2.PreviousPage) s.Require().NotNil(page2.NextPage, "Second page should have next page") s.Require().Equal(uint64(3), *page2.NextPage) // Verify different results on different pages if len(page1.Results) > 0 && len(page2.Results) > 0 { page1File := page1.Results[0] page2File := page2.Results[0] s.Require().NotEqual(page1File.ID, page2File.ID) } } func (s *FileStoreTestSuite) TestListFileObjectsDefaultPagination() { // Test default values (page 0 should become 1, pageSize 0 should become 20) result, err := s.Store.ListFileObjects(s.ctx, 0, 0) s.Require().Nil(err) s.Require().Equal(uint64(1), result.CurrentPage) s.Require().LessOrEqual(len(result.Results), 20) } func (s *FileStoreTestSuite) TestUpdateFileObjectName() { newName := "updated-name.txt" updated, err := s.Store.UpdateFileObject(s.ctx, s.Fixtures.FileObjects[0].ID, params.UpdateFileObjectParams{ Name: &newName, }) s.Require().Nil(err) s.Require().Equal(newName, updated.Name) s.Require().Equal(s.Fixtures.FileObjects[0].ID, updated.ID) s.Require().Equal(s.Fixtures.FileObjects[0].Size, updated.Size) // Size should not change s.Require().Equal(s.Fixtures.FileObjects[0].SHA256, updated.SHA256) // SHA256 should not change // Verify the change persists retrieved, err := s.Store.GetFileObject(s.ctx, s.Fixtures.FileObjects[0].ID) s.Require().Nil(err) s.Require().Equal(newName, retrieved.Name) } func (s *FileStoreTestSuite) TestUpdateFileObjectTags() { newTags := []string{"updated", "tags", "here"} updated, err := s.Store.UpdateFileObject(s.ctx, s.Fixtures.FileObjects[0].ID, params.UpdateFileObjectParams{ Tags: newTags, }) s.Require().Nil(err) s.Require().ElementsMatch(newTags, updated.Tags) s.Require().Equal(s.Fixtures.FileObjects[0].Name, updated.Name) // Name should not change // Verify the change persists retrieved, err := s.Store.GetFileObject(s.ctx, s.Fixtures.FileObjects[0].ID) s.Require().Nil(err) s.Require().ElementsMatch(newTags, retrieved.Tags) } func (s *FileStoreTestSuite) TestUpdateFileObjectNameAndTags() { newName := "completely-updated.txt" newTags := []string{"both", "changed"} updated, err := s.Store.UpdateFileObject(s.ctx, s.Fixtures.FileObjects[0].ID, params.UpdateFileObjectParams{ Name: &newName, Tags: newTags, }) s.Require().Nil(err) s.Require().Equal(newName, updated.Name) s.Require().ElementsMatch(newTags, updated.Tags) } func (s *FileStoreTestSuite) TestUpdateFileObjectEmptyTags() { // Test clearing all tags emptyTags := []string{} updated, err := s.Store.UpdateFileObject(s.ctx, s.Fixtures.FileObjects[0].ID, params.UpdateFileObjectParams{ Tags: emptyTags, }) s.Require().Nil(err) s.Require().Empty(updated.Tags) // Verify the change persists retrieved, err := s.Store.GetFileObject(s.ctx, s.Fixtures.FileObjects[0].ID) s.Require().Nil(err) s.Require().Empty(retrieved.Tags) } func (s *FileStoreTestSuite) TestUpdateFileObjectNoChanges() { // Update with no changes updated, err := s.Store.UpdateFileObject(s.ctx, s.Fixtures.FileObjects[0].ID, params.UpdateFileObjectParams{}) s.Require().Nil(err) s.Require().Equal(s.Fixtures.FileObjects[0].Name, updated.Name) s.Require().ElementsMatch(s.Fixtures.FileObjects[0].Tags, updated.Tags) } func (s *FileStoreTestSuite) TestUpdateFileObjectNotFound() { newName := "does-not-exist.txt" _, err := s.Store.UpdateFileObject(s.ctx, 99999, params.UpdateFileObjectParams{ Name: &newName, }) s.Require().NotNil(err) s.Require().ErrorIs(err, runnerErrors.ErrNotFound) } func (s *FileStoreTestSuite) TestUpdateFileObjectEmptyName() { emptyName := "" _, err := s.Store.UpdateFileObject(s.ctx, s.Fixtures.FileObjects[0].ID, params.UpdateFileObjectParams{ Name: &emptyName, }) s.Require().NotNil(err) s.Require().Contains(err.Error(), "name cannot be empty") } func (s *FileStoreTestSuite) TestDeleteFileObject() { // Create a file to delete content := []byte("To be deleted") param := params.CreateFileObjectParams{ Name: "delete-me.txt", Size: int64(len(content)), Tags: []string{"delete"}, } fileObj, err := s.Store.CreateFileObject(s.ctx, param, bytes.NewReader(content)) s.Require().Nil(err) // Delete the file err = s.Store.DeleteFileObject(s.ctx, fileObj.ID) s.Require().Nil(err) // Verify it's deleted _, err = s.Store.GetFileObject(s.ctx, fileObj.ID) s.Require().NotNil(err) s.Require().ErrorIs(err, runnerErrors.ErrNotFound) } func (s *FileStoreTestSuite) TestDeleteFileObjectNotFound() { // Deleting non-existent file should not error err := s.Store.DeleteFileObject(s.ctx, 99999) s.Require().Nil(err) } func (s *FileStoreTestSuite) TestCreateFileObjectLargeContent() { // Test with larger content (1MB) size := 1024 * 1024 content := make([]byte, size) for i := range content { content[i] = byte(i % 256) } param := params.CreateFileObjectParams{ Name: "large-file.bin", Size: int64(size), Tags: []string{"large", "binary"}, } fileObj, err := s.Store.CreateFileObject(s.ctx, param, bytes.NewReader(content)) s.Require().Nil(err) s.Require().Equal(int64(size), fileObj.Size) // Verify we can read it back reader, err := s.Store.OpenFileObjectContent(s.ctx, fileObj.ID) s.Require().Nil(err) defer reader.Close() readContent, err := io.ReadAll(reader) s.Require().Nil(err) s.Require().Equal(content, readContent) } func (s *FileStoreTestSuite) TestFileObjectImmutableFields() { // Create a file content := []byte("Immutable test content") param := params.CreateFileObjectParams{ Name: "immutable-test.txt", Size: int64(len(content)), Tags: []string{"original"}, } fileObj, err := s.Store.CreateFileObject(s.ctx, param, bytes.NewReader(content)) s.Require().Nil(err) originalSize := fileObj.Size originalSHA256 := fileObj.SHA256 originalFileType := fileObj.FileType // Update name and tags newName := "updated-immutable-test.txt" updated, err := s.Store.UpdateFileObject(s.ctx, fileObj.ID, params.UpdateFileObjectParams{ Name: &newName, Tags: []string{"updated"}, }) s.Require().Nil(err) // Verify immutable fields haven't changed s.Require().Equal(originalSize, updated.Size) s.Require().Equal(originalSHA256, updated.SHA256) s.Require().Equal(originalFileType, updated.FileType) // Verify content hasn't changed reader, err := s.Store.OpenFileObjectContent(s.ctx, fileObj.ID) s.Require().Nil(err) defer reader.Close() readContent, err := io.ReadAll(reader) s.Require().Nil(err) s.Require().Equal(content, readContent) } func (s *FileStoreTestSuite) TestSearchFileObjectByTags() { // Create files with specific tags for searching content1 := []byte("File with tag1 and tag2") param := params.CreateFileObjectParams{ Name: "search-file-1.txt", Size: int64(len(content1)), Tags: []string{"tag1", "tag2"}, } file1, err := s.Store.CreateFileObject(s.ctx, param, bytes.NewReader(content1)) s.Require().Nil(err) content2 := []byte("File with tag1, tag2, and tag3") param = params.CreateFileObjectParams{ Name: "search-file-2.txt", Size: int64(len(content2)), Tags: []string{"tag1", "tag2", "tag3"}, } file2, err := s.Store.CreateFileObject(s.ctx, param, bytes.NewReader(content2)) s.Require().Nil(err) content3 := []byte("File with only tag1") param = params.CreateFileObjectParams{ Name: "search-file-3.txt", Size: int64(len(content3)), Tags: []string{"tag1"}, } file3, err := s.Store.CreateFileObject(s.ctx, param, bytes.NewReader(content3)) s.Require().Nil(err) content4 := []byte("File with tag3 only") param = params.CreateFileObjectParams{ Name: "search-file-4.txt", Size: int64(len(content4)), Tags: []string{"tag3"}, } _, err = s.Store.CreateFileObject(s.ctx, param, bytes.NewReader(content4)) s.Require().Nil(err) // Search for files with tag1 - should return 3 files result, err := s.Store.SearchFileObjectByTags(s.ctx, []string{"tag1"}, 1, 10) s.Require().Nil(err) s.Require().GreaterOrEqual(len(result.Results), 3) // Verify the expected files are in the results foundIDs := make(map[uint]bool) for _, fileObj := range result.Results { foundIDs[fileObj.ID] = true } s.Require().True(foundIDs[file1.ID], "file1 should be in results") s.Require().True(foundIDs[file2.ID], "file2 should be in results") s.Require().True(foundIDs[file3.ID], "file3 should be in results") } func (s *FileStoreTestSuite) TestSearchFileObjectByTagsMultipleTags() { // Create files with various tag combinations content1 := []byte("File with search1 and search2") param := params.CreateFileObjectParams{ Name: "multi-search-1.txt", Size: int64(len(content1)), Tags: []string{"search1", "search2"}, } file1, err := s.Store.CreateFileObject(s.ctx, param, bytes.NewReader(content1)) s.Require().Nil(err) content2 := []byte("File with search1, search2, and search3") param = params.CreateFileObjectParams{ Name: "multi-search-2.txt", Size: int64(len(content2)), Tags: []string{"search1", "search2", "search3"}, } file2, err := s.Store.CreateFileObject(s.ctx, param, bytes.NewReader(content2)) s.Require().Nil(err) content3 := []byte("File with only search1") param = params.CreateFileObjectParams{ Name: "multi-search-3.txt", Size: int64(len(content3)), Tags: []string{"search1"}, } _, err = s.Store.CreateFileObject(s.ctx, param, bytes.NewReader(content3)) s.Require().Nil(err) // Search for files with both search1 AND search2 - should return only 2 files result, err := s.Store.SearchFileObjectByTags(s.ctx, []string{"search1", "search2"}, 1, 10) s.Require().Nil(err) s.Require().Equal(2, len(result.Results)) // Verify the correct files are returned foundIDs := make(map[uint]bool) for _, fileObj := range result.Results { foundIDs[fileObj.ID] = true } s.Require().True(foundIDs[file1.ID], "file1 should be in results") s.Require().True(foundIDs[file2.ID], "file2 should be in results") } func (s *FileStoreTestSuite) TestSearchFileObjectByTagsNoResults() { // Search for a tag that doesn't exist result, err := s.Store.SearchFileObjectByTags(s.ctx, []string{"nonexistent-tag"}, 1, 10) s.Require().Nil(err) s.Require().Equal(0, len(result.Results)) s.Require().Equal(uint64(0), result.Pages) s.Require().Equal(uint64(1), result.CurrentPage) } func (s *FileStoreTestSuite) TestSearchFileObjectByTagsEmptyTags() { // Search with empty tag list - should return all files result, err := s.Store.SearchFileObjectByTags(s.ctx, []string{}, 1, 100) s.Require().Nil(err) // Should return all files (fixtures + any created in other tests) s.Require().GreaterOrEqual(len(result.Results), len(s.Fixtures.FileObjects)) } func (s *FileStoreTestSuite) TestSearchFileObjectByTagsPagination() { // Create multiple files with the same tag for i := 0; i < 5; i++ { content := []byte(fmt.Sprintf("Pagination test file %d", i)) param := params.CreateFileObjectParams{ Name: fmt.Sprintf("page-search-%d.txt", i), Size: int64(len(content)), Tags: []string{"pagination-test"}, } _, err := s.Store.CreateFileObject(s.ctx, param, bytes.NewReader(content)) s.Require().Nil(err) } // Test first page with page size of 2 page1, err := s.Store.SearchFileObjectByTags(s.ctx, []string{"pagination-test"}, 1, 2) s.Require().Nil(err) s.Require().Equal(2, len(page1.Results)) s.Require().Equal(uint64(1), page1.CurrentPage) s.Require().GreaterOrEqual(page1.Pages, uint64(3)) // At least 3 pages for 5 files // Test second page page2, err := s.Store.SearchFileObjectByTags(s.ctx, []string{"pagination-test"}, 2, 2) s.Require().Nil(err) s.Require().Equal(2, len(page2.Results)) s.Require().Equal(uint64(2), page2.CurrentPage) // Verify different results on different pages if len(page1.Results) > 0 && len(page2.Results) > 0 { page1File := page1.Results[0] page2File := page2.Results[0] s.Require().NotEqual(page1File.ID, page2File.ID) } } func (s *FileStoreTestSuite) TestSearchFileObjectByTagsDefaultPagination() { // Create a file with a unique tag content := []byte("Default pagination test") param := params.CreateFileObjectParams{ Name: "default-page-search.txt", Size: int64(len(content)), Tags: []string{"default-pagination"}, } _, err := s.Store.CreateFileObject(s.ctx, param, bytes.NewReader(content)) s.Require().Nil(err) // Test default values (page 0 should become 1, pageSize 0 should become 20) result, err := s.Store.SearchFileObjectByTags(s.ctx, []string{"default-pagination"}, 0, 0) s.Require().Nil(err) s.Require().Equal(uint64(1), result.CurrentPage) s.Require().LessOrEqual(len(result.Results), 20) s.Require().GreaterOrEqual(len(result.Results), 1) } func (s *FileStoreTestSuite) TestSearchFileObjectByTagsAllTagsRequired() { // Test that search requires ALL specified tags (AND logic, not OR) content1 := []byte("Has A and B") param := params.CreateFileObjectParams{ Name: "and-test-1.txt", Size: int64(len(content1)), Tags: []string{"tagA", "tagB"}, } file1, err := s.Store.CreateFileObject(s.ctx, param, bytes.NewReader(content1)) s.Require().Nil(err) content2 := []byte("Has A, B, and C") param = params.CreateFileObjectParams{ Name: "and-test-2.txt", Size: int64(len(content2)), Tags: []string{"tagA", "tagB", "tagC"}, } file2, err := s.Store.CreateFileObject(s.ctx, param, bytes.NewReader(content2)) s.Require().Nil(err) content3 := []byte("Has only A") param = params.CreateFileObjectParams{ Name: "and-test-3.txt", Size: int64(len(content3)), Tags: []string{"tagA"}, } _, err = s.Store.CreateFileObject(s.ctx, param, bytes.NewReader(content3)) s.Require().Nil(err) content4 := []byte("Has only B") param = params.CreateFileObjectParams{ Name: "and-test-4.txt", Size: int64(len(content4)), Tags: []string{"tagB"}, } _, err = s.Store.CreateFileObject(s.ctx, param, bytes.NewReader(content4)) s.Require().Nil(err) // Search for files with BOTH tagA AND tagB result, err := s.Store.SearchFileObjectByTags(s.ctx, []string{"tagA", "tagB"}, 1, 10) s.Require().Nil(err) s.Require().Equal(2, len(result.Results), "Should only return files with BOTH tags") // Verify the correct files are returned foundIDs := make(map[uint]bool) for _, fileObj := range result.Results { foundIDs[fileObj.ID] = true // Verify each result has both tags s.Require().Contains(fileObj.Tags, "tagA") s.Require().Contains(fileObj.Tags, "tagB") } s.Require().True(foundIDs[file1.ID]) s.Require().True(foundIDs[file2.ID]) } func (s *FileStoreTestSuite) TestSearchFileObjectByTagsCaseInsensitive() { // Test case insensitivity of tag search (COLLATE NOCASE) content1 := []byte("File with lowercase tag") param := params.CreateFileObjectParams{ Name: "case-test-1.txt", Size: int64(len(content1)), Tags: []string{"TestTag"}, } file1, err := s.Store.CreateFileObject(s.ctx, param, bytes.NewReader(content1)) s.Require().Nil(err) content2 := []byte("File with UPPERCASE tag") param = params.CreateFileObjectParams{ Name: "case-test-2.txt", Size: int64(len(content2)), Tags: []string{"TESTTAG"}, } file2, err := s.Store.CreateFileObject(s.ctx, param, bytes.NewReader(content2)) s.Require().Nil(err) content3 := []byte("File with MixedCase tag") param = params.CreateFileObjectParams{ Name: "case-test-3.txt", Size: int64(len(content3)), Tags: []string{"testTAG"}, } file3, err := s.Store.CreateFileObject(s.ctx, param, bytes.NewReader(content3)) s.Require().Nil(err) // Search for lowercase - should return all files (case insensitive) result, err := s.Store.SearchFileObjectByTags(s.ctx, []string{"testtag"}, 1, 10) s.Require().Nil(err) s.Require().Equal(3, len(result.Results), "Should match all case variations") foundIDs := make(map[uint]bool) for _, fileObj := range result.Results { foundIDs[fileObj.ID] = true } s.Require().True(foundIDs[file1.ID], "Should find file with 'TestTag'") s.Require().True(foundIDs[file2.ID], "Should find file with 'TESTTAG'") s.Require().True(foundIDs[file3.ID], "Should find file with 'testTAG'") // Search for UPPERCASE - should also return all files result, err = s.Store.SearchFileObjectByTags(s.ctx, []string{"TESTTAG"}, 1, 10) s.Require().Nil(err) s.Require().Equal(3, len(result.Results), "Should match all case variations") // Search for MixedCase - should also return all files result, err = s.Store.SearchFileObjectByTags(s.ctx, []string{"TeStTaG"}, 1, 10) s.Require().Nil(err) s.Require().Equal(3, len(result.Results), "Should match all case variations") } func (s *FileStoreTestSuite) TestSearchFileObjectByTagsOrderByCreatedAt() { // Create files with same tag at different times to test ordering tag := "order-test" content1 := []byte("First file") param := params.CreateFileObjectParams{ Name: "order-1.txt", Size: int64(len(content1)), Tags: []string{tag}, } file1, err := s.Store.CreateFileObject(s.ctx, param, bytes.NewReader(content1)) s.Require().Nil(err) content2 := []byte("Second file") param = params.CreateFileObjectParams{ Name: "order-2.txt", Size: int64(len(content2)), Tags: []string{tag}, } file2, err := s.Store.CreateFileObject(s.ctx, param, bytes.NewReader(content2)) s.Require().Nil(err) content3 := []byte("Third file") param = params.CreateFileObjectParams{ Name: "order-3.txt", Size: int64(len(content3)), Tags: []string{tag}, } file3, err := s.Store.CreateFileObject(s.ctx, param, bytes.NewReader(content3)) s.Require().Nil(err) // Search and verify order (should be DESC by created_at, so newest first) result, err := s.Store.SearchFileObjectByTags(s.ctx, []string{tag}, 1, 10) s.Require().Nil(err) s.Require().GreaterOrEqual(len(result.Results), 3) // The most recently created files should be first // We can at least verify that file3 comes before file1 in the results var file1Idx, file3Idx int for i, fileObj := range result.Results { if fileObj.ID == file1.ID { file1Idx = i } if fileObj.ID == file3.ID { file3Idx = i } } s.Require().Less(file3Idx, file1Idx, "Newer file (file3) should appear before older file (file1)") // Also verify file2 comes before file1 var file2Idx int for i, fileObj := range result.Results { if fileObj.ID == file2.ID { file2Idx = i } } s.Require().Less(file2Idx, file1Idx, "Newer file (file2) should appear before older file (file1)") } func (s *FileStoreTestSuite) TestPaginationFieldsLastPage() { // Create exactly 5 files for i := 0; i < 5; i++ { content := []byte(fmt.Sprintf("Last page test %d", i)) param := params.CreateFileObjectParams{ Name: fmt.Sprintf("last-page-test-%d.txt", i), Size: int64(len(content)), Tags: []string{"last-page"}, } _, err := s.Store.CreateFileObject(s.ctx, param, bytes.NewReader(content)) s.Require().Nil(err) } // Get the last page (should have 1 item with pageSize=2) // Total: 5 items, pageSize: 2, so pages: 3 (2, 2, 1) lastPage, err := s.Store.SearchFileObjectByTags(s.ctx, []string{"last-page"}, 3, 2) s.Require().Nil(err) s.Require().Equal(uint64(3), lastPage.CurrentPage) s.Require().Equal(uint64(3), lastPage.Pages) s.Require().Equal(uint64(5), lastPage.TotalCount) s.Require().Equal(1, len(lastPage.Results), "Last page should have 1 item") s.Require().NotNil(lastPage.PreviousPage, "Last page should have previous page") s.Require().Equal(uint64(2), *lastPage.PreviousPage) s.Require().Nil(lastPage.NextPage, "Last page should not have next page") } func (s *FileStoreTestSuite) TestPaginationFieldsSinglePage() { // Test when all results fit in a single page content := []byte("Single page test") param := params.CreateFileObjectParams{ Name: "single-page-test.txt", Size: int64(len(content)), Tags: []string{"single-page-unique-tag"}, } _, err := s.Store.CreateFileObject(s.ctx, param, bytes.NewReader(content)) s.Require().Nil(err) result, err := s.Store.SearchFileObjectByTags(s.ctx, []string{"single-page-unique-tag"}, 1, 20) s.Require().Nil(err) s.Require().Equal(uint64(1), result.TotalCount) s.Require().Equal(uint64(1), result.Pages) s.Require().Equal(uint64(1), result.CurrentPage) s.Require().Nil(result.PreviousPage, "Single page should not have previous") s.Require().Nil(result.NextPage, "Single page should not have next") } func TestFileStoreTestSuite(t *testing.T) { suite.Run(t, new(FileStoreTestSuite)) }