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("error"), }, { name: "generic SVG", contentType: "application/octet-stream", body: []byte(``), }, { name: "empty body", }, { name: "malformed media type", contentType: "image/jpeg; invalid", body: []byte{0xff, 0xd8, 0xff}, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { body := &trackingImageBody{ Reader: bytes.NewReader(test.body), } _, content, err := productImageContent( test.contentType, body, ) if err == nil || content != nil { t.Fatalf( "productImageContent() = %#v, %v", content, err, ) } if body.closed { t.Fatal("helper unexpectedly closed source body") } }) } } func TestSessionManagerFetchProductImageClassifiesFailures(t *testing.T) { manager, closeServer := imageSessionManager( t, func(writer http.ResponseWriter, request *http.Request) { switch request.URL.Query().Get("id") { case "190": writer.WriteHeader(http.StatusNotFound) case "191": writer.Header().Set("Content-Type", "application/json") _, _ = writer.Write([]byte(`{"status":false}`)) case "192": writer.Header().Set("Location", ProductImagePath+"?id=190") writer.WriteHeader(http.StatusFound) case "193": writer.Header().Set( "Content-Type", "image/jpeg", ) writer.Header().Set( "Content-Length", "20971521", ) default: writer.WriteHeader(http.StatusUnauthorized) } }, ) defer closeServer() tests := []struct { ref string want error }{ {"190", domain.ErrFreightImageNotFound}, {"191", domain.ErrFreightImageInvalid}, {"192", domain.ErrFreightSourceUnavailable}, {"193", domain.ErrFreightImageInvalid}, {"194", domain.ErrFreightSourceSessionNeeded}, } for _, test := range tests { image, err := manager.FetchProductImage( context.Background(), test.ref, ) if image.Content != nil { image.Content.Close() } if !errors.Is(err, test.want) { t.Fatalf("FetchProductImage(%q) error = %v", test.ref, err) } } if _, err := manager.FetchProductImage( context.Background(), "195", ); !errors.Is(err, domain.ErrFreightSourceSessionNeeded) { t.Fatalf("request after unauthorized error = %v", err) } } type trackingImageBody struct { *bytes.Reader closed bool } func (body *trackingImageBody) Close() error { body.closed = true return nil } func TestSessionManagerFetchProductImageRejectsUntrustedReferences( t *testing.T, ) { manager := testSessionManager(t, "http://127.0.0.1:1", "user", "password") for _, value := range []string{ "", "0", "0190", "-1", "1.5", "https://invalid.example/image", } { if _, err := manager.FetchProductImage( context.Background(), value, ); !errors.Is(err, domain.ErrFreightImageInvalid) { t.Fatalf("FetchProductImage(%q) error = %v", value, err) } } } func imageSessionManager( t *testing.T, imageHandler http.HandlerFunc, ) (*SessionManager, func()) { t.Helper() server := httptest.NewServer(http.HandlerFunc(func( writer http.ResponseWriter, request *http.Request, ) { switch request.URL.Path { case CaptchaPath: http.SetCookie( writer, &http.Cookie{Name: "captcha", Value: "ready", Path: "/"}, ) writer.Header().Set("Content-Type", "image/png") _, _ = writer.Write([]byte("captcha")) case LoginPath: http.SetCookie( writer, &http.Cookie{ Name: "authenticated", Value: "yes", Path: "/", }, ) _, _ = writer.Write( []byte( `{"status":true,"data":{"user":{"id":1,"username":"test-user"}}}`, ), ) case UserPath: _, _ = writer.Write( []byte( `{"status":true,"data":{"id":1,"username":"test-user"}}`, ), ) case ProductImagePath: imageHandler(writer, request) default: writer.WriteHeader(http.StatusNotFound) } })) manager := testSessionManager( t, server.URL, "test-user", "test-password", ) loginForSource(t, manager) return manager, server.Close }