popeye-cli 1.7.0 → 1.9.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/README.md +148 -7
- package/cheatsheet.md +440 -0
- package/dist/cli/commands/db.d.ts +10 -0
- package/dist/cli/commands/db.d.ts.map +1 -0
- package/dist/cli/commands/db.js +240 -0
- package/dist/cli/commands/db.js.map +1 -0
- package/dist/cli/commands/doctor.d.ts +18 -0
- package/dist/cli/commands/doctor.d.ts.map +1 -0
- package/dist/cli/commands/doctor.js +255 -0
- package/dist/cli/commands/doctor.js.map +1 -0
- package/dist/cli/commands/index.d.ts +3 -0
- package/dist/cli/commands/index.d.ts.map +1 -1
- package/dist/cli/commands/index.js +3 -0
- package/dist/cli/commands/index.js.map +1 -1
- package/dist/cli/commands/review.d.ts +31 -0
- package/dist/cli/commands/review.d.ts.map +1 -0
- package/dist/cli/commands/review.js +156 -0
- package/dist/cli/commands/review.js.map +1 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +4 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/interactive.d.ts.map +1 -1
- package/dist/cli/interactive.js +218 -61
- package/dist/cli/interactive.js.map +1 -1
- package/dist/generators/admin-wizard.d.ts +25 -0
- package/dist/generators/admin-wizard.d.ts.map +1 -0
- package/dist/generators/admin-wizard.js +123 -0
- package/dist/generators/admin-wizard.js.map +1 -0
- package/dist/generators/all.d.ts.map +1 -1
- package/dist/generators/all.js +10 -3
- package/dist/generators/all.js.map +1 -1
- package/dist/generators/database.d.ts +58 -0
- package/dist/generators/database.d.ts.map +1 -0
- package/dist/generators/database.js +229 -0
- package/dist/generators/database.js.map +1 -0
- package/dist/generators/fullstack.d.ts.map +1 -1
- package/dist/generators/fullstack.js +23 -7
- package/dist/generators/fullstack.js.map +1 -1
- package/dist/generators/index.d.ts +2 -0
- package/dist/generators/index.d.ts.map +1 -1
- package/dist/generators/index.js +2 -0
- package/dist/generators/index.js.map +1 -1
- package/dist/generators/templates/admin-wizard-python.d.ts +32 -0
- package/dist/generators/templates/admin-wizard-python.d.ts.map +1 -0
- package/dist/generators/templates/admin-wizard-python.js +425 -0
- package/dist/generators/templates/admin-wizard-python.js.map +1 -0
- package/dist/generators/templates/admin-wizard-react.d.ts +48 -0
- package/dist/generators/templates/admin-wizard-react.d.ts.map +1 -0
- package/dist/generators/templates/admin-wizard-react.js +554 -0
- package/dist/generators/templates/admin-wizard-react.js.map +1 -0
- package/dist/generators/templates/database-docker.d.ts +23 -0
- package/dist/generators/templates/database-docker.d.ts.map +1 -0
- package/dist/generators/templates/database-docker.js +221 -0
- package/dist/generators/templates/database-docker.js.map +1 -0
- package/dist/generators/templates/database-python.d.ts +54 -0
- package/dist/generators/templates/database-python.d.ts.map +1 -0
- package/dist/generators/templates/database-python.js +723 -0
- package/dist/generators/templates/database-python.js.map +1 -0
- package/dist/generators/templates/database-typescript.d.ts +34 -0
- package/dist/generators/templates/database-typescript.d.ts.map +1 -0
- package/dist/generators/templates/database-typescript.js +232 -0
- package/dist/generators/templates/database-typescript.js.map +1 -0
- package/dist/generators/templates/fullstack.d.ts.map +1 -1
- package/dist/generators/templates/fullstack.js +29 -0
- package/dist/generators/templates/fullstack.js.map +1 -1
- package/dist/generators/templates/index.d.ts +5 -0
- package/dist/generators/templates/index.d.ts.map +1 -1
- package/dist/generators/templates/index.js +5 -0
- package/dist/generators/templates/index.js.map +1 -1
- package/dist/state/index.d.ts +10 -0
- package/dist/state/index.d.ts.map +1 -1
- package/dist/state/index.js +21 -0
- package/dist/state/index.js.map +1 -1
- package/dist/types/audit.d.ts +623 -0
- package/dist/types/audit.d.ts.map +1 -0
- package/dist/types/audit.js +240 -0
- package/dist/types/audit.js.map +1 -0
- package/dist/types/database-runtime.d.ts +86 -0
- package/dist/types/database-runtime.d.ts.map +1 -0
- package/dist/types/database-runtime.js +61 -0
- package/dist/types/database-runtime.js.map +1 -0
- package/dist/types/database.d.ts +85 -0
- package/dist/types/database.d.ts.map +1 -0
- package/dist/types/database.js +71 -0
- package/dist/types/database.js.map +1 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +4 -0
- package/dist/types/index.js.map +1 -1
- package/dist/types/workflow.d.ts +36 -0
- package/dist/types/workflow.d.ts.map +1 -1
- package/dist/types/workflow.js +7 -0
- package/dist/types/workflow.js.map +1 -1
- package/dist/workflow/audit-analyzer.d.ts +58 -0
- package/dist/workflow/audit-analyzer.d.ts.map +1 -0
- package/dist/workflow/audit-analyzer.js +420 -0
- package/dist/workflow/audit-analyzer.js.map +1 -0
- package/dist/workflow/audit-mode.d.ts +28 -0
- package/dist/workflow/audit-mode.d.ts.map +1 -0
- package/dist/workflow/audit-mode.js +169 -0
- package/dist/workflow/audit-mode.js.map +1 -0
- package/dist/workflow/audit-recovery.d.ts +61 -0
- package/dist/workflow/audit-recovery.d.ts.map +1 -0
- package/dist/workflow/audit-recovery.js +242 -0
- package/dist/workflow/audit-recovery.js.map +1 -0
- package/dist/workflow/audit-reporter.d.ts +65 -0
- package/dist/workflow/audit-reporter.d.ts.map +1 -0
- package/dist/workflow/audit-reporter.js +301 -0
- package/dist/workflow/audit-reporter.js.map +1 -0
- package/dist/workflow/audit-scanner.d.ts +87 -0
- package/dist/workflow/audit-scanner.d.ts.map +1 -0
- package/dist/workflow/audit-scanner.js +768 -0
- package/dist/workflow/audit-scanner.js.map +1 -0
- package/dist/workflow/db-setup-runner.d.ts +63 -0
- package/dist/workflow/db-setup-runner.d.ts.map +1 -0
- package/dist/workflow/db-setup-runner.js +336 -0
- package/dist/workflow/db-setup-runner.js.map +1 -0
- package/dist/workflow/db-state-machine.d.ts +30 -0
- package/dist/workflow/db-state-machine.d.ts.map +1 -0
- package/dist/workflow/db-state-machine.js +51 -0
- package/dist/workflow/db-state-machine.js.map +1 -0
- package/dist/workflow/index.d.ts +7 -0
- package/dist/workflow/index.d.ts.map +1 -1
- package/dist/workflow/index.js +7 -0
- package/dist/workflow/index.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/commands/db.ts +281 -0
- package/src/cli/commands/doctor.ts +273 -0
- package/src/cli/commands/index.ts +3 -0
- package/src/cli/commands/review.ts +187 -0
- package/src/cli/index.ts +6 -0
- package/src/cli/interactive.ts +174 -4
- package/src/generators/admin-wizard.ts +146 -0
- package/src/generators/all.ts +10 -3
- package/src/generators/database.ts +286 -0
- package/src/generators/fullstack.ts +26 -9
- package/src/generators/index.ts +12 -0
- package/src/generators/templates/admin-wizard-python.ts +431 -0
- package/src/generators/templates/admin-wizard-react.ts +560 -0
- package/src/generators/templates/database-docker.ts +227 -0
- package/src/generators/templates/database-python.ts +734 -0
- package/src/generators/templates/database-typescript.ts +238 -0
- package/src/generators/templates/fullstack.ts +29 -0
- package/src/generators/templates/index.ts +5 -0
- package/src/state/index.ts +28 -0
- package/src/types/audit.ts +294 -0
- package/src/types/database-runtime.ts +69 -0
- package/src/types/database.ts +84 -0
- package/src/types/index.ts +29 -0
- package/src/types/workflow.ts +20 -0
- package/src/workflow/audit-analyzer.ts +491 -0
- package/src/workflow/audit-mode.ts +240 -0
- package/src/workflow/audit-recovery.ts +284 -0
- package/src/workflow/audit-reporter.ts +370 -0
- package/src/workflow/audit-scanner.ts +873 -0
- package/src/workflow/db-setup-runner.ts +391 -0
- package/src/workflow/db-state-machine.ts +58 -0
- package/src/workflow/index.ts +7 -0
- package/tests/cli/commands/review.test.ts +52 -0
- package/tests/generators/admin-wizard-orchestrator.test.ts +64 -0
- package/tests/generators/admin-wizard-templates.test.ts +366 -0
- package/tests/generators/cross-phase-integration.test.ts +383 -0
- package/tests/generators/database.test.ts +456 -0
- package/tests/generators/fe-be-db-integration.test.ts +613 -0
- package/tests/types/audit.test.ts +250 -0
- package/tests/types/database-runtime.test.ts +158 -0
- package/tests/types/database.test.ts +187 -0
- package/tests/workflow/audit-analyzer.test.ts +281 -0
- package/tests/workflow/audit-mode.test.ts +114 -0
- package/tests/workflow/audit-recovery.test.ts +237 -0
- package/tests/workflow/audit-reporter.test.ts +254 -0
- package/tests/workflow/audit-scanner.test.ts +270 -0
- package/tests/workflow/db-setup-runner.test.ts +211 -0
- package/tests/workflow/db-state-machine.test.ts +117 -0
|
@@ -0,0 +1,613 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Frontend ↔ Backend ↔ Database integration tests
|
|
3
|
+
* Verifies that generated FE, BE, and DB layers are properly wired
|
|
4
|
+
* and that Popeye's test runner validates fullstack projects correctly.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect } from 'vitest';
|
|
8
|
+
|
|
9
|
+
// Generated frontend templates
|
|
10
|
+
import {
|
|
11
|
+
generateAppTsxWithAdmin,
|
|
12
|
+
generateUseAdminApiHook,
|
|
13
|
+
generateDbStatusBanner,
|
|
14
|
+
generateConnectionForm,
|
|
15
|
+
generateMigrationProgress,
|
|
16
|
+
} from '../../src/generators/templates/admin-wizard-react.js';
|
|
17
|
+
|
|
18
|
+
// Generated backend templates
|
|
19
|
+
import {
|
|
20
|
+
generateFastAPIMainWithAdmin,
|
|
21
|
+
generateAdminDbRoutes,
|
|
22
|
+
generateAdminAuthMiddleware,
|
|
23
|
+
} from '../../src/generators/templates/admin-wizard-python.js';
|
|
24
|
+
|
|
25
|
+
// Generated DB templates
|
|
26
|
+
import {
|
|
27
|
+
generateDbConnection,
|
|
28
|
+
generateDbStartupHook,
|
|
29
|
+
generateDbHealthRoute,
|
|
30
|
+
generateDbConftest,
|
|
31
|
+
} from '../../src/generators/templates/database-python.js';
|
|
32
|
+
|
|
33
|
+
// Generated infra templates
|
|
34
|
+
import {
|
|
35
|
+
generateDockerComposeWithDb,
|
|
36
|
+
generateAllDockerComposeWithDb,
|
|
37
|
+
generateDbEnvExample,
|
|
38
|
+
} from '../../src/generators/templates/database-docker.js';
|
|
39
|
+
|
|
40
|
+
import {
|
|
41
|
+
generateViteConfigReact,
|
|
42
|
+
generateNginxConfig,
|
|
43
|
+
generateFrontendTest,
|
|
44
|
+
} from '../../src/generators/templates/fullstack.js';
|
|
45
|
+
|
|
46
|
+
// Test runner
|
|
47
|
+
import {
|
|
48
|
+
buildTestCommand,
|
|
49
|
+
parseTestOutput,
|
|
50
|
+
DEFAULT_TEST_COMMANDS,
|
|
51
|
+
} from '../../src/workflow/test-runner.js';
|
|
52
|
+
|
|
53
|
+
const TEST_PACKAGE = 'my_project';
|
|
54
|
+
const TEST_PROJECT = 'my-project';
|
|
55
|
+
|
|
56
|
+
// ============================================================
|
|
57
|
+
// FE → BE: API endpoint path alignment
|
|
58
|
+
// ============================================================
|
|
59
|
+
|
|
60
|
+
describe('FE → BE: Admin API endpoint paths match', () => {
|
|
61
|
+
const adminRoutes = generateAdminDbRoutes(TEST_PACKAGE);
|
|
62
|
+
const statusBanner = generateDbStatusBanner();
|
|
63
|
+
const connectionForm = generateConnectionForm();
|
|
64
|
+
const migrationProgress = generateMigrationProgress();
|
|
65
|
+
|
|
66
|
+
it('admin routes should use /api/admin/db prefix', () => {
|
|
67
|
+
expect(adminRoutes).toContain('prefix="/api/admin/db"');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('FE status banner should call /api/admin/db/status', () => {
|
|
71
|
+
expect(statusBanner).toContain('/api/admin/db/status');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('admin routes should have GET /status endpoint', () => {
|
|
75
|
+
expect(adminRoutes).toContain('@router.get("/status")');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('FE connection form should call /api/admin/db/test', () => {
|
|
79
|
+
expect(connectionForm).toContain('/api/admin/db/test');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('admin routes should have POST /test endpoint', () => {
|
|
83
|
+
expect(adminRoutes).toContain('@router.post("/test")');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('FE migration progress should call /api/admin/db/apply', () => {
|
|
87
|
+
expect(migrationProgress).toContain('/api/admin/db/apply');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('admin routes should have POST /apply endpoint', () => {
|
|
91
|
+
expect(adminRoutes).toContain('@router.post("/apply")');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('admin routes should have POST /retry endpoint', () => {
|
|
95
|
+
expect(adminRoutes).toContain('@router.post("/retry")');
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe('FE → BE: Health endpoint path alignment', () => {
|
|
100
|
+
const appTsx = generateAppTsxWithAdmin(TEST_PROJECT);
|
|
101
|
+
const mainPy = generateFastAPIMainWithAdmin(TEST_PROJECT, TEST_PACKAGE);
|
|
102
|
+
const healthRoute = generateDbHealthRoute(TEST_PACKAGE);
|
|
103
|
+
|
|
104
|
+
it('FE App.tsx should call /health endpoint', () => {
|
|
105
|
+
expect(appTsx).toContain('/health');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('BE main.py should expose /health endpoint', () => {
|
|
109
|
+
expect(mainPy).toContain('@app.get("/health")');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('BE health_db route should expose /health/db endpoint', () => {
|
|
113
|
+
expect(healthRoute).toContain('/health/db');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('BE main.py should include health_db_router', () => {
|
|
117
|
+
expect(mainPy).toContain('app.include_router(health_db_router)');
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// ============================================================
|
|
122
|
+
// FE → BE: Auth token header alignment
|
|
123
|
+
// ============================================================
|
|
124
|
+
|
|
125
|
+
describe('FE → BE: Admin token header alignment', () => {
|
|
126
|
+
const apiHook = generateUseAdminApiHook();
|
|
127
|
+
const authMiddleware = generateAdminAuthMiddleware();
|
|
128
|
+
|
|
129
|
+
it('FE should send X-Admin-Token header', () => {
|
|
130
|
+
expect(apiHook).toContain('X-Admin-Token');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('BE should check X-Admin-Token header', () => {
|
|
134
|
+
expect(authMiddleware).toContain('X-Admin-Token');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('FE should read token from VITE_ADMIN_TOKEN env var', () => {
|
|
138
|
+
expect(apiHook).toContain('VITE_ADMIN_TOKEN');
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('BE should read token from ADMIN_SETUP_TOKEN env var', () => {
|
|
142
|
+
expect(authMiddleware).toContain('ADMIN_SETUP_TOKEN');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('BE should return 403 when token is invalid', () => {
|
|
146
|
+
expect(authMiddleware).toContain('403');
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// ============================================================
|
|
151
|
+
// FE → BE: API URL configuration alignment
|
|
152
|
+
// ============================================================
|
|
153
|
+
|
|
154
|
+
describe('FE → BE: API URL configuration', () => {
|
|
155
|
+
const appTsx = generateAppTsxWithAdmin(TEST_PROJECT);
|
|
156
|
+
const apiHook = generateUseAdminApiHook();
|
|
157
|
+
const viteConfig = generateViteConfigReact();
|
|
158
|
+
|
|
159
|
+
it('App.tsx should use VITE_API_URL env var', () => {
|
|
160
|
+
expect(appTsx).toContain('VITE_API_URL');
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('useAdminApi hook should use VITE_API_URL env var', () => {
|
|
164
|
+
expect(apiHook).toContain('VITE_API_URL');
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('both should default to http://localhost:8000', () => {
|
|
168
|
+
expect(appTsx).toContain('http://localhost:8000');
|
|
169
|
+
expect(apiHook).toContain('http://localhost:8000');
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('Vite dev proxy should forward /api to backend', () => {
|
|
173
|
+
expect(viteConfig).toContain("'/api'");
|
|
174
|
+
expect(viteConfig).toContain('http://localhost:8000');
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('Vite dev server should run on port 5173', () => {
|
|
178
|
+
expect(viteConfig).toContain('port: 5173');
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// ============================================================
|
|
183
|
+
// BE → DB: Database connection wiring
|
|
184
|
+
// ============================================================
|
|
185
|
+
|
|
186
|
+
describe('BE → DB: Database connection wiring', () => {
|
|
187
|
+
const dbConnection = generateDbConnection(TEST_PACKAGE);
|
|
188
|
+
const dbStartup = generateDbStartupHook(TEST_PACKAGE);
|
|
189
|
+
const mainPy = generateFastAPIMainWithAdmin(TEST_PROJECT, TEST_PACKAGE);
|
|
190
|
+
|
|
191
|
+
it('DB connection should read DATABASE_URL from env', () => {
|
|
192
|
+
expect(dbConnection).toContain('DATABASE_URL');
|
|
193
|
+
expect(dbConnection).toContain('os.getenv');
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('DB connection should use async SQLAlchemy engine', () => {
|
|
197
|
+
expect(dbConnection).toContain('create_async_engine');
|
|
198
|
+
expect(dbConnection).toContain('AsyncSession');
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('DB startup hook should check connectivity on startup', () => {
|
|
202
|
+
expect(dbStartup).toContain('DATABASE_URL');
|
|
203
|
+
expect(dbStartup).toContain('check_db_connection');
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('main.py should include startup hook or lifecycle', () => {
|
|
207
|
+
// The main.py should set up the app with some startup wiring
|
|
208
|
+
expect(mainPy).toContain('FastAPI');
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
describe('BE → DB: Admin routes use asyncpg for direct DB access', () => {
|
|
213
|
+
const adminRoutes = generateAdminDbRoutes(TEST_PACKAGE);
|
|
214
|
+
|
|
215
|
+
it('admin /test should use asyncpg.connect()', () => {
|
|
216
|
+
expect(adminRoutes).toContain('asyncpg.connect');
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('admin /test should execute SELECT 1 for connectivity check', () => {
|
|
220
|
+
expect(adminRoutes).toContain('SELECT 1');
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('admin /apply should run alembic upgrade head', () => {
|
|
224
|
+
expect(adminRoutes).toContain('alembic upgrade head');
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('admin /status should check alembic_version table', () => {
|
|
228
|
+
expect(adminRoutes).toContain('alembic_version');
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('admin routes should convert SQLAlchemy URL to asyncpg format', () => {
|
|
232
|
+
// The admin routes need to strip "postgresql+asyncpg://" to "postgresql://"
|
|
233
|
+
expect(adminRoutes).toContain('postgresql+asyncpg://');
|
|
234
|
+
expect(adminRoutes).toContain('postgresql://');
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// ============================================================
|
|
239
|
+
// BE → DB: Health check validates real DB connectivity
|
|
240
|
+
// ============================================================
|
|
241
|
+
|
|
242
|
+
describe('BE → DB: Health check route validates DB', () => {
|
|
243
|
+
const healthRoute = generateDbHealthRoute(TEST_PACKAGE);
|
|
244
|
+
|
|
245
|
+
it('should check if DATABASE_URL is configured', () => {
|
|
246
|
+
expect(healthRoute).toContain('DATABASE_URL');
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it('should return 503 when DB is not configured', () => {
|
|
250
|
+
expect(healthRoute).toContain('503');
|
|
251
|
+
expect(healthRoute).toContain('DB_NOT_READY');
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('should execute SELECT 1 to verify connectivity', () => {
|
|
255
|
+
expect(healthRoute).toContain('SELECT 1');
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('should check alembic_version for migration status', () => {
|
|
259
|
+
expect(healthRoute).toContain('alembic_version');
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
// ============================================================
|
|
264
|
+
// CORS: Frontend ports match backend CORS origins
|
|
265
|
+
// ============================================================
|
|
266
|
+
|
|
267
|
+
describe('CORS: FE dev ports match BE CORS origins', () => {
|
|
268
|
+
const mainPy = generateFastAPIMainWithAdmin(TEST_PROJECT, TEST_PACKAGE);
|
|
269
|
+
const viteConfig = generateViteConfigReact();
|
|
270
|
+
|
|
271
|
+
it('BE CORS should allow Vite dev port 5173', () => {
|
|
272
|
+
expect(mainPy).toContain('http://localhost:5173');
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it('BE CORS should allow production port 3000', () => {
|
|
276
|
+
expect(mainPy).toContain('http://localhost:3000');
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('Vite config should use port 5173 (matching CORS)', () => {
|
|
280
|
+
expect(viteConfig).toContain('port: 5173');
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it('CORS should allow all methods and headers', () => {
|
|
284
|
+
expect(mainPy).toContain('allow_methods=["*"]');
|
|
285
|
+
expect(mainPy).toContain('allow_headers=["*"]');
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
// ============================================================
|
|
290
|
+
// Docker Compose: Service wiring FE → BE → DB
|
|
291
|
+
// ============================================================
|
|
292
|
+
|
|
293
|
+
describe('Docker Compose: Fullstack service wiring', () => {
|
|
294
|
+
const compose = generateDockerComposeWithDb(TEST_PROJECT);
|
|
295
|
+
|
|
296
|
+
it('frontend should depend on backend', () => {
|
|
297
|
+
expect(compose).toContain('depends_on:\n - backend');
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it('frontend should use backend service URL for API', () => {
|
|
301
|
+
expect(compose).toContain('VITE_API_URL=http://backend:8000');
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it('backend should depend on postgres with health condition', () => {
|
|
305
|
+
expect(compose).toContain('condition: service_healthy');
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it('backend should use postgres service in DATABASE_URL', () => {
|
|
309
|
+
expect(compose).toContain('postgres:5432');
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it('backend DATABASE_URL should use correct DB name', () => {
|
|
313
|
+
const dbName = TEST_PROJECT.replace(/-/g, '_') + '_db';
|
|
314
|
+
expect(compose).toContain(dbName);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it('postgres should have healthcheck', () => {
|
|
318
|
+
expect(compose).toContain('pg_isready');
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it('backend-dev should also depend on healthy postgres', () => {
|
|
322
|
+
// Verify the dev backend also waits for postgres
|
|
323
|
+
const devSection = compose.split('backend-dev:')[1];
|
|
324
|
+
expect(devSection).toContain('condition: service_healthy');
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
describe('Docker Compose: All project service wiring', () => {
|
|
329
|
+
const compose = generateAllDockerComposeWithDb(TEST_PROJECT);
|
|
330
|
+
|
|
331
|
+
it('should include frontend, backend, website, and postgres services', () => {
|
|
332
|
+
expect(compose).toContain('frontend:');
|
|
333
|
+
expect(compose).toContain('backend:');
|
|
334
|
+
expect(compose).toContain('website:');
|
|
335
|
+
expect(compose).toContain('postgres:');
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it('backend should depend on postgres with health condition', () => {
|
|
339
|
+
expect(compose).toContain('condition: service_healthy');
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it('all services should be on the same network', () => {
|
|
343
|
+
const networkName = `${TEST_PROJECT}-network`;
|
|
344
|
+
expect(compose).toContain(networkName);
|
|
345
|
+
// Count network references - should appear in services + network definition
|
|
346
|
+
const networkCount = (compose.match(new RegExp(networkName, 'g')) || []).length;
|
|
347
|
+
expect(networkCount).toBeGreaterThanOrEqual(5); // 4 services + 1 definition
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it('frontend API URL should point to backend service', () => {
|
|
351
|
+
expect(compose).toContain('VITE_API_URL=http://backend:8000');
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
// ============================================================
|
|
356
|
+
// Nginx: Proxy paths match backend routes
|
|
357
|
+
// ============================================================
|
|
358
|
+
|
|
359
|
+
describe('Nginx: Proxy config matches BE route structure', () => {
|
|
360
|
+
const nginx = generateNginxConfig();
|
|
361
|
+
const mainPy = generateFastAPIMainWithAdmin(TEST_PROJECT, TEST_PACKAGE);
|
|
362
|
+
|
|
363
|
+
it('nginx should proxy /api to backend:8000', () => {
|
|
364
|
+
expect(nginx).toContain('location /api');
|
|
365
|
+
expect(nginx).toContain('proxy_pass http://backend:8000');
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
it('admin routes start with /api prefix (matching nginx proxy)', () => {
|
|
369
|
+
// Admin routes use prefix="/api/admin/db" which falls under nginx /api proxy
|
|
370
|
+
const adminRoutes = generateAdminDbRoutes(TEST_PACKAGE);
|
|
371
|
+
expect(adminRoutes).toContain('prefix="/api/admin/db"');
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
it('nginx should handle SPA routing (catch-all to index.html)', () => {
|
|
375
|
+
expect(nginx).toContain('try_files $uri $uri/ /index.html');
|
|
376
|
+
});
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
// ============================================================
|
|
380
|
+
// Env vars: FE and BE env examples are consistent
|
|
381
|
+
// ============================================================
|
|
382
|
+
|
|
383
|
+
describe('Env vars: Backend and frontend env alignment', () => {
|
|
384
|
+
const backendEnv = generateDbEnvExample(TEST_PROJECT);
|
|
385
|
+
|
|
386
|
+
it('backend env should have DATABASE_URL for DB connection', () => {
|
|
387
|
+
expect(backendEnv).toContain('DATABASE_URL=postgresql+asyncpg://');
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
it('backend env should have ADMIN_SETUP_TOKEN for admin auth', () => {
|
|
391
|
+
expect(backendEnv).toContain('ADMIN_SETUP_TOKEN=');
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
it('DATABASE_URL format should match admin route URL conversion', () => {
|
|
395
|
+
// Backend env uses postgresql+asyncpg:// format
|
|
396
|
+
// Admin routes convert this to postgresql:// for asyncpg
|
|
397
|
+
expect(backendEnv).toContain('postgresql+asyncpg://');
|
|
398
|
+
const adminRoutes = generateAdminDbRoutes(TEST_PACKAGE);
|
|
399
|
+
expect(adminRoutes).toContain('.replace("postgresql+asyncpg://", "postgresql://")');
|
|
400
|
+
});
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
// ============================================================
|
|
404
|
+
// Generated test files: Backend tests match endpoints
|
|
405
|
+
// ============================================================
|
|
406
|
+
|
|
407
|
+
describe('Generated backend test validates real endpoints', () => {
|
|
408
|
+
const frontendTest = generateFrontendTest(TEST_PROJECT);
|
|
409
|
+
const appTsx = generateAppTsxWithAdmin(TEST_PROJECT);
|
|
410
|
+
|
|
411
|
+
it('generated FE test should check for project name text', () => {
|
|
412
|
+
expect(frontendTest).toContain(TEST_PROJECT);
|
|
413
|
+
// App.tsx should also contain the project name
|
|
414
|
+
expect(appTsx).toContain(TEST_PROJECT);
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
it('generated FE test should check for loading state', () => {
|
|
418
|
+
expect(frontendTest).toContain('Checking...');
|
|
419
|
+
// App.tsx should also have this text
|
|
420
|
+
expect(appTsx).toContain('Checking...');
|
|
421
|
+
});
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
describe('Generated DB conftest provides proper test fixtures', () => {
|
|
425
|
+
const conftest = generateDbConftest(TEST_PACKAGE);
|
|
426
|
+
|
|
427
|
+
it('should use TEST_DATABASE_URL (not production DATABASE_URL)', () => {
|
|
428
|
+
expect(conftest).toContain('TEST_DATABASE_URL');
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
it('should create async engine for tests', () => {
|
|
432
|
+
expect(conftest).toContain('create_async_engine');
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
it('should provide db_session fixture', () => {
|
|
436
|
+
expect(conftest).toContain('db_session');
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
it('should create tables before tests and drop after', () => {
|
|
440
|
+
expect(conftest).toContain('Base.metadata.create_all');
|
|
441
|
+
expect(conftest).toContain('Base.metadata.drop_all');
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
it('should import models from the correct package', () => {
|
|
445
|
+
expect(conftest).toContain(`from src.${TEST_PACKAGE}.database.models import Base`);
|
|
446
|
+
});
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
// ============================================================
|
|
450
|
+
// Popeye test runner: Fullstack/All project support
|
|
451
|
+
// ============================================================
|
|
452
|
+
|
|
453
|
+
describe('Popeye test runner: Fullstack project commands', () => {
|
|
454
|
+
it('fullstack should use test:all command', () => {
|
|
455
|
+
expect(DEFAULT_TEST_COMMANDS.fullstack).toBe('npm run test:all');
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
it('all should use test:all command', () => {
|
|
459
|
+
expect(DEFAULT_TEST_COMMANDS.all).toBe('npm run test:all');
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
it('buildTestCommand should return test:all for fullstack', () => {
|
|
463
|
+
const cmd = buildTestCommand({ language: 'fullstack' });
|
|
464
|
+
expect(cmd).toBe('npm run test:all');
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
it('buildTestCommand should return test:all for all', () => {
|
|
468
|
+
const cmd = buildTestCommand({ language: 'all' });
|
|
469
|
+
expect(cmd).toBe('npm run test:all');
|
|
470
|
+
});
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
describe('Popeye test runner: Fullstack output parsing', () => {
|
|
474
|
+
it('should parse combined pytest + jest output for fullstack', () => {
|
|
475
|
+
const output = `
|
|
476
|
+
=== Backend Tests ===
|
|
477
|
+
============================= test session starts ==============================
|
|
478
|
+
tests/test_main.py .. [100%]
|
|
479
|
+
============================== 2 passed in 0.54s ==============================
|
|
480
|
+
|
|
481
|
+
=== Frontend Tests ===
|
|
482
|
+
PASS tests/App.test.tsx
|
|
483
|
+
App
|
|
484
|
+
v renders the project name (25 ms)
|
|
485
|
+
v shows loading state initially (12 ms)
|
|
486
|
+
|
|
487
|
+
Tests: 2 passed, 2 total
|
|
488
|
+
`;
|
|
489
|
+
const result = parseTestOutput(output, 'fullstack');
|
|
490
|
+
|
|
491
|
+
expect(result.success).toBe(true);
|
|
492
|
+
expect(result.passed).toBe(4); // 2 pytest + 2 jest
|
|
493
|
+
expect(result.failed).toBe(0);
|
|
494
|
+
expect(result.total).toBe(4);
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
it('should detect backend failures in fullstack output', () => {
|
|
498
|
+
const output = `
|
|
499
|
+
=== Backend Tests ===
|
|
500
|
+
FAILED tests/test_main.py::test_health_check
|
|
501
|
+
============================== 1 failed, 1 passed in 0.54s ==============================
|
|
502
|
+
|
|
503
|
+
=== Frontend Tests ===
|
|
504
|
+
Tests: 2 passed, 2 total
|
|
505
|
+
`;
|
|
506
|
+
const result = parseTestOutput(output, 'fullstack');
|
|
507
|
+
|
|
508
|
+
expect(result.success).toBe(false);
|
|
509
|
+
expect(result.failed).toBe(1);
|
|
510
|
+
expect(result.passed).toBe(3); // 1 pytest + 2 jest
|
|
511
|
+
expect(result.failedTests).toContain('tests/test_main.py::test_health_check');
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
it('should detect frontend failures in fullstack output', () => {
|
|
515
|
+
// Note: pytest regex `(\d+)\s+failed` can match jest "1 failed" line too,
|
|
516
|
+
// causing double-count. The parser counts pytest failed (1) + jest failed (1) = 2.
|
|
517
|
+
const output = `
|
|
518
|
+
=== Backend Tests ===
|
|
519
|
+
============================== 2 passed in 0.54s ==============================
|
|
520
|
+
|
|
521
|
+
=== Frontend Tests ===
|
|
522
|
+
FAIL tests/App.test.tsx
|
|
523
|
+
App
|
|
524
|
+
✕ renders the project name (25 ms)
|
|
525
|
+
v shows loading state initially (12 ms)
|
|
526
|
+
|
|
527
|
+
Tests: 1 failed, 1 passed, 2 total
|
|
528
|
+
`;
|
|
529
|
+
const result = parseTestOutput(output, 'fullstack');
|
|
530
|
+
|
|
531
|
+
expect(result.success).toBe(false);
|
|
532
|
+
expect(result.passed).toBe(3); // 2 pytest + 1 jest
|
|
533
|
+
expect(result.failed).toBe(2); // 1 jest + 1 double-counted by pytest regex
|
|
534
|
+
expect(result.failedTests?.some((t) => t.includes('renders the project name'))).toBe(true);
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
it('should parse all project output (multiple jest outputs)', () => {
|
|
538
|
+
const output = `
|
|
539
|
+
=== Backend Tests ===
|
|
540
|
+
============================== 3 passed in 0.54s ==============================
|
|
541
|
+
|
|
542
|
+
=== Frontend Tests ===
|
|
543
|
+
Tests: 2 passed, 2 total
|
|
544
|
+
|
|
545
|
+
=== Website Tests ===
|
|
546
|
+
Tests: 4 passed, 4 total
|
|
547
|
+
`;
|
|
548
|
+
const result = parseTestOutput(output, 'all');
|
|
549
|
+
|
|
550
|
+
expect(result.success).toBe(true);
|
|
551
|
+
expect(result.passed).toBe(9); // 3 pytest + 2 jest + 4 jest
|
|
552
|
+
expect(result.failed).toBe(0);
|
|
553
|
+
});
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
// ============================================================
|
|
557
|
+
// FE ↔ BE ↔ DB: Full-stack data flow alignment
|
|
558
|
+
// ============================================================
|
|
559
|
+
|
|
560
|
+
describe('Full-stack data flow: Admin wizard lifecycle', () => {
|
|
561
|
+
const apiHook = generateUseAdminApiHook();
|
|
562
|
+
const adminRoutes = generateAdminDbRoutes(TEST_PACKAGE);
|
|
563
|
+
const statusBanner = generateDbStatusBanner();
|
|
564
|
+
|
|
565
|
+
it('FE sends database_url in test request body', () => {
|
|
566
|
+
expect(generateConnectionForm()).toContain('database_url');
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
it('BE /test expects database_url field', () => {
|
|
570
|
+
expect(adminRoutes).toContain('database_url');
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
it('FE sends database_url in apply request body', () => {
|
|
574
|
+
expect(generateMigrationProgress()).toContain('database_url');
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
it('BE /apply expects database_url field', () => {
|
|
578
|
+
expect(adminRoutes).toContain('database_url');
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
it('BE /status returns status field that FE checks', () => {
|
|
582
|
+
// BE returns { "status": "ready"|"unconfigured"|... }
|
|
583
|
+
expect(adminRoutes).toContain('"status"');
|
|
584
|
+
// FE checks status.status
|
|
585
|
+
expect(statusBanner).toContain('status.status');
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
it('FE hides banner when status is ready (matches BE response)', () => {
|
|
589
|
+
expect(statusBanner).toContain("status === 'ready'");
|
|
590
|
+
});
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
describe('Full-stack data flow: DB URL format consistency', () => {
|
|
594
|
+
const backendEnv = generateDbEnvExample(TEST_PROJECT);
|
|
595
|
+
const dockerCompose = generateDockerComposeWithDb(TEST_PROJECT);
|
|
596
|
+
const dbConnection = generateDbConnection(TEST_PACKAGE);
|
|
597
|
+
const adminRoutes = generateAdminDbRoutes(TEST_PACKAGE);
|
|
598
|
+
|
|
599
|
+
it('all DATABASE_URL values use postgresql+asyncpg:// scheme', () => {
|
|
600
|
+
expect(backendEnv).toContain('postgresql+asyncpg://');
|
|
601
|
+
expect(dockerCompose).toContain('postgresql+asyncpg://');
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
it('DB connection module reads DATABASE_URL', () => {
|
|
605
|
+
expect(dbConnection).toContain('DATABASE_URL');
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
it('admin routes convert URL scheme for asyncpg direct use', () => {
|
|
609
|
+
expect(adminRoutes).toContain(
|
|
610
|
+
'replace("postgresql+asyncpg://", "postgresql://")'
|
|
611
|
+
);
|
|
612
|
+
});
|
|
613
|
+
});
|