Add tests for API and Models type guards

This commit is contained in:
2025-05-29 13:10:20 +02:00
parent 2211f85193
commit 19771dd094
2 changed files with 1380 additions and 0 deletions

513
app/src/types/api.test.ts Normal file
View File

@@ -0,0 +1,513 @@
import { describe, it, expect } from 'vitest';
import {
isLoginResponse,
isLookupResponse,
isSaveFileResponse,
type LoginResponse,
type LookupResponse,
type SaveFileResponse,
} from './api';
import { UserRole, type User } from './models';
// Mock user data for testing
const mockUser: User = {
id: 1,
email: 'test@example.com',
displayName: 'Test User',
role: UserRole.Editor,
createdAt: '2024-01-01T00:00:00Z',
lastWorkspaceId: 1,
};
describe('API Type Guards', () => {
describe('isLoginResponse', () => {
it('returns true for valid login response with all fields', () => {
const validLoginResponse: LoginResponse = {
user: mockUser,
sessionId: 'session123',
expiresAt: '2024-01-01T12:00:00Z',
};
expect(isLoginResponse(validLoginResponse)).toBe(true);
});
it('returns true for valid login response with only required fields', () => {
const validLoginResponse: LoginResponse = {
user: mockUser,
};
expect(isLoginResponse(validLoginResponse)).toBe(true);
});
it('returns true for valid login response with sessionId only', () => {
const validLoginResponse: LoginResponse = {
user: mockUser,
sessionId: 'session123',
};
expect(isLoginResponse(validLoginResponse)).toBe(true);
});
it('returns true for valid login response with expiresAt only', () => {
const validLoginResponse: LoginResponse = {
user: mockUser,
expiresAt: '2024-01-01T12:00:00Z',
};
expect(isLoginResponse(validLoginResponse)).toBe(true);
});
it('returns false for null', () => {
expect(isLoginResponse(null)).toBe(false);
});
it('returns false for undefined', () => {
expect(isLoginResponse(undefined)).toBe(false);
});
it('returns false for non-object values', () => {
expect(isLoginResponse('string')).toBe(false);
expect(isLoginResponse(123)).toBe(false);
expect(isLoginResponse(true)).toBe(false);
expect(isLoginResponse([])).toBe(false);
});
it('returns false for empty object', () => {
expect(isLoginResponse({})).toBe(false);
});
it('returns false when user field is missing', () => {
const invalidResponse = {
sessionId: 'session123',
expiresAt: '2024-01-01T12:00:00Z',
};
expect(isLoginResponse(invalidResponse)).toBe(false);
});
it('returns false when user field is invalid', () => {
const invalidResponse = {
user: {
id: 'not-a-number', // Invalid user
email: 'test@example.com',
},
sessionId: 'session123',
};
expect(isLoginResponse(invalidResponse)).toBe(false);
});
it('returns false when sessionId is not a string', () => {
const invalidResponse = {
user: mockUser,
sessionId: 123, // Should be string
};
expect(isLoginResponse(invalidResponse)).toBe(false);
});
it('returns false when expiresAt is not a string', () => {
const invalidResponse = {
user: mockUser,
expiresAt: new Date(), // Should be string
};
expect(isLoginResponse(invalidResponse)).toBe(false);
});
it('handles objects with extra properties', () => {
const responseWithExtra = {
user: mockUser,
sessionId: 'session123',
expiresAt: '2024-01-01T12:00:00Z',
extraField: 'should be ignored',
};
expect(isLoginResponse(responseWithExtra)).toBe(true);
});
it('returns false for user with missing required fields', () => {
const invalidUser = {
id: 1,
email: 'test@example.com',
// Missing role, createdAt, lastWorkspaceId
};
const invalidResponse = {
user: invalidUser,
};
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', () => {
it('returns true for valid lookup response with paths', () => {
const validLookupResponse: LookupResponse = {
paths: ['path1.md', 'path2.md', 'folder/path3.md'],
};
expect(isLookupResponse(validLookupResponse)).toBe(true);
});
it('returns true for valid lookup response with empty paths array', () => {
const validLookupResponse: LookupResponse = {
paths: [],
};
expect(isLookupResponse(validLookupResponse)).toBe(true);
});
it('returns true for single path', () => {
const validLookupResponse: LookupResponse = {
paths: ['single-path.md'],
};
expect(isLookupResponse(validLookupResponse)).toBe(true);
});
it('returns false for null', () => {
expect(isLookupResponse(null)).toBe(false);
});
it('returns false for undefined', () => {
expect(isLookupResponse(undefined)).toBe(false);
});
it('returns false for non-object values', () => {
expect(isLookupResponse('string')).toBe(false);
expect(isLookupResponse(123)).toBe(false);
expect(isLookupResponse(true)).toBe(false);
expect(isLookupResponse([])).toBe(false);
});
it('returns false for empty object', () => {
expect(isLookupResponse({})).toBe(false);
});
it('returns false when paths field is missing', () => {
const invalidResponse = {
otherField: 'value',
};
expect(isLookupResponse(invalidResponse)).toBe(false);
});
it('returns false when paths is not an array', () => {
const invalidResponse = {
paths: 'not-an-array',
};
expect(isLookupResponse(invalidResponse)).toBe(false);
});
it('returns false when paths contains non-string values', () => {
const invalidResponse = {
paths: ['valid-path.md', 123, 'another-path.md'],
};
expect(isLookupResponse(invalidResponse)).toBe(false);
});
it('returns false when paths contains null values', () => {
const invalidResponse = {
paths: ['path1.md', null, 'path2.md'],
};
expect(isLookupResponse(invalidResponse)).toBe(false);
});
it('returns false when paths contains undefined values', () => {
const invalidResponse = {
paths: ['path1.md', undefined, 'path2.md'],
};
expect(isLookupResponse(invalidResponse)).toBe(false);
});
it('handles objects with extra properties', () => {
const responseWithExtra = {
paths: ['path1.md', 'path2.md'],
extraField: 'should be ignored',
};
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', () => {
it('returns true for valid save file response', () => {
const validSaveFileResponse: SaveFileResponse = {
filePath: 'documents/test.md',
size: 1024,
updatedAt: '2024-01-01T10:00:00Z',
};
expect(isSaveFileResponse(validSaveFileResponse)).toBe(true);
});
it('returns true for save file response with zero size', () => {
const validSaveFileResponse: SaveFileResponse = {
filePath: 'empty.md',
size: 0,
updatedAt: '2024-01-01T10:00:00Z',
};
expect(isSaveFileResponse(validSaveFileResponse)).toBe(true);
});
it('returns true for save file response with large size', () => {
const validSaveFileResponse: SaveFileResponse = {
filePath: 'large-file.md',
size: 999999999,
updatedAt: '2024-01-01T10:00:00Z',
};
expect(isSaveFileResponse(validSaveFileResponse)).toBe(true);
});
it('returns false for null', () => {
expect(isSaveFileResponse(null)).toBe(false);
});
it('returns false for undefined', () => {
expect(isSaveFileResponse(undefined)).toBe(false);
});
it('returns false for non-object values', () => {
expect(isSaveFileResponse('string')).toBe(false);
expect(isSaveFileResponse(123)).toBe(false);
expect(isSaveFileResponse(true)).toBe(false);
expect(isSaveFileResponse([])).toBe(false);
});
it('returns false for empty object', () => {
expect(isSaveFileResponse({})).toBe(false);
});
it('returns false when filePath is missing', () => {
const invalidResponse = {
size: 1024,
updatedAt: '2024-01-01T10:00:00Z',
};
expect(isSaveFileResponse(invalidResponse)).toBe(false);
});
it('returns false when filePath is not a string', () => {
const invalidResponse = {
filePath: 123,
size: 1024,
updatedAt: '2024-01-01T10:00:00Z',
};
expect(isSaveFileResponse(invalidResponse)).toBe(false);
});
it('returns false when size is missing', () => {
const invalidResponse = {
filePath: 'test.md',
updatedAt: '2024-01-01T10:00:00Z',
};
expect(isSaveFileResponse(invalidResponse)).toBe(false);
});
it('returns false when size is not a number', () => {
const invalidResponse = {
filePath: 'test.md',
size: '1024',
updatedAt: '2024-01-01T10:00:00Z',
};
expect(isSaveFileResponse(invalidResponse)).toBe(false);
});
it('returns false when updatedAt is missing', () => {
const invalidResponse = {
filePath: 'test.md',
size: 1024,
};
expect(isSaveFileResponse(invalidResponse)).toBe(false);
});
it('returns false when updatedAt is not a string', () => {
const invalidResponse = {
filePath: 'test.md',
size: 1024,
updatedAt: new Date(),
};
expect(isSaveFileResponse(invalidResponse)).toBe(false);
});
it('returns false for negative size', () => {
const invalidResponse = {
filePath: 'test.md',
size: -1,
updatedAt: '2024-01-01T10:00:00Z',
};
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', () => {
const responseWithExtra = {
filePath: 'test.md',
size: 1024,
updatedAt: '2024-01-01T10:00:00Z',
extraField: 'should be ignored',
};
expect(isSaveFileResponse(responseWithExtra)).toBe(true);
});
it('handles complex file paths', () => {
const validSaveFileResponse: SaveFileResponse = {
filePath: 'deep/nested/path/file with spaces & symbols.md',
size: 2048,
updatedAt: '2024-01-01T10:00:00Z',
};
expect(isSaveFileResponse(validSaveFileResponse)).toBe(true);
});
it('handles various ISO date formats', () => {
const dateFormats = [
'2024-01-01T10:00:00Z',
'2024-01-01T10:00:00.000Z',
'2024-01-01T10:00:00+00:00',
'2024-01-01T10:00:00.123456Z',
];
dateFormats.forEach((dateString) => {
const validResponse: SaveFileResponse = {
filePath: 'test.md',
size: 1024,
updatedAt: dateString,
};
expect(isSaveFileResponse(validResponse)).toBe(true);
});
});
});
describe('edge cases and error conditions', () => {
it('handles circular references gracefully', () => {
const circularObj: { paths: string[]; self?: unknown } = { paths: [] };
circularObj.self = circularObj;
// Should not throw an error
expect(isLookupResponse(circularObj)).toBe(true);
});
it('handles deeply nested objects', () => {
const deeplyNested = {
user: {
...mockUser,
nested: {
deep: {
deeper: {
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);
});
});
});

View File

@@ -0,0 +1,867 @@
import { describe, it, expect } from 'vitest';
import {
isUser,
isUserRole,
isWorkspace,
isFileNode,
isSystemStats,
UserRole,
Theme,
type User,
type Workspace,
type FileNode,
type SystemStats,
} from './models';
describe('Models Type Guards', () => {
describe('isUserRole', () => {
it('returns true for valid admin role', () => {
expect(isUserRole(UserRole.Admin)).toBe(true);
expect(isUserRole('admin')).toBe(true);
});
it('returns true for valid editor role', () => {
expect(isUserRole(UserRole.Editor)).toBe(true);
expect(isUserRole('editor')).toBe(true);
});
it('returns true for valid viewer role', () => {
expect(isUserRole(UserRole.Viewer)).toBe(true);
expect(isUserRole('viewer')).toBe(true);
});
it('returns false for invalid role strings', () => {
expect(isUserRole('invalid')).toBe(false);
expect(isUserRole('Administrator')).toBe(false);
expect(isUserRole('ADMIN')).toBe(false);
expect(isUserRole('')).toBe(false);
});
it('returns false for non-string values', () => {
expect(isUserRole(123)).toBe(false);
expect(isUserRole(null)).toBe(false);
expect(isUserRole(undefined)).toBe(false);
expect(isUserRole({})).toBe(false);
expect(isUserRole([])).toBe(false);
expect(isUserRole(true)).toBe(false);
});
it('handles objects with prototype pollution attempts', () => {
const maliciousRole = 'editor';
Object.setPrototypeOf(maliciousRole, {
toString: () => 'malicious',
valueOf: () => 'malicious',
});
expect(isUserRole(maliciousRole)).toBe(true);
});
});
describe('isUser', () => {
const validUser: User = {
id: 1,
email: 'test@example.com',
displayName: 'Test User',
role: UserRole.Editor,
createdAt: '2024-01-01T00:00:00Z',
lastWorkspaceId: 1,
};
it('returns true for valid user with all fields', () => {
expect(isUser(validUser)).toBe(true);
});
it('returns true for valid user without optional displayName', () => {
const userWithoutDisplayName: User = {
id: 1,
email: 'test@example.com',
role: UserRole.Editor,
createdAt: '2024-01-01T00:00:00Z',
lastWorkspaceId: 1,
};
expect(isUser(userWithoutDisplayName)).toBe(true);
});
it('returns true for valid user with empty displayName', () => {
const userWithEmptyDisplayName = {
...validUser,
displayName: '',
};
expect(isUser(userWithEmptyDisplayName)).toBe(true);
});
it('returns false for null', () => {
expect(isUser(null)).toBe(false);
});
it('returns false for undefined', () => {
expect(isUser(undefined)).toBe(false);
});
it('returns false for non-object values', () => {
expect(isUser('string')).toBe(false);
expect(isUser(123)).toBe(false);
expect(isUser(true)).toBe(false);
expect(isUser([])).toBe(false);
});
it('returns false for empty object', () => {
expect(isUser({})).toBe(false);
});
it('returns false when id is missing', () => {
const { id: _id, ...userWithoutId } = validUser;
expect(isUser(userWithoutId)).toBe(false);
});
it('returns false when id is not a number', () => {
const userWithInvalidId = { ...validUser, id: '1' };
expect(isUser(userWithInvalidId)).toBe(false);
});
it('returns false when email is missing', () => {
const { email: _email, ...userWithoutEmail } = validUser;
expect(isUser(userWithoutEmail)).toBe(false);
});
it('returns false when email is not a string', () => {
const userWithInvalidEmail = { ...validUser, email: 123 };
expect(isUser(userWithInvalidEmail)).toBe(false);
});
it('returns false when displayName is not a string', () => {
const userWithInvalidDisplayName = { ...validUser, displayName: 123 };
expect(isUser(userWithInvalidDisplayName)).toBe(false);
});
it('returns false when role is missing', () => {
const { role: _role, ...userWithoutRole } = validUser;
expect(isUser(userWithoutRole)).toBe(false);
});
it('returns false when role is invalid', () => {
const userWithInvalidRole = { ...validUser, role: 'invalid' };
expect(isUser(userWithInvalidRole)).toBe(false);
});
it('returns false when createdAt is missing', () => {
const { createdAt: _createdAt, ...userWithoutCreatedAt } = validUser;
expect(isUser(userWithoutCreatedAt)).toBe(false);
});
it('returns false when createdAt is not a string', () => {
const userWithInvalidCreatedAt = { ...validUser, createdAt: new Date() };
expect(isUser(userWithInvalidCreatedAt)).toBe(false);
});
it('returns false when lastWorkspaceId is missing', () => {
const {
lastWorkspaceId: _lastWorkspaceId,
...userWithoutLastWorkspaceId
} = validUser;
expect(isUser(userWithoutLastWorkspaceId)).toBe(false);
});
it('returns false when lastWorkspaceId is not a number', () => {
const userWithInvalidLastWorkspaceId = {
...validUser,
lastWorkspaceId: '1',
};
expect(isUser(userWithInvalidLastWorkspaceId)).toBe(false);
});
it('handles objects with extra properties', () => {
const userWithExtra = {
...validUser,
extraField: 'should be ignored',
};
expect(isUser(userWithExtra)).toBe(true);
});
it('handles objects with prototype pollution attempts', () => {
const maliciousUser = {
id: 1,
email: 'test@example.com',
role: UserRole.Editor,
createdAt: '2024-01-01T00:00:00Z',
lastWorkspaceId: 1,
__proto__: { malicious: true },
constructor: { prototype: { polluted: true } },
};
expect(isUser(maliciousUser)).toBe(true);
});
it('handles different user roles', () => {
expect(isUser({ ...validUser, role: UserRole.Admin })).toBe(true);
expect(isUser({ ...validUser, role: UserRole.Editor })).toBe(true);
expect(isUser({ ...validUser, role: UserRole.Viewer })).toBe(true);
});
it('handles various email formats', () => {
const emailFormats = [
'simple@example.com',
'user.name@example.com',
'user+tag@example.com',
'user123@example-domain.com',
'very.long.email.address@very.long.domain.name.com',
];
emailFormats.forEach((email) => {
expect(isUser({ ...validUser, email })).toBe(true);
});
});
});
describe('isWorkspace', () => {
const validWorkspace: Workspace = {
id: 1,
userId: 1,
name: 'test-workspace',
createdAt: '2024-01-01T00:00:00Z',
theme: Theme.Light,
autoSave: false,
showHiddenFiles: false,
gitEnabled: false,
gitUrl: '',
gitUser: '',
gitToken: '',
gitAutoCommit: false,
gitCommitMsgTemplate: '${action} ${filename}',
gitCommitName: '',
gitCommitEmail: '',
};
it('returns true for valid workspace with all fields', () => {
expect(isWorkspace(validWorkspace)).toBe(true);
});
it('returns true for workspace without optional id and userId', () => {
const {
id: _id,
userId: _userId,
...workspaceWithoutIds
} = validWorkspace;
expect(isWorkspace(workspaceWithoutIds)).toBe(true);
});
it('returns true for workspace with numeric createdAt', () => {
const workspaceWithNumericCreatedAt = {
...validWorkspace,
createdAt: Date.now(),
};
expect(isWorkspace(workspaceWithNumericCreatedAt)).toBe(true);
});
it('returns true for workspace with dark theme', () => {
const darkWorkspace = {
...validWorkspace,
theme: Theme.Dark,
};
expect(isWorkspace(darkWorkspace)).toBe(true);
});
it('returns true for workspace with git enabled', () => {
const gitWorkspace = {
...validWorkspace,
gitEnabled: true,
gitUrl: 'https://github.com/user/repo.git',
gitUser: 'username',
gitToken: 'token123',
gitAutoCommit: true,
gitCommitMsgTemplate: 'auto: ${action} ${filename}',
gitCommitName: 'Git User',
gitCommitEmail: 'git@example.com',
};
expect(isWorkspace(gitWorkspace)).toBe(true);
});
it('returns false for null', () => {
expect(isWorkspace(null)).toBe(false);
});
it('returns false for undefined', () => {
expect(isWorkspace(undefined)).toBe(false);
});
it('returns false for non-object values', () => {
expect(isWorkspace('string')).toBe(false);
expect(isWorkspace(123)).toBe(false);
expect(isWorkspace(true)).toBe(false);
expect(isWorkspace([])).toBe(false);
});
it('returns false for empty object', () => {
expect(isWorkspace({})).toBe(false);
});
it('returns false when name is missing', () => {
const { name: _name, ...workspaceWithoutName } = validWorkspace;
expect(isWorkspace(workspaceWithoutName)).toBe(false);
});
it('returns false when name is not a string', () => {
const workspaceWithInvalidName = { ...validWorkspace, name: 123 };
expect(isWorkspace(workspaceWithInvalidName)).toBe(false);
});
it('returns false when createdAt is missing', () => {
const { createdAt: _createdAt, ...workspaceWithoutCreatedAt } =
validWorkspace;
expect(isWorkspace(workspaceWithoutCreatedAt)).toBe(false);
});
it('returns false when createdAt is neither string nor number', () => {
const workspaceWithInvalidCreatedAt = {
...validWorkspace,
createdAt: new Date(),
};
expect(isWorkspace(workspaceWithInvalidCreatedAt)).toBe(false);
});
it('returns false when theme is missing', () => {
const { theme: _theme, ...workspaceWithoutTheme } = validWorkspace;
expect(isWorkspace(workspaceWithoutTheme)).toBe(false);
});
it('returns false when theme is not a string', () => {
const workspaceWithInvalidTheme = { ...validWorkspace, theme: 123 };
expect(isWorkspace(workspaceWithInvalidTheme)).toBe(false);
});
it('returns false when boolean fields are not boolean', () => {
const booleanFields = [
'autoSave',
'showHiddenFiles',
'gitEnabled',
'gitAutoCommit',
];
booleanFields.forEach((field) => {
const workspaceWithInvalidBoolean = {
...validWorkspace,
[field]: 'true',
};
expect(isWorkspace(workspaceWithInvalidBoolean)).toBe(false);
});
});
it('returns false when string fields are not strings', () => {
const stringFields = [
'gitUrl',
'gitUser',
'gitToken',
'gitCommitMsgTemplate',
'gitCommitName',
'gitCommitEmail',
];
stringFields.forEach((field) => {
const workspaceWithInvalidString = { ...validWorkspace, [field]: 123 };
expect(isWorkspace(workspaceWithInvalidString)).toBe(false);
});
});
it('handles objects with extra properties', () => {
const workspaceWithExtra = {
...validWorkspace,
extraField: 'should be ignored',
};
expect(isWorkspace(workspaceWithExtra)).toBe(true);
});
it('handles objects with prototype pollution attempts', () => {
const maliciousWorkspace = {
...validWorkspace,
__proto__: { malicious: true },
constructor: { prototype: { polluted: true } },
};
expect(isWorkspace(maliciousWorkspace)).toBe(true);
});
it('handles various workspace names', () => {
const workspaceNames = [
'simple',
'workspace-with-dashes',
'workspace_with_underscores',
'workspace with spaces',
'workspace123',
'very-long-workspace-name-with-many-characters',
'unicode-工作区',
];
workspaceNames.forEach((name) => {
expect(isWorkspace({ ...validWorkspace, name })).toBe(true);
});
});
});
describe('isFileNode', () => {
const validFileNode: FileNode = {
id: '1',
name: 'test.md',
path: 'documents/test.md',
};
const validFolderNode: FileNode = {
id: '2',
name: 'documents',
path: 'documents',
children: [
{
id: '3',
name: 'nested.md',
path: 'documents/nested.md',
},
],
};
it('returns true for valid file node without children', () => {
expect(isFileNode(validFileNode)).toBe(true);
});
it('returns true for valid folder node with children', () => {
expect(isFileNode(validFolderNode)).toBe(true);
});
it('returns true for node with empty children array', () => {
const nodeWithEmptyChildren = {
...validFileNode,
children: [],
};
expect(isFileNode(nodeWithEmptyChildren)).toBe(true);
});
it('returns true for node with null children', () => {
const nodeWithNullChildren = {
...validFileNode,
children: null,
};
expect(isFileNode(nodeWithNullChildren)).toBe(true);
});
it('returns true for node with undefined children', () => {
const nodeWithUndefinedChildren = {
...validFileNode,
children: undefined,
};
expect(isFileNode(nodeWithUndefinedChildren)).toBe(true);
});
it('returns false for null', () => {
expect(isFileNode(null)).toBe(false);
});
it('returns false for undefined', () => {
expect(isFileNode(undefined)).toBe(false);
});
it('returns false for non-object values', () => {
expect(isFileNode('string')).toBe(false);
expect(isFileNode(123)).toBe(false);
expect(isFileNode(true)).toBe(false);
expect(isFileNode([])).toBe(false);
});
it('returns false for empty object', () => {
expect(isFileNode({})).toBe(false);
});
it('returns false when id is missing', () => {
const { id: _id, ...nodeWithoutId } = validFileNode;
expect(isFileNode(nodeWithoutId)).toBe(false);
});
it('returns false when id is not a string', () => {
const nodeWithInvalidId = { ...validFileNode, id: 123 };
expect(isFileNode(nodeWithInvalidId)).toBe(false);
});
it('returns false when name is missing', () => {
const { name: _name, ...nodeWithoutName } = validFileNode;
expect(isFileNode(nodeWithoutName)).toBe(false);
});
it('returns false when name is not a string', () => {
const nodeWithInvalidName = { ...validFileNode, name: 123 };
expect(isFileNode(nodeWithInvalidName)).toBe(false);
});
it('returns false when path is missing', () => {
const { path: _path, ...nodeWithoutPath } = validFileNode;
expect(isFileNode(nodeWithoutPath)).toBe(false);
});
it('returns false when path is not a string', () => {
const nodeWithInvalidPath = { ...validFileNode, path: 123 };
expect(isFileNode(nodeWithInvalidPath)).toBe(false);
});
it('returns false when children is not an array', () => {
const nodeWithInvalidChildren = {
...validFileNode,
children: 'not-an-array',
};
expect(isFileNode(nodeWithInvalidChildren)).toBe(false);
});
it('handles nested file structures', () => {
const deeplyNestedNode: FileNode = {
id: '1',
name: 'root',
path: 'root',
children: [
{
id: '2',
name: 'level1',
path: 'root/level1',
children: [
{
id: '3',
name: 'level2',
path: 'root/level1/level2',
children: [
{
id: '4',
name: 'deep-file.md',
path: 'root/level1/level2/deep-file.md',
},
],
},
],
},
],
};
expect(isFileNode(deeplyNestedNode)).toBe(true);
});
it('handles various file and folder names', () => {
const names = [
'simple.md',
'file-with-dashes.md',
'file_with_underscores.md',
'file with spaces.md',
'file.with.dots.md',
'UPPERCASE.MD',
'MixedCase.Md',
'unicode-文件.md',
'no-extension',
'.hidden-file',
'folder',
];
names.forEach((name) => {
const node = {
id: '1',
name,
path: name,
};
expect(isFileNode(node)).toBe(true);
});
});
it('handles objects with extra properties', () => {
const nodeWithExtra = {
...validFileNode,
extraField: 'should be ignored',
};
expect(isFileNode(nodeWithExtra)).toBe(true);
});
it('handles objects with prototype pollution attempts', () => {
const maliciousNode = {
...validFileNode,
__proto__: { malicious: true },
constructor: { prototype: { polluted: true } },
};
expect(isFileNode(maliciousNode)).toBe(true);
});
});
describe('isSystemStats', () => {
const validSystemStats: SystemStats = {
totalUsers: 10,
totalWorkspaces: 5,
activeUsers: 8,
totalFiles: 100,
totalSize: 1024000,
};
it('returns true for valid system stats', () => {
expect(isSystemStats(validSystemStats)).toBe(true);
});
it('returns true for stats with zero values', () => {
const statsWithZeros = {
totalUsers: 0,
totalWorkspaces: 0,
activeUsers: 0,
totalFiles: 0,
totalSize: 0,
};
expect(isSystemStats(statsWithZeros)).toBe(true);
});
it('returns true for stats with large numbers', () => {
const statsWithLargeNumbers = {
totalUsers: 999999,
totalWorkspaces: 888888,
activeUsers: 777777,
totalFiles: 666666,
totalSize: 555555555555,
};
expect(isSystemStats(statsWithLargeNumbers)).toBe(true);
});
it('returns false for null', () => {
expect(isSystemStats(null)).toBe(false);
});
it('returns false for undefined', () => {
expect(isSystemStats(undefined)).toBe(false);
});
it('returns false for non-object values', () => {
expect(isSystemStats('string')).toBe(false);
expect(isSystemStats(123)).toBe(false);
expect(isSystemStats(true)).toBe(false);
expect(isSystemStats([])).toBe(false);
});
it('returns false for empty object', () => {
expect(isSystemStats({})).toBe(false);
});
it('returns false when totalUsers is missing', () => {
const { totalUsers: _totalUsers, ...statsWithoutTotalUsers } =
validSystemStats;
expect(isSystemStats(statsWithoutTotalUsers)).toBe(false);
});
it('returns false when totalUsers is not a number', () => {
const statsWithInvalidTotalUsers = {
...validSystemStats,
totalUsers: '10',
};
expect(isSystemStats(statsWithInvalidTotalUsers)).toBe(false);
});
it('returns false when totalWorkspaces is missing', () => {
const {
totalWorkspaces: _totalWorkspaces,
...statsWithoutTotalWorkspaces
} = validSystemStats;
expect(isSystemStats(statsWithoutTotalWorkspaces)).toBe(false);
});
it('returns false when totalWorkspaces is not a number', () => {
const statsWithInvalidTotalWorkspaces = {
...validSystemStats,
totalWorkspaces: '5',
};
expect(isSystemStats(statsWithInvalidTotalWorkspaces)).toBe(false);
});
it('returns false when activeUsers is missing', () => {
const { activeUsers: _activeUsers, ...statsWithoutActiveUsers } =
validSystemStats;
expect(isSystemStats(statsWithoutActiveUsers)).toBe(false);
});
it('returns false when activeUsers is not a number', () => {
const statsWithInvalidActiveUsers = {
...validSystemStats,
activeUsers: '8',
};
expect(isSystemStats(statsWithInvalidActiveUsers)).toBe(false);
});
it('returns false when totalFiles is missing', () => {
const { totalFiles: _totalFiles, ...statsWithoutTotalFiles } =
validSystemStats;
expect(isSystemStats(statsWithoutTotalFiles)).toBe(false);
});
it('returns false when totalFiles is not a number', () => {
const statsWithInvalidTotalFiles = {
...validSystemStats,
totalFiles: '100',
};
expect(isSystemStats(statsWithInvalidTotalFiles)).toBe(false);
});
it('returns false when totalSize is missing', () => {
const { totalSize: _totalSize, ...statsWithoutTotalSize } =
validSystemStats;
expect(isSystemStats(statsWithoutTotalSize)).toBe(false);
});
it('returns false when totalSize is not a number', () => {
const statsWithInvalidTotalSize = {
...validSystemStats,
totalSize: '1024000',
};
expect(isSystemStats(statsWithInvalidTotalSize)).toBe(false);
});
it('handles objects with extra properties', () => {
const statsWithExtra = {
...validSystemStats,
extraField: 'should be ignored',
};
expect(isSystemStats(statsWithExtra)).toBe(true);
});
it('handles objects with prototype pollution attempts', () => {
const maliciousStats = {
...validSystemStats,
__proto__: { malicious: true },
constructor: { prototype: { polluted: true } },
};
expect(isSystemStats(maliciousStats)).toBe(true);
});
it('handles floating point numbers', () => {
const statsWithFloats = {
totalUsers: 10.5,
totalWorkspaces: 5.7,
activeUsers: 8.2,
totalFiles: 100.9,
totalSize: 1024000.123,
};
expect(isSystemStats(statsWithFloats)).toBe(true);
});
it('handles negative numbers', () => {
const statsWithNegatives = {
totalUsers: -10,
totalWorkspaces: -5,
activeUsers: -8,
totalFiles: -100,
totalSize: -1024000,
};
expect(isSystemStats(statsWithNegatives)).toBe(true); // Type guard doesn't validate ranges
});
});
describe('edge cases and error conditions', () => {
it('handles circular references gracefully', () => {
const circularUser: Record<string, unknown> = {
id: 1,
email: 'test@example.com',
role: UserRole.Editor,
createdAt: '2024-01-01T00:00:00Z',
lastWorkspaceId: 1,
};
circularUser['self'] = circularUser;
expect(isUser(circularUser)).toBe(true);
});
it('handles deeply nested file structures', () => {
let deepNode: FileNode = {
id: '1',
name: 'root',
path: 'root',
};
// Create a deeply nested structure
for (let i = 2; i <= 100; i++) {
deepNode = {
id: i.toString(),
name: `level${i}`,
path: `root/level${i}`,
children: [deepNode],
};
}
expect(isFileNode(deepNode)).toBe(true);
});
it('handles frozen objects', () => {
const frozenUser = Object.freeze({
id: 1,
email: 'test@example.com',
role: UserRole.Editor,
createdAt: '2024-01-01T00:00:00Z',
lastWorkspaceId: 1,
});
expect(isUser(frozenUser)).toBe(true);
});
it('handles objects created with null prototype', () => {
const nullProtoStats = Object.create(null) as Record<string, unknown>;
nullProtoStats['totalUsers'] = 10;
nullProtoStats['totalWorkspaces'] = 5;
nullProtoStats['activeUsers'] = 8;
nullProtoStats['totalFiles'] = 100;
nullProtoStats['totalSize'] = 1024000;
expect(isSystemStats(nullProtoStats)).toBe(true);
});
});
describe('performance with large data', () => {
it('handles large file trees efficiently', () => {
const largeChildren = Array.from({ length: 1000 }, (_, i) => ({
id: i.toString(),
name: `file${i}.md`,
path: `folder/file${i}.md`,
}));
const largeFileTree: FileNode = {
id: 'root',
name: 'folder',
path: 'folder',
children: largeChildren,
};
const start = performance.now();
const result = isFileNode(largeFileTree);
const end = performance.now();
expect(result).toBe(true);
expect(end - start).toBeLessThan(100); // Should complete in under 100ms
});
it('handles very long strings efficiently', () => {
const longString = 'a'.repeat(100000);
const userWithLongEmail = {
id: 1,
email: longString,
role: UserRole.Editor,
createdAt: '2024-01-01T00:00:00Z',
lastWorkspaceId: 1,
};
const start = performance.now();
const result = isUser(userWithLongEmail);
const end = performance.now();
expect(result).toBe(true);
expect(end - start).toBeLessThan(10); // Should be very fast
});
});
});