@mars-stack/cli 7.0.5 → 7.0.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mars-stack/cli",
3
- "version": "7.0.5",
3
+ "version": "7.0.6",
4
4
  "description": "MARS CLI: scaffold, configure, and maintain SaaS apps",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -2,6 +2,26 @@
2
2
 
3
3
  Browser-level tests for user-visible behaviour. Run: `yarn test:e2e` from the project root (`playwright.config.ts`).
4
4
 
5
+ ## Smoke (MARS-044)
6
+
7
+ | Spec | What it covers |
8
+ |------|----------------|
9
+ | `smoke.spec.ts` | Seeded user (`user@example.com` from `scripts/seed.ts`) sign-in → `/settings` → PATCH display name (CSRF); PATCH without CSRF returns 403; mobile viewport sign-in |
10
+
11
+ Monorepo CI runs this on pull requests via the `template-e2e` job in `.github/workflows/ci.yml` (Postgres service + `E2E_USE_CI_SERVER=1` + `scripts/start-e2e-server.mjs`).
12
+
13
+ ### CI recipe for generated apps
14
+
15
+ Copy the **`template-e2e`** job from the Mars repo’s `.github/workflows/ci.yml` into your project workflow, or mirror its steps:
16
+
17
+ 1. **Postgres service** (or your own `DATABASE_URL` to a disposable DB).
18
+ 2. **Build:** `yarn build` from the app root (after `yarn install`).
19
+ 3. **Env for Playwright:** `CI=true`, `E2E_USE_CI_SERVER=1`, `JWT_SECRET` (≥32 chars), `DATABASE_URL`, `NODE_ENV=production`.
20
+ 4. **Browsers:** `npx playwright install --with-deps chromium` in the app directory.
21
+ 5. **Tests:** `yarn test:e2e` in the app directory (uses `scripts/start-e2e-server.mjs` to `db push`, `db seed`, then `next start`).
22
+
23
+ If `DATABASE_URL` is missing or wrong, API routes fail at runtime and mutations can surface as 500s instead of deterministic CSRF 403s — fix env before interpreting smoke failures.
24
+
5
25
  ## Monorepo constraint (MARS-041)
6
26
 
7
27
  The Mars **template package** in this repo is the pre-generator baseline. Most `mars generate` features are **not** present under `template/src/features/` until generators run (as in `scripts/test-scaffold-matrix.ts` with the **kitchen-sink** fixture). Full CLI catalog E2E in CI therefore requires **Option A** from MARS-041: scaffold a max-feature app to a working directory, provision `JWT_SECRET` / `DATABASE_URL`, then run Playwright from **that** tree. Specs in this folder exercise whatever the committed template actually contains (today: auth, public, API baseline); the README inventory below tracks **target** coverage once the scaffolded-app E2E job exists.
@@ -0,0 +1,54 @@
1
+ import { test, expect } from '@playwright/test';
2
+
3
+ /**
4
+ * Created by `template/scripts/seed.ts` (run in CI via `start-e2e-server.mjs` before `next start`).
5
+ * Local: run `yarn db:seed` after schema is applied, or use `yarn dev` (ensure-db) then seed.
6
+ */
7
+ const SEEDED_USER_EMAIL = 'user@example.com';
8
+ const SEEDED_USER_PASSWORD = 'Password123!';
9
+
10
+ async function signInWithSeededUser(page: import('@playwright/test').Page): Promise<void> {
11
+ await page.goto('/sign-in');
12
+ await page.getByLabel('Email Address').fill(SEEDED_USER_EMAIL);
13
+ await page.getByLabel('Password', { exact: true }).fill(SEEDED_USER_PASSWORD);
14
+ await page.getByRole('button', { name: /^sign in$/i }).click();
15
+ await expect(page).toHaveURL(/\/dashboard/);
16
+ }
17
+
18
+ test.describe('Critical path smoke (MARS-044)', () => {
19
+ test.describe.configure({ timeout: 120_000 });
20
+
21
+ test('sign-in → settings PATCH display name with CSRF (protected mutation)', async ({ page }) => {
22
+ await signInWithSeededUser(page);
23
+
24
+ await page.goto('/settings');
25
+ await expect(page.getByRole('heading', { name: 'Settings' })).toBeVisible();
26
+
27
+ const uniqueName = `Smoke User ${Date.now()}`;
28
+ await page.locator('#display-name').fill(uniqueName);
29
+ await page.getByRole('button', { name: 'Save Name' }).click();
30
+ await expect(page.getByText('Display name updated.')).toBeVisible();
31
+ });
32
+
33
+ test('PATCH profile without CSRF headers returns 403', async ({ page }) => {
34
+ await signInWithSeededUser(page);
35
+
36
+ const status = await page.evaluate(async () => {
37
+ const response = await fetch('/api/protected/user/profile', {
38
+ method: 'PATCH',
39
+ credentials: 'include',
40
+ headers: { 'Content-Type': 'application/json' },
41
+ body: JSON.stringify({ name: 'Should Not Apply' }),
42
+ });
43
+ return response.status;
44
+ });
45
+
46
+ expect(status).toBe(403);
47
+ });
48
+
49
+ test('sign-in path is usable at ~375px width', async ({ page }) => {
50
+ await page.setViewportSize({ width: 375, height: 667 });
51
+ await signInWithSeededUser(page);
52
+ await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
53
+ });
54
+ });
@@ -1,4 +1,15 @@
1
1
  import { defineConfig, devices } from '@playwright/test';
2
+ import dotenv from 'dotenv';
3
+ import { dirname, resolve } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+
6
+ const __dirname = dirname(fileURLToPath(import.meta.url));
7
+
8
+ /** So `next start` (production) sees DATABASE_URL from local dev setup; CI uses explicit env. */
9
+ if (!process.env.CI && process.env.E2E_USE_CI_SERVER !== '1') {
10
+ dotenv.config({ path: resolve(__dirname, '.env.development.local') });
11
+ dotenv.config({ path: resolve(__dirname, '.env.local') });
12
+ }
2
13
 
3
14
  export default defineConfig({
4
15
  testDir: './e2e',
@@ -22,10 +33,20 @@ export default defineConfig({
22
33
  use: { ...devices['iPhone 14'] },
23
34
  },
24
35
  ],
25
- webServer: {
26
- command: 'yarn build && yarn start',
27
- url: 'http://localhost:3000',
28
- reuseExistingServer: !process.env.CI,
29
- timeout: 120_000,
30
- },
36
+ webServer:
37
+ process.env.E2E_USE_CI_SERVER === '1'
38
+ ? {
39
+ command: 'node scripts/start-e2e-server.mjs',
40
+ url: 'http://localhost:3000',
41
+ reuseExistingServer: false,
42
+ timeout: 180_000,
43
+ env: { ...process.env, MARS_CI_TEMPLATE_E2E: '1' },
44
+ }
45
+ : {
46
+ command: 'node scripts/playwright-web-server.mjs',
47
+ url: 'http://localhost:3000',
48
+ // Always boot a fresh server so DATABASE_URL from ensure-db is not shadowed by an old `next start`.
49
+ reuseExistingServer: false,
50
+ timeout: 300_000,
51
+ },
31
52
  });
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Playwright webServer for local `yarn test:e2e` (not CI).
4
+ * Runs ensure-db then `yarn dev` so Next loads `.env.development.local`.
5
+ * CI uses `E2E_USE_CI_SERVER=1` and `start-e2e-server.mjs` instead.
6
+ */
7
+ import { spawn } from 'node:child_process';
8
+ import { dirname, resolve } from 'node:path';
9
+ import { fileURLToPath } from 'node:url';
10
+
11
+ const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
12
+
13
+ const child = spawn('sh', ['-c', 'node scripts/ensure-db.mjs && yarn dev'], {
14
+ cwd: ROOT,
15
+ stdio: 'inherit',
16
+ shell: false,
17
+ env: process.env,
18
+ });
19
+
20
+ child.on('exit', (code) => process.exit(code ?? 0));
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Production server bootstrap for Playwright E2E in CI.
4
+ * Local runs use `yarn build && yarn start` from playwright.config (DB already provisioned via `yarn dev` or manual db push).
5
+ */
6
+ import { spawnSync } from 'node:child_process';
7
+ import { fileURLToPath } from 'node:url';
8
+ import { dirname, resolve } from 'node:path';
9
+
10
+ const __dirname = dirname(fileURLToPath(import.meta.url));
11
+ const ROOT = resolve(__dirname, '..');
12
+
13
+ function run(cmd, args) {
14
+ const r = spawnSync(cmd, args, {
15
+ cwd: ROOT,
16
+ stdio: 'inherit',
17
+ env: {
18
+ ...process.env,
19
+ NODE_ENV: 'production',
20
+ MARS_CI_TEMPLATE_E2E: '1',
21
+ },
22
+ });
23
+ if (r.status !== 0) {
24
+ process.exit(r.status ?? 1);
25
+ }
26
+ }
27
+
28
+ if (!process.env.DATABASE_URL?.trim()) {
29
+ console.error('start-e2e-server: DATABASE_URL must be set for CI E2E.');
30
+ process.exit(1);
31
+ }
32
+
33
+ run('npx', ['prisma', 'db', 'push']);
34
+ run('npx', ['prisma', 'db', 'seed']);
35
+ run('npx', ['next', 'start', '-p', '3000']);