package shunyunbao import ( "bytes" "context" "errors" "image" "image/color" "image/jpeg" "io" "net/http" "net/http/httptest" "strings" "testing" "cmroubao/backend-api/internal/domain" "cmroubao/backend-api/internal/platform/assetstore" ) func TestSessionManagerFetchProductImageUsesFixedAuthenticatedEndpoint( t *testing.T, ) { var requestPath, rawQuery, accept string manager, closeServer := imageSessionManager( t, func(writer http.ResponseWriter, request *http.Request) { requestPath = request.URL.Path rawQuery = request.URL.RawQuery accept = request.Header.Get("Accept") if cookie, err := request.Cookie("authenticated"); err != nil || cookie.Value != "yes" { t.Fatalf("image cookie = %v, %v", cookie, err) } writer.Header().Set("Content-Type", "image/png") writer.Header().Set("Content-Length", "4") _, _ = writer.Write([]byte("png!")) }, ) defer closeServer() image, err := manager.FetchProductImage(context.Background(), "190") if err != nil { t.Fatalf("FetchProductImage() error = %v", err) } defer image.Content.Close() content, err := io.ReadAll(image.Content) if err != nil { t.Fatalf("read image: %v", err) } if requestPath != ProductImagePath || rawQuery != "id=190" || image.MediaType != "image/png" || string(content) != "png!" || !strings.Contains(accept, "image/jpeg") { t.Fatalf( "image request/result = %q / %q / %q / %q / %q", requestPath, rawQuery, accept, image.MediaType, content, ) } } func TestSessionManagerFetchProductImageSniffsMissingContentType( t *testing.T, ) { body := []byte{0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 'J', 'F', 'I', 'F'} manager, closeServer := imageSessionManager( t, func(writer http.ResponseWriter, _ *http.Request) { writer.WriteHeader(http.StatusOK) _, _ = writer.Write(body) }, ) defer closeServer() image, err := manager.FetchProductImage(context.Background(), "190") if err != nil { t.Fatalf("FetchProductImage() error = %v", err) } content, err := io.ReadAll(image.Content) if closeErr := image.Content.Close(); closeErr != nil { t.Fatalf("close image content: %v", closeErr) } if err != nil || image.MediaType != "image/jpeg" || !bytes.Equal(content, body) { t.Fatalf( "image = %q / %x / %v", image.MediaType, content, err, ) } } func TestMissingContentTypeImageCanBeNormalizedAndStored(t *testing.T) { var encoded bytes.Buffer source := image.NewRGBA(image.Rect(0, 0, 2, 2)) source.Set(0, 0, color.RGBA{R: 255, A: 255}) source.Set(1, 0, color.RGBA{G: 255, A: 255}) source.Set(0, 1, color.RGBA{B: 255, A: 255}) source.Set(1, 1, color.RGBA{R: 255, G: 255, A: 255}) if err := jpeg.Encode( &encoded, source, &jpeg.Options{Quality: 90}, ); err != nil { t.Fatalf("jpeg.Encode() error = %v", err) } manager, closeServer := imageSessionManager( t, func(writer http.ResponseWriter, _ *http.Request) { writer.WriteHeader(http.StatusOK) _, _ = writer.Write(encoded.Bytes()) }, ) defer closeServer() sourceImage, err := manager.FetchProductImage( context.Background(), "190", ) if err != nil { t.Fatalf("FetchProductImage() error = %v", err) } defer sourceImage.Content.Close() files, err := assetstore.New(t.TempDir()) if err != nil { t.Fatalf("assetstore.New() error = %v", err) } stored, err := files.Put( context.Background(), "00000000-0000-4000-8000-000000000190", sourceImage.MediaType, sourceImage.Content, ) if err != nil { t.Fatalf("Put() error = %v", err) } if sourceImage.MediaType != "image/jpeg" || stored.MediaType != domain.NormalizedImageMediaType || stored.SizeBytes <= 0 || stored.StorageKey == "" { t.Fatalf("stored image = %+v / %q", stored, sourceImage.MediaType) } content, err := files.Open(context.Background(), stored.StorageKey) if err != nil { t.Fatalf("Open() error = %v", err) } defer content.Close() _, format, err := image.Decode(content) if err != nil || format != "jpeg" { t.Fatalf("stored format = %q, %v", format, err) } } func TestProductImageContentSniffsGenericBinaryAndPreservesStream( t *testing.T, ) { tests := []struct { name string contentType string body []byte wantType string }{ { name: "missing JPEG", body: []byte{0xff, 0xd8, 0xff, 0xe1, 'j', 'p', 'e', 'g'}, wantType: "image/jpeg", }, { name: "generic PNG", contentType: "application/octet-stream", body: []byte{ 0x89, 'P', 'N', 'G', '\r', '\n', 0x1a, '\n', 'p', 'n', 'g', }, wantType: "image/png", }, { name: "generic WebP", contentType: "APPLICATION/OCTET-STREAM", body: []byte{ 'R', 'I', 'F', 'F', 0x04, 0x00, 0x00, 0x00, 'W', 'E', 'B', 'P', }, wantType: "image/webp", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { body := &trackingImageBody{ Reader: bytes.NewReader(test.body), } mediaType, content, err := productImageContent( test.contentType, body, ) if err != nil { t.Fatalf("productImageContent() error = %v", err) } got, err := io.ReadAll(content) if err != nil || mediaType != test.wantType || !bytes.Equal(got, test.body) { t.Fatalf( "content = %q / %x / %v", mediaType, got, err, ) } if err := content.Close(); err != nil { t.Fatalf("Close() error = %v", err) } if !body.closed { t.Fatal("source body was not closed") } }) } } func TestProductImageContentRejectsUntrustedFallbacks(t *testing.T) { tests := []struct { name string contentType string body []byte }{ { name: "explicit JSON overrides image bytes", contentType: "application/json", body: []byte{0xff, 0xd8, 0xff}, }, { name: "missing JSON", body: []byte(`{"status":false}`), }, { name: "generic HTML", contentType: "application/octet-stream", body: []byte("