diff --git a/src/app/components/search-bar/search-bar.spec.ts b/src/app/components/search-bar/search-bar.spec.ts new file mode 100644 index 0000000..b115a14 --- /dev/null +++ b/src/app/components/search-bar/search-bar.spec.ts @@ -0,0 +1,83 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { SearchBarComponent } from './search-bar'; +import { SearchService } from '../../services/search.service'; +import { Router } from '@angular/router'; +import { of } from 'rxjs'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; + +describe('SearchBarComponent', () => { + let component: SearchBarComponent; + let fixture: ComponentFixture; + let mockSearchService: any; + let mockRouter: any; + + beforeEach(async () => { + mockSearchService = { + getRecentSearches: jasmine.createSpy('getRecentSearches').and.returnValue([]), + search: jasmine.createSpy('search').and.returnValue(of([])), + addRecentSearch: jasmine.createSpy('addRecentSearch'), + clearRecentSearches: jasmine.createSpy('clearRecentSearches') + }; + + mockRouter = { + navigate: jasmine.createSpy('navigate') + }; + + await TestBed.configureTestingModule({ + imports: [SearchBarComponent, NoopAnimationsModule], + providers: [ + { provide: SearchService, useValue: mockSearchService }, + { provide: Router, useValue: mockRouter } + ] + }).compileComponents(); + + fixture = TestBed.createComponent(SearchBarComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + describe('highlightMatch', () => { + it('should wrap matches in tags', () => { + component.query = 'test'; + const result = component.highlightMatch('This is a test string'); + expect(result).toBe('This is a test string'); + }); + + it('should be case insensitive', () => { + component.query = 'TEST'; + const result = component.highlightMatch('This is a test string'); + expect(result).toBe('This is a test string'); + }); + + it('should return original text if no query', () => { + component.query = ''; + const result = component.highlightMatch('Some text'); + expect(result).toBe('Some text'); + }); + + it('should escape HTML in the input text', () => { + component.query = 'test'; + const maliciousInput = 'Normal text test'; + const result = component.highlightMatch(maliciousInput); + expect(result).not.toContain('test'); + }); + + it('should handle regex special characters in query', () => { + component.query = '('; + const result = component.highlightMatch('Some text with ('); + expect(result).toBe('Some text with ('); + }); + + it('should handle HTML special characters in query', () => { + component.query = '<'; + const result = component.highlightMatch('Match < this'); + expect(result).toBe('Match < this'); + }); + }); +}); diff --git a/src/app/components/search-bar/search-bar.ts b/src/app/components/search-bar/search-bar.ts index 26153ef..da1d9c1 100644 --- a/src/app/components/search-bar/search-bar.ts +++ b/src/app/components/search-bar/search-bar.ts @@ -120,7 +120,25 @@ export class SearchBarComponent implements OnInit, OnDestroy { highlightMatch(text: string): string { if (!this.query || !text) return text; - const regex = new RegExp(`(${this.query})`, 'gi'); - return text.replace(regex, '$1'); + + // First escape HTML in the input text to prevent XSS + const escapedText = text + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); + + // Also escape HTML and regex special characters in the query to match correctly + const escapedQuery = this.query + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') + .replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + + const regex = new RegExp(`(${escapedQuery})`, 'gi'); + return escapedText.replace(regex, '$1'); } }