Skip to content

Commit fa71af3

Browse files
committed
Add api response validator
1 parent 9cb740f commit fa71af3

File tree

6 files changed

+410
-0
lines changed

6 files changed

+410
-0
lines changed

views/interactivity/src/components/api/dictionary.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { AuthorizationError } from '../../error/authorization';
2+
import { InvalidResponseError } from '../../error/invalid-response';
23
import { NotFoundError } from '../../error/not-found';
34
import { InternalServerError } from '../../error/server';
45
import { DictionaryApi } from './dictionary';
@@ -17,6 +18,16 @@ const validResponse = {
1718
},
1819
};
1920

21+
const invalidResponse = {
22+
dictionaryWord: {
23+
word: searchWord,
24+
},
25+
accessSummary: {
26+
totalAccess: 1,
27+
lastAccessAt: '2024-10-28T16:20:42.846Z',
28+
},
29+
};
30+
2031
describe('GET /dictionary/:word', () => {
2132
test('api should return success response', async () => {
2233
global.fetch = jest.fn(
@@ -38,6 +49,21 @@ describe('GET /dictionary/:word', () => {
3849
});
3950
});
4051

52+
test('api should return error on invalid response', async () => {
53+
global.fetch = jest.fn(
54+
() =>
55+
Promise.resolve({
56+
ok: true,
57+
status: 200,
58+
json: () => Promise.resolve(invalidResponse),
59+
}) as Promise<Response>,
60+
);
61+
62+
await expect(
63+
async () => await dictionaryApi.fetch(searchWord),
64+
).rejects.toThrowError(InvalidResponseError);
65+
});
66+
4167
test('api should return error if search-word is not found', async () => {
4268
global.fetch = jest.fn(
4369
() =>

views/interactivity/src/components/api/dictionary.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import { DictionaryEntry } from '../../types';
33
import { AuthorizationError } from '../../error/authorization';
44
import { InternalServerError } from '../../error/server';
55
import { NotFoundError } from '../../error/not-found';
6+
import { lexicalEntrySchema, responseSchema } from './dictionary.validator';
7+
import { InvalidResponseError } from '../../error/invalid-response';
68

79
export class DictionaryApi {
810
private token: string;
@@ -34,9 +36,20 @@ export class DictionaryApi {
3436
}
3537

3638
const data = await response.json();
39+
const isResponseValid = await responseSchema.isValid(data);
40+
if (!isResponseValid) {
41+
throw new InvalidResponseError();
42+
}
43+
3744
data.dictionaryWord = JSON.parse(
3845
this.utils.decodeBase64Gzip(data.dictionaryWord.lexicalEntry),
3946
);
47+
const isDictionaryWordValid = await lexicalEntrySchema.isValid(
48+
data.dictionaryWord,
49+
);
50+
if (!isDictionaryWordValid) {
51+
throw new InvalidResponseError();
52+
}
4053

4154
return data as unknown as DictionaryEntry;
4255
}
Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
import { lexicalEntrySchema, responseSchema } from './dictionary.validator';
2+
3+
describe('lexical-entry schema validate', () => {
4+
test.each([
5+
[
6+
{
7+
name: 'hello',
8+
entry: {
9+
ipaListings: {
10+
uk: [
11+
{
12+
category: '',
13+
ipa: '/heˈləʊ/',
14+
audio:
15+
'https://dictionary.cambridge.org/media/english/uk_pron/u/ukh/ukhef/ukheft_029.mp3',
16+
},
17+
],
18+
us: [
19+
{
20+
category: '',
21+
ipa: '/heˈloʊ/',
22+
audio:
23+
'https://dictionary.cambridge.org/media/english/us_pron/h/hel/hello/hello.mp3',
24+
},
25+
],
26+
},
27+
meanings: [
28+
{
29+
categories: 'exclamation, noun',
30+
entries: [
31+
{
32+
meaning: 'used when meeting or greeting someone:',
33+
examples: ["Hello, Paul. I haven't seen you for ages."],
34+
},
35+
],
36+
},
37+
],
38+
},
39+
},
40+
],
41+
[
42+
{
43+
name: 'hello',
44+
entry: {
45+
ipaListings: {
46+
uk: [
47+
{
48+
category: '',
49+
ipa: '/heˈləʊ/',
50+
audio:
51+
'https://dictionary.cambridge.org/media/english/uk_pron/u/ukh/ukhef/ukheft_029.mp3',
52+
},
53+
],
54+
},
55+
meanings: [],
56+
},
57+
},
58+
],
59+
[
60+
{
61+
name: 'hello',
62+
entry: {
63+
ipaListings: {
64+
us: [
65+
{
66+
category: '',
67+
ipa: '/heˈloʊ/',
68+
audio:
69+
'https://dictionary.cambridge.org/media/english/us_pron/h/hel/hello/hello.mp3',
70+
},
71+
],
72+
},
73+
meanings: [
74+
{
75+
categories: 'exclamation, noun',
76+
entries: [],
77+
},
78+
],
79+
},
80+
},
81+
],
82+
])('schema should accept all valid lexical-entries', (validObject) => {
83+
expect(lexicalEntrySchema.isValidSync(validObject)).toBe(true);
84+
});
85+
86+
test.each([
87+
[
88+
{
89+
entry: {
90+
ipaListings: {
91+
us: [
92+
{
93+
category: '',
94+
ipa: '/heˈloʊ/',
95+
audio:
96+
'https://dictionary.cambridge.org/media/english/us_pron/h/hel/hello/hello.mp3',
97+
},
98+
],
99+
},
100+
meanings: [
101+
{
102+
categories: 'exclamation, noun',
103+
entries: [
104+
{
105+
meaning: 'used when meeting or greeting someone:',
106+
examples: ["Hello, Paul. I haven't seen you for ages."],
107+
},
108+
],
109+
},
110+
],
111+
},
112+
},
113+
],
114+
[
115+
{
116+
name: 'hello',
117+
entry: {
118+
ipaListings: {},
119+
meanings: [
120+
{
121+
categories: 'exclamation, noun',
122+
entries: [
123+
{
124+
meaning: 'used when meeting or greeting someone:',
125+
examples: ["Hello, Paul. I haven't seen you for ages."],
126+
},
127+
],
128+
},
129+
],
130+
},
131+
},
132+
],
133+
[
134+
{
135+
name: 'hello',
136+
entry: {
137+
ipaListings: {
138+
us: [
139+
{
140+
category: '',
141+
ipa: '/heˈloʊ/',
142+
audio:
143+
'https://dictionary.cambridge.org/media/english/us_pron/h/hel/hello/hello.mp3',
144+
},
145+
],
146+
},
147+
},
148+
},
149+
],
150+
[
151+
{
152+
name: 'hello',
153+
entry: {
154+
meanings: [
155+
{
156+
categories: 'exclamation, noun',
157+
entries: [
158+
{
159+
meaning: 'used when meeting or greeting someone:',
160+
examples: ["Hello, Paul. I haven't seen you for ages."],
161+
},
162+
],
163+
},
164+
],
165+
},
166+
},
167+
],
168+
[
169+
{
170+
name: 'hello',
171+
},
172+
],
173+
])('schema should reject all invalid lexical-entries', (invalidObject) => {
174+
expect(lexicalEntrySchema.isValidSync(invalidObject)).toBe(false);
175+
});
176+
});
177+
178+
describe('response-schema validate', () => {
179+
test.each([
180+
[
181+
{
182+
dictionaryWord: {
183+
word: 'hello',
184+
lexicalEntry: 'base64-encoded string',
185+
},
186+
accessSummary: {
187+
totalAccess: 1,
188+
lastAccessAt: '2024-10-30T08:04:48.390Z',
189+
},
190+
},
191+
],
192+
[
193+
{
194+
dictionaryWord: {
195+
word: 'hello',
196+
lexicalEntry: 'base64-encoded string',
197+
},
198+
accessSummary: null,
199+
},
200+
],
201+
])('schema should accept all valid response', (validResponse) => {
202+
expect(responseSchema.isValidSync(validResponse)).toBe(true);
203+
});
204+
205+
test.each([
206+
[
207+
{
208+
dictionaryWord: {
209+
word: 'hello',
210+
},
211+
accessSummary: {
212+
totalAccess: 1,
213+
lastAccessAt: '2024-10-30T08:04:48.390Z',
214+
},
215+
},
216+
],
217+
[
218+
{
219+
dictionaryWord: {
220+
lexicalEntry: 'base64-encoded string',
221+
},
222+
accessSummary: {
223+
totalAccess: 1,
224+
lastAccessAt: '2024-10-30T08:04:48.390Z',
225+
},
226+
},
227+
],
228+
[
229+
{
230+
dictionaryWord: {},
231+
accessSummary: {
232+
totalAccess: 1,
233+
lastAccessAt: '2024-10-30T08:04:48.390Z',
234+
},
235+
},
236+
],
237+
[
238+
{
239+
dictionaryWord: {
240+
word: 'hello',
241+
lexicalEntry: 'base64-encoded string',
242+
},
243+
accessSummary: {
244+
lastAccessAt: '2024-10-30T08:04:48.390Z',
245+
},
246+
},
247+
],
248+
[
249+
{
250+
dictionaryWord: {
251+
word: 'hello',
252+
lexicalEntry: 'base64-encoded string',
253+
},
254+
accessSummary: {
255+
totalAccess: 1,
256+
},
257+
},
258+
],
259+
[
260+
{
261+
accessSummary: {
262+
totalAccess: 1,
263+
lastAccessAt: '2024-10-30T08:04:48.390Z',
264+
},
265+
},
266+
],
267+
])('schema should reject all invalid response', (invalidResponse) => {
268+
expect(responseSchema.isValidSync(invalidResponse)).toBe(false);
269+
});
270+
});

0 commit comments

Comments
 (0)