create-qpq-app 0.1.19 → 0.1.21
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/lib/commonjs/steps/006_applyDomain.js +3 -8
- package/lib/commonjs/steps/006_applyDomain.js.map +1 -1
- package/lib/commonjs/steps/013_printNextSteps.js +1 -1
- package/lib/commonjs/steps/013_printNextSteps.js.map +1 -1
- package/lib/esm/steps/006_applyDomain.js +3 -8
- package/lib/esm/steps/006_applyDomain.js.map +1 -1
- package/lib/esm/steps/013_printNextSteps.js +1 -1
- package/lib/esm/steps/013_printNextSteps.js.map +1 -1
- package/package.json +2 -2
- package/template/README.md +6 -3
- package/template/apps/qpqjs/account.qpq.ts +5 -13
- package/template/apps/qpqjs/bootstrap.qpq.ts +29 -14
- package/template/apps/qpqjs/deploy.config.json +0 -1
- package/template/apps/qpqjs/packages/constants/src/domain.ts +3 -1
- package/template/apps/qpqjs/packages/service-utils/src/defineQpqjsService.ts +15 -9
- package/template/apps/qpqjs/services/design/views/src/components/LandingPage/components/Infrastructure.tsx +1 -2
- package/template/apps/qpqjs/services/shell/service/src/infrastructure.ts +1 -4
- package/template/apps/qpqjs/services/test/models/package.json +2 -1
- package/template/apps/qpqjs/services/test/models/src/EchoRequest.ts +8 -0
- package/template/apps/qpqjs/services/test/models/src/EchoResponse.ts +9 -0
- package/template/apps/qpqjs/services/test/models/src/SmokeRun.ts +11 -9
- package/template/apps/qpqjs/services/test/models/src/SmokeRunStarted.ts +8 -0
- package/template/apps/qpqjs/services/test/models/src/SmokeRunSummary.ts +10 -6
- package/template/apps/qpqjs/services/test/models/src/SmokeRunWithSummary.ts +9 -5
- package/template/apps/qpqjs/services/test/models/src/SmokeTestResult.ts +11 -9
- package/template/apps/qpqjs/services/test/models/src/index.ts +3 -0
- package/template/apps/qpqjs/services/test/service/src/echo/config/defineEcho.ts +9 -0
- package/template/apps/qpqjs/services/test/service/src/echo/controller/echo.ts +32 -0
- package/template/apps/qpqjs/services/test/service/src/echo/controller/index.ts +1 -0
- package/template/apps/qpqjs/services/test/service/src/infrastructure.ts +3 -0
- package/template/apps/qpqjs/services/test/service/src/smoke/config/defineSmoke.ts +83 -18
- package/template/apps/qpqjs/services/test/service/src/smoke/constants/index.ts +1 -0
- package/template/apps/qpqjs/services/test/service/src/smoke/constants/smokeEventDoc.ts +27 -0
- package/template/apps/qpqjs/services/test/service/src/smoke/constants/smokeRunQueue.ts +4 -4
- package/template/apps/qpqjs/services/test/service/src/smoke/controller/askRunSmokeTests.ts +5 -1
- package/template/apps/qpqjs/services/test/service/src/smoke/data/askRecordSmokeTestResult.ts +32 -0
- package/template/apps/qpqjs/services/test/service/src/smoke/data/askSetSmokeRunOutcome.ts +24 -0
- package/template/apps/qpqjs/services/test/service/src/smoke/data/index.ts +2 -0
- package/template/apps/qpqjs/services/test/service/src/smoke/eventDoc/index.ts +1 -0
- package/template/apps/qpqjs/services/test/service/src/smoke/eventDoc/smokeProbeDocDefinition.ts +74 -0
- package/template/apps/qpqjs/services/test/service/src/smoke/index.ts +1 -0
- package/template/apps/qpqjs/services/test/service/src/smoke/logic/smokeRun/askExecuteSmokeTest.ts +97 -0
- package/template/apps/qpqjs/services/test/service/src/smoke/logic/smokeRun/askStartSmokeRun.ts +18 -10
- package/template/apps/qpqjs/services/test/service/src/smoke/logic/smokeRun/index.ts +1 -1
- package/template/apps/qpqjs/services/test/service/src/smoke/models/SmokeEventDocAppendQueueEvent.ts +15 -0
- package/template/apps/qpqjs/services/test/service/src/smoke/models/SmokeEventDocMark.ts +8 -0
- package/template/apps/qpqjs/services/test/service/src/smoke/models/SmokeTestRequestedQueueEvent.ts +12 -0
- package/template/apps/qpqjs/services/test/service/src/smoke/models/index.ts +3 -1
- package/template/apps/qpqjs/services/test/service/src/smoke/queue/index.ts +2 -1
- package/template/apps/qpqjs/services/test/service/src/smoke/queue/onSmokeEventDocAppend.ts +64 -0
- package/template/apps/qpqjs/services/test/service/src/smoke/queue/onSmokeTestRequested.ts +17 -0
- package/template/apps/qpqjs/services/test/service/src/smoke/tests/eventDoc/askAssertContiguousEventIds.ts +18 -0
- package/template/apps/qpqjs/services/test/service/src/smoke/tests/eventDoc/askAssertSmokeProbeDocState.ts +32 -0
- package/template/apps/qpqjs/services/test/service/src/smoke/tests/eventDoc/askAwaitSmokeEventDocLog.ts +35 -0
- package/template/apps/qpqjs/services/test/service/src/smoke/tests/eventDoc/askRunEventDocConcurrentAppendTest.ts +82 -0
- package/template/apps/qpqjs/services/test/service/src/smoke/tests/eventDoc/askRunEventDocInterleavedBatchAppendTest.ts +110 -0
- package/template/apps/qpqjs/services/test/service/src/smoke/tests/eventDoc/index.ts +5 -0
- package/template/apps/qpqjs/services/test/service/src/smoke/tests/index.ts +2 -0
- package/template/apps/qpqjs/services/test/service/src/smoke/tests/openApi/askRunEchoRoundTripTest.ts +45 -0
- package/template/apps/qpqjs/services/test/service/src/smoke/tests/openApi/askRunOpenApiDocumentTest.ts +42 -0
- package/template/apps/qpqjs/services/test/service/src/smoke/tests/openApi/index.ts +2 -0
- package/template/apps/qpqjs/services/test/service/src/smoke/tests/smokeTestRegistry.ts +18 -2
- package/template/apps/todo/deploy.config.json +0 -1
- package/template/apps/todo/packages/constants/src/domain.ts +2 -1
- package/template/apps/todo/packages/service-utils/src/defineTodoService.ts +5 -6
- package/template/apps/todo/services/shell/service/src/infrastructure.ts +1 -3
- package/template/docusaurus/docs/actions/core/key-value-store/ask-key-value-store-upsert-with-retry.md +2 -2
- package/template/docusaurus/docs/actions/core/key-value-store/ask-key-value-store-upsert.md +2 -1
- package/template/docusaurus/docs/actions/features/event-doc/ask-event-doc-append-server-event.md +1 -1
- package/template/docusaurus/docs/actions/features/event-doc/ask-event-doc-create.md +1 -1
- package/template/docusaurus/docs/actions/features/event-doc/ask-event-doc-event-append.md +18 -12
- package/template/docusaurus/docs/actions/features/event-doc/ask-event-doc-event-list.md +4 -4
- package/template/docusaurus/docs/actions/features/event-doc/ask-event-doc-event-write.md +6 -6
- package/template/docusaurus/docs/actions/features/event-doc/ask-event-doc-get-by-id.md +2 -2
- package/template/docusaurus/docs/actions/features/event-doc/ask-event-doc-get-draft.md +2 -2
- package/template/docusaurus/docs/actions/webserver/dns/ask-dns-list.md +9 -8
- package/template/docusaurus/docs/actions/webserver/dns/ask-dns-resolve-hosts.md +42 -0
- package/template/docusaurus/docs/actions/webserver/open-api/_category_.json +1 -0
- package/template/docusaurus/docs/actions/webserver/open-api/ask-open-api-get-document.md +41 -0
- package/template/docusaurus/docs/config/config-aws/account-github-oidc-provider.md +31 -0
- package/template/docusaurus/docs/config/config-aws/domain-certificate.md +16 -32
- package/template/docusaurus/docs/config/config-aws/email-sender-allow-list.md +7 -14
- package/template/docusaurus/docs/config/config-aws/github-deploy-role.md +68 -0
- package/template/docusaurus/docs/config/core/user-directory.md +2 -3
- package/template/docusaurus/docs/config/features/admin-settings.md +2 -6
- package/template/docusaurus/docs/config/features/event-doc-summary.md +2 -2
- package/template/docusaurus/docs/config/features/open-api-routes.md +82 -0
- package/template/docusaurus/docs/config/features/tenanted-web-socket-queue.md +2 -3
- package/template/docusaurus/docs/config/features/web-socket-queue.md +1 -5
- package/template/docusaurus/docs/config/webserver/api.md +6 -9
- package/template/docusaurus/docs/config/webserver/cache.md +1 -1
- package/template/docusaurus/docs/config/webserver/dns.md +25 -17
- package/template/docusaurus/docs/config/webserver/domain-proxy.md +1 -5
- package/template/docusaurus/docs/config/webserver/email-sender.md +9 -15
- package/template/docusaurus/docs/config/webserver/route.md +1 -0
- package/template/docusaurus/docs/config/webserver/seo.md +1 -1
- package/template/docusaurus/docs/config/webserver/web-entry.md +0 -5
- package/template/docusaurus/docs/config/webserver/websocket.md +3 -7
- package/template/docusaurus/docs/domains.md +76 -0
- package/template/docusaurus/docs/use-cases.md +0 -6
- package/template/docusaurus/docusaurus.config.ts +38 -14
- package/template/docusaurus/sidebars.ts +5 -5
- package/template/docusaurus/src/components/HomepageFeatures/index.tsx +2 -2
- package/template/scripts/deployedSmoke.mjs +82 -23
- package/template/scripts/smoke/runEdgeChecks.mjs +152 -0
- package/template/scripts/smoke/runSmokeRun.mjs +1 -1
- package/template/apps/qpqjs/services/test/service/src/smoke/logic/smokeRun/askExecuteSmokeRun.ts +0 -83
- package/template/apps/qpqjs/services/test/service/src/smoke/models/SmokeRunRequestedQueueEvent.ts +0 -9
- package/template/apps/qpqjs/services/test/service/src/smoke/queue/onSmokeRunRequested.ts +0 -15
- package/template/docusaurus/docs/actions/webserver/open-api-spec/_category_.json +0 -1
- package/template/docusaurus/docs/actions/webserver/open-api-spec/ask-get-open-api-spec.md +0 -43
- package/template/docusaurus/docs/config/webserver/open-api.md +0 -36
|
@@ -154,12 +154,6 @@ export const api = createApi({
|
|
|
154
154
|
});
|
|
155
155
|
```
|
|
156
156
|
|
|
157
|
-
**Automatic OpenAPI Documentation**
|
|
158
|
-
- Generate OpenAPI specs from your route definitions
|
|
159
|
-
- Type-safe request/response contracts
|
|
160
|
-
- Automatic validation against schemas
|
|
161
|
-
- Interactive API documentation
|
|
162
|
-
|
|
163
157
|
**Built-in Authentication & Authorization**
|
|
164
158
|
```typescript
|
|
165
159
|
function* protectedRoute(request: HTTPRequest) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import type { PrismTheme } from 'prism-react-renderer';
|
|
1
2
|
import type * as Preset from '@docusaurus/preset-classic';
|
|
2
|
-
import type {Config} from '@docusaurus/types';
|
|
3
|
-
import type {PrismTheme} from 'prism-react-renderer';
|
|
3
|
+
import type { Config } from '@docusaurus/types';
|
|
4
4
|
|
|
5
5
|
// This runs in Node.js - Don't use client-side code here (browser APIs, JSX...)
|
|
6
6
|
|
|
@@ -12,16 +12,36 @@ const qpqPrismTheme: PrismTheme = {
|
|
|
12
12
|
backgroundColor: 'transparent',
|
|
13
13
|
},
|
|
14
14
|
styles: [
|
|
15
|
-
{types: ['keyword', 'operator', 'atrule'], style: {color: '#7dd3fc'}},
|
|
16
|
-
{types: ['function', 'function-variable'], style: {color: '#8df6ff'}},
|
|
17
|
-
{
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
{
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
15
|
+
{ types: ['keyword', 'operator', 'atrule'], style: { color: '#7dd3fc' } },
|
|
16
|
+
{ types: ['function', 'function-variable'], style: { color: '#8df6ff' } },
|
|
17
|
+
{
|
|
18
|
+
types: ['string', 'char', 'attr-value', 'url', 'regex'],
|
|
19
|
+
style: { color: '#fbbf77' },
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
types: ['class-name', 'maybe-class-name', 'builtin', 'namespace'],
|
|
23
|
+
style: { color: '#86efac' },
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
types: ['comment', 'prolog', 'doctype', 'cdata'],
|
|
27
|
+
style: { color: '#5c7181', fontStyle: 'italic' as const },
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
types: [
|
|
31
|
+
'number',
|
|
32
|
+
'boolean',
|
|
33
|
+
'constant',
|
|
34
|
+
'symbol',
|
|
35
|
+
'property',
|
|
36
|
+
'attr-name',
|
|
37
|
+
'variable',
|
|
38
|
+
],
|
|
39
|
+
style: { color: '#c4b5fd' },
|
|
40
|
+
},
|
|
41
|
+
{ types: ['punctuation'], style: { color: '#8fa8b8' } },
|
|
42
|
+
{ types: ['tag', 'selector'], style: { color: '#7dd3fc' } },
|
|
43
|
+
{ types: ['deleted'], style: { color: '#ff5f57' } },
|
|
44
|
+
{ types: ['inserted'], style: { color: '#28c840' } },
|
|
25
45
|
],
|
|
26
46
|
};
|
|
27
47
|
|
|
@@ -65,11 +85,15 @@ const config: Config = {
|
|
|
65
85
|
headTags: [
|
|
66
86
|
{
|
|
67
87
|
tagName: 'link',
|
|
68
|
-
attributes: {rel: 'preconnect', href: 'https://fonts.googleapis.com'},
|
|
88
|
+
attributes: { rel: 'preconnect', href: 'https://fonts.googleapis.com' },
|
|
69
89
|
},
|
|
70
90
|
{
|
|
71
91
|
tagName: 'link',
|
|
72
|
-
attributes: {
|
|
92
|
+
attributes: {
|
|
93
|
+
rel: 'preconnect',
|
|
94
|
+
href: 'https://fonts.gstatic.com',
|
|
95
|
+
crossorigin: 'anonymous',
|
|
96
|
+
},
|
|
73
97
|
},
|
|
74
98
|
],
|
|
75
99
|
stylesheets: [
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {SidebarsConfig} from '@docusaurus/plugin-content-docs';
|
|
1
|
+
import type { SidebarsConfig } from '@docusaurus/plugin-content-docs';
|
|
2
2
|
|
|
3
3
|
const sidebars: SidebarsConfig = {
|
|
4
4
|
tutorialSidebar: [
|
|
@@ -9,20 +9,20 @@ const sidebars: SidebarsConfig = {
|
|
|
9
9
|
'architecture-overview',
|
|
10
10
|
'eslint-plugin',
|
|
11
11
|
],
|
|
12
|
-
|
|
12
|
+
|
|
13
13
|
apiSidebar: [
|
|
14
14
|
'api/index',
|
|
15
15
|
{
|
|
16
16
|
type: 'category',
|
|
17
17
|
label: 'Config',
|
|
18
|
-
items: [{type: 'autogenerated', dirName: 'config'}],
|
|
18
|
+
items: [{ type: 'autogenerated', dirName: 'config' }],
|
|
19
19
|
},
|
|
20
20
|
{
|
|
21
21
|
type: 'category',
|
|
22
22
|
label: 'Action Requesters',
|
|
23
|
-
items: [{type: 'autogenerated', dirName: 'actions'}],
|
|
23
|
+
items: [{ type: 'autogenerated', dirName: 'actions' }],
|
|
24
24
|
},
|
|
25
25
|
],
|
|
26
26
|
};
|
|
27
27
|
|
|
28
|
-
export default sidebars;
|
|
28
|
+
export default sidebars;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import clsx from 'clsx';
|
|
2
|
-
import type {ReactNode} from 'react';
|
|
2
|
+
import type { ReactNode } from 'react';
|
|
3
3
|
import Heading from '@theme/Heading';
|
|
4
4
|
|
|
5
5
|
import styles from './styles.module.css';
|
|
@@ -43,7 +43,7 @@ const FeatureList: FeatureItem[] = [
|
|
|
43
43
|
},
|
|
44
44
|
];
|
|
45
45
|
|
|
46
|
-
function Feature({title, Svg, description}: FeatureItem) {
|
|
46
|
+
function Feature({ title, Svg, description }: FeatureItem) {
|
|
47
47
|
return (
|
|
48
48
|
<div className={clsx('col col--4')}>
|
|
49
49
|
<div className="text--center">
|
|
@@ -1,29 +1,34 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// Runs the smoke suite against a just-deployed test service
|
|
3
|
-
// if
|
|
4
|
-
//
|
|
2
|
+
// Runs the smoke suite against a just-deployed test service on EVERY root
|
|
3
|
+
// domain and exits non-zero if any root does not pass. Per root it runs the
|
|
4
|
+
// edge checks (scripts/smoke/runEdgeChecks.mjs: aliases, certs, cross-root
|
|
5
|
+
// cors, no token) and then the api smoke run (scripts/smoke/runSmokeRun.mjs,
|
|
6
|
+
// shared with localSmoke.mjs); this file only resolves where to point them.
|
|
5
7
|
//
|
|
6
8
|
// Runs inside the deploy workflow's deploy job, which supplies:
|
|
7
9
|
// SMOKE_ENVIRONMENT the environment that was just deployed (development, staging)
|
|
8
10
|
// ACTIONS_ID_TOKEN_REQUEST_URL provided by Actions when the job has id-token: write
|
|
9
11
|
// ACTIONS_ID_TOKEN_REQUEST_TOKEN provided by Actions when the job has id-token: write
|
|
12
|
+
// SMOKE_ROOTS optional comma list to run a subset of the roots
|
|
10
13
|
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
// https://api.<environment>.<
|
|
14
|
+
// Urls are derived, not configured: the roots come from the app's constants
|
|
15
|
+
// package (the same list every service's defineDns reads), hosts follow the
|
|
16
|
+
// app's default domain shape (see getRootHosts), and the api gateway maps each
|
|
17
|
+
// service under its own base path:
|
|
18
|
+
// https://api.<environment>.<root>/<service>
|
|
16
19
|
|
|
17
|
-
import {
|
|
20
|
+
import { createRequire } from 'node:module';
|
|
18
21
|
import { dirname, join } from 'node:path';
|
|
19
22
|
import { fileURLToPath } from 'node:url';
|
|
20
23
|
|
|
24
|
+
import { getRootHosts, runEdgeChecks } from './smoke/runEdgeChecks.mjs';
|
|
21
25
|
import { runSmokeRun } from './smoke/runSmokeRun.mjs';
|
|
22
26
|
|
|
23
27
|
const SERVICE_NAME = 'test';
|
|
24
|
-
|
|
28
|
+
// The built constants package: the deploy job builds the app before this runs.
|
|
29
|
+
const DOMAIN_CONSTANTS_PATH = join(
|
|
25
30
|
dirname(fileURLToPath(import.meta.url)),
|
|
26
|
-
'../apps/qpqjs/
|
|
31
|
+
'../apps/qpqjs/packages/constants/dist/src/domain.js'
|
|
27
32
|
);
|
|
28
33
|
|
|
29
34
|
const log = (message) => console.log(`deployed-smoke: ${message}`);
|
|
@@ -41,21 +46,75 @@ const requireEnv = (name) => {
|
|
|
41
46
|
return value;
|
|
42
47
|
};
|
|
43
48
|
|
|
44
|
-
const
|
|
45
|
-
const {
|
|
46
|
-
|
|
47
|
-
|
|
49
|
+
const getRoots = () => {
|
|
50
|
+
const { QPQJS_DOMAINS } = createRequire(import.meta.url)(
|
|
51
|
+
DOMAIN_CONSTANTS_PATH
|
|
52
|
+
);
|
|
53
|
+
if (!QPQJS_DOMAINS?.length) {
|
|
54
|
+
fail(`no QPQJS_DOMAINS in ${DOMAIN_CONSTANTS_PATH}; build the app first`);
|
|
48
55
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
56
|
+
|
|
57
|
+
const subset = process.env.SMOKE_ROOTS?.split(',')
|
|
58
|
+
.map((root) => root.trim())
|
|
59
|
+
.filter(Boolean);
|
|
60
|
+
if (!subset?.length) {
|
|
61
|
+
return QPQJS_DOMAINS;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const unknown = subset.filter((root) => !QPQJS_DOMAINS.includes(root));
|
|
65
|
+
if (unknown.length > 0) {
|
|
66
|
+
fail(`SMOKE_ROOTS names roots not in QPQJS_DOMAINS: ${unknown.join(', ')}`);
|
|
67
|
+
}
|
|
68
|
+
return subset;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const printTable = (root, results) => {
|
|
72
|
+
for (const result of results) {
|
|
73
|
+
log(
|
|
74
|
+
` [${root}] ${result.ok ? 'pass' : 'FAIL'} ${result.name.padEnd(36)} ${result.detail}`
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// Every root runs to the end, so one report covers all of them.
|
|
80
|
+
const smokeRoot = async (hosts, allHosts) => {
|
|
81
|
+
const rootLog = (message) => log(`[${hosts.root}] ${message}`);
|
|
82
|
+
rootLog(`edge checks against ${hosts.site}`);
|
|
83
|
+
|
|
84
|
+
const edge = await runEdgeChecks(hosts, allHosts);
|
|
85
|
+
printTable(hosts.root, edge);
|
|
86
|
+
const edgeOk = edge.every((result) => result.ok);
|
|
87
|
+
|
|
88
|
+
const apiUrl = `https://${hosts.api}/${SERVICE_NAME}`;
|
|
89
|
+
rootLog(`api smoke against ${apiUrl}`);
|
|
90
|
+
let apiOk = true;
|
|
91
|
+
try {
|
|
92
|
+
await runSmokeRun(apiUrl, rootLog);
|
|
93
|
+
} catch (error) {
|
|
94
|
+
apiOk = false;
|
|
95
|
+
rootLog(
|
|
96
|
+
`api smoke failed: ${error instanceof Error ? error.message : String(error)}`
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return edgeOk && apiOk;
|
|
52
101
|
};
|
|
53
102
|
|
|
54
103
|
const environment = requireEnv('SMOKE_ENVIRONMENT');
|
|
55
|
-
const
|
|
104
|
+
const roots = getRoots();
|
|
105
|
+
const allHosts = roots.map((root) => getRootHosts(root, environment));
|
|
56
106
|
|
|
57
|
-
log(`environment=${environment}
|
|
107
|
+
log(`environment=${environment} roots=${roots.join(', ')}`);
|
|
58
108
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
);
|
|
109
|
+
const outcomes = [];
|
|
110
|
+
for (const hosts of allHosts) {
|
|
111
|
+
outcomes.push({ root: hosts.root, ok: await smokeRoot(hosts, allHosts) });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
for (const outcome of outcomes) {
|
|
115
|
+
log(`${outcome.ok ? 'PASS' : 'FAIL'} ${outcome.root}`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (outcomes.some((outcome) => !outcome.ok)) {
|
|
119
|
+
fail(`${outcomes.filter((outcome) => !outcome.ok).length} root(s) failed`);
|
|
120
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// Edge checks for one root domain: what the in-lambda smoke suite cannot see
|
|
2
|
+
// from inside, namely the CloudFront aliases, the api gateway custom domain,
|
|
3
|
+
// the certificates and the cross-root CORS listing. Plain fetches, no token.
|
|
4
|
+
//
|
|
5
|
+
// Every check returns { name, ok, detail } rather than throwing, so a root
|
|
6
|
+
// reports its whole table before the run decides pass or fail.
|
|
7
|
+
|
|
8
|
+
const TIMEOUT_MS = 15 * 1000;
|
|
9
|
+
|
|
10
|
+
const fetchWithTimeout = (url, init = {}) =>
|
|
11
|
+
fetch(url, {
|
|
12
|
+
...init,
|
|
13
|
+
signal: AbortSignal.timeout(TIMEOUT_MS),
|
|
14
|
+
redirect: 'manual',
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
// Hosts for one root under the app's default shape (`<sub>.<env>.<root>`, no
|
|
18
|
+
// environment label in production). If the app ever adds a domain resolver
|
|
19
|
+
// pointer, derive these with quidproquo-webserver's resolveHosts instead.
|
|
20
|
+
export const getRootHosts = (root, environment) => {
|
|
21
|
+
const site = environment === 'production' ? root : `${environment}.${root}`;
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
root,
|
|
25
|
+
site,
|
|
26
|
+
api: `api.${site}`,
|
|
27
|
+
views: `views.${site}`,
|
|
28
|
+
docs: `docs.${site}`,
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const check = async (name, run) => {
|
|
33
|
+
try {
|
|
34
|
+
const detail = await run();
|
|
35
|
+
return { name, ok: true, detail };
|
|
36
|
+
} catch (error) {
|
|
37
|
+
return {
|
|
38
|
+
name,
|
|
39
|
+
ok: false,
|
|
40
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const expectStatus = async (url, statuses) => {
|
|
46
|
+
const response = await fetchWithTimeout(url);
|
|
47
|
+
if (!statuses.includes(response.status)) {
|
|
48
|
+
throw new Error(`${url} -> ${response.status}`);
|
|
49
|
+
}
|
|
50
|
+
return `${response.status}`;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// CloudFront serves index.html for the site root and the docs; the views host
|
|
54
|
+
// is a module-federation remote root, whose own root may 403 while its manifest
|
|
55
|
+
// serves, so accept either a 200 or a redirect there.
|
|
56
|
+
const checkServed = (hosts) => [
|
|
57
|
+
check('site root served', () =>
|
|
58
|
+
expectStatus(`https://${hosts.site}/`, [200, 301, 302])
|
|
59
|
+
),
|
|
60
|
+
check('docs served', () =>
|
|
61
|
+
expectStatus(`https://${hosts.docs}/`, [200, 301, 302])
|
|
62
|
+
),
|
|
63
|
+
check('views served', () =>
|
|
64
|
+
expectStatus(`https://${hosts.views}/`, [200, 301, 302, 403, 404])
|
|
65
|
+
),
|
|
66
|
+
check('api health', () =>
|
|
67
|
+
expectStatus(`https://${hosts.api}/shell/v1/health`, [200])
|
|
68
|
+
),
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
// The OpenAPI servers list is the runtime's own view of the roots: it proves
|
|
72
|
+
// the deployed lambda resolves every root, not just the one it was reached on.
|
|
73
|
+
const checkOpenApiServers = (hosts, allHosts) =>
|
|
74
|
+
check('openapi lists every root', async () => {
|
|
75
|
+
const response = await fetchWithTimeout(
|
|
76
|
+
`https://${hosts.api}/test/v1/docs/openapi.json`
|
|
77
|
+
);
|
|
78
|
+
if (!response.ok) {
|
|
79
|
+
throw new Error(`openapi.json -> ${response.status}`);
|
|
80
|
+
}
|
|
81
|
+
const servers = ((await response.json()).servers ?? []).map(
|
|
82
|
+
(server) => server.url
|
|
83
|
+
);
|
|
84
|
+
const missing = allHosts
|
|
85
|
+
.map((other) => other.api)
|
|
86
|
+
.filter((api) => !servers.some((url) => url.includes(api)));
|
|
87
|
+
if (missing.length > 0) {
|
|
88
|
+
throw new Error(
|
|
89
|
+
`servers ${JSON.stringify(servers)} missing ${missing.join(', ')}`
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
return `${servers.length} servers`;
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// A page on root B calls the api on root A: the preflight must echo B's origin.
|
|
96
|
+
const checkCrossRootCors = (hosts, allHosts) =>
|
|
97
|
+
allHosts
|
|
98
|
+
.filter((other) => other.root !== hosts.root)
|
|
99
|
+
.map((other) =>
|
|
100
|
+
check(`api cors allows ${other.site}`, async () => {
|
|
101
|
+
const origin = `https://${other.site}`;
|
|
102
|
+
const response = await fetchWithTimeout(
|
|
103
|
+
`https://${hosts.api}/shell/v1/health`,
|
|
104
|
+
{
|
|
105
|
+
method: 'OPTIONS',
|
|
106
|
+
headers: { Origin: origin, 'Access-Control-Request-Method': 'GET' },
|
|
107
|
+
}
|
|
108
|
+
);
|
|
109
|
+
const allowed = response.headers.get('access-control-allow-origin');
|
|
110
|
+
if (allowed !== origin && allowed !== '*') {
|
|
111
|
+
throw new Error(
|
|
112
|
+
`allow-origin ${allowed ?? '(none)'} for origin ${origin}`
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
return allowed;
|
|
116
|
+
})
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
// The module-federation remotes are baked to the primary root, so a page on
|
|
120
|
+
// an alternate root loads them cross-origin; the views web entry's cors policy
|
|
121
|
+
// must allow that origin.
|
|
122
|
+
const checkViewsAssetCors = (hosts, allHosts) =>
|
|
123
|
+
allHosts
|
|
124
|
+
.filter((other) => other.root !== hosts.root)
|
|
125
|
+
.map((other) =>
|
|
126
|
+
check(`views cors allows ${other.site}`, async () => {
|
|
127
|
+
const origin = `https://${other.site}`;
|
|
128
|
+
const response = await fetchWithTimeout(
|
|
129
|
+
`https://${hosts.views}/shell/mf-manifest.json`,
|
|
130
|
+
{ headers: { Origin: origin } }
|
|
131
|
+
);
|
|
132
|
+
const allowed = response.headers.get('access-control-allow-origin');
|
|
133
|
+
if (!allowed) {
|
|
134
|
+
throw new Error(
|
|
135
|
+
`no allow-origin header (status ${response.status}) for origin ${origin}`
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
return allowed;
|
|
139
|
+
})
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Run every edge check for one root against the full root list and resolve the
|
|
144
|
+
* results, never throwing; `ok` is false on every row that failed.
|
|
145
|
+
*/
|
|
146
|
+
export const runEdgeChecks = async (hosts, allHosts) =>
|
|
147
|
+
Promise.all([
|
|
148
|
+
...checkServed(hosts),
|
|
149
|
+
checkOpenApiServers(hosts, allHosts),
|
|
150
|
+
...checkCrossRootCors(hosts, allHosts),
|
|
151
|
+
...checkViewsAssetCors(hosts, allHosts),
|
|
152
|
+
]);
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
//
|
|
5
5
|
// Target-agnostic on purpose. The two callers differ only in the api url they
|
|
6
6
|
// resolve, which each derives for itself:
|
|
7
|
-
// deployedSmoke.mjs the just-deployed environment, from
|
|
7
|
+
// deployedSmoke.mjs the just-deployed environment, from the app domain constant
|
|
8
8
|
// localSmoke.mjs a dev server it started, on localhost
|
|
9
9
|
//
|
|
10
10
|
// Both talk to the same routes over the same protocol with the same token, so
|
package/template/apps/qpqjs/services/test/service/src/smoke/logic/smokeRun/askExecuteSmokeRun.ts
DELETED
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
askCatch,
|
|
3
|
-
askDateNow,
|
|
4
|
-
AskResponse,
|
|
5
|
-
askThrowError,
|
|
6
|
-
ErrorTypeEnum,
|
|
7
|
-
} from 'quidproquo';
|
|
8
|
-
|
|
9
|
-
import {
|
|
10
|
-
SmokeRun,
|
|
11
|
-
SmokeRunStatus,
|
|
12
|
-
SmokeTestResult,
|
|
13
|
-
SmokeTestStatus,
|
|
14
|
-
} from '@qpqjs/test-models';
|
|
15
|
-
|
|
16
|
-
import { askGetSmokeRunById } from '../../data/askGetSmokeRunById';
|
|
17
|
-
import { askSaveSmokeRun } from '../../data/askSaveSmokeRun';
|
|
18
|
-
import { SmokeTestDefinition } from '../../tests/SmokeTestDefinition';
|
|
19
|
-
import { smokeTestRegistry } from '../../tests/smokeTestRegistry';
|
|
20
|
-
|
|
21
|
-
// Runs one registered test, returning its completed result entry.
|
|
22
|
-
function* askRunSmokeTest(
|
|
23
|
-
test: SmokeTestDefinition,
|
|
24
|
-
pending: SmokeTestResult,
|
|
25
|
-
runId: string
|
|
26
|
-
): AskResponse<SmokeTestResult> {
|
|
27
|
-
const startedAt = yield* askDateNow();
|
|
28
|
-
const outcome = yield* askCatch(test.askRun(runId));
|
|
29
|
-
const finishedAt = yield* askDateNow();
|
|
30
|
-
|
|
31
|
-
return {
|
|
32
|
-
...pending,
|
|
33
|
-
status: outcome.success ? SmokeTestStatus.passed : SmokeTestStatus.failed,
|
|
34
|
-
message: outcome.success ? 'passed' : outcome.error.errorText,
|
|
35
|
-
startedAt,
|
|
36
|
-
finishedAt,
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// Executes every registered test for an existing run (created by
|
|
41
|
-
// askStartSmokeRun), persisting the record after each test so a poll sees
|
|
42
|
-
// progress, then finalizes the run status.
|
|
43
|
-
export function* askExecuteSmokeRun(runId: string): AskResponse<SmokeRun> {
|
|
44
|
-
const existing = yield* askGetSmokeRunById(runId);
|
|
45
|
-
if (!existing) {
|
|
46
|
-
return yield* askThrowError(
|
|
47
|
-
ErrorTypeEnum.NotFound,
|
|
48
|
-
`no smoke run [${runId}] to execute`
|
|
49
|
-
);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
let smokeRun: SmokeRun = existing;
|
|
53
|
-
|
|
54
|
-
for (let index = 0; index < smokeTestRegistry.length; index += 1) {
|
|
55
|
-
const result = yield* askRunSmokeTest(
|
|
56
|
-
smokeTestRegistry[index],
|
|
57
|
-
smokeRun.tests[index],
|
|
58
|
-
runId
|
|
59
|
-
);
|
|
60
|
-
|
|
61
|
-
smokeRun = {
|
|
62
|
-
...smokeRun,
|
|
63
|
-
tests: smokeRun.tests.map((t) => (t.id === result.id ? result : t)),
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
yield* askSaveSmokeRun(smokeRun);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const finishedAt = yield* askDateNow();
|
|
70
|
-
const anyFailed = smokeRun.tests.some(
|
|
71
|
-
(t) => t.status !== SmokeTestStatus.passed
|
|
72
|
-
);
|
|
73
|
-
|
|
74
|
-
smokeRun = {
|
|
75
|
-
...smokeRun,
|
|
76
|
-
status: anyFailed ? SmokeRunStatus.failed : SmokeRunStatus.passed,
|
|
77
|
-
finishedAt,
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
yield* askSaveSmokeRun(smokeRun);
|
|
81
|
-
|
|
82
|
-
return smokeRun;
|
|
83
|
-
}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import { AskResponse, QueueEventResponse } from 'quidproquo';
|
|
2
|
-
|
|
3
|
-
import { askExecuteSmokeRun } from '../logic/smokeRun/askExecuteSmokeRun';
|
|
4
|
-
import { SmokeRunRequestedQueueEvent } from '../models/SmokeRunRequestedQueueEvent';
|
|
5
|
-
|
|
6
|
-
// Queue entry for a requested smoke run: executes the registered tests
|
|
7
|
-
// against the run record askStartSmokeRun created. A throw here (e.g. the
|
|
8
|
-
// record is missing) fails the message so the platform can redeliver it.
|
|
9
|
-
export function* onSmokeRunRequested(
|
|
10
|
-
event: SmokeRunRequestedQueueEvent
|
|
11
|
-
): AskResponse<QueueEventResponse> {
|
|
12
|
-
yield* askExecuteSmokeRun(event.message.payload.runId);
|
|
13
|
-
|
|
14
|
-
return true;
|
|
15
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{ "label": "OpenAPI Spec", "link": { "type": "generated-index", "description": "Read the service's generated OpenAPI spec from within a story." } }
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: askGetOpenApiSpec
|
|
3
|
-
description: Return the service's generated OpenAPI spec as a string.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# askGetOpenApiSpec
|
|
7
|
-
|
|
8
|
-
Returns the service's **OpenAPI spec** as a string. Use it in a route handler to serve the spec (for documentation UIs or client generation) directly from the running service.
|
|
9
|
-
|
|
10
|
-
- **Action type:** `OpenApiSpecActionType.GetOpenApiSpec`
|
|
11
|
-
|
|
12
|
-
```typescript
|
|
13
|
-
import { askGetOpenApiSpec } from 'quidproquo-webserver';
|
|
14
|
-
|
|
15
|
-
export function* getOpenApiRoute() {
|
|
16
|
-
const spec = yield* askGetOpenApiSpec();
|
|
17
|
-
|
|
18
|
-
return {
|
|
19
|
-
status: 200,
|
|
20
|
-
headers: { 'content-type': 'application/json' },
|
|
21
|
-
body: spec,
|
|
22
|
-
};
|
|
23
|
-
}
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
## Signature
|
|
27
|
-
|
|
28
|
-
```typescript
|
|
29
|
-
function* askGetOpenApiSpec(): AskResponse<string>;
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
## Parameters
|
|
33
|
-
|
|
34
|
-
None.
|
|
35
|
-
|
|
36
|
-
## Returns
|
|
37
|
-
|
|
38
|
-
`string` — the OpenAPI spec. Associate a spec with the service using [defineOpenApi](../../../config/webserver/open-api.md).
|
|
39
|
-
|
|
40
|
-
## Related
|
|
41
|
-
|
|
42
|
-
- [defineOpenApi](../../../config/webserver/open-api.md) — declares the spec this action returns.
|
|
43
|
-
- [defineRoute](../../../config/webserver/route.md) — expose the spec at an HTTP path.
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: defineOpenApi
|
|
3
|
-
description: Point a service at an OpenAPI spec file describing its HTTP API.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# defineOpenApi
|
|
7
|
-
|
|
8
|
-
Registers an **OpenAPI spec** for the service by pointing at a spec file on disk. This associates a machine-readable description of the service's HTTP API with the config.
|
|
9
|
-
|
|
10
|
-
- **On AWS:** no infrastructure of its own — it records the spec path in the config. The generated spec is retrievable at runtime with [askGetOpenApiSpec](../../actions/webserver/open-api-spec/ask-get-open-api-spec.md).
|
|
11
|
-
|
|
12
|
-
```typescript
|
|
13
|
-
import { defineOpenApi } from 'quidproquo-webserver';
|
|
14
|
-
|
|
15
|
-
export default [
|
|
16
|
-
defineOpenApi('./openapi.json'),
|
|
17
|
-
];
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
## Signature
|
|
21
|
-
|
|
22
|
-
```typescript
|
|
23
|
-
function defineOpenApi(openApiSpecPath: string): OpenApiQPQWebServerConfigSetting;
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
## Parameters
|
|
27
|
-
|
|
28
|
-
### `openApiSpecPath` — `string` (required)
|
|
29
|
-
|
|
30
|
-
Path to the OpenAPI spec file for the service. This value is also the setting's `uniqueKey`.
|
|
31
|
-
|
|
32
|
-
## Related
|
|
33
|
-
|
|
34
|
-
- [askGetOpenApiSpec](../../actions/webserver/open-api-spec/ask-get-open-api-spec.md) — read the OpenAPI spec from within a story.
|
|
35
|
-
- [defineApi](./api.md) — the HTTP API the spec describes.
|
|
36
|
-
- [defineRoute](./route.md) — the routes that make up the API surface.
|