/ tests / unit / workspace / validation.test.ts
validation.test.ts
  1  /**
  2   * Tests for workspace validation utilities
  3   */
  4  
  5  import { describe, it, expect } from 'vitest';
  6  import {
  7    validateWorkspace,
  8    validateTab,
  9    validateHierarchy,
 10    canAddChild,
 11  } from '../../../src/lib/workspace/validation';
 12  import type {
 13    ParentWorkspace,
 14    ChildWorkspace,
 15    StandaloneWorkspace,
 16    SavedTab,
 17  } from '../../../src/lib/types';
 18  
 19  // Helper to create valid metadata
 20  const createMetadata = () => ({
 21    created: new Date().toISOString(),
 22    lastModified: new Date().toISOString(),
 23    lastAccessed: new Date().toISOString(),
 24    switchCount: 0,
 25    totalTimeActive: 0,
 26  });
 27  
 28  describe('validateWorkspace', () => {
 29    it('validates a valid parent workspace', () => {
 30      const parent: ParentWorkspace = {
 31        id: 'parent-1',
 32        name: 'Test Parent',
 33        type: 'parent',
 34        parentId: null,
 35        isTranscendent: false,
 36        children: [],
 37        metadata: createMetadata(),
 38      };
 39  
 40      const result = validateWorkspace(parent);
 41      expect(result.valid).toBe(true);
 42      expect(result.errors).toHaveLength(0);
 43    });
 44  
 45    it('validates a valid child workspace', () => {
 46      const child: ChildWorkspace = {
 47        id: 'child-1',
 48        name: 'Test Child',
 49        type: 'child',
 50        parentId: 'parent-1',
 51        isTranscendent: false,
 52        tabs: [],
 53        metadata: createMetadata(),
 54      };
 55  
 56      const result = validateWorkspace(child);
 57      expect(result.valid).toBe(true);
 58      expect(result.errors).toHaveLength(0);
 59    });
 60  
 61    it('validates a valid standalone workspace', () => {
 62      const standalone: StandaloneWorkspace = {
 63        id: 'standalone-1',
 64        name: 'Test Standalone',
 65        type: 'standalone',
 66        parentId: null,
 67        isTranscendent: false,
 68        tabs: [],
 69        metadata: createMetadata(),
 70      };
 71  
 72      const result = validateWorkspace(standalone);
 73      expect(result.valid).toBe(true);
 74      expect(result.errors).toHaveLength(0);
 75    });
 76  
 77    it('rejects workspace without ID', () => {
 78      const workspace = {
 79        id: '',
 80        name: 'No ID',
 81        type: 'standalone',
 82        parentId: null,
 83        isTranscendent: false,
 84        tabs: [],
 85        metadata: createMetadata(),
 86      } as StandaloneWorkspace;
 87  
 88      const result = validateWorkspace(workspace);
 89      expect(result.valid).toBe(false);
 90      expect(result.errors.some((e) => e.field === 'id')).toBe(true);
 91    });
 92  
 93    it('rejects workspace without name', () => {
 94      const workspace = {
 95        id: 'test-1',
 96        name: '',
 97        type: 'standalone',
 98        parentId: null,
 99        isTranscendent: false,
100        tabs: [],
101        metadata: createMetadata(),
102      } as StandaloneWorkspace;
103  
104      const result = validateWorkspace(workspace);
105      expect(result.valid).toBe(false);
106      expect(result.errors.some((e) => e.field === 'name')).toBe(true);
107    });
108  
109    it('rejects parent with non-null parentId', () => {
110      const workspace = {
111        id: 'parent-1',
112        name: 'Bad Parent',
113        type: 'parent',
114        parentId: 'some-id', // Should be null
115        isTranscendent: false,
116        children: [],
117        metadata: createMetadata(),
118      } as unknown as ParentWorkspace;
119  
120      const result = validateWorkspace(workspace);
121      expect(result.valid).toBe(false);
122      expect(result.errors.some((e) => e.message.includes('single-level'))).toBe(true);
123    });
124  
125    it('rejects child without parentId', () => {
126      const workspace = {
127        id: 'child-1',
128        name: 'Orphan Child',
129        type: 'child',
130        parentId: '',
131        isTranscendent: false,
132        tabs: [],
133        metadata: createMetadata(),
134      } as ChildWorkspace;
135  
136      const result = validateWorkspace(workspace);
137      expect(result.valid).toBe(false);
138      expect(result.errors.some((e) => e.field === 'parentId')).toBe(true);
139    });
140  });
141  
142  describe('validateTab', () => {
143    it('validates a valid tab', () => {
144      const tab: SavedTab = {
145        url: 'https://example.com',
146        title: 'Example',
147        pinned: false,
148      };
149  
150      const result = validateTab(tab);
151      expect(result.valid).toBe(true);
152    });
153  
154    it('rejects tab without URL', () => {
155      const tab = {
156        url: '',
157        title: 'No URL',
158        pinned: false,
159      } as SavedTab;
160  
161      const result = validateTab(tab);
162      expect(result.valid).toBe(false);
163      expect(result.errors.some((e) => e.field === 'url')).toBe(true);
164    });
165  });
166  
167  describe('validateHierarchy', () => {
168    it('validates a valid hierarchy', () => {
169      const parent: ParentWorkspace = {
170        id: 'parent-1',
171        name: 'Parent',
172        type: 'parent',
173        parentId: null,
174        isTranscendent: false,
175        children: ['child-1', 'child-2'],
176        metadata: createMetadata(),
177      };
178  
179      const child1: ChildWorkspace = {
180        id: 'child-1',
181        name: 'Child 1',
182        type: 'child',
183        parentId: 'parent-1',
184        isTranscendent: false,
185        tabs: [],
186        metadata: createMetadata(),
187      };
188  
189      const child2: ChildWorkspace = {
190        id: 'child-2',
191        name: 'Child 2',
192        type: 'child',
193        parentId: 'parent-1',
194        isTranscendent: false,
195        tabs: [],
196        metadata: createMetadata(),
197      };
198  
199      const result = validateHierarchy([parent, child1, child2]);
200      expect(result.valid).toBe(true);
201      expect(result.errors).toHaveLength(0);
202    });
203  
204    it('detects duplicate IDs', () => {
205      const ws1: StandaloneWorkspace = {
206        id: 'duplicate-id',
207        name: 'First',
208        type: 'standalone',
209        parentId: null,
210        isTranscendent: false,
211        tabs: [],
212        metadata: createMetadata(),
213      };
214  
215      const ws2: StandaloneWorkspace = {
216        id: 'duplicate-id',
217        name: 'Second',
218        type: 'standalone',
219        parentId: null,
220        isTranscendent: false,
221        tabs: [],
222        metadata: createMetadata(),
223      };
224  
225      const result = validateHierarchy([ws1, ws2]);
226      expect(result.valid).toBe(false);
227      expect(result.errors.some((e) => e.message.includes('Duplicate'))).toBe(true);
228    });
229  
230    it('detects orphaned children', () => {
231      const child: ChildWorkspace = {
232        id: 'orphan-child',
233        name: 'Orphan',
234        type: 'child',
235        parentId: 'non-existent-parent',
236        isTranscendent: false,
237        tabs: [],
238        metadata: createMetadata(),
239      };
240  
241      const result = validateHierarchy([child]);
242      expect(result.valid).toBe(false);
243      expect(result.errors.some((e) => e.message.includes('non-existent parent'))).toBe(true);
244    });
245  
246    it('detects missing children in parent array', () => {
247      const parent: ParentWorkspace = {
248        id: 'parent-1',
249        name: 'Parent',
250        type: 'parent',
251        parentId: null,
252        isTranscendent: false,
253        children: ['non-existent-child'],
254        metadata: createMetadata(),
255      };
256  
257      const result = validateHierarchy([parent]);
258      expect(result.valid).toBe(false);
259      expect(result.errors.some((e) => e.message.includes('non-existent child'))).toBe(true);
260    });
261  });
262  
263  describe('canAddChild', () => {
264    it('allows adding standalone to parent', () => {
265      const parent: ParentWorkspace = {
266        id: 'parent-1',
267        name: 'Parent',
268        type: 'parent',
269        parentId: null,
270        isTranscendent: false,
271        children: [],
272        metadata: createMetadata(),
273      };
274  
275      const standalone: StandaloneWorkspace = {
276        id: 'standalone-1',
277        name: 'Standalone',
278        type: 'standalone',
279        parentId: null,
280        isTranscendent: false,
281        tabs: [],
282        metadata: createMetadata(),
283      };
284  
285      const result = canAddChild([parent, standalone], 'standalone-1', 'parent-1');
286      expect(result.valid).toBe(true);
287    });
288  
289    it('rejects adding parent as child', () => {
290      const parent1: ParentWorkspace = {
291        id: 'parent-1',
292        name: 'Parent 1',
293        type: 'parent',
294        parentId: null,
295        isTranscendent: false,
296        children: [],
297        metadata: createMetadata(),
298      };
299  
300      const parent2: ParentWorkspace = {
301        id: 'parent-2',
302        name: 'Parent 2',
303        type: 'parent',
304        parentId: null,
305        isTranscendent: false,
306        children: [],
307        metadata: createMetadata(),
308      };
309  
310      const result = canAddChild([parent1, parent2], 'parent-2', 'parent-1');
311      expect(result.valid).toBe(false);
312      expect(result.errors.some((e) => e.message.includes('single-level'))).toBe(true);
313    });
314  });