n8n-nodes-duckduckgo-search
Version:
A powerful and comprehensive n8n community node that seamlessly integrates DuckDuckGo search capabilities into your workflows. Search the web, find images, discover news, and explore videos - all with privacy-focused, reliable results.
580 lines (579 loc) • 28.4 kB
JavaScript
"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"));
const directSearch = __importStar(require("../directSearch"));
jest.mock('duck-duck-scrape', () => ({
search: jest.fn(),
searchNews: jest.fn(),
searchImages: jest.fn(),
searchVideos: jest.fn(),
SafeSearchType: {
STRICT: 'strict',
MODERATE: 'moderate',
OFF: 'off',
},
SearchTimeType: {
DAY: 'd',
WEEK: 'w',
MONTH: 'm',
YEAR: 'y',
ALL: 'a',
},
VideoDefinition: {
HIGH: 'high',
STANDARD: 'standard',
ALL: 'all',
},
VideoDuration: {
SHORT: 'short',
MEDIUM: 'medium',
LONG: 'long',
ALL: 'all',
},
VideoLicense: {
CREATIVE_COMMONS: 'creativeCommons',
YOUTUBE: 'youtube',
ALL: 'all',
},
}));
jest.mock('../cache', () => ({
getCached: jest.fn(),
setCache: jest.fn(),
clearCache: jest.fn(),
getCacheSize: jest.fn(),
pruneExpiredEntries: jest.fn(),
}));
jest.mock('../directSearch', () => ({
directWebSearch: jest.fn(),
directImageSearch: jest.fn(),
getSafeSearchString: jest.fn((value) => {
switch (value) {
case 2: return 'strict';
case 1: return 'moderate';
default: return 'off';
}
}),
}));
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.map((item, index) => ({ json: item, pairedItem: { item: index } }))),
},
continueOnFail: jest.fn().mockReturnValue(false),
getCredentials: jest.fn().mockResolvedValue({ apiKey: 'test-api-key' }),
logger: {
info: jest.fn(),
warn: jest.fn(),
error: jest.fn(),
debug: jest.fn(),
},
};
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 || 99,
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 || 300;
case 'debugMode':
return options.debugMode || false;
case 'useApiKey':
return options.useApiKey || false;
case 'errorHandling':
return options.errorHandling || 'continueOnFail';
case 'enableTelemetry':
return options.enableTelemetry || false;
case 'cacheSettings':
return options.cacheSettings || { enableCache: options.useCache !== undefined ? options.useCache : false };
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');
const mockDirectResults = {
results: mockWebSearchResults.results.map(r => ({
title: r.title,
url: r.url,
description: r.description,
}))
};
directSearch.directWebSearch.mockResolvedValue(mockDirectResults);
const result = await duckDuckGoNode.execute.call(mockExecuteFunction);
expect(directSearch.directWebSearch).toHaveBeenCalledWith('"test query"', expect.objectContaining({
locale: 'us-en',
safeSearch: 'moderate',
}));
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: 300 });
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(directSearch.directWebSearch).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: 300 });
cache.getCached.mockReturnValue(undefined);
const mockDirectResults = {
results: mockWebSearchResults.results.map(r => ({
title: r.title,
url: r.url,
description: r.description,
}))
};
directSearch.directWebSearch.mockResolvedValue(mockDirectResults);
await duckDuckGoNode.execute.call(mockExecuteFunction);
expect(cache.getCached).toHaveBeenCalled();
expect(directSearch.directWebSearch).toHaveBeenCalled();
expect(cache.setCache).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({
results: expect.any(Array),
noResults: false,
}), 300);
});
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';
directSearch.directWebSearch.mockRejectedValue(apiError);
const result = await duckDuckGoNode.execute.call(mockExecuteFunction);
expect(directSearch.directWebSearch).toHaveBeenCalledWith('error query 2025', 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(typeof result[0][0].json.error).toBe('string');
});
it('should handle empty search results', async () => {
setupNodeParameters('search', 'no results query');
const emptyResults = { results: [] };
directSearch.directWebSearch.mockResolvedValue(emptyResults);
const result = await duckDuckGoNode.execute.call(mockExecuteFunction);
expect(directSearch.directWebSearch).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 '';
if (parameter === 'webSearchOptions')
return {
useSearchOperators: false,
safeSearch: 1,
locale: 'us-en',
timePeriod: '',
useCache: false,
cacheTtl: 300,
debugMode: false
};
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');
const mockDirectImageResults = {
results: mockImageSearchResults.results.map(r => ({
title: r.title,
url: r.image,
thumbnail: r.thumbnail,
width: r.width,
height: r.height,
source: r.url,
}))
};
directSearch.directImageSearch.mockResolvedValue(mockDirectImageResults);
const result = await duckDuckGoNode.execute.call(mockExecuteFunction);
expect(directSearch.directImageSearch).toHaveBeenCalledWith('cat pictures', expect.objectContaining({
locale: 'us-en',
safeSearch: 'moderate',
}));
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',
});
directSearch.directImageSearch.mockRejectedValue(errorWithCode);
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('image search');
});
});
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: 'd' });
duckDuckScrape.searchNews.mockResolvedValue(mockNewsSearchResults);
const result = await duckDuckGoNode.execute.call(mockExecuteFunction);
expect(duckDuckScrape.searchNews).toHaveBeenCalledWith('latest tech news', expect.objectContaining({
time: '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: 300 });
cache.getCached.mockReturnValue(undefined);
duckDuckScrape.searchNews.mockResolvedValue(mockNewsSearchResults);
await duckDuckGoNode.execute.call(mockExecuteFunction);
expect(cache.setCache).toHaveBeenCalledWith(expect.any(String), mockNewsSearchResults, 300);
});
});
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 });
directSearch.directWebSearch.mockResolvedValue({ results: [] });
await duckDuckGoNode.execute.call(mockExecuteFunction);
expect(directSearch.directWebSearch).toHaveBeenCalledWith('api auth query', expect.any(Object));
});
});
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';
directSearch.directWebSearch.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',
});
directSearch.directWebSearch.mockRejectedValue(errorWithCode);
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('Rate limit exceeded');
expect(result[0][0].json).toHaveProperty('errorDetails');
});
it('should throw NodeApiError when validation fails and continueOnFail is false', async () => {
mockGetNodeParameter.mockImplementation((parameter) => {
if (parameter === 'operation')
return 'search';
if (parameter === 'query')
return '';
if (parameter === 'webSearchOptions')
return {
useSearchOperators: false,
safeSearch: 1,
locale: 'us-en',
timePeriod: '',
useCache: false,
cacheTtl: 300,
debugMode: false
};
return null;
});
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', { debugMode: true });
directSearch.directWebSearch.mockResolvedValue({ results: [] });
await duckDuckGoNode.execute.call(mockExecuteFunction);
expect(directSearch.directWebSearch).toHaveBeenCalledWith('"test query"', expect.not.objectContaining({
maxResults: expect.any(Number),
}));
});
it('should validate and handle invalid time period', async () => {
setupNodeParameters('search', 'time period test', { timePeriod: 'invalidValue' });
directSearch.directWebSearch.mockResolvedValue({ results: [] });
await duckDuckGoNode.execute.call(mockExecuteFunction);
expect(directSearch.directWebSearch).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;
});
directSearch.directWebSearch
.mockResolvedValueOnce({ results: [{ title: 'Result 1', url: 'https://example.com/1', description: 'Desc 1' }] })
.mockResolvedValueOnce({ results: [{ title: 'Result 2', url: 'https://example.com/2', description: 'Desc 2' }] });
const result = await duckDuckGoNode.execute.call(mockExecuteFunction);
expect(directSearch.directWebSearch).toHaveBeenCalledTimes(2);
expect(directSearch.directWebSearch).toHaveBeenNthCalledWith(1, '"query 1"', expect.any(Object));
expect(directSearch.directWebSearch).toHaveBeenNthCalledWith(2, '"query 2"', expect.any(Object));
expect(result).toHaveLength(1);
expect(result[0]).toHaveLength(2);
expect(result[0][0].json).toHaveProperty('title', 'Result 1');
expect(result[0][1].json).toHaveProperty('title', 'Result 2');
expect(result[0][0].pairedItem).toEqual({ item: 0 });
expect(result[0][1].pairedItem).toEqual({ item: 1 });
});
});
});