@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.
- package/.env.example +15 -0
- package/LICENSE +21 -0
- package/README.md +73 -0
- package/docker-compose.yml +23 -0
- package/jest.config.js +7 -0
- package/next-env.d.ts +5 -0
- package/next.config.mjs +22 -0
- package/package.json +82 -0
- package/postcss.config.js +6 -0
- package/prisma/schema.prisma +105 -0
- package/prisma/seed.ts +211 -0
- package/src/app/(app)/ai/page.tsx +122 -0
- package/src/app/(app)/collections/page.tsx +155 -0
- package/src/app/(app)/environments/page.tsx +96 -0
- package/src/app/(app)/history/page.tsx +107 -0
- package/src/app/(app)/import/page.tsx +102 -0
- package/src/app/(app)/layout.tsx +60 -0
- package/src/app/(app)/settings/page.tsx +79 -0
- package/src/app/(app)/workspace/page.tsx +284 -0
- package/src/app/api/ai/discover/route.ts +17 -0
- package/src/app/api/ai/explain/route.ts +29 -0
- package/src/app/api/ai/generate-tests/route.ts +37 -0
- package/src/app/api/ai/suggest/route.ts +29 -0
- package/src/app/api/collections/[id]/route.ts +66 -0
- package/src/app/api/collections/route.ts +48 -0
- package/src/app/api/environments/route.ts +40 -0
- package/src/app/api/export/openapi/route.ts +17 -0
- package/src/app/api/export/postman/route.ts +18 -0
- package/src/app/api/import/curl/route.ts +18 -0
- package/src/app/api/import/har/route.ts +20 -0
- package/src/app/api/import/openapi/route.ts +21 -0
- package/src/app/api/import/postman/route.ts +21 -0
- package/src/app/api/proxy/route.ts +35 -0
- package/src/app/api/requests/[id]/execute/route.ts +85 -0
- package/src/app/api/requests/[id]/history/route.ts +23 -0
- package/src/app/api/requests/[id]/route.ts +66 -0
- package/src/app/api/requests/route.ts +49 -0
- package/src/app/api/workspaces/route.ts +38 -0
- package/src/app/globals.css +99 -0
- package/src/app/layout.tsx +24 -0
- package/src/app/page.tsx +182 -0
- package/src/components/ai/ai-panel.tsx +65 -0
- package/src/components/ai/code-explainer.tsx +51 -0
- package/src/components/ai/endpoint-discovery.tsx +62 -0
- package/src/components/ai/test-generator.tsx +49 -0
- package/src/components/collections/collection-actions.tsx +36 -0
- package/src/components/collections/collection-tree.tsx +55 -0
- package/src/components/collections/folder-creator.tsx +54 -0
- package/src/components/landing/comparison.tsx +43 -0
- package/src/components/landing/cta.tsx +16 -0
- package/src/components/landing/features.tsx +24 -0
- package/src/components/landing/hero.tsx +23 -0
- package/src/components/response/body-viewer.tsx +33 -0
- package/src/components/response/headers-viewer.tsx +23 -0
- package/src/components/response/status-badge.tsx +25 -0
- package/src/components/response/test-results.tsx +50 -0
- package/src/components/response/timing-chart.tsx +39 -0
- package/src/components/ui/badge.tsx +24 -0
- package/src/components/ui/button.tsx +32 -0
- package/src/components/ui/code-editor.tsx +51 -0
- package/src/components/ui/dialog.tsx +56 -0
- package/src/components/ui/dropdown.tsx +63 -0
- package/src/components/ui/input.tsx +22 -0
- package/src/components/ui/key-value-editor.tsx +75 -0
- package/src/components/ui/select.tsx +24 -0
- package/src/components/ui/tabs.tsx +85 -0
- package/src/components/ui/textarea.tsx +22 -0
- package/src/components/ui/toast.tsx +54 -0
- package/src/components/workspace/request-panel.tsx +38 -0
- package/src/components/workspace/response-panel.tsx +81 -0
- package/src/components/workspace/sidebar.tsx +52 -0
- package/src/components/workspace/split-pane.tsx +49 -0
- package/src/components/workspace/tabs/auth-tab.tsx +94 -0
- package/src/components/workspace/tabs/body-tab.tsx +41 -0
- package/src/components/workspace/tabs/headers-tab.tsx +23 -0
- package/src/components/workspace/tabs/params-tab.tsx +23 -0
- package/src/components/workspace/tabs/pre-request-tab.tsx +26 -0
- package/src/components/workspace/url-bar.tsx +53 -0
- package/src/hooks/use-ai.ts +115 -0
- package/src/hooks/use-collection.ts +71 -0
- package/src/hooks/use-environment.ts +73 -0
- package/src/hooks/use-request.ts +111 -0
- package/src/lib/ai/endpoint-discovery.ts +158 -0
- package/src/lib/ai/explainer.ts +127 -0
- package/src/lib/ai/suggester.ts +164 -0
- package/src/lib/ai/test-generator.ts +161 -0
- package/src/lib/auth/api-key.ts +28 -0
- package/src/lib/auth/aws-sig.ts +131 -0
- package/src/lib/auth/basic.ts +17 -0
- package/src/lib/auth/bearer.ts +15 -0
- package/src/lib/auth/oauth2.ts +155 -0
- package/src/lib/auth/types.ts +16 -0
- package/src/lib/db/client.ts +15 -0
- package/src/lib/env/manager.ts +32 -0
- package/src/lib/env/resolver.ts +30 -0
- package/src/lib/exporters/openapi.ts +193 -0
- package/src/lib/exporters/postman.ts +140 -0
- package/src/lib/graphql/builder.ts +249 -0
- package/src/lib/graphql/formatter.ts +147 -0
- package/src/lib/graphql/index.ts +43 -0
- package/src/lib/graphql/introspection.ts +175 -0
- package/src/lib/graphql/types.ts +99 -0
- package/src/lib/graphql/validator.ts +216 -0
- package/src/lib/http/client.ts +112 -0
- package/src/lib/http/proxy.ts +83 -0
- package/src/lib/http/request-builder.ts +214 -0
- package/src/lib/http/response-parser.ts +106 -0
- package/src/lib/http/timing.ts +63 -0
- package/src/lib/importers/curl-parser.ts +346 -0
- package/src/lib/importers/har-parser.ts +128 -0
- package/src/lib/importers/openapi.ts +324 -0
- package/src/lib/importers/postman.ts +312 -0
- package/src/lib/test-runner/assertions.ts +163 -0
- package/src/lib/test-runner/reporter.ts +90 -0
- package/src/lib/test-runner/runner.ts +69 -0
- package/src/lib/utils/api-response.ts +85 -0
- package/src/lib/utils/cn.ts +6 -0
- package/src/lib/utils/content-type.ts +123 -0
- package/src/lib/utils/download.ts +53 -0
- package/src/lib/utils/errors.ts +92 -0
- package/src/lib/utils/format.ts +142 -0
- package/src/lib/utils/syntax-highlight.ts +108 -0
- package/src/lib/utils/validation.ts +231 -0
- package/src/lib/websocket/client.ts +182 -0
- package/src/lib/websocket/frames.ts +96 -0
- package/src/lib/websocket/history.ts +121 -0
- package/src/lib/websocket/index.ts +25 -0
- package/src/lib/websocket/types.ts +57 -0
- package/src/types/ai.ts +28 -0
- package/src/types/collection.ts +24 -0
- package/src/types/environment.ts +16 -0
- package/src/types/request.ts +54 -0
- package/src/types/response.ts +37 -0
- package/tailwind.config.ts +82 -0
- package/tests/lib/env/resolver.test.ts +108 -0
- package/tests/lib/graphql/builder.test.ts +349 -0
- package/tests/lib/graphql/formatter.test.ts +99 -0
- package/tests/lib/http/request-builder.test.ts +160 -0
- package/tests/lib/http/response-parser.test.ts +150 -0
- package/tests/lib/http/timing.test.ts +188 -0
- package/tests/lib/importers/curl-parser.test.ts +245 -0
- package/tests/lib/test-runner/assertions.test.ts +342 -0
- package/tests/lib/utils/cn.test.ts +46 -0
- package/tests/lib/utils/content-type.test.ts +175 -0
- package/tests/lib/utils/format.test.ts +188 -0
- package/tests/lib/utils/validation.test.ts +237 -0
- package/tests/lib/websocket/history.test.ts +186 -0
- package/tsconfig.json +29 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/vitest.config.ts +21 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
formatJson,
|
|
4
|
+
minifyJson,
|
|
5
|
+
validateJson,
|
|
6
|
+
formatXml,
|
|
7
|
+
formatHtml,
|
|
8
|
+
formatBytes,
|
|
9
|
+
formatDuration,
|
|
10
|
+
} from '@/lib/utils/format';
|
|
11
|
+
|
|
12
|
+
describe('formatJson', () => {
|
|
13
|
+
it('should format valid JSON with default indent of 2', () => {
|
|
14
|
+
expect(formatJson('{"a":1}')).toBe('{\n "a": 1\n}');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('should format with custom indent', () => {
|
|
18
|
+
expect(formatJson('{"a":1}', 4)).toBe('{\n "a": 1\n}');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('should return original string for invalid JSON', () => {
|
|
22
|
+
expect(formatJson('not json')).toBe('not json');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('should format nested objects', () => {
|
|
26
|
+
const input = '{"a":{"b":2}}';
|
|
27
|
+
const result = formatJson(input);
|
|
28
|
+
expect(result).toContain('"a"');
|
|
29
|
+
expect(result).toContain('"b"');
|
|
30
|
+
expect(result).toContain('2');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should format arrays', () => {
|
|
34
|
+
expect(formatJson('[1,2,3]')).toBe('[\n 1,\n 2,\n 3\n]');
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe('minifyJson', () => {
|
|
39
|
+
it('should minify formatted JSON', () => {
|
|
40
|
+
expect(minifyJson('{\n "a": 1\n}')).toBe('{"a":1}');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should return original string for invalid JSON', () => {
|
|
44
|
+
expect(minifyJson('{invalid}')).toBe('{invalid}');
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe('validateJson', () => {
|
|
49
|
+
it('should return valid:true with parsed data for valid JSON', () => {
|
|
50
|
+
const result = validateJson('{"key":"value"}');
|
|
51
|
+
expect(result).toEqual({ valid: true, data: { key: 'value' } });
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should return valid:false with error message for invalid JSON', () => {
|
|
55
|
+
const result = validateJson('not json');
|
|
56
|
+
expect(result.valid).toBe(false);
|
|
57
|
+
if (!result.valid) {
|
|
58
|
+
expect(result.error).toBeTruthy();
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should parse arrays', () => {
|
|
63
|
+
const result = validateJson('[1,2,3]');
|
|
64
|
+
expect(result).toEqual({ valid: true, data: [1, 2, 3] });
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should parse primitives', () => {
|
|
68
|
+
expect(validateJson('42')).toEqual({ valid: true, data: 42 });
|
|
69
|
+
expect(validateJson('"hello"')).toEqual({ valid: true, data: 'hello' });
|
|
70
|
+
expect(validateJson('true')).toEqual({ valid: true, data: true });
|
|
71
|
+
expect(validateJson('null')).toEqual({ valid: true, data: null });
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe('formatXml', () => {
|
|
76
|
+
it('should format XML with proper indentation', () => {
|
|
77
|
+
const input = '<root><child>text</child></root>';
|
|
78
|
+
const result = formatXml(input);
|
|
79
|
+
expect(result).toContain('\n');
|
|
80
|
+
expect(result).toContain(' <child>');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('should handle self-closing tags', () => {
|
|
84
|
+
const input = '<root><br/></root>';
|
|
85
|
+
const result = formatXml(input);
|
|
86
|
+
expect(result).toContain('<br/>');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('should handle processing instructions', () => {
|
|
90
|
+
const input = '<?xml version="1.0"?><root><child/></root>';
|
|
91
|
+
const result = formatXml(input);
|
|
92
|
+
expect(result).toContain('<?xml');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should handle CDATA sections', () => {
|
|
96
|
+
const input = '<root><![CDATA[some text]]></root>';
|
|
97
|
+
const result = formatXml(input);
|
|
98
|
+
expect(result).toContain('CDATA');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('should respect custom indent size', () => {
|
|
102
|
+
const input = '<root><child/></root>';
|
|
103
|
+
const result4 = formatXml(input, 4);
|
|
104
|
+
expect(result4).toContain(' ');
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe('formatHtml', () => {
|
|
109
|
+
it('should format HTML with proper indentation', () => {
|
|
110
|
+
const input = '<html><body><p>text</p></body></html>';
|
|
111
|
+
const result = formatHtml(input);
|
|
112
|
+
expect(result).toContain('\n');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('should not increase indent for self-closing tags like <br>', () => {
|
|
116
|
+
const input = '<div><br><p>text</p></div>';
|
|
117
|
+
const result = formatHtml(input);
|
|
118
|
+
const lines = result.split('\n');
|
|
119
|
+
// br should be at the same level as the following p
|
|
120
|
+
const brLine = lines.find((l) => l.includes('<br>'));
|
|
121
|
+
const pLine = lines.find((l) => l.includes('<p>'));
|
|
122
|
+
expect(brLine).toBeDefined();
|
|
123
|
+
expect(pLine).toBeDefined();
|
|
124
|
+
// Both should be at the same indentation level (default indent = 2 spaces)
|
|
125
|
+
expect(brLine!.startsWith(' ')).toBe(true);
|
|
126
|
+
expect(pLine!.startsWith(' ')).toBe(true);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('should handle self-closing syntax />', () => {
|
|
130
|
+
const input = '<div><img src="x.png"/></div>';
|
|
131
|
+
const result = formatHtml(input);
|
|
132
|
+
expect(result).toContain('<img');
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('should respect custom indent size', () => {
|
|
136
|
+
const input = '<div><p>hello</p></div>';
|
|
137
|
+
const result4 = formatHtml(input, 4);
|
|
138
|
+
expect(result4).toContain(' <p>');
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe('formatBytes', () => {
|
|
143
|
+
it('should format 0 bytes', () => {
|
|
144
|
+
expect(formatBytes(0)).toBe('0 B');
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('should format bytes', () => {
|
|
148
|
+
expect(formatBytes(100)).toBe('100 B');
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('should format kilobytes', () => {
|
|
152
|
+
expect(formatBytes(1024)).toBe('1.0 KB');
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('should format megabytes', () => {
|
|
156
|
+
expect(formatBytes(1024 * 1024)).toBe('1.0 MB');
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('should format gigabytes', () => {
|
|
160
|
+
expect(formatBytes(1024 * 1024 * 1024)).toBe('1.0 GB');
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('should format fractional kilobytes', () => {
|
|
164
|
+
expect(formatBytes(1536)).toBe('1.5 KB');
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
describe('formatDuration', () => {
|
|
169
|
+
it('should format microseconds', () => {
|
|
170
|
+
expect(formatDuration(0.5)).toBe('500us');
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('should format milliseconds', () => {
|
|
174
|
+
expect(formatDuration(150)).toBe('150ms');
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('should format seconds', () => {
|
|
178
|
+
expect(formatDuration(1500)).toBe('1.50s');
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('should format very small durations', () => {
|
|
182
|
+
expect(formatDuration(0.001)).toBe('1us');
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('should format exactly 1ms', () => {
|
|
186
|
+
expect(formatDuration(1)).toBe('1ms');
|
|
187
|
+
});
|
|
188
|
+
});
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
HttpMethodSchema,
|
|
4
|
+
KeyValueSchema,
|
|
5
|
+
BodyTypeSchema,
|
|
6
|
+
AuthConfigSchema,
|
|
7
|
+
CreateRequestSchema,
|
|
8
|
+
UpdateRequestSchema,
|
|
9
|
+
CreateCollectionSchema,
|
|
10
|
+
CreateWorkspaceSchema,
|
|
11
|
+
ExecuteRequestSchema,
|
|
12
|
+
EnvironmentVariableSchema,
|
|
13
|
+
ImportCurlSchema,
|
|
14
|
+
} from '@/lib/utils/validation';
|
|
15
|
+
|
|
16
|
+
describe('HttpMethodSchema', () => {
|
|
17
|
+
it('should accept valid HTTP methods', () => {
|
|
18
|
+
const methods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS', 'TRACE', 'CONNECT'];
|
|
19
|
+
for (const method of methods) {
|
|
20
|
+
expect(HttpMethodSchema.parse(method)).toBe(method);
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('should reject invalid HTTP methods', () => {
|
|
25
|
+
expect(() => HttpMethodSchema.parse('INVALID')).toThrow();
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe('KeyValueSchema', () => {
|
|
30
|
+
it('should accept valid key-value pair', () => {
|
|
31
|
+
const result = KeyValueSchema.parse({ key: 'Content-Type', value: 'application/json' });
|
|
32
|
+
expect(result).toEqual({ key: 'Content-Type', value: 'application/json', enabled: true });
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should default enabled to true', () => {
|
|
36
|
+
const result = KeyValueSchema.parse({ key: 'foo', value: 'bar' });
|
|
37
|
+
expect(result.enabled).toBe(true);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should reject empty key', () => {
|
|
41
|
+
expect(() => KeyValueSchema.parse({ key: '', value: 'bar' })).toThrow();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should accept optional description', () => {
|
|
45
|
+
const result = KeyValueSchema.parse({ key: 'foo', value: 'bar', description: 'test' });
|
|
46
|
+
expect(result.description).toBe('test');
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe('BodyTypeSchema', () => {
|
|
51
|
+
it('should accept valid body types', () => {
|
|
52
|
+
const types = ['none', 'json', 'form', 'urlencoded', 'raw', 'graphql', 'binary'];
|
|
53
|
+
for (const t of types) {
|
|
54
|
+
expect(BodyTypeSchema.parse(t)).toBe(t);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should reject invalid body types', () => {
|
|
59
|
+
expect(() => BodyTypeSchema.parse('invalid')).toThrow();
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe('AuthConfigSchema', () => {
|
|
64
|
+
it('should accept none auth', () => {
|
|
65
|
+
const result = AuthConfigSchema.parse({ type: 'none' });
|
|
66
|
+
expect(result.type).toBe('none');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should accept basic auth', () => {
|
|
70
|
+
const result = AuthConfigSchema.parse({ type: 'basic', username: 'user', password: 'pass' });
|
|
71
|
+
expect(result.type).toBe('basic');
|
|
72
|
+
if (result.type === 'basic') {
|
|
73
|
+
expect(result.username).toBe('user');
|
|
74
|
+
expect(result.password).toBe('pass');
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('should accept bearer auth', () => {
|
|
79
|
+
const result = AuthConfigSchema.parse({ type: 'bearer', token: 'my-token' });
|
|
80
|
+
expect(result.type).toBe('bearer');
|
|
81
|
+
if (result.type === 'bearer') {
|
|
82
|
+
expect(result.token).toBe('my-token');
|
|
83
|
+
expect(result.prefix).toBe('Bearer');
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('should accept apikey auth', () => {
|
|
88
|
+
const result = AuthConfigSchema.parse({
|
|
89
|
+
type: 'apikey',
|
|
90
|
+
key: 'X-API-Key',
|
|
91
|
+
value: 'secret',
|
|
92
|
+
addTo: 'header',
|
|
93
|
+
});
|
|
94
|
+
expect(result.type).toBe('apikey');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('should reject invalid auth type', () => {
|
|
98
|
+
expect(() => AuthConfigSchema.parse({ type: 'invalid' })).toThrow();
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe('CreateRequestSchema', () => {
|
|
103
|
+
const validRequest = {
|
|
104
|
+
name: 'Test Request',
|
|
105
|
+
collectionId: 'col-1',
|
|
106
|
+
url: 'https://api.example.com',
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
it('should accept valid request with defaults', () => {
|
|
110
|
+
const result = CreateRequestSchema.parse(validRequest);
|
|
111
|
+
expect(result.method).toBe('GET');
|
|
112
|
+
expect(result.headers).toEqual([]);
|
|
113
|
+
expect(result.params).toEqual([]);
|
|
114
|
+
expect(result.bodyType).toBe('none');
|
|
115
|
+
expect(result.sortOrder).toBe(0);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('should reject request without name', () => {
|
|
119
|
+
expect(() => CreateRequestSchema.parse({ collectionId: 'col-1', url: 'https://example.com' })).toThrow();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('should reject request with empty name', () => {
|
|
123
|
+
expect(() => CreateRequestSchema.parse({ name: '', collectionId: 'col-1', url: 'https://example.com' })).toThrow();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('should reject request without URL', () => {
|
|
127
|
+
expect(() => CreateRequestSchema.parse({ name: 'Test', collectionId: 'col-1' })).toThrow();
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('should reject request name over 200 chars', () => {
|
|
131
|
+
expect(() => CreateRequestSchema.parse({
|
|
132
|
+
name: 'x'.repeat(201),
|
|
133
|
+
collectionId: 'col-1',
|
|
134
|
+
url: 'https://example.com',
|
|
135
|
+
})).toThrow();
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe('UpdateRequestSchema', () => {
|
|
140
|
+
it('should accept partial updates with id', () => {
|
|
141
|
+
const result = UpdateRequestSchema.parse({ id: 'req-1', name: 'Updated' });
|
|
142
|
+
expect(result.id).toBe('req-1');
|
|
143
|
+
expect(result.name).toBe('Updated');
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('should require id', () => {
|
|
147
|
+
expect(() => UpdateRequestSchema.parse({ name: 'Updated' })).toThrow();
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
describe('CreateCollectionSchema', () => {
|
|
152
|
+
it('should accept valid collection', () => {
|
|
153
|
+
const result = CreateCollectionSchema.parse({
|
|
154
|
+
name: 'My Collection',
|
|
155
|
+
workspaceId: 'ws-1',
|
|
156
|
+
});
|
|
157
|
+
expect(result.name).toBe('My Collection');
|
|
158
|
+
expect(result.variables).toEqual({});
|
|
159
|
+
expect(result.sortOrder).toBe(0);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('should reject collection without workspaceId', () => {
|
|
163
|
+
expect(() => CreateCollectionSchema.parse({ name: 'Test' })).toThrow();
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
describe('CreateWorkspaceSchema', () => {
|
|
168
|
+
it('should accept valid workspace', () => {
|
|
169
|
+
const result = CreateWorkspaceSchema.parse({ name: 'Workspace 1' });
|
|
170
|
+
expect(result.name).toBe('Workspace 1');
|
|
171
|
+
expect(result.userId).toBe('demo-user');
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
describe('ExecuteRequestSchema', () => {
|
|
176
|
+
it('should accept valid execute request', () => {
|
|
177
|
+
const result = ExecuteRequestSchema.parse({
|
|
178
|
+
method: 'GET',
|
|
179
|
+
url: 'https://api.example.com',
|
|
180
|
+
});
|
|
181
|
+
expect(result.timeout).toBe(30000);
|
|
182
|
+
expect(result.followRedirects).toBe(true);
|
|
183
|
+
expect(result.maxRedirects).toBe(5);
|
|
184
|
+
expect(result.verifySSL).toBe(true);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('should apply default timeout', () => {
|
|
188
|
+
const result = ExecuteRequestSchema.parse({
|
|
189
|
+
method: 'POST',
|
|
190
|
+
url: 'https://api.example.com',
|
|
191
|
+
});
|
|
192
|
+
expect(result.timeout).toBe(30000);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('should reject timeout over 300000', () => {
|
|
196
|
+
expect(() => ExecuteRequestSchema.parse({
|
|
197
|
+
method: 'GET',
|
|
198
|
+
url: 'https://example.com',
|
|
199
|
+
timeout: 400000,
|
|
200
|
+
})).toThrow();
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('should reject timeout of 0', () => {
|
|
204
|
+
expect(() => ExecuteRequestSchema.parse({
|
|
205
|
+
method: 'GET',
|
|
206
|
+
url: 'https://example.com',
|
|
207
|
+
timeout: 0,
|
|
208
|
+
})).toThrow();
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
describe('EnvironmentVariableSchema', () => {
|
|
213
|
+
it('should accept valid environment variable', () => {
|
|
214
|
+
const result = EnvironmentVariableSchema.parse({ key: 'API_KEY', value: 'secret' });
|
|
215
|
+
expect(result.type).toBe('string');
|
|
216
|
+
expect(result.enabled).toBe(true);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('should accept secret type', () => {
|
|
220
|
+
const result = EnvironmentVariableSchema.parse({ key: 'TOKEN', value: 'abc', type: 'secret' });
|
|
221
|
+
expect(result.type).toBe('secret');
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
describe('ImportCurlSchema', () => {
|
|
226
|
+
it('should accept valid cURL import', () => {
|
|
227
|
+
const result = ImportCurlSchema.parse({
|
|
228
|
+
command: 'curl https://example.com',
|
|
229
|
+
collectionId: 'col-1',
|
|
230
|
+
});
|
|
231
|
+
expect(result.command).toBe('curl https://example.com');
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it('should reject empty command', () => {
|
|
235
|
+
expect(() => ImportCurlSchema.parse({ command: '', collectionId: 'col-1' })).toThrow();
|
|
236
|
+
});
|
|
237
|
+
});
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { MessageHistory } from '@/lib/websocket/history';
|
|
3
|
+
import type { WSMessage } from '@/lib/websocket/types';
|
|
4
|
+
|
|
5
|
+
function createMessage(direction: 'sent' | 'received', data: string, ts?: number): WSMessage {
|
|
6
|
+
return {
|
|
7
|
+
id: `msg_${Math.random().toString(36).slice(2, 8)}`,
|
|
8
|
+
direction,
|
|
9
|
+
data,
|
|
10
|
+
type: 'text',
|
|
11
|
+
timestamp: ts ?? Date.now(),
|
|
12
|
+
size: new TextEncoder().encode(data).length,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe('MessageHistory', () => {
|
|
17
|
+
it('should add and retrieve messages', () => {
|
|
18
|
+
const history = new MessageHistory();
|
|
19
|
+
const msg = createMessage('sent', 'hello');
|
|
20
|
+
|
|
21
|
+
history.add(msg);
|
|
22
|
+
const all = history.getHistory();
|
|
23
|
+
|
|
24
|
+
expect(all).toHaveLength(1);
|
|
25
|
+
expect(all[0]?.data).toBe('hello');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should track multiple messages in order', () => {
|
|
29
|
+
const history = new MessageHistory();
|
|
30
|
+
history.add(createMessage('sent', 'first'));
|
|
31
|
+
history.add(createMessage('received', 'second'));
|
|
32
|
+
history.add(createMessage('sent', 'third'));
|
|
33
|
+
|
|
34
|
+
const all = history.getHistory();
|
|
35
|
+
expect(all).toHaveLength(3);
|
|
36
|
+
expect(all[0]?.data).toBe('first');
|
|
37
|
+
expect(all[1]?.data).toBe('second');
|
|
38
|
+
expect(all[2]?.data).toBe('third');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should filter by direction', () => {
|
|
42
|
+
const history = new MessageHistory();
|
|
43
|
+
history.add(createMessage('sent', 'out1'));
|
|
44
|
+
history.add(createMessage('received', 'in1'));
|
|
45
|
+
history.add(createMessage('sent', 'out2'));
|
|
46
|
+
history.add(createMessage('received', 'in2'));
|
|
47
|
+
|
|
48
|
+
const sent = history.getByDirection('sent');
|
|
49
|
+
const received = history.getByDirection('received');
|
|
50
|
+
|
|
51
|
+
expect(sent).toHaveLength(2);
|
|
52
|
+
expect(received).toHaveLength(2);
|
|
53
|
+
expect(sent.every((m) => m.direction === 'sent')).toBe(true);
|
|
54
|
+
expect(received.every((m) => m.direction === 'received')).toBe(true);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should search messages by content', () => {
|
|
58
|
+
const history = new MessageHistory();
|
|
59
|
+
history.add(createMessage('sent', '{"type":"greeting","msg":"hello"}'));
|
|
60
|
+
history.add(createMessage('received', '{"type":"response","msg":"world"}'));
|
|
61
|
+
history.add(createMessage('sent', '{"type":"ping"}'));
|
|
62
|
+
|
|
63
|
+
const results = history.search('greeting');
|
|
64
|
+
expect(results).toHaveLength(1);
|
|
65
|
+
expect(results[0]?.data).toContain('greeting');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should perform case-insensitive search', () => {
|
|
69
|
+
const history = new MessageHistory();
|
|
70
|
+
history.add(createMessage('sent', 'HELLO WORLD'));
|
|
71
|
+
|
|
72
|
+
const results = history.search('hello');
|
|
73
|
+
expect(results).toHaveLength(1);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should filter messages by time range', () => {
|
|
77
|
+
const history = new MessageHistory();
|
|
78
|
+
const now = Date.now();
|
|
79
|
+
history.add(createMessage('sent', 'old', now - 5000));
|
|
80
|
+
history.add(createMessage('sent', 'mid', now - 2000));
|
|
81
|
+
history.add(createMessage('sent', 'new', now));
|
|
82
|
+
|
|
83
|
+
const results = history.getByTimeRange(now - 3000, now);
|
|
84
|
+
expect(results).toHaveLength(2);
|
|
85
|
+
expect(results[0]?.data).toBe('mid');
|
|
86
|
+
expect(results[1]?.data).toBe('new');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('should get recent N messages', () => {
|
|
90
|
+
const history = new MessageHistory();
|
|
91
|
+
for (let i = 0; i < 10; i++) {
|
|
92
|
+
history.add(createMessage('sent', `msg_${i}`));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const recent = history.getRecent(3);
|
|
96
|
+
expect(recent).toHaveLength(3);
|
|
97
|
+
expect(recent[0]?.data).toBe('msg_7');
|
|
98
|
+
expect(recent[2]?.data).toBe('msg_9');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('should clear all messages', () => {
|
|
102
|
+
const history = new MessageHistory();
|
|
103
|
+
history.add(createMessage('sent', 'a'));
|
|
104
|
+
history.add(createMessage('sent', 'b'));
|
|
105
|
+
|
|
106
|
+
expect(history.count).toBe(2);
|
|
107
|
+
history.clear();
|
|
108
|
+
expect(history.count).toBe(0);
|
|
109
|
+
expect(history.getHistory()).toHaveLength(0);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('should export as JSON', () => {
|
|
113
|
+
const history = new MessageHistory();
|
|
114
|
+
history.add(createMessage('sent', 'test'));
|
|
115
|
+
|
|
116
|
+
const exported = history.export();
|
|
117
|
+
const parsed = JSON.parse(exported);
|
|
118
|
+
|
|
119
|
+
expect(Array.isArray(parsed)).toBe(true);
|
|
120
|
+
expect(parsed).toHaveLength(1);
|
|
121
|
+
expect(parsed[0]?.data).toBe('test');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('should import from JSON', () => {
|
|
125
|
+
const history = new MessageHistory();
|
|
126
|
+
const msg = createMessage('received', 'imported');
|
|
127
|
+
|
|
128
|
+
history.import(JSON.stringify([msg]));
|
|
129
|
+
expect(history.count).toBe(1);
|
|
130
|
+
expect(history.getHistory()[0]?.data).toBe('imported');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('should reject invalid JSON on import', () => {
|
|
134
|
+
const history = new MessageHistory();
|
|
135
|
+
expect(() => history.import('not-json')).toThrow('Invalid JSON');
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('should evict oldest messages when exceeding maxSize', () => {
|
|
139
|
+
const history = new MessageHistory(3);
|
|
140
|
+
history.add(createMessage('sent', 'first'));
|
|
141
|
+
history.add(createMessage('sent', 'second'));
|
|
142
|
+
history.add(createMessage('sent', 'third'));
|
|
143
|
+
history.add(createMessage('sent', 'fourth'));
|
|
144
|
+
|
|
145
|
+
expect(history.count).toBe(3);
|
|
146
|
+
const all = history.getHistory();
|
|
147
|
+
expect(all[0]?.data).toBe('second');
|
|
148
|
+
expect(all[2]?.data).toBe('fourth');
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('should calculate total bytes', () => {
|
|
152
|
+
const history = new MessageHistory();
|
|
153
|
+
history.add(createMessage('sent', 'hello')); // 5 bytes
|
|
154
|
+
history.add(createMessage('sent', 'world')); // 5 bytes
|
|
155
|
+
|
|
156
|
+
expect(history.totalBytes).toBe(10);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('should toArray return copies not references', () => {
|
|
160
|
+
const history = new MessageHistory();
|
|
161
|
+
history.add(createMessage('sent', 'original'));
|
|
162
|
+
|
|
163
|
+
const arr = history.toArray();
|
|
164
|
+
arr[0]!.data = 'modified';
|
|
165
|
+
|
|
166
|
+
// Original should be unchanged
|
|
167
|
+
expect(history.getHistory()[0]?.data).toBe('original');
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('should search by message type', () => {
|
|
171
|
+
const history = new MessageHistory();
|
|
172
|
+
|
|
173
|
+
const jsonMsg = createMessage('sent', 'json msg');
|
|
174
|
+
jsonMsg.type = 'json';
|
|
175
|
+
|
|
176
|
+
const textMsg = createMessage('received', 'text msg');
|
|
177
|
+
textMsg.type = 'text';
|
|
178
|
+
|
|
179
|
+
history.add(jsonMsg);
|
|
180
|
+
history.add(textMsg);
|
|
181
|
+
|
|
182
|
+
const jsonResults = history.searchByType('json');
|
|
183
|
+
expect(jsonResults).toHaveLength(1);
|
|
184
|
+
expect(jsonResults[0]?.type).toBe('json');
|
|
185
|
+
});
|
|
186
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"lib": ["dom", "dom.iterable", "esnext"],
|
|
5
|
+
"allowJs": true,
|
|
6
|
+
"skipLibCheck": true,
|
|
7
|
+
"strict": true,
|
|
8
|
+
"noEmit": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"module": "esnext",
|
|
11
|
+
"moduleResolution": "bundler",
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"isolatedModules": true,
|
|
14
|
+
"jsx": "preserve",
|
|
15
|
+
"incremental": true,
|
|
16
|
+
"plugins": [
|
|
17
|
+
{ "name": "next" }
|
|
18
|
+
],
|
|
19
|
+
"paths": {
|
|
20
|
+
"@/*": ["./src/*"]
|
|
21
|
+
},
|
|
22
|
+
"forceConsistentCasingInFileNames": true,
|
|
23
|
+
"noUncheckedIndexedAccess": true,
|
|
24
|
+
"noImplicitOverride": true,
|
|
25
|
+
"noPropertyAccessFromIndexSignature": true
|
|
26
|
+
},
|
|
27
|
+
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
|
28
|
+
"exclude": ["node_modules"]
|
|
29
|
+
}
|