aitesting 1.1.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/LICENSE +21 -0
- package/README.md +220 -0
- package/bin/postmanai.js +32 -0
- package/package.json +51 -0
- package/src/generate.js +833 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 devXcant
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
# ๐ AITesting - Complete API Testing & Documentation Suite
|
|
2
|
+
|
|
3
|
+
An AI-powered CLI tool that automatically generates Postman collections, API documentation, test files, and live endpoint testing from your backend codebase.
|
|
4
|
+
|
|
5
|
+
## โจ Features
|
|
6
|
+
|
|
7
|
+
- ๐ **Route Scanning** - Automatically discovers all API routes from TypeScript files
|
|
8
|
+
- ๐งช **Live Endpoint Testing** - Makes REAL HTTP requests and records actual responses
|
|
9
|
+
- ๐ **Auth Detection** - Automatically detects login endpoints and manages JWT tokens
|
|
10
|
+
- ๐ **Schema Detection** - Finds Zod, Joi, and class-validator schemas
|
|
11
|
+
- ๐ **Postman Collections** - Generates organized collections with example responses
|
|
12
|
+
- ๐ **Environment Files** - Creates local & production Postman environments
|
|
13
|
+
- ๐งช **Test Generation** - Generates Bun test files for all routes
|
|
14
|
+
- ๐ **OpenAPI/Swagger** - Generates OpenAPI 3.0 spec with interactive Swagger UI
|
|
15
|
+
- ๐ **Enhanced Documentation** - Creates comprehensive markdown API docs
|
|
16
|
+
- ๐ **Test Reports** - Saves detailed live test results in JSON format
|
|
17
|
+
|
|
18
|
+
## ๐ ๏ธ Installation
|
|
19
|
+
|
|
20
|
+
### Install Globally (Recommended)
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
cd /Users/devx/Desktop/AITESTING
|
|
24
|
+
bun link
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Now use `aitesting` command from anywhere!
|
|
28
|
+
|
|
29
|
+
### Or Run Locally
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
cd /Users/devx/Desktop/AITESTING
|
|
33
|
+
bun install
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## ๐ Usage
|
|
37
|
+
|
|
38
|
+
### Simple Mode (Just scan and generate Postman collection)
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
aitesting --path /path/to/your/backend
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Complete Mode (Everything!)
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
aitesting --path /path/to/your/backend --all
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### With Remote Backend
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
aitesting --path /path/to/backend --all --url https://api.yourapp.com
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Specify Output Directory
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
aitesting --path /backend --all -o ~/Documents/api-docs
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
The `--all` flag runs the complete workflow:
|
|
63
|
+
|
|
64
|
+
1. โ
Scans backend for routes
|
|
65
|
+
2. โ
Detects authentication endpoints
|
|
66
|
+
3. โ
Finds validation schemas
|
|
67
|
+
4. โ
Tests all endpoints live
|
|
68
|
+
5. โ
Captures JWT tokens automatically
|
|
69
|
+
6. โ
Generates Postman collection with example responses
|
|
70
|
+
7. โ
Creates environment files
|
|
71
|
+
8. โ
Generates automated test files
|
|
72
|
+
9. โ
Creates enhanced documentation
|
|
73
|
+
|
|
74
|
+
## ๐ฆ Generated Files
|
|
75
|
+
|
|
76
|
+
When you run with `--all`, you'll get:
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
๐ Project Root
|
|
80
|
+
โโโ postman_collection.json # Postman collection with examples
|
|
81
|
+
โโโ local_environment.postman_environment.json
|
|
82
|
+
โโโ production_environment.postman_environment.json
|
|
83
|
+
โโโ openapi.json # OpenAPI 3.0 specification
|
|
84
|
+
โโโ api-docs.html # Swagger UI (open in browser!)
|
|
85
|
+
โโโ API_DOCUMENTATION.md # Markdown API docs
|
|
86
|
+
โโโ .aitesting/
|
|
87
|
+
โ โโโ report.json # Live test results
|
|
88
|
+
โโโ tests/auto/
|
|
89
|
+
โโโ users.test.ts # Auto-generated tests
|
|
90
|
+
โโโ auth.test.ts
|
|
91
|
+
โโโ ...
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## ๐ฏ What It Does
|
|
95
|
+
|
|
96
|
+
### 1. Route Discovery
|
|
97
|
+
|
|
98
|
+
Scans your TypeScript files using AST parsing to find all route definitions:
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
router.get("/users/:id", handler);
|
|
102
|
+
router.post("/auth/login", handler);
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### 2. Live Testing (REAL HTTP Requests!)
|
|
106
|
+
|
|
107
|
+
**โ ๏ธ Makes actual HTTP requests to your backend:**
|
|
108
|
+
|
|
109
|
+
- Tests each endpoint with real network calls (using `fetch`)
|
|
110
|
+
- Records actual response status codes
|
|
111
|
+
- Captures real response bodies
|
|
112
|
+
- Saves headers and error messages
|
|
113
|
+
- **Requires your backend to be running!**
|
|
114
|
+
|
|
115
|
+
### 3. Auth Handling
|
|
116
|
+
|
|
117
|
+
- Automatically detects login/auth endpoints
|
|
118
|
+
- Obtains JWT tokens during testing
|
|
119
|
+
- Injects tokens into subsequent requests
|
|
120
|
+
- Adds auth headers to Postman collection
|
|
121
|
+
|
|
122
|
+
### 4. Schema Detection
|
|
123
|
+
|
|
124
|
+
Finds validation schemas in your code:
|
|
125
|
+
|
|
126
|
+
- Zod schemas (`z.object(...)`)
|
|
127
|
+
- Joi schemas (`Joi.object(...)`)
|
|
128
|
+
- class-validator decorators (`@IsString()`, etc.)
|
|
129
|
+
|
|
130
|
+
### 5. Test Generation
|
|
131
|
+
|
|
132
|
+
Creates ready-to-run Bun tests:
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
import { test, expect, describe } from "bun:test";
|
|
136
|
+
|
|
137
|
+
describe("Users Routes", () => {
|
|
138
|
+
test("GET /users/:id", async () => {
|
|
139
|
+
const response = await fetch(`${baseUrl}/users/1`);
|
|
140
|
+
expect(response).toBeDefined();
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### 6. OpenAPI/Swagger Generation
|
|
146
|
+
|
|
147
|
+
Generates industry-standard OpenAPI 3.0 specification:
|
|
148
|
+
|
|
149
|
+
- **`openapi.json`** - Complete OpenAPI 3.0 spec
|
|
150
|
+
- **`api-docs.html`** - Interactive Swagger UI
|
|
151
|
+
- Includes auth schemes, request/response examples
|
|
152
|
+
- Path parameters, request bodies, responses
|
|
153
|
+
- **Open `api-docs.html` in browser for interactive docs!**
|
|
154
|
+
|
|
155
|
+
## ๐ง Configuration
|
|
156
|
+
|
|
157
|
+
The tool works out-of-the-box, but you can customize:
|
|
158
|
+
|
|
159
|
+
- **Base URL**: Edit the `baseUrl` in generated environment files
|
|
160
|
+
- **Auth Token Detection**: Supports `token`, `access_token`, and `accessToken` fields
|
|
161
|
+
- **Route Detection**: Looks for `router.*` patterns in TypeScript files
|
|
162
|
+
|
|
163
|
+
## ๐ Requirements
|
|
164
|
+
|
|
165
|
+
- Bun runtime
|
|
166
|
+
- Backend with TypeScript route definitions
|
|
167
|
+
- Routes using `router.get()`, `router.post()`, etc. pattern
|
|
168
|
+
|
|
169
|
+
## ๐ฆ Example Output
|
|
170
|
+
|
|
171
|
+
```
|
|
172
|
+
๐ AITesting v1.0 - Full Workflow
|
|
173
|
+
|
|
174
|
+
๐ Scanning backend in: ./backend
|
|
175
|
+
|
|
176
|
+
โ
Found 23 routes
|
|
177
|
+
๐ Detected auth endpoint: POST /auth/login
|
|
178
|
+
๐ Detected schemas: zod, class-validator
|
|
179
|
+
|
|
180
|
+
๐งช Running live endpoint tests...
|
|
181
|
+
|
|
182
|
+
โ
Auth token obtained
|
|
183
|
+
โ GET /users 200
|
|
184
|
+
โ POST /users 201
|
|
185
|
+
โ GET /users/:id 200
|
|
186
|
+
โ DELETE /users/:id 401
|
|
187
|
+
|
|
188
|
+
๐ Live test report saved to .aitesting/report.json
|
|
189
|
+
|
|
190
|
+
๐ Generated: postman_collection.json
|
|
191
|
+
๐ Environments generated (local & production)
|
|
192
|
+
๐งช Test files generated in tests/auto/
|
|
193
|
+
๐ Enhanced documentation generated (API_DOCUMENTATION.md)
|
|
194
|
+
|
|
195
|
+
โ
Complete! All features executed successfully.
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## ๐ Important Notes
|
|
199
|
+
|
|
200
|
+
### Live Testing (with `--all` flag)
|
|
201
|
+
|
|
202
|
+
- **โ ๏ธ Requires your backend to be running** at `http://localhost:5000` (default)
|
|
203
|
+
- Makes **REAL HTTP requests** using `fetch()` - not mock/sample data!
|
|
204
|
+
- Path parameters (e.g., `:id`) are replaced with `1` during testing
|
|
205
|
+
- Failed tests are recorded in the report but don't stop the workflow
|
|
206
|
+
- Responses in `openapi.json` and Postman collection are **actual responses** from your API
|
|
207
|
+
|
|
208
|
+
### OpenAPI/Swagger UI
|
|
209
|
+
|
|
210
|
+
- Open `api-docs.html` in any browser for interactive documentation
|
|
211
|
+
- Works offline - Swagger UI loads from CDN
|
|
212
|
+
- Can be hosted on any web server for team sharing
|
|
213
|
+
|
|
214
|
+
## ๐ค Contributing
|
|
215
|
+
|
|
216
|
+
This is a productivity tool. Feel free to extend and customize for your needs!
|
|
217
|
+
|
|
218
|
+
## ๐ License
|
|
219
|
+
|
|
220
|
+
See LICENSE file.
|
package/bin/postmanai.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { program } from "commander";
|
|
3
|
+
import { generateCollection } from "../src/generate.js";
|
|
4
|
+
|
|
5
|
+
console.log("โ
AITesting CLI v1.1.0");
|
|
6
|
+
|
|
7
|
+
program
|
|
8
|
+
.name("aitesting")
|
|
9
|
+
.description("AI-powered API testing & documentation generator")
|
|
10
|
+
.version("1.1.0")
|
|
11
|
+
.option("-p, --path <path>", "Path to your backend folder", ".")
|
|
12
|
+
.option(
|
|
13
|
+
"--all",
|
|
14
|
+
"Run complete workflow (scan, test, schemas, auth, tests, docs)"
|
|
15
|
+
)
|
|
16
|
+
.option(
|
|
17
|
+
"--url <url>",
|
|
18
|
+
"Base URL for live testing (default: http://localhost:5050)"
|
|
19
|
+
)
|
|
20
|
+
.option(
|
|
21
|
+
"-o, --output <path>",
|
|
22
|
+
"Output directory for generated files (default: current directory)"
|
|
23
|
+
)
|
|
24
|
+
.action(async (opts) => {
|
|
25
|
+
await generateCollection(opts.path, {
|
|
26
|
+
all: opts.all || false,
|
|
27
|
+
baseUrl: opts.url,
|
|
28
|
+
outputDir: opts.output,
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
program.parse(process.argv);
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "aitesting",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "AI-powered API testing & documentation generator with OpenAPI/Swagger support",
|
|
5
|
+
"main": "bin/postmanai.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=18.0.0"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"prepublishOnly": "echo 'Ready to publish!'"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"bin",
|
|
15
|
+
"src",
|
|
16
|
+
"README.md",
|
|
17
|
+
"LICENSE"
|
|
18
|
+
],
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@babel/parser": "^7.28.4",
|
|
21
|
+
"@babel/traverse": "^7.28.4",
|
|
22
|
+
"chalk": "^5.6.2",
|
|
23
|
+
"commander": "^14.0.1",
|
|
24
|
+
"glob": "^11.0.3"
|
|
25
|
+
},
|
|
26
|
+
"bin": {
|
|
27
|
+
"aitesting": "./bin/postmanai.js"
|
|
28
|
+
},
|
|
29
|
+
"keywords": [
|
|
30
|
+
"api",
|
|
31
|
+
"testing",
|
|
32
|
+
"openapi",
|
|
33
|
+
"swagger",
|
|
34
|
+
"postman",
|
|
35
|
+
"documentation",
|
|
36
|
+
"cli",
|
|
37
|
+
"api-documentation",
|
|
38
|
+
"test-generator",
|
|
39
|
+
"openapi-generator"
|
|
40
|
+
],
|
|
41
|
+
"author": "devxcant",
|
|
42
|
+
"license": "MIT",
|
|
43
|
+
"repository": {
|
|
44
|
+
"type": "git",
|
|
45
|
+
"url": "git@github-devx:devXcant/api_testing_ai.git"
|
|
46
|
+
},
|
|
47
|
+
"bugs": {
|
|
48
|
+
"url": "https://github.com/devXcant/api_testing_ai/issues"
|
|
49
|
+
},
|
|
50
|
+
"homepage": "https://github.com/devXcant/api_testing_ai"
|
|
51
|
+
}
|
package/src/generate.js
ADDED
|
@@ -0,0 +1,833 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import * as babel from "@babel/parser";
|
|
4
|
+
import * as t from "@babel/types";
|
|
5
|
+
import traverseModule from "@babel/traverse";
|
|
6
|
+
import chalk from "chalk";
|
|
7
|
+
|
|
8
|
+
import * as globModule from "glob";
|
|
9
|
+
const glob = globModule.glob || globModule.default || globModule;
|
|
10
|
+
|
|
11
|
+
const traverse = traverseModule.default || traverseModule;
|
|
12
|
+
|
|
13
|
+
function detectRoutePrefixes(basePath) {
|
|
14
|
+
const mainFiles = glob.sync(
|
|
15
|
+
`${basePath}/**/{index,app,server,main}.{ts,js}`,
|
|
16
|
+
{
|
|
17
|
+
ignore: ["**/node_modules/**", "**/dist/**", "**/build/**"],
|
|
18
|
+
}
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
const prefixMap = {};
|
|
22
|
+
|
|
23
|
+
for (const file of mainFiles) {
|
|
24
|
+
const ast = parseFile(file);
|
|
25
|
+
if (!ast) continue;
|
|
26
|
+
|
|
27
|
+
traverse(ast, {
|
|
28
|
+
CallExpression(nodePath) {
|
|
29
|
+
const callee = nodePath.node.callee;
|
|
30
|
+
|
|
31
|
+
if (
|
|
32
|
+
t.isMemberExpression(callee) &&
|
|
33
|
+
t.isIdentifier(callee.property, { name: "use" })
|
|
34
|
+
) {
|
|
35
|
+
const args = nodePath.node.arguments;
|
|
36
|
+
|
|
37
|
+
if (args.length >= 2 && t.isStringLiteral(args[0])) {
|
|
38
|
+
const prefix = args[0].value;
|
|
39
|
+
const routeImport = args[1];
|
|
40
|
+
|
|
41
|
+
let routeIdentifier = null;
|
|
42
|
+
if (t.isIdentifier(routeImport)) {
|
|
43
|
+
routeIdentifier = routeImport.name;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (routeIdentifier) {
|
|
47
|
+
nodePath.scope.traverse(nodePath.scope.block, {
|
|
48
|
+
ImportDeclaration(importPath) {
|
|
49
|
+
importPath.node.specifiers.forEach((spec) => {
|
|
50
|
+
if (
|
|
51
|
+
t.isImportDefaultSpecifier(spec) &&
|
|
52
|
+
spec.local.name === routeIdentifier
|
|
53
|
+
) {
|
|
54
|
+
const importSource = importPath.node.source.value;
|
|
55
|
+
const routeFile = importSource
|
|
56
|
+
.replace(/^\.\//, "")
|
|
57
|
+
.replace(/^\//, "");
|
|
58
|
+
prefixMap[routeFile] = prefix;
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return prefixMap;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function parseFile(filePath) {
|
|
74
|
+
try {
|
|
75
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
76
|
+
return babel.parse(content, {
|
|
77
|
+
sourceType: "module",
|
|
78
|
+
plugins: ["typescript", "jsx", "decorators-legacy"],
|
|
79
|
+
});
|
|
80
|
+
} catch (err) {
|
|
81
|
+
console.warn(
|
|
82
|
+
chalk.yellow(`โ ๏ธ Failed to parse ${filePath}: ${err.message}`)
|
|
83
|
+
);
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function extractRoutesFromFile(filePath) {
|
|
89
|
+
const ast = parseFile(filePath);
|
|
90
|
+
if (!ast) return { routes: [], authEndpoint: null };
|
|
91
|
+
|
|
92
|
+
const routes = [];
|
|
93
|
+
let authEndpoint = null;
|
|
94
|
+
|
|
95
|
+
traverse(ast, {
|
|
96
|
+
CallExpression(nodePath) {
|
|
97
|
+
const callee = nodePath.node.callee;
|
|
98
|
+
if (
|
|
99
|
+
t.isMemberExpression(callee) &&
|
|
100
|
+
t.isIdentifier(callee.object, { name: "router" })
|
|
101
|
+
) {
|
|
102
|
+
const method = callee.property.name?.toUpperCase?.();
|
|
103
|
+
const routeArg = nodePath.node.arguments?.[0];
|
|
104
|
+
if (method && t.isStringLiteral(routeArg)) {
|
|
105
|
+
const routePath = routeArg.value;
|
|
106
|
+
const isAuth =
|
|
107
|
+
routePath.includes("login") || routePath.includes("signin");
|
|
108
|
+
|
|
109
|
+
const route = {
|
|
110
|
+
method,
|
|
111
|
+
path: routePath,
|
|
112
|
+
file: filePath,
|
|
113
|
+
isAuth,
|
|
114
|
+
schema: null,
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
if (isAuth && method === "POST" && !authEndpoint) {
|
|
118
|
+
authEndpoint = route;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
routes.push(route);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
return { routes, authEndpoint };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function detectSchemas(filePath) {
|
|
131
|
+
const ast = parseFile(filePath);
|
|
132
|
+
if (!ast) return [];
|
|
133
|
+
|
|
134
|
+
const schemas = [];
|
|
135
|
+
|
|
136
|
+
traverse(ast, {
|
|
137
|
+
CallExpression(nodePath) {
|
|
138
|
+
const callee = nodePath.node.callee;
|
|
139
|
+
|
|
140
|
+
if (t.isMemberExpression(callee)) {
|
|
141
|
+
const objName = callee.object?.name;
|
|
142
|
+
const propName = callee.property?.name;
|
|
143
|
+
|
|
144
|
+
if (
|
|
145
|
+
objName === "z" ||
|
|
146
|
+
(propName &&
|
|
147
|
+
["object", "string", "number", "boolean", "array"].includes(
|
|
148
|
+
propName
|
|
149
|
+
))
|
|
150
|
+
) {
|
|
151
|
+
schemas.push({ type: "zod", path: filePath });
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (t.isIdentifier(callee) && callee.name === "Joi") {
|
|
156
|
+
schemas.push({ type: "joi", path: filePath });
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
Decorator(nodePath) {
|
|
160
|
+
const expr = nodePath.node.expression;
|
|
161
|
+
if (t.isCallExpression(expr) && t.isIdentifier(expr.callee)) {
|
|
162
|
+
const decoratorName = expr.callee.name;
|
|
163
|
+
if (
|
|
164
|
+
["IsString", "IsNumber", "IsEmail", "IsOptional"].includes(
|
|
165
|
+
decoratorName
|
|
166
|
+
)
|
|
167
|
+
) {
|
|
168
|
+
schemas.push({ type: "class-validator", path: filePath });
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
return schemas;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function groupRoutesByController(routes) {
|
|
178
|
+
const grouped = {};
|
|
179
|
+
for (const route of routes) {
|
|
180
|
+
const folder = path.basename(path.dirname(route.file));
|
|
181
|
+
if (!grouped[folder]) grouped[folder] = [];
|
|
182
|
+
grouped[folder].push(route);
|
|
183
|
+
}
|
|
184
|
+
return grouped;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function testLiveEndpoint(route, baseUrl, authToken = null) {
|
|
188
|
+
try {
|
|
189
|
+
const url = `${baseUrl}${route.path.replace(/:\w+/g, "1")}`;
|
|
190
|
+
const headers = { "Content-Type": "application/json" };
|
|
191
|
+
if (authToken) headers["Authorization"] = `Bearer ${authToken}`;
|
|
192
|
+
|
|
193
|
+
const options = {
|
|
194
|
+
method: route.method,
|
|
195
|
+
headers,
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
if (["POST", "PUT", "PATCH"].includes(route.method)) {
|
|
199
|
+
options.body = JSON.stringify({});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const response = await fetch(url, options);
|
|
203
|
+
const responseText = await response.text();
|
|
204
|
+
|
|
205
|
+
let responseBody;
|
|
206
|
+
try {
|
|
207
|
+
responseBody = JSON.parse(responseText);
|
|
208
|
+
} catch {
|
|
209
|
+
responseBody = responseText;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
success: response.ok,
|
|
214
|
+
status: response.status,
|
|
215
|
+
headers: Object.fromEntries(response.headers.entries()),
|
|
216
|
+
body: responseBody,
|
|
217
|
+
};
|
|
218
|
+
} catch (err) {
|
|
219
|
+
return {
|
|
220
|
+
success: false,
|
|
221
|
+
error: err.message,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function createPostmanCollection(
|
|
227
|
+
groupedRoutes,
|
|
228
|
+
liveResults = {},
|
|
229
|
+
authInfo = null
|
|
230
|
+
) {
|
|
231
|
+
const collection = {
|
|
232
|
+
info: {
|
|
233
|
+
name: "AI Testing Collection",
|
|
234
|
+
schema:
|
|
235
|
+
"https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
|
|
236
|
+
},
|
|
237
|
+
item: [],
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
for (const [folder, routes] of Object.entries(groupedRoutes)) {
|
|
241
|
+
const folderItem = {
|
|
242
|
+
name: folder.charAt(0).toUpperCase() + folder.slice(1),
|
|
243
|
+
item: routes.map((r) => {
|
|
244
|
+
const item = {
|
|
245
|
+
name: `${r.method} ${r.path}`,
|
|
246
|
+
request: {
|
|
247
|
+
method: r.method,
|
|
248
|
+
header: [],
|
|
249
|
+
url: {
|
|
250
|
+
raw: `{{baseUrl}}${r.path}`,
|
|
251
|
+
host: ["{{baseUrl}}"],
|
|
252
|
+
path: r.path.split("/").filter(Boolean),
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
if (authInfo && !r.isAuth) {
|
|
258
|
+
item.request.header.push({
|
|
259
|
+
key: "Authorization",
|
|
260
|
+
value: "Bearer {{authToken}}",
|
|
261
|
+
type: "text",
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (["POST", "PUT", "PATCH"].includes(r.method)) {
|
|
266
|
+
item.request.header.push({
|
|
267
|
+
key: "Content-Type",
|
|
268
|
+
value: "application/json",
|
|
269
|
+
});
|
|
270
|
+
item.request.body = {
|
|
271
|
+
mode: "raw",
|
|
272
|
+
raw: JSON.stringify({}, null, 2),
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const liveKey = `${r.method} ${r.path}`;
|
|
277
|
+
if (liveResults[liveKey]) {
|
|
278
|
+
item.response = [
|
|
279
|
+
{
|
|
280
|
+
name: "Example Response",
|
|
281
|
+
status: `${liveResults[liveKey].status}`,
|
|
282
|
+
code: liveResults[liveKey].status,
|
|
283
|
+
body: JSON.stringify(liveResults[liveKey].body, null, 2),
|
|
284
|
+
},
|
|
285
|
+
];
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return item;
|
|
289
|
+
}),
|
|
290
|
+
};
|
|
291
|
+
collection.item.push(folderItem);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return collection;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function createEnvironmentFiles(authInfo = null) {
|
|
298
|
+
const baseValues = [
|
|
299
|
+
{ key: "baseUrl", value: "http://localhost:5050", enabled: true },
|
|
300
|
+
];
|
|
301
|
+
|
|
302
|
+
if (authInfo) {
|
|
303
|
+
baseValues.push({
|
|
304
|
+
key: "authToken",
|
|
305
|
+
value: "your_jwt_token_here",
|
|
306
|
+
enabled: true,
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const envs = [
|
|
311
|
+
{
|
|
312
|
+
name: "Local Environment",
|
|
313
|
+
values: baseValues,
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
name: "Production Environment",
|
|
317
|
+
values: [
|
|
318
|
+
{ key: "baseUrl", value: "https://api.example.com", enabled: true },
|
|
319
|
+
...(authInfo
|
|
320
|
+
? [{ key: "authToken", value: "your_jwt_token_here", enabled: true }]
|
|
321
|
+
: []),
|
|
322
|
+
],
|
|
323
|
+
},
|
|
324
|
+
];
|
|
325
|
+
|
|
326
|
+
envs.forEach((env) => {
|
|
327
|
+
fs.writeFileSync(
|
|
328
|
+
`${env.name.replace(" ", "_").toLowerCase()}.postman_environment.json`,
|
|
329
|
+
JSON.stringify(env, null, 2)
|
|
330
|
+
);
|
|
331
|
+
});
|
|
332
|
+
console.log(chalk.green("๐ Environments generated (local & production)"));
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function generateTestFiles(groupedRoutes, authInfo = null) {
|
|
336
|
+
const testsDir = "./tests/auto";
|
|
337
|
+
if (!fs.existsSync(testsDir)) {
|
|
338
|
+
fs.mkdirSync(testsDir, { recursive: true });
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
for (const [folder, routes] of Object.entries(groupedRoutes)) {
|
|
342
|
+
const testContent = `import { test, expect, describe } from "bun:test";
|
|
343
|
+
|
|
344
|
+
const baseUrl = "http://localhost:5050";
|
|
345
|
+
${authInfo ? 'let authToken = "";' : ""}
|
|
346
|
+
|
|
347
|
+
${
|
|
348
|
+
authInfo
|
|
349
|
+
? `describe("Authentication", () => {
|
|
350
|
+
test("should login and get token", async () => {
|
|
351
|
+
const response = await fetch(\`\${baseUrl}${authInfo.path}\`, {
|
|
352
|
+
method: "POST",
|
|
353
|
+
headers: { "Content-Type": "application/json" },
|
|
354
|
+
body: JSON.stringify({
|
|
355
|
+
email: "test@example.com",
|
|
356
|
+
password: "password123"
|
|
357
|
+
})
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
const data = await response.json();
|
|
361
|
+
expect(response.ok).toBe(true);
|
|
362
|
+
|
|
363
|
+
authToken = data.token || data.access_token || data.accessToken;
|
|
364
|
+
expect(authToken).toBeDefined();
|
|
365
|
+
});
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
`
|
|
369
|
+
: ""
|
|
370
|
+
}describe("${folder.charAt(0).toUpperCase() + folder.slice(1)} Routes", () => {
|
|
371
|
+
${routes
|
|
372
|
+
.map(
|
|
373
|
+
(r) => ` test("${r.method} ${r.path}", async () => {
|
|
374
|
+
const url = \`\${baseUrl}${r.path.replace(/:\w+/g, "1")}\`;
|
|
375
|
+
const options = {
|
|
376
|
+
method: "${r.method}",
|
|
377
|
+
headers: {
|
|
378
|
+
"Content-Type": "application/json"${
|
|
379
|
+
authInfo && !r.isAuth
|
|
380
|
+
? ',\n "Authorization": `Bearer ${authToken}`'
|
|
381
|
+
: ""
|
|
382
|
+
}
|
|
383
|
+
}${
|
|
384
|
+
["POST", "PUT", "PATCH"].includes(r.method)
|
|
385
|
+
? ",\n body: JSON.stringify({})"
|
|
386
|
+
: ""
|
|
387
|
+
}
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
const response = await fetch(url, options);
|
|
391
|
+
expect(response).toBeDefined();
|
|
392
|
+
});
|
|
393
|
+
`
|
|
394
|
+
)
|
|
395
|
+
.join("\n")}});
|
|
396
|
+
`;
|
|
397
|
+
|
|
398
|
+
fs.writeFileSync(`${testsDir}/${folder}.test.ts`, testContent);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
console.log(chalk.green(`๐งช Test files generated in ${testsDir}/`));
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function generateOpenAPISpec(
|
|
405
|
+
groupedRoutes,
|
|
406
|
+
schemas = [],
|
|
407
|
+
authInfo = null,
|
|
408
|
+
liveResults = {}
|
|
409
|
+
) {
|
|
410
|
+
const spec = {
|
|
411
|
+
openapi: "3.0.0",
|
|
412
|
+
info: {
|
|
413
|
+
title: "API Documentation",
|
|
414
|
+
version: "1.0.0",
|
|
415
|
+
description: "Auto-generated API documentation from AITesting",
|
|
416
|
+
},
|
|
417
|
+
servers: [
|
|
418
|
+
{
|
|
419
|
+
url: "http://localhost:5050",
|
|
420
|
+
description: "Local Development Server",
|
|
421
|
+
},
|
|
422
|
+
{
|
|
423
|
+
url: "https://api.example.com",
|
|
424
|
+
description: "Production Server",
|
|
425
|
+
},
|
|
426
|
+
],
|
|
427
|
+
paths: {},
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
if (authInfo) {
|
|
431
|
+
spec.components = {
|
|
432
|
+
securitySchemes: {
|
|
433
|
+
BearerAuth: {
|
|
434
|
+
type: "http",
|
|
435
|
+
scheme: "bearer",
|
|
436
|
+
bearerFormat: "JWT",
|
|
437
|
+
},
|
|
438
|
+
},
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
for (const [folder, routes] of Object.entries(groupedRoutes)) {
|
|
443
|
+
for (const route of routes) {
|
|
444
|
+
const pathKey = route.path.replace(/:(\w+)/g, "{$1}");
|
|
445
|
+
|
|
446
|
+
if (!spec.paths[pathKey]) {
|
|
447
|
+
spec.paths[pathKey] = {};
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
const operation = {
|
|
451
|
+
tags: [folder.charAt(0).toUpperCase() + folder.slice(1)],
|
|
452
|
+
summary: `${route.method} ${route.path}`,
|
|
453
|
+
description: `Source: ${route.file}`,
|
|
454
|
+
parameters: [],
|
|
455
|
+
responses: {},
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
if (authInfo && !route.isAuth) {
|
|
459
|
+
operation.security = [{ BearerAuth: [] }];
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
const pathParams = route.path.match(/:(\w+)/g);
|
|
463
|
+
if (pathParams) {
|
|
464
|
+
pathParams.forEach((param) => {
|
|
465
|
+
operation.parameters.push({
|
|
466
|
+
name: param.slice(1),
|
|
467
|
+
in: "path",
|
|
468
|
+
required: true,
|
|
469
|
+
schema: { type: "string" },
|
|
470
|
+
description: "Resource identifier",
|
|
471
|
+
});
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
if (["POST", "PUT", "PATCH"].includes(route.method)) {
|
|
476
|
+
operation.requestBody = {
|
|
477
|
+
required: true,
|
|
478
|
+
content: {
|
|
479
|
+
"application/json": {
|
|
480
|
+
schema: {
|
|
481
|
+
type: "object",
|
|
482
|
+
properties: {},
|
|
483
|
+
},
|
|
484
|
+
},
|
|
485
|
+
},
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const liveKey = `${route.method} ${route.path}`;
|
|
490
|
+
if (liveResults[liveKey]) {
|
|
491
|
+
const result = liveResults[liveKey];
|
|
492
|
+
const statusCode = result.status || 200;
|
|
493
|
+
|
|
494
|
+
operation.responses[statusCode] = {
|
|
495
|
+
description: result.success
|
|
496
|
+
? "Successful response"
|
|
497
|
+
: "Error response",
|
|
498
|
+
content: {
|
|
499
|
+
"application/json": {
|
|
500
|
+
schema: {
|
|
501
|
+
type: "object",
|
|
502
|
+
},
|
|
503
|
+
example: result.body,
|
|
504
|
+
},
|
|
505
|
+
},
|
|
506
|
+
};
|
|
507
|
+
} else {
|
|
508
|
+
operation.responses["200"] = {
|
|
509
|
+
description: "Successful response",
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
spec.paths[pathKey][route.method.toLowerCase()] = operation;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
fs.writeFileSync("openapi.json", JSON.stringify(spec, null, 2));
|
|
518
|
+
console.log(chalk.green("๐ OpenAPI specification generated (openapi.json)"));
|
|
519
|
+
|
|
520
|
+
const swaggerHTML = `<!DOCTYPE html>
|
|
521
|
+
<html lang="en">
|
|
522
|
+
<head>
|
|
523
|
+
<meta charset="UTF-8">
|
|
524
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
525
|
+
<title>API Documentation - Swagger UI</title>
|
|
526
|
+
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5.10.3/swagger-ui.css">
|
|
527
|
+
</head>
|
|
528
|
+
<body>
|
|
529
|
+
<div id="swagger-ui"></div>
|
|
530
|
+
<script src="https://unpkg.com/swagger-ui-dist@5.10.3/swagger-ui-bundle.js"></script>
|
|
531
|
+
<script src="https://unpkg.com/swagger-ui-dist@5.10.3/swagger-ui-standalone-preset.js"></script>
|
|
532
|
+
<script>
|
|
533
|
+
window.onload = function() {
|
|
534
|
+
SwaggerUIBundle({
|
|
535
|
+
url: './openapi.json',
|
|
536
|
+
dom_id: '#swagger-ui',
|
|
537
|
+
deepLinking: true,
|
|
538
|
+
presets: [
|
|
539
|
+
SwaggerUIBundle.presets.apis,
|
|
540
|
+
SwaggerUIStandalonePreset
|
|
541
|
+
],
|
|
542
|
+
plugins: [
|
|
543
|
+
SwaggerUIBundle.plugins.DownloadUrl
|
|
544
|
+
],
|
|
545
|
+
layout: "StandaloneLayout"
|
|
546
|
+
});
|
|
547
|
+
};
|
|
548
|
+
</script>
|
|
549
|
+
</body>
|
|
550
|
+
</html>`;
|
|
551
|
+
|
|
552
|
+
fs.writeFileSync("api-docs.html", swaggerHTML);
|
|
553
|
+
console.log(chalk.green("๐ Swagger UI page generated (api-docs.html)"));
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function generateEnhancedDocs(
|
|
557
|
+
groupedRoutes,
|
|
558
|
+
schemas = [],
|
|
559
|
+
authInfo = null,
|
|
560
|
+
liveResults = {}
|
|
561
|
+
) {
|
|
562
|
+
let doc = `# ๐ API Documentation
|
|
563
|
+
|
|
564
|
+
Generated by AITesting - ${new Date().toLocaleDateString()}
|
|
565
|
+
|
|
566
|
+
---
|
|
567
|
+
|
|
568
|
+
`;
|
|
569
|
+
|
|
570
|
+
if (authInfo) {
|
|
571
|
+
doc += `## ๐ Authentication
|
|
572
|
+
|
|
573
|
+
This API uses JWT Bearer token authentication.
|
|
574
|
+
|
|
575
|
+
**Login Endpoint:** \`${authInfo.method} ${authInfo.path}\`
|
|
576
|
+
|
|
577
|
+
**Headers Required:**
|
|
578
|
+
\`\`\`
|
|
579
|
+
Authorization: Bearer <your_token>
|
|
580
|
+
\`\`\`
|
|
581
|
+
|
|
582
|
+
---
|
|
583
|
+
|
|
584
|
+
`;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
if (schemas.length > 0) {
|
|
588
|
+
const schemaTypes = [...new Set(schemas.map((s) => s.type))];
|
|
589
|
+
doc += `## ๐ Validation Schemas
|
|
590
|
+
|
|
591
|
+
Detected validation libraries: ${schemaTypes.join(", ")}
|
|
592
|
+
|
|
593
|
+
---
|
|
594
|
+
|
|
595
|
+
`;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
for (const [folder, routes] of Object.entries(groupedRoutes)) {
|
|
599
|
+
doc += `## ${folder.charAt(0).toUpperCase() + folder.slice(1)}\n\n`;
|
|
600
|
+
|
|
601
|
+
for (const route of routes) {
|
|
602
|
+
doc += `### ${route.method} ${route.path}\n\n`;
|
|
603
|
+
|
|
604
|
+
if (route.isAuth) {
|
|
605
|
+
doc += `๐ **Public endpoint** (Authentication)\n\n`;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
const pathParams = route.path.match(/:\w+/g);
|
|
609
|
+
if (pathParams) {
|
|
610
|
+
doc += `**Path Parameters:**\n`;
|
|
611
|
+
pathParams.forEach((param) => {
|
|
612
|
+
doc += `- \`${param.slice(1)}\` - ID or identifier\n`;
|
|
613
|
+
});
|
|
614
|
+
doc += `\n`;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
if (["POST", "PUT", "PATCH"].includes(route.method)) {
|
|
618
|
+
doc += `**Request Body:**\n\`\`\`json\n{}\n\`\`\`\n\n`;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
const liveKey = `${route.method} ${route.path}`;
|
|
622
|
+
if (liveResults[liveKey] && liveResults[liveKey].success) {
|
|
623
|
+
doc += `**Example Response (${
|
|
624
|
+
liveResults[liveKey].status
|
|
625
|
+
}):**\n\`\`\`json\n${JSON.stringify(
|
|
626
|
+
liveResults[liveKey].body,
|
|
627
|
+
null,
|
|
628
|
+
2
|
|
629
|
+
)}\n\`\`\`\n\n`;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
doc += `**File:** \`${route.file}\`\n\n`;
|
|
633
|
+
doc += `---\n\n`;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
fs.writeFileSync("API_DOCUMENTATION.md", doc);
|
|
638
|
+
console.log(
|
|
639
|
+
chalk.green("๐ Enhanced documentation generated (API_DOCUMENTATION.md)")
|
|
640
|
+
);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
export async function generateCollection(basePath, options = {}) {
|
|
644
|
+
const baseUrl = options.baseUrl || "http://localhost:5050";
|
|
645
|
+
const outputDir = options.outputDir || ".";
|
|
646
|
+
|
|
647
|
+
console.log(chalk.cyan(`\n๐ AITesting v1.1.0 - Full Workflow\n`));
|
|
648
|
+
console.log(chalk.cyan(`๐ Scanning backend in: ${basePath}\n`));
|
|
649
|
+
if (options.all && baseUrl) {
|
|
650
|
+
console.log(chalk.blue(`๐ Testing URL: ${baseUrl}\n`));
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
console.log(chalk.cyan(`๐ Detecting route prefixes...\n`));
|
|
654
|
+
const prefixMap = detectRoutePrefixes(basePath);
|
|
655
|
+
|
|
656
|
+
if (Object.keys(prefixMap).length > 0) {
|
|
657
|
+
console.log(chalk.green(`โ
Found route prefixes:`));
|
|
658
|
+
Object.entries(prefixMap).forEach(([file, prefix]) => {
|
|
659
|
+
console.log(chalk.white(` ${file} โ ${prefix}`));
|
|
660
|
+
});
|
|
661
|
+
console.log();
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
const files = glob.sync(`${basePath}/**/*.ts`, {
|
|
665
|
+
ignore: ["**/node_modules/**", "**/dist/**", "**/build/**", "**/.git/**"],
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
const allRoutes = [];
|
|
669
|
+
let authEndpoint = null;
|
|
670
|
+
const allSchemas = [];
|
|
671
|
+
|
|
672
|
+
for (const file of files) {
|
|
673
|
+
const { routes, authEndpoint: auth } = extractRoutesFromFile(file);
|
|
674
|
+
|
|
675
|
+
const relativeFile = file.replace(`${basePath}/`, "").replace(/\\/g, "/");
|
|
676
|
+
|
|
677
|
+
routes.forEach((route) => {
|
|
678
|
+
for (const [routeFile, prefix] of Object.entries(prefixMap)) {
|
|
679
|
+
if (
|
|
680
|
+
relativeFile.includes(routeFile) ||
|
|
681
|
+
relativeFile.endsWith(routeFile + ".ts")
|
|
682
|
+
) {
|
|
683
|
+
route.path = prefix + route.path;
|
|
684
|
+
route.prefix = prefix;
|
|
685
|
+
break;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
allRoutes.push(...routes);
|
|
691
|
+
|
|
692
|
+
if (auth) {
|
|
693
|
+
authEndpoint = auth;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
if (options.all) {
|
|
697
|
+
const schemas = detectSchemas(file);
|
|
698
|
+
allSchemas.push(...schemas);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
if (!allRoutes.length) {
|
|
703
|
+
console.warn(chalk.yellow("โ ๏ธ No routes found."));
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
console.log(chalk.green(`โ
Found ${allRoutes.length} routes`));
|
|
708
|
+
|
|
709
|
+
if (authEndpoint) {
|
|
710
|
+
console.log(
|
|
711
|
+
chalk.blue(
|
|
712
|
+
`๐ Detected auth endpoint: ${authEndpoint.method} ${authEndpoint.path}`
|
|
713
|
+
)
|
|
714
|
+
);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
if (allSchemas.length > 0) {
|
|
718
|
+
const schemaTypes = [...new Set(allSchemas.map((s) => s.type))];
|
|
719
|
+
console.log(
|
|
720
|
+
chalk.magenta(`๐ Detected schemas: ${schemaTypes.join(", ")}`)
|
|
721
|
+
);
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
const groupedRoutes = groupRoutesByController(allRoutes);
|
|
725
|
+
|
|
726
|
+
let liveResults = {};
|
|
727
|
+
let authToken = null;
|
|
728
|
+
|
|
729
|
+
if (options.all) {
|
|
730
|
+
console.log(chalk.cyan(`\n๐งช Running live endpoint tests...\n`));
|
|
731
|
+
console.log(
|
|
732
|
+
chalk.yellow(`โ ๏ธ Making REAL HTTP requests to your backend!\n`)
|
|
733
|
+
);
|
|
734
|
+
|
|
735
|
+
if (authEndpoint) {
|
|
736
|
+
const authResult = await testLiveEndpoint(authEndpoint, baseUrl);
|
|
737
|
+
if (authResult.success && authResult.body) {
|
|
738
|
+
authToken =
|
|
739
|
+
authResult.body.token ||
|
|
740
|
+
authResult.body.access_token ||
|
|
741
|
+
authResult.body.accessToken;
|
|
742
|
+
if (authToken) {
|
|
743
|
+
console.log(chalk.green(`โ
Auth token obtained`));
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
liveResults[`${authEndpoint.method} ${authEndpoint.path}`] = authResult;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
for (const route of allRoutes) {
|
|
750
|
+
if (route.isAuth) continue;
|
|
751
|
+
|
|
752
|
+
const result = await testLiveEndpoint(route, baseUrl, authToken);
|
|
753
|
+
liveResults[`${route.method} ${route.path}`] = result;
|
|
754
|
+
|
|
755
|
+
const status = result.success ? chalk.green("โ") : chalk.red("โ");
|
|
756
|
+
console.log(
|
|
757
|
+
`${status} ${route.method} ${route.path} ${result.status || ""}`
|
|
758
|
+
);
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
const reportDir = "./.aitesting";
|
|
762
|
+
if (!fs.existsSync(reportDir)) {
|
|
763
|
+
fs.mkdirSync(reportDir, { recursive: true });
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
fs.writeFileSync(
|
|
767
|
+
`${reportDir}/report.json`,
|
|
768
|
+
JSON.stringify(
|
|
769
|
+
{ routes: allRoutes, results: liveResults, authEndpoint },
|
|
770
|
+
null,
|
|
771
|
+
2
|
|
772
|
+
)
|
|
773
|
+
);
|
|
774
|
+
console.log(
|
|
775
|
+
chalk.green(`\n๐ Live test report saved to .aitesting/report.json`)
|
|
776
|
+
);
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
const collection = createPostmanCollection(
|
|
780
|
+
groupedRoutes,
|
|
781
|
+
liveResults,
|
|
782
|
+
authEndpoint
|
|
783
|
+
);
|
|
784
|
+
fs.writeFileSync(
|
|
785
|
+
"postman_collection.json",
|
|
786
|
+
JSON.stringify(collection, null, 2)
|
|
787
|
+
);
|
|
788
|
+
console.log(chalk.green(`\n๐ Generated: postman_collection.json`));
|
|
789
|
+
|
|
790
|
+
createEnvironmentFiles(authEndpoint);
|
|
791
|
+
|
|
792
|
+
if (options.all) {
|
|
793
|
+
generateTestFiles(groupedRoutes, authEndpoint);
|
|
794
|
+
generateOpenAPISpec(groupedRoutes, allSchemas, authEndpoint, liveResults);
|
|
795
|
+
generateEnhancedDocs(groupedRoutes, allSchemas, authEndpoint, liveResults);
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
console.log(
|
|
799
|
+
chalk.green.bold("\nโ
Complete! All features executed successfully.\n")
|
|
800
|
+
);
|
|
801
|
+
|
|
802
|
+
if (options.all) {
|
|
803
|
+
console.log(chalk.cyan("๐ฆ Generated files:"));
|
|
804
|
+
console.log(
|
|
805
|
+
chalk.white(" - postman_collection.json (with example responses)")
|
|
806
|
+
);
|
|
807
|
+
console.log(chalk.white(" - local_environment.postman_environment.json"));
|
|
808
|
+
console.log(
|
|
809
|
+
chalk.white(" - production_environment.postman_environment.json")
|
|
810
|
+
);
|
|
811
|
+
console.log(chalk.white(" - openapi.json (OpenAPI 3.0 specification)"));
|
|
812
|
+
console.log(
|
|
813
|
+
chalk.white(" - api-docs.html (Swagger UI - open in browser)")
|
|
814
|
+
);
|
|
815
|
+
console.log(
|
|
816
|
+
chalk.white(" - API_DOCUMENTATION.md (markdown documentation)")
|
|
817
|
+
);
|
|
818
|
+
console.log(chalk.white(" - tests/auto/*.test.ts (automated test files)"));
|
|
819
|
+
console.log(
|
|
820
|
+
chalk.white(" - .aitesting/report.json (live test results)\n")
|
|
821
|
+
);
|
|
822
|
+
console.log(
|
|
823
|
+
chalk.blue(
|
|
824
|
+
"\n๐ก Tip: Open api-docs.html in your browser to view interactive Swagger UI!"
|
|
825
|
+
)
|
|
826
|
+
);
|
|
827
|
+
console.log(
|
|
828
|
+
chalk.yellow(
|
|
829
|
+
"โ ๏ธ Note: Live responses are from REAL HTTP requests to your backend.\n"
|
|
830
|
+
)
|
|
831
|
+
);
|
|
832
|
+
}
|
|
833
|
+
}
|