沉淀生产收口规约与清理证据(07-15-production-trust-closeout-cleanup)
这个提交包含在:
@@ -169,3 +169,128 @@ if (acknowledgementCount > 0) {
|
||||
throw new AppError({ code: "VALID_001", message: "参数校验失败", statusCode: 409 });
|
||||
}
|
||||
```
|
||||
|
||||
## Scenario: Safe Development Seed
|
||||
|
||||
### 1. Scope / Trigger
|
||||
|
||||
- Trigger: changing or running `server/src/scripts/seed-dev.js`, its safety
|
||||
guard, development account defaults or the `seed:dev` npm command.
|
||||
|
||||
### 2. Signatures
|
||||
|
||||
- Command: `SEED_DEV_ALLOW=1 SEED_DEV_DATABASE_CONFIRM=<db> npm run seed:dev`.
|
||||
- Optional reset: `SEED_DEV_RESET_PASSWORDS=1`.
|
||||
- Target: `DATABASE_URL` parsed with the same `pg` semantics used by Prisma's
|
||||
PostgreSQL adapter.
|
||||
|
||||
### 3. Contracts
|
||||
|
||||
- `NODE_ENV=production` is permanently refused.
|
||||
- The explicit allow switch and exact database-name confirmation are both
|
||||
required.
|
||||
- The effective PostgreSQL host, including query-string overrides, must be
|
||||
loopback; checking only the URI authority is insufficient.
|
||||
- Existing passwords are preserved by default. Newly created accounts and
|
||||
explicitly rotated accounts set `must_change_password=true`.
|
||||
- The guard and seed logs never print credentials, complete targets or account
|
||||
passwords.
|
||||
|
||||
### 4. Validation & Error Matrix
|
||||
|
||||
- Production -> `production_forbidden`.
|
||||
- Missing allow switch -> `explicit_allow_required`.
|
||||
- Invalid/non-PostgreSQL URL -> `database_url_invalid` or
|
||||
`database_protocol_invalid`.
|
||||
- Effective non-loopback host, including `?host=...` ->
|
||||
`database_host_not_local`.
|
||||
- Missing/mismatched confirmation -> `database_confirmation_mismatch`.
|
||||
|
||||
### 5. Good/Base/Bad Cases
|
||||
|
||||
- Good: loopback test database, exact confirmation, new accounts require a
|
||||
password change.
|
||||
- Base: rerun without reset preserves existing password hashes.
|
||||
- Bad: accept `postgresql://[email protected]/db?host=remote.internal` because only
|
||||
`URL.hostname` was checked.
|
||||
|
||||
### 6. Tests Required
|
||||
|
||||
- Pure guard tests for production, missing allow, remote authority, host-query
|
||||
override, exact confirmation and redacted errors.
|
||||
- Unit tests for existing-password preservation, explicit rotation and new
|
||||
account first-login change.
|
||||
- Do not validate refusal paths by connecting to a real database.
|
||||
|
||||
### 7. Wrong vs Correct
|
||||
|
||||
#### Wrong
|
||||
|
||||
```js
|
||||
if (new URL(env.DATABASE_URL).hostname === "127.0.0.1") allowSeed();
|
||||
```
|
||||
|
||||
#### Correct
|
||||
|
||||
```js
|
||||
const client = new Client({ connectionString: env.DATABASE_URL });
|
||||
if (!LOOPBACK_HOSTS.has(client.host)) refuse("database_host_not_local");
|
||||
```
|
||||
|
||||
## Scenario: Database-Free App Contract Tests
|
||||
|
||||
### 1. Scope / Trigger
|
||||
|
||||
- Trigger: unit tests call the production `buildApp()` factory to verify health,
|
||||
readiness, static serving or other app-level contracts in CI without a DB.
|
||||
|
||||
### 2. Signatures
|
||||
|
||||
- Production: `buildApp()`.
|
||||
- Test seam: `buildApp({ prismaPlugin, startJobs })`.
|
||||
|
||||
### 3. Contracts
|
||||
|
||||
- Omitted dependencies always use the production Prisma plugin and job starter.
|
||||
- Unit tests inject a `fastify-plugin`-wrapped Prisma decorator and a no-op job
|
||||
starter; they must not rely on ESM `vi.mock` intercepting a CJS `require`.
|
||||
- App-level test doubles expose only methods used by the tested requests.
|
||||
- `DATABASE_URL='' JWT_SECRET=''` must not make isolated app tests access a
|
||||
developer `.env` or local PostgreSQL instance.
|
||||
|
||||
### 4. Validation & Error Matrix
|
||||
|
||||
- Production call with missing DB target -> normal startup refusal.
|
||||
- Test call with injected Prisma plugin -> no production client construction.
|
||||
- Test double missing a method exercised by the request -> focused unit failure.
|
||||
- Injected dependencies leaking into `buildApp()` callers without options ->
|
||||
contract regression.
|
||||
|
||||
### 5. Good/Base/Bad Cases
|
||||
|
||||
- Good: health/static tests inject Prisma and pass with empty DB/JWT process
|
||||
variables after setting their own test JWT.
|
||||
- Base: API/Worker call `buildApp()` without options and retain production wiring.
|
||||
- Bad: a hoisted `vi.mock("../prismaClient")` appears green only because a local
|
||||
`.env` database is reachable.
|
||||
|
||||
### 6. Tests Required
|
||||
|
||||
- Run health and static tests with `DATABASE_URL='' JWT_SECRET=''`.
|
||||
- Run the full `scripts/check-minimal.sh` under the same empty environment.
|
||||
- Keep the production default path covered by normal startup/smoke checks.
|
||||
|
||||
### 7. Wrong vs Correct
|
||||
|
||||
#### Wrong
|
||||
|
||||
```js
|
||||
vi.mock("../../src/prismaClient", () => ({ createPrismaClient: fake }));
|
||||
await buildApp(); // CJS plugin may still construct the real client
|
||||
```
|
||||
|
||||
#### Correct
|
||||
|
||||
```js
|
||||
await buildApp({ prismaPlugin: testPrismaPlugin, startJobs: noOpStartJobs });
|
||||
```
|
||||
|
||||
在新工单中引用
屏蔽一个用户