@theihtisham/devtools-with-cloud 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/.env.example +15 -0
  2. package/LICENSE +21 -0
  3. package/README.md +73 -0
  4. package/docker-compose.yml +23 -0
  5. package/jest.config.js +7 -0
  6. package/next-env.d.ts +5 -0
  7. package/next.config.mjs +22 -0
  8. package/package.json +82 -0
  9. package/postcss.config.js +6 -0
  10. package/prisma/schema.prisma +105 -0
  11. package/prisma/seed.ts +211 -0
  12. package/src/app/(app)/ai/page.tsx +122 -0
  13. package/src/app/(app)/collections/page.tsx +155 -0
  14. package/src/app/(app)/environments/page.tsx +96 -0
  15. package/src/app/(app)/history/page.tsx +107 -0
  16. package/src/app/(app)/import/page.tsx +102 -0
  17. package/src/app/(app)/layout.tsx +60 -0
  18. package/src/app/(app)/settings/page.tsx +79 -0
  19. package/src/app/(app)/workspace/page.tsx +284 -0
  20. package/src/app/api/ai/discover/route.ts +17 -0
  21. package/src/app/api/ai/explain/route.ts +29 -0
  22. package/src/app/api/ai/generate-tests/route.ts +37 -0
  23. package/src/app/api/ai/suggest/route.ts +29 -0
  24. package/src/app/api/collections/[id]/route.ts +66 -0
  25. package/src/app/api/collections/route.ts +48 -0
  26. package/src/app/api/environments/route.ts +40 -0
  27. package/src/app/api/export/openapi/route.ts +17 -0
  28. package/src/app/api/export/postman/route.ts +18 -0
  29. package/src/app/api/import/curl/route.ts +18 -0
  30. package/src/app/api/import/har/route.ts +20 -0
  31. package/src/app/api/import/openapi/route.ts +21 -0
  32. package/src/app/api/import/postman/route.ts +21 -0
  33. package/src/app/api/proxy/route.ts +35 -0
  34. package/src/app/api/requests/[id]/execute/route.ts +85 -0
  35. package/src/app/api/requests/[id]/history/route.ts +23 -0
  36. package/src/app/api/requests/[id]/route.ts +66 -0
  37. package/src/app/api/requests/route.ts +49 -0
  38. package/src/app/api/workspaces/route.ts +38 -0
  39. package/src/app/globals.css +99 -0
  40. package/src/app/layout.tsx +24 -0
  41. package/src/app/page.tsx +182 -0
  42. package/src/components/ai/ai-panel.tsx +65 -0
  43. package/src/components/ai/code-explainer.tsx +51 -0
  44. package/src/components/ai/endpoint-discovery.tsx +62 -0
  45. package/src/components/ai/test-generator.tsx +49 -0
  46. package/src/components/collections/collection-actions.tsx +36 -0
  47. package/src/components/collections/collection-tree.tsx +55 -0
  48. package/src/components/collections/folder-creator.tsx +54 -0
  49. package/src/components/landing/comparison.tsx +43 -0
  50. package/src/components/landing/cta.tsx +16 -0
  51. package/src/components/landing/features.tsx +24 -0
  52. package/src/components/landing/hero.tsx +23 -0
  53. package/src/components/response/body-viewer.tsx +33 -0
  54. package/src/components/response/headers-viewer.tsx +23 -0
  55. package/src/components/response/status-badge.tsx +25 -0
  56. package/src/components/response/test-results.tsx +50 -0
  57. package/src/components/response/timing-chart.tsx +39 -0
  58. package/src/components/ui/badge.tsx +24 -0
  59. package/src/components/ui/button.tsx +32 -0
  60. package/src/components/ui/code-editor.tsx +51 -0
  61. package/src/components/ui/dialog.tsx +56 -0
  62. package/src/components/ui/dropdown.tsx +63 -0
  63. package/src/components/ui/input.tsx +22 -0
  64. package/src/components/ui/key-value-editor.tsx +75 -0
  65. package/src/components/ui/select.tsx +24 -0
  66. package/src/components/ui/tabs.tsx +85 -0
  67. package/src/components/ui/textarea.tsx +22 -0
  68. package/src/components/ui/toast.tsx +54 -0
  69. package/src/components/workspace/request-panel.tsx +38 -0
  70. package/src/components/workspace/response-panel.tsx +81 -0
  71. package/src/components/workspace/sidebar.tsx +52 -0
  72. package/src/components/workspace/split-pane.tsx +49 -0
  73. package/src/components/workspace/tabs/auth-tab.tsx +94 -0
  74. package/src/components/workspace/tabs/body-tab.tsx +41 -0
  75. package/src/components/workspace/tabs/headers-tab.tsx +23 -0
  76. package/src/components/workspace/tabs/params-tab.tsx +23 -0
  77. package/src/components/workspace/tabs/pre-request-tab.tsx +26 -0
  78. package/src/components/workspace/url-bar.tsx +53 -0
  79. package/src/hooks/use-ai.ts +115 -0
  80. package/src/hooks/use-collection.ts +71 -0
  81. package/src/hooks/use-environment.ts +73 -0
  82. package/src/hooks/use-request.ts +111 -0
  83. package/src/lib/ai/endpoint-discovery.ts +158 -0
  84. package/src/lib/ai/explainer.ts +127 -0
  85. package/src/lib/ai/suggester.ts +164 -0
  86. package/src/lib/ai/test-generator.ts +161 -0
  87. package/src/lib/auth/api-key.ts +28 -0
  88. package/src/lib/auth/aws-sig.ts +131 -0
  89. package/src/lib/auth/basic.ts +17 -0
  90. package/src/lib/auth/bearer.ts +15 -0
  91. package/src/lib/auth/oauth2.ts +155 -0
  92. package/src/lib/auth/types.ts +16 -0
  93. package/src/lib/db/client.ts +15 -0
  94. package/src/lib/env/manager.ts +32 -0
  95. package/src/lib/env/resolver.ts +30 -0
  96. package/src/lib/exporters/openapi.ts +193 -0
  97. package/src/lib/exporters/postman.ts +140 -0
  98. package/src/lib/graphql/builder.ts +249 -0
  99. package/src/lib/graphql/formatter.ts +147 -0
  100. package/src/lib/graphql/index.ts +43 -0
  101. package/src/lib/graphql/introspection.ts +175 -0
  102. package/src/lib/graphql/types.ts +99 -0
  103. package/src/lib/graphql/validator.ts +216 -0
  104. package/src/lib/http/client.ts +112 -0
  105. package/src/lib/http/proxy.ts +83 -0
  106. package/src/lib/http/request-builder.ts +214 -0
  107. package/src/lib/http/response-parser.ts +106 -0
  108. package/src/lib/http/timing.ts +63 -0
  109. package/src/lib/importers/curl-parser.ts +346 -0
  110. package/src/lib/importers/har-parser.ts +128 -0
  111. package/src/lib/importers/openapi.ts +324 -0
  112. package/src/lib/importers/postman.ts +312 -0
  113. package/src/lib/test-runner/assertions.ts +163 -0
  114. package/src/lib/test-runner/reporter.ts +90 -0
  115. package/src/lib/test-runner/runner.ts +69 -0
  116. package/src/lib/utils/api-response.ts +85 -0
  117. package/src/lib/utils/cn.ts +6 -0
  118. package/src/lib/utils/content-type.ts +123 -0
  119. package/src/lib/utils/download.ts +53 -0
  120. package/src/lib/utils/errors.ts +92 -0
  121. package/src/lib/utils/format.ts +142 -0
  122. package/src/lib/utils/syntax-highlight.ts +108 -0
  123. package/src/lib/utils/validation.ts +231 -0
  124. package/src/lib/websocket/client.ts +182 -0
  125. package/src/lib/websocket/frames.ts +96 -0
  126. package/src/lib/websocket/history.ts +121 -0
  127. package/src/lib/websocket/index.ts +25 -0
  128. package/src/lib/websocket/types.ts +57 -0
  129. package/src/types/ai.ts +28 -0
  130. package/src/types/collection.ts +24 -0
  131. package/src/types/environment.ts +16 -0
  132. package/src/types/request.ts +54 -0
  133. package/src/types/response.ts +37 -0
  134. package/tailwind.config.ts +82 -0
  135. package/tests/lib/env/resolver.test.ts +108 -0
  136. package/tests/lib/graphql/builder.test.ts +349 -0
  137. package/tests/lib/graphql/formatter.test.ts +99 -0
  138. package/tests/lib/http/request-builder.test.ts +160 -0
  139. package/tests/lib/http/response-parser.test.ts +150 -0
  140. package/tests/lib/http/timing.test.ts +188 -0
  141. package/tests/lib/importers/curl-parser.test.ts +245 -0
  142. package/tests/lib/test-runner/assertions.test.ts +342 -0
  143. package/tests/lib/utils/cn.test.ts +46 -0
  144. package/tests/lib/utils/content-type.test.ts +175 -0
  145. package/tests/lib/utils/format.test.ts +188 -0
  146. package/tests/lib/utils/validation.test.ts +237 -0
  147. package/tests/lib/websocket/history.test.ts +186 -0
  148. package/tsconfig.json +29 -0
  149. package/tsconfig.tsbuildinfo +1 -0
  150. package/vitest.config.ts +21 -0
@@ -0,0 +1,349 @@
1
+ // @ts-nocheck
2
+ import { describe, it, expect } from 'vitest';
3
+ import { QueryBuilder } from '@/lib/graphql/builder';
4
+ import { validateQuery, validateQueryString, validateAgainstSchema } from '@/lib/graphql/validator';
5
+ import type { GraphQLSchema } from '@/lib/graphql/types';
6
+
7
+ describe('QueryBuilder', () => {
8
+ describe('basic queries', () => {
9
+ it('should build a simple query with no name', () => {
10
+ const result = new QueryBuilder()
11
+ .query()
12
+ .field('users')
13
+ .build();
14
+
15
+ expect(result).toContain('query');
16
+ expect(result).toContain('users');
17
+ });
18
+
19
+ it('should build a named query', () => {
20
+ const result = new QueryBuilder()
21
+ .query('GetUsers')
22
+ .field('users')
23
+ .build();
24
+
25
+ expect(result).toBe('query GetUsers { users }');
26
+ });
27
+
28
+ it('should build a query with multiple fields', () => {
29
+ const result = new QueryBuilder()
30
+ .query('getUser')
31
+ .field('id')
32
+ .field('name')
33
+ .field('email')
34
+ .build();
35
+
36
+ expect(result).toBe('query getUser { id name email }');
37
+ });
38
+
39
+ it('should build a mutation', () => {
40
+ const result = new QueryBuilder()
41
+ .mutation('createUser')
42
+ .field('createUser', { input: '{ name: "John" }' }, ['id', 'name'])
43
+ .build();
44
+
45
+ expect(result).toContain('mutation createUser');
46
+ expect(result).toContain('createUser(input: { name: "John" })');
47
+ expect(result).toContain('id');
48
+ expect(result).toContain('name');
49
+ });
50
+
51
+ it('should build a subscription', () => {
52
+ const result = new QueryBuilder()
53
+ .subscription('onMessage')
54
+ .field('messageAdded', {}, ['id', 'text'])
55
+ .build();
56
+
57
+ expect(result).toContain('subscription onMessage');
58
+ expect(result).toContain('messageAdded');
59
+ });
60
+ });
61
+
62
+ describe('nested fields', () => {
63
+ it('should handle nested subfields as strings', () => {
64
+ const result = new QueryBuilder()
65
+ .query('getUser')
66
+ .field('user', { id: '1' }, ['id', 'name', 'email'])
67
+ .build();
68
+
69
+ expect(result).toBe('query getUser { user(id: 1) { id name email } }');
70
+ });
71
+
72
+ it('should handle deeply nested subfields', () => {
73
+ const result = new QueryBuilder()
74
+ .query('getUser')
75
+ .field('user', { id: '1' }, [
76
+ { name: 'id' },
77
+ { name: 'profile', subfields: [{ name: 'avatar' }, { name: 'bio' }] },
78
+ ])
79
+ .build();
80
+
81
+ expect(result).toContain('user(id: 1)');
82
+ expect(result).toContain('profile { avatar bio }');
83
+ });
84
+ });
85
+
86
+ describe('variables', () => {
87
+ it('should add variables to the query', () => {
88
+ const result = new QueryBuilder()
89
+ .query('getUser')
90
+ .variable('userId', 'ID!')
91
+ .field('user', { id: '$userId' }, ['id', 'name'])
92
+ .build();
93
+
94
+ expect(result).toContain('($userId: ID!)');
95
+ expect(result).toContain('user(id: $userId)');
96
+ });
97
+
98
+ it('should handle variables with default values', () => {
99
+ const result = new QueryBuilder()
100
+ .query('getUsers')
101
+ .variable('limit', 'Int', '10')
102
+ .field('users', { limit: '$limit' }, ['id', 'name'])
103
+ .build();
104
+
105
+ expect(result).toContain('($limit: Int = 10)');
106
+ });
107
+
108
+ it('should handle multiple variables', () => {
109
+ const result = new QueryBuilder()
110
+ .query('searchUsers')
111
+ .variable('query', 'String!')
112
+ .variable('limit', 'Int', '20')
113
+ .variable('offset', 'Int', '0')
114
+ .field('search', { query: '$query', limit: '$limit', offset: '$offset' }, ['id', 'name'])
115
+ .build();
116
+
117
+ expect(result).toContain('$query: String!');
118
+ expect(result).toContain('$limit: Int = 20');
119
+ expect(result).toContain('$offset: Int = 0');
120
+ });
121
+ });
122
+
123
+ describe('fragments', () => {
124
+ it('should define and use a fragment', () => {
125
+ const result = new QueryBuilder()
126
+ .fragment('UserFields', 'User', ['id', 'name', 'email'])
127
+ .query('getUser')
128
+ .field('user', { id: '1' })
129
+ .spreadFragment('UserFields')
130
+ .build();
131
+
132
+ expect(result).toContain('fragment UserFields on User { id name email }');
133
+ expect(result).toContain('...UserFields');
134
+ });
135
+
136
+ it('should define multiple fragments', () => {
137
+ const result = new QueryBuilder()
138
+ .fragment('UserFields', 'User', ['id', 'name'])
139
+ .fragment('PostFields', 'Post', ['id', 'title'])
140
+ .query('getFeed')
141
+ .field('feed', {}, [
142
+ { name: 'id' },
143
+ { name: 'user', subfields: [{ name: '...UserFields' }] },
144
+ { name: 'post', subfields: [{ name: '...PostFields' }] },
145
+ ])
146
+ .build();
147
+
148
+ expect(result).toContain('fragment UserFields on User { id name }');
149
+ expect(result).toContain('fragment PostFields on Post { id title }');
150
+ });
151
+ });
152
+
153
+ describe('alias', () => {
154
+ it('should support field aliases', () => {
155
+ const result = new QueryBuilder()
156
+ .query('getUsers')
157
+ .aliasField('admin', 'user', { role: '"admin"' }, ['id', 'name'])
158
+ .aliasField('member', 'user', { role: '"member"' }, ['id', 'name'])
159
+ .build();
160
+
161
+ expect(result).toContain('admin: user(role: "admin") { id name }');
162
+ expect(result).toContain('member: user(role: "member") { id name }');
163
+ });
164
+ });
165
+
166
+ describe('toQuery', () => {
167
+ it('should return a structured query object', () => {
168
+ const query = new QueryBuilder()
169
+ .query('test')
170
+ .variable('id', 'ID!')
171
+ .field('user', { id: '$id' }, ['name'])
172
+ .toQuery();
173
+
174
+ expect(query.operationType).toBe('query');
175
+ expect(query.name).toBe('test');
176
+ expect(query.variables).toHaveLength(1);
177
+ expect(query.fields).toHaveLength(1);
178
+ });
179
+ });
180
+
181
+ describe('reset', () => {
182
+ it('should reset the builder state', () => {
183
+ const builder = new QueryBuilder()
184
+ .query('test')
185
+ .field('users')
186
+ .reset();
187
+
188
+ const result = builder.query('newTest').field('posts').build();
189
+ expect(result).toBe('query newTest { posts }');
190
+ });
191
+ });
192
+ });
193
+
194
+ describe('validateQuery', () => {
195
+ it('should validate a correct query', () => {
196
+ const result = validateQuery({
197
+ operationType: 'query',
198
+ name: 'test',
199
+ variables: [],
200
+ fields: [{ name: 'users' }],
201
+ fragments: [],
202
+ });
203
+
204
+ expect(result.valid).toBe(true);
205
+ expect(result.errors).toHaveLength(0);
206
+ });
207
+
208
+ it('should reject empty selection set', () => {
209
+ const result = validateQuery({
210
+ operationType: 'query',
211
+ variables: [],
212
+ fields: [],
213
+ fragments: [],
214
+ });
215
+
216
+ expect(result.valid).toBe(false);
217
+ expect(result.errors[0]?.message).toContain('empty');
218
+ });
219
+
220
+ it('should reject invalid operation type', () => {
221
+ const result = validateQuery({
222
+ operationType: 'invalid' as 'query',
223
+ variables: [],
224
+ fields: [{ name: 'test' }],
225
+ fragments: [],
226
+ });
227
+
228
+ expect(result.valid).toBe(false);
229
+ expect(result.errors.some((e) => e.message.includes('Invalid operation type'))).toBe(true);
230
+ });
231
+
232
+ it('should reject variables missing types', () => {
233
+ const result = validateQuery({
234
+ operationType: 'query',
235
+ variables: [{ name: 'id', type: '' }],
236
+ fields: [{ name: 'test' }],
237
+ fragments: [],
238
+ });
239
+
240
+ expect(result.valid).toBe(false);
241
+ expect(result.errors.some((e) => e.message.includes('missing a type'))).toBe(true);
242
+ });
243
+
244
+ it('should reject fragment missing onType', () => {
245
+ const result = validateQuery({
246
+ operationType: 'query',
247
+ variables: [],
248
+ fields: [{ name: 'test' }],
249
+ fragments: [{ name: 'Foo', onType: '', fields: [{ name: 'id' }] }],
250
+ });
251
+
252
+ expect(result.valid).toBe(false);
253
+ expect(result.errors.some((e) => e.message.includes('onType'))).toBe(true);
254
+ });
255
+ });
256
+
257
+ describe('validateQueryString', () => {
258
+ it('should validate a correct query string', () => {
259
+ const result = validateQueryString('query { users { id name } }');
260
+ expect(result.valid).toBe(true);
261
+ });
262
+
263
+ it('should reject empty query', () => {
264
+ const result = validateQueryString('');
265
+ expect(result.valid).toBe(false);
266
+ expect(result.errors[0]?.message).toContain('empty');
267
+ });
268
+
269
+ it('should reject unbalanced braces', () => {
270
+ const result = validateQueryString('query { users { id }');
271
+ expect(result.valid).toBe(false);
272
+ expect(result.errors.some((e) => e.message.includes('closing brace'))).toBe(true);
273
+ });
274
+
275
+ it('should reject query not starting with valid keyword', () => {
276
+ const result = validateQueryString('random { stuff }');
277
+ expect(result.valid).toBe(false);
278
+ expect(result.errors.some((e) => e.message.includes('must start with'))).toBe(true);
279
+ });
280
+
281
+ it('should accept inline query starting with {', () => {
282
+ const result = validateQueryString('{ users { id } }');
283
+ expect(result.valid).toBe(true);
284
+ });
285
+ });
286
+
287
+ describe('validateAgainstSchema', () => {
288
+ const mockSchema: GraphQLSchema = {
289
+ queryType: { name: 'Query' },
290
+ mutationType: { name: 'Mutation' },
291
+ types: [
292
+ {
293
+ name: 'Query',
294
+ kind: 'OBJECT',
295
+ fields: [
296
+ { name: 'users', type: { name: 'User', kind: 'OBJECT' }, args: [] },
297
+ { name: 'user', type: { name: 'User', kind: 'OBJECT' }, args: [{ name: 'id', type: { name: 'ID', kind: 'SCALAR' } }] },
298
+ ],
299
+ },
300
+ {
301
+ name: 'Mutation',
302
+ kind: 'OBJECT',
303
+ fields: [
304
+ { name: 'createUser', type: { name: 'User', kind: 'OBJECT' }, args: [] },
305
+ ],
306
+ },
307
+ ],
308
+ };
309
+
310
+ it('should validate fields that exist in the schema', () => {
311
+ const result = validateAgainstSchema(
312
+ { operationType: 'query', variables: [], fields: [{ name: 'users' }], fragments: [] },
313
+ mockSchema,
314
+ );
315
+
316
+ expect(result.valid).toBe(true);
317
+ });
318
+
319
+ it('should reject fields that do not exist in the schema', () => {
320
+ const result = validateAgainstSchema(
321
+ { operationType: 'query', variables: [], fields: [{ name: 'nonexistent' }], fragments: [] },
322
+ mockSchema,
323
+ );
324
+
325
+ expect(result.valid).toBe(false);
326
+ expect(result.errors.some((e) => e.message.includes('does not exist'))).toBe(true);
327
+ });
328
+
329
+ it('should reject when schema has no mutation type', () => {
330
+ const noMutSchema: GraphQLSchema = {
331
+ queryType: { name: 'Query' },
332
+ mutationType: null,
333
+ types: [
334
+ {
335
+ name: 'Query',
336
+ kind: 'OBJECT',
337
+ fields: [{ name: 'users', type: { name: 'User', kind: 'OBJECT' }, args: [] }],
338
+ },
339
+ ],
340
+ };
341
+
342
+ const result = validateAgainstSchema(
343
+ { operationType: 'mutation', variables: [], fields: [{ name: 'createUser' }], fragments: [] },
344
+ noMutSchema,
345
+ );
346
+
347
+ expect(result.valid).toBe(false);
348
+ });
349
+ });
@@ -0,0 +1,99 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { formatQuery, minifyQuery } from '@/lib/graphql/formatter';
3
+
4
+ describe('formatQuery', () => {
5
+ it('should format a simple query', () => {
6
+ const input = 'query{users{id name}}';
7
+ const result = formatQuery(input);
8
+
9
+ expect(result).toContain('query');
10
+ expect(result).toContain('{');
11
+ expect(result).toContain('users');
12
+ expect(result).toContain('id');
13
+ expect(result).toContain('name');
14
+ expect(result).toContain('}');
15
+ });
16
+
17
+ it('should format a query with variables', () => {
18
+ const input = 'query getUser($id:ID!){user(id:$id){id name email}}';
19
+ const result = formatQuery(input);
20
+
21
+ expect(result).toContain('getUser');
22
+ expect(result).toContain('$id');
23
+ expect(result).toContain('ID!');
24
+ });
25
+
26
+ it('should format a mutation', () => {
27
+ const input = 'mutation{createUser(input:{name:"John"}){id name}}';
28
+ const result = formatQuery(input);
29
+
30
+ expect(result).toContain('mutation');
31
+ expect(result).toContain('createUser');
32
+ });
33
+
34
+ it('should handle fragment definitions', () => {
35
+ const input = 'fragment UserFields on User{id name email}query{users{...UserFields}}';
36
+ const result = formatQuery(input);
37
+
38
+ expect(result).toContain('fragment UserFields on User');
39
+ expect(result).toContain('...UserFields');
40
+ });
41
+
42
+ it('should return empty string for empty input', () => {
43
+ expect(formatQuery('')).toBe('');
44
+ expect(formatQuery(' ')).toBe('');
45
+ });
46
+
47
+ it('should strip comments', () => {
48
+ const input = `# This is a comment
49
+ query { # inline comment
50
+ users { id } # another comment
51
+ }`;
52
+ const result = formatQuery(input);
53
+ expect(result).not.toContain('#');
54
+ expect(result).toContain('users');
55
+ });
56
+
57
+ it('should handle string values', () => {
58
+ const input = 'query{search(q:"hello world"){id}}';
59
+ const result = formatQuery(input);
60
+ expect(result).toContain('"hello world"');
61
+ });
62
+ });
63
+
64
+ describe('minifyQuery', () => {
65
+ it('should minify a formatted query', () => {
66
+ const input = `
67
+ query getUser($id: ID!) {
68
+ user(id: $id) {
69
+ id
70
+ name
71
+ email
72
+ }
73
+ }
74
+ `;
75
+ const result = minifyQuery(input);
76
+
77
+ // Minified should be shorter
78
+ expect(result.length).toBeLessThan(input.length);
79
+ // Should still contain all essential parts
80
+ expect(result).toContain('getUser');
81
+ expect(result).toContain('$id');
82
+ expect(result).toContain('user');
83
+ });
84
+
85
+ it('should remove comments when minifying', () => {
86
+ const input = `# Comment
87
+ query { users { id } }`;
88
+ const result = minifyQuery(input);
89
+ expect(result).not.toContain('#');
90
+ });
91
+
92
+ it('should handle already-minified queries', () => {
93
+ const input = '{users{id name}}';
94
+ const result = minifyQuery(input);
95
+ expect(result).toContain('users');
96
+ expect(result).toContain('id');
97
+ expect(result).toContain('name');
98
+ });
99
+ });
@@ -0,0 +1,160 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+
3
+ // Mock auth modules before importing the module under test
4
+ vi.mock('@/lib/auth/basic', () => ({
5
+ applyBasicAuth: vi.fn((headers: Record<string, string>, config: { username: string; password: string }) => {
6
+ headers['Authorization'] = `Basic ${btoa(`${config.username}:${config.password}`)}`;
7
+ }),
8
+ }));
9
+
10
+ vi.mock('@/lib/auth/bearer', () => ({
11
+ applyBearerAuth: vi.fn((headers: Record<string, string>, config: { token: string; prefix: string }) => {
12
+ headers['Authorization'] = `${config.prefix} ${config.token}`;
13
+ }),
14
+ }));
15
+
16
+ vi.mock('@/lib/auth/api-key', () => ({
17
+ applyApiKeyAuth: vi.fn((headers: Record<string, string>, params: Record<string, string>, config: { key: string; value: string; addTo: string }) => {
18
+ if (config.addTo === 'header') headers[config.key] = config.value;
19
+ else if (config.addTo === 'query') params[config.key] = config.value;
20
+ }),
21
+ }));
22
+
23
+ vi.mock('@/lib/auth/aws-sig', () => ({
24
+ applyAwsAuth: vi.fn((headers: Record<string, string>) => {
25
+ headers['Authorization'] = 'AWS4-HMAC-SHA256 mock-signature';
26
+ }),
27
+ }));
28
+
29
+ // Mock resolver to pass through variables
30
+ vi.mock('@/lib/env/resolver', () => ({
31
+ resolveVariables: vi.fn((input: string, vars: Record<string, string> = {}) => {
32
+ return input.replace(/\{\{(\w+)\}\}/g, (_: string, key: string) => vars[key] ?? `{{${key}}}`);
33
+ }),
34
+ }));
35
+
36
+ import { buildRequest } from '@/lib/http/request-builder';
37
+
38
+ describe('buildRequest', () => {
39
+ const baseUrl = 'https://api.example.com';
40
+
41
+ it('should build a basic GET request', () => {
42
+ const result = buildRequest('GET', baseUrl, [], [], null, 'none', null);
43
+
44
+ expect(result.method).toBe('GET');
45
+ expect(result.url).toContain('api.example.com');
46
+ expect(result.body).toBeNull();
47
+ expect(result.headers).toEqual({});
48
+ });
49
+
50
+ it('should resolve variables in URL', () => {
51
+ const result = buildRequest(
52
+ 'GET', '{{base_url}}/users', [], [], null, 'none', null,
53
+ { base_url: 'https://api.example.com' },
54
+ );
55
+ expect(result.url).toContain('https://api.example.com/users');
56
+ });
57
+
58
+ it('should include enabled headers', () => {
59
+ const headers = [
60
+ { key: 'Content-Type', value: 'application/json', enabled: true },
61
+ { key: 'X-Disabled', value: 'skip', enabled: false },
62
+ ];
63
+ const result = buildRequest('GET', baseUrl, headers, [], null, 'none', null);
64
+ expect(result.headers['Content-Type']).toBe('application/json');
65
+ expect(result.headers['X-Disabled']).toBeUndefined();
66
+ });
67
+
68
+ it('should resolve variables in headers', () => {
69
+ const headers = [
70
+ { key: 'Authorization', value: 'Bearer {{token}}', enabled: true },
71
+ ];
72
+ const result = buildRequest('GET', baseUrl, headers, [], null, 'none', null, { token: 'abc123' });
73
+ expect(result.headers['Authorization']).toBe('Bearer abc123');
74
+ });
75
+
76
+ it('should include enabled query params', () => {
77
+ const params = [
78
+ { key: 'page', value: '1', enabled: true },
79
+ { key: 'disabled', value: 'skip', enabled: false },
80
+ ];
81
+ const result = buildRequest('GET', baseUrl, [], params, null, 'none', null);
82
+ expect(result.url).toContain('page=1');
83
+ expect(result.url).not.toContain('disabled');
84
+ });
85
+
86
+ it('should build JSON body', () => {
87
+ const body = { raw: '{"key":"value"}' };
88
+ const result = buildRequest('POST', baseUrl, [], [], body, 'json', null);
89
+ expect(result.body).toBe('{"key":"value"}');
90
+ expect(result.headers['Content-Type']).toBe('application/json');
91
+ });
92
+
93
+ it('should build urlencoded body', () => {
94
+ const body = {
95
+ form: [
96
+ { key: 'username', value: 'test', enabled: true },
97
+ { key: 'password', value: 'pass', enabled: true },
98
+ ],
99
+ };
100
+ const result = buildRequest('POST', baseUrl, [], [], body, 'urlencoded', null);
101
+ expect(result.body).toBe('username=test&password=pass');
102
+ expect(result.headers['Content-Type']).toBe('application/x-www-form-urlencoded');
103
+ });
104
+
105
+ it('should return null body for none body type', () => {
106
+ const result = buildRequest('GET', baseUrl, [], [], null, 'none', null);
107
+ expect(result.body).toBeNull();
108
+ });
109
+
110
+ it('should return null body when body is undefined', () => {
111
+ const result = buildRequest('GET', baseUrl, [], [], undefined, undefined, null);
112
+ expect(result.body).toBeNull();
113
+ });
114
+
115
+ it('should build raw body with text/plain content type', () => {
116
+ const body = { raw: 'plain text content' };
117
+ const result = buildRequest('POST', baseUrl, [], [], body, 'raw', null);
118
+ expect(result.body).toBe('plain text content');
119
+ expect(result.headers['Content-Type']).toBe('text/plain');
120
+ });
121
+
122
+ it('should build graphql body', () => {
123
+ const body = {
124
+ graphql: {
125
+ query: '{ users { id name } }',
126
+ variables: '{"limit": 10}',
127
+ },
128
+ };
129
+ const result = buildRequest('POST', baseUrl, [], [], body, 'graphql', null);
130
+ expect(result.headers['Content-Type']).toBe('application/json');
131
+ const parsed = JSON.parse(result.body as string);
132
+ expect(parsed.query).toBe('{ users { id name } }');
133
+ expect(parsed.variables).toEqual({ limit: 10 });
134
+ });
135
+
136
+ it('should build binary body', () => {
137
+ const body = { binary: 'base64data' };
138
+ const result = buildRequest('POST', baseUrl, [], [], body, 'binary', null);
139
+ expect(result.body).toBe('base64data');
140
+ expect(result.headers['Content-Type']).toBe('application/octet-stream');
141
+ });
142
+
143
+ it('should not set Content-Type if user already provided one', () => {
144
+ const headers = [{ key: 'Content-Type', value: 'application/vnd.api+json', enabled: true }];
145
+ const body = { raw: '{"data":"test"}' };
146
+ const result = buildRequest('POST', baseUrl, headers, [], body, 'json', null);
147
+ expect(result.headers['Content-Type']).toBe('application/vnd.api+json');
148
+ });
149
+
150
+ it('should skip disabled form pairs in urlencoded body', () => {
151
+ const body = {
152
+ form: [
153
+ { key: 'keep', value: 'yes', enabled: true },
154
+ { key: 'skip', value: 'no', enabled: false },
155
+ ],
156
+ };
157
+ const result = buildRequest('POST', baseUrl, [], [], body, 'urlencoded', null);
158
+ expect(result.body).toBe('keep=yes');
159
+ });
160
+ });