UNPKG

n8n-nodes-duckduckgo-search

Version:

Integrate DuckDuckGo search seamlessly into your n8n workflows with advanced pagination and human-like behavior. Get more results without hitting rate limits.

477 lines (476 loc) 24.7 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; Object.defineProperty(exports, "__esModule", { value: true }); const DuckDuckGo_node_1 = require("../DuckDuckGo.node"); const duckDuckScrape = __importStar(require("duck-duck-scrape")); const cache = __importStar(require("../cache")); jest.mock('duck-duck-scrape', () => ({ search: jest.fn(), searchNews: jest.fn(), searchImages: jest.fn(), searchVideos: jest.fn(), })); jest.mock('../cache', () => ({ getCached: jest.fn(), setCache: jest.fn(), clearCache: jest.fn(), getCacheSize: jest.fn(), pruneExpiredEntries: jest.fn(), })); jest.mock('uuid', () => ({ v4: jest.fn().mockReturnValue('mock-uuid-1234'), })); describe('DuckDuckGo Node', () => { let duckDuckGoNode; let mockExecuteFunction; let mockGetNodeParameter; beforeEach(() => { duckDuckGoNode = new DuckDuckGo_node_1.DuckDuckGo(); mockGetNodeParameter = jest.fn(); mockExecuteFunction = { getInputData: jest.fn().mockReturnValue([{ json: {} }]), getNodeParameter: mockGetNodeParameter, getNode: jest.fn().mockReturnValue({ name: 'DuckDuckGo', type: 'n8n-nodes-base.duckDuckGo', typeVersion: 1 }), helpers: { returnJsonArray: jest.fn((items) => items), }, continueOnFail: jest.fn().mockReturnValue(false), getCredentials: jest.fn().mockResolvedValue({ apiKey: 'test-api-key' }), }; jest.clearAllMocks(); }); const setupNodeParameters = (operation, query, options = {}, additionalParams = {}) => { mockGetNodeParameter.mockImplementation((parameter, _itemIndex, fallback) => { switch (parameter) { case 'operation': return operation; case 'query': case 'imageQuery': case 'newsQuery': case 'videoQuery': return query; case 'webSearchOptions': case 'imageSearchOptions': case 'newsSearchOptions': case 'videoSearchOptions': return { maxResults: options.maxResults || 10, region: options.region || 'us-en', safeSearch: options.safeSearch !== undefined ? options.safeSearch : 1, timePeriod: options.timePeriod || '', ...options }; case 'locale': return options.locale || 'en-us'; case 'useCache': return options.useCache !== undefined ? options.useCache : true; case 'cacheTtl': return options.cacheTtl || 3600; case 'debugMode': return options.debugMode || false; case 'useApiKey': return options.useApiKey || false; case 'errorHandling': return options.errorHandling || 'continueOnFail'; default: if (additionalParams[parameter] !== undefined) { return additionalParams[parameter]; } return fallback; } }); }; describe('Web Search Operation', () => { const mockWebSearchResults = { results: [ { title: 'Test Result 1', description: 'Description for test result 1', url: 'https://example.com/1', hostname: 'example.com', icon: 'https://example.com/favicon.ico', }, { title: 'Test Result 2', description: 'Description for test result 2', url: 'https://example.com/2', hostname: 'example.com', icon: 'https://example.com/favicon.ico', }, ], }; it('should return web search results successfully', async () => { setupNodeParameters('search', 'test query'); duckDuckScrape.search.mockResolvedValue(mockWebSearchResults); const result = await duckDuckGoNode.execute.call(mockExecuteFunction); expect(duckDuckScrape.search).toHaveBeenCalledWith('test query', expect.any(Object)); expect(result).toHaveLength(1); expect(result[0]).toHaveLength(2); expect(result[0][0].json).toHaveProperty('url', 'https://example.com/1'); expect(result[0][0].json).toHaveProperty('title', 'Test Result 1'); expect(result[0][0].json).toHaveProperty('description', 'Description for test result 1'); expect(result[0][0].json).toHaveProperty('sourceType', 'web'); expect(result[0][1].json).toHaveProperty('url', 'https://example.com/2'); expect(result[0][1].json).toHaveProperty('title', 'Test Result 2'); }); it('should use cache when available', async () => { setupNodeParameters('search', 'cached query', { useCache: true, cacheTtl: 60 }); const cachedResults = { results: [ { title: 'Cached Result', description: 'This is a cached result', url: 'https://example.com/cached', hostname: 'example.com', }, ], }; cache.getCached.mockReturnValue(cachedResults); const result = await duckDuckGoNode.execute.call(mockExecuteFunction); expect(cache.getCached).toHaveBeenCalled(); expect(duckDuckScrape.search).not.toHaveBeenCalled(); expect(result).toHaveLength(1); expect(result[0]).toHaveLength(1); expect(result[0][0].json).toHaveProperty('title', 'Cached Result'); expect(result[0][0].json).toHaveProperty('url', 'https://example.com/cached'); }); it('should store results in cache when cache is enabled', async () => { setupNodeParameters('search', 'query to cache', { useCache: true, cacheTtl: 120 }); cache.getCached.mockReturnValue(undefined); duckDuckScrape.search.mockResolvedValue(mockWebSearchResults); await duckDuckGoNode.execute.call(mockExecuteFunction); expect(cache.getCached).toHaveBeenCalled(); expect(duckDuckScrape.search).toHaveBeenCalled(); expect(cache.setCache).toHaveBeenCalledWith(expect.any(String), mockWebSearchResults, 120); }); it('should handle API errors gracefully', async () => { setupNodeParameters('search', 'error query'); const apiError = new Error('API request failed'); apiError.name = 'FetchError'; apiError.stack = 'Error stack trace'; duckDuckScrape.search.mockRejectedValue(apiError); const result = await duckDuckGoNode.execute.call(mockExecuteFunction); expect(duckDuckScrape.search).toHaveBeenCalledWith('error query', expect.any(Object)); expect(result).toHaveLength(1); expect(result[0]).toHaveLength(1); expect(result[0][0].json).toHaveProperty('success', false); expect(result[0][0].json).toHaveProperty('error'); expect(result[0][0].json.error).toContain('API request failed'); }); it('should handle empty search results', async () => { setupNodeParameters('search', 'no results query'); const emptyResults = { results: [] }; duckDuckScrape.search.mockResolvedValue(emptyResults); const result = await duckDuckGoNode.execute.call(mockExecuteFunction); expect(duckDuckScrape.search).toHaveBeenCalledWith('no results query', expect.any(Object)); expect(result).toHaveLength(1); expect(result[0]).toHaveLength(1); expect(result[0][0].json).toHaveProperty('success', true); expect(result[0][0].json).toHaveProperty('results'); expect(result[0][0].json.results).toHaveLength(0); }); it('should throw an error when query is missing', async () => { mockGetNodeParameter.mockImplementation((parameter) => { if (parameter === 'operation') return 'search'; if (parameter === 'query') return ''; return null; }); await expect(duckDuckGoNode.execute.call(mockExecuteFunction)) .rejects .toThrow(/query is required/i); }); }); describe('Image Search Operation', () => { const mockImageSearchResults = { results: [ { title: 'Image 1', image: 'https://example.com/image1.jpg', thumbnail: 'https://example.com/thumb1.jpg', url: 'https://example.com/page1', width: 800, height: 600, source: 'example.com' }, { title: 'Image 2', image: 'https://example.com/image2.jpg', thumbnail: 'https://example.com/thumb2.jpg', url: 'https://example.com/page2', width: 1024, height: 768, source: 'example.com' } ] }; it('should return image search results successfully', async () => { setupNodeParameters('searchImages', 'cat pictures'); duckDuckScrape.searchImages.mockResolvedValue(mockImageSearchResults); const result = await duckDuckGoNode.execute.call(mockExecuteFunction); expect(duckDuckScrape.searchImages).toHaveBeenCalledWith('cat pictures', expect.any(Object)); expect(result).toHaveLength(1); expect(result[0]).toHaveLength(2); expect(result[0][0].json).toHaveProperty('imageUrl', 'https://example.com/image1.jpg'); expect(result[0][0].json).toHaveProperty('thumbnailUrl', 'https://example.com/thumb1.jpg'); expect(result[0][0].json).toHaveProperty('title', 'Image 1'); expect(result[0][0].json).toHaveProperty('width', 800); expect(result[0][0].json).toHaveProperty('height', 600); expect(result[0][0].json).toHaveProperty('sourceType', 'image'); }); it('should handle error in image search', async () => { setupNodeParameters('searchImages', 'error query'); const apiError = new Error('Image search failed'); apiError.name = 'HTTPError'; const errorWithCode = Object.assign(apiError, { httpCode: 500, code: 'INTERNAL_SERVER_ERROR', message: 'Image search failed', }); duckDuckScrape.searchImages.mockRejectedValue(errorWithCode); const result = await duckDuckGoNode.execute.call(mockExecuteFunction); expect(duckDuckScrape.searchImages).toHaveBeenCalled(); expect(result).toHaveLength(1); expect(result[0]).toHaveLength(1); expect(result[0][0].json).toHaveProperty('success', false); expect(result[0][0].json).toHaveProperty('error'); expect(result[0][0].json.error).toContain('Image search failed'); }); }); describe('News Search Operation', () => { const mockNewsSearchResults = { results: [ { title: 'News Article 1', excerpt: 'First news article excerpt', url: 'https://news.example.com/article1', date: 1625097600000, relativeTime: '2 hours ago', image: 'https://news.example.com/image1.jpg', syndicate: 'Example News' }, { title: 'News Article 2', excerpt: 'Second news article excerpt', url: 'https://news.example.com/article2', date: 1625094000000, relativeTime: '3 hours ago', image: 'https://news.example.com/image2.jpg', syndicate: 'Example News' } ] }; it('should return news search results successfully', async () => { setupNodeParameters('searchNews', 'latest tech news', { timePeriod: 'pastDay' }); duckDuckScrape.searchNews.mockResolvedValue(mockNewsSearchResults); const result = await duckDuckGoNode.execute.call(mockExecuteFunction); expect(duckDuckScrape.searchNews).toHaveBeenCalledWith('latest tech news', expect.objectContaining({ timePeriod: 'd' })); expect(result).toHaveLength(1); expect(result[0]).toHaveLength(2); expect(result[0][0].json).toHaveProperty('title', 'News Article 1'); expect(result[0][0].json).toHaveProperty('description', 'First news article excerpt'); expect(result[0][0].json).toHaveProperty('url', 'https://news.example.com/article1'); expect(result[0][0].json).toHaveProperty('relativeTime', '2 hours ago'); expect(result[0][0].json).toHaveProperty('imageUrl', 'https://news.example.com/image1.jpg'); expect(result[0][0].json).toHaveProperty('sourceType', 'news'); }); it('should cache news search results when enabled', async () => { setupNodeParameters('searchNews', 'cached news query', { useCache: true, cacheTtl: 30 }); cache.getCached.mockReturnValue(undefined); duckDuckScrape.searchNews.mockResolvedValue(mockNewsSearchResults); await duckDuckGoNode.execute.call(mockExecuteFunction); expect(cache.setCache).toHaveBeenCalledWith(expect.any(String), mockNewsSearchResults, 30); }); }); describe('Video Search Operation', () => { const mockVideoSearchResults = { results: [ { title: 'Video 1', description: 'Description for video 1', url: 'https://videos.example.com/video1', image: 'https://videos.example.com/thumb1.jpg', duration: '10:15', published: '2023-01-15', publishedOn: 'Example Videos', publisher: 'Example Publisher', viewCount: '1.2M views' }, { title: 'Video 2', description: 'Description for video 2', url: 'https://videos.example.com/video2', image: 'https://videos.example.com/thumb2.jpg', duration: '5:30', published: '2023-01-10', publishedOn: 'Example Videos', publisher: 'Example Publisher', viewCount: '562K views' } ] }; it('should return video search results successfully', async () => { setupNodeParameters('searchVideos', 'tutorial videos'); duckDuckScrape.searchVideos.mockResolvedValue(mockVideoSearchResults); const result = await duckDuckGoNode.execute.call(mockExecuteFunction); expect(duckDuckScrape.searchVideos).toHaveBeenCalledWith('tutorial videos', expect.any(Object)); expect(result).toHaveLength(1); expect(result[0]).toHaveLength(2); expect(result[0][0].json).toHaveProperty('title', 'Video 1'); expect(result[0][0].json).toHaveProperty('description', 'Description for video 1'); expect(result[0][0].json).toHaveProperty('url', 'https://videos.example.com/video1'); expect(result[0][0].json).toHaveProperty('duration', '10:15'); expect(result[0][0].json).toHaveProperty('imageUrl', 'https://videos.example.com/thumb1.jpg'); expect(result[0][0].json).toHaveProperty('viewCount', '1.2M views'); expect(result[0][0].json).toHaveProperty('sourceType', 'video'); }); it('should handle server error (500) in video search', async () => { setupNodeParameters('searchVideos', 'server error', { debugMode: true }); const serverError = new Error('Internal Server Error'); serverError.name = 'ServerError'; const errorWithCode = Object.assign(serverError, { httpCode: 500, code: 'INTERNAL_SERVER_ERROR', message: 'Internal Server Error', }); duckDuckScrape.searchVideos.mockRejectedValue(errorWithCode); const result = await duckDuckGoNode.execute.call(mockExecuteFunction); expect(duckDuckScrape.searchVideos).toHaveBeenCalled(); expect(result).toHaveLength(1); expect(result[0]).toHaveLength(1); expect(result[0][0].json).toHaveProperty('success', false); expect(result[0][0].json).toHaveProperty('error'); expect(result[0][0].json).toHaveProperty('errorDetails'); expect(result[0][0].json.error).toContain('Internal Server Error'); }); }); describe('API Key Authentication', () => { it('should include API key when authentication is enabled', async () => { setupNodeParameters('search', 'api auth query', { useApiKey: true }); duckDuckScrape.search.mockResolvedValue({ results: [] }); await duckDuckGoNode.execute.call(mockExecuteFunction); expect(duckDuckScrape.search).toHaveBeenCalledWith('api auth query', expect.objectContaining({ headers: expect.objectContaining({ Authorization: 'Bearer test-api-key' }) })); }); }); describe('Error Handling', () => { it('should handle network timeout errors', async () => { setupNodeParameters('search', 'timeout query'); const timeoutError = new Error('Network request timed out'); timeoutError.name = 'TimeoutError'; duckDuckScrape.search.mockRejectedValue(timeoutError); const result = await duckDuckGoNode.execute.call(mockExecuteFunction); expect(result).toHaveLength(1); expect(result[0]).toHaveLength(1); expect(result[0][0].json).toHaveProperty('success', false); expect(result[0][0].json).toHaveProperty('error'); expect(result[0][0].json.error).toContain('Network request timed out'); }); it('should handle rate limit errors (429)', async () => { setupNodeParameters('search', 'rate limited', { debugMode: true }); const rateLimitError = new Error('Rate limit exceeded'); rateLimitError.name = 'RateLimitError'; const errorWithCode = Object.assign(rateLimitError, { httpCode: 429, code: 'TOO_MANY_REQUESTS', message: 'Rate limit exceeded', }); duckDuckScrape.search.mockRejectedValue(errorWithCode); const result = await duckDuckGoNode.execute.call(mockExecuteFunction); expect(result).toHaveLength(1); expect(result[0]).toHaveLength(1); expect(result[0][0].json).toHaveProperty('error'); expect(result[0][0].json.error).toContain('Rate limit exceeded'); expect(result[0][0].json).toHaveProperty('errorDetails'); }); it('should throw NodeApiError when validation fails and continueOnFail is false', async () => { setupNodeParameters('search', ''); mockExecuteFunction.continueOnFail = jest.fn().mockReturnValue(false); await expect(duckDuckGoNode.execute.call(mockExecuteFunction)) .rejects .toThrow(); }); }); describe('Input Validation', () => { it('should validate maxResults is within acceptable range', async () => { setupNodeParameters('search', 'test query', { maxResults: 1000 }); duckDuckScrape.search.mockResolvedValue({ results: [] }); await duckDuckGoNode.execute.call(mockExecuteFunction); expect(duckDuckScrape.search).toHaveBeenCalledWith('test query', expect.objectContaining({ maxResults: expect.any(Number) })); const callArgs = duckDuckScrape.search.mock.calls[0][1]; expect(callArgs.maxResults).toBeLessThan(1000); }); it('should validate and handle invalid time period', async () => { setupNodeParameters('search', 'time period test', { timePeriod: 'invalidValue' }); await duckDuckGoNode.execute.call(mockExecuteFunction); expect(duckDuckScrape.search).toHaveBeenCalledWith('time period test', expect.not.objectContaining({ timePeriod: 'invalidValue' })); }); }); describe('Multiple Input Items', () => { it('should process multiple input items correctly', async () => { mockExecuteFunction.getInputData = jest.fn().mockReturnValue([ { json: { query: 'first query' } }, { json: { query: 'second query' } } ]); mockGetNodeParameter.mockImplementation((parameter, itemIndex) => { if (parameter === 'operation') return 'search'; if (parameter === 'query') return `query ${itemIndex + 1}`; if (parameter === 'webSearchOptions') return { maxResults: 5 }; return null; }); duckDuckScrape.search .mockResolvedValueOnce({ results: [{ title: 'Result 1', url: 'https://example.com/1' }] }) .mockResolvedValueOnce({ results: [{ title: 'Result 2', url: 'https://example.com/2' }] }); const result = await duckDuckGoNode.execute.call(mockExecuteFunction); expect(duckDuckScrape.search).toHaveBeenCalledTimes(2); expect(duckDuckScrape.search).toHaveBeenNthCalledWith(1, 'query 1', expect.any(Object)); expect(duckDuckScrape.search).toHaveBeenNthCalledWith(2, 'query 2', expect.any(Object)); expect(result).toHaveLength(2); expect(result[0]).toHaveLength(1); expect(result[1]).toHaveLength(1); expect(result[0][0].json).toHaveProperty('title', 'Result 1'); expect(result[1][0].json).toHaveProperty('title', 'Result 2'); expect(result[0][0].pairedItem).toEqual({ item: 0 }); expect(result[1][0].pairedItem).toEqual({ item: 1 }); }); }); });