mirror of
				https://github.com/lingble/chatwoot.git
				synced 2025-10-31 02:57:57 +00:00 
			
		
		
		
	 2ba4780bda
			
		
	
	2ba4780bda
	
	
	
		
			
			## Summary - add allowed domains controls in the web widget configuration page. <img width="1064" height="699" alt="Screenshot 2025-09-23 at 8 52 21 PM" src="https://github.com/user-attachments/assets/8afd60b6-c81d-4f52-9cbe-07e70ad003d2" /> fixes: https://linear.app/chatwoot/issue/CW-5661/add-the-options-for-configure-allowed-domains-for-web-widget --------- Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com> Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Co-authored-by: iamsivin <iamsivin@gmail.com>
		
			
				
	
	
		
			351 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			351 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| import {
 | |
|   frontendURL,
 | |
|   conversationUrl,
 | |
|   isValidURL,
 | |
|   conversationListPageURL,
 | |
|   getArticleSearchURL,
 | |
|   hasValidAvatarUrl,
 | |
|   timeStampAppendedURL,
 | |
|   getHostNameFromURL,
 | |
|   extractFilenameFromUrl,
 | |
|   sanitizeAllowedDomains,
 | |
| } from '../URLHelper';
 | |
| 
 | |
| describe('#URL Helpers', () => {
 | |
|   describe('conversationListPageURL', () => {
 | |
|     it('should return url to dashboard', () => {
 | |
|       expect(conversationListPageURL({ accountId: 1 })).toBe(
 | |
|         '/app/accounts/1/dashboard'
 | |
|       );
 | |
|     });
 | |
|     it('should return url to inbox', () => {
 | |
|       expect(conversationListPageURL({ accountId: 1, inboxId: 1 })).toBe(
 | |
|         '/app/accounts/1/inbox/1'
 | |
|       );
 | |
|     });
 | |
|     it('should return url to label', () => {
 | |
|       expect(conversationListPageURL({ accountId: 1, label: 'support' })).toBe(
 | |
|         '/app/accounts/1/label/support'
 | |
|       );
 | |
|     });
 | |
| 
 | |
|     it('should return url to team', () => {
 | |
|       expect(conversationListPageURL({ accountId: 1, teamId: 1 })).toBe(
 | |
|         '/app/accounts/1/team/1'
 | |
|       );
 | |
|     });
 | |
| 
 | |
|     it('should return url to custom view', () => {
 | |
|       expect(conversationListPageURL({ accountId: 1, customViewId: 1 })).toBe(
 | |
|         '/app/accounts/1/custom_view/1'
 | |
|       );
 | |
|     });
 | |
|   });
 | |
|   describe('conversationUrl', () => {
 | |
|     it('should return direct conversation URL if activeInbox is nil', () => {
 | |
|       expect(conversationUrl({ accountId: 1, id: 1 })).toBe(
 | |
|         'accounts/1/conversations/1'
 | |
|       );
 | |
|     });
 | |
|     it('should return inbox conversation URL if activeInbox is not nil', () => {
 | |
|       expect(conversationUrl({ accountId: 1, id: 1, activeInbox: 2 })).toBe(
 | |
|         'accounts/1/inbox/2/conversations/1'
 | |
|       );
 | |
|     });
 | |
|     it('should return correct conversation URL if label is active', () => {
 | |
|       expect(
 | |
|         conversationUrl({ accountId: 1, label: 'customer-support', id: 1 })
 | |
|       ).toBe('accounts/1/label/customer-support/conversations/1');
 | |
|     });
 | |
|     it('should return correct conversation URL if team Id is available', () => {
 | |
|       expect(conversationUrl({ accountId: 1, teamId: 1, id: 1 })).toBe(
 | |
|         'accounts/1/team/1/conversations/1'
 | |
|       );
 | |
|     });
 | |
|   });
 | |
| 
 | |
|   describe('frontendURL', () => {
 | |
|     it('should return url without params if params passed is nil', () => {
 | |
|       expect(frontendURL('main', null)).toBe('/app/main');
 | |
|     });
 | |
|     it('should return url without params if params passed is not nil', () => {
 | |
|       expect(frontendURL('main', { ping: 'pong' })).toBe('/app/main?ping=pong');
 | |
|     });
 | |
|   });
 | |
| 
 | |
|   describe('isValidURL', () => {
 | |
|     it('should return true if valid url is passed', () => {
 | |
|       expect(isValidURL('https://chatwoot.com')).toBe(true);
 | |
|     });
 | |
|     it('should return false if invalid url is passed', () => {
 | |
|       expect(isValidURL('alert.window')).toBe(false);
 | |
|     });
 | |
|   });
 | |
| 
 | |
|   describe('getArticleSearchURL', () => {
 | |
|     it('should generate a basic URL without optional parameters', () => {
 | |
|       const url = getArticleSearchURL({
 | |
|         portalSlug: 'news',
 | |
|         pageNumber: 1,
 | |
|         locale: 'en',
 | |
|         host: 'myurl.com',
 | |
|       });
 | |
|       expect(url).toBe('myurl.com/news/articles?page=1&locale=en');
 | |
|     });
 | |
| 
 | |
|     it('should include status parameter if provided', () => {
 | |
|       const url = getArticleSearchURL({
 | |
|         portalSlug: 'news',
 | |
|         pageNumber: 1,
 | |
|         locale: 'en',
 | |
|         status: 'published',
 | |
|         host: 'myurl.com',
 | |
|       });
 | |
|       expect(url).toBe(
 | |
|         'myurl.com/news/articles?page=1&locale=en&status=published'
 | |
|       );
 | |
|     });
 | |
| 
 | |
|     it('should include author_id parameter if provided', () => {
 | |
|       const url = getArticleSearchURL({
 | |
|         portalSlug: 'news',
 | |
|         pageNumber: 1,
 | |
|         locale: 'en',
 | |
|         authorId: 123,
 | |
|         host: 'myurl.com',
 | |
|       });
 | |
|       expect(url).toBe(
 | |
|         'myurl.com/news/articles?page=1&locale=en&author_id=123'
 | |
|       );
 | |
|     });
 | |
| 
 | |
|     it('should include category_slug parameter if provided', () => {
 | |
|       const url = getArticleSearchURL({
 | |
|         portalSlug: 'news',
 | |
|         pageNumber: 1,
 | |
|         locale: 'en',
 | |
|         categorySlug: 'technology',
 | |
|         host: 'myurl.com',
 | |
|       });
 | |
|       expect(url).toBe(
 | |
|         'myurl.com/news/articles?page=1&locale=en&category_slug=technology'
 | |
|       );
 | |
|     });
 | |
| 
 | |
|     it('should include sort parameter if provided', () => {
 | |
|       const url = getArticleSearchURL({
 | |
|         portalSlug: 'news',
 | |
|         pageNumber: 1,
 | |
|         locale: 'en',
 | |
|         sort: 'views',
 | |
|         host: 'myurl.com',
 | |
|       });
 | |
|       expect(url).toBe('myurl.com/news/articles?page=1&locale=en&sort=views');
 | |
|     });
 | |
| 
 | |
|     it('should handle multiple optional parameters', () => {
 | |
|       const url = getArticleSearchURL({
 | |
|         portalSlug: 'news',
 | |
|         pageNumber: 1,
 | |
|         locale: 'en',
 | |
|         status: 'draft',
 | |
|         authorId: 456,
 | |
|         categorySlug: 'science',
 | |
|         sort: 'views',
 | |
|         host: 'myurl.com',
 | |
|       });
 | |
|       expect(url).toBe(
 | |
|         'myurl.com/news/articles?page=1&locale=en&status=draft&author_id=456&category_slug=science&sort=views'
 | |
|       );
 | |
|     });
 | |
| 
 | |
|     it('should handle missing optional parameters gracefully', () => {
 | |
|       const url = getArticleSearchURL({
 | |
|         portalSlug: 'news',
 | |
|         pageNumber: 1,
 | |
|         locale: 'en',
 | |
|         host: 'myurl.com',
 | |
|       });
 | |
|       expect(url).toBe('myurl.com/news/articles?page=1&locale=en');
 | |
|     });
 | |
|   });
 | |
| 
 | |
|   describe('hasValidAvatarUrl', () => {
 | |
|     test('should return true for valid non-Gravatar URL', () => {
 | |
|       expect(hasValidAvatarUrl('https://chatwoot.com/avatar.jpg')).toBe(true);
 | |
|     });
 | |
| 
 | |
|     test('should return false for a Gravatar URL (www.gravatar.com)', () => {
 | |
|       expect(hasValidAvatarUrl('https://www.gravatar.com/avatar.jpg')).toBe(
 | |
|         false
 | |
|       );
 | |
|     });
 | |
| 
 | |
|     test('should return false for a Gravatar URL (gravatar)', () => {
 | |
|       expect(hasValidAvatarUrl('https://gravatar/avatar.jpg')).toBe(false);
 | |
|     });
 | |
| 
 | |
|     test('should handle invalid URL', () => {
 | |
|       expect(hasValidAvatarUrl('invalid-url')).toBe(false); // or expect an error, depending on function design
 | |
|     });
 | |
| 
 | |
|     test('should return false for empty or undefined URL', () => {
 | |
|       expect(hasValidAvatarUrl('')).toBe(false);
 | |
|       expect(hasValidAvatarUrl()).toBe(false);
 | |
|     });
 | |
|   });
 | |
| 
 | |
|   describe('timeStampAppendedURL', () => {
 | |
|     const FIXED_TIMESTAMP = 1234567890000;
 | |
| 
 | |
|     beforeEach(() => {
 | |
|       vi.spyOn(Date, 'now').mockImplementation(() => FIXED_TIMESTAMP);
 | |
|     });
 | |
| 
 | |
|     afterEach(() => {
 | |
|       vi.restoreAllMocks();
 | |
|     });
 | |
| 
 | |
|     it('should append timestamp to a URL without query parameters', () => {
 | |
|       const input = 'https://example.com/audio.mp3';
 | |
|       const expected = `https://example.com/audio.mp3?t=${FIXED_TIMESTAMP}`;
 | |
|       expect(timeStampAppendedURL(input)).toBe(expected);
 | |
|     });
 | |
| 
 | |
|     it('should append timestamp to a URL with existing query parameters', () => {
 | |
|       const input = 'https://example.com/audio.mp3?volume=50';
 | |
|       const expected = `https://example.com/audio.mp3?volume=50&t=${FIXED_TIMESTAMP}`;
 | |
|       expect(timeStampAppendedURL(input)).toBe(expected);
 | |
|     });
 | |
| 
 | |
|     it('should not append timestamp if it already exists', () => {
 | |
|       const input = 'https://example.com/audio.mp3?t=9876543210';
 | |
|       expect(timeStampAppendedURL(input)).toBe(input);
 | |
|     });
 | |
| 
 | |
|     it('should handle URLs with hash fragments', () => {
 | |
|       const input = 'https://example.com/audio.mp3#section1';
 | |
|       const expected = `https://example.com/audio.mp3?t=${FIXED_TIMESTAMP}#section1`;
 | |
|       expect(timeStampAppendedURL(input)).toBe(expected);
 | |
|     });
 | |
| 
 | |
|     it('should handle complex URLs', () => {
 | |
|       const input =
 | |
|         'https://example.com/path/to/audio.mp3?key1=value1&key2=value2#fragment';
 | |
|       const expected = `https://example.com/path/to/audio.mp3?key1=value1&key2=value2&t=${FIXED_TIMESTAMP}#fragment`;
 | |
|       expect(timeStampAppendedURL(input)).toBe(expected);
 | |
|     });
 | |
| 
 | |
|     it('should throw an error for invalid URLs', () => {
 | |
|       const input = 'not a valid url';
 | |
|       expect(() => timeStampAppendedURL(input)).toThrow();
 | |
|     });
 | |
|   });
 | |
| 
 | |
|   describe('getHostNameFromURL', () => {
 | |
|     it('should return the hostname from a valid URL', () => {
 | |
|       expect(getHostNameFromURL('https://example.com/path')).toBe(
 | |
|         'example.com'
 | |
|       );
 | |
|     });
 | |
| 
 | |
|     it('should return null for an invalid URL', () => {
 | |
|       expect(getHostNameFromURL('not a valid url')).toBe(null);
 | |
|     });
 | |
| 
 | |
|     it('should return null for an empty string', () => {
 | |
|       expect(getHostNameFromURL('')).toBe(null);
 | |
|     });
 | |
| 
 | |
|     it('should return null for undefined input', () => {
 | |
|       expect(getHostNameFromURL(undefined)).toBe(null);
 | |
|     });
 | |
| 
 | |
|     it('should correctly handle URLs with non-standard TLDs', () => {
 | |
|       expect(getHostNameFromURL('https://chatwoot.help')).toBe('chatwoot.help');
 | |
|     });
 | |
|   });
 | |
| 
 | |
|   describe('extractFilenameFromUrl', () => {
 | |
|     it('should extract filename from a valid URL', () => {
 | |
|       expect(
 | |
|         extractFilenameFromUrl('https://example.com/path/to/file.jpg')
 | |
|       ).toBe('file.jpg');
 | |
|       expect(extractFilenameFromUrl('https://example.com/image.png')).toBe(
 | |
|         'image.png'
 | |
|       );
 | |
|       expect(
 | |
|         extractFilenameFromUrl(
 | |
|           'https://example.com/folder/document.pdf?query=1'
 | |
|         )
 | |
|       ).toBe('document.pdf');
 | |
|       expect(
 | |
|         extractFilenameFromUrl('https://example.com/file.txt#section')
 | |
|       ).toBe('file.txt');
 | |
|     });
 | |
| 
 | |
|     it('should handle URLs without filename', () => {
 | |
|       expect(extractFilenameFromUrl('https://example.com/')).toBe(
 | |
|         'https://example.com/'
 | |
|       );
 | |
|       expect(extractFilenameFromUrl('https://example.com')).toBe(
 | |
|         'https://example.com'
 | |
|       );
 | |
|     });
 | |
| 
 | |
|     it('should handle invalid URLs gracefully', () => {
 | |
|       expect(extractFilenameFromUrl('not-a-url/file.txt')).toBe('file.txt');
 | |
|       expect(extractFilenameFromUrl('invalid-url')).toBe('invalid-url');
 | |
|     });
 | |
| 
 | |
|     it('should handle edge cases', () => {
 | |
|       expect(extractFilenameFromUrl('')).toBe('');
 | |
|       expect(extractFilenameFromUrl(null)).toBe(null);
 | |
|       expect(extractFilenameFromUrl(undefined)).toBe(undefined);
 | |
|       expect(extractFilenameFromUrl(123)).toBe(123);
 | |
|     });
 | |
| 
 | |
|     it('should handle URLs with query parameters and fragments', () => {
 | |
|       expect(
 | |
|         extractFilenameFromUrl(
 | |
|           'https://example.com/file.jpg?size=large&format=png'
 | |
|         )
 | |
|       ).toBe('file.jpg');
 | |
|       expect(
 | |
|         extractFilenameFromUrl('https://example.com/file.pdf#page=1')
 | |
|       ).toBe('file.pdf');
 | |
|       expect(
 | |
|         extractFilenameFromUrl('https://example.com/file.doc?v=1#section')
 | |
|       ).toBe('file.doc');
 | |
|     });
 | |
|   });
 | |
| 
 | |
|   describe('sanitizeAllowedDomains', () => {
 | |
|     it('returns empty string for falsy input', () => {
 | |
|       expect(sanitizeAllowedDomains('')).toBe('');
 | |
|       expect(sanitizeAllowedDomains(null)).toBe('');
 | |
|       expect(sanitizeAllowedDomains(undefined)).toBe('');
 | |
|     });
 | |
| 
 | |
|     it('trims whitespace and converts newlines to commas', () => {
 | |
|       const input = '  example.com  \n  foo.bar\nbar.baz  ';
 | |
|       expect(sanitizeAllowedDomains(input)).toBe('example.com,foo.bar,bar.baz');
 | |
|     });
 | |
| 
 | |
|     it('handles Windows newlines and mixed spacing', () => {
 | |
|       const input = ' example.com\r\n\tfoo.bar  ,  bar.baz ';
 | |
|       expect(sanitizeAllowedDomains(input)).toBe('example.com,foo.bar,bar.baz');
 | |
|     });
 | |
| 
 | |
|     it('removes empty values from repeated commas', () => {
 | |
|       const input = ',,example.com,,foo.bar,,';
 | |
|       expect(sanitizeAllowedDomains(input)).toBe('example.com,foo.bar');
 | |
|     });
 | |
| 
 | |
|     it('lowercases entries and de-duplicates preserving order', () => {
 | |
|       const input = 'Example.com,FOO.bar,example.com,Bar.Baz,foo.BAR';
 | |
|       expect(sanitizeAllowedDomains(input)).toBe('example.com,foo.bar,bar.baz');
 | |
|     });
 | |
|   });
 | |
| });
 |