package app import ( "context" "errors" "strings" "testing" "time" "github.com/geo-platform/tenant-api/internal/shared/objectstorage" "github.com/stretchr/testify/require" ) func TestNormalizeOSSCommonPrefixesDerivesNextLevelFolders(t *testing.T) { t.Parallel() objects := []objectstorage.ObjectInfo{ {Key: "reports/2026/report.txt"}, {Key: "reports/2026/monthly/january.txt"}, {Key: "reports/2025/summary.txt"}, {Key: "reports/readme.txt"}, {Key: "images/logo.png"}, } require.Equal(t, []string{"reports/2026/", "reports/2025/"}, normalizeOSSCommonPrefixes("reports/", nil, objects, false), ) require.Equal(t, []objectstorage.ObjectInfo{{Key: "reports/readme.txt"}}, normalizeOSSListedObjects("reports/", objects, false), ) } func TestNormalizeOSSListedObjectsFiltersFolderPlaceholders(t *testing.T) { t.Parallel() objects := []objectstorage.ObjectInfo{ {Key: "reports/"}, {Key: "reports/readme.txt"}, {Key: "reports/2026/"}, {Key: "reports/2026/report.txt"}, } require.Equal(t, []objectstorage.ObjectInfo{{Key: "reports/readme.txt"}}, normalizeOSSListedObjects("reports/", objects, false), ) require.Equal(t, []objectstorage.ObjectInfo{{Key: "reports/readme.txt"}, {Key: "reports/2026/report.txt"}}, normalizeOSSListedObjects("reports/", objects, true), ) } func TestNormalizeOSSCommonPrefixesKeepsProviderPrefixes(t *testing.T) { t.Parallel() got := normalizeOSSCommonPrefixes( "reports/", []string{"reports/2026/", "reports/2025/"}, []objectstorage.ObjectInfo{ {Key: "reports/2026/report.txt"}, {Key: "reports/2025/summary.txt"}, }, false, ) require.Equal(t, []string{"reports/2026/", "reports/2025/"}, got) } func TestNormalizeOSSCommonPrefixesOmitsFoldersInRecursiveMode(t *testing.T) { t.Parallel() got := normalizeOSSCommonPrefixes( "reports/", []string{"reports/2026/"}, []objectstorage.ObjectInfo{{Key: "reports/2026/report.txt"}}, true, ) require.Empty(t, got) } func TestShouldFallbackOSSDirectoryList(t *testing.T) { t.Parallel() require.True(t, shouldFallbackOSSDirectoryList( ObjectStorageListInput{Prefix: "desktop-client/"}, "", nil, nil, )) require.False(t, shouldFallbackOSSDirectoryList( ObjectStorageListInput{Prefix: "desktop-client/", Recursive: true}, "", nil, nil, )) require.False(t, shouldFallbackOSSDirectoryList( ObjectStorageListInput{Prefix: "desktop-client/"}, "release", nil, nil, )) require.False(t, shouldFallbackOSSDirectoryList( ObjectStorageListInput{Prefix: "desktop-client/"}, "", nil, []string{"desktop-client/releases/"}, )) } func TestCreateFolderWritesPlaceholder(t *testing.T) { t.Parallel() storage := newFakeObjectStorage() svc := &ObjectStorageService{storage: storage} result, err := svc.CreateFolder(context.Background(), nil, ObjectStorageFolderCreateInput{Prefix: "reports/2026"}) require.NoError(t, err) require.Equal(t, "reports/2026/", result.Prefix) require.Contains(t, storage.objects, "reports/2026/") require.Equal(t, []byte(nil), storage.objects["reports/2026/"]) require.Equal(t, "application/octet-stream", storage.contentTypes["reports/2026/"]) } func TestCreateFolderRejectsExistingPrefix(t *testing.T) { t.Parallel() storage := newFakeObjectStorage() storage.objects["reports/2026/report.txt"] = []byte("content") svc := &ObjectStorageService{storage: storage} _, err := svc.CreateFolder(context.Background(), nil, ObjectStorageFolderCreateInput{Prefix: "reports/2026/"}) require.Error(t, err) require.Contains(t, err.Error(), "object_storage_folder_exists") } func TestDeleteFolderDeletesAllObjectsUnderPrefix(t *testing.T) { t.Parallel() storage := newFakeObjectStorage() storage.objects["reports/2026/"] = nil storage.objects["reports/2026/a.txt"] = []byte("a") storage.objects["reports/2026/nested/b.txt"] = []byte("b") storage.objects["reports/2025/a.txt"] = []byte("old") svc := &ObjectStorageService{storage: storage} result, err := svc.DeleteFolder(context.Background(), nil, "reports/2026/") require.NoError(t, err) require.Equal(t, "reports/2026/", result.Prefix) require.Equal(t, 3, result.DeletedCount) require.NotContains(t, storage.objects, "reports/2026/") require.NotContains(t, storage.objects, "reports/2026/a.txt") require.NotContains(t, storage.objects, "reports/2026/nested/b.txt") require.Contains(t, storage.objects, "reports/2025/a.txt") } func TestGetReturnsPreviewURLAndOmitsNonPublicURL(t *testing.T) { t.Parallel() storage := newFakeObjectStorage() storage.objects["reports/secret.png"] = []byte("image") storage.publicReadable = false svc := &ObjectStorageService{storage: storage} result, err := svc.Get(context.Background(), "reports/secret.png") require.NoError(t, err) require.Nil(t, result.PublicURL) require.NotNil(t, result.PreviewURL) require.Equal(t, "https://example.test/download/reports/secret.png", *result.PreviewURL) } type fakeObjectStorage struct { objects map[string][]byte contentTypes map[string]string publicReadable bool } func newFakeObjectStorage() *fakeObjectStorage { return &fakeObjectStorage{ objects: make(map[string][]byte), contentTypes: make(map[string]string), publicReadable: true, } } func (s *fakeObjectStorage) Validate() error { return nil } func (s *fakeObjectStorage) PutBytes(_ context.Context, objectKey string, content []byte, contentType string) error { s.objects[objectKey] = content s.contentTypes[objectKey] = contentType return nil } func (s *fakeObjectStorage) GetBytes(_ context.Context, objectKey string) ([]byte, error) { content, ok := s.objects[objectKey] if !ok { return nil, objectstorage.ErrObjectNotFound } return content, nil } func (s *fakeObjectStorage) Exists(_ context.Context, objectKey string) (bool, error) { _, ok := s.objects[objectKey] return ok, nil } func (s *fakeObjectStorage) Delete(_ context.Context, objectKey string) error { delete(s.objects, objectKey) return nil } func (s *fakeObjectStorage) PublicURL(objectKey string) (string, error) { return "https://example.test/" + objectKey, nil } func (s *fakeObjectStorage) PublicReadableURL(objectKey string) (string, error) { if !s.publicReadable { return "", objectstorage.ErrObjectNotPublic } return "https://example.test/" + objectKey, nil } func (s *fakeObjectStorage) DownloadURL(objectKey string) (string, error) { return "https://example.test/download/" + objectKey, nil } func (s *fakeObjectStorage) List(_ context.Context, in objectstorage.ListInput) (*objectstorage.ListResult, error) { if in.Limit == 0 { return nil, errors.New("test list limit must be set") } result := &objectstorage.ListResult{} for key, content := range s.objects { if strings.HasPrefix(key, in.Prefix) { result.Objects = append(result.Objects, objectstorage.ObjectInfo{ Key: key, Size: int64(len(content)), LastModified: time.Now(), }) } } return result, nil } func (s *fakeObjectStorage) Stat(_ context.Context, objectKey string) (*objectstorage.ObjectInfo, error) { content, ok := s.objects[objectKey] if !ok { return nil, objectstorage.ErrObjectNotFound } return &objectstorage.ObjectInfo{Key: objectKey, Size: int64(len(content)), LastModified: time.Now()}, nil } func (s *fakeObjectStorage) Copy(_ context.Context, sourceKey, destinationKey string) error { content, ok := s.objects[sourceKey] if !ok { return objectstorage.ErrObjectNotFound } s.objects[destinationKey] = append([]byte(nil), content...) return nil }