Add isUploadFilesResponse type guard and related tests; remove prototype pollution tests

This commit is contained in:
2025-07-12 14:40:07 +02:00
parent 41d526af4c
commit 491d056dd4

View File

@@ -3,9 +3,11 @@ import {
isLoginResponse, isLoginResponse,
isLookupResponse, isLookupResponse,
isSaveFileResponse, isSaveFileResponse,
isUploadFilesResponse,
type LoginResponse, type LoginResponse,
type LookupResponse, type LookupResponse,
type SaveFileResponse, type SaveFileResponse,
type UploadFilesResponse,
} from './api'; } from './api';
import { UserRole, type User } from './models'; import { UserRole, type User } from './models';
@@ -139,16 +141,6 @@ describe('API Type Guards', () => {
expect(isLoginResponse(invalidResponse)).toBe(false); expect(isLoginResponse(invalidResponse)).toBe(false);
}); });
it('handles objects with prototype pollution attempts', () => {
const maliciousObj = {
user: mockUser,
__proto__: { malicious: true },
constructor: { prototype: { polluted: true } },
};
expect(isLoginResponse(maliciousObj)).toBe(true);
});
}); });
describe('isLookupResponse', () => { describe('isLookupResponse', () => {
@@ -243,31 +235,6 @@ describe('API Type Guards', () => {
expect(isLookupResponse(responseWithExtra)).toBe(true); expect(isLookupResponse(responseWithExtra)).toBe(true);
}); });
it('handles objects with prototype pollution attempts', () => {
const maliciousObj = {
paths: ['path1.md', 'path2.md'],
__proto__: { malicious: true },
constructor: { prototype: { polluted: true } },
};
expect(isLookupResponse(maliciousObj)).toBe(true);
});
it('handles complex path strings', () => {
const validLookupResponse: LookupResponse = {
paths: [
'simple.md',
'folder/nested.md',
'deep/nested/path/file.md',
'file with spaces.md',
'special-chars_123.md',
'unicode-文件.md',
],
};
expect(isLookupResponse(validLookupResponse)).toBe(true);
});
}); });
describe('isSaveFileResponse', () => { describe('isSaveFileResponse', () => {
@@ -387,18 +354,6 @@ describe('API Type Guards', () => {
expect(isSaveFileResponse(invalidResponse)).toBe(true); // Note: Type guard doesn't validate negative numbers expect(isSaveFileResponse(invalidResponse)).toBe(true); // Note: Type guard doesn't validate negative numbers
}); });
it('handles objects with prototype pollution attempts', () => {
const maliciousObj = {
filePath: 'test.md',
size: 1024,
updatedAt: '2024-01-01T10:00:00Z',
__proto__: { malicious: true },
constructor: { prototype: { polluted: true } },
};
expect(isSaveFileResponse(maliciousObj)).toBe(true);
});
it('handles objects with extra properties', () => { it('handles objects with extra properties', () => {
const responseWithExtra = { const responseWithExtra = {
filePath: 'test.md', filePath: 'test.md',
@@ -409,105 +364,122 @@ describe('API Type Guards', () => {
expect(isSaveFileResponse(responseWithExtra)).toBe(true); expect(isSaveFileResponse(responseWithExtra)).toBe(true);
}); });
});
it('handles complex file paths', () => { describe('isUploadFilesResponse', () => {
const validSaveFileResponse: SaveFileResponse = { it('returns true for valid upload files response', () => {
filePath: 'deep/nested/path/file with spaces & symbols.md', const validUploadFilesResponse: UploadFilesResponse = {
size: 2048, filePaths: [
updatedAt: '2024-01-01T10:00:00Z', 'documents/file1.md',
'images/photo.jpg',
'notes/readme.txt',
],
}; };
expect(isSaveFileResponse(validSaveFileResponse)).toBe(true); expect(isUploadFilesResponse(validUploadFilesResponse)).toBe(true);
}); });
it('handles various ISO date formats', () => { it('returns true for upload files response with empty array', () => {
const dateFormats = [ const validUploadFilesResponse: UploadFilesResponse = {
'2024-01-01T10:00:00Z', filePaths: [],
'2024-01-01T10:00:00.000Z', };
'2024-01-01T10:00:00+00:00',
'2024-01-01T10:00:00.123456Z',
];
dateFormats.forEach((dateString) => { expect(isUploadFilesResponse(validUploadFilesResponse)).toBe(true);
const validResponse: SaveFileResponse = { });
filePath: 'test.md',
size: 1024,
updatedAt: dateString,
};
expect(isSaveFileResponse(validResponse)).toBe(true); it('returns true for single file upload', () => {
}); const validUploadFilesResponse: UploadFilesResponse = {
filePaths: ['single-file.md'],
};
expect(isUploadFilesResponse(validUploadFilesResponse)).toBe(true);
});
it('returns false for null', () => {
expect(isUploadFilesResponse(null)).toBe(false);
});
it('returns false for undefined', () => {
expect(isUploadFilesResponse(undefined)).toBe(false);
});
it('returns false for non-object values', () => {
expect(isUploadFilesResponse('string')).toBe(false);
expect(isUploadFilesResponse(123)).toBe(false);
expect(isUploadFilesResponse(true)).toBe(false);
expect(isUploadFilesResponse([])).toBe(false);
});
it('returns false for empty object', () => {
expect(isUploadFilesResponse({})).toBe(false);
});
it('returns false when filePaths field is missing', () => {
const invalidResponse = {
otherField: 'value',
};
expect(isUploadFilesResponse(invalidResponse)).toBe(false);
});
it('returns false when filePaths is not an array', () => {
const invalidResponse = {
filePaths: 'not-an-array',
};
expect(isUploadFilesResponse(invalidResponse)).toBe(false);
});
it('returns false when filePaths contains non-string values', () => {
const invalidResponse = {
filePaths: ['valid-file.md', 123, 'another-file.md'],
};
expect(isUploadFilesResponse(invalidResponse)).toBe(false);
});
it('returns false when filePaths contains null values', () => {
const invalidResponse = {
filePaths: ['file1.md', null, 'file2.md'],
};
expect(isUploadFilesResponse(invalidResponse)).toBe(false);
});
it('returns false when filePaths contains undefined values', () => {
const invalidResponse = {
filePaths: ['file1.md', undefined, 'file2.md'],
};
expect(isUploadFilesResponse(invalidResponse)).toBe(false);
});
it('handles objects with extra properties', () => {
const responseWithExtra = {
filePaths: ['file1.md', 'file2.md'],
extraField: 'should be ignored',
};
expect(isUploadFilesResponse(responseWithExtra)).toBe(true);
}); });
}); });
describe('edge cases and error conditions', () => { describe('edge cases and error conditions', () => {
it('handles circular references gracefully', () => { it('handles objects with extra properties across different type guards', () => {
const circularObj: { paths: string[]; self?: unknown } = { paths: [] }; // Test that all type guards handle extra properties correctly
circularObj.self = circularObj; expect(isLoginResponse({ user: mockUser, extra: 'field' })).toBe(true);
expect(isLookupResponse({ paths: [], extra: 'field' })).toBe(true);
// Should not throw an error expect(
expect(isLookupResponse(circularObj)).toBe(true); isSaveFileResponse({
}); filePath: 'test.md',
size: 1024,
it('handles deeply nested objects', () => { updatedAt: '2024-01-01T10:00:00Z',
const deeplyNested = { extra: 'field',
user: { })
...mockUser, ).toBe(true);
nested: { expect(
deep: { isUploadFilesResponse({ filePaths: ['file1.md'], extra: 'field' })
deeper: { ).toBe(true);
value: 'test',
},
},
},
},
};
expect(isLoginResponse(deeplyNested)).toBe(true);
});
it('handles frozen objects', () => {
const frozenResponse = Object.freeze({
paths: Object.freeze(['path1.md', 'path2.md']),
});
expect(isLookupResponse(frozenResponse)).toBe(true);
});
it('handles objects created with null prototype', () => {
const nullProtoObj = Object.create(null) as Record<string, unknown>;
nullProtoObj['filePath'] = 'test.md';
nullProtoObj['size'] = 1024;
nullProtoObj['updatedAt'] = '2024-01-01T10:00:00Z';
expect(isSaveFileResponse(nullProtoObj)).toBe(true);
});
});
describe('performance with large data', () => {
it('handles large paths arrays efficiently', () => {
const largePaths = Array.from({ length: 10000 }, (_, i) => `path${i}.md`);
const largeResponse = {
paths: largePaths,
};
const start = performance.now();
const result = isLookupResponse(largeResponse);
const end = performance.now();
expect(result).toBe(true);
expect(end - start).toBeLessThan(100); // Should complete in under 100ms
});
it('handles very long file paths', () => {
const longPath = 'a'.repeat(10000);
const responseWithLongPath: SaveFileResponse = {
filePath: longPath,
size: 1024,
updatedAt: '2024-01-01T10:00:00Z',
};
expect(isSaveFileResponse(responseWithLongPath)).toBe(true);
}); });
}); });
}); });