@closedloop-ai/cli 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/dist/__tests__/cli.test.js +37 -0
  2. package/dist/__tests__/cli.test.js.map +1 -0
  3. package/dist/__tests__/config.test.js +33 -0
  4. package/dist/__tests__/config.test.js.map +1 -0
  5. package/dist/__tests__/errors.test.js +109 -0
  6. package/dist/__tests__/errors.test.js.map +1 -0
  7. package/dist/__tests__/setup.js +20 -0
  8. package/dist/__tests__/setup.js.map +1 -0
  9. package/dist/__tests__/validation.test.js +124 -0
  10. package/dist/__tests__/validation.test.js.map +1 -0
  11. package/dist/api/auth.js +27 -0
  12. package/dist/api/auth.js.map +1 -0
  13. package/dist/api/feedback.js +70 -0
  14. package/dist/api/feedback.js.map +1 -0
  15. package/dist/api/input.js +60 -0
  16. package/dist/api/input.js.map +1 -0
  17. package/dist/api/usage.js +49 -0
  18. package/dist/api/usage.js.map +1 -0
  19. package/dist/cli.js +156 -0
  20. package/dist/cli.js.map +1 -0
  21. package/dist/commands/config.js +186 -0
  22. package/dist/commands/config.js.map +1 -0
  23. package/dist/commands/feedback.js +234 -0
  24. package/dist/commands/feedback.js.map +1 -0
  25. package/dist/commands/input.js +370 -0
  26. package/dist/commands/input.js.map +1 -0
  27. package/dist/commands/list.js +81 -0
  28. package/dist/commands/list.js.map +1 -0
  29. package/dist/commands/status.js +93 -0
  30. package/dist/commands/status.js.map +1 -0
  31. package/dist/commands/submit.js +225 -0
  32. package/dist/commands/submit.js.map +1 -0
  33. package/dist/commands/usage.js +168 -0
  34. package/dist/commands/usage.js.map +1 -0
  35. package/dist/config/constants.js +37 -0
  36. package/dist/config/constants.js.map +1 -0
  37. package/dist/config/index.js +84 -0
  38. package/dist/config/index.js.map +1 -0
  39. package/dist/types/index.js +3 -0
  40. package/dist/types/index.js.map +1 -0
  41. package/dist/utils/config.js +55 -0
  42. package/dist/utils/config.js.map +1 -0
  43. package/dist/utils/errors.js +70 -0
  44. package/dist/utils/errors.js.map +1 -0
  45. package/dist/utils/validation.js +133 -0
  46. package/dist/utils/validation.js.map +1 -0
  47. package/jest.config.js +14 -0
  48. package/package.json +42 -0
  49. package/src/__tests__/cli.test.ts +44 -0
  50. package/src/__tests__/config.test.ts +38 -0
  51. package/src/__tests__/errors.test.ts +132 -0
  52. package/src/__tests__/setup.ts +21 -0
  53. package/src/__tests__/validation.test.ts +156 -0
  54. package/src/api/auth.ts +21 -0
  55. package/src/api/feedback.ts +64 -0
  56. package/src/api/input.ts +55 -0
  57. package/src/api/usage.ts +45 -0
  58. package/src/cli.ts +158 -0
  59. package/src/commands/config.ts +192 -0
  60. package/src/commands/feedback.ts +250 -0
  61. package/src/commands/input.ts +389 -0
  62. package/src/commands/list.ts +84 -0
  63. package/src/commands/status.ts +96 -0
  64. package/src/commands/submit.ts +214 -0
  65. package/src/config/constants.ts +36 -0
  66. package/src/config/index.ts +59 -0
  67. package/src/types/index.ts +119 -0
  68. package/src/utils/errors.ts +98 -0
  69. package/src/utils/validation.ts +166 -0
  70. package/tsconfig.json +25 -0
@@ -0,0 +1,70 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ERROR_CODES = exports.CliError = void 0;
4
+ exports.handleApiError = handleApiError;
5
+ exports.handleValidationError = handleValidationError;
6
+ exports.handleInputError = handleInputError;
7
+ exports.handleProcessingError = handleProcessingError;
8
+ class CliError extends Error {
9
+ constructor(message, code, exitCode = 1) {
10
+ super(message);
11
+ this.code = code;
12
+ this.exitCode = exitCode;
13
+ this.name = 'CliError';
14
+ }
15
+ }
16
+ exports.CliError = CliError;
17
+ exports.ERROR_CODES = {
18
+ NO_API_KEY: 'NO_API_KEY',
19
+ INVALID_API_KEY: 'INVALID_API_KEY',
20
+ INVALID_INPUT: 'INVALID_INPUT',
21
+ API_ERROR: 'API_ERROR',
22
+ VALIDATION_ERROR: 'VALIDATION_ERROR',
23
+ CONNECTION_ERROR: 'CONNECTION_ERROR',
24
+ PROCESSING_ERROR: 'PROCESSING_ERROR',
25
+ NOT_FOUND: 'NOT_FOUND',
26
+ UNAUTHORIZED: 'UNAUTHORIZED',
27
+ RATE_LIMITED: 'RATE_LIMITED',
28
+ SERVER_ERROR: 'SERVER_ERROR',
29
+ UNKNOWN_ERROR: 'UNKNOWN_ERROR'
30
+ };
31
+ function handleApiError(error) {
32
+ if (error.response) {
33
+ const status = error.response.status;
34
+ const data = error.response.data;
35
+ switch (status) {
36
+ case 401:
37
+ return new CliError('Invalid API key. Please check your configuration.', exports.ERROR_CODES.UNAUTHORIZED, 1);
38
+ case 404:
39
+ return new CliError('Resource not found. Please check the ID and try again.', exports.ERROR_CODES.NOT_FOUND, 1);
40
+ case 429:
41
+ return new CliError('Rate limit exceeded. Please wait a moment and try again.', exports.ERROR_CODES.RATE_LIMITED, 1);
42
+ case 500:
43
+ case 502:
44
+ case 503:
45
+ case 504:
46
+ return new CliError('Server error. Please try again later.', exports.ERROR_CODES.SERVER_ERROR, 1);
47
+ default:
48
+ return new CliError(data?.message || `API error (${status})`, exports.ERROR_CODES.API_ERROR, 1);
49
+ }
50
+ }
51
+ else if (error.code === 'ENOTFOUND' || error.code === 'ECONNREFUSED') {
52
+ return new CliError('Cannot connect to ClosedLoop AI servers. Please check your internet connection.', exports.ERROR_CODES.CONNECTION_ERROR, 1);
53
+ }
54
+ else if (error.code === 'ETIMEDOUT') {
55
+ return new CliError('Request timed out. Please try again.', exports.ERROR_CODES.CONNECTION_ERROR, 1);
56
+ }
57
+ else {
58
+ return new CliError(error.message || 'An unknown error occurred', exports.ERROR_CODES.UNKNOWN_ERROR, 1);
59
+ }
60
+ }
61
+ function handleValidationError(message) {
62
+ return new CliError(message, exports.ERROR_CODES.VALIDATION_ERROR, 1);
63
+ }
64
+ function handleInputError(message) {
65
+ return new CliError(message, exports.ERROR_CODES.INVALID_INPUT, 1);
66
+ }
67
+ function handleProcessingError(message) {
68
+ return new CliError(message, exports.ERROR_CODES.PROCESSING_ERROR, 1);
69
+ }
70
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/utils/errors.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,133 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.validateUuid = validateUuid;
4
+ exports.validateInputContent = validateInputContent;
5
+ exports.validateApiKey = validateApiKey;
6
+ exports.validateEmail = validateEmail;
7
+ exports.validateUrl = validateUrl;
8
+ exports.validatePagination = validatePagination;
9
+ exports.validateTitle = validateTitle;
10
+ exports.validateCustomerId = validateCustomerId;
11
+ exports.validateName = validateName;
12
+ exports.sanitizeInput = sanitizeInput;
13
+ exports.validateAndSanitizeInput = validateAndSanitizeInput;
14
+ const errors_1 = require("./errors");
15
+ const constants_1 = require("../config/constants");
16
+ // UUID validation
17
+ function validateUuid(id) {
18
+ const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
19
+ return uuidRegex.test(id);
20
+ }
21
+ // Content validation
22
+ function validateInputContent(content) {
23
+ if (!content || content.trim().length === 0) {
24
+ throw (0, errors_1.handleInputError)('Content cannot be empty');
25
+ }
26
+ if (content.length > constants_1.CONFIG.MAX_CONTENT_LENGTH) {
27
+ throw (0, errors_1.handleInputError)(`Content too long (max ${constants_1.CONFIG.MAX_CONTENT_LENGTH} characters)`);
28
+ }
29
+ return content.trim();
30
+ }
31
+ // API key validation
32
+ function validateApiKey(apiKey) {
33
+ if (!apiKey || apiKey.trim().length === 0 || apiKey.length < constants_1.CONFIG.MIN_API_KEY_LENGTH) {
34
+ throw (0, errors_1.handleValidationError)('Invalid API key. Get your free API key at https://closedloop.sh');
35
+ }
36
+ return apiKey.trim();
37
+ }
38
+ // Email validation
39
+ function validateEmail(email) {
40
+ if (!email || email.trim().length === 0) {
41
+ throw (0, errors_1.handleValidationError)('Email cannot be empty');
42
+ }
43
+ const trimmedEmail = email.trim();
44
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
45
+ if (!emailRegex.test(trimmedEmail)) {
46
+ throw (0, errors_1.handleValidationError)('Invalid email format');
47
+ }
48
+ return trimmedEmail.toLowerCase();
49
+ }
50
+ // URL validation
51
+ function validateUrl(url) {
52
+ if (!url || url.trim().length === 0) {
53
+ throw (0, errors_1.handleValidationError)('URL cannot be empty');
54
+ }
55
+ try {
56
+ const urlObj = new URL(url);
57
+ if (!['http:', 'https:'].includes(urlObj.protocol)) {
58
+ throw (0, errors_1.handleValidationError)('URL must use HTTP or HTTPS protocol');
59
+ }
60
+ return urlObj.toString();
61
+ }
62
+ catch (error) {
63
+ throw (0, errors_1.handleValidationError)('Invalid URL format');
64
+ }
65
+ }
66
+ // Pagination validation
67
+ function validatePagination(page, limit) {
68
+ const validatedPage = Math.max(1, page || 1);
69
+ const validatedLimit = Math.min(Math.max(1, limit || constants_1.CONFIG.DEFAULT_PAGE_SIZE), constants_1.CONFIG.MAX_PAGE_SIZE);
70
+ return { page: validatedPage, limit: validatedLimit };
71
+ }
72
+ // Title validation
73
+ function validateTitle(title) {
74
+ if (!title || title.trim().length === 0) {
75
+ throw (0, errors_1.handleValidationError)('Title cannot be empty');
76
+ }
77
+ if (title.length > 200) {
78
+ throw (0, errors_1.handleValidationError)('Title too long (max 200 characters)');
79
+ }
80
+ return title.trim();
81
+ }
82
+ // Customer ID validation
83
+ function validateCustomerId(customerId) {
84
+ if (!customerId || customerId.trim().length === 0) {
85
+ throw (0, errors_1.handleValidationError)('Customer ID cannot be empty');
86
+ }
87
+ if (customerId.length > 100) {
88
+ throw (0, errors_1.handleValidationError)('Customer ID too long (max 100 characters)');
89
+ }
90
+ return customerId.trim();
91
+ }
92
+ // Name validation
93
+ function validateName(name) {
94
+ if (!name || name.trim().length === 0) {
95
+ throw (0, errors_1.handleValidationError)('Name cannot be empty');
96
+ }
97
+ if (name.length > 100) {
98
+ throw (0, errors_1.handleValidationError)('Name too long (max 100 characters)');
99
+ }
100
+ return name.trim();
101
+ }
102
+ // Sanitize input to prevent XSS
103
+ function sanitizeInput(input) {
104
+ return input
105
+ .replace(/[<>]/g, '') // Remove < and >
106
+ .replace(/javascript:/gi, '') // Remove javascript: protocol
107
+ .replace(/on\w+=/gi, '') // Remove event handlers
108
+ .trim();
109
+ }
110
+ // Validate and sanitize all input data
111
+ function validateAndSanitizeInput(data) {
112
+ const validated = {};
113
+ // Required fields
114
+ validated.content = sanitizeInput(validateInputContent(data.content));
115
+ // Optional fields
116
+ if (data.title) {
117
+ validated.title = sanitizeInput(validateTitle(data.title));
118
+ }
119
+ if (data.source_url) {
120
+ validated.source_url = validateUrl(data.source_url);
121
+ }
122
+ if (data.customer_id) {
123
+ validated.customer_id = sanitizeInput(validateCustomerId(data.customer_id));
124
+ }
125
+ if (data.reporter_name) {
126
+ validated.reporter_name = sanitizeInput(validateName(data.reporter_name));
127
+ }
128
+ if (data.reporter_email) {
129
+ validated.reporter_email = validateEmail(data.reporter_email);
130
+ }
131
+ return validated;
132
+ }
133
+ //# sourceMappingURL=validation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validation.js","sourceRoot":"","sources":["../../src/utils/validation.ts"],"names":[],"mappings":"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"}
package/jest.config.js ADDED
@@ -0,0 +1,14 @@
1
+ module.exports = {
2
+ preset: 'ts-jest',
3
+ testEnvironment: 'node',
4
+ roots: ['<rootDir>/src'],
5
+ testMatch: ['**/__tests__/**/*.test.ts'],
6
+ collectCoverageFrom: [
7
+ 'src/**/*.ts',
8
+ '!src/**/*.d.ts',
9
+ '!src/__tests__/**/*'
10
+ ],
11
+ coverageDirectory: 'coverage',
12
+ coverageReporters: ['text', 'lcov', 'html'],
13
+ setupFilesAfterEnv: ['<rootDir>/src/__tests__/setup.ts']
14
+ };
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@closedloop-ai/cli",
3
+ "version": "0.2.0",
4
+ "description": "ClosedLoop AI CLI - AI-powered customer product feedback analysis tool",
5
+ "main": "dist/cli.js",
6
+ "bin": {
7
+ "cl": "./dist/cli.js"
8
+ },
9
+ "scripts": {
10
+ "build": "tsc",
11
+ "test": "jest",
12
+ "lint": "eslint src/**/*.ts",
13
+ "clean": "rm -rf dist"
14
+ },
15
+ "dependencies": {
16
+ "commander": "^11.0.0",
17
+ "axios": "^1.6.0",
18
+ "chalk": "^4.1.2",
19
+ "ora": "^5.4.1",
20
+ "inquirer": "^8.2.6",
21
+ "table": "^6.8.1"
22
+ },
23
+ "devDependencies": {
24
+ "@types/node": "^20.0.0",
25
+ "@types/inquirer": "^8.2.0",
26
+ "typescript": "^5.0.0",
27
+ "jest": "^29.0.0",
28
+ "ts-jest": "^29.0.0"
29
+ },
30
+ "keywords": [
31
+ "closedloop",
32
+ "cli",
33
+ "command-line",
34
+ "ai",
35
+ "feedback",
36
+ "analysis",
37
+ "customer-feedback",
38
+ "product-insights"
39
+ ],
40
+ "author": "ClosedLoop AI",
41
+ "license": "MIT"
42
+ }
@@ -0,0 +1,44 @@
1
+ import { Command } from 'commander';
2
+
3
+ // Mock the command handlers
4
+ jest.mock('../commands/input', () => ({
5
+ handleInputCommand: jest.fn()
6
+ }));
7
+
8
+ jest.mock('../commands/feedback', () => ({
9
+ handleFeedbackCommand: jest.fn()
10
+ }));
11
+
12
+ jest.mock('../commands/config', () => ({
13
+ configCommand: new Command('config')
14
+ }));
15
+
16
+ describe('CLI Structure', () => {
17
+ it('should have correct command structure', () => {
18
+ // This is a basic smoke test to ensure the CLI can be imported
19
+ // without errors
20
+ expect(() => {
21
+ require('../cli');
22
+ }).not.toThrow();
23
+ });
24
+
25
+ it('should handle input command', async () => {
26
+ const { handleInputCommand } = require('../commands/input');
27
+
28
+ // Mock successful execution
29
+ (handleInputCommand as jest.Mock).mockResolvedValue(undefined);
30
+
31
+ await handleInputCommand('test content', {});
32
+ expect(handleInputCommand).toHaveBeenCalledWith('test content', {});
33
+ });
34
+
35
+ it('should handle feedback command', async () => {
36
+ const { handleFeedbackCommand } = require('../commands/feedback');
37
+
38
+ // Mock successful execution
39
+ (handleFeedbackCommand as jest.Mock).mockResolvedValue(undefined);
40
+
41
+ await handleFeedbackCommand('test-id', {});
42
+ expect(handleFeedbackCommand).toHaveBeenCalledWith('test-id', {});
43
+ });
44
+ });
@@ -0,0 +1,38 @@
1
+ import { config } from '../config';
2
+
3
+ // Mock the config for testing
4
+ jest.mock('../config', () => ({
5
+ config: {
6
+ get: jest.fn(),
7
+ set: jest.fn(),
8
+ getConfigPath: jest.fn()
9
+ }
10
+ }));
11
+
12
+ describe('Config Manager', () => {
13
+ beforeEach(() => {
14
+ jest.clearAllMocks();
15
+ });
16
+
17
+ it('should get config values', () => {
18
+ (config.get as jest.Mock).mockReturnValue({ apiKey: 'test-key' });
19
+
20
+ const result = config.get();
21
+ expect(result).toEqual({ apiKey: 'test-key' });
22
+ expect(config.get).toHaveBeenCalled();
23
+ });
24
+
25
+ it('should set config values', () => {
26
+ (config.set as jest.Mock).mockImplementation(() => {});
27
+
28
+ config.set('apiKey', 'new-key');
29
+ expect(config.set).toHaveBeenCalledWith('apiKey', 'new-key');
30
+ });
31
+
32
+ it('should get config path', () => {
33
+ (config.getConfigPath as jest.Mock).mockReturnValue('/path/to/config.json');
34
+
35
+ const result = config.getConfigPath();
36
+ expect(result).toBe('/path/to/config.json');
37
+ });
38
+ });
@@ -0,0 +1,132 @@
1
+ import {
2
+ CliError,
3
+ ERROR_CODES,
4
+ handleApiError,
5
+ handleValidationError,
6
+ handleInputError,
7
+ handleProcessingError
8
+ } from '../utils/errors';
9
+
10
+ describe('Error Utils', () => {
11
+ describe('CliError', () => {
12
+ it('should create error with message and code', () => {
13
+ const error = new CliError('Test error', ERROR_CODES.VALIDATION_ERROR);
14
+ expect(error.message).toBe('Test error');
15
+ expect(error.code).toBe(ERROR_CODES.VALIDATION_ERROR);
16
+ expect(error.exitCode).toBe(1);
17
+ });
18
+
19
+ it('should create error with custom exit code', () => {
20
+ const error = new CliError('Test error', ERROR_CODES.VALIDATION_ERROR, 2);
21
+ expect(error.exitCode).toBe(2);
22
+ });
23
+ });
24
+
25
+ describe('handleApiError', () => {
26
+ it('should handle 401 errors', () => {
27
+ const mockError = {
28
+ response: {
29
+ status: 401,
30
+ data: { message: 'Unauthorized' }
31
+ }
32
+ };
33
+
34
+ const result = handleApiError(mockError);
35
+ expect(result.message).toContain('Invalid API key');
36
+ expect(result.code).toBe(ERROR_CODES.UNAUTHORIZED);
37
+ });
38
+
39
+ it('should handle 404 errors', () => {
40
+ const mockError = {
41
+ response: {
42
+ status: 404,
43
+ data: { message: 'Not found' }
44
+ }
45
+ };
46
+
47
+ const result = handleApiError(mockError);
48
+ expect(result.message).toContain('Resource not found');
49
+ expect(result.code).toBe(ERROR_CODES.NOT_FOUND);
50
+ });
51
+
52
+ it('should handle 429 errors', () => {
53
+ const mockError = {
54
+ response: {
55
+ status: 429,
56
+ data: { message: 'Rate limited' }
57
+ }
58
+ };
59
+
60
+ const result = handleApiError(mockError);
61
+ expect(result.message).toContain('Rate limit exceeded');
62
+ expect(result.code).toBe(ERROR_CODES.RATE_LIMITED);
63
+ });
64
+
65
+ it('should handle 500 errors', () => {
66
+ const mockError = {
67
+ response: {
68
+ status: 500,
69
+ data: { message: 'Internal server error' }
70
+ }
71
+ };
72
+
73
+ const result = handleApiError(mockError);
74
+ expect(result.message).toContain('Server error');
75
+ expect(result.code).toBe(ERROR_CODES.SERVER_ERROR);
76
+ });
77
+
78
+ it('should handle connection errors', () => {
79
+ const mockError = {
80
+ code: 'ENOTFOUND'
81
+ };
82
+
83
+ const result = handleApiError(mockError);
84
+ expect(result.message).toContain('Cannot connect to ClosedLoop servers');
85
+ expect(result.code).toBe(ERROR_CODES.CONNECTION_ERROR);
86
+ });
87
+
88
+ it('should handle timeout errors', () => {
89
+ const mockError = {
90
+ code: 'ETIMEDOUT'
91
+ };
92
+
93
+ const result = handleApiError(mockError);
94
+ expect(result.message).toContain('Request timed out');
95
+ expect(result.code).toBe(ERROR_CODES.CONNECTION_ERROR);
96
+ });
97
+
98
+ it('should handle unknown errors', () => {
99
+ const mockError = {
100
+ message: 'Unknown error'
101
+ };
102
+
103
+ const result = handleApiError(mockError);
104
+ expect(result.message).toBe('Unknown error');
105
+ expect(result.code).toBe(ERROR_CODES.UNKNOWN_ERROR);
106
+ });
107
+ });
108
+
109
+ describe('handleValidationError', () => {
110
+ it('should create validation error', () => {
111
+ const result = handleValidationError('Validation failed');
112
+ expect(result.message).toBe('Validation failed');
113
+ expect(result.code).toBe(ERROR_CODES.VALIDATION_ERROR);
114
+ });
115
+ });
116
+
117
+ describe('handleInputError', () => {
118
+ it('should create input error', () => {
119
+ const result = handleInputError('Input invalid');
120
+ expect(result.message).toBe('Input invalid');
121
+ expect(result.code).toBe(ERROR_CODES.INVALID_INPUT);
122
+ });
123
+ });
124
+
125
+ describe('handleProcessingError', () => {
126
+ it('should create processing error', () => {
127
+ const result = handleProcessingError('Processing failed');
128
+ expect(result.message).toBe('Processing failed');
129
+ expect(result.code).toBe(ERROR_CODES.PROCESSING_ERROR);
130
+ });
131
+ });
132
+ });
@@ -0,0 +1,21 @@
1
+ // Test setup file
2
+ // This file runs before each test file
3
+
4
+ // Mock console methods to avoid cluttering test output
5
+ global.console = {
6
+ ...console,
7
+ log: jest.fn(),
8
+ error: jest.fn(),
9
+ warn: jest.fn(),
10
+ info: jest.fn()
11
+ };
12
+
13
+ // Mock process.exit to prevent tests from actually exiting
14
+ const originalExit = process.exit;
15
+ beforeAll(() => {
16
+ process.exit = jest.fn() as any;
17
+ });
18
+
19
+ afterAll(() => {
20
+ process.exit = originalExit;
21
+ });
@@ -0,0 +1,156 @@
1
+ import {
2
+ validateUuid,
3
+ validateInputContent,
4
+ validateApiKey,
5
+ validateEmail,
6
+ validateUrl,
7
+ validatePagination,
8
+ validateTitle,
9
+ validateCustomerId,
10
+ validateName,
11
+ sanitizeInput,
12
+ validateAndSanitizeInput
13
+ } from '../utils/validation';
14
+ import { CliError } from '../utils/errors';
15
+
16
+ describe('Validation Utils', () => {
17
+ describe('validateUuid', () => {
18
+ it('should validate correct UUIDs', () => {
19
+ expect(validateUuid('123e4567-e89b-12d3-a456-426614174000')).toBe(true);
20
+ expect(validateUuid('00000000-0000-0000-0000-000000000000')).toBe(true);
21
+ });
22
+
23
+ it('should reject invalid UUIDs', () => {
24
+ expect(validateUuid('invalid-uuid')).toBe(false);
25
+ expect(validateUuid('123e4567-e89b-12d3-a456')).toBe(false);
26
+ expect(validateUuid('')).toBe(false);
27
+ });
28
+ });
29
+
30
+ describe('validateInputContent', () => {
31
+ it('should validate correct content', () => {
32
+ expect(validateInputContent('Valid content')).toBe('Valid content');
33
+ expect(validateInputContent(' Valid content ')).toBe('Valid content');
34
+ });
35
+
36
+ it('should reject empty content', () => {
37
+ expect(() => validateInputContent('')).toThrow(CliError);
38
+ expect(() => validateInputContent(' ')).toThrow(CliError);
39
+ });
40
+
41
+ it('should reject content that is too long', () => {
42
+ const longContent = 'a'.repeat(10001);
43
+ expect(() => validateInputContent(longContent)).toThrow(CliError);
44
+ });
45
+ });
46
+
47
+ describe('validateApiKey', () => {
48
+ it('should validate correct API keys', () => {
49
+ expect(validateApiKey('valid-api-key-123')).toBe('valid-api-key-123');
50
+ expect(validateApiKey(' valid-api-key-123 ')).toBe('valid-api-key-123');
51
+ });
52
+
53
+ it('should reject empty API keys', () => {
54
+ expect(() => validateApiKey('')).toThrow(CliError);
55
+ expect(() => validateApiKey(' ')).toThrow(CliError);
56
+ });
57
+
58
+ it('should reject API keys that are too short', () => {
59
+ expect(() => validateApiKey('short')).toThrow(CliError);
60
+ });
61
+ });
62
+
63
+ describe('validateEmail', () => {
64
+ it('should validate correct emails', () => {
65
+ expect(validateEmail('test@example.com')).toBe('test@example.com');
66
+ expect(validateEmail(' TEST@EXAMPLE.COM ')).toBe('test@example.com');
67
+ });
68
+
69
+ it('should reject invalid emails', () => {
70
+ expect(() => validateEmail('invalid-email')).toThrow(CliError);
71
+ expect(() => validateEmail('test@')).toThrow(CliError);
72
+ expect(() => validateEmail('@example.com')).toThrow(CliError);
73
+ });
74
+ });
75
+
76
+ describe('validateUrl', () => {
77
+ it('should validate correct URLs', () => {
78
+ expect(validateUrl('https://example.com')).toBe('https://example.com/');
79
+ expect(validateUrl('http://example.com')).toBe('http://example.com/');
80
+ });
81
+
82
+ it('should reject invalid URLs', () => {
83
+ expect(() => validateUrl('invalid-url')).toThrow(CliError);
84
+ expect(() => validateUrl('ftp://example.com')).toThrow(CliError);
85
+ });
86
+ });
87
+
88
+ describe('validatePagination', () => {
89
+ it('should validate correct pagination', () => {
90
+ expect(validatePagination(1, 20)).toEqual({ page: 1, limit: 20 });
91
+ expect(validatePagination(undefined, undefined)).toEqual({ page: 1, limit: 20 });
92
+ });
93
+
94
+ it('should enforce minimum values', () => {
95
+ expect(validatePagination(0, 0)).toEqual({ page: 1, limit: 20 });
96
+ expect(validatePagination(-1, -1)).toEqual({ page: 1, limit: 1 });
97
+ });
98
+
99
+ it('should enforce maximum limit', () => {
100
+ const result = validatePagination(1, 1000);
101
+ expect(result.limit).toBeLessThanOrEqual(100);
102
+ });
103
+ });
104
+
105
+ describe('sanitizeInput', () => {
106
+ it('should sanitize dangerous content', () => {
107
+ expect(sanitizeInput('<script>alert("xss")</script>')).toBe('scriptalert("xss")/script');
108
+ expect(sanitizeInput('javascript:alert("xss")')).toBe('alert("xss")');
109
+ expect(sanitizeInput('onclick="alert(\'xss\')"')).toBe('"alert(\'xss\')"');
110
+ });
111
+
112
+ it('should preserve safe content', () => {
113
+ expect(sanitizeInput('Safe content')).toBe('Safe content');
114
+ expect(sanitizeInput(' Safe content ')).toBe('Safe content');
115
+ });
116
+ });
117
+
118
+ describe('validateAndSanitizeInput', () => {
119
+ it('should validate and sanitize complete input data', () => {
120
+ const inputData = {
121
+ content: 'Test content',
122
+ title: 'Test title',
123
+ source_url: 'https://example.com',
124
+ customer_id: 'customer-123',
125
+ reporter_name: 'John Doe',
126
+ reporter_email: 'john@example.com'
127
+ };
128
+
129
+ const result = validateAndSanitizeInput(inputData);
130
+ expect(result.content).toBe('Test content');
131
+ expect(result.title).toBe('Test title');
132
+ expect(result.source_url).toBe('https://example.com/');
133
+ expect(result.customer_id).toBe('customer-123');
134
+ expect(result.reporter_name).toBe('John Doe');
135
+ expect(result.reporter_email).toBe('john@example.com');
136
+ });
137
+
138
+ it('should handle optional fields', () => {
139
+ const inputData = {
140
+ content: 'Test content'
141
+ };
142
+
143
+ const result = validateAndSanitizeInput(inputData);
144
+ expect(result.content).toBe('Test content');
145
+ expect(result.title).toBeUndefined();
146
+ });
147
+
148
+ it('should reject invalid content', () => {
149
+ const inputData = {
150
+ content: ''
151
+ };
152
+
153
+ expect(() => validateAndSanitizeInput(inputData)).toThrow(CliError);
154
+ });
155
+ });
156
+ });