Files
gitea/modules/storage/azureblob_test.go
T

126 lines
4.1 KiB
Go

// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package storage
import (
"io"
"io/fs"
"net/http"
"os"
"strings"
"sync/atomic"
"testing"
"testing/iotest"
"gitea.dev/modules/setting"
"gitea.dev/modules/test"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type azureBlobFaultTransport struct {
failed, truncated atomic.Bool
}
func (t *azureBlobFaultTransport) RoundTrip(req *http.Request) (*http.Response, error) {
if t.failed.CompareAndSwap(false, true) {
return &http.Response{StatusCode: http.StatusServiceUnavailable, Body: http.NoBody}, nil
}
resp, err := http.DefaultTransport.RoundTrip(req)
if err == nil && req.Method == http.MethodGet && t.truncated.CompareAndSwap(false, true) {
resp.Body = struct {
io.Reader
io.Closer
}{io.MultiReader(io.LimitReader(resp.Body, 2), iotest.ErrReader(io.ErrUnexpectedEOF)), resp.Body}
}
return resp, err
}
func newAzureBlobTestStorage(t *testing.T, basePath string) *AzureBlobStorage {
endpoint := test.ExternalServiceHTTP(t, "TEST_AZURESTORAGE_ENDPOINT", "http://devstoreaccount1.azurite.local:10000")
objStore, err := NewStorage(setting.AzureBlobStorageType, &setting.Storage{
AzureBlobConfig: setting.AzureBlobStorageConfig{
Endpoint: endpoint,
AccountName: "devstoreaccount1",
AccountKey: "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==",
Container: "test-container",
BasePath: basePath,
},
})
require.NoError(t, err)
s, ok := objStore.(*AzureBlobStorage)
require.True(t, ok)
return s
}
func TestAzureBlobStorage(t *testing.T) {
t.Run("General", func(t *testing.T) { testStorageGeneral(t, newAzureBlobTestStorage(t, "")) })
t.Run("GeneralWithBasePath", func(t *testing.T) { testStorageGeneral(t, newAzureBlobTestStorage(t, "test-base-path")) })
s := newAzureBlobTestStorage(t, "")
s.blockSize, s.concurrency, s.retryDelay = 4, 2, 0
transport := &azureBlobFaultTransport{}
s.client.Transport = transport
data := "Q2xTckt6Y1hDOWh0"
t.Run("SaveBlocksWithRetryAndRejectTruncatedInput", func(t *testing.T) {
written, err := s.Save("test.txt", strings.NewReader(data), -1)
require.NoError(t, err)
assert.EqualValues(t, len(data), written)
assert.True(t, transport.failed.Load())
_, err = s.Save("truncated.txt", io.MultiReader(strings.NewReader(data), iotest.ErrReader(io.ErrUnexpectedEOF)), -1)
assert.ErrorIs(t, err, io.ErrUnexpectedEOF)
})
t.Run("ReadResumesSeeksAndFailsOnChangedBlob", func(t *testing.T) {
obj, err := s.Open("test.txt")
require.NoError(t, err)
defer obj.Close()
buf := make([]byte, 4)
_, err = io.ReadFull(obj, buf)
require.NoError(t, err)
assert.Equal(t, data[:4], string(buf))
assert.True(t, transport.truncated.Load())
_, err = obj.Seek(-5, io.SeekEnd)
require.NoError(t, err)
_, err = io.ReadFull(obj, buf)
require.NoError(t, err)
assert.Equal(t, data[11:15], string(buf))
_, err = s.Save("test.txt", strings.NewReader("changed"), -1)
require.NoError(t, err)
_, err = obj.Seek(0, io.SeekStart)
require.NoError(t, err)
_, err = io.ReadAll(obj)
assert.ErrorIs(t, err, azureBlobError("ConditionNotMet"))
})
t.Run("ServeDirectURLAllowsPut", func(t *testing.T) {
u, err := s.ServeDirectURL("direct.txt", "direct.txt", http.MethodPut, nil)
require.NoError(t, err)
req, err := http.NewRequestWithContext(t.Context(), http.MethodPut, u.String(), strings.NewReader("direct"))
require.NoError(t, err)
req.Header.Set("x-ms-blob-type", "BlockBlob")
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
_ = resp.Body.Close()
assert.Equal(t, http.StatusCreated, resp.StatusCode)
obj, err := s.Open("direct.txt")
require.NoError(t, err)
defer obj.Close()
content, err := io.ReadAll(obj)
require.NoError(t, err)
assert.Equal(t, "direct", string(content))
})
t.Run("StatAfterDeleteReturnsNilInfo", func(t *testing.T) {
assert.NoError(t, s.Delete("test.txt"))
assert.NoError(t, s.Delete("direct.txt"))
info, err := s.Stat("test.txt")
assert.ErrorIs(t, err, fs.ErrNotExist)
assert.Equal(t, os.FileInfo(nil), info)
})
}