@optimizely/ocp-cli 1.2.13 → 1.2.14

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 (31) hide show
  1. package/dist/commands/app/Init.js +1 -1
  2. package/dist/commands/app/Init.js.map +1 -1
  3. package/dist/oo-cli.manifest.json +1 -1
  4. package/package.json +10 -6
  5. package/src/commands/app/Init.ts +1 -1
  6. package/src/test/e2e/__tests__/accounts/accounts.test.ts +120 -0
  7. package/src/test/e2e/__tests__/availability/availability.test.ts +156 -0
  8. package/src/test/e2e/__tests__/directory/directory.test.ts +668 -0
  9. package/src/test/e2e/__tests__/jobs/jobs.test.ts +487 -0
  10. package/src/test/e2e/__tests__/review/review.test.ts +355 -0
  11. package/src/test/e2e/config/fixture-loader.ts +130 -0
  12. package/src/test/e2e/config/setup.ts +29 -0
  13. package/src/test/e2e/config/test-data-config.ts +27 -0
  14. package/src/test/e2e/config/test-data-helpers.ts +23 -0
  15. package/src/test/e2e/fixtures/baselines/accounts/whoami.txt +11 -0
  16. package/src/test/e2e/fixtures/baselines/accounts/whois.txt +4 -0
  17. package/src/test/e2e/fixtures/baselines/directory/info.txt +7 -0
  18. package/src/test/e2e/fixtures/baselines/directory/list.txt +4 -0
  19. package/src/test/e2e/fixtures/baselines/jobs/list.txt +4 -0
  20. package/src/test/e2e/fixtures/baselines/review/list.txt +4 -0
  21. package/src/test/e2e/lib/base-test.ts +150 -0
  22. package/src/test/e2e/lib/command-discovery.ts +324 -0
  23. package/src/test/e2e/utils/baseline-normalizer.ts +79 -0
  24. package/src/test/e2e/utils/cli-executor.ts +349 -0
  25. package/src/test/e2e/utils/command-registry.ts +99 -0
  26. package/src/test/e2e/utils/output-validator.ts +661 -0
  27. package/src/test/setup.ts +3 -1
  28. package/src/test/tsconfig.json +17 -0
  29. package/dist/test/setup.d.ts +0 -0
  30. package/dist/test/setup.js +0 -4
  31. package/dist/test/setup.js.map +0 -1
@@ -0,0 +1,661 @@
1
+ import { CLIExecutionResult } from './cli-executor';
2
+
3
+ export enum OutputType {
4
+ JSON = 'json',
5
+ YAML = 'yaml',
6
+ TABLE = 'table',
7
+ PLAIN_TEXT = 'plain_text'
8
+ }
9
+
10
+ export interface ValidationResult {
11
+ isValid: boolean;
12
+ errors: string[];
13
+ warnings: string[];
14
+ outputType: OutputType;
15
+ metadata?: {
16
+ lineCount?: number;
17
+ columnCount?: number;
18
+ jsonKeys?: string[];
19
+ yamlKeys?: string[];
20
+ tableHeaders?: string[];
21
+ detectedPatterns?: string[];
22
+ exitCode?: number;
23
+ executionTime?: number;
24
+ timedOut?: boolean;
25
+ };
26
+ }
27
+
28
+ export interface SchemaValidationRule {
29
+ path: string;
30
+ type: 'string' | 'number' | 'boolean' | 'array' | 'object';
31
+ required?: boolean;
32
+ pattern?: RegExp;
33
+ minLength?: number;
34
+ maxLength?: number;
35
+ min?: number;
36
+ max?: number;
37
+ enum?: any[];
38
+ }
39
+
40
+ export interface TextPattern {
41
+ name: string;
42
+ pattern: RegExp;
43
+ required?: boolean;
44
+ description?: string;
45
+ }
46
+
47
+ export interface TableValidationOptions {
48
+ expectedColumns?: string[];
49
+ minRows?: number;
50
+ maxRows?: number;
51
+ columnSeparator?: string | RegExp;
52
+ headerRequired?: boolean;
53
+ allowEmptyRows?: boolean;
54
+ }
55
+
56
+ export interface JsonValidationOptions {
57
+ schema?: SchemaValidationRule[];
58
+ strictMode?: boolean;
59
+ allowAdditionalProperties?: boolean;
60
+ }
61
+
62
+ export interface YamlValidationOptions {
63
+ schema?: SchemaValidationRule[];
64
+ strictMode?: boolean;
65
+ allowMultipleDocuments?: boolean;
66
+ }
67
+
68
+ export interface TextValidationOptions {
69
+ patterns?: TextPattern[];
70
+ minLines?: number;
71
+ maxLines?: number;
72
+ encoding?: string;
73
+ allowEmptyOutput?: boolean;
74
+ }
75
+
76
+ export class OutputValidator {
77
+ /**
78
+ * Detect the type of output with improved accuracy
79
+ */
80
+ static detectOutputType(output: string): OutputType {
81
+ const trimmed = output.trim();
82
+
83
+ if (!trimmed) {
84
+ return OutputType.PLAIN_TEXT;
85
+ }
86
+
87
+ // Check for JSON first (most specific)
88
+ if (this.isValidJson(trimmed)) {
89
+ return OutputType.JSON;
90
+ }
91
+
92
+ // Check for YAML (improved detection)
93
+ if (this.looksLikeYaml(trimmed)) {
94
+ return OutputType.YAML;
95
+ }
96
+
97
+ // Check for table format (improved detection)
98
+ if (this.looksLikeTable(trimmed)) {
99
+ return OutputType.TABLE;
100
+ }
101
+
102
+ return OutputType.PLAIN_TEXT;
103
+ }
104
+
105
+ /**
106
+ * Comprehensive output validation with detailed results
107
+ */
108
+ static validateOutput(
109
+ output: string,
110
+ expectedType?: OutputType,
111
+ options?: JsonValidationOptions | YamlValidationOptions | TableValidationOptions | TextValidationOptions
112
+ ): ValidationResult {
113
+ const detectedType = this.detectOutputType(output);
114
+
115
+ // Check if detected type matches expected type
116
+ if (expectedType && detectedType !== expectedType) {
117
+ return {
118
+ isValid: false,
119
+ errors: [`Expected ${expectedType} output, but detected ${detectedType}`],
120
+ warnings: [],
121
+ outputType: detectedType
122
+ };
123
+ }
124
+
125
+ // Validate based on detected type
126
+ switch (detectedType) {
127
+ case OutputType.JSON:
128
+ return this.validateJson(output, options as JsonValidationOptions);
129
+ case OutputType.YAML:
130
+ return this.validateYaml(output, options as YamlValidationOptions);
131
+ case OutputType.TABLE:
132
+ return this.validateTable(output, options as TableValidationOptions);
133
+ case OutputType.PLAIN_TEXT:
134
+ return this.validatePlainText(output, options as TextValidationOptions);
135
+ default:
136
+ return {
137
+ isValid: false,
138
+ errors: [`Unknown output type: ${detectedType}`],
139
+ warnings: [],
140
+ outputType: detectedType
141
+ };
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Enhanced JSON validation with schema support
147
+ */
148
+ static validateJson(output: string, options: JsonValidationOptions = {}): ValidationResult {
149
+ const errors: string[] = [];
150
+ const warnings: string[] = [];
151
+ const metadata: any = {};
152
+
153
+ try {
154
+ const parsed = JSON.parse(output);
155
+
156
+ // Extract JSON keys for metadata
157
+ if (typeof parsed === 'object' && parsed !== null) {
158
+ metadata.jsonKeys = this.extractJsonKeys(parsed);
159
+ }
160
+
161
+ // Schema validation if provided
162
+ if (options.schema) {
163
+ const schemaErrors = this.validateJsonSchema(parsed, options.schema);
164
+ errors.push(...schemaErrors);
165
+ }
166
+
167
+ // Check for additional properties if strict mode
168
+ if (options.strictMode && !options.allowAdditionalProperties && options.schema) {
169
+ const additionalProps = this.findAdditionalProperties(parsed, options.schema);
170
+ if (additionalProps.length > 0) {
171
+ warnings.push(`Additional properties found: ${additionalProps.join(', ')}`);
172
+ }
173
+ }
174
+
175
+ return {
176
+ isValid: errors.length === 0,
177
+ errors,
178
+ warnings,
179
+ outputType: OutputType.JSON,
180
+ metadata
181
+ };
182
+ } catch (error) {
183
+ return {
184
+ isValid: false,
185
+ errors: [`Invalid JSON: ${error instanceof Error ? error.message : String(error)}`],
186
+ warnings: [],
187
+ outputType: OutputType.JSON
188
+ };
189
+ }
190
+ }
191
+
192
+ /**
193
+ * YAML validation with schema support
194
+ */
195
+ static validateYaml(output: string, options: YamlValidationOptions = {}): ValidationResult {
196
+ const errors: string[] = [];
197
+ const warnings: string[] = [];
198
+ const metadata: any = {};
199
+
200
+ try {
201
+ // Basic YAML parsing (simplified - would need yaml library for full support)
202
+ const yamlKeys = this.extractYamlKeys(output);
203
+ metadata.yamlKeys = yamlKeys;
204
+
205
+ // Check for multiple documents
206
+ const documentCount = (output.match(/^---/gm) || []).length;
207
+ if (documentCount > 1 && !options.allowMultipleDocuments) {
208
+ warnings.push(`Multiple YAML documents found (${documentCount})`);
209
+ }
210
+
211
+ // Additional YAML-specific warnings could be added here
212
+ if (options.strictMode && yamlKeys.length === 0) {
213
+ warnings.push('No YAML keys detected in strict mode');
214
+ }
215
+
216
+ // Basic structure validation
217
+ if (yamlKeys.length === 0) {
218
+ errors.push('No YAML keys found in output');
219
+ }
220
+
221
+ return {
222
+ isValid: errors.length === 0,
223
+ errors,
224
+ warnings,
225
+ outputType: OutputType.YAML,
226
+ metadata
227
+ };
228
+ } catch (error) {
229
+ return {
230
+ isValid: false,
231
+ errors: [`Invalid YAML: ${error instanceof Error ? error.message : String(error)}`],
232
+ warnings: [],
233
+ outputType: OutputType.YAML
234
+ };
235
+ }
236
+ }
237
+
238
+ /**
239
+ * Enhanced table validation with comprehensive options
240
+ */
241
+ static validateTable(output: string, options: TableValidationOptions = {}): ValidationResult {
242
+ const errors: string[] = [];
243
+ const warnings: string[] = [];
244
+ const metadata: any = {};
245
+
246
+ const lines = output.split('\n');
247
+ const nonEmptyLines = lines.filter(line => line.trim());
248
+
249
+ metadata.lineCount = lines.length;
250
+
251
+ if (nonEmptyLines.length === 0) {
252
+ errors.push('Empty table output');
253
+ return {
254
+ isValid: false,
255
+ errors,
256
+ warnings,
257
+ outputType: OutputType.TABLE,
258
+ metadata
259
+ };
260
+ }
261
+
262
+ // Detect table structure
263
+ const tableInfo = this.analyzeTableStructure(output, options.columnSeparator);
264
+ metadata.columnCount = tableInfo.columnCount;
265
+ metadata.tableHeaders = tableInfo.headers;
266
+
267
+ // Validate row count
268
+ if (options.minRows && tableInfo.dataRows < options.minRows) {
269
+ errors.push(`Expected at least ${options.minRows} data rows, found ${tableInfo.dataRows}`);
270
+ }
271
+ if (options.maxRows && tableInfo.dataRows > options.maxRows) {
272
+ errors.push(`Expected at most ${options.maxRows} data rows, found ${tableInfo.dataRows}`);
273
+ }
274
+
275
+ // Validate headers
276
+ if (options.headerRequired !== false && !tableInfo.hasHeader) {
277
+ errors.push('Table header is required but not found');
278
+ }
279
+
280
+ // Check for expected columns
281
+ if (options.expectedColumns && tableInfo.headers) {
282
+ const missingColumns = options.expectedColumns.filter(col =>
283
+ !tableInfo.headers!.some(header => header.toLowerCase().includes(col.toLowerCase()))
284
+ );
285
+ if (missingColumns.length > 0) {
286
+ errors.push(`Missing expected columns: ${missingColumns.join(', ')}`);
287
+ }
288
+ }
289
+
290
+ // Check for empty rows
291
+ if (!options.allowEmptyRows && tableInfo.emptyRows > 0) {
292
+ warnings.push(`Found ${tableInfo.emptyRows} empty rows in table`);
293
+ }
294
+
295
+ return {
296
+ isValid: errors.length === 0,
297
+ errors,
298
+ warnings,
299
+ outputType: OutputType.TABLE,
300
+ metadata
301
+ };
302
+ }
303
+
304
+ /**
305
+ * Plain text validation with pattern matching
306
+ */
307
+ static validatePlainText(output: string, options: TextValidationOptions = {}): ValidationResult {
308
+ const errors: string[] = [];
309
+ const warnings: string[] = [];
310
+ const metadata: any = {};
311
+
312
+ const lines = output.split('\n');
313
+ metadata.lineCount = lines.length;
314
+
315
+ // Check if empty output is allowed
316
+ if (!output.trim() && !options.allowEmptyOutput) {
317
+ errors.push('Empty output not allowed');
318
+ }
319
+
320
+ // Validate line count
321
+ if (options.minLines && lines.length < options.minLines) {
322
+ errors.push(`Expected at least ${options.minLines} lines, found ${lines.length}`);
323
+ }
324
+ if (options.maxLines && lines.length > options.maxLines) {
325
+ errors.push(`Expected at most ${options.maxLines} lines, found ${lines.length}`);
326
+ }
327
+
328
+ // Pattern matching
329
+ if (options.patterns) {
330
+ const detectedPatterns: string[] = [];
331
+ for (const pattern of options.patterns) {
332
+ const matches = output.match(pattern.pattern);
333
+ if (matches) {
334
+ detectedPatterns.push(pattern.name);
335
+ } else if (pattern.required) {
336
+ errors.push(`Required pattern '${pattern.name}' not found: ${pattern.description || pattern.pattern.toString()}`);
337
+ }
338
+ }
339
+ metadata.detectedPatterns = detectedPatterns;
340
+ }
341
+
342
+ return {
343
+ isValid: errors.length === 0,
344
+ errors,
345
+ warnings,
346
+ outputType: OutputType.PLAIN_TEXT,
347
+ metadata
348
+ };
349
+ }
350
+
351
+ /**
352
+ * Validate command execution result with comprehensive options
353
+ */
354
+ static validateResult(
355
+ result: CLIExecutionResult,
356
+ expectedType?: OutputType,
357
+ options?: JsonValidationOptions | YamlValidationOptions | TableValidationOptions | TextValidationOptions
358
+ ): ValidationResult {
359
+ // Check for command execution failure
360
+ if (result.exitCode !== 0) {
361
+ return {
362
+ isValid: false,
363
+ errors: [`Command failed with exit code ${result.exitCode}: ${result.stderr}`],
364
+ warnings: [],
365
+ outputType: OutputType.PLAIN_TEXT,
366
+ metadata: {
367
+ exitCode: result.exitCode,
368
+ executionTime: result.executionTime,
369
+ timedOut: result.timedOut
370
+ }
371
+ };
372
+ }
373
+
374
+ // Validate the output
375
+ return this.validateOutput(result.stdout, expectedType, options);
376
+ }
377
+
378
+ /**
379
+ * Create a detailed validation report
380
+ */
381
+ static createValidationReport(results: ValidationResult[]): string {
382
+ const report: string[] = [];
383
+
384
+ report.push('=== Output Validation Report ===\n');
385
+
386
+ let totalTests = results.length;
387
+ let passedTests = results.filter(r => r.isValid).length;
388
+ let failedTests = totalTests - passedTests;
389
+
390
+ report.push(`Total Tests: ${totalTests}`);
391
+ report.push(`Passed: ${passedTests}`);
392
+ report.push(`Failed: ${failedTests}`);
393
+ report.push(`Success Rate: ${((passedTests / totalTests) * 100).toFixed(1)}%\n`);
394
+
395
+ // Group by output type
396
+ const byType = results.reduce((acc, result, index) => {
397
+ if (!acc[result.outputType]) {
398
+ acc[result.outputType] = [];
399
+ }
400
+ acc[result.outputType].push({ result, index });
401
+ return acc;
402
+ }, {} as Record<OutputType, Array<{ result: ValidationResult; index: number }>>);
403
+
404
+ for (const [type, typeResults] of Object.entries(byType)) {
405
+ report.push(`\n--- ${type.toUpperCase()} Results ---`);
406
+
407
+ typeResults.forEach(({ result, index }) => {
408
+ const status = result.isValid ? '✓ PASS' : '✗ FAIL';
409
+ report.push(`Test ${index + 1}: ${status}`);
410
+
411
+ if (result.errors.length > 0) {
412
+ report.push(` Errors:`);
413
+ result.errors.forEach(error => report.push(` - ${error}`));
414
+ }
415
+
416
+ if (result.warnings.length > 0) {
417
+ report.push(` Warnings:`);
418
+ result.warnings.forEach(warning => report.push(` - ${warning}`));
419
+ }
420
+
421
+ if (result.metadata) {
422
+ report.push(` Metadata: ${JSON.stringify(result.metadata, null, 2)}`);
423
+ }
424
+ });
425
+ }
426
+
427
+ return report.join('\n');
428
+ }
429
+
430
+ // Private helper methods
431
+ private static isValidJson(str: string): boolean {
432
+ try {
433
+ JSON.parse(str);
434
+ return true;
435
+ } catch {
436
+ return false;
437
+ }
438
+ }
439
+
440
+ private static looksLikeYaml(str: string): boolean {
441
+ const lines = str.split('\n');
442
+
443
+ // YAML characteristics
444
+ const hasColons = str.includes(':');
445
+ const hasIndentation = lines.some(line => line.match(/^\s+/));
446
+ const hasListItems = lines.some(line => line.trim().startsWith('-'));
447
+ const hasDocumentSeparator = str.includes('---');
448
+
449
+ // Must have colons and either indentation, list items, or document separators
450
+ return hasColons && (hasIndentation || hasListItems || hasDocumentSeparator);
451
+ }
452
+
453
+ private static looksLikeTable(str: string): boolean {
454
+ const lines = str.split('\n').filter(line => line.trim());
455
+
456
+ if (lines.length < 2) return false;
457
+
458
+ // Check for common table patterns
459
+ const hasPipes = lines.some(line => line.includes('|'));
460
+ const hasTabs = lines.some(line => line.includes('\t'));
461
+ const hasMultipleSpaces = lines.some(line => /\s{2,}/.test(line));
462
+ const hasConsistentColumns = this.hasConsistentColumnStructure(lines);
463
+
464
+ return hasPipes || hasTabs || (hasMultipleSpaces && hasConsistentColumns);
465
+ }
466
+
467
+ private static hasConsistentColumnStructure(lines: string[]): boolean {
468
+ if (lines.length < 2) return false;
469
+
470
+ // Check if lines have similar structure (similar number of words/columns)
471
+ const wordCounts = lines.map(line => line.trim().split(/\s+/).length);
472
+ const avgWordCount = wordCounts.reduce((a, b) => a + b, 0) / wordCounts.length;
473
+
474
+ // Allow some variance but expect consistency
475
+ return wordCounts.every(count => Math.abs(count - avgWordCount) <= 2);
476
+ }
477
+
478
+ private static extractJsonKeys(obj: any, prefix = ''): string[] {
479
+ const keys: string[] = [];
480
+
481
+ if (typeof obj === 'object' && obj !== null) {
482
+ if (Array.isArray(obj)) {
483
+ obj.forEach((item, index) => {
484
+ const itemKeys = this.extractJsonKeys(item, `${prefix}[${index}]`);
485
+ keys.push(...itemKeys);
486
+ });
487
+ } else {
488
+ Object.keys(obj).forEach(key => {
489
+ const fullKey = prefix ? `${prefix}.${key}` : key;
490
+ keys.push(fullKey);
491
+
492
+ const nestedKeys = this.extractJsonKeys(obj[key], fullKey);
493
+ keys.push(...nestedKeys);
494
+ });
495
+ }
496
+ }
497
+
498
+ return keys;
499
+ }
500
+
501
+ private static extractYamlKeys(yamlStr: string): string[] {
502
+ const keys: string[] = [];
503
+ const lines = yamlStr.split('\n');
504
+
505
+ for (const line of lines) {
506
+ const trimmed = line.trim();
507
+ if (trimmed && trimmed.includes(':') && !trimmed.startsWith('#')) {
508
+ const colonIndex = trimmed.indexOf(':');
509
+ const key = trimmed.substring(0, colonIndex).trim();
510
+ if (key && !key.startsWith('-')) {
511
+ keys.push(key);
512
+ }
513
+ }
514
+ }
515
+
516
+ return keys;
517
+ }
518
+
519
+ private static analyzeTableStructure(output: string, separator?: string | RegExp) {
520
+ const lines = output.split('\n');
521
+ const nonEmptyLines = lines.filter(line => line.trim());
522
+
523
+ let headers: string[] | null = null;
524
+ let columnCount = 0;
525
+ let dataRows = 0;
526
+ let emptyRows = 0;
527
+ let hasHeader = false;
528
+
529
+ if (nonEmptyLines.length > 0) {
530
+ // Assume first non-empty line is header
531
+ const firstLine = nonEmptyLines[0];
532
+
533
+ if (separator) {
534
+ headers = firstLine.split(separator).map(h => h.trim());
535
+ } else {
536
+ // Auto-detect separator
537
+ if (firstLine.includes('|')) {
538
+ headers = firstLine.split('|').map(h => h.trim()).filter(h => h);
539
+ } else if (firstLine.includes('\t')) {
540
+ headers = firstLine.split('\t').map(h => h.trim());
541
+ } else {
542
+ headers = firstLine.split(/\s{2,}/).map(h => h.trim());
543
+ }
544
+ }
545
+
546
+ columnCount = headers.length;
547
+ hasHeader = true;
548
+
549
+ // Count actual data rows, excluding header and separator lines
550
+ dataRows = nonEmptyLines.slice(1).filter(line => {
551
+ // Skip markdown-style separator lines like |------|-----|------|
552
+ const trimmed = line.trim();
553
+ return !this.isTableSeparatorLine(trimmed);
554
+ }).length;
555
+ }
556
+
557
+ emptyRows = lines.length - nonEmptyLines.length;
558
+
559
+ return {
560
+ headers,
561
+ columnCount,
562
+ dataRows,
563
+ emptyRows,
564
+ hasHeader
565
+ };
566
+ }
567
+
568
+ private static isTableSeparatorLine(line: string): boolean {
569
+ // Check if line is a markdown-style table separator (e.g., |------|-----|------|)
570
+ const trimmed = line.trim();
571
+ if (trimmed.includes('|')) {
572
+ // Remove pipes and check if remaining content is mostly dashes and spaces
573
+ const withoutPipes = trimmed.replace(/\|/g, '');
574
+ return /^[\s\-:]*$/.test(withoutPipes);
575
+ }
576
+ return false;
577
+ }
578
+
579
+ private static validateJsonSchema(data: any, schema: SchemaValidationRule[]): string[] {
580
+ const errors: string[] = [];
581
+
582
+ for (const rule of schema) {
583
+ const value = this.getValueByPath(data, rule.path);
584
+
585
+ // Check if required field is missing
586
+ if (rule.required && (value === undefined || value === null)) {
587
+ errors.push(`Required field '${rule.path}' is missing`);
588
+ continue;
589
+ }
590
+
591
+ // Skip validation if field is not required and missing
592
+ if (!rule.required && (value === undefined || value === null)) {
593
+ continue;
594
+ }
595
+
596
+ // Type validation
597
+ if (!this.validateType(value, rule.type)) {
598
+ errors.push(`Field '${rule.path}' expected type ${rule.type}, got ${typeof value}`);
599
+ continue;
600
+ }
601
+
602
+ // Additional validations based on type
603
+ if (rule.type === 'string' && typeof value === 'string') {
604
+ if (rule.pattern && !rule.pattern.test(value)) {
605
+ errors.push(`Field '${rule.path}' does not match pattern ${rule.pattern}`);
606
+ }
607
+ if (rule.minLength && value.length < rule.minLength) {
608
+ errors.push(`Field '${rule.path}' length ${value.length} is less than minimum ${rule.minLength}`);
609
+ }
610
+ if (rule.maxLength && value.length > rule.maxLength) {
611
+ errors.push(`Field '${rule.path}' length ${value.length} exceeds maximum ${rule.maxLength}`);
612
+ }
613
+ }
614
+
615
+ if (rule.type === 'number' && typeof value === 'number') {
616
+ if (rule.min !== undefined && value < rule.min) {
617
+ errors.push(`Field '${rule.path}' value ${value} is less than minimum ${rule.min}`);
618
+ }
619
+ if (rule.max !== undefined && value > rule.max) {
620
+ errors.push(`Field '${rule.path}' value ${value} exceeds maximum ${rule.max}`);
621
+ }
622
+ }
623
+
624
+ if (rule.enum && !rule.enum.includes(value)) {
625
+ errors.push(`Field '${rule.path}' value '${value}' is not in allowed values: ${rule.enum.join(', ')}`);
626
+ }
627
+ }
628
+
629
+ return errors;
630
+ }
631
+
632
+ private static getValueByPath(obj: any, path: string): any {
633
+ return path.split('.').reduce((current, key) => {
634
+ return current && current[key] !== undefined ? current[key] : undefined;
635
+ }, obj);
636
+ }
637
+
638
+ private static validateType(value: any, expectedType: string): boolean {
639
+ switch (expectedType) {
640
+ case 'string':
641
+ return typeof value === 'string';
642
+ case 'number':
643
+ return typeof value === 'number' && !isNaN(value);
644
+ case 'boolean':
645
+ return typeof value === 'boolean';
646
+ case 'array':
647
+ return Array.isArray(value);
648
+ case 'object':
649
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
650
+ default:
651
+ return false;
652
+ }
653
+ }
654
+
655
+ private static findAdditionalProperties(data: any, schema: SchemaValidationRule[]): string[] {
656
+ const definedPaths = new Set(schema.map(rule => rule.path));
657
+ const actualPaths = this.extractJsonKeys(data);
658
+
659
+ return actualPaths.filter(path => !definedPaths.has(path));
660
+ }
661
+ }
package/src/test/setup.ts CHANGED
@@ -1,2 +1,4 @@
1
+ import { vi } from 'vitest';
2
+
1
3
  // silence expected console.error()
2
- jest.spyOn(global.console, 'error').mockImplementation(() => jest.fn());
4
+ vi.spyOn(global.console, 'error').mockImplementation(() => vi.fn());
@@ -0,0 +1,17 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "include": [
4
+ "**/*.ts"
5
+ ],
6
+ "exclude": [
7
+ "node_modules"
8
+ ],
9
+ "compilerOptions": {
10
+ "types": ["vitest/globals", "node"],
11
+ "moduleResolution": "node",
12
+ "allowSyntheticDefaultImports": true,
13
+ "esModuleInterop": true,
14
+ "skipLibCheck": true,
15
+ "noEmit": true
16
+ }
17
+ }
File without changes
@@ -1,4 +0,0 @@
1
- "use strict";
2
- // silence expected console.error()
3
- jest.spyOn(global.console, 'error').mockImplementation(() => jest.fn());
4
- //# sourceMappingURL=setup.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"setup.js","sourceRoot":"","sources":["../../src/test/setup.ts"],"names":[],"mappings":";AAAA,mCAAmC;AACnC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,kBAAkB,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC"}