Repository navigation
Expand file tree
/
Copy patherror-message.normalisation-boundary.test.ts
More file actions
120 lines (113 loc) · 4.92 KB
/
Copy patherror-message.normalisation-boundary.test.ts
File metadata and controls
120 lines (113 loc) · 4.92 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
/**
* ObjectUI
* Copyright (c) 2024-present ObjectStack Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import { describe, it, expect } from 'vitest';
import { normaliseClientError } from '@object-ui/data-objectstack';
import { declaredUserMessage, extractFieldErrors } from './error-message';
/**
* The two halves of the `userMessage` channel, pinned where they meet.
*
* `@object-ui/data-objectstack` parks the producer's marking on the typed
* error it constructs; `declaredUserMessage` here lifts it back off. Neither
* half can pin the pair alone — the adapter cannot import this reader (this
* package depends on THAT one, so the symbol cannot travel upstream), and the
* reader's own unit tests build their inputs by hand and so never exercise the
* adapter's re-wrap.
*
* That gap is exactly where the defect lived: both re-wraps dropped the
* marking, every unit test on both sides stayed green, and the only visible
* symptom was a user reading a generic string instead of the sentence their
* administrator wrote. These pins fail if either half stops holding up its end.
*/
describe('userMessage survives the adapter boundary', () => {
it('survives the VALIDATION_FAILED re-wrap', () => {
const raw = Object.assign(new Error('Amount is not allowed'), {
code: 'VALIDATION_FAILED',
httpStatus: 400,
details: {
code: 'VALIDATION_FAILED',
userMessage: 'Amounts over 10k need finance approval.',
fields: [{ field: 'amount', message: 'Amount is not allowed' }],
},
});
// The marking is readable before the boundary…
expect(declaredUserMessage(raw)).toBe('Amounts over 10k need finance approval.');
// …and must still be readable after it.
expect(declaredUserMessage(normaliseClientError(raw))).toBe(
'Amounts over 10k need finance approval.',
);
});
it('survives the CONCURRENT_UPDATE re-wrap', () => {
const raw = Object.assign(new Error('Record was modified by another user'), {
code: 'CONCURRENT_UPDATE',
httpStatus: 409,
details: {
currentVersion: '2026-05-22T07:14:00.000Z',
currentRecord: { id: 'rec_1' },
userMessage: 'Someone in your team saved this first — reload before retrying.',
},
});
expect(declaredUserMessage(raw)).toBe(
'Someone in your team saved this first — reload before retrying.',
);
expect(declaredUserMessage(normaliseClientError(raw))).toBe(
'Someone in your team saved this first — reload before retrying.',
);
});
it('leaves the untouched passthrough arm exactly as it was', () => {
// A 403 is returned unchanged by `normaliseClientError` — it is neither of
// the two re-wrapped shapes. Pinned as the control: it reads the same
// before and after, so a failure in the two above is about the re-wraps
// and not about the reader.
const raw = Object.assign(new Error('FORBIDDEN: insufficient privileges'), {
code: 'FORBIDDEN',
httpStatus: 403,
userMessage: 'Ask finance to approve this record first.',
});
expect(normaliseClientError(raw)).toBe(raw);
expect(declaredUserMessage(normaliseClientError(raw))).toBe(
'Ask finance to approve this record first.',
);
});
it('still answers null for an unmarked refusal on both re-wrapped shapes', () => {
// objectstack#3821 holds by construction: nothing the producer did not
// opt into can reach the user through this channel.
const validation = Object.assign(new Error('Validation failed'), {
code: 'VALIDATION_FAILED',
details: { fields: [{ field: 'name', message: 'Name is required' }] },
});
const conflict = Object.assign(new Error('stale'), {
code: 'CONCURRENT_UPDATE',
httpStatus: 409,
});
expect(declaredUserMessage(normaliseClientError(validation))).toBeNull();
expect(declaredUserMessage(normaliseClientError(conflict))).toBeNull();
});
});
describe('a UNIQUE_VIOLATION field survives the adapter boundary', () => {
it('reaches extractFieldErrors through the passthrough arm', () => {
// `normaliseClientError` re-wraps only VALIDATION_FAILED and
// CONCURRENT_UPDATE; a unique-index 409 leaves it untouched, so the single
// `details.field` the client parked is what the form reads.
const raw = Object.assign(new Error('A record with this SKU already exists'), {
code: 'UNIQUE_VIOLATION',
httpStatus: 409,
details: {
error: 'A record with this SKU already exists',
code: 'UNIQUE_VIOLATION',
field: 'sku',
object: 'product',
},
});
const normalised = normaliseClientError(raw) as typeof raw;
expect(normalised.code).toBe('UNIQUE_VIOLATION');
expect(normalised.httpStatus).toBe(409);
expect(extractFieldErrors(normalised)).toEqual([
{ field: 'sku', message: 'A record with this SKU already exists' },
]);
});
});