Files
probo/pkg/filemanager/s3_test.go
Bryan Frimin 21d098afb1 Style
Signed-off-by: Bryan Frimin <bryan@probo.com>
2026-07-02 18:45:51 +02:00

343 lines
8.0 KiB
Go

// Copyright (c) 2026 Probo Inc <hello@probo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package filemanager_test
import (
"context"
"io"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/credentials"
awss3 "github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/filemanager"
)
func newTestS3Service(t *testing.T, handler http.HandlerFunc) *filemanager.Service {
t.Helper()
srv := httptest.NewServer(handler)
t.Cleanup(srv.Close)
s3Client := awss3.NewFromConfig(
aws.Config{
Region: "us-east-1",
Credentials: credentials.NewStaticCredentialsProvider("access-key", "secret-key", ""),
},
func(o *awss3.Options) {
o.BaseEndpoint = aws.String(srv.URL)
o.UsePathStyle = true
},
)
return filemanager.NewService(nil, nil, s3Client)
}
func TestOpenFile_StreamsBody(t *testing.T) {
t.Parallel()
const (
etag = `"abc123"`
content = "hello world"
)
svc := newTestS3Service(
t,
func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("ETag", etag)
w.Header().Set("Content-Type", "application/octet-stream")
_, _ = io.WriteString(w, content)
},
)
file := &coredata.File{
BucketName: "uploads",
FileKey: "tenant/file",
MimeType: "text/plain",
FileSize: int64(len(content)),
}
obj, err := svc.OpenFile(context.Background(), file, filemanager.FileConditions{})
require.NoError(t, err)
require.NotNil(t, obj)
require.False(t, obj.NotModified)
defer func() { _ = obj.Body.Close() }()
assert.Equal(t, etag, obj.ETag)
assert.Equal(t, "text/plain", obj.ContentType)
assert.Equal(t, int64(len(content)), obj.ContentLength)
body, err := io.ReadAll(obj.Body)
require.NoError(t, err)
assert.Equal(t, content, string(body))
}
func TestOpenFile_RangeRequestReturnsPartialContent(t *testing.T) {
t.Parallel()
const (
etag = `"abc123"`
content = "hello world"
)
svc := newTestS3Service(
t,
func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "bytes=0-4", r.Header.Get("Range"))
w.Header().Set("ETag", etag)
w.Header().Set("Content-Range", "bytes 0-4/11")
w.Header().Set("Content-Length", "5")
w.WriteHeader(http.StatusPartialContent)
_, _ = io.WriteString(w, content[:5])
},
)
file := &coredata.File{
BucketName: "uploads",
FileKey: "tenant/file",
MimeType: "text/plain",
FileSize: int64(len(content)),
}
obj, err := svc.OpenFile(
context.Background(),
file,
filemanager.FileConditions{Range: "bytes=0-4"},
)
require.NoError(t, err)
require.NotNil(t, obj)
defer func() { _ = obj.Body.Close() }()
assert.True(t, obj.PartialContent)
assert.False(t, obj.NotModified)
assert.Equal(t, "bytes 0-4/11", obj.ContentRange)
assert.Equal(t, int64(5), obj.ContentLength)
body, err := io.ReadAll(obj.Body)
require.NoError(t, err)
assert.Equal(t, content[:5], string(body))
}
func TestOpenFile_IfRangeMatchHonorsRange(t *testing.T) {
t.Parallel()
const (
etag = `"abc123"`
content = "hello world"
)
svc := newTestS3Service(
t,
func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodHead {
w.Header().Set("ETag", etag)
w.WriteHeader(http.StatusOK)
return
}
assert.Equal(t, "bytes=0-4", r.Header.Get("Range"))
w.Header().Set("ETag", etag)
w.Header().Set("Content-Range", "bytes 0-4/11")
w.Header().Set("Content-Length", "5")
w.WriteHeader(http.StatusPartialContent)
_, _ = io.WriteString(w, content[:5])
},
)
file := &coredata.File{
BucketName: "uploads",
FileKey: "tenant/file",
MimeType: "text/plain",
FileSize: int64(len(content)),
}
obj, err := svc.OpenFile(
context.Background(),
file,
filemanager.FileConditions{Range: "bytes=0-4", IfRange: etag},
)
require.NoError(t, err)
require.NotNil(t, obj)
defer func() { _ = obj.Body.Close() }()
assert.True(t, obj.PartialContent)
assert.Equal(t, "bytes 0-4/11", obj.ContentRange)
body, err := io.ReadAll(obj.Body)
require.NoError(t, err)
assert.Equal(t, content[:5], string(body))
}
func TestOpenFile_IfRangeMismatchServesFullContent(t *testing.T) {
t.Parallel()
const (
staleETag = `"old"`
currentETag = `"new"`
content = "hello world"
)
svc := newTestS3Service(
t,
func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodHead {
w.Header().Set("ETag", currentETag)
w.WriteHeader(http.StatusOK)
return
}
// The stale If-Range guard must have dropped the Range so S3
// returns the full object rather than a 206 of the fresh bytes.
assert.Empty(t, r.Header.Get("Range"))
w.Header().Set("ETag", currentETag)
_, _ = io.WriteString(w, content)
},
)
file := &coredata.File{
BucketName: "uploads",
FileKey: "tenant/file",
MimeType: "text/plain",
FileSize: int64(len(content)),
}
obj, err := svc.OpenFile(
context.Background(),
file,
filemanager.FileConditions{Range: "bytes=0-4", IfRange: staleETag},
)
require.NoError(t, err)
require.NotNil(t, obj)
defer func() { _ = obj.Body.Close() }()
assert.False(t, obj.PartialContent)
assert.Empty(t, obj.ContentRange)
body, err := io.ReadAll(obj.Body)
require.NoError(t, err)
assert.Equal(t, content, string(body))
}
func TestOpenFile_RangeNotSatisfiable(t *testing.T) {
t.Parallel()
const content = "hello world"
svc := newTestS3Service(
t,
func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Range", "bytes */11")
w.WriteHeader(http.StatusRequestedRangeNotSatisfiable)
},
)
file := &coredata.File{
BucketName: "uploads",
FileKey: "tenant/file",
MimeType: "text/plain",
FileSize: int64(len(content)),
}
obj, err := svc.OpenFile(
context.Background(),
file,
filemanager.FileConditions{Range: "bytes=999-1000"},
)
require.NoError(t, err)
require.NotNil(t, obj)
assert.True(t, obj.RangeNotSatisfiable)
assert.False(t, obj.PartialContent)
assert.Nil(t, obj.Body)
assert.Equal(t, int64(len(content)), obj.ContentLength)
}
func TestOpenFile_NotModifiedByETag(t *testing.T) {
t.Parallel()
const etag = `"abc123"`
svc := newTestS3Service(
t,
func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("If-None-Match") == etag {
w.WriteHeader(http.StatusNotModified)
return
}
w.Header().Set("ETag", etag)
_, _ = io.WriteString(w, "content")
},
)
file := &coredata.File{
BucketName: "uploads",
FileKey: "tenant/file",
MimeType: "text/plain",
}
obj, err := svc.OpenFile(context.Background(), file, filemanager.FileConditions{IfNoneMatch: etag})
require.NoError(t, err)
require.NotNil(t, obj)
assert.True(t, obj.NotModified)
assert.Nil(t, obj.Body)
}
func TestOpenFile_NotModifiedByModifiedSince(t *testing.T) {
t.Parallel()
svc := newTestS3Service(
t,
func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("If-Modified-Since") != "" {
w.WriteHeader(http.StatusNotModified)
return
}
_, _ = io.WriteString(w, "content")
},
)
file := &coredata.File{
BucketName: "uploads",
FileKey: "tenant/file",
MimeType: "text/plain",
}
obj, err := svc.OpenFile(
context.Background(),
file,
filemanager.FileConditions{IfModifiedSince: time.Now()},
)
require.NoError(t, err)
require.NotNil(t, obj)
assert.True(t, obj.NotModified)
assert.Nil(t, obj.Body)
}