更新
This commit is contained in:
@@ -0,0 +1,225 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"database/sql"
|
||||
"database/sql/driver"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"image"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type mediaUploadDB struct {
|
||||
directory string
|
||||
inserted, finalized, deleted, failFinalize bool
|
||||
publicURL, mime string
|
||||
size int64
|
||||
}
|
||||
type mediaUploadConnector struct{ db *mediaUploadDB }
|
||||
|
||||
func (c mediaUploadConnector) Connect(context.Context) (driver.Conn, error) { return c.db, nil }
|
||||
func (c mediaUploadConnector) Driver() driver.Driver { return oauthTestDriver{} }
|
||||
func (*mediaUploadDB) Prepare(string) (driver.Stmt, error) {
|
||||
return nil, errors.New("unexpected prepare")
|
||||
}
|
||||
func (*mediaUploadDB) Begin() (driver.Tx, error) { return nil, errors.New("unexpected transaction") }
|
||||
func (*mediaUploadDB) Close() error { return nil }
|
||||
func (s *mediaUploadDB) QueryContext(_ context.Context, query string, args []driver.NamedValue) (driver.Rows, error) {
|
||||
if !strings.HasPrefix(query, "SELECT config_value,value_type FROM system_configs") {
|
||||
return nil, errors.New("unexpected query")
|
||||
}
|
||||
values := map[string]string{"storage.provider": "local", "storage.object_prefix": "media", "storage.local.directory": s.directory, "storage.local.public_base_url": "https://media.example.com/uploads"}
|
||||
value, ok := values[args[0].Value.(string)]
|
||||
if !ok {
|
||||
return &oauthTestRows{columns: []string{"config_value", "value_type"}}, nil
|
||||
}
|
||||
return oauthRow(value, "text"), nil
|
||||
}
|
||||
func (s *mediaUploadDB) ExecContext(_ context.Context, query string, args []driver.NamedValue) (driver.Result, error) {
|
||||
switch {
|
||||
case strings.HasPrefix(query, "INSERT INTO media_assets"):
|
||||
s.inserted = true
|
||||
s.mime = args[5].Value.(string)
|
||||
s.size = args[6].Value.(int64)
|
||||
return provisioningInsertResult{}, nil
|
||||
case strings.HasPrefix(query, "UPDATE media_assets SET public_url="):
|
||||
if s.failFinalize {
|
||||
return nil, errors.New("finalization unavailable")
|
||||
}
|
||||
s.publicURL = args[0].Value.(string)
|
||||
s.finalized = true
|
||||
case strings.HasPrefix(query, "DELETE FROM media_assets"):
|
||||
s.deleted = true
|
||||
default:
|
||||
return nil, errors.New("unexpected exec")
|
||||
}
|
||||
return driver.RowsAffected(1), nil
|
||||
}
|
||||
|
||||
func uploadTestApp(t *testing.T) (*App, *mediaUploadDB) {
|
||||
t.Helper()
|
||||
store := &mediaUploadDB{directory: t.TempDir()}
|
||||
db := sql.OpenDB(mediaUploadConnector{store})
|
||||
t.Cleanup(func() { _ = db.Close() })
|
||||
return &App{db: db, config: Config{MediaDir: store.directory, Environment: "development"}}, store
|
||||
}
|
||||
func uploadTestRequest(t *testing.T, a *App, source []byte, purpose string) *httptest.ResponseRecorder {
|
||||
t.Helper()
|
||||
var body bytes.Buffer
|
||||
writer := multipart.NewWriter(&body)
|
||||
part, err := writer.CreateFormFile("file", "camera.png")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err = part.Write(source); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if purpose != "" {
|
||||
if err = writer.WriteField("purpose", purpose); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if err = writer.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r := httptest.NewRequest(http.MethodPost, "/api/v1/media/upload", &body)
|
||||
r.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
r = r.WithContext(context.WithValue(r.Context(), identityKey{}, identity{ID: 42, Role: "user"}))
|
||||
w := httptest.NewRecorder()
|
||||
a.uploadMedia(w, r)
|
||||
return w
|
||||
}
|
||||
|
||||
func TestAvatarUploadStoresAndServesAllVariants(t *testing.T) {
|
||||
a, store := uploadTestApp(t)
|
||||
source := avatarPNG(t, 1000, 800)
|
||||
w := uploadTestRequest(t, a, source, "avatar")
|
||||
if w.Code != 200 {
|
||||
t.Fatalf("upload failed: %s", w.Body.String())
|
||||
}
|
||||
var payload struct {
|
||||
Data struct {
|
||||
URL string `json:"url"`
|
||||
ThumbnailURL string `json:"thumbnailUrl"`
|
||||
OriginalURL string `json:"originalUrl"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &payload); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
data := payload.Data
|
||||
if !store.inserted || !store.finalized || store.publicURL != data.URL || store.mime != "image/jpeg" {
|
||||
t.Fatal("media record does not identify the display image")
|
||||
}
|
||||
for _, variant := range []struct {
|
||||
url string
|
||||
max int
|
||||
}{{data.URL, 640}, {data.ThumbnailURL, 256}, {data.OriginalURL, 0}} {
|
||||
u, err := url.Parse(variant.url)
|
||||
if err != nil || !mediaNamePattern.MatchString(filepath.Base(u.Path)) {
|
||||
t.Fatal("invalid variant URL", variant.url)
|
||||
}
|
||||
content, err := os.ReadFile(filepath.Join(store.directory, filepath.Base(u.Path)))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if variant.max == 0 {
|
||||
if !bytes.Equal(content, source) {
|
||||
t.Fatal("original upload was not preserved")
|
||||
}
|
||||
} else {
|
||||
cfg, format, err := image.DecodeConfig(bytes.NewReader(content))
|
||||
if err != nil || format != "jpeg" || cfg.Width != variant.max || cfg.Height > variant.max {
|
||||
t.Fatal("invalid variant dimensions", err)
|
||||
}
|
||||
if variant.url == data.URL && store.size != int64(len(content)) {
|
||||
t.Fatal("DB file_size does not match uploaded JPEG")
|
||||
}
|
||||
}
|
||||
response := httptest.NewRecorder()
|
||||
a.serveMedia(response, httptest.NewRequest(http.MethodGet, u.Path, nil))
|
||||
if response.Code != 200 || !bytes.Equal(response.Body.Bytes(), content) || !strings.Contains(response.Header().Get("Cache-Control"), "immutable") {
|
||||
t.Fatal("variant is not publicly cacheable")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAvatarUploadFinalizationFailureCleansEveryObject(t *testing.T) {
|
||||
a, store := uploadTestApp(t)
|
||||
store.failFinalize = true
|
||||
w := uploadTestRequest(t, a, avatarPNG(t, 300, 200), "avatar")
|
||||
files, err := os.ReadDir(store.directory)
|
||||
if w.Code != 500 || !store.deleted || store.finalized || err != nil || len(files) != 0 {
|
||||
t.Fatal("failed upload retained objects or an active record", w.Code, err, len(files))
|
||||
}
|
||||
}
|
||||
|
||||
func TestOrdinaryMediaUploadPreservesOriginal(t *testing.T) {
|
||||
a, store := uploadTestApp(t)
|
||||
source := avatarPNG(t, 800, 400)
|
||||
w := uploadTestRequest(t, a, source, "")
|
||||
if w.Code != 200 || store.mime != "image/png" || store.size != int64(len(source)) {
|
||||
t.Fatal("ordinary image upload changed", w.Body.String())
|
||||
}
|
||||
var payload struct {
|
||||
Data struct {
|
||||
URL string `json:"url"`
|
||||
ThumbnailURL string `json:"thumbnailUrl"`
|
||||
OriginalURL string `json:"originalUrl"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &payload); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if payload.Data.ThumbnailURL == "" || payload.Data.OriginalURL != "" || !strings.Contains(payload.Data.URL, "-im1.png") {
|
||||
t.Fatal("ordinary image did not publish a bounded list thumbnail")
|
||||
}
|
||||
files, err := os.ReadDir(store.directory)
|
||||
if err != nil || len(files) != 2 {
|
||||
t.Fatal("unexpected variants on ordinary media")
|
||||
}
|
||||
mainURL, _ := url.Parse(payload.Data.URL)
|
||||
data, err := os.ReadFile(filepath.Join(store.directory, filepath.Base(mainURL.Path)))
|
||||
if err != nil || !bytes.Equal(data, source) {
|
||||
t.Fatal("ordinary image was recompressed")
|
||||
}
|
||||
thumbnailURL, _ := url.Parse(payload.Data.ThumbnailURL)
|
||||
thumbnail, err := os.ReadFile(filepath.Join(store.directory, filepath.Base(thumbnailURL.Path)))
|
||||
config, format, decodeErr := image.DecodeConfig(bytes.NewReader(thumbnail))
|
||||
if err != nil || decodeErr != nil || format != "jpeg" || config.Width > 256 || config.Height > 256 {
|
||||
t.Fatal("ordinary image thumbnail is invalid", err, decodeErr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInvalidAvatarUploadCreatesNoRecords(t *testing.T) {
|
||||
for _, purpose := range []string{"avatar", "unknown"} {
|
||||
a, store := uploadTestApp(t)
|
||||
w := uploadTestRequest(t, a, []byte("GIF89a\x01\x00\x01\x00broken"), purpose)
|
||||
if w.Code != 400 || store.inserted {
|
||||
t.Fatal("invalid upload created media records", w.Code)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMediaThumbnailURLOnlyRewritesVersionedImages(t *testing.T) {
|
||||
tests := map[string]string{
|
||||
"https://cdn.example.com/media/42-1-im1.png": "https://cdn.example.com/media/42-1-im1-thumb.jpg",
|
||||
"https://cdn.example.com/media/42-1-im1-thumb.jpg": "https://cdn.example.com/media/42-1-im1-thumb.jpg",
|
||||
"https://cdn.example.com/media/legacy.png": "https://cdn.example.com/media/legacy.png",
|
||||
"https://cdn.example.com/media/42-1-im1.png?signature=x": "https://cdn.example.com/media/42-1-im1.png?signature=x",
|
||||
}
|
||||
for source, want := range tests {
|
||||
if got := mediaThumbnailURL(source); got != want {
|
||||
t.Errorf("mediaThumbnailURL(%q) = %q, want %q", source, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user