268 lines
7.4 KiB
Go
268 lines
7.4 KiB
Go
|
|
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
|
||
|
|
}
|