diff --git a/src/routes/__tests__/README.md b/src/routes/__tests__/README.md new file mode 100644 index 0000000..a7e1b64 --- /dev/null +++ b/src/routes/__tests__/README.md @@ -0,0 +1,37 @@ +# Variant Route Tests + +This directory contains tests for the variant route normalization functionality. + +## Test Files + +- `test-utils.ts` - Shared utilities for testing range normalization +- `variant.route.test.ts` - Unit tests for the `normalizeRange` function +- `variant.route.integration.test.ts` - Integration test simulating the `/api/variant/all` endpoint + +## Running Tests + +These tests are meant to be run with `ts-node`. They are excluded from the TypeScript build process (see `tsconfig.json`). + +To run the tests: + +```bash +# Install dependencies first +yarn install + +# Run unit tests +npx ts-node src/routes/__tests__/variant.route.test.ts + +# Run integration tests +npx ts-node src/routes/__tests__/variant.route.integration.test.ts +``` + +## What the Tests Validate + +The tests ensure that the `normalizeRange` function correctly: +- Handles empty strings +- Handles null/undefined values +- Handles whitespace-only strings +- Handles invalid JSON +- Handles malformed JSON (unclosed braces, etc.) +- Passes through valid JSON unchanged +- Always returns parseable JSON with `start` and `end` properties diff --git a/src/routes/__tests__/test-utils.ts b/src/routes/__tests__/test-utils.ts new file mode 100644 index 0000000..59d73a7 --- /dev/null +++ b/src/routes/__tests__/test-utils.ts @@ -0,0 +1,23 @@ +/** + * Test utilities for variant range normalization + */ + +// Default range value for malformed/empty range strings +export const DEFAULT_RANGE = '{"start":0,"end":0}'; + +export function normalizeRange(range: string | null | undefined, variantId: string): string { + // Check if range is falsy or empty + if (!range || range.trim() === '') { + console.log(`[Variant API] Normalizing empty range for variant ${variantId} - using default range`); + return DEFAULT_RANGE; + } + + // Try to parse the range to validate it's proper JSON + try { + JSON.parse(range); + return range; // Valid JSON, return as-is + } catch (error) { + console.log(`[Variant API] Normalizing invalid range for variant ${variantId} - using default range. Original value: "${range}"`); + return DEFAULT_RANGE; + } +} diff --git a/src/routes/__tests__/variant.route.integration.test.ts b/src/routes/__tests__/variant.route.integration.test.ts new file mode 100644 index 0000000..d11510b --- /dev/null +++ b/src/routes/__tests__/variant.route.integration.test.ts @@ -0,0 +1,58 @@ +/** + * Integration test to demonstrate the variant endpoint behavior + * This simulates what happens when the /api/variant/all endpoint is called + * Run with: npx ts-node src/routes/__tests__/variant.route.integration.test.ts + */ + +import { DEFAULT_RANGE, normalizeRange } from './test-utils'; + +// Simulate database response with various problematic range values +const mockDatabaseVariants = [ + { s_no: 'VAR-001', series: 'Series A', range: '{"start":10,"end":20}' }, // Valid + { s_no: 'VAR-002', series: 'Series B', range: '' }, // Empty string + { s_no: 'VAR-003', series: 'Series C', range: null }, // Null + { s_no: 'VAR-004', series: 'Series D', range: ' ' }, // Whitespace + { s_no: 'VAR-005', series: 'Series E', range: '{invalid json}' }, // Invalid JSON + { s_no: 'VAR-006', series: 'Series F', range: '{"start":0' }, // Malformed JSON + { s_no: 'VAR-007', series: 'Series G', range: '{"start":0,"end":0}' }, // Default valid +]; + +console.log('=== Simulating /api/variant/all endpoint ===\n'); + +// Simulate the endpoint logic +console.log('Step 1: Fetching variants from database...'); +console.log(`Found ${mockDatabaseVariants.length} variants\n`); + +console.log('Step 2: Normalizing range values...'); +const normalizedVariants = mockDatabaseVariants.map(variant => ({ + ...variant, + range: normalizeRange(variant.range as any, variant.s_no) +})); + +console.log('\nStep 3: Preparing response...'); +const response = { variants: normalizedVariants }; + +console.log('\n=== Final API Response ==='); +console.log(JSON.stringify(response, null, 2)); + +console.log('\n=== Verification ==='); +let allRangesParseable = true; +response.variants.forEach(variant => { + try { + const parsed = JSON.parse(variant.range); + console.log(`✓ ${variant.s_no}: range is parseable JSON with start=${parsed.start}, end=${parsed.end}`); + } catch (error) { + console.error(`✗ ${variant.s_no}: range is NOT parseable JSON: "${variant.range}"`); + allRangesParseable = false; + } +}); + +console.log('\n=== Result ==='); +if (allRangesParseable) { + console.log('✓ SUCCESS: All ranges are safe to parse!'); + console.log('✓ The API will never return unparseable range values.'); + process.exit(0); +} else { + console.error('✗ FAILURE: Some ranges are not parseable!'); + process.exit(1); +} diff --git a/src/routes/__tests__/variant.route.test.ts b/src/routes/__tests__/variant.route.test.ts new file mode 100644 index 0000000..14a165b --- /dev/null +++ b/src/routes/__tests__/variant.route.test.ts @@ -0,0 +1,110 @@ +/** + * Simple test file to demonstrate variant range normalization + * Run with: npx ts-node src/routes/__tests__/variant.route.test.ts + */ + +import { DEFAULT_RANGE, normalizeRange } from './test-utils'; + +// Test cases +console.log('=== Running Variant Range Normalization Tests ===\n'); + +let testsPassed = 0; +let testsFailed = 0; + +function test(description: string, fn: () => void) { + try { + fn(); + console.log(`✓ ${description}`); + testsPassed++; + } catch (error) { + console.error(`✗ ${description}`); + console.error(` Error: ${error}`); + testsFailed++; + } +} + +function assertEqual(actual: any, expected: any, message?: string) { + if (actual !== expected) { + throw new Error(message || `Expected ${expected}, but got ${actual}`); + } +} + +// Test 1: Valid JSON range should pass through unchanged +test('Valid JSON range should pass through unchanged', () => { + const input = '{"start":10,"end":20}'; + const result = normalizeRange(input, 'TEST-001'); + assertEqual(result, input, 'Valid JSON should not be modified'); +}); + +// Test 2: Empty string should be normalized to default +test('Empty string should be normalized to default', () => { + const result = normalizeRange('', 'TEST-002'); + assertEqual(result, DEFAULT_RANGE, 'Empty string should return default range'); +}); + +// Test 3: Null value should be normalized to default +test('Null value should be normalized to default', () => { + const result = normalizeRange(null, 'TEST-003'); + assertEqual(result, DEFAULT_RANGE, 'Null value should return default range'); +}); + +// Test 4: Undefined value should be normalized to default +test('Undefined value should be normalized to default', () => { + const result = normalizeRange(undefined, 'TEST-004'); + assertEqual(result, DEFAULT_RANGE, 'Undefined value should return default range'); +}); + +// Test 5: Whitespace-only string should be normalized to default +test('Whitespace-only string should be normalized to default', () => { + const result = normalizeRange(' ', 'TEST-005'); + assertEqual(result, DEFAULT_RANGE, 'Whitespace string should return default range'); +}); + +// Test 6: Invalid JSON should be normalized to default +test('Invalid JSON should be normalized to default', () => { + const result = normalizeRange('{invalid json}', 'TEST-006'); + assertEqual(result, DEFAULT_RANGE, 'Invalid JSON should return default range'); +}); + +// Test 7: Malformed JSON (unclosed brace) should be normalized +test('Malformed JSON (unclosed brace) should be normalized', () => { + const result = normalizeRange('{"start":0', 'TEST-007'); + assertEqual(result, DEFAULT_RANGE, 'Malformed JSON should return default range'); +}); + +// Test 8: Plain text should be normalized to default +test('Plain text should be normalized to default', () => { + const result = normalizeRange('not json at all', 'TEST-008'); + assertEqual(result, DEFAULT_RANGE, 'Plain text should return default range'); +}); + +// Test 9: Default range string should pass through +test('Default range string should pass through unchanged', () => { + const result = normalizeRange(DEFAULT_RANGE, 'TEST-009'); + assertEqual(result, DEFAULT_RANGE, 'Default range should pass through unchanged'); +}); + +// Test 10: Ensure normalized result is always parseable +test('Normalized result should always be parseable JSON', () => { + const testCases = ['', null, undefined, 'bad', '{incomplete', ' ']; + testCases.forEach((testCase, index) => { + const result = normalizeRange(testCase as any, `TEST-010-${index}`); + try { + const parsed = JSON.parse(result); + if (!parsed.hasOwnProperty('start') || !parsed.hasOwnProperty('end')) { + throw new Error('Parsed result does not have start and end properties'); + } + } catch (error) { + throw new Error(`Result "${result}" is not parseable JSON: ${error}`); + } + }); +}); + +console.log('\n=== Test Summary ==='); +console.log(`Passed: ${testsPassed}`); +console.log(`Failed: ${testsFailed}`); +console.log(`Total: ${testsPassed + testsFailed}`); + +if (testsFailed > 0) { + process.exit(1); +} diff --git a/src/routes/variant.route.ts b/src/routes/variant.route.ts index 5ed2497..081f6cd 100644 --- a/src/routes/variant.route.ts +++ b/src/routes/variant.route.ts @@ -3,6 +3,31 @@ import { prisma } from "../../prisma"; const variantRouter = Router(); +// Default range value for malformed/empty range strings +const DEFAULT_RANGE = '{"start":0,"end":0}'; + +/** + * Normalizes a range string to ensure it's valid JSON. + * If the range is empty, falsy, or invalid JSON, returns the default range. + * Logs when normalization occurs. + */ +function normalizeRange(range: string | null | undefined, variantId: string): string { + // Check if range is falsy or empty + if (!range || range.trim() === '') { + console.log(`[Variant API] Normalizing empty range for variant ${variantId} - using default range`); + return DEFAULT_RANGE; + } + + // Try to parse the range to validate it's proper JSON + try { + JSON.parse(range); + return range; // Valid JSON, return as-is + } catch (error) { + console.log(`[Variant API] Normalizing invalid range for variant ${variantId} - using default range. Original value: "${range}"`); + return DEFAULT_RANGE; + } +} + // variantRouter.get("/:sid", async (req, res) => { // const sid = req.params.sid; // await prisma.variant @@ -28,11 +53,18 @@ variantRouter.get("/all", async (req, res) => { const variants = await prisma.variant.findMany({ select: { s_no: true, - series: true, - range: true, + series: true, + range: true, }, }); - res.send({ variants: variants }); + + // Normalize range values to ensure they're always safe to parse + const normalizedVariants = variants.map(variant => ({ + ...variant, + range: normalizeRange(variant.range, variant.s_no) + })); + + res.send({ variants: normalizedVariants }); }); export { variantRouter }; diff --git a/tsconfig.json b/tsconfig.json index 06bd6d3..1cc3470 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,5 +6,9 @@ "lib": ["esnext", "es2015", "dom"], "esModuleInterop": true, "resolveJsonModule": true - } + }, + "exclude": [ + "node_modules", + "**/__tests__/**" + ] }