No description
Find a file
Uwe Schuster 5ee6894916 #7: Add tests/e2e/ for the initial password-setup flow (Option A1)
Closes the integration gap that let two prior regressions ship:
  1. oatpp-authkit query-string 401 (v0.3.3 / commit 46971ac)
  2. VITE_BASE blank page (v0.3.6 / commit b1a13b8)

A1 scope: skips the host-provisioning side of new-project.sh (root,
systemd, Apache, Forgejo). Instead clones webapp-template into a tmp
dir, builds with VITE_BASE pinned to /projects/tmp-foo/, boots the
binary on an ephemeral port, fronts it with an in-process
PrefixStrippingProxy that mirrors the production Apache vhost. Tests
then drive the same flow a real user would.

Files:
- tests/e2e/proxy.py — stdlib-only reverse proxy (~100 LOC,
  ThreadingHTTPServer + urllib). Strips the /projects/<name>/ prefix
  and sets X-Forwarded-Prefix exactly like Apache's ProxyPass.
- tests/e2e/conftest.py — webapp_template_src / built_webapp /
  boot_app / proxy / admin_token fixtures. Honours
  WEBAPP_TEMPLATE_DIR + WEBAPP_TEMPLATE_BUILD_DIR env vars so CI can
  point at a pre-built tree to skip the build step.
- tests/e2e/test_password_setup.py — three assertions per #7:
    - /set-password?token=… returns HTML, not JSON 401
    - every <script src>/<link href> resolves through the prefix
    - /api/* still returns JSON 401 (sanity-check negotiation)

No Selenium dependency — the assertions are HTTP-level and reliable
in CI without a Chrome/Geckodriver setup. Selenium can be added later
for actual form-submission coverage if needed.

Test runs are skipped automatically when webapp-template source is
absent, so the suite is safe to drop into any pytest invocation.

Closes #7

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 22:23:30 +02:00
bin gitignore: drop accidentally-committed __pycache__/.pytest_cache 2026-04-25 21:38:32 +02:00
src #5: createCoreFetch default responseShape flipped to 'body' 2026-04-25 21:47:40 +02:00
templates v0.1.0: fetch/postprocess/inject scripts + vite config factories 2026-04-21 22:06:58 +02:00
tests #7: Add tests/e2e/ for the initial password-setup flow (Option A1) 2026-04-25 22:23:30 +02:00
.gitignore gitignore: drop accidentally-committed __pycache__/.pytest_cache 2026-04-25 21:38:32 +02:00
package-lock.json #4: vitest harness + suites for createCoreFetch / createI18n / useI18nStore 2026-04-25 21:45:31 +02:00
package.json #5: createCoreFetch default responseShape flipped to 'body' 2026-04-25 21:47:40 +02:00
README.md #5: createCoreFetch default responseShape flipped to 'body' 2026-04-25 21:47:40 +02:00
tsconfig.json v0.3.1: ship compiled dist/ so npm git installs work without TS stripping 2026-04-21 22:23:36 +02:00
vitest.config.ts #4: vitest harness + suites for createCoreFetch / createI18n / useI18nStore 2026-04-25 21:45:31 +02:00

@uschuster/webapp-scaffold

Shared frontend build glue for webapp-template-derived projects.

What's in v0.1

Path Purpose
bin/fetch-openapi.sh Pull the oatpp Swagger spec from a running backend. Configurable via OPENAPI_URL / APP_URL / APP_TEST_PORT, optional Bearer via APP_API_KEY. JSON-validated.
bin/postprocess-openapi.py Clean up oatpp 1.3's rough edges (missing operationIds, missing tags) before handing the spec to Orval.
bin/inject-hashed-filenames.py Rewrite HTML script tags to point at Vite's manifest-declared hashed bundle. Config-driven so projects with multiple entry points (admin + guest) use a single invocation.
src/vite-config.ts defineAdminConfig({root, vendorChunks?, outDir?}) and defineGuestConfig({root, entry?, vendorChunks?, outDir?}) helpers that bake in the VITE_BASE-driven prefix convention, manifest output, and the static/{dist,guest/dist} output layout the StaticController expects.
templates/orval.config.template.ts Starting orval.config.ts for derived projects to copy-and-tweak.

Install

{
  "devDependencies": {
    "@uschuster/webapp-scaffold": "^0.1.0"
  }
}

Register the internal Forgejo npm registry (see your ~/.npmrc):

@uschuster:registry=http://127.0.0.1:3000/api/packages/uwe.admin/npm/

Consumer wiring

// frontend/vite.config.ts
import { defineAdminConfig } from '@uschuster/webapp-scaffold';
export default defineAdminConfig({ root: __dirname });
# frontend/package.json > scripts > codegen
webapp-scaffold-fetch-openapi && \
webapp-scaffold-postprocess-openapi && \
orval

createCoreFetch (v0.4 — default flipped to 'body')

Orval's client: 'fetch' calls a mutator coreFetch<T>(url, init). The return shape is configurable; the default is 'body' (returns the parsed JSON body directly, matching Orval's includeHttpResponseReturnType: false). Pass responseShape: 'wrapped' to opt back into {data, status, headers} when callers need the Response metadata. createCoreFetch(opts) returns such a function, with credentials/CSRF headers / 204 / content-type handling already wired, and hooks for project-specific concerns:

import { createCoreFetch } from '@uschuster/webapp-scaffold/core-fetch';

export const coreFetch = createCoreFetch({
    baseUrl: import.meta.env.BASE_URL,
    syncTables: new Set(['bookings', 'persons', 'contacts']),
    onEnqueue: (req) => syncQueue.push(req),       // returns true ⇒ skip network
    on401:     () => window.location.assign('/admin'),
    on409:     (_req, body) => ({ conflict: JSON.parse(body) }),
});

i18n (v0.3)

Two axes: locale (de, en, …) × tone (formal, informal). Bundles are keyed by <locale> (fallback) and <locale>-<tone> (primary); the resolver walks <cur-locale>-<cur-tone><cur-locale><default-locale>-<default-tone><default-locale> → the raw key (missing keys log via an onMiss hook instead of breaking render).

// Define bundles — only strings that differ between tones need a
// tone-specific entry. Everything else lives in the locale bundle.
import { createI18n }    from '@uschuster/webapp-scaffold/i18n';
import { useI18nStore }  from '@uschuster/webapp-scaffold/i18n-react';  // React-only

const bundles = {
    'de':          { greeting: 'Willkommen',  see_more: 'Mehr anzeigen' },
    'de-informal': { greeting: 'Hi!' },
    'en':          { greeting: 'Welcome',     see_more: 'See more' },
} as const;

export const i18n = createI18n<typeof bundles['de']>({
    bundles,
    defaultLocale: 'de',
    defaultTone:   'formal',
    onMiss: (key, loc, tone) =>
        console.warn(`[i18n] missing ${String(key)} @ ${loc}-${tone}`),
});

// React — components re-render on setLocale / setTone:
export const useI18n = () => useI18nStore(i18n);

// In a component:
const { t, setTone } = useI18n();
return <button onClick={() => setTone('informal')}>{t('greeting')}</button>;

SSR-safe: pass initialLocale / initialTone from the server render. Tree-shakeable: only the bundles you import ship in the bundle.