@tollwyn/baukasten 0.0.2 → 1.2.3
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 +254 -1
- package/dist/bin/check.d.ts +2 -0
- package/dist/bin/check.js +38 -0
- package/dist/bin/migrate.d.ts +2 -0
- package/dist/bin/migrate.js +111 -0
- package/dist/bin/stubs.d.ts +2 -0
- package/dist/bin/stubs.js +38 -0
- package/dist/bin/test-accounts.d.ts +2 -0
- package/dist/bin/test-accounts.js +62 -0
- package/dist/check.d.ts +20 -0
- package/dist/check.js +285 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +9 -0
- package/dist/product.d.ts +161 -0
- package/dist/product.js +152 -0
- package/dist/server/accounts.d.ts +128 -0
- package/dist/server/accounts.js +282 -0
- package/dist/server/adapter/db.d.ts +5 -0
- package/dist/server/adapter/db.js +63 -0
- package/dist/server/adapter/log.d.ts +3 -0
- package/dist/server/adapter/log.js +12 -0
- package/dist/server/adapter/mailer.d.ts +4 -0
- package/dist/server/adapter/mailer.js +56 -0
- package/dist/server/adapter/migrate.d.ts +21 -0
- package/dist/server/adapter/migrate.js +113 -0
- package/dist/server/adapter/secrets.d.ts +2 -0
- package/dist/server/adapter/secrets.js +40 -0
- package/dist/server/adapter/stripe.d.ts +11 -0
- package/dist/server/adapter/stripe.js +333 -0
- package/dist/server/api.d.ts +25 -0
- package/dist/server/api.js +98 -0
- package/dist/server/baukasten-migrations.d.ts +4 -0
- package/dist/server/baukasten-migrations.js +126 -0
- package/dist/server/billing.d.ts +94 -0
- package/dist/server/billing.js +349 -0
- package/dist/server/catalog.d.ts +39 -0
- package/dist/server/catalog.js +193 -0
- package/dist/server/config.d.ts +108 -0
- package/dist/server/config.js +244 -0
- package/dist/server/endpoints.d.ts +44 -0
- package/dist/server/endpoints.js +159 -0
- package/dist/server/fields.d.ts +7 -0
- package/dist/server/fields.js +18 -0
- package/dist/server/headers.d.ts +1 -0
- package/dist/server/headers.js +9 -0
- package/dist/server/health.d.ts +34 -0
- package/dist/server/health.js +116 -0
- package/dist/server/hook.d.ts +92 -0
- package/dist/server/hook.js +293 -0
- package/dist/server/index.d.ts +18 -0
- package/dist/server/index.js +11 -0
- package/dist/server/legal/de.d.ts +2 -0
- package/dist/server/legal/de.js +251 -0
- package/dist/server/legal/document.d.ts +37 -0
- package/dist/server/legal/document.js +30 -0
- package/dist/server/legal/en.d.ts +2 -0
- package/dist/server/legal/en.js +251 -0
- package/dist/server/legal/fleet.d.ts +114 -0
- package/dist/server/legal/fleet.js +89 -0
- package/dist/server/legal/index.d.ts +3 -0
- package/dist/server/legal/index.js +11 -0
- package/dist/server/limits.d.ts +58 -0
- package/dist/server/limits.js +76 -0
- package/dist/server/log.d.ts +26 -0
- package/dist/server/log.js +61 -0
- package/dist/server/machine.d.ts +4 -0
- package/dist/server/machine.js +57 -0
- package/dist/server/mail.d.ts +20 -0
- package/dist/server/mail.js +29 -0
- package/dist/server/messages.d.ts +105 -0
- package/dist/server/messages.js +232 -0
- package/dist/server/migrations.d.ts +28 -0
- package/dist/server/migrations.js +59 -0
- package/dist/server/openapi.d.ts +24 -0
- package/dist/server/openapi.js +173 -0
- package/dist/server/pages.d.ts +158 -0
- package/dist/server/pages.js +491 -0
- package/dist/server/ports.d.ts +273 -0
- package/dist/server/ports.js +1 -0
- package/dist/server/runs.d.ts +30 -0
- package/dist/server/runs.js +55 -0
- package/dist/server/sales.d.ts +38 -0
- package/dist/server/sales.js +132 -0
- package/dist/server/services.d.ts +57 -0
- package/dist/server/services.js +44 -0
- package/dist/server/start.d.ts +14 -0
- package/dist/server/start.js +65 -0
- package/dist/server/stripe.d.ts +33 -0
- package/dist/server/stripe.js +29 -0
- package/dist/server/terms.d.ts +7 -0
- package/dist/server/terms.js +9 -0
- package/dist/server/test-accounts.d.ts +28 -0
- package/dist/server/test-accounts.js +62 -0
- package/dist/server/via.d.ts +9 -0
- package/dist/server/via.js +15 -0
- package/dist/server/web.d.ts +48 -0
- package/dist/server/web.js +105 -0
- package/dist/shopify/adapter/shopify.d.ts +8 -0
- package/dist/shopify/adapter/shopify.js +232 -0
- package/dist/shopify/admin.d.ts +33 -0
- package/dist/shopify/admin.js +47 -0
- package/dist/shopify/app.d.ts +60 -0
- package/dist/shopify/app.js +67 -0
- package/dist/shopify/config.d.ts +53 -0
- package/dist/shopify/config.js +133 -0
- package/dist/shopify/constants.d.ts +27 -0
- package/dist/shopify/constants.js +27 -0
- package/dist/shopify/headers.d.ts +1 -0
- package/dist/shopify/headers.js +8 -0
- package/dist/shopify/hmac.d.ts +1 -0
- package/dist/shopify/hmac.js +9 -0
- package/dist/shopify/hook.d.ts +60 -0
- package/dist/shopify/hook.js +508 -0
- package/dist/shopify/id-token.d.ts +12 -0
- package/dist/shopify/id-token.js +66 -0
- package/dist/shopify/index.d.ts +19 -0
- package/dist/shopify/index.js +15 -0
- package/dist/shopify/legal/de.d.ts +3 -0
- package/dist/shopify/legal/de.js +123 -0
- package/dist/shopify/legal/en.d.ts +3 -0
- package/dist/shopify/legal/en.js +121 -0
- package/dist/shopify/legal/index.d.ts +19 -0
- package/dist/shopify/legal/index.js +8 -0
- package/dist/shopify/legal/shopify.d.ts +16 -0
- package/dist/shopify/legal/shopify.js +21 -0
- package/dist/shopify/migrations.d.ts +4 -0
- package/dist/shopify/migrations.js +67 -0
- package/dist/shopify/pages.d.ts +45 -0
- package/dist/shopify/pages.js +146 -0
- package/dist/shopify/ports.d.ts +77 -0
- package/dist/shopify/ports.js +1 -0
- package/dist/shopify/sales.d.ts +4 -0
- package/dist/shopify/sales.js +27 -0
- package/dist/shopify/scopes.d.ts +2 -0
- package/dist/shopify/scopes.js +13 -0
- package/dist/shopify/seal.d.ts +12 -0
- package/dist/shopify/seal.js +60 -0
- package/dist/shopify/shop.d.ts +4 -0
- package/dist/shopify/shop.js +15 -0
- package/dist/shopify/shops.d.ts +76 -0
- package/dist/shopify/shops.js +256 -0
- package/dist/shopify/sveltekit.d.ts +19 -0
- package/dist/shopify/sveltekit.js +18 -0
- package/dist/shopify/texts.d.ts +28 -0
- package/dist/shopify/texts.js +49 -0
- package/dist/shopify/toml.d.ts +22 -0
- package/dist/shopify/toml.js +125 -0
- package/dist/shopify/webhooks.d.ts +65 -0
- package/dist/shopify/webhooks.js +220 -0
- package/dist/stubs.d.ts +15 -0
- package/dist/stubs.js +287 -0
- package/dist/sveltekit.d.ts +20 -0
- package/dist/sveltekit.js +18 -0
- package/dist/testing/app.d.ts +36 -0
- package/dist/testing/app.js +155 -0
- package/dist/testing/browser.d.ts +21 -0
- package/dist/testing/browser.js +21 -0
- package/dist/testing/connection.d.ts +7 -0
- package/dist/testing/connection.js +303 -0
- package/dist/testing/database.d.ts +21 -0
- package/dist/testing/database.js +54 -0
- package/dist/testing/env-example.d.ts +4 -0
- package/dist/testing/env-example.js +33 -0
- package/dist/testing/index.d.ts +9 -0
- package/dist/testing/index.js +9 -0
- package/dist/testing/shopify-connection.d.ts +8 -0
- package/dist/testing/shopify-connection.js +313 -0
- package/dist/testing/shopify.d.ts +71 -0
- package/dist/testing/shopify.js +278 -0
- package/dist/testing/smtp.d.ts +38 -0
- package/dist/testing/smtp.js +165 -0
- package/dist/testing/stripe.d.ts +128 -0
- package/dist/testing/stripe.js +511 -0
- package/dist/texts.d.ts +139 -0
- package/dist/texts.js +265 -0
- package/dist/ui/AccountPage.svelte +143 -0
- package/dist/ui/AccountPage.svelte.d.ts +10 -0
- package/dist/ui/ConfirmEmailPage.svelte +21 -0
- package/dist/ui/ConfirmEmailPage.svelte.d.ts +10 -0
- package/dist/ui/ErrorPage.svelte +14 -0
- package/dist/ui/ErrorPage.svelte.d.ts +3 -0
- package/dist/ui/Field.svelte +12 -0
- package/dist/ui/Field.svelte.d.ts +10 -0
- package/dist/ui/ForgotPasswordPage.svelte +32 -0
- package/dist/ui/ForgotPasswordPage.svelte.d.ts +7 -0
- package/dist/ui/Form.svelte +30 -0
- package/dist/ui/Form.svelte.d.ts +11 -0
- package/dist/ui/Frame.svelte +84 -0
- package/dist/ui/Frame.svelte.d.ts +12 -0
- package/dist/ui/LegalPage.svelte +38 -0
- package/dist/ui/LegalPage.svelte.d.ts +9 -0
- package/dist/ui/Linked.svelte +5 -0
- package/dist/ui/Linked.svelte.d.ts +8 -0
- package/dist/ui/LoginPage.svelte +41 -0
- package/dist/ui/LoginPage.svelte.d.ts +10 -0
- package/dist/ui/LogoutPage.svelte +19 -0
- package/dist/ui/LogoutPage.svelte.d.ts +9 -0
- package/dist/ui/ResetPasswordPage.svelte +30 -0
- package/dist/ui/ResetPasswordPage.svelte.d.ts +10 -0
- package/dist/ui/SignupPage.svelte +52 -0
- package/dist/ui/SignupPage.svelte.d.ts +10 -0
- package/dist/ui/VerifyEmailPage.svelte +50 -0
- package/dist/ui/VerifyEmailPage.svelte.d.ts +10 -0
- package/dist/ui/base.css +287 -0
- package/dist/ui/data.d.ts +167 -0
- package/dist/ui/data.js +1 -0
- package/dist/ui/frame.d.ts +17 -0
- package/dist/ui/frame.js +27 -0
- package/dist/ui/index.d.ts +15 -0
- package/dist/ui/index.js +15 -0
- package/dist/variables.d.ts +20 -0
- package/dist/variables.js +62 -0
- package/package.json +88 -4
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
// The connection test of a product, which its e2e/baukasten.spec.ts (a stub) runs against the built
|
|
2
|
+
// product with the doubles. Playwright comes from the product, handed in by the stub, so that one
|
|
3
|
+
// copy of it runs; the package names it no peer, since npm keeps an optional peer of a dependency in
|
|
4
|
+
// the image of every product (`devOptional` outlives `npm prune --omit=dev`). The product's own
|
|
5
|
+
// variables come from the environment of the test run: its playwright.config.ts sets those its
|
|
6
|
+
// configuration requires.
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
import { SECURITY_HEADERS } from '../server/headers.js';
|
|
10
|
+
import { TEXTS } from '../texts.js';
|
|
11
|
+
import { spawnNode, startApp } from './app.js';
|
|
12
|
+
import { newCaller, open, waitForHydration } from './browser.js';
|
|
13
|
+
import { createTestDatabase } from './database.js';
|
|
14
|
+
import { linkIn, startSmtp } from './smtp.js';
|
|
15
|
+
import { startStripe } from './stripe.js';
|
|
16
|
+
const MIGRATE = fileURLToPath(new URL('../bin/migrate.js', import.meta.url));
|
|
17
|
+
/** What the delivery service sets in production, as far as the test sets it itself. */
|
|
18
|
+
const STANDARD = {
|
|
19
|
+
NODE_ENV: 'production',
|
|
20
|
+
MAIL_FROM: 'hello@example.com',
|
|
21
|
+
MAIL_FROM_NAME: 'Connection Test',
|
|
22
|
+
OPERATOR_NAME: 'Connection Test Operator',
|
|
23
|
+
OPERATOR_ADDRESS: 'Street 1, 5400 Baden',
|
|
24
|
+
OPERATOR_REGISTER: 'none',
|
|
25
|
+
OPERATOR_VAT: 'none',
|
|
26
|
+
OPERATOR_JURISDICTION: 'Baden',
|
|
27
|
+
HOSTING_LOCATION: 'Example Hosting, Nuremberg',
|
|
28
|
+
VERTRIEB_TOKEN: 'v'.repeat(64)
|
|
29
|
+
};
|
|
30
|
+
const VIA = 'connection-test';
|
|
31
|
+
const EMAIL = 'connection-test@example.com';
|
|
32
|
+
const SESSION = '__Host-session';
|
|
33
|
+
/** The product's own variables from the environment of the test run; loud on a required one missing. */
|
|
34
|
+
function ownVariables(env) {
|
|
35
|
+
const given = {};
|
|
36
|
+
const lacking = [];
|
|
37
|
+
for (const [name, schema] of Object.entries(env)) {
|
|
38
|
+
const value = process.env[name];
|
|
39
|
+
if (value !== undefined)
|
|
40
|
+
given[name] = value;
|
|
41
|
+
else if (!schema.safeParse(undefined).success)
|
|
42
|
+
lacking.push(name);
|
|
43
|
+
}
|
|
44
|
+
if (lacking.length > 0) {
|
|
45
|
+
throw new Error(`the connection test takes the product's own variables from the environment of the test run, which lacks ${lacking.join(', ')}: set them in playwright.config.ts`);
|
|
46
|
+
}
|
|
47
|
+
return given;
|
|
48
|
+
}
|
|
49
|
+
/** Registers the connection test of `product` with the test and expect of the product's Playwright. */
|
|
50
|
+
export function connectionTest(product, playwright) {
|
|
51
|
+
const { test, expect } = playwright;
|
|
52
|
+
const t = TEXTS[product.language];
|
|
53
|
+
const home = product.accounts.home;
|
|
54
|
+
const [price] = product.catalog.products.flatMap((sold) => sold.prices);
|
|
55
|
+
if (price === undefined)
|
|
56
|
+
throw new Error('the catalog of the product names no price');
|
|
57
|
+
test.describe('the connection of the baukasten', () => {
|
|
58
|
+
test.describe.configure({ mode: 'serial' });
|
|
59
|
+
let database;
|
|
60
|
+
let smtp;
|
|
61
|
+
let stripe;
|
|
62
|
+
let app;
|
|
63
|
+
let context;
|
|
64
|
+
let page;
|
|
65
|
+
let key;
|
|
66
|
+
/** Undoes what beforeAll set up, as far as it came, the last first. */
|
|
67
|
+
const undo = [];
|
|
68
|
+
const bearer = (secret) => ({
|
|
69
|
+
...newCaller().headers,
|
|
70
|
+
Authorization: `Bearer ${secret}`
|
|
71
|
+
});
|
|
72
|
+
const cookies = async () => (await context.cookies()).map((cookie) => cookie.name);
|
|
73
|
+
async function logIn(password) {
|
|
74
|
+
await open(page, `${app.url}/login`);
|
|
75
|
+
await page.getByLabel(t.login.email, { exact: true }).fill(EMAIL);
|
|
76
|
+
await page.getByLabel(t.login.password, { exact: true }).fill(password);
|
|
77
|
+
await page.getByRole('button', { name: t.login.button }).click();
|
|
78
|
+
await expect(page).toHaveURL(`${app.url}${home}`);
|
|
79
|
+
}
|
|
80
|
+
async function logOut() {
|
|
81
|
+
await page
|
|
82
|
+
.getByRole('navigation', { name: t.frame.accountMenu })
|
|
83
|
+
.getByRole('button', { name: t.frame.logOut })
|
|
84
|
+
.click();
|
|
85
|
+
await expect(page).toHaveURL(`${app.url}/logout`);
|
|
86
|
+
await waitForHydration(page);
|
|
87
|
+
}
|
|
88
|
+
test.beforeAll(async ({ browser }) => {
|
|
89
|
+
const own = ownVariables(product.env);
|
|
90
|
+
database = await createTestDatabase();
|
|
91
|
+
undo.push(() => database.drop());
|
|
92
|
+
smtp = await startSmtp();
|
|
93
|
+
undo.push(() => smtp.stop());
|
|
94
|
+
stripe = await startStripe();
|
|
95
|
+
undo.push(() => stripe.stop());
|
|
96
|
+
app = await startApp({
|
|
97
|
+
env: { ...STANDARD, ...own, DATABASE_URL: database.url },
|
|
98
|
+
smtp,
|
|
99
|
+
stripe
|
|
100
|
+
});
|
|
101
|
+
undo.push(() => app.stop());
|
|
102
|
+
// The catalog at the double, as the migration sets it up before every start.
|
|
103
|
+
const migrate = spawnNode([MIGRATE], {
|
|
104
|
+
...STANDARD,
|
|
105
|
+
...own,
|
|
106
|
+
DATABASE_URL: database.url,
|
|
107
|
+
PUBLIC_ORIGIN: app.url,
|
|
108
|
+
PORT: new URL(app.url).port,
|
|
109
|
+
SMTP_HOST: smtp.host,
|
|
110
|
+
SMTP_PORT: String(smtp.port),
|
|
111
|
+
STRIPE_SECRET_KEY: 'rk_test_double',
|
|
112
|
+
STRIPE_API_BASE: stripe.url,
|
|
113
|
+
STRIPE_WEBHOOK_SECRET: stripe.webhookSecret
|
|
114
|
+
});
|
|
115
|
+
expect(await migrate.exited, JSON.stringify(migrate.logs)).toBe(0);
|
|
116
|
+
context = await browser.newContext({ extraHTTPHeaders: newCaller().headers });
|
|
117
|
+
undo.push(() => context.close());
|
|
118
|
+
page = await context.newPage();
|
|
119
|
+
});
|
|
120
|
+
test.afterAll(async () => {
|
|
121
|
+
// Every step, though one before it failed: the test database goes in any case.
|
|
122
|
+
const failed = [];
|
|
123
|
+
for (const step of undo.reverse()) {
|
|
124
|
+
try {
|
|
125
|
+
await step();
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
failed.push(error);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (failed.length > 0)
|
|
132
|
+
throw new AggregateError(failed, 'cleaning up after the connection test failed');
|
|
133
|
+
});
|
|
134
|
+
test('a Kennung travels from a page to the sign-up, and the mail confirms the address', async () => {
|
|
135
|
+
await open(page, `${app.url}/imprint?via=${VIA}`);
|
|
136
|
+
await page
|
|
137
|
+
.getByRole('navigation', { name: t.frame.accountMenu })
|
|
138
|
+
.getByRole('link', { name: t.frame.signUp })
|
|
139
|
+
.click();
|
|
140
|
+
await expect(page).toHaveURL(`${app.url}/signup?via=${VIA}`);
|
|
141
|
+
expect(await cookies()).toEqual([]);
|
|
142
|
+
await page.getByLabel(t.signup.email, { exact: true }).fill(EMAIL);
|
|
143
|
+
await page.getByLabel(t.signup.password, { exact: true }).fill('the first password');
|
|
144
|
+
const terms = t.signup.terms;
|
|
145
|
+
await page
|
|
146
|
+
.getByRole('checkbox', { name: `${terms.before}${terms.link}${terms.after}` })
|
|
147
|
+
.check();
|
|
148
|
+
await page.getByRole('checkbox', { name: t.signup.business }).check();
|
|
149
|
+
await page.getByRole('button', { name: t.signup.button }).click();
|
|
150
|
+
await expect(page).toHaveURL(`${app.url}${home}`);
|
|
151
|
+
expect(await context.cookies()).toEqual([
|
|
152
|
+
expect.objectContaining({ name: SESSION, httpOnly: true, secure: true, sameSite: 'Lax' })
|
|
153
|
+
]);
|
|
154
|
+
const mail = await smtp.next(EMAIL);
|
|
155
|
+
await open(page, linkIn(mail.text, `${app.url}/verify-email?token=`));
|
|
156
|
+
await page.getByRole('button', { name: t.verify.button }).click();
|
|
157
|
+
await expect(page).toHaveURL(`${app.url}/account?verified`);
|
|
158
|
+
});
|
|
159
|
+
test('log out and in, and a new password by mail', async () => {
|
|
160
|
+
await logOut();
|
|
161
|
+
await logIn('the first password');
|
|
162
|
+
await logOut();
|
|
163
|
+
await open(page, `${app.url}/forgot-password`);
|
|
164
|
+
await page.getByLabel(t.forgot.email, { exact: true }).fill(EMAIL);
|
|
165
|
+
await page.getByRole('button', { name: t.forgot.button }).click();
|
|
166
|
+
await expect(page.getByRole('status')).toHaveText(t.forgot.sent);
|
|
167
|
+
const mail = await smtp.next(EMAIL);
|
|
168
|
+
await open(page, linkIn(mail.text, `${app.url}/reset-password?token=`));
|
|
169
|
+
await page.getByLabel(t.reset.password, { exact: true }).fill('the second password');
|
|
170
|
+
await page.getByRole('button', { name: t.reset.button }).click();
|
|
171
|
+
await expect(page).toHaveURL(`${app.url}${home}`);
|
|
172
|
+
await logOut();
|
|
173
|
+
await logIn('the second password');
|
|
174
|
+
});
|
|
175
|
+
test('an API key reads its account', async ({ request }) => {
|
|
176
|
+
await open(page, `${app.url}/account`);
|
|
177
|
+
await page.getByLabel(t.account.keys.name, { exact: true }).fill('connection test');
|
|
178
|
+
await page.getByRole('button', { name: t.account.keys.button }).click();
|
|
179
|
+
const shown = page.locator('code.bk-secret');
|
|
180
|
+
await expect(shown).toHaveText(new RegExp(`^${product.accounts.keyPrefix}_[A-Za-z0-9_-]+$`));
|
|
181
|
+
key = (await shown.innerText()).trim();
|
|
182
|
+
const account = await request.get(`${app.url}/api/v1/account`, { headers: bearer(key) });
|
|
183
|
+
expect(account.status()).toBe(200);
|
|
184
|
+
const body = await account.json();
|
|
185
|
+
expect(body).toMatchObject({ email: EMAIL, email_verified: true });
|
|
186
|
+
});
|
|
187
|
+
test('Checkout pays the first price of the catalog, and the account opens the customer portal', async ({ request }) => {
|
|
188
|
+
const checkout = await request.post(`${app.url}/api/v1/billing/checkout`, {
|
|
189
|
+
headers: bearer(key),
|
|
190
|
+
data: { price: price.lookupKey }
|
|
191
|
+
});
|
|
192
|
+
expect(checkout.status()).toBe(200);
|
|
193
|
+
const opened = await checkout.json();
|
|
194
|
+
await page.goto(z.object({ url: z.string() }).parse(opened).url);
|
|
195
|
+
await expect(page).toHaveTitle('Stripe Checkout (test double)');
|
|
196
|
+
await page.getByRole('button', { name: 'Pay' }).click();
|
|
197
|
+
await expect(page).toHaveURL(`${app.url}/account`);
|
|
198
|
+
await waitForHydration(page);
|
|
199
|
+
await page.getByRole('button', { name: t.account.billing.button }).click();
|
|
200
|
+
await expect(page).toHaveTitle('Stripe customer portal (test double)');
|
|
201
|
+
await page.getByRole('link', { name: 'Back' }).click();
|
|
202
|
+
await expect(page).toHaveURL(`${app.url}/account`);
|
|
203
|
+
await waitForHydration(page);
|
|
204
|
+
});
|
|
205
|
+
test('the sales numbers count the account and its payment under its Kennung', async ({ request }) => {
|
|
206
|
+
const refused = await request.get(`${app.url}/vertrieb/zahlen`);
|
|
207
|
+
expect(refused.status()).toBe(401);
|
|
208
|
+
const numbers = await request.get(`${app.url}/vertrieb/zahlen`, {
|
|
209
|
+
headers: { Authorization: `Bearer ${STANDARD.VERTRIEB_TOKEN}` }
|
|
210
|
+
});
|
|
211
|
+
expect(numbers.status()).toBe(200);
|
|
212
|
+
const body = await numbers.json();
|
|
213
|
+
expect(body).toMatchObject({
|
|
214
|
+
je_kennung: [
|
|
215
|
+
{
|
|
216
|
+
kennung: VIA,
|
|
217
|
+
werte: expect.arrayContaining([
|
|
218
|
+
{ name: 'Konten', wert: 1 },
|
|
219
|
+
{ name: 'Zahlende', wert: 1 }
|
|
220
|
+
]),
|
|
221
|
+
umsatz: { cent: price.amount, waehrung: product.catalog.currency }
|
|
222
|
+
}
|
|
223
|
+
]
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
test('the legal pages name the operator, in the language of the product', async () => {
|
|
227
|
+
const footer = t.frame;
|
|
228
|
+
for (const [path, link] of [
|
|
229
|
+
['imprint', footer.imprint],
|
|
230
|
+
['privacy', footer.privacy],
|
|
231
|
+
['terms', footer.terms]
|
|
232
|
+
]) {
|
|
233
|
+
await open(page, `${app.url}/${path}`);
|
|
234
|
+
await expect(page.locator('html')).toHaveAttribute('lang', product.language);
|
|
235
|
+
await expect(page.getByRole('contentinfo').getByRole('link', { name: link, exact: true })).toHaveAttribute('href', `/${path}`);
|
|
236
|
+
const text = await page.getByRole('main').innerText();
|
|
237
|
+
expect(text, path).toContain(STANDARD.OPERATOR_NAME);
|
|
238
|
+
expect(text, path).toContain(STANDARD.OPERATOR_ADDRESS);
|
|
239
|
+
expect(text, path).not.toMatch(/undefined|null|NaN|\[object/);
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
test('a page carries the security headers and its policy, and the session is the only cookie', async () => {
|
|
243
|
+
const response = await page.goto(`${app.url}/account`);
|
|
244
|
+
if (response === null)
|
|
245
|
+
throw new Error('the page gave no response');
|
|
246
|
+
const headers = response.headers();
|
|
247
|
+
for (const [name, value] of Object.entries(SECURITY_HEADERS)) {
|
|
248
|
+
expect(headers[name], name).toBe(value);
|
|
249
|
+
}
|
|
250
|
+
expect(headers['content-security-policy']).toMatch(/script-src 'self'/);
|
|
251
|
+
expect(headers['content-security-policy']).toContain("frame-ancestors 'none'");
|
|
252
|
+
expect(await cookies()).toEqual([SESSION]);
|
|
253
|
+
});
|
|
254
|
+
test('the head shows the logo of the product, and its icon and the picture of a shared link load', async ({ request }) => {
|
|
255
|
+
await open(page, `${app.url}/imprint`);
|
|
256
|
+
const head = page.getByRole('banner');
|
|
257
|
+
const brand = product.brand;
|
|
258
|
+
if (brand === undefined) {
|
|
259
|
+
// Without marks the frame names the product as text.
|
|
260
|
+
await expect(head.getByRole('link', { name: STANDARD.MAIL_FROM_NAME })).toBeVisible();
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
const logo = head.getByRole('img', { name: STANDARD.MAIL_FROM_NAME });
|
|
264
|
+
await expect(logo).toHaveAttribute('src', brand.logo.src);
|
|
265
|
+
// After hydration the head may name the icon at the whole address of the page.
|
|
266
|
+
const icon = brand.icon.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
267
|
+
await expect(page.locator('link[rel="icon"]')).toHaveAttribute('href', new RegExp(`^(${app.url.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})?${icon}$`));
|
|
268
|
+
await expect(page.locator('meta[property="og:image"]')).toHaveAttribute('content', new URL(brand.image, app.url).href);
|
|
269
|
+
const files = [
|
|
270
|
+
[brand.logo.src, 'image/svg+xml'],
|
|
271
|
+
[brand.icon, brand.icon.endsWith('.svg') ? 'image/svg+xml' : 'image/png'],
|
|
272
|
+
[brand.image, 'image/png']
|
|
273
|
+
];
|
|
274
|
+
for (const [path, type] of files) {
|
|
275
|
+
const file = await request.get(`${app.url}${path}`, { headers: newCaller().headers });
|
|
276
|
+
expect(file.status(), path).toBe(200);
|
|
277
|
+
expect(file.headers()['content-type'], path).toBe(type);
|
|
278
|
+
}
|
|
279
|
+
// A PNG names its width and height at the start of its first chunk: 1200 × 630 for a link.
|
|
280
|
+
const image = await (await request.get(`${app.url}${brand.image}`)).body();
|
|
281
|
+
expect(image.subarray(1, 4).toString('latin1')).toBe('PNG');
|
|
282
|
+
expect([image.readUInt32BE(16), image.readUInt32BE(20)]).toEqual([1200, 630]);
|
|
283
|
+
});
|
|
284
|
+
test('deleting the account ends its key and its subscriptions, and logs out', async ({ request }) => {
|
|
285
|
+
await open(page, `${app.url}/account`);
|
|
286
|
+
await page.getByLabel(t.account.delete.confirm, { exact: true }).fill(EMAIL);
|
|
287
|
+
await page.getByRole('button', { name: t.account.delete.button }).click();
|
|
288
|
+
await expect(page).toHaveURL(`${app.url}/logout?deleted`);
|
|
289
|
+
expect(await cookies()).toEqual([]);
|
|
290
|
+
const account = await request.get(`${app.url}/api/v1/account`, { headers: bearer(key) });
|
|
291
|
+
expect(account.status()).toBe(401);
|
|
292
|
+
await stripe.idle();
|
|
293
|
+
const running = [...stripe.subscriptions.values()].filter((s) => s.status === 'active');
|
|
294
|
+
expect(running).toEqual([]);
|
|
295
|
+
});
|
|
296
|
+
test('the log of the product is JSON lines without the address, and stderr stays empty, to its end', async () => {
|
|
297
|
+
await app.stop();
|
|
298
|
+
expect(app.logs.filter((line) => 'notJson' in line)).toEqual([]);
|
|
299
|
+
expect(JSON.stringify(app.logs)).not.toContain(EMAIL);
|
|
300
|
+
expect(app.stderr()).toBe('');
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import pg from 'pg';
|
|
2
|
+
import { type Art } from '../server/adapter/migrate.js';
|
|
3
|
+
export interface TestDatabase {
|
|
4
|
+
url: string;
|
|
5
|
+
/** Runs SQL in the database, for what a test sets up or looks at; one pool, opened on first use. */
|
|
6
|
+
query<T extends pg.QueryResultRow = pg.QueryResultRow>(sql: string, params?: unknown[]): Promise<T[]>;
|
|
7
|
+
/** Closes the pool and drops the database. */
|
|
8
|
+
drop(): Promise<void>;
|
|
9
|
+
}
|
|
10
|
+
/** Postgres allows 63 characters for a name. */
|
|
11
|
+
export declare function testDatabaseName(server: string, suffix: string): string;
|
|
12
|
+
/**
|
|
13
|
+
* Creates the database; `migrate: false` leaves it empty, `root` is the root of the product
|
|
14
|
+
* (default: the working directory), whose `migrations/` holds the second layer, and `art` the art of
|
|
15
|
+
* the product (default: a web app), whose layer of the baukasten comes first.
|
|
16
|
+
*/
|
|
17
|
+
export declare function createTestDatabase(options?: {
|
|
18
|
+
migrate?: boolean;
|
|
19
|
+
root?: string;
|
|
20
|
+
art?: Art;
|
|
21
|
+
}): Promise<TestDatabase>;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// A test database's name starts with that of the server database of TEST_DATABASE_URL, so that the
|
|
2
|
+
// test databases of one worktree or container stay recognisable.
|
|
3
|
+
import { randomBytes } from 'node:crypto';
|
|
4
|
+
import pg from 'pg';
|
|
5
|
+
import { migrate, migrationSources } from '../server/adapter/migrate.js';
|
|
6
|
+
/** Postgres allows 63 characters for a name. */
|
|
7
|
+
export function testDatabaseName(server, suffix) {
|
|
8
|
+
return `${server.slice(0, 63 - suffix.length - 1)}_${suffix}`;
|
|
9
|
+
}
|
|
10
|
+
function serverUrl() {
|
|
11
|
+
const url = process.env.TEST_DATABASE_URL;
|
|
12
|
+
if (url === undefined || url === '') {
|
|
13
|
+
throw new Error('TEST_DATABASE_URL is not set: it names a Postgres server on which the tests may create and drop databases');
|
|
14
|
+
}
|
|
15
|
+
return new URL(url);
|
|
16
|
+
}
|
|
17
|
+
async function onServer(server, sql) {
|
|
18
|
+
const client = new pg.Client({ connectionString: server.toString() });
|
|
19
|
+
await client.connect();
|
|
20
|
+
try {
|
|
21
|
+
await client.query(sql);
|
|
22
|
+
}
|
|
23
|
+
finally {
|
|
24
|
+
await client.end();
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Creates the database; `migrate: false` leaves it empty, `root` is the root of the product
|
|
29
|
+
* (default: the working directory), whose `migrations/` holds the second layer, and `art` the art of
|
|
30
|
+
* the product (default: a web app), whose layer of the baukasten comes first.
|
|
31
|
+
*/
|
|
32
|
+
export async function createTestDatabase(options = {}) {
|
|
33
|
+
const server = serverUrl();
|
|
34
|
+
const name = testDatabaseName(server.pathname.slice(1), randomBytes(6).toString('hex'));
|
|
35
|
+
await onServer(server, `CREATE DATABASE "${name}"`);
|
|
36
|
+
const url = new URL(server);
|
|
37
|
+
url.pathname = `/${name}`;
|
|
38
|
+
let pool;
|
|
39
|
+
const database = {
|
|
40
|
+
url: url.toString(),
|
|
41
|
+
async query(sql, params) {
|
|
42
|
+
pool ??= new pg.Pool({ connectionString: url.toString(), max: 2 });
|
|
43
|
+
return (await pool.query(sql, params)).rows;
|
|
44
|
+
},
|
|
45
|
+
async drop() {
|
|
46
|
+
await pool?.end();
|
|
47
|
+
await onServer(server, `DROP DATABASE IF EXISTS "${name}" WITH (FORCE)`);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
if (options.migrate ?? true) {
|
|
51
|
+
await migrate(database.url, migrationSources(options.root ?? process.cwd(), options.art ?? 'webapp'));
|
|
52
|
+
}
|
|
53
|
+
return database;
|
|
54
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { Product } from '../product.js';
|
|
2
|
+
import { type ShopifyApp } from '../shopify/app.js';
|
|
3
|
+
/** What is wrong with the text of `.env.example`; empty when nothing is. */
|
|
4
|
+
export declare function envExampleProblems(text: string, product: Product | ShopifyApp): string[];
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { isShopifyApp } from '../shopify/app.js';
|
|
2
|
+
import { SHOPIFY_VARIABLES, WEBAPP_VARIABLES } from '../variables.js';
|
|
3
|
+
const LINE = /^([A-Z][A-Z0-9_]*)=(.*)$/;
|
|
4
|
+
/** What is wrong with the text of `.env.example`; empty when nothing is. */
|
|
5
|
+
export function envExampleProblems(text, product) {
|
|
6
|
+
const problems = [];
|
|
7
|
+
const named = [];
|
|
8
|
+
text.split(/\r?\n/).forEach((line, index) => {
|
|
9
|
+
if (line.trim() === '' || line.startsWith('#'))
|
|
10
|
+
return;
|
|
11
|
+
const match = LINE.exec(line);
|
|
12
|
+
const name = match?.[1];
|
|
13
|
+
if (match === null || name === undefined) {
|
|
14
|
+
problems.push(`.env.example line ${index + 1} is neither a comment nor NAME=`);
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
if (match[2] !== '') {
|
|
18
|
+
problems.push(`.env.example sets a value in line ${index + 1} (${name}); it names variables only`);
|
|
19
|
+
}
|
|
20
|
+
if (named.includes(name))
|
|
21
|
+
problems.push(`.env.example names ${name} twice`);
|
|
22
|
+
named.push(name);
|
|
23
|
+
});
|
|
24
|
+
const baukasten = isShopifyApp(product) ? SHOPIFY_VARIABLES : WEBAPP_VARIABLES;
|
|
25
|
+
const read = [...baukasten, ...Object.keys(product.env)];
|
|
26
|
+
const lacking = read.filter((name) => !named.includes(name));
|
|
27
|
+
if (lacking.length > 0)
|
|
28
|
+
problems.push(`.env.example lacks ${lacking.join(', ')}`);
|
|
29
|
+
const unread = named.filter((name) => !read.includes(name));
|
|
30
|
+
if (unread.length > 0)
|
|
31
|
+
problems.push(`.env.example names what nothing reads: ${unread.join(', ')}`);
|
|
32
|
+
return problems;
|
|
33
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { freePort, spawnNode, startApp, type AppProcess, type LogLine, type RunningApp } from './app.js';
|
|
2
|
+
export { connectionTest } from './connection.js';
|
|
3
|
+
export { HYDRATED, newCaller, open, waitForHydration, type PageLike } from './browser.js';
|
|
4
|
+
export { createTestDatabase, type TestDatabase } from './database.js';
|
|
5
|
+
export { envExampleProblems } from './env-example.js';
|
|
6
|
+
export { shopifyConnectionTest } from './shopify-connection.js';
|
|
7
|
+
export { startShopify, type AdminAnswer, type DoubleShop, type ShopifyDouble } from './shopify.js';
|
|
8
|
+
export { linkIn, startSmtp, type ReceivedMail, type SmtpDouble } from './smtp.js';
|
|
9
|
+
export { startStripe, stripeSignature, type DoubleEndpoint, type DoublePrice, type DoubleSession, type DoubleSubscription, type StripeDouble } from './stripe.js';
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { freePort, spawnNode, startApp } from './app.js';
|
|
2
|
+
export { connectionTest } from './connection.js';
|
|
3
|
+
export { HYDRATED, newCaller, open, waitForHydration } from './browser.js';
|
|
4
|
+
export { createTestDatabase } from './database.js';
|
|
5
|
+
export { envExampleProblems } from './env-example.js';
|
|
6
|
+
export { shopifyConnectionTest } from './shopify-connection.js';
|
|
7
|
+
export { startShopify } from './shopify.js';
|
|
8
|
+
export { linkIn, startSmtp } from './smtp.js';
|
|
9
|
+
export { startStripe, stripeSignature } from './stripe.js';
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { expect as Expect, test as Test } from '@playwright/test';
|
|
2
|
+
import type { ProductEnv } from '../product.js';
|
|
3
|
+
import type { ShopifyApp } from '../shopify/app.js';
|
|
4
|
+
/** Registers the connection test of `app` with the test and expect of the app's Playwright. */
|
|
5
|
+
export declare function shopifyConnectionTest<E extends ProductEnv>(app: ShopifyApp<E>, playwright: {
|
|
6
|
+
test: typeof Test;
|
|
7
|
+
expect: typeof Expect;
|
|
8
|
+
}): void;
|