src/core/errors.spec.ts   A
last analyzed

Complexity

Total Complexity 8
Complexity/F 0

Size

Lines of Code 474
Function Count 0

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
wmc 8
eloc 366
mnd 8
bc 8
fnc 0
dl 0
loc 474
rs 10
bpm 0
cpm 0
noi 0
c 0
b 0
f 0
1
import { fail } from '@test/utils/testUtils';
2
import {
3
  Result,
4
  ResultAsync,
5
  isOk,
6
  isError,
7
  wrap,
8
  unwrap,
9
  unwrapAsync,
10
  wrapValue,
11
  wrapError,
12
  ResultOk,
13
} from '@/core/errors';
14
15
describe('Result Types', () => {
16
17
  it('isOk returns true for successful results', () => {
18
    // GIVEN
19
    const result: Result<string> = { ok: true, value: 'success' };
20
21
    // WHEN
22
    const isOkResult = isOk(result);
23
24
    // THEN
25
    expect(isOkResult).toBe(true);
26
  });
27
28
  it('isOk returns false for error results', () => {
29
    // GIVEN
30
    const result: Result<string> = { ok: false, error: new Error('failed') };
31
32
    // WHEN
33
    const isOkResult = isOk(result);
34
35
    // THEN
36
    expect(isOkResult).toBe(false);
37
  });
38
39
  it('isOk narrows type correctly for successful results', () => {
40
    // GIVEN
41
    const result: Result<number> = { ok: true, value: 42 };
42
43
    // WHEN
44
    if (isOk(result)) {
45
      // THEN
46
      expect(typeof result.value).toBe('number');
47
      expect(result.value).toBe(42);
48
    }
49
  });
50
51
  it('isError returns false for successful results', () => {
52
    // GIVEN
53
    const result: Result<string> = { ok: true, value: 'success' };
54
55
    // WHEN
56
    const isErrorResult = isError(result);
57
58
    // THEN
59
    expect(isErrorResult).toBe(false);
60
  });
61
62
  it('isError returns true for error results', () => {
63
    // GIVEN
64
    const result: Result<string> = { ok: false, error: new Error('failed') };
65
66
    // WHEN
67
    const isErrorResult = isError(result);
68
69
    // THEN
70
    expect(isErrorResult).toBe(true);
71
  });
72
73
  it('isError narrows type correctly for error results', () => {
74
    // GIVEN
75
    const error = new Error('test error');
76
    const result: Result<string> = { ok: false, error };
77
78
    // WHEN
79
    if (isError(result)) {
80
      // THEN
81
      expect(result.error).toBe(error);
82
      expect(result.error.message).toBe('test error');
83
    }
84
  });
85
86
  it('wrap wraps successful synchronous function calls', () => {
87
    // GIVEN
88
    const fn = () => 'success';
89
90
    // WHEN
91
    const result = wrap(fn);
92
93
    // THEN
94
    expect(result).toEqual({ ok: true, value: 'success' });
95
    expect(isOk(result as Result<string>)).toBe(true);
96
  });
97
98
  it('wrap wraps failed synchronous function calls', () => {
99
    // GIVEN
100
    const error = new Error('sync error');
101
    const fn = () => { throw error; };
102
103
    // WHEN
104
    const result = wrap(fn);
105
106
    // THEN
107
    expect(result).toEqual({ ok: false, error });
108
    expect(isError(result as Result<string>)).toBe(true);
109
  });
110
111
  it('wrap converts non-Error exceptions to Error objects', () => {
112
    // GIVEN
113
    const fn = () => { throw 'string error'; };
114
115
    // WHEN
116
    const result = wrap(fn) as Result<string>;
117
118
    // THEN
119
    expect(isError(result)).toBe(true);
120
    if (isError(result)) {
121
      expect(result.error).toBeInstanceOf(Error);
122
      expect(result.error.message).toBe('string error');
123
    }
124
  });
125
126
  it('wrap handles null and undefined throws', () => {
127
    // GIVEN
128
    const fnNull = () => { throw null; };
129
    const fnUndefined = () => { throw undefined; };
130
131
    // WHEN
132
    const resultNull = wrap(fnNull) as Result<any>;
133
    const resultUndefined = wrap(fnUndefined) as Result<any>;
134
135
    // THEN
136
    expect(isError(resultNull)).toBe(true);
137
    expect(isError(resultUndefined)).toBe(true);
138
139
    if (isError(resultNull)) {
140
      expect(resultNull.error.message).toBe('null');
141
    }
142
    if (isError(resultUndefined)) {
143
      expect(resultUndefined.error.message).toBe('undefined');
144
    }
145
  });
146
147
  it('wrap wraps successful async function calls', async () => {
148
    // GIVEN
149
    const fn = async () => 'async success';
150
151
    // WHEN
152
    const resultPromise = wrap(fn);
153
154
    // THEN
155
    expect(resultPromise).toBeInstanceOf(Promise);
156
    const result = await resultPromise;
157
    expect(result).toEqual({ ok: true, value: 'async success' });
158
  });
159
160
  it('wrap wraps failed async function calls', async () => {
161
    // GIVEN
162
    const error = new Error('async error');
163
    const fn = async () => { throw error; };
164
165
    // WHEN
166
    const resultPromise = wrap(fn);
167
168
    // THEN
169
    expect(resultPromise).toBeInstanceOf(Promise);
170
    const result = await resultPromise;
171
    expect(result).toEqual({ ok: false, error });
172
  });
173
174
  it('wrap handles async functions that return promises', async () => {
175
    // GIVEN
176
    const fn = () => Promise.resolve(42);
177
178
    // WHEN
179
    const resultPromise = wrap(fn);
180
181
    // THEN
182
    const result = await resultPromise;
183
    expect(result).toEqual({ ok: true, value: 42 });
184
  });
185
186
  it('wrap handles async functions that return rejected promises', async () => {
187
    // GIVEN
188
    const error = new Error('rejected promise');
189
    const fn = () => Promise.reject(error);
190
191
    // WHEN
192
    const resultPromise = wrap(fn);
193
194
    // THEN
195
    const result = await resultPromise;
196
    expect(result).toEqual({ ok: false, error });
197
  });
198
199
  it('wrap converts non-Error async exceptions to Error objects', async () => {
200
    // GIVEN
201
    const fn = async () => { throw 'async string error'; };
202
203
    // WHEN
204
    const result = await wrap(fn);
205
206
    // THEN
207
    expect(isError(result)).toBe(true);
208
    if (isError(result)) {
209
      expect(result.error).toBeInstanceOf(Error);
210
      expect(result.error.message).toBe('async string error');
211
    }
212
  });
213
214
  it('unwrap returns value for successful results', () => {
215
    // GIVEN
216
    const result: Result<string> = { ok: true, value: 'success' };
217
218
    // WHEN
219
    const value = unwrap(result);
220
221
    // THEN
222
    expect(value).toBe('success');
223
  });
224
225
  it('unwrap throws error for failed results', () => {
226
    // GIVEN
227
    const error = new Error('unwrap error');
228
    const result: Result<string> = { ok: false, error };
229
230
    // WHEN & THEN
231
    expect(() => unwrap(result)).toThrow(error);
232
  });
233
234
  it('unwrap preserves original error when throwing', () => {
235
    // GIVEN
236
    const originalError = new Error('original message');
237
    originalError.name = 'CustomError';
238
    const result: Result<string> = { ok: false, error: originalError };
239
240
    // WHEN & THEN
241
    expect(() => unwrap(result)).toThrow(originalError);
242
    try {
243
      unwrap(result);
244
    } catch (thrownError) {
245
      expect(thrownError).toBe(originalError);
246
      expect((thrownError as Error).name).toBe('CustomError');
247
    }
248
  });
249
250
  it('unwrapAsync returns value for successful async results', async () => {
251
    // GIVEN
252
    const resultPromise: ResultAsync<string> = Promise.resolve({ ok: true, value: 'async success' });
253
254
    // WHEN
255
    const value = await unwrapAsync(resultPromise);
256
257
    // THEN
258
    expect(value).toBe('async success');
259
  });
260
261
  it('unwrapAsync throws error for failed async results', async () => {
262
    // GIVEN
263
    const error = new Error('async unwrap error');
264
    const resultPromise: ResultAsync<string> = Promise.resolve({ ok: false, error });
265
266
    // WHEN & THEN
267
    await expect(unwrapAsync(resultPromise)).rejects.toThrow(error);
268
  });
269
270
  it('unwrapAsync preserves original error when throwing', async () => {
271
    // GIVEN
272
    const originalError = new Error('async original message');
273
    originalError.name = 'AsyncCustomError';
274
    const resultPromise: ResultAsync<string> = Promise
275
      .resolve({ ok: false, error: originalError });
276
277
    // WHEN & THEN
278
    try {
279
      await unwrapAsync(resultPromise);
280
      fail('Should have thrown');
281
    } catch (thrownError) {
282
      expect(thrownError).toBe(originalError);
283
      expect((thrownError as Error).name).toBe('AsyncCustomError');
284
    }
285
  });
286
287
  it('unwrapAsync handles rejected promises', async () => {
288
    // GIVEN
289
    const rejectionError = new Error('promise rejection');
290
    const resultPromise: ResultAsync<string> = Promise.reject(rejectionError);
291
292
    // WHEN & THEN
293
    await expect(unwrapAsync(resultPromise)).rejects.toThrow(rejectionError);
294
  });
295
296
  it('wrapValue wraps primitive values', () => {
297
    // GIVEN
298
    const stringValue = 'string';
299
    const numberValue = 42;
300
    const booleanValue = true;
301
302
    // WHEN & THEN
303
    expect(wrapValue(stringValue)).toEqual({ ok: true, value: 'string' });
304
    expect(wrapValue(numberValue)).toEqual({ ok: true, value: 42 });
305
    expect(wrapValue(booleanValue)).toEqual({ ok: true, value: true });
306
  });
307
308
  it('wrapValue wraps object values', () => {
309
    // GIVEN
310
    const obj = { key: 'value' };
311
312
    // WHEN
313
    const result = wrapValue(obj);
314
315
    // THEN
316
    expect(result).toEqual({ ok: true, value: obj });
317
    expect(isOk(result)).toBe(true);
318
    expect((result as ResultOk<typeof obj>).value).toBe(obj);
319
  });
320
321
  it('wrapValue wraps null and undefined', () => {
322
    // GIVEN
323
    const nullValue = null;
324
    const undefinedValue = undefined;
325
326
    // WHEN & THEN
327
    expect(wrapValue(nullValue)).toEqual({ ok: true, value: null });
328
    expect(wrapValue(undefinedValue)).toEqual({ ok: true, value: undefined });
329
  });
330
331
  it('wrapValue creates proper ResultOk type', () => {
332
    // GIVEN
333
    const testValue = 'test';
334
335
    // WHEN
336
    const result = wrapValue(testValue);
337
338
    // THEN
339
    expect(isOk(result)).toBe(true);
340
    expect(isError(result)).toBe(false);
341
  });
342
343
  it('wrapError wraps Error objects', () => {
344
    // GIVEN
345
    const error = new Error('test error');
346
347
    // WHEN
348
    const result = wrapError(error);
349
350
    // THEN
351
    expect(result).toEqual({ ok: false, error });
352
    expect(isError(result)).toBe(true);
353
    expect(isOk(result)).toBe(false);
354
  });
355
356
  it('wrapError wraps custom error types', () => {
357
    // GIVEN
358
    class CustomError extends Error {
359
      constructor(message: string, public code: number) {
360
        super(message);
361
        this.name = 'CustomError';
362
      }
363
    }
364
    const customError = new CustomError('custom message', 404);
365
366
    // WHEN
367
    const result = wrapError(customError);
368
369
    // THEN
370
    expect(result).toEqual({ ok: false, error: customError });
371
    expect(result.error).toBe(customError);
372
  });
373
374
  it('wrapError wraps non-Error values', () => {
375
    // GIVEN
376
    const stringError = 'string error';
377
378
    // WHEN
379
    const result = wrapError(stringError);
380
381
    // THEN
382
    expect(result).toEqual({ ok: false, error: stringError });
383
    expect(result.error).toBe(stringError);
384
  });
385
386
  it('wrapError creates proper ResultError type', () => {
387
    // GIVEN
388
    const error = new Error('test');
389
390
    // WHEN
391
    const result = wrapError(error);
392
393
    // THEN
394
    expect(isError(result)).toBe(true);
395
    expect(isOk(result)).toBe(false);
396
  });
397
398
  it('integration - works with complete success flow', () => {
399
    // GIVEN
400
    const fn = () => 'integration test';
401
402
    // WHEN
403
    const wrapped = wrap(fn) as Result<string>;
404
405
    // THEN
406
    expect(isOk(wrapped)).toBe(true);
407
    const unwrapped = unwrap(wrapped);
408
    expect(unwrapped).toBe('integration test');
409
  });
410
411
  it('integration - works with complete error flow', () => {
412
    // GIVEN
413
    const error = new Error('integration error');
414
    const fn = () => { throw error; };
415
416
    // WHEN
417
    const wrapped = wrap(fn) as Result<string>;
418
419
    // THEN
420
    expect(isError(wrapped)).toBe(true);
421
    expect(() => unwrap(wrapped)).toThrow(error);
422
  });
423
424
  it('integration - works with async success flow', async () => {
425
    // GIVEN
426
    const fn = async () => 'async integration';
427
428
    // WHEN
429
    const wrapped = await wrap(fn);
430
431
    // THEN
432
    expect(isOk(wrapped)).toBe(true);
433
    const unwrapped = unwrap(wrapped);
434
    expect(unwrapped).toBe('async integration');
435
  });
436
437
  it('integration - works with async error flow', async () => {
438
    // GIVEN
439
    const error = new Error('async integration error');
440
    const fn = async () => { throw error; };
441
442
    // WHEN
443
    const wrapped = await wrap(fn);
444
445
    // THEN
446
    expect(isError(wrapped)).toBe(true);
447
    expect(() => unwrap(wrapped)).toThrow(error);
448
  });
449
450
  it('integration - chains operations correctly', async () => {
451
    // GIVEN
452
    const step1 = () => 10;
453
    const step2 = (x: number) => x * 2;
454
    const step3 = (x: number) => `Result: ${x}`;
455
456
    // WHEN
457
    const result1 = wrap(step1) as Result<number>;
458
    expect(isOk(result1)).toBe(true);
459
460
    const value1 = unwrap(result1);
461
    const result2 = wrap(() => step2(value1)) as Result<number>;
462
    expect(isOk(result2)).toBe(true);
463
464
    const value2 = unwrap(result2);
465
    const result3 = wrap(() => step3(value2)) as Result<string>;
466
    expect(isOk(result3)).toBe(true);
467
468
    const finalValue = unwrap(result3);
469
470
    // THEN
471
    expect(finalValue).toBe('Result: 20');
472
  });
473
});
474