package attach import ( "bytes" "errors" "image" "image/color" "image/jpeg" "io" "strings" "testing" "gocloud.dev/blob" "gocloud.dev/blob/memblob" ) func TestThumbFilename(t *testing.T) { got := ThumbFilename(42, 200, 200, 0, 0, "crop", "jpg") const want = "thumb_42_200_200_0_0_crop.jpg" if got != want { t.Fatalf("ThumbFilename = %q, want %q", got, want) } } func TestThumbFilenameRejectsUnsafeTokens(t *testing.T) { for _, tc := range []struct{ mode, ext string }{ {"../../secret", "jpg"}, {"auto", "../jpg"}, {"a/b", "jpg"}, {"auto", "j.pg"}, {"", ""}, {"Crop", "JPG"}, } { got := ThumbFilename(42, 200, 200, 0, 0, tc.mode, tc.ext) if strings.ContainsAny(got, `/\`) || strings.Contains(got, "..") { t.Fatalf("mode=%q ext=%q produced unsafe name %q", tc.mode, tc.ext, got) } if !strings.HasPrefix(got, "thumb_42_200_200_0_0_") { t.Fatalf("mode=%q ext=%q name %q lost its prefix", tc.mode, tc.ext, got) } } if got := ThumbFilename(42, 200, 200, 0, 0, "../../secret", "jpg"); got != "thumb_42_200_200_0_0_auto.jpg" { t.Fatalf("unsafe mode coerced to %q", got) } } func TestFileThumbRejectsOutOfRangeSize(t *testing.T) { bucket := memblob.OpenBucket(nil) defer bucket.Close() f := &File{ID: 1, DiskName: "abc123xyz.jpg"} for _, tc := range []struct{ w, h int }{ {0, 200}, {200, 0}, {-1, -1}, {maxThumbEdge + 1, 200}, {200, maxThumbEdge + 1}, {100000, 100000}, } { if _, err := f.Thumb(t.Context(), bucket, tc.w, tc.h, "crop"); err == nil { t.Fatalf("size %dx%d must be rejected", tc.w, tc.h) } } iter := bucket.List(nil) if obj, err := iter.Next(t.Context()); err != io.EOF { t.Fatalf("rejected sizes must not touch the bucket, found %v (err %v)", obj, err) } } func TestFileThumbRejectsTraversalMode(t *testing.T) { bucket := memblob.OpenBucket(nil) defer bucket.Close() f := &File{ID: 42, DiskName: "abc123xyz789.jpg"} for _, mode := range []string{"../../secret", "a/b", "auto.jpg", "cr op"} { if _, err := f.Thumb(t.Context(), bucket, 200, 200, mode); err == nil { t.Fatalf("mode %q must be rejected", mode) } } iter := bucket.List(nil) if obj, err := iter.Next(t.Context()); err != io.EOF { t.Fatalf("rejected modes must write nothing, found %v (err %v)", obj, err) } } func TestPartitionDirectory(t *testing.T) { got := PartitionDirectory("abc123xyz.jpg") const want = "abc/123/xyz/" if got != want { t.Fatalf("PartitionDirectory = %q, want %q", got, want) } } func TestFileThumbResizesOnce(t *testing.T) { ctx := t.Context() bucket := memblob.OpenBucket(nil) t.Cleanup(func() { _ = bucket.Close() }) f := &File{ID: 42, DiskName: "abc123xyz.jpg", FileName: "cover.jpg"} origKey := PartitionDirectory(f.DiskName) + f.DiskName if origKey == "abc123xyz.jpg" || origKey == "" { // PartitionDirectory still stubbed — write under the Winter key the // GREEN implementation will look up so this test stays the behavior spec. origKey = "abc/123/xyz/" + f.DiskName } if err := bucket.WriteAll(ctx, origKey, testJPEG(t), &blob.WriterOptions{ContentType: "image/jpeg"}); err != nil { t.Fatal(err) } var n int orig := resizeImage resizeImage = func(src image.Image, w, h int, mode string) image.Image { n++ return orig(src, w, h, mode) } t.Cleanup(func() { resizeImage = orig }) url1, err := f.Thumb(ctx, bucket, 200, 200, "crop") if err != nil { t.Fatalf("first Thumb: %v", err) } url2, err := f.Thumb(ctx, bucket, 200, 200, "crop") if err != nil { t.Fatalf("second Thumb: %v", err) } if n != 1 { t.Fatalf("resize calls = %d, want 1", n) } want := "/storage/uploads/abc/123/xyz/thumb_42_200_200_0_0_crop.jpg" if url1 != want || url2 != want { t.Fatalf("urls = %q %q, want %q", url1, url2, want) } } func TestFileThumbEncodeFailureDoesNotCache(t *testing.T) { ctx := t.Context() bucket := memblob.OpenBucket(nil) t.Cleanup(func() { _ = bucket.Close() }) f := &File{ID: 7, DiskName: "abc123xyz.jpg"} if err := bucket.WriteAll(ctx, BlobKey(f.DiskName), testJPEG(t), &blob.WriterOptions{ContentType: "image/jpeg"}); err != nil { t.Fatal(err) } orig := encodeImage encodeImage = func(w io.Writer, img image.Image, ext string) error { _, _ = w.Write([]byte("partial")) return errors.New("encode boom") } t.Cleanup(func() { encodeImage = orig }) if _, err := f.Thumb(ctx, bucket, 200, 200, "crop"); err == nil { t.Fatal("encode failure must surface") } thumbKey := PartitionDirectory(f.DiskName) + ThumbFilename(f.ID, 200, 200, 0, 0, "crop", "jpg") exists, err := bucket.Exists(ctx, thumbKey) if err != nil { t.Fatal(err) } if exists { t.Fatal("failed encode must not leave a cached thumb blob") } encodeImage = orig url, err := f.Thumb(ctx, bucket, 200, 200, "crop") if err != nil { t.Fatalf("retry after failed encode: %v", err) } want := "/storage/uploads/abc/123/xyz/thumb_7_200_200_0_0_crop.jpg" if url != want { t.Fatalf("retry url = %q, want %q", url, want) } } func testJPEG(t *testing.T) []byte { t.Helper() img := image.NewRGBA(image.Rect(0, 0, 8, 8)) for y := 0; y < 8; y++ { for x := 0; x < 8; x++ { img.Set(x, y, color.RGBA{R: 200, A: 255}) } } var buf bytes.Buffer if err := jpeg.Encode(&buf, img, &jpeg.Options{Quality: 90}); err != nil { t.Fatal(err) } return buf.Bytes() }