From de8e728969fa37ebeba1b7a65de8264940a4e596 Mon Sep 17 00:00:00 2001 From: dishabhagat Date: Tue, 31 Mar 2026 14:18:57 -0400 Subject: [PATCH] validate values for --server and --include flag --- .../src/__tests__/handlers/export.test.ts | 134 +++++++++++++++ .../src/__tests__/handlers/import.test.ts | 66 ++++++++ .../src/__tests__/validators.test.ts | 159 +++++++++++++++++- tools/integration/src/cli.ts | 2 + tools/integration/src/handlers/export.ts | 19 ++- tools/integration/src/handlers/import.ts | 8 + tools/integration/src/validators.ts | 44 +++++ 7 files changed, 430 insertions(+), 2 deletions(-) diff --git a/tools/integration/src/__tests__/handlers/export.test.ts b/tools/integration/src/__tests__/handlers/export.test.ts index b4a02ed0..449a217b 100644 --- a/tools/integration/src/__tests__/handlers/export.test.ts +++ b/tools/integration/src/__tests__/handlers/export.test.ts @@ -1,4 +1,5 @@ import * as utils from '../../utils'; +import * as validators from '../../validators'; import axios from 'axios'; import fs from 'fs'; @@ -11,11 +12,13 @@ jest.mock('axios'); jest.mock('fs'); jest.mock('../../logger'); jest.mock('../../utils'); +jest.mock('../../validators'); const mockedAxios = axios as jest.Mocked; const mockedFs = fs as jest.Mocked; const mockedLogger = logger as jest.Mocked; const mockedUtils = utils as jest.Mocked; +const mockedValidators = validators as jest.Mocked; describe('handleExport', () => { let mockAxiosInstance: any; @@ -60,6 +63,137 @@ describe('handleExport', () => { mockFilterElementsBy.mockRestore(); }); + describe('Include Type Validation', () => { + it('should reject invalid include type "dashboards"', async () => { + process.argv = ['node', 'script.js', 'export', '--include', 'type=dashboards']; + + const argv = { + server: 'test-server.com', + token: 'test-token', + location: '/test/export', + debug: false + }; + + mockedValidators.validateServerAddress = jest.fn(); + mockedValidators.validateIncludeTypes = jest.fn().mockImplementation(() => { + throw new Error('Invalid --include type value(s): "dashboards". Valid types are: "dashboard", "event", "entity", "smart-alert", "all"'); + }); + + await expect(handleExport(argv)).rejects.toThrow('process.exit(1)'); + expect(mockedLogger.error).toHaveBeenCalledWith( + expect.stringContaining('Invalid --include type value(s): "dashboards"') + ); + }); + + it('should reject invalid include type "events"', async () => { + process.argv = ['node', 'script.js', 'export', '--include', 'type=events']; + + const argv = { + server: 'test-server.com', + token: 'test-token', + location: '/test/export', + debug: false + }; + + mockedValidators.validateServerAddress = jest.fn(); + mockedValidators.validateIncludeTypes = jest.fn().mockImplementation(() => { + throw new Error('Invalid --include type value(s): "events". Valid types are: "dashboard", "event", "entity", "smart-alert", "all"'); + }); + + await expect(handleExport(argv)).rejects.toThrow('process.exit(1)'); + expect(mockedLogger.error).toHaveBeenCalledWith( + expect.stringContaining('Invalid --include type value(s): "events"') + ); + }); + + it('should accept valid include type "dashboard"', async () => { + process.argv = ['node', 'script.js', 'export', '--include', 'type=dashboard']; + + const argv = { + server: 'test-server.com', + token: 'test-token', + location: '/test/export', + debug: false + }; + + mockedValidators.validateServerAddress = jest.fn(); + mockedValidators.validateIncludeTypes = jest.fn(); + mockedFs.existsSync = jest.fn().mockReturnValue(false); + mockedFs.mkdirSync = jest.fn(); + mockedFs.readdirSync = jest.fn().mockReturnValue([]); + mockedUtils.parseIncludesFromArgv = jest.fn().mockReturnValue([ + { type: 'dashboard', conditions: [], explicitlyTyped: true } + ]); + mockedUtils.sanitizeTitles = jest.fn().mockReturnValue([]); + mockAxiosInstance.get.mockResolvedValue({ status: 200, data: [] }); + + await handleExport(argv); + + expect(mockedValidators.validateIncludeTypes).toHaveBeenCalled(); + }); + }); + + describe('Server Validation', () => { + it('should reject server address with https:// protocol', async () => { + const argv = { + server: 'https://test-server.com', + token: 'test-token', + location: '/test/export', + debug: false + }; + + mockedValidators.validateServerAddress = jest.fn().mockImplementation(() => { + throw new Error('Invalid server address: Do not include protocol (http:// or https://). Please use only the hostname, e.g., "example.com" instead of "https://test-server.com"'); + }); + + await expect(handleExport(argv)).rejects.toThrow('process.exit(1)'); + expect(mockedLogger.error).toHaveBeenCalledWith( + expect.stringContaining('Invalid server address: Do not include protocol') + ); + }); + + it('should reject server address with http:// protocol', async () => { + const argv = { + server: 'http://test-server.com', + token: 'test-token', + location: '/test/export', + debug: false + }; + + mockedValidators.validateServerAddress = jest.fn().mockImplementation(() => { + throw new Error('Invalid server address: Do not include protocol (http:// or https://). Please use only the hostname, e.g., "example.com" instead of "http://test-server.com"'); + }); + + await expect(handleExport(argv)).rejects.toThrow('process.exit(1)'); + expect(mockedLogger.error).toHaveBeenCalledWith( + expect.stringContaining('Invalid server address: Do not include protocol') + ); + }); + + it('should accept valid server address without protocol', async () => { + const argv = { + server: 'test-server.com', + token: 'test-token', + location: '/test/export', + debug: false + }; + + mockedValidators.validateServerAddress = jest.fn(); + mockedFs.existsSync = jest.fn().mockReturnValue(false); + mockedFs.mkdirSync = jest.fn(); + mockedFs.readdirSync = jest.fn().mockReturnValue([]); + mockedUtils.parseIncludesFromArgv = jest.fn().mockReturnValue([ + { type: 'all', conditions: [], explicitlyTyped: false } + ]); + mockedUtils.sanitizeTitles = jest.fn().mockReturnValue([]); + mockAxiosInstance.get.mockResolvedValue({ status: 200, data: [] }); + + await handleExport(argv); + + expect(mockedValidators.validateServerAddress).toHaveBeenCalledWith('test-server.com'); + }); + }); + describe('Directory Validation', () => { it('should create export directory if it does not exist', async () => { const argv = { diff --git a/tools/integration/src/__tests__/handlers/import.test.ts b/tools/integration/src/__tests__/handlers/import.test.ts index d36dd9d5..a76ee5f5 100644 --- a/tools/integration/src/__tests__/handlers/import.test.ts +++ b/tools/integration/src/__tests__/handlers/import.test.ts @@ -54,6 +54,72 @@ describe('handleImport', () => { mockProcessExit.mockRestore(); }); + describe('Server Validation', () => { + it('should reject server address with https:// protocol', async () => { + const argv = { + package: '/test/package', + server: 'https://test-server.com', + token: 'test-token', + location: '/test/location', + debug: false + }; + + mockedValidators.validateServerAddress = jest.fn().mockImplementation(() => { + throw new Error('Invalid server address: Do not include protocol (http:// or https://). Please use only the hostname, e.g., "example.com" instead of "https://test-server.com"'); + }); + + await expect(handleImport(argv)).rejects.toThrow('process.exit(1)'); + expect(mockedLogger.error).toHaveBeenCalledWith( + expect.stringContaining('Invalid server address: Do not include protocol') + ); + }); + + it('should reject server address with http:// protocol', async () => { + const argv = { + package: '/test/package', + server: 'http://test-server.com', + token: 'test-token', + location: '/test/location', + debug: false + }; + + mockedValidators.validateServerAddress = jest.fn().mockImplementation(() => { + throw new Error('Invalid server address: Do not include protocol (http:// or https://). Please use only the hostname, e.g., "example.com" instead of "http://test-server.com"'); + }); + + await expect(handleImport(argv)).rejects.toThrow('process.exit(1)'); + expect(mockedLogger.error).toHaveBeenCalledWith( + expect.stringContaining('Invalid server address: Do not include protocol') + ); + }); + + it('should accept valid server address without protocol', async () => { + const argv = { + package: '/test/package', + server: 'test-server.com', + token: 'test-token', + location: '/test/location', + include: 'dashboards/**/*.json', + debug: false + }; + + mockedValidators.validateServerAddress = jest.fn(); + mockedFs.existsSync = jest.fn().mockReturnValue(true); + mockedGlobSync.mockReturnValue(['/test/package/dashboards/test.json']); + mockedFs.readFileSync = jest.fn().mockReturnValue(JSON.stringify({ + title: 'Test Dashboard', + accessRules: [{ accessType: 'READ_WRITE', relationType: 'GLOBAL' }] + })); + mockAxiosInstance.post.mockResolvedValue({ status: 200 }); + mockedValidators.getEntityDashboardRefs = jest.fn().mockReturnValue(new Set()); + + await handleImport(argv); + + expect(mockedValidators.validateServerAddress).toHaveBeenCalledWith('test-server.com'); + expect(mockAxiosInstance.post).toHaveBeenCalled(); + }); + }); + describe('Basic Import Functionality', () => { it('should import dashboards successfully', async () => { const argv = { diff --git a/tools/integration/src/__tests__/validators.test.ts b/tools/integration/src/__tests__/validators.test.ts index f35a2090..de31871c 100644 --- a/tools/integration/src/__tests__/validators.test.ts +++ b/tools/integration/src/__tests__/validators.test.ts @@ -1,5 +1,6 @@ import * as validators from '../validators'; +import { VALID_INCLUDE_TYPES, validateIncludeTypes, validateServerAddress } from '../validators'; import { afterEach, beforeEach, describe, expect, it, jest } from '@jest/globals'; import axios from 'axios'; @@ -1046,4 +1047,160 @@ describe('validators', () => { expect(errors).toHaveLength(0); }); }); -}); \ No newline at end of file + }); + + describe('validateServerAddress', () => { + it('should accept valid server addresses without protocol', () => { + expect(() => validateServerAddress('example.com')).not.toThrow(); + expect(() => validateServerAddress('api.example.com')).not.toThrow(); + expect(() => validateServerAddress('192.168.1.1')).not.toThrow(); + expect(() => validateServerAddress('localhost')).not.toThrow(); + expect(() => validateServerAddress('example.com:8080')).not.toThrow(); + expect(() => validateServerAddress('api.example.com:443')).not.toThrow(); + }); + + it('should reject server addresses with http:// protocol', () => { + expect(() => validateServerAddress('http://example.com')).toThrow( + 'Invalid server address: Do not include protocol (http:// or https://). Please use only the hostname, e.g., "example.com" instead of "http://example.com"' + ); + }); + + it('should reject server addresses with https:// protocol', () => { + expect(() => validateServerAddress('https://example.com')).toThrow( + 'Invalid server address: Do not include protocol (http:// or https://). Please use only the hostname, e.g., "example.com" instead of "https://example.com"' + ); + }); + + it('should reject server addresses with https:// protocol and port', () => { + expect(() => validateServerAddress('https://example.com:8080')).toThrow( + 'Invalid server address: Do not include protocol (http:// or https://). Please use only the hostname, e.g., "example.com" instead of "https://example.com:8080"' + ); + }); + + it('should reject server addresses with other protocols', () => { + expect(() => validateServerAddress('ftp://example.com')).toThrow( + 'Invalid server address: Protocol prefix detected. Please use only the hostname, e.g., "example.com" instead of "ftp://example.com"' + ); + }); + + it('should handle server addresses with whitespace', () => { + expect(() => validateServerAddress(' https://example.com ')).toThrow( + 'Invalid server address: Do not include protocol (http:// or https://). Please use only the hostname, e.g., "example.com" instead of "https://example.com"' + ); + }); + + it('should reject empty server addresses', () => { + expect(() => validateServerAddress('')).toThrow( + 'Server address is required and must be a string' + ); + }); + + it('should reject null or undefined server addresses', () => { + expect(() => validateServerAddress(null as any)).toThrow( + 'Server address is required and must be a string' + ); + expect(() => validateServerAddress(undefined as any)).toThrow( + 'Server address is required and must be a string' + ); + }); + + it('should reject non-string server addresses', () => { + expect(() => validateServerAddress(123 as any)).toThrow( + 'Server address is required and must be a string' + ); + expect(() => validateServerAddress({} as any)).toThrow( + 'Server address is required and must be a string' + ); + }); + }); + + describe('validateIncludeTypes', () => { + it('should accept valid include types', () => { + const validIncludes = [ + { type: 'dashboard', conditions: [], explicitlyTyped: true }, + { type: 'event', conditions: [], explicitlyTyped: true }, + { type: 'entity', conditions: [], explicitlyTyped: true }, + { type: 'smart-alert', conditions: [], explicitlyTyped: true }, + { type: 'all', conditions: [], explicitlyTyped: true } + ]; + expect(() => validateIncludeTypes(validIncludes)).not.toThrow(); + }); + + it('should accept multiple valid include types', () => { + const validIncludes = [ + { type: 'dashboard', conditions: ['title=test'], explicitlyTyped: true }, + { type: 'event', conditions: ['name=test'], explicitlyTyped: true } + ]; + expect(() => validateIncludeTypes(validIncludes)).not.toThrow(); + }); + + it('should reject invalid include type "dashboards"', () => { + const invalidIncludes = [ + { type: 'dashboards', conditions: [], explicitlyTyped: true } + ]; + expect(() => validateIncludeTypes(invalidIncludes)).toThrow( + 'Invalid --include type value(s): "dashboards". Valid types are: "dashboard", "event", "entity", "smart-alert", "all"' + ); + }); + + it('should reject invalid include type "events"', () => { + const invalidIncludes = [ + { type: 'events', conditions: [], explicitlyTyped: true } + ]; + expect(() => validateIncludeTypes(invalidIncludes)).toThrow( + 'Invalid --include type value(s): "events". Valid types are: "dashboard", "event", "entity", "smart-alert", "all"' + ); + }); + + it('should reject invalid include type "entities"', () => { + const invalidIncludes = [ + { type: 'entities', conditions: [], explicitlyTyped: true } + ]; + expect(() => validateIncludeTypes(invalidIncludes)).toThrow( + 'Invalid --include type value(s): "entities". Valid types are: "dashboard", "event", "entity", "smart-alert", "all"' + ); + }); + + it('should reject completely invalid type', () => { + const invalidIncludes = [ + { type: 'invalid-type', conditions: [], explicitlyTyped: true } + ]; + expect(() => validateIncludeTypes(invalidIncludes)).toThrow( + 'Invalid --include type value(s): "invalid-type". Valid types are: "dashboard", "event", "entity", "smart-alert", "all"' + ); + }); + + it('should reject multiple invalid types and show all unique ones', () => { + const invalidIncludes = [ + { type: 'dashboards', conditions: [], explicitlyTyped: true }, + { type: 'events', conditions: [], explicitlyTyped: true }, + { type: 'dashboards', conditions: [], explicitlyTyped: true } // duplicate + ]; + expect(() => validateIncludeTypes(invalidIncludes)).toThrow( + 'Invalid --include type value(s): "dashboards", "events". Valid types are: "dashboard", "event", "entity", "smart-alert", "all"' + ); + }); + + it('should accept mix of valid and implicitly typed (not explicitly typed)', () => { + const mixedIncludes = [ + { type: 'dashboard', conditions: [], explicitlyTyped: true }, + { type: 'all', conditions: [], explicitlyTyped: false } // implicitly typed, should not be validated + ]; + expect(() => validateIncludeTypes(mixedIncludes)).not.toThrow(); + }); + + it('should only validate explicitly typed includes', () => { + const includes = [ + { type: 'invalid-type', conditions: [], explicitlyTyped: false } // not explicitly typed, should be ignored + ]; + expect(() => validateIncludeTypes(includes)).not.toThrow(); + }); + + it('should accept empty array', () => { + expect(() => validateIncludeTypes([])).not.toThrow(); + }); + + it('should have correct valid types constant', () => { + expect(VALID_INCLUDE_TYPES).toEqual(['dashboard', 'event', 'entity', 'smart-alert', 'all']); + }); + }); \ No newline at end of file diff --git a/tools/integration/src/cli.ts b/tools/integration/src/cli.ts index 766cc86e..823269d0 100644 --- a/tools/integration/src/cli.ts +++ b/tools/integration/src/cli.ts @@ -24,6 +24,7 @@ Import integration package with parameters replaced: ${execName} import --package my-package --server example.com --include "dashboards/**/test-*.json" --set key1=value1 --set key2=value2 ${execName} import --package my-package --server example.com --include "events/**/*.json" ${execName} import --package my-package --server example.com --include "entities/**/*.json" + ${execName} import --package my-package --server example.com --include "smart-alerts/**/*.json" `; const examplesForExport = ` @@ -33,6 +34,7 @@ Export integration elements: ${execName} export --server example.com --include type=dashboard title="exampleTitle" --location ./my-package ${execName} export --server example.com --include type=event id=exampleid --location ./my-package ${execName} export --server example.com --include type=entity title="exampleTitle" --location ./my-package + ${execName} export --server example.com --include type=smart-alert title="exampleTitle" --location ./my-package `; /** diff --git a/tools/integration/src/handlers/export.ts b/tools/integration/src/handlers/export.ts index 06a213f2..ba0f3203 100644 --- a/tools/integration/src/handlers/export.ts +++ b/tools/integration/src/handlers/export.ts @@ -1,4 +1,5 @@ import * as utils from '../utils'; +import * as validators from '../validators'; import axios from 'axios'; import fs from 'fs'; @@ -23,9 +24,25 @@ export async function handleExport(argv: any) { logger.level = 'debug'; } + // Validate server address + try { + validators.validateServerAddress(server); + } catch (error) { + logger.error(error instanceof Error ? error.message : String(error)); + process.exit(1); + } + const parsedIncludes = utils.parseIncludesFromArgv(process.argv); - const exportPath = path.resolve(location); + // Validate include types + try { + validators.validateIncludeTypes(parsedIncludes); + } catch (error) { + logger.error(error instanceof Error ? error.message : String(error)); + process.exit(1); + } + + const exportPath = path.resolve(location); if (fs.existsSync(exportPath)) { const foldersToCheck = [ path.join(location, 'dashboards'), diff --git a/tools/integration/src/handlers/import.ts b/tools/integration/src/handlers/import.ts index b2a7a442..a5170ffc 100644 --- a/tools/integration/src/handlers/import.ts +++ b/tools/integration/src/handlers/import.ts @@ -29,6 +29,14 @@ export async function handleImport(argv: any) { logger.level = 'debug'; } + // Validate server address + try { + validators.validateServerAddress(server); + } catch (error) { + logger.error(error instanceof Error ? error.message : String(error)); + process.exit(1); + } + let packagePath = packageNameOrPath; if (!fs.existsSync(packageNameOrPath)) { packagePath = path.join(location, 'node_modules', packageNameOrPath); diff --git a/tools/integration/src/validators.ts b/tools/integration/src/validators.ts index 472cbb00..c5b1c7aa 100644 --- a/tools/integration/src/validators.ts +++ b/tools/integration/src/validators.ts @@ -11,6 +11,50 @@ export interface ValidationResult { successMessages: string[]; } +/* Validates that the server address does not include protocol (http:// or https://) */ +export function validateServerAddress(server: string): void { + if (!server || typeof server !== 'string') { + throw new Error('Server address is required and must be a string'); + } + + const trimmedServer = server.trim(); + + if (trimmedServer.startsWith('http://') || trimmedServer.startsWith('https://')) { + throw new Error( + 'Invalid server address: Do not include protocol (http:// or https://). Please use only the hostname.' + ); + } + + if (trimmedServer.includes('://')) { + throw new Error( + 'Invalid server address: Protocol prefix detected. Please use only the hostname.' + ); + } +} + +/* Valid types for the --include type parameter */ +export const VALID_INCLUDE_TYPES = ['dashboard', 'event', 'entity', 'smart-alert'] as const; +export type ValidIncludeType = typeof VALID_INCLUDE_TYPES[number]; + +/* Validates that include types are valid */ +export function validateIncludeTypes(parsedIncludes: Array<{ type: string; conditions: string[]; explicitlyTyped: boolean }>): void { + const invalidTypes: string[] = []; + + for (const include of parsedIncludes) { + if (include.explicitlyTyped && !VALID_INCLUDE_TYPES.includes(include.type as any)) { + invalidTypes.push(include.type); + } + } + + if (invalidTypes.length > 0) { + const uniqueInvalidTypes = [...new Set(invalidTypes)]; + throw new Error( + `Invalid --include type value(s): ${uniqueInvalidTypes.map(t => `"${t}"`).join(', ')}. ` + + `Valid types are: ${VALID_INCLUDE_TYPES.map(t => `"${t}"`).join(', ')}` + ); + } +} + /** * Validates package.json file for required fields and version constraints */