Skip to content

Commit a902d8b

Browse files
authored
Reject usage reports with incorrect duration and errorsTotal values (#6433)
1 parent c29d3c5 commit a902d8b

File tree

5 files changed

+675
-9
lines changed

5 files changed

+675
-9
lines changed

.changeset/healthy-flies-wink.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'hive': patch
3+
---
4+
5+
Improves validation for operation durations and error totals. Prevents processing of invalid usage report data.
Lines changed: 384 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,384 @@
1+
import { initSeed } from 'testkit/seed';
2+
import { getServiceHost } from '../../testkit/utils';
3+
4+
test('valid operation is accepted', async () => {
5+
const { createOrg } = await initSeed().createOwner();
6+
const { createProject } = await createOrg();
7+
const { createTargetAccessToken } = await createProject();
8+
const { secret: accessToken } = await createTargetAccessToken({});
9+
10+
const usageAddress = await getServiceHost('usage', 8081);
11+
12+
const response = await fetch(`http://${usageAddress}`, {
13+
method: 'POST',
14+
headers: {
15+
'Content-Type': 'application/json',
16+
Authorization: `Bearer ${accessToken}`,
17+
},
18+
body: JSON.stringify({
19+
size: 1,
20+
map: {
21+
c3b6d9b0: {
22+
operationName: 'me',
23+
operation: 'query me { me { id name } }',
24+
fields: ['Query', 'Query.me', 'User', 'User.id', 'User.name'],
25+
},
26+
},
27+
operations: [
28+
{
29+
operationMapKey: 'c3b6d9b0',
30+
timestamp: 1663158676535,
31+
execution: {
32+
ok: true,
33+
duration: 150000000,
34+
errorsTotal: 0,
35+
},
36+
metadata: {
37+
client: {
38+
name: 'demo',
39+
version: '0.0.1',
40+
},
41+
},
42+
},
43+
],
44+
}),
45+
});
46+
expect(response.status).toBe(200);
47+
expect(await response.json()).toMatchObject({
48+
id: expect.any(String),
49+
operations: {
50+
accepted: 1,
51+
rejected: 0,
52+
},
53+
});
54+
});
55+
56+
test('invalid operation is rejected', async () => {
57+
const { createOrg } = await initSeed().createOwner();
58+
const { createProject } = await createOrg();
59+
const { createTargetAccessToken } = await createProject();
60+
const { secret: accessToken } = await createTargetAccessToken({});
61+
62+
const usageAddress = await getServiceHost('usage', 8081);
63+
// GraphQL operation is invalid at Query.me(id:)
64+
const faultyOperation = 'query me { me(id: ) { id name } }';
65+
66+
const response = await fetch(`http://${usageAddress}`, {
67+
method: 'POST',
68+
headers: {
69+
'Content-Type': 'application/json',
70+
Authorization: `Bearer ${accessToken}`,
71+
},
72+
body: JSON.stringify({
73+
size: 3,
74+
map: {
75+
c3b6d9b0: {
76+
operationName: 'me',
77+
operation: faultyOperation,
78+
fields: ['Query', 'Query.me', 'User', 'User.id', 'User.name'],
79+
},
80+
},
81+
operations: [
82+
{
83+
operationMapKey: 'c3b6d9b0',
84+
timestamp: 1663158676535,
85+
execution: {
86+
ok: true,
87+
duration: 150000000,
88+
errorsTotal: 0,
89+
},
90+
metadata: {
91+
client: {
92+
name: 'demo',
93+
version: '0.0.1',
94+
},
95+
},
96+
},
97+
],
98+
}),
99+
});
100+
expect(response.status).toBe(200);
101+
expect(await response.json()).toMatchObject({
102+
id: expect.any(String),
103+
operations: {
104+
accepted: 0,
105+
rejected: 1,
106+
},
107+
});
108+
});
109+
110+
test('reject a report with a negative timestamp', async () => {
111+
const { createOrg } = await initSeed().createOwner();
112+
const { createProject } = await createOrg();
113+
const { createTargetAccessToken } = await createProject();
114+
const { secret: accessToken } = await createTargetAccessToken({});
115+
116+
const usageAddress = await getServiceHost('usage', 8081);
117+
118+
const response = await fetch(`http://${usageAddress}`, {
119+
method: 'POST',
120+
headers: {
121+
'Content-Type': 'application/json',
122+
Authorization: `Bearer ${accessToken}`,
123+
},
124+
body: JSON.stringify({
125+
size: 1,
126+
map: {
127+
c3b6d9b0: {
128+
operationName: 'me',
129+
operation: 'query me { me { id name } }',
130+
fields: ['Query', 'Query.me', 'User', 'User.id', 'User.name'],
131+
},
132+
},
133+
operations: [
134+
{
135+
operationMapKey: 'c3b6d9b0',
136+
timestamp: -1663158676535,
137+
execution: {
138+
ok: true,
139+
duration: 150000000,
140+
errorsTotal: 0,
141+
},
142+
},
143+
],
144+
}),
145+
});
146+
expect(response.status).toBe(200);
147+
expect(await response.json()).toMatchObject({
148+
id: expect.any(String),
149+
operations: {
150+
accepted: 0,
151+
rejected: 1,
152+
},
153+
});
154+
});
155+
156+
test('reject a report with an too short timestamp', async () => {
157+
const { createOrg } = await initSeed().createOwner();
158+
const { createProject } = await createOrg();
159+
const { createTargetAccessToken } = await createProject();
160+
const { secret: accessToken } = await createTargetAccessToken({});
161+
162+
const usageAddress = await getServiceHost('usage', 8081);
163+
164+
const response = await fetch(`http://${usageAddress}`, {
165+
method: 'POST',
166+
headers: {
167+
'Content-Type': 'application/json',
168+
Authorization: `Bearer ${accessToken}`,
169+
},
170+
body: JSON.stringify({
171+
size: 1,
172+
map: {
173+
c3b6d9b0: {
174+
operationName: 'me',
175+
operation: 'query me { me { id name } }',
176+
fields: ['Query', 'Query.me', 'User', 'User.id', 'User.name'],
177+
},
178+
},
179+
operations: [
180+
{
181+
operationMapKey: 'c3b6d9b0',
182+
timestamp: 1234,
183+
execution: {
184+
ok: true,
185+
duration: 150000000,
186+
errorsTotal: 0,
187+
},
188+
},
189+
],
190+
}),
191+
});
192+
expect(response.status).toBe(200);
193+
expect(await response.json()).toMatchObject({
194+
id: expect.any(String),
195+
operations: {
196+
accepted: 0,
197+
rejected: 1,
198+
},
199+
});
200+
});
201+
202+
test('reject a report with a negative duration', async () => {
203+
const { createOrg } = await initSeed().createOwner();
204+
const { createProject } = await createOrg();
205+
const { createTargetAccessToken } = await createProject();
206+
const { secret: accessToken } = await createTargetAccessToken({});
207+
208+
const usageAddress = await getServiceHost('usage', 8081);
209+
210+
const response = await fetch(`http://${usageAddress}`, {
211+
method: 'POST',
212+
headers: {
213+
'Content-Type': 'application/json',
214+
Authorization: `Bearer ${accessToken}`,
215+
},
216+
body: JSON.stringify({
217+
size: 1,
218+
map: {
219+
c3b6d9b0: {
220+
operationName: 'me',
221+
operation: 'query me { me { id name } }',
222+
fields: ['Query', 'Query.me', 'User', 'User.id', 'User.name'],
223+
},
224+
},
225+
operations: [
226+
{
227+
operationMapKey: 'c3b6d9b0',
228+
timestamp: 1663158676535,
229+
execution: {
230+
ok: true,
231+
duration: -150000000,
232+
errorsTotal: 0,
233+
},
234+
},
235+
],
236+
}),
237+
});
238+
expect(response.status).toBe(200);
239+
expect(await response.json()).toMatchObject({
240+
id: expect.any(String),
241+
operations: {
242+
accepted: 0,
243+
rejected: 1,
244+
},
245+
});
246+
});
247+
248+
test('reject a report with a too big duration', async () => {
249+
const { createOrg } = await initSeed().createOwner();
250+
const { createProject } = await createOrg();
251+
const { createTargetAccessToken } = await createProject();
252+
const { secret: accessToken } = await createTargetAccessToken({});
253+
254+
const usageAddress = await getServiceHost('usage', 8081);
255+
256+
const response = await fetch(`http://${usageAddress}`, {
257+
method: 'POST',
258+
headers: {
259+
'Content-Type': 'application/json',
260+
Authorization: `Bearer ${accessToken}`,
261+
},
262+
body: JSON.stringify({
263+
size: 1,
264+
map: {
265+
c3b6d9b0: {
266+
operationName: 'me',
267+
operation: 'query me { me { id name } }',
268+
fields: ['Query', 'Query.me', 'User', 'User.id', 'User.name'],
269+
},
270+
},
271+
operations: [
272+
{
273+
operationMapKey: 'c3b6d9b0',
274+
timestamp: 1663158676535,
275+
execution: {
276+
ok: true,
277+
duration: Math.pow(2, 64),
278+
errorsTotal: 0,
279+
},
280+
},
281+
],
282+
}),
283+
});
284+
expect(response.status).toBe(200);
285+
expect(await response.json()).toMatchObject({
286+
id: expect.any(String),
287+
operations: {
288+
accepted: 0,
289+
rejected: 1,
290+
},
291+
});
292+
});
293+
294+
test('reject a report with a negative errorsTotal', async () => {
295+
const { createOrg } = await initSeed().createOwner();
296+
const { createProject } = await createOrg();
297+
const { createTargetAccessToken } = await createProject();
298+
const { secret: accessToken } = await createTargetAccessToken({});
299+
300+
const usageAddress = await getServiceHost('usage', 8081);
301+
302+
const response = await fetch(`http://${usageAddress}`, {
303+
method: 'POST',
304+
headers: {
305+
'Content-Type': 'application/json',
306+
Authorization: `Bearer ${accessToken}`,
307+
},
308+
body: JSON.stringify({
309+
size: 1,
310+
map: {
311+
c3b6d9b0: {
312+
operationName: 'me',
313+
operation: 'query me { me { id name } }',
314+
fields: ['Query', 'Query.me', 'User', 'User.id', 'User.name'],
315+
},
316+
},
317+
operations: [
318+
{
319+
operationMapKey: 'c3b6d9b0',
320+
timestamp: 1663158676535,
321+
execution: {
322+
ok: true,
323+
duration: 150000000,
324+
errorsTotal: -2,
325+
},
326+
},
327+
],
328+
}),
329+
});
330+
expect(response.status).toBe(200);
331+
expect(await response.json()).toMatchObject({
332+
id: expect.any(String),
333+
operations: {
334+
accepted: 0,
335+
rejected: 1,
336+
},
337+
});
338+
});
339+
340+
test('reject a report with a too big errorsTotal', async () => {
341+
const { createOrg } = await initSeed().createOwner();
342+
const { createProject } = await createOrg();
343+
const { createTargetAccessToken } = await createProject();
344+
const { secret: accessToken } = await createTargetAccessToken({});
345+
346+
const usageAddress = await getServiceHost('usage', 8081);
347+
348+
const response = await fetch(`http://${usageAddress}`, {
349+
method: 'POST',
350+
headers: {
351+
'Content-Type': 'application/json',
352+
Authorization: `Bearer ${accessToken}`,
353+
},
354+
body: JSON.stringify({
355+
size: 1,
356+
map: {
357+
c3b6d9b0: {
358+
operationName: 'me',
359+
operation: 'query me { me { id name } }',
360+
fields: ['Query', 'Query.me', 'User', 'User.id', 'User.name'],
361+
},
362+
},
363+
operations: [
364+
{
365+
operationMapKey: 'c3b6d9b0',
366+
timestamp: 1663158676535,
367+
execution: {
368+
ok: true,
369+
duration: Math.pow(2, 10),
370+
errorsTotal: Math.pow(2, 25),
371+
},
372+
},
373+
],
374+
}),
375+
});
376+
expect(response.status).toBe(200);
377+
expect(await response.json()).toMatchObject({
378+
id: expect.any(String),
379+
operations: {
380+
accepted: 0,
381+
rejected: 1,
382+
},
383+
});
384+
});

0 commit comments

Comments
 (0)