flamingo.me/flamingo-commerce/v3@v3.11.0/product/interfaces/controller/controller_test.go (about)

     1  package controller
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"net/http"
     7  	"net/url"
     8  	"testing"
     9  
    10  	"flamingo.me/flamingo/v3/framework/flamingo"
    11  	"flamingo.me/flamingo/v3/framework/web"
    12  	"github.com/stretchr/testify/assert"
    13  	"github.com/stretchr/testify/require"
    14  
    15  	"flamingo.me/flamingo-commerce/v3/product/application"
    16  	"flamingo.me/flamingo-commerce/v3/product/domain"
    17  )
    18  
    19  type (
    20  	MockProductService struct{}
    21  	routes             struct{}
    22  )
    23  
    24  // Routes definition for the reverse routing functionality needed
    25  func (r *routes) Routes(registry *web.RouterRegistry) {
    26  	_, _ = registry.Route("/", `product.view(marketplacecode?="test", name?="test", variantcode?="test")`)
    27  	registry.HandleGet("product.view", nil)
    28  }
    29  
    30  func getController() *View {
    31  	r := new(web.Router)
    32  	r.Inject(
    33  		&struct {
    34  			// base url configuration
    35  			Scheme      string `inject:"config:flamingo.router.scheme,optional"`
    36  			Host        string `inject:"config:flamingo.router.host,optional"`
    37  			Path        string `inject:"config:flamingo.router.path,optional"`
    38  			External    string `inject:"config:flamingo.router.external,optional"`
    39  			SessionName string `inject:"config:flamingo.session.name,optional"`
    40  		}{
    41  			Scheme: "http://",
    42  			Host:   "test",
    43  		},
    44  		nil,
    45  		nil,
    46  		func() []web.Filter {
    47  			return nil
    48  		},
    49  		func() []web.RoutesModule {
    50  			return []web.RoutesModule{&routes{}}
    51  		},
    52  		new(flamingo.NullLogger),
    53  		nil,
    54  		nil,
    55  	)
    56  	// create a new handler to initialize the router registry
    57  	r.Handler()
    58  
    59  	vc := &View{
    60  		ProductService: new(MockProductService),
    61  		Responder:      new(web.Responder),
    62  		URLService:     new(application.URLService).Inject(r, nil),
    63  		Template:       "product/product",
    64  		Router:         r,
    65  	}
    66  
    67  	return vc
    68  }
    69  
    70  func (mps *MockProductService) Get(_ context.Context, marketplacecode string) (domain.BasicProduct, error) {
    71  	if marketplacecode == "fail" {
    72  		return nil, errors.New("fail")
    73  	}
    74  	if marketplacecode == "not_found" {
    75  		return nil, domain.ProductNotFound{MarketplaceCode: "not_found"}
    76  	}
    77  	if marketplacecode == "simple" {
    78  		return domain.SimpleProduct{
    79  			BasicProductData: domain.BasicProductData{Title: "My Product Title", MarketPlaceCode: marketplacecode},
    80  		}, nil
    81  	}
    82  	return domain.ConfigurableProduct{
    83  		BasicProductData: domain.BasicProductData{Title: "My Configurable Product Title", MarketPlaceCode: marketplacecode},
    84  		Variants: []domain.Variant{
    85  			{
    86  				BasicProductData: domain.BasicProductData{Title: "My Variant Title", MarketPlaceCode: marketplacecode + "_1"},
    87  			},
    88  		},
    89  	}, nil
    90  }
    91  
    92  func TestViewController_Get(t *testing.T) {
    93  	vc := getController()
    94  
    95  	// call with correct name parameter and expect Rendering
    96  	ctx := context.Background()
    97  	r := web.CreateRequest(&http.Request{}, nil)
    98  	r.Request().URL = &url.URL{}
    99  	r.Params = web.RequestParams{
   100  		"marketplacecode": "simple",
   101  		"name":            "my-product-title",
   102  	}
   103  
   104  	result := vc.Get(ctx, r)
   105  	require.IsType(t, &web.RenderResponse{}, result)
   106  
   107  	renderResponse := result.(*web.RenderResponse)
   108  	assert.Equal(t, "product/product", renderResponse.Template)
   109  	require.IsType(t, productViewData{}, renderResponse.DataResponse.Data)
   110  	p, _ := new(MockProductService).Get(context.Background(), "simple")
   111  	assert.Equal(t, p, renderResponse.DataResponse.Data.(productViewData).Product)
   112  }
   113  
   114  func TestViewController_GetNotFound(t *testing.T) {
   115  	tests := []struct {
   116  		name            string
   117  		marketPlaceCode string
   118  		expectedStatus  uint
   119  	}{
   120  		{
   121  			name:            "error",
   122  			marketPlaceCode: "fail",
   123  			expectedStatus:  http.StatusInternalServerError,
   124  		},
   125  		{
   126  			name:            "not found",
   127  			marketPlaceCode: "not_found",
   128  			expectedStatus:  http.StatusNotFound,
   129  		},
   130  	}
   131  	for _, tt := range tests {
   132  		vc := getController()
   133  
   134  		// call with correct name parameter and expect Rendering
   135  		ctx := context.Background()
   136  		r := web.CreateRequest(&http.Request{}, nil)
   137  		r.Request().URL = &url.URL{}
   138  		r.Params = web.RequestParams{
   139  			"marketplacecode": tt.marketPlaceCode,
   140  			"name":            tt.marketPlaceCode,
   141  		}
   142  
   143  		result := vc.Get(ctx, r)
   144  		require.IsType(t, &web.ServerErrorResponse{}, result)
   145  		assert.Equal(t, int(tt.expectedStatus), int(result.(*web.ServerErrorResponse).Response.Status))
   146  	}
   147  }
   148  
   149  func TestViewController_ExpectRedirect(t *testing.T) {
   150  	tests := []struct {
   151  		name            string
   152  		marketPlaceCode string
   153  		productName     string
   154  		variantCode     string
   155  		expectedStatus  uint
   156  		expectedURL     string
   157  	}{
   158  		{
   159  			name:            "call simple with wrong name and expect redirect",
   160  			marketPlaceCode: "simple",
   161  			productName:     "testname",
   162  			expectedStatus:  http.StatusMovedPermanently,
   163  			expectedURL:     "/?marketplacecode=simple&name=my-product-title",
   164  		},
   165  		{
   166  			name:            "call configurable with wrong name and expect redirect",
   167  			marketPlaceCode: "configurable",
   168  			productName:     "testname",
   169  			expectedStatus:  http.StatusMovedPermanently,
   170  			expectedURL:     "/?marketplacecode=configurable&name=my-configurable-product-title",
   171  		},
   172  		{
   173  			name:            "call configurable_with_variant with wrong name and expect redirect",
   174  			marketPlaceCode: "configurable",
   175  			productName:     "testname",
   176  			variantCode:     "configurable_1",
   177  			expectedStatus:  http.StatusMovedPermanently,
   178  			expectedURL:     "/?marketplacecode=configurable&name=my-variant-title&variantcode=configurable_1",
   179  		},
   180  	}
   181  	for _, tt := range tests {
   182  		vc := getController()
   183  
   184  		r := web.CreateRequest(&http.Request{}, nil)
   185  		r.Request().URL = &url.URL{}
   186  		r.Params = web.RequestParams{
   187  			"marketplacecode": tt.marketPlaceCode,
   188  			"name":            tt.productName,
   189  		}
   190  		if tt.variantCode != "" {
   191  			r.Params["variantcode"] = "configurable_1"
   192  		}
   193  		result := vc.Get(context.Background(), r)
   194  		require.IsType(t, &web.URLRedirectResponse{}, result)
   195  		redirectResponse := result.(*web.URLRedirectResponse)
   196  		assert.Equal(t, int(tt.expectedStatus), int(redirectResponse.Response.Status))
   197  		assert.Equal(t, tt.expectedURL, redirectResponse.URL.String())
   198  	}
   199  }
   200  
   201  // This test is added to help better understand what variantSelection method is doing.
   202  // Unfortunately the assert library is unable to compare []variantSelection slices because
   203  // the order of values in some of the fields is not guaranteed (because how Go maps work).
   204  // For this reason we try to compare manually using helper functions.
   205  func TestViewController_variantSelection(t *testing.T) {
   206  	vc := getController()
   207  
   208  	testCases := []struct {
   209  		variantVariationAttributesSorting map[string][]string
   210  		variants                          []domain.Variant
   211  		activeVariant                     *domain.Variant
   212  
   213  		out variantSelection
   214  	}{
   215  		{
   216  			variantVariationAttributesSorting: map[string][]string{"color": {"red", "blue"}, "size": {"s", "m", "l"}},
   217  			variants: []domain.Variant{
   218  				{
   219  					BasicProductData: domain.BasicProductData{
   220  						Attributes: domain.Attributes{"color": {Label: "Red", CodeLabel: "Colour", RawValue: "red"}, "size": {Label: "S", CodeLabel: "Clothing Size", RawValue: "s"}},
   221  						Stock:      getStock(false, domain.StockLevelOutOfStock, 0),
   222  					},
   223  				},
   224  				{
   225  					BasicProductData: domain.BasicProductData{
   226  						Attributes: domain.Attributes{"color": {Label: "Red", CodeLabel: "Colour", RawValue: "red"}, "size": {Label: "M", CodeLabel: "Clothing Size", RawValue: "m"}},
   227  						Stock:      getStock(false, domain.StockLevelOutOfStock, 0),
   228  					},
   229  				},
   230  				{
   231  					BasicProductData: domain.BasicProductData{
   232  						Attributes: domain.Attributes{"color": {Label: "Red", CodeLabel: "Colour", RawValue: "red"}, "size": {Label: "L", CodeLabel: "Clothing Size", RawValue: "l"}},
   233  						Stock:      getStock(true, domain.StockLevelLowStock, 100),
   234  					},
   235  				},
   236  				{
   237  					BasicProductData: domain.BasicProductData{
   238  						Attributes: domain.Attributes{"color": {Label: "Blue", CodeLabel: "Colour", RawValue: "blue"}, "size": {Label: "S", CodeLabel: "Clothing Size", RawValue: "s"}},
   239  						Stock:      getStock(true, domain.StockLevelInStock, 999),
   240  					},
   241  				},
   242  				{
   243  					BasicProductData: domain.BasicProductData{
   244  						Attributes: domain.Attributes{"color": {Label: "Blue", CodeLabel: "Colour", RawValue: "blue"}, "size": {Label: "M", CodeLabel: "Clothing Size", RawValue: "m"}},
   245  						Stock:      getStock(false, domain.StockLevelOutOfStock, 0),
   246  					},
   247  				},
   248  			},
   249  			activeVariant: &domain.Variant{
   250  				BasicProductData: domain.BasicProductData{
   251  					Attributes: map[string]domain.Attribute{
   252  						"color": {Label: "Blue", CodeLabel: "Colour", RawValue: "blue"}, "size": {Label: "M", CodeLabel: "Clothing Size", RawValue: "m"},
   253  					},
   254  				},
   255  			},
   256  
   257  			out: variantSelection{
   258  				Attributes: []viewVariantAttribute{
   259  					{
   260  						Key:       "color",
   261  						Title:     "Color",
   262  						CodeLabel: "Colour",
   263  						Options: []viewVariantOption{
   264  							{
   265  								Key: "red", Title: "Red",
   266  								Combinations: map[string][]string{"size": {"s", "m", "l"}},
   267  							},
   268  							{
   269  								Key: "blue", Title: "Blue",
   270  								Combinations: map[string][]string{"size": {"s", "m"}},
   271  								Selected:     true,
   272  							},
   273  						},
   274  					},
   275  					{
   276  						Key:       "size",
   277  						Title:     "Size",
   278  						CodeLabel: "Clothing Size",
   279  						Options: []viewVariantOption{
   280  							{
   281  								Key: "s", Title: "S",
   282  								Combinations: map[string][]string{"color": {"red", "blue"}},
   283  							},
   284  							{
   285  								Key: "m", Title: "M",
   286  								Combinations: map[string][]string{"color": {"red", "blue"}},
   287  								Selected:     true,
   288  							},
   289  							{
   290  								Key: "l", Title: "L",
   291  								Combinations: map[string][]string{"color": {"red"}},
   292  							},
   293  						},
   294  					},
   295  				},
   296  				Variants: []viewVariant{
   297  					{
   298  						Attributes: map[string]string{"color": "red", "size": "s"},
   299  						URL:        "/?marketplacecode=&name=&variantcode=",
   300  						InStock:    false,
   301  					},
   302  					{
   303  						Attributes: map[string]string{"color": "red", "size": "m"},
   304  						URL:        "/?marketplacecode=&name=&variantcode=",
   305  						InStock:    false,
   306  					},
   307  					{
   308  						Attributes: map[string]string{"color": "red", "size": "l"},
   309  						URL:        "/?marketplacecode=&name=&variantcode=",
   310  						InStock:    true,
   311  					},
   312  					{
   313  						Attributes: map[string]string{"color": "blue", "size": "s"},
   314  						URL:        "/?marketplacecode=&name=&variantcode=",
   315  						InStock:    true,
   316  					},
   317  					{
   318  						Attributes: map[string]string{"color": "blue", "size": "m"},
   319  						URL:        "/?marketplacecode=&name=&variantcode=",
   320  						InStock:    false,
   321  					},
   322  				},
   323  			},
   324  		},
   325  		{
   326  			variantVariationAttributesSorting: map[string][]string{"volume": {"500", "1"}},
   327  			variants: []domain.Variant{
   328  				{
   329  					BasicProductData: domain.BasicProductData{
   330  						Attributes: domain.Attributes{"volume": {CodeLabel: "Volume", RawValue: "500", UnitCode: "MILLILITRE"}},
   331  						Stock:      getStock(true, domain.StockLevelInStock, 999),
   332  					},
   333  				},
   334  				{
   335  					BasicProductData: domain.BasicProductData{
   336  						Attributes: domain.Attributes{"volume": {CodeLabel: "Volume", RawValue: "1", UnitCode: "LITRE"}},
   337  						Stock:      getStock(true, domain.StockLevelInStock, 999),
   338  					},
   339  				},
   340  			},
   341  			activeVariant: &domain.Variant{
   342  				BasicProductData: domain.BasicProductData{
   343  					Attributes: map[string]domain.Attribute{
   344  						"volume": {CodeLabel: "Volume", RawValue: "500", UnitCode: "MILLILITRE"},
   345  					},
   346  				},
   347  			},
   348  
   349  			out: variantSelection{
   350  				Attributes: []viewVariantAttribute{
   351  					{
   352  						Key:       "volume",
   353  						Title:     "Volume",
   354  						CodeLabel: "Volume",
   355  						Options: []viewVariantOption{
   356  							{
   357  								Key:      "500",
   358  								Title:    "500",
   359  								Selected: true,
   360  								Unit:     "MILLILITRE",
   361  							},
   362  							{
   363  								Key:   "1",
   364  								Title: "1",
   365  								Unit:  "LITRE",
   366  							},
   367  						},
   368  					},
   369  				},
   370  				Variants: []viewVariant{
   371  					{
   372  						Attributes: map[string]string{"volume": "500"},
   373  						URL:        "/?marketplacecode=&name=&variantcode=",
   374  						InStock:    true,
   375  					},
   376  					{
   377  						Attributes: map[string]string{"volume": "1"},
   378  						URL:        "/?marketplacecode=&name=&variantcode=",
   379  						InStock:    true,
   380  					},
   381  				},
   382  			},
   383  		},
   384  	}
   385  
   386  	for _, tc := range testCases {
   387  
   388  		var variantVariationAttributes []string
   389  		for key := range tc.variantVariationAttributesSorting {
   390  			variantVariationAttributes = append(variantVariationAttributes, key)
   391  		}
   392  
   393  		configurableProduct := domain.ConfigurableProduct{
   394  			VariantVariationAttributes:        variantVariationAttributes,
   395  			VariantVariationAttributesSorting: tc.variantVariationAttributesSorting,
   396  			Variants:                          tc.variants,
   397  		}
   398  
   399  		vs := vc.variantSelection(configurableProduct, tc.activeVariant)
   400  
   401  		assert.Len(t, vs.Attributes, len(tc.out.Attributes))
   402  		assert.Len(t, vs.Variants, len(tc.out.Variants))
   403  
   404  		aEqual := true
   405  		for _, a1 := range vs.Attributes {
   406  			var aFound bool
   407  			for _, a2 := range tc.out.Attributes {
   408  				aFound = aFound || viewVariantAttributesEqual(t, a1, a2)
   409  			}
   410  			assert.True(t, aFound, "attributes not the same: attribute '%v' not found in '%v'", a1, tc.out.Attributes)
   411  			aEqual = aEqual && aFound
   412  		}
   413  		assert.True(t, aEqual, "attributes do not match")
   414  
   415  		vEqual := true
   416  		for _, v1 := range vs.Variants {
   417  			var vFound bool
   418  			for _, v2 := range tc.out.Variants {
   419  				vFound = vFound || viewVariantsEqual(t, v1, v2)
   420  			}
   421  			assert.True(t, vFound, "variants not the same: variant '%v' not found in '%v'", v1, tc.out.Variants)
   422  			vEqual = vEqual && vFound
   423  		}
   424  		assert.True(t, vEqual, "variants do not match")
   425  	}
   426  }
   427  
   428  func viewVariantAttributesEqual(t *testing.T, a1, a2 viewVariantAttribute) bool {
   429  	t.Helper()
   430  	if a1.Title != a2.Title {
   431  		return false
   432  	}
   433  	if a1.Key != a2.Key {
   434  		return false
   435  	}
   436  	if len(a1.Options) != len(a2.Options) {
   437  		return false
   438  	}
   439  	oEqual := true
   440  	for _, o1 := range a1.Options {
   441  		var oFound bool
   442  		for _, o2 := range a2.Options {
   443  			oFound = oFound || viewVariantOptionsEqual(t, o1, o2)
   444  		}
   445  		oEqual = oEqual && oFound
   446  	}
   447  	return oEqual
   448  }
   449  
   450  func viewVariantOptionsEqual(t *testing.T, o1, o2 viewVariantOption) bool {
   451  	t.Helper()
   452  	if o1.Title != o2.Title {
   453  		return false
   454  	}
   455  	if o1.Key != o2.Key {
   456  		return false
   457  	}
   458  	if o1.Selected != o2.Selected {
   459  		return false
   460  	}
   461  	if o1.Unit != o2.Unit {
   462  		return false
   463  	}
   464  	if len(o1.Combinations) != len(o2.Combinations) {
   465  		return false
   466  	}
   467  	for k1, v1 := range o1.Combinations {
   468  		v2, ok := o2.Combinations[k1]
   469  		if !ok {
   470  			return false
   471  		}
   472  		if !slicesEqual(t, v1, v2) {
   473  			return false
   474  		}
   475  	}
   476  	return true
   477  }
   478  
   479  func slicesEqual(t *testing.T, s1, s2 []string) bool {
   480  	t.Helper()
   481  	if len(s1) != len(s2) {
   482  		return false
   483  	}
   484  	for _, v1 := range s1 {
   485  		var sFound bool
   486  		for _, v2 := range s2 {
   487  			sFound = sFound || v1 == v2
   488  		}
   489  		if !sFound {
   490  			return false
   491  		}
   492  	}
   493  	return true
   494  }
   495  
   496  func viewVariantsEqual(t *testing.T, variant1, variant2 viewVariant) bool {
   497  	t.Helper()
   498  	if variant1.Title != variant2.Title {
   499  		return false
   500  	}
   501  	if variant1.URL != variant2.URL {
   502  		return false
   503  	}
   504  	if variant1.Marketplacecode != variant2.Marketplacecode {
   505  		return false
   506  	}
   507  	if variant1.InStock != variant2.InStock {
   508  		return false
   509  	}
   510  	if len(variant1.Attributes) != len(variant2.Attributes) {
   511  		return false
   512  	}
   513  	for k1, v1 := range variant1.Attributes {
   514  		v2, ok := variant2.Attributes[k1]
   515  		if !ok {
   516  			return false
   517  		}
   518  		if v1 != v2 {
   519  			return false
   520  		}
   521  	}
   522  	return true
   523  }
   524  
   525  func getStock(inStock bool, level string, amount int) []domain.Stock {
   526  	stock := make([]domain.Stock, 0)
   527  
   528  	stock = append(stock, domain.Stock{
   529  		Amount:       amount,
   530  		InStock:      inStock,
   531  		Level:        level,
   532  		DeliveryCode: "",
   533  	})
   534  
   535  	return stock
   536  }