- Store bundled template posters in public S3 when available - Presign imported user collection posters in API responses - Thread frontend assets into router and collection handlers
176 lines
4.3 KiB
Go
176 lines
4.3 KiB
Go
package handlers
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"image"
|
|
"image/color"
|
|
"image/jpeg"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"sync"
|
|
"testing"
|
|
"testing/fstest"
|
|
|
|
"github.com/Silo-Server/silo-server/internal/s3client"
|
|
)
|
|
|
|
type collectionArtworkS3Recorder struct {
|
|
server *httptest.Server
|
|
mu sync.Mutex
|
|
puts []string
|
|
}
|
|
|
|
func newCollectionArtworkS3Recorder(t *testing.T) *collectionArtworkS3Recorder {
|
|
t.Helper()
|
|
|
|
recorder := &collectionArtworkS3Recorder{}
|
|
recorder.server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
_, _ = io.Copy(io.Discard, r.Body)
|
|
_ = r.Body.Close()
|
|
if r.Method == http.MethodPut {
|
|
recorder.mu.Lock()
|
|
recorder.puts = append(recorder.puts, r.URL.Path)
|
|
recorder.mu.Unlock()
|
|
}
|
|
w.WriteHeader(http.StatusOK)
|
|
}))
|
|
t.Cleanup(recorder.server.Close)
|
|
return recorder
|
|
}
|
|
|
|
func (r *collectionArtworkS3Recorder) client() *s3client.Client {
|
|
return s3client.NewClient(s3client.BucketConfig{
|
|
Endpoint: r.server.URL,
|
|
Region: "us-east-1",
|
|
Bucket: "public-assets",
|
|
AccessKey: "test",
|
|
SecretKey: "test",
|
|
PathStyle: true,
|
|
})
|
|
}
|
|
|
|
func (r *collectionArtworkS3Recorder) putPaths() []string {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
out := make([]string, len(r.puts))
|
|
copy(out, r.puts)
|
|
return out
|
|
}
|
|
|
|
func TestStoreBundledCollectionPosterIfS3Configured_NoS3KeepsPath(t *testing.T) {
|
|
path := "/images/collection-templates/template.jpg"
|
|
gotPath, gotThumbhash, stored, err := storeBundledCollectionPosterIfS3Configured(
|
|
context.Background(),
|
|
nil,
|
|
fstest.MapFS{},
|
|
"collection-1",
|
|
adminCollectionImagePrefix,
|
|
path,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("storeBundledCollectionPosterIfS3Configured: %v", err)
|
|
}
|
|
if stored {
|
|
t.Fatal("stored = true, want false")
|
|
}
|
|
if gotPath != path {
|
|
t.Fatalf("path = %q, want %q", gotPath, path)
|
|
}
|
|
if gotThumbhash != "" {
|
|
t.Fatalf("thumbhash = %q, want empty", gotThumbhash)
|
|
}
|
|
}
|
|
|
|
func TestStoreBundledCollectionPosterIfS3Configured_IgnoresNonTemplatePath(t *testing.T) {
|
|
recorder := newCollectionArtworkS3Recorder(t)
|
|
path := "collection-images/existing/poster/original.webp"
|
|
|
|
gotPath, gotThumbhash, stored, err := storeBundledCollectionPosterIfS3Configured(
|
|
context.Background(),
|
|
recorder.client(),
|
|
fstest.MapFS{},
|
|
"collection-1",
|
|
adminCollectionImagePrefix,
|
|
path,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("storeBundledCollectionPosterIfS3Configured: %v", err)
|
|
}
|
|
if stored {
|
|
t.Fatal("stored = true, want false")
|
|
}
|
|
if gotPath != path {
|
|
t.Fatalf("path = %q, want %q", gotPath, path)
|
|
}
|
|
if gotThumbhash != "" {
|
|
t.Fatalf("thumbhash = %q, want empty", gotThumbhash)
|
|
}
|
|
if puts := recorder.putPaths(); len(puts) != 0 {
|
|
t.Fatalf("PUT paths = %#v, want none", puts)
|
|
}
|
|
}
|
|
|
|
func TestStoreBundledCollectionPosterIfS3Configured_UploadsTemplatePoster(t *testing.T) {
|
|
recorder := newCollectionArtworkS3Recorder(t)
|
|
frontendFS := fstest.MapFS{
|
|
"images/collection-templates/template.jpg": {
|
|
Data: testCollectionPosterJPEG(t),
|
|
},
|
|
}
|
|
|
|
gotPath, gotThumbhash, stored, err := storeBundledCollectionPosterIfS3Configured(
|
|
context.Background(),
|
|
recorder.client(),
|
|
frontendFS,
|
|
"collection-1",
|
|
adminCollectionImagePrefix,
|
|
"/images/collection-templates/template.jpg",
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("storeBundledCollectionPosterIfS3Configured: %v", err)
|
|
}
|
|
if !stored {
|
|
t.Fatal("stored = false, want true")
|
|
}
|
|
if gotPath != "collection-images/collection-1/poster/original.webp" {
|
|
t.Fatalf("path = %q", gotPath)
|
|
}
|
|
if gotThumbhash == "" {
|
|
t.Fatal("thumbhash is empty")
|
|
}
|
|
|
|
want := map[string]bool{
|
|
"/public-assets/collection-images/collection-1/poster/original.webp": true,
|
|
"/public-assets/collection-images/collection-1/poster/w500.webp": true,
|
|
"/public-assets/collection-images/collection-1/poster/w300.webp": true,
|
|
}
|
|
puts := recorder.putPaths()
|
|
if len(puts) != len(want) {
|
|
t.Fatalf("PUT paths = %#v", puts)
|
|
}
|
|
for _, path := range puts {
|
|
if !want[path] {
|
|
t.Fatalf("unexpected PUT path %q in %#v", path, puts)
|
|
}
|
|
}
|
|
}
|
|
|
|
func testCollectionPosterJPEG(t *testing.T) []byte {
|
|
t.Helper()
|
|
|
|
img := image.NewRGBA(image.Rect(0, 0, 32, 48))
|
|
for y := 0; y < 48; y++ {
|
|
for x := 0; x < 32; x++ {
|
|
img.Set(x, y, color.RGBA{R: uint8(x * 6), G: uint8(y * 4), B: 120, A: 255})
|
|
}
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
if err := jpeg.Encode(&buf, img, &jpeg.Options{Quality: 90}); err != nil {
|
|
t.Fatalf("encode jpeg: %v", err)
|
|
}
|
|
return buf.Bytes()
|
|
}
|