juice-shop
352 строки · 14.9 Кб
1/*
2* Copyright (c) 2014-2024 Bjoern Kimminich & the OWASP Juice Shop contributors.
3* SPDX-License-Identifier: MIT
4*/
5
6import { TranslateModule, TranslateService } from '@ngx-translate/core'
7import { MatDividerModule } from '@angular/material/divider'
8import { HttpClientTestingModule } from '@angular/common/http/testing'
9import { type ComponentFixture, fakeAsync, TestBed, waitForAsync } from '@angular/core/testing'
10import { SearchResultComponent } from './search-result.component'
11import { ProductService } from '../Services/product.service'
12import { RouterTestingModule } from '@angular/router/testing'
13import { MatGridListModule } from '@angular/material/grid-list'
14import { MatCardModule } from '@angular/material/card'
15import { MatSnackBar } from '@angular/material/snack-bar'
16
17import { BrowserAnimationsModule } from '@angular/platform-browser/animations'
18import { MatTableModule } from '@angular/material/table'
19import { MatPaginatorModule } from '@angular/material/paginator'
20import { MatDialog, MatDialogModule } from '@angular/material/dialog'
21import { of } from 'rxjs'
22import { DomSanitizer } from '@angular/platform-browser'
23import { throwError } from 'rxjs/internal/observable/throwError'
24import { ProductDetailsComponent } from 'src/app/product-details/product-details.component'
25import { BasketService } from '../Services/basket.service'
26import { EventEmitter } from '@angular/core'
27import { ActivatedRoute } from '@angular/router'
28import { SocketIoService } from '../Services/socket-io.service'
29import { type Product } from '../Models/product.model'
30import { QuantityService } from '../Services/quantity.service'
31import { DeluxeGuard } from '../app.guard'
32
33class MockSocket {
34on (str: string, callback: any) {
35callback(str)
36}
37
38emit (a: any, b: any) {
39return null
40}
41}
42
43class MockActivatedRoute {
44snapshot = { queryParams: { q: '' } }
45
46setQueryParameter (arg: string) {
47this.snapshot.queryParams.q = arg
48}
49}
50
51describe('SearchResultComponent', () => {
52let component: SearchResultComponent
53let fixture: ComponentFixture<SearchResultComponent>
54let productService: any
55let basketService: any
56let translateService: any
57let activatedRoute: MockActivatedRoute
58let dialog: any
59let sanitizer: any
60let socketIoService: any
61let mockSocket: any
62let quantityService
63let deluxeGuard
64let snackBar: any
65
66beforeEach(waitForAsync(() => {
67dialog = jasmine.createSpyObj('MatDialog', ['open'])
68dialog.open.and.returnValue(null)
69quantityService = jasmine.createSpyObj('QuantityService', ['getAll'])
70quantityService.getAll.and.returnValue(of([]))
71snackBar = jasmine.createSpyObj('MatSnackBar', ['open'])
72productService = jasmine.createSpyObj('ProductService', ['search', 'get'])
73productService.search.and.returnValue(of([]))
74productService.get.and.returnValue(of({}))
75basketService = jasmine.createSpyObj('BasketService', ['find', 'get', 'put', 'save', 'updateNumberOfCartItems'])
76basketService.find.and.returnValue(of({ Products: [] }))
77basketService.get.and.returnValue(of({ quantinty: 1 }))
78basketService.put.and.returnValue(of({ ProductId: 1 }))
79basketService.save.and.returnValue(of({ ProductId: 1 }))
80basketService.updateNumberOfCartItems.and.returnValue(of({}))
81translateService = jasmine.createSpyObj('TranslateService', ['get'])
82translateService.get.and.returnValue(of({}))
83translateService.onLangChange = new EventEmitter()
84translateService.onTranslationChange = new EventEmitter()
85translateService.onDefaultLangChange = new EventEmitter()
86sanitizer = jasmine.createSpyObj('DomSanitizer', ['bypassSecurityTrustHtml', 'sanitize'])
87sanitizer.bypassSecurityTrustHtml.and.returnValue(of({}))
88sanitizer.sanitize.and.returnValue({})
89activatedRoute = new MockActivatedRoute()
90mockSocket = new MockSocket()
91socketIoService = jasmine.createSpyObj('SocketIoService', ['socket'])
92socketIoService.socket.and.returnValue(mockSocket)
93deluxeGuard = jasmine.createSpyObj('', ['isDeluxe'])
94deluxeGuard.isDeluxe.and.returnValue(of(false))
95
96TestBed.configureTestingModule({
97declarations: [SearchResultComponent],
98imports: [
99RouterTestingModule,
100HttpClientTestingModule,
101TranslateModule.forRoot(),
102BrowserAnimationsModule,
103MatTableModule,
104MatPaginatorModule,
105MatDialogModule,
106MatDividerModule,
107MatGridListModule,
108MatCardModule
109],
110providers: [
111{ provide: TranslateService, useValue: translateService },
112{ provide: MatDialog, useValue: dialog },
113{ provide: MatSnackBar, useValue: snackBar },
114{ provide: BasketService, useValue: basketService },
115{ provide: ProductService, useValue: productService },
116{ provide: DomSanitizer, useValue: sanitizer },
117{ provide: ActivatedRoute, useValue: activatedRoute },
118{ provide: SocketIoService, useValue: socketIoService },
119{ provide: QuantityService, useValue: quantityService },
120{ provide: DeluxeGuard, useValue: deluxeGuard }
121]
122})
123.compileComponents()
124}))
125
126beforeEach(() => {
127fixture = TestBed.createComponent(SearchResultComponent)
128component = fixture.componentInstance
129component.ngAfterViewInit()
130fixture.detectChanges()
131})
132
133it('should create', () => {
134expect(component).toBeTruthy()
135})
136
137it('should render product descriptions as trusted HTML', () => {
138productService.search.and.returnValue(of([{ description: '<script>alert("XSS")</script>' }]))
139component.ngAfterViewInit()
140fixture.detectChanges()
141expect(sanitizer.bypassSecurityTrustHtml).toHaveBeenCalledWith('<script>alert("XSS")</script>')
142})
143
144it('should hold no products when product search API call fails', () => {
145productService.search.and.returnValue(throwError('Error'))
146component.ngAfterViewInit()
147fixture.detectChanges()
148expect(component.tableData).toEqual([])
149})
150
151it('should log error from product search API call directly to browser console', fakeAsync(() => {
152productService.search.and.returnValue(throwError('Error'))
153console.log = jasmine.createSpy('log')
154component.ngAfterViewInit()
155fixture.detectChanges()
156expect(console.log).toHaveBeenCalledWith('Error')
157}))
158
159it('should hold no products when quantity getAll API call fails', () => {
160quantityService.getAll.and.returnValue(throwError('Error'))
161component.ngAfterViewInit()
162fixture.detectChanges()
163expect(component.tableData).toEqual([])
164})
165
166it('should log error from quantity getAll API call directly to browser console', fakeAsync(() => {
167quantityService.getAll.and.returnValue(throwError('Error'))
168console.log = jasmine.createSpy('log')
169component.ngAfterViewInit()
170fixture.detectChanges()
171expect(console.log).toHaveBeenCalledWith('Error')
172}))
173
174it('should notify socket if search query includes DOM XSS payload while filtering table', () => {
175activatedRoute.setQueryParameter('<iframe src="javascript:alert(`xss`)"> Payload')
176spyOn(mockSocket, 'emit')
177component.filterTable()
178expect(mockSocket.emit.calls.mostRecent().args[0]).toBe('verifyLocalXssChallenge')
179expect(mockSocket.emit.calls.mostRecent().args[1]).toBe(activatedRoute.snapshot.queryParams.q)
180})
181
182it('should trim the queryparameter while filtering the datasource', () => {
183activatedRoute.setQueryParameter(' Product Search ')
184component.filterTable()
185expect(component.dataSource.filter).toEqual('product search')
186})
187
188it('should pass the search query as trusted HTML', () => {
189activatedRoute.setQueryParameter('<script>scripttag</script>')
190component.filterTable()
191expect(sanitizer.bypassSecurityTrustHtml).toHaveBeenCalledWith('<script>scripttag</script>')
192})
193
194it('should open a modal dialog with product details', () => {
195component.showDetail({ id: 42 } as Product)
196expect(dialog.open).toHaveBeenCalledWith(ProductDetailsComponent, {
197width: '500px',
198height: 'max-content',
199data: {
200productData: { id: 42 }
201}
202})
203})
204
205it('should add new product to basket', () => {
206basketService.find.and.returnValue(of({ Products: [] }))
207productService.search.and.returnValue(of([]))
208basketService.save.and.returnValue(of({ ProductId: 1 }))
209productService.get.and.returnValue(of({ name: 'Cherry Juice' }))
210sessionStorage.setItem('bid', '4711')
211component.addToBasket(1)
212expect(basketService.find).toHaveBeenCalled()
213expect(basketService.save).toHaveBeenCalled()
214expect(productService.get).toHaveBeenCalled()
215expect(translateService.get).toHaveBeenCalledWith('BASKET_ADD_PRODUCT', { product: 'Cherry Juice' })
216})
217
218it('should translate BASKET_ADD_PRODUCT message', () => {
219basketService.find.and.returnValue(of({ Products: [] }))
220productService.search.and.returnValue(of([]))
221basketService.save.and.returnValue(of({ ProductId: 1 }))
222productService.get.and.returnValue(of({ name: 'Cherry Juice' }))
223translateService.get.and.returnValue(of('Translation of BASKET_ADD_PRODUCT'))
224sessionStorage.setItem('bid', '4711')
225component.addToBasket(1)
226expect(basketService.find).toHaveBeenCalled()
227expect(basketService.save).toHaveBeenCalled()
228expect(productService.get).toHaveBeenCalled()
229expect(snackBar.open).toHaveBeenCalled()
230})
231
232it('should add similar product to basket', () => {
233basketService.find.and.returnValue(of({ Products: [{ id: 1 }, { id: 2, name: 'Tomato Juice', BasketItem: { id: 42 } }] }))
234basketService.get.and.returnValue(of({ id: 42, quantity: 5 }))
235basketService.put.and.returnValue(of({ ProductId: 2 }))
236productService.get.and.returnValue(of({ name: 'Tomato Juice' }))
237translateService.get.and.returnValue(of(undefined))
238sessionStorage.setItem('bid', '4711')
239component.addToBasket(2)
240expect(basketService.find).toHaveBeenCalled()
241expect(basketService.get).toHaveBeenCalled()
242expect(basketService.put).toHaveBeenCalled()
243expect(productService.get).toHaveBeenCalled()
244expect(translateService.get).toHaveBeenCalledWith('BASKET_ADD_SAME_PRODUCT', { product: 'Tomato Juice' })
245})
246
247it('should translate BASKET_ADD_SAME_PRODUCT message', () => {
248basketService.find.and.returnValue(of({ Products: [{ id: 1 }, { id: 2, name: 'Tomato Juice', BasketItem: { id: 42 } }] }))
249basketService.get.and.returnValue(of({ id: 42, quantity: 5 }))
250basketService.put.and.returnValue(of({ ProductId: 2 }))
251productService.get.and.returnValue(of({ name: 'Tomato Juice' }))
252translateService.get.and.returnValue(of('Translation of BASKET_ADD_SAME_PRODUCT'))
253sessionStorage.setItem('bid', '4711')
254component.addToBasket(2)
255expect(basketService.find).toHaveBeenCalled()
256expect(basketService.get).toHaveBeenCalled()
257expect(basketService.put).toHaveBeenCalled()
258expect(productService.get).toHaveBeenCalled()
259})
260
261it('should not add anything to basket on error retrieving basket', fakeAsync(() => {
262basketService.find.and.returnValue(throwError('Error'))
263sessionStorage.setItem('bid', '815')
264component.addToBasket(undefined)
265expect(snackBar.open).not.toHaveBeenCalled()
266}))
267
268it('should log errors retrieving basket directly to browser console', fakeAsync(() => {
269basketService.find.and.returnValue(throwError('Error'))
270sessionStorage.setItem('bid', '815')
271console.log = jasmine.createSpy('log')
272component.addToBasket(2)
273expect(console.log).toHaveBeenCalledWith('Error')
274}))
275
276it('should not add anything to basket on error retrieving existing basket item', fakeAsync(() => {
277basketService.find.and.returnValue(of({ Products: [{ id: 1 }, { id: 2, name: 'Tomato Juice', BasketItem: { id: 42 } }] }))
278basketService.get.and.returnValue(throwError('Error'))
279sessionStorage.setItem('bid', '4711')
280component.addToBasket(2)
281expect(snackBar.open).not.toHaveBeenCalled()
282}))
283
284it('should log errors retrieving basket item directly to browser console', fakeAsync(() => {
285basketService.find.and.returnValue(of({ Products: [{ id: 1 }, { id: 2, name: 'Tomato Juice', BasketItem: { id: 42 } }] }))
286basketService.get.and.returnValue(throwError('Error'))
287sessionStorage.setItem('bid', '4711')
288console.log = jasmine.createSpy('log')
289component.addToBasket(2)
290expect(console.log).toHaveBeenCalledWith('Error')
291}))
292
293it('should log errors updating basket directly to browser console', fakeAsync(() => {
294basketService.find.and.returnValue(of({ Products: [{ id: 1 }, { id: 2, name: 'Tomato Juice', BasketItem: { id: 42 } }] }))
295basketService.put.and.returnValue(throwError('Error'))
296sessionStorage.setItem('bid', '4711')
297console.log = jasmine.createSpy('log')
298component.addToBasket(2)
299expect(console.log).toHaveBeenCalledWith('Error')
300}))
301
302it('should not add anything to basket on error retrieving product associated with basket item', fakeAsync(() => {
303basketService.find.and.returnValue(of({ Products: [{ id: 1 }, { id: 2, name: 'Tomato Juice', BasketItem: { id: 42 } }] }))
304productService.get.and.returnValue(throwError('Error'))
305sessionStorage.setItem('bid', '4711')
306component.addToBasket(2)
307expect(snackBar.open).not.toHaveBeenCalled()
308}))
309
310it('should log errors retrieving product associated with basket item directly to browser console', fakeAsync(() => {
311basketService.find.and.returnValue(of({ Products: [{ id: 1 }, { id: 2, name: 'Tomato Juice', BasketItem: { id: 42 } }] }))
312productService.get.and.returnValue(throwError('Error'))
313sessionStorage.setItem('bid', '4711')
314console.log = jasmine.createSpy('log')
315component.addToBasket(2)
316expect(console.log).toHaveBeenCalledWith('Error')
317}))
318
319it('should not add anything on error creating new basket item', fakeAsync(() => {
320basketService.find.and.returnValue(of({ Products: [] }))
321basketService.save.and.returnValue(throwError('Error'))
322sessionStorage.setItem('bid', '4711')
323component.addToBasket(2)
324expect(snackBar.open).toHaveBeenCalled()
325}))
326
327it('should log errors creating new basket item directly to browser console', fakeAsync(() => {
328basketService.find.and.returnValue(of({ Products: [] }))
329basketService.save.and.returnValue(throwError('Error'))
330console.log = jasmine.createSpy('log')
331sessionStorage.setItem('bid', '4711')
332component.addToBasket(2)
333expect(snackBar.open).toHaveBeenCalled()
334}))
335
336it('should not add anything on error retrieving product after creating new basket item', fakeAsync(() => {
337basketService.find.and.returnValue(of({ Products: [] }))
338productService.get.and.returnValue(throwError('Error'))
339sessionStorage.setItem('bid', '4711')
340component.addToBasket(2)
341expect(snackBar.open).not.toHaveBeenCalled()
342}))
343
344it('should log errors retrieving product after creating new basket item directly to browser console', fakeAsync(() => {
345basketService.find.and.returnValue(of({ Products: [] }))
346productService.get.and.returnValue(throwError('Error'))
347console.log = jasmine.createSpy('log')
348sessionStorage.setItem('bid', '4711')
349component.addToBasket(2)
350expect(console.log).toHaveBeenCalledWith('Error')
351}))
352})
353