zod-codegen 1.7.0 → 1.7.2
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/CHANGELOG.md +12 -0
- package/dist/src/services/file-reader.service.d.ts +2 -0
- package/dist/src/services/file-reader.service.d.ts.map +1 -1
- package/dist/src/services/file-reader.service.js +24 -10
- package/dist/src/services/file-writer.service.d.ts +1 -0
- package/dist/src/services/file-writer.service.d.ts.map +1 -1
- package/dist/src/services/file-writer.service.js +14 -1
- package/dist/tests/integration/cli-comprehensive.test.js +13 -0
- package/dist/tests/unit/file-reader.test.js +35 -7
- package/dist/tests/unit/file-writer.test.d.ts +2 -0
- package/dist/tests/unit/file-writer.test.d.ts.map +1 -0
- package/dist/tests/unit/file-writer.test.js +53 -0
- package/package.json +1 -1
- package/src/services/file-reader.service.ts +30 -11
- package/src/services/file-writer.service.ts +14 -1
- package/tests/integration/cli-comprehensive.test.ts +16 -0
- package/tests/unit/file-reader.test.ts +47 -7
- package/tests/unit/file-writer.test.ts +69 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
## [1.7.2](https://github.com/julienandreu/zod-codegen/compare/v1.7.1...v1.7.2) (2026-03-10)
|
|
2
|
+
|
|
3
|
+
### 🐛 Bug Fixes
|
|
4
|
+
|
|
5
|
+
- redact credentials from source URL in generated file headers ([#88](https://github.com/julienandreu/zod-codegen/issues/88)) ([692901d](https://github.com/julienandreu/zod-codegen/commit/692901d0b3ea8a53cbb92102bc9c3f7f90aa44aa))
|
|
6
|
+
|
|
7
|
+
## [1.7.1](https://github.com/julienandreu/zod-codegen/compare/v1.7.0...v1.7.1) (2026-03-10)
|
|
8
|
+
|
|
9
|
+
### 🐛 Bug Fixes
|
|
10
|
+
|
|
11
|
+
- properly handle URL inputs with authentication support ([#87](https://github.com/julienandreu/zod-codegen/issues/87)) ([49ba2c4](https://github.com/julienandreu/zod-codegen/commit/49ba2c483458397d9f12e03b8f6c634981e4454a))
|
|
12
|
+
|
|
1
13
|
## [1.7.0](https://github.com/julienandreu/zod-codegen/compare/v1.6.3...v1.7.0) (2026-03-09)
|
|
2
14
|
|
|
3
15
|
### 🚀 Features
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { OpenApiFileParser, OpenApiFileReader } from '../interfaces/file-reader';
|
|
2
2
|
import type { OpenApiSpecType } from '../types/openapi';
|
|
3
3
|
export declare class SyncFileReaderService implements OpenApiFileReader {
|
|
4
|
+
private isUrl;
|
|
4
5
|
readFile(path: string): Promise<string>;
|
|
6
|
+
private fetchUrl;
|
|
5
7
|
}
|
|
6
8
|
export declare class OpenApiFileParserService implements OpenApiFileParser<OpenApiSpecType> {
|
|
7
9
|
parse(input: unknown): OpenApiSpecType;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file-reader.service.d.ts","sourceRoot":"","sources":["../../../src/services/file-reader.service.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AACtF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAGxD,qBAAa,qBAAsB,YAAW,iBAAiB;
|
|
1
|
+
{"version":3,"file":"file-reader.service.d.ts","sourceRoot":"","sources":["../../../src/services/file-reader.service.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AACtF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAGxD,qBAAa,qBAAsB,YAAW,iBAAiB;IAC7D,OAAO,CAAC,KAAK;IASP,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;YAQ/B,QAAQ;CAkBvB;AAED,qBAAa,wBAAyB,YAAW,iBAAiB,CAAC,eAAe,CAAC;IACjF,KAAK,CAAC,KAAK,EAAE,OAAO,GAAG,eAAe;CAgBvC"}
|
|
@@ -2,21 +2,35 @@ import { load } from 'js-yaml';
|
|
|
2
2
|
import { readFileSync } from 'node:fs';
|
|
3
3
|
import { OpenApiSpec } from '../types/openapi.js';
|
|
4
4
|
export class SyncFileReaderService {
|
|
5
|
-
|
|
6
|
-
// Check if path is a URL
|
|
5
|
+
isUrl(path) {
|
|
7
6
|
try {
|
|
8
7
|
const url = new URL(path);
|
|
9
|
-
|
|
10
|
-
const response = await fetch(url.toString());
|
|
11
|
-
if (!response.ok) {
|
|
12
|
-
throw new Error(`Failed to fetch ${path}: ${String(response.status)} ${response.statusText}`);
|
|
13
|
-
}
|
|
14
|
-
return await response.text();
|
|
8
|
+
return url.protocol === 'http:' || url.protocol === 'https:';
|
|
15
9
|
}
|
|
16
10
|
catch {
|
|
17
|
-
|
|
18
|
-
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
async readFile(path) {
|
|
15
|
+
if (this.isUrl(path)) {
|
|
16
|
+
return await this.fetchUrl(path);
|
|
17
|
+
}
|
|
18
|
+
return readFileSync(path, 'utf8');
|
|
19
|
+
}
|
|
20
|
+
async fetchUrl(path) {
|
|
21
|
+
const url = new URL(path);
|
|
22
|
+
const headers = {};
|
|
23
|
+
if (url.username || url.password) {
|
|
24
|
+
const credentials = `${decodeURIComponent(url.username)}:${decodeURIComponent(url.password)}`;
|
|
25
|
+
headers['Authorization'] = `Basic ${btoa(credentials)}`;
|
|
26
|
+
url.username = '';
|
|
27
|
+
url.password = '';
|
|
28
|
+
}
|
|
29
|
+
const response = await fetch(url.toString(), { headers });
|
|
30
|
+
if (!response.ok) {
|
|
31
|
+
throw new Error(`Failed to fetch ${path}: ${String(response.status)} ${response.statusText}`);
|
|
19
32
|
}
|
|
33
|
+
return await response.text();
|
|
20
34
|
}
|
|
21
35
|
}
|
|
22
36
|
export class OpenApiFileParserService {
|
|
@@ -4,6 +4,7 @@ export declare class SyncFileWriterService implements FileWriter {
|
|
|
4
4
|
private readonly version;
|
|
5
5
|
private readonly inputPath;
|
|
6
6
|
constructor(name: string, version: string, inputPath: string);
|
|
7
|
+
private redactUrl;
|
|
7
8
|
writeFile(filePath: string, content: string): void;
|
|
8
9
|
resolveOutputPath(outputDir: string, fileName?: string): string;
|
|
9
10
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file-writer.service.d.ts","sourceRoot":"","sources":["../../../src/services/file-writer.service.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,8BAA8B,CAAC;AAE/D,qBAAa,qBAAsB,YAAW,UAAU;IAEpD,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,SAAS;gBAFT,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM;IAGpC,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI;IAqBlD,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,SAAW,GAAG,MAAM;CAGlE"}
|
|
1
|
+
{"version":3,"file":"file-writer.service.d.ts","sourceRoot":"","sources":["../../../src/services/file-writer.service.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,8BAA8B,CAAC;AAE/D,qBAAa,qBAAsB,YAAW,UAAU;IAEpD,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,SAAS;gBAFT,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM;IAGpC,OAAO,CAAC,SAAS;IAajB,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI;IAqBlD,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,SAAW,GAAG,MAAM;CAGlE"}
|
|
@@ -9,12 +9,25 @@ export class SyncFileWriterService {
|
|
|
9
9
|
this.version = version;
|
|
10
10
|
this.inputPath = inputPath;
|
|
11
11
|
}
|
|
12
|
+
redactUrl(path) {
|
|
13
|
+
try {
|
|
14
|
+
const url = new URL(path);
|
|
15
|
+
if ((url.protocol === 'http:' || url.protocol === 'https:') && url.password) {
|
|
16
|
+
url.password = '***';
|
|
17
|
+
return url.toString();
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
// Not a URL, return as-is
|
|
22
|
+
}
|
|
23
|
+
return path;
|
|
24
|
+
}
|
|
12
25
|
writeFile(filePath, content) {
|
|
13
26
|
const generatedContent = [
|
|
14
27
|
'// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.',
|
|
15
28
|
`// Built with ${this.name}@${this.version}`,
|
|
16
29
|
`// Latest edit: ${new Date().toUTCString()}`,
|
|
17
|
-
`// Source file: ${this.inputPath}`,
|
|
30
|
+
`// Source file: ${this.redactUrl(this.inputPath)}`,
|
|
18
31
|
'/* eslint-disable */',
|
|
19
32
|
'// @ts-nocheck',
|
|
20
33
|
'',
|
|
@@ -106,5 +106,18 @@ describe('CLI Comprehensive Integration', () => {
|
|
|
106
106
|
const outputFile = resolve(testOutputDir, 'api.ts');
|
|
107
107
|
expect(existsSync(outputFile)).toBe(true);
|
|
108
108
|
});
|
|
109
|
+
it('should generate code from a remote URL', () => {
|
|
110
|
+
execSync(`node ./dist/src/cli.js --input https://petstore3.swagger.io/api/v3/openapi.json --output ${testOutputDir}`, {
|
|
111
|
+
encoding: 'utf-8',
|
|
112
|
+
cwd,
|
|
113
|
+
timeout: 30000
|
|
114
|
+
});
|
|
115
|
+
const outputFile = resolve(testOutputDir, 'api.ts');
|
|
116
|
+
expect(existsSync(outputFile)).toBe(true);
|
|
117
|
+
const content = readFileSync(outputFile, 'utf-8');
|
|
118
|
+
expect(content).toContain('export default class');
|
|
119
|
+
expect(content).toContain('class ResponseValidationError<T> extends Error');
|
|
120
|
+
expect(content).toContain('import { z }');
|
|
121
|
+
});
|
|
109
122
|
});
|
|
110
123
|
});
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
+
import { dirname, join } from 'node:path';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
1
3
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
4
|
import { OpenApiFileParserService, SyncFileReaderService } from '../../src/services/file-reader.service';
|
|
3
|
-
import { join } from 'node:path';
|
|
4
|
-
import { fileURLToPath } from 'node:url';
|
|
5
|
-
import { dirname } from 'node:path';
|
|
6
5
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
6
|
describe('SyncFileReaderService', () => {
|
|
8
7
|
let reader;
|
|
@@ -25,7 +24,6 @@ describe('SyncFileReaderService', () => {
|
|
|
25
24
|
await expect(reader.readFile('./non-existent-file.json')).rejects.toThrow();
|
|
26
25
|
});
|
|
27
26
|
it('should handle URLs', async () => {
|
|
28
|
-
// Mock fetch for URL test
|
|
29
27
|
const originalFetch = global.fetch;
|
|
30
28
|
global.fetch = vi.fn().mockResolvedValue({
|
|
31
29
|
ok: true,
|
|
@@ -33,6 +31,7 @@ describe('SyncFileReaderService', () => {
|
|
|
33
31
|
});
|
|
34
32
|
const content = await reader.readFile('https://example.com/openapi.json');
|
|
35
33
|
expect(content).toBeTruthy();
|
|
34
|
+
expect(global.fetch).toHaveBeenCalledWith('https://example.com/openapi.json', { headers: {} });
|
|
36
35
|
global.fetch = originalFetch;
|
|
37
36
|
});
|
|
38
37
|
it('should handle URL fetch errors with non-ok responses', async () => {
|
|
@@ -43,11 +42,40 @@ describe('SyncFileReaderService', () => {
|
|
|
43
42
|
statusText: 'Not Found'
|
|
44
43
|
});
|
|
45
44
|
global.fetch = mockFetch;
|
|
46
|
-
await expect(reader.readFile('https://example.com/not-found.json')).rejects.toThrow();
|
|
47
|
-
|
|
48
|
-
|
|
45
|
+
await expect(reader.readFile('https://example.com/not-found.json')).rejects.toThrow('Failed to fetch');
|
|
46
|
+
global.fetch = originalFetch;
|
|
47
|
+
});
|
|
48
|
+
it('should propagate network errors for URLs instead of falling back to readFileSync', async () => {
|
|
49
|
+
const originalFetch = global.fetch;
|
|
50
|
+
global.fetch = vi.fn().mockRejectedValue(new TypeError('fetch failed'));
|
|
51
|
+
await expect(reader.readFile('https://example.com/openapi.json')).rejects.toThrow('fetch failed');
|
|
52
|
+
global.fetch = originalFetch;
|
|
53
|
+
});
|
|
54
|
+
it('should extract basic auth credentials from URL into Authorization header', async () => {
|
|
55
|
+
const originalFetch = global.fetch;
|
|
56
|
+
const mockFetch = vi.fn().mockResolvedValue({
|
|
57
|
+
ok: true,
|
|
58
|
+
text: async () => '{}'
|
|
59
|
+
});
|
|
60
|
+
global.fetch = mockFetch;
|
|
61
|
+
await reader.readFile('https://user:pass@example.com/openapi.json');
|
|
62
|
+
expect(mockFetch).toHaveBeenCalledWith('https://example.com/openapi.json', { headers: { Authorization: `Basic ${btoa('user:pass')}` } });
|
|
63
|
+
global.fetch = originalFetch;
|
|
64
|
+
});
|
|
65
|
+
it('should decode percent-encoded credentials from URL', async () => {
|
|
66
|
+
const originalFetch = global.fetch;
|
|
67
|
+
const mockFetch = vi.fn().mockResolvedValue({
|
|
68
|
+
ok: true,
|
|
69
|
+
text: async () => '{}'
|
|
70
|
+
});
|
|
71
|
+
global.fetch = mockFetch;
|
|
72
|
+
await reader.readFile('https://julien%2Bstaging%40saris.ai:qwerty123@api.staging.saris.ai/api/openapi.json');
|
|
73
|
+
expect(mockFetch).toHaveBeenCalledWith('https://api.staging.saris.ai/api/openapi.json', { headers: { Authorization: `Basic ${btoa('julien+staging@saris.ai:qwerty123')}` } });
|
|
49
74
|
global.fetch = originalFetch;
|
|
50
75
|
});
|
|
76
|
+
it('should not treat non-http schemes as URLs', async () => {
|
|
77
|
+
await expect(reader.readFile('file:///etc/passwd')).rejects.toThrow();
|
|
78
|
+
});
|
|
51
79
|
});
|
|
52
80
|
});
|
|
53
81
|
describe('OpenApiFileParserService', () => {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"file-writer.test.d.ts","sourceRoot":"","sources":["../../../tests/unit/file-writer.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, rmSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
5
|
+
import { SyncFileWriterService } from '../../src/services/file-writer.service';
|
|
6
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const testOutputDir = join(__dirname, '../../test-output-writer');
|
|
8
|
+
describe('SyncFileWriterService', () => {
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
if (existsSync(testOutputDir)) {
|
|
11
|
+
rmSync(testOutputDir, { recursive: true, force: true });
|
|
12
|
+
}
|
|
13
|
+
mkdirSync(testOutputDir, { recursive: true });
|
|
14
|
+
});
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
if (existsSync(testOutputDir)) {
|
|
17
|
+
rmSync(testOutputDir, { recursive: true, force: true });
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
describe('credential redaction', () => {
|
|
21
|
+
it('should redact password from URL with embedded credentials', () => {
|
|
22
|
+
const writer = new SyncFileWriterService('zod-codegen', '1.0.0', 'https://user:secretpass@api.example.com/openapi.json');
|
|
23
|
+
const outPath = join(testOutputDir, 'api.ts');
|
|
24
|
+
writer.writeFile(outPath, 'const x = 1;');
|
|
25
|
+
const content = readFileSync(outPath, 'utf-8');
|
|
26
|
+
expect(content).toContain('// Source file: https://user:***@api.example.com/openapi.json');
|
|
27
|
+
expect(content).not.toContain('secretpass');
|
|
28
|
+
});
|
|
29
|
+
it('should redact password from URL with percent-encoded credentials', () => {
|
|
30
|
+
const writer = new SyncFileWriterService('zod-codegen', '1.0.0', 'https://api%2Bstaging%40saris.ai:hnt.xjx3rby8YKM8wnm@api.staging.saris.ai/api/openapi.json');
|
|
31
|
+
const outPath = join(testOutputDir, 'api.ts');
|
|
32
|
+
writer.writeFile(outPath, 'const x = 1;');
|
|
33
|
+
const content = readFileSync(outPath, 'utf-8');
|
|
34
|
+
expect(content).toContain('api%2Bstaging%40saris.ai');
|
|
35
|
+
expect(content).toContain(':***@');
|
|
36
|
+
expect(content).not.toContain('hnt.xjx3rby8YKM8wnm');
|
|
37
|
+
});
|
|
38
|
+
it('should leave URL without credentials unchanged', () => {
|
|
39
|
+
const writer = new SyncFileWriterService('zod-codegen', '1.0.0', 'https://api.example.com/openapi.json');
|
|
40
|
+
const outPath = join(testOutputDir, 'api.ts');
|
|
41
|
+
writer.writeFile(outPath, 'const x = 1;');
|
|
42
|
+
const content = readFileSync(outPath, 'utf-8');
|
|
43
|
+
expect(content).toContain('// Source file: https://api.example.com/openapi.json');
|
|
44
|
+
});
|
|
45
|
+
it('should leave local file paths unchanged', () => {
|
|
46
|
+
const writer = new SyncFileWriterService('zod-codegen', '1.0.0', './samples/openapi.json');
|
|
47
|
+
const outPath = join(testOutputDir, 'api.ts');
|
|
48
|
+
writer.writeFile(outPath, 'const x = 1;');
|
|
49
|
+
const content = readFileSync(outPath, 'utf-8');
|
|
50
|
+
expect(content).toContain('// Source file: ./samples/openapi.json');
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
});
|
package/package.json
CHANGED
|
@@ -5,21 +5,40 @@ import type { OpenApiSpecType } from '../types/openapi';
|
|
|
5
5
|
import { OpenApiSpec } from '../types/openapi';
|
|
6
6
|
|
|
7
7
|
export class SyncFileReaderService implements OpenApiFileReader {
|
|
8
|
-
|
|
9
|
-
// Check if path is a URL
|
|
8
|
+
private isUrl(path: string): boolean {
|
|
10
9
|
try {
|
|
11
10
|
const url = new URL(path);
|
|
12
|
-
|
|
13
|
-
const response = await fetch(url.toString());
|
|
14
|
-
if (!response.ok) {
|
|
15
|
-
throw new Error(`Failed to fetch ${path}: ${String(response.status)} ${response.statusText}`);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
return await response.text();
|
|
11
|
+
return url.protocol === 'http:' || url.protocol === 'https:';
|
|
19
12
|
} catch {
|
|
20
|
-
|
|
21
|
-
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async readFile(path: string): Promise<string> {
|
|
18
|
+
if (this.isUrl(path)) {
|
|
19
|
+
return await this.fetchUrl(path);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return readFileSync(path, 'utf8');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
private async fetchUrl(path: string): Promise<string> {
|
|
26
|
+
const url = new URL(path);
|
|
27
|
+
|
|
28
|
+
const headers: Record<string, string> = {};
|
|
29
|
+
if (url.username || url.password) {
|
|
30
|
+
const credentials = `${decodeURIComponent(url.username)}:${decodeURIComponent(url.password)}`;
|
|
31
|
+
headers['Authorization'] = `Basic ${btoa(credentials)}`;
|
|
32
|
+
url.username = '';
|
|
33
|
+
url.password = '';
|
|
22
34
|
}
|
|
35
|
+
|
|
36
|
+
const response = await fetch(url.toString(), { headers });
|
|
37
|
+
if (!response.ok) {
|
|
38
|
+
throw new Error(`Failed to fetch ${path}: ${String(response.status)} ${response.statusText}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return await response.text();
|
|
23
42
|
}
|
|
24
43
|
}
|
|
25
44
|
|
|
@@ -9,12 +9,25 @@ export class SyncFileWriterService implements FileWriter {
|
|
|
9
9
|
private readonly inputPath: string
|
|
10
10
|
) {}
|
|
11
11
|
|
|
12
|
+
private redactUrl(path: string): string {
|
|
13
|
+
try {
|
|
14
|
+
const url = new URL(path);
|
|
15
|
+
if ((url.protocol === 'http:' || url.protocol === 'https:') && url.password) {
|
|
16
|
+
url.password = '***';
|
|
17
|
+
return url.toString();
|
|
18
|
+
}
|
|
19
|
+
} catch {
|
|
20
|
+
// Not a URL, return as-is
|
|
21
|
+
}
|
|
22
|
+
return path;
|
|
23
|
+
}
|
|
24
|
+
|
|
12
25
|
writeFile(filePath: string, content: string): void {
|
|
13
26
|
const generatedContent = [
|
|
14
27
|
'// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.',
|
|
15
28
|
`// Built with ${this.name}@${this.version}`,
|
|
16
29
|
`// Latest edit: ${new Date().toUTCString()}`,
|
|
17
|
-
`// Source file: ${this.inputPath}`,
|
|
30
|
+
`// Source file: ${this.redactUrl(this.inputPath)}`,
|
|
18
31
|
'/* eslint-disable */',
|
|
19
32
|
'// @ts-nocheck',
|
|
20
33
|
'',
|
|
@@ -125,5 +125,21 @@ describe('CLI Comprehensive Integration', () => {
|
|
|
125
125
|
const outputFile = resolve(testOutputDir, 'api.ts');
|
|
126
126
|
expect(existsSync(outputFile)).toBe(true);
|
|
127
127
|
});
|
|
128
|
+
|
|
129
|
+
it('should generate code from a remote URL', () => {
|
|
130
|
+
execSync(`node ./dist/src/cli.js --input https://petstore3.swagger.io/api/v3/openapi.json --output ${testOutputDir}`, {
|
|
131
|
+
encoding: 'utf-8',
|
|
132
|
+
cwd,
|
|
133
|
+
timeout: 30000
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const outputFile = resolve(testOutputDir, 'api.ts');
|
|
137
|
+
expect(existsSync(outputFile)).toBe(true);
|
|
138
|
+
|
|
139
|
+
const content = readFileSync(outputFile, 'utf-8');
|
|
140
|
+
expect(content).toContain('export default class');
|
|
141
|
+
expect(content).toContain('class ResponseValidationError<T> extends Error');
|
|
142
|
+
expect(content).toContain('import { z }');
|
|
143
|
+
});
|
|
128
144
|
});
|
|
129
145
|
});
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
+
import { dirname, join } from 'node:path';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
1
3
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
4
|
import { OpenApiFileParserService, SyncFileReaderService } from '../../src/services/file-reader.service';
|
|
3
|
-
import { join } from 'node:path';
|
|
4
|
-
import { fileURLToPath } from 'node:url';
|
|
5
|
-
import { dirname } from 'node:path';
|
|
6
5
|
|
|
7
6
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
7
|
|
|
@@ -32,7 +31,6 @@ describe('SyncFileReaderService', () => {
|
|
|
32
31
|
});
|
|
33
32
|
|
|
34
33
|
it('should handle URLs', async () => {
|
|
35
|
-
// Mock fetch for URL test
|
|
36
34
|
const originalFetch = global.fetch;
|
|
37
35
|
global.fetch = vi.fn().mockResolvedValue({
|
|
38
36
|
ok: true,
|
|
@@ -41,6 +39,7 @@ describe('SyncFileReaderService', () => {
|
|
|
41
39
|
|
|
42
40
|
const content = await reader.readFile('https://example.com/openapi.json');
|
|
43
41
|
expect(content).toBeTruthy();
|
|
42
|
+
expect(global.fetch).toHaveBeenCalledWith('https://example.com/openapi.json', { headers: {} });
|
|
44
43
|
|
|
45
44
|
global.fetch = originalFetch;
|
|
46
45
|
});
|
|
@@ -54,12 +53,53 @@ describe('SyncFileReaderService', () => {
|
|
|
54
53
|
} as Response);
|
|
55
54
|
global.fetch = mockFetch;
|
|
56
55
|
|
|
57
|
-
await expect(reader.readFile('https://example.com/not-found.json')).rejects.toThrow();
|
|
56
|
+
await expect(reader.readFile('https://example.com/not-found.json')).rejects.toThrow('Failed to fetch');
|
|
57
|
+
|
|
58
|
+
global.fetch = originalFetch;
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should propagate network errors for URLs instead of falling back to readFileSync', async () => {
|
|
62
|
+
const originalFetch = global.fetch;
|
|
63
|
+
global.fetch = vi.fn().mockRejectedValue(new TypeError('fetch failed'));
|
|
64
|
+
|
|
65
|
+
await expect(reader.readFile('https://example.com/openapi.json')).rejects.toThrow('fetch failed');
|
|
66
|
+
|
|
67
|
+
global.fetch = originalFetch;
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should extract basic auth credentials from URL into Authorization header', async () => {
|
|
71
|
+
const originalFetch = global.fetch;
|
|
72
|
+
const mockFetch = vi.fn().mockResolvedValue({
|
|
73
|
+
ok: true,
|
|
74
|
+
text: async () => '{}'
|
|
75
|
+
} as Response);
|
|
76
|
+
global.fetch = mockFetch;
|
|
77
|
+
|
|
78
|
+
await reader.readFile('https://user:pass@example.com/openapi.json');
|
|
79
|
+
|
|
80
|
+
expect(mockFetch).toHaveBeenCalledWith('https://example.com/openapi.json', { headers: { Authorization: `Basic ${btoa('user:pass')}` } });
|
|
81
|
+
|
|
82
|
+
global.fetch = originalFetch;
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should decode percent-encoded credentials from URL', async () => {
|
|
86
|
+
const originalFetch = global.fetch;
|
|
87
|
+
const mockFetch = vi.fn().mockResolvedValue({
|
|
88
|
+
ok: true,
|
|
89
|
+
text: async () => '{}'
|
|
90
|
+
} as Response);
|
|
91
|
+
global.fetch = mockFetch;
|
|
92
|
+
|
|
93
|
+
await reader.readFile('https://julien%2Bstaging%40saris.ai:qwerty123@api.staging.saris.ai/api/openapi.json');
|
|
94
|
+
|
|
95
|
+
expect(mockFetch).toHaveBeenCalledWith('https://api.staging.saris.ai/api/openapi.json', { headers: { Authorization: `Basic ${btoa('julien+staging@saris.ai:qwerty123')}` } });
|
|
58
96
|
|
|
59
|
-
// Verify fetch was called
|
|
60
|
-
expect(mockFetch).toHaveBeenCalledWith('https://example.com/not-found.json');
|
|
61
97
|
global.fetch = originalFetch;
|
|
62
98
|
});
|
|
99
|
+
|
|
100
|
+
it('should not treat non-http schemes as URLs', async () => {
|
|
101
|
+
await expect(reader.readFile('file:///etc/passwd')).rejects.toThrow();
|
|
102
|
+
});
|
|
63
103
|
});
|
|
64
104
|
});
|
|
65
105
|
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, rmSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
5
|
+
import { SyncFileWriterService } from '../../src/services/file-writer.service';
|
|
6
|
+
|
|
7
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const testOutputDir = join(__dirname, '../../test-output-writer');
|
|
9
|
+
|
|
10
|
+
describe('SyncFileWriterService', () => {
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
if (existsSync(testOutputDir)) {
|
|
13
|
+
rmSync(testOutputDir, { recursive: true, force: true });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
mkdirSync(testOutputDir, { recursive: true });
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
if (existsSync(testOutputDir)) {
|
|
21
|
+
rmSync(testOutputDir, { recursive: true, force: true });
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe('credential redaction', () => {
|
|
26
|
+
it('should redact password from URL with embedded credentials', () => {
|
|
27
|
+
const writer = new SyncFileWriterService('zod-codegen', '1.0.0', 'https://user:secretpass@api.example.com/openapi.json');
|
|
28
|
+
|
|
29
|
+
const outPath = join(testOutputDir, 'api.ts');
|
|
30
|
+
writer.writeFile(outPath, 'const x = 1;');
|
|
31
|
+
|
|
32
|
+
const content = readFileSync(outPath, 'utf-8');
|
|
33
|
+
expect(content).toContain('// Source file: https://user:***@api.example.com/openapi.json');
|
|
34
|
+
expect(content).not.toContain('secretpass');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should redact password from URL with percent-encoded credentials', () => {
|
|
38
|
+
const writer = new SyncFileWriterService('zod-codegen', '1.0.0', 'https://api%2Bstaging%40saris.ai:hnt.xjx3rby8YKM8wnm@api.staging.saris.ai/api/openapi.json');
|
|
39
|
+
|
|
40
|
+
const outPath = join(testOutputDir, 'api.ts');
|
|
41
|
+
writer.writeFile(outPath, 'const x = 1;');
|
|
42
|
+
|
|
43
|
+
const content = readFileSync(outPath, 'utf-8');
|
|
44
|
+
expect(content).toContain('api%2Bstaging%40saris.ai');
|
|
45
|
+
expect(content).toContain(':***@');
|
|
46
|
+
expect(content).not.toContain('hnt.xjx3rby8YKM8wnm');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should leave URL without credentials unchanged', () => {
|
|
50
|
+
const writer = new SyncFileWriterService('zod-codegen', '1.0.0', 'https://api.example.com/openapi.json');
|
|
51
|
+
|
|
52
|
+
const outPath = join(testOutputDir, 'api.ts');
|
|
53
|
+
writer.writeFile(outPath, 'const x = 1;');
|
|
54
|
+
|
|
55
|
+
const content = readFileSync(outPath, 'utf-8');
|
|
56
|
+
expect(content).toContain('// Source file: https://api.example.com/openapi.json');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should leave local file paths unchanged', () => {
|
|
60
|
+
const writer = new SyncFileWriterService('zod-codegen', '1.0.0', './samples/openapi.json');
|
|
61
|
+
|
|
62
|
+
const outPath = join(testOutputDir, 'api.ts');
|
|
63
|
+
writer.writeFile(outPath, 'const x = 1;');
|
|
64
|
+
|
|
65
|
+
const content = readFileSync(outPath, 'utf-8');
|
|
66
|
+
expect(content).toContain('// Source file: ./samples/openapi.json');
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
});
|