diff --git a/app/src/types/api.test.ts b/app/src/types/api.test.ts new file mode 100644 index 0000000..5c78066 --- /dev/null +++ b/app/src/types/api.test.ts @@ -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; + 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); + }); + }); +}); diff --git a/app/src/types/models.test.ts b/app/src/types/models.test.ts new file mode 100644 index 0000000..ca9fa3a --- /dev/null +++ b/app/src/types/models.test.ts @@ -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 = { + 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; + 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 + }); + }); +});