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 }