Changelog¶
All notable changes to this project are documented here.
The format follows Keep a Changelog and this project adheres to Semantic Versioning.
0.5.2 — 2026-09-08¶
Python package only — @spltz/viur-testing (npm) is unchanged and stays at
0.5.0. Nothing in the runner contract moved: the token is derived the same way,
/_test/config/status answers the same payload, and the changes below are a
viur-mirror robustness fix, a token-lifecycle fix and reworded rejections.
Fixed¶
-
viur-mirrorno longer overflows the commit payload limit.copy_kindbatched on the mutation count alone (PUT_BATCH_SIZE = 500), but a Datastore commit is capped twice — 500 mutations and ~11 MiB of request payload. Runs over large entities aborted withINVALID_ARGUMENT: Request payload size exceeds the limit: 11534336 bytes. The batch now also tracks the serialized size and commits before the byte budget (PUT_BATCH_BYTES, 8 MiB) is spent. Lowering the entity count is not an equivalent fix: entity sizes are skewed, so the same count weighs orders of magnitude differently depending on which entities land together. -
An entity too large to copy no longer aborts the whole run. Datastore rejects any single entity above 1 MiB, and the limit applies to the clone, which is bigger than its source: re-keying writes the target partition (database id, namespace) into the entity key and into every embedded relation key. Measured on an entity with 1951 relation keys, the clone grew from 1 048 393 to 1 163 607 bytes — from just under the limit to over it. Such entities cannot be mirrored at all;
copy_kindnow records and skips them andmainlists them by kind, key and size at the end of the run, instead of losing every kind that had not been copied yet. -
The daily token now really survives a server restart. 0.5.1 made the token value deterministic — derived from database, namespace, project id and the UTC day — and the documentation promises that a cookie armed once in the morning keeps working until UTC midnight, restarts included. The value did survive; the process's knowledge of it did not.
ConfigModule._tokenwas only ever set bystatusandenter, so a restarted process heldNoneand answered every request with403 viur-test: no session token issued yet — call /_test/config/status first, while the browser was holding a cookie that was still valid for the rest of the day.
activate() now primes today's token through the new
ConfigModule.prime_daily_token(), before the request validator goes live.
Manual browsing across a restart works without re-arming; the Playwright suite
was never affected, because its global setup posts to status on every run.
The token is derived here, with no datastore roundtrip: activation already
probes the database, and the value is a pure function of state the process
holds — no reason to put another way to fail into the boot path. The token
entity is still written by status/enter as before.
This does not widen access. The token is not a secret (status hands it to
any local caller) and the real protection remains the production guard and the
dev-server check; the previous 403 was a lifecycle artifact, not a gate.
finish still clears the token and ends the session until the next
status/enter.
- A rejected request now names an endpoint you can actually open. The
three
TokenValidatorrefusals described the failure but not the remedy, and the one that tried pointed the wrong way: "no session token issued yet — call/_test/config/statusfirst" is unreachable advice for the case that produces it, becausestatusis@force_postand the reader is a human in a browser. All three now name/json/_test/config/enter, the GET route manual browsing is armed with;statusis mentioned only as the test runner's entry point, alongside it. The mismatch case also says why a cookie goes stale — the token rotates at UTC midnight — since that is the one branch a developer hits without having changed anything.
Messages only. Which requests pass and which are refused is unchanged, and the
new ENTER_PATH / STATUS_PATH constants are documentation strings, not
routing.
Changed¶
-
copy_kindgained the keyword argumentsbatch_bytes,max_entity_bytesandskipped. The three size limits now default toNoneand are resolved from the module constants at call time rather than bound at definition time, so overridingcli.PUT_BATCH_SIZEand friends actually takes effect. Passingbatch_size=explicitly keeps working unchanged. -
The
viur-mirrordocumentation gained a Size limits section explaining the clone inflation and what a skipped entity means for the copied slice.
0.5.1 — 2026-06-17¶
Python package only — @spltz/viur-testing (npm) is unchanged and stays at
0.5.0; it reads the token from /_test/config/status regardless of how the
token is generated.
- Deterministic per-day session token. The token is now derived from the
session identity (database, namespace, project id) plus the current UTC day,
so it is identical all day and across server restarts,
finishcalls and re-issues, and rotates at UTC midnight. A cookie armed once via/_test/config/enter(or set by the Playwright fixtures) therefore stays valid for the whole day. It is not a secret —/_test/config/statushands it out freely; the production guard + dev-server gate remain the real protection.
0.5.0 — 2026-06-16¶
Combined Python + @spltz/viur-testing (npm) release. Switches the test-token
transport from a header to a cookie (so manual browsing finally works on a
hard navigation), simplifies the env var to a single value, and removes the
tokenless dev mode and the Vite token plugin.
- BREAKING — cookie-first token transport. The session token now travels as
a
viur-test-tokencookie (SameSite=Strict; HttpOnly; Path=/) instead of theX-Viur-Test-Tokenheader. TheTokenValidatoraccepts only the cookie. A newGET /_test/config/enterendpoint sets the cookie for manual browsing — hard navigations, reloads and server-rendered pages included, with the token still fully enforced. npm: the fixtures set the cookie viacontext.addCookies;authenticatedApi/backendApisend it as aCookieheader;finishTestMode/finish()send no token (bootstrap endpoint). - BREAKING — single-value env var.
VIUR_TESTING=1(ortrue/on) = on, default namespace; any other value is the namespace verbatim (VIUR_TESTING=ak). The former<mode>[:<namespace>]grammar and thetest/devkeywords are gone —test/devare now ordinary namespace names.setup()drops themodeparameter. - BREAKING (npm) — Vite token plugin removed.
viurTestingTokenFetch/withTokenInjectionare gone; the dev-server proxy is now plain (the browser carries the cookie).viteis no longer a peer/dev dependency of the package. The scaffoldedvite.e2e.config.tsis a plain proxy. - Removed — tokenless dev mode. The
devmode,arm_tokenless_browsing,setup(tokenless_app_ids=…)and the PIN-at-boot path are gone — manual browsing is the cookie flow above. Dev-Mirror seeding (viur-mirror) is unchanged (still PIN-gated); bootVIUR_TESTING=<namespace>to work in your slice. - BREAKING — cookie-based production guard; legacy header fully gone. The
TOKEN_HEADERconstant and its export are removed. TheProductionGuardValidatornow watches theviur-test-tokencookie: on a non-dev server any request carrying it gets an immediate 403 (in dev theTokenValidatorowns the cookie). Nothing in the package reads theX-Viur-Test-Tokenheader any more. - Unchanged — Datastore isolation, Guarded Mode.
Migration¶
# env var
VIUR_TESTING=test → VIUR_TESTING=1
VIUR_TESTING=test:ak → VIUR_TESTING=ak
VIUR_TESTING=dev:ak → VIUR_TESTING=ak # dev/tokenless gone
# main.py — drop the removed setup() kwargs
viur.testing.setup(tokenless_app_ids=[...]) → viur.testing.setup()
# regenerate the e2e scaffold (plain Vite proxy, no token injection)
npx viur-testing-init
Note: a leftover VIUR_TESTING=test now selects a namespace named
"test", not "on, default namespace" — change it to VIUR_TESTING=1. To
browse the test instance by hand, navigate once to
/_test/config/enter (sets the cookie), then browse normally.
0.4.0 — 2026-06-16¶
Combined Python + @spltz/viur-testing (npm) release. This version unifies
test-mode activation behind a single env var, adds the Development mode
(live-data mirroring + tokenless browsing), streamlines the
viur-testing-init scaffolder, and ships a bilingual (EN/DE) documentation
site.
-
BREAKING (Python) — one boot env var.
VIUR_TESTING_ENABLE,VIUR_TESTING_NAMESPACEandVIUR_TESTING_TOKENLESScollapse into a singleVIUR_TESTING=<mode>[:<namespace>](mode=testordev;1/true/onaliastest; unset/0/off/false= off).devmode now requires a namespace.setup()drops the three*_env_varparams and gainsenv_var(defaultVIUR_TESTING) plus explicitmode/namespaceoverrides.VIUR_TESTING_ENABLE=1 → VIUR_TESTING=test …_ENABLE=1 …_NAMESPACE=ak → VIUR_TESTING=test:ak …_ENABLE=1 …_NAMESPACE=ak …_TOKENLESS=1 → VIUR_TESTING=dev:ak
-
Development mode (Python). New
viur-mirrorconsole script seeds a per-developer namespace ofviur-testsfrom the live(default)database — read-only source client, viur-core secret/system kinds excluded (viur-confincl. hmacKey,viur-session,viur-securitykey,viur-relations,file/file_rootNode/viur-blob-locks), keys and relations remapped onto the target partition, PIN-gated, never writes(default). BootingVIUR_TESTING=dev:<ns>then PIN-arms tokenless browsing of that slice for whitelisted dev servers (setup(tokenless_app_ids=…)/arm_tokenless_browsing(...)), so a full dev environment incl. Admin works without theX-Viur-Test-Tokenheader — only ever on theviur-testsslice. -
BREAKING (npm) — streamlined
viur-testing-init. The scaffolder no longer prompts for a mode; it always scaffolds a Test Mode suite (the--mode/--guardedflags and the Guarded-Mode preset are gone — Guarded Mode remains as a runtime auto-detect). It auto-locates the project root by walking up for adeploy/directory and proposes<root>/testing/e2e(confirmable/overridable), and additionally generates the backend test-API packagetesting/api/__init__.py+ an exampletesting/api/user.py(UserTestApiwithsetup/teardown). -
npm — dependency refresh.
vitepeer range widened to^5 || ^6 || ^7 || ^8; dev toolchain bumped (TypeScript 6,@types/node25,@playwright/test1.61, Vite 8). -
Docs — bilingual site (EN/DE). Added
mkdocs-static-i18n(folder structure, Material language switcher); API reference stays English (generated from docstrings) with the German nav redirecting to it; the Dev-Mirror Mode page is renamed Development Mode; SVG logo + green favicon added.
0.3.0 — 2026-05-28¶
@spltz/viur-testing only — the Python package stays at 0.2.0.
Introduces Guarded Mode: automatic detection of whether the
target backend is in test mode, with an interactive PIN gate for
the live-backend case. Lets Playwright drive read-only smoke tests
against a deployed application without spinning up a dedicated
test backend, while keeping the bilateral guarantee for everything
that does have one.
Added¶
- Auto mode detection in
createGlobalSetup()—POST /json/_test/config/statusdecides the run mode: - 200 + valid payload → Test Mode (unchanged flow).
- 404 → Guarded Mode + interactive PIN challenge.
- 5xx / timeout / malformed / integrity-fail → hard error, never a silent downgrade.
- 6-digit PIN challenge (
runPinChallenge): fresh code per run, displayed yellow + space-separated above the backend URL. Wrong PIN → abort. No TTY → "Run from an interactive terminal." No persisted ACK file, no env-var bypass — every run is its own human-in-the-loop decision. - Mode propagation via
VIUR_TESTING_MODEenv var (exported asMODE_ENV_VAR). Workers inherit and the fixtures branch on it. - New public exports:
detectMode,probeStatusEndpoint,runPinChallenge+ their option types, for hosts that want to build custom setup flows on top. createGlobalSetup({ backendUrl })— explicit option in addition to the existingE2E_BACKEND_URLenv var (option wins when both are set).viur-testing-initscaffolder picks the mode interactively. On a TTY the CLI asks[1] Test Mode / [2] Guarded Mode, defaulting to Test. Non-TTY runs (CI scaffolding) default to Test silently. Skip the prompt with--mode test|guardedor the--guardedshortcut. The Guarded preset drops Vite,.env.e2e, and theserverStatus-using example spec; the generatedplaywright.config.tspointsbaseURLat the deployed backend instead of a local Vite dev server.
Changed¶
contextfixture is mode-aware. In Test Mode it injects theX-Viur-Test-Tokenheader as before; in Guarded Mode it returns a vanilla browser context (no headers, no overrides) so Playwright behaves like a real browser against the live application.serverStatus,backendApifixtures auto-skip in Guarded Mode viatestInfo.skip(true, "uses _test infrastructure, ..."). The consuming test counts as skipped, not failed — the spec stays valid in both modes without conditional code.callTestModule/callTestModuleRawauto-skip in Guarded Mode viatest.skip(...). Called fromtest.beforeAllskips all tests in the describe; called from a test body skips that test only.global-teardownis a no-op in Guarded Mode — no session token was issued, nothing to release.
Documentation¶
- Top-level README has a new "Guarded Mode" section pointing at the Playwright README for details.
- Playwright README has a full auto-detect table and the Guarded-Mode contract (TTY-required, no persisted ACK, what auto-skips).
- mkdocs site has a dedicated
Guarded Modepage (docs/guarded-mode.md) with the auto-detect table, PIN display/input rules, fixture auto-skip semantics and the--guardedscaffold flag. - Getting Started + top-level README runner section rewritten
around the npm package. The pytest
conftest.pyexample is gone — the canonical e2e wiring is nownpx viur-testing-init→npm test. The Python primitives (require_test_mode,finish,ServerStatus) remain documented in the API reference for hosts that drive their own Python-side runner.
Internal¶
test-mode.ts:requireTestModeis now a thin wrapper over a sharedprobeStatusEndpointhelper that returns{ kind: "armed", status } | { kind: "unarmed" }. Both the explicit "require" path and the auto-detect path use it.- New files:
pin-challenge.ts(interactive 6-digit gate with injectable IO surface for tests) andmode-detect.ts(detectModeorchestrator).
Quality¶
- TypeScript smoke harness covers the three probe outcomes
(armed/unarmed/ambiguous), the PIN challenge (success / wrong
PIN / no-TTY), the mode-detect dispatcher, and the
globalSetupenv-var plumbing. All 17 checks green.
0.2.0 — 2026-05-28¶
Post-design audit: tightens the bilateral guarantee, mostly on the
runner-side TypeScript half, and normalises namespace handling
end-to-end. No public API breaks; tokenFile is the only removal.
Added¶
- TS runner:
expectedProjectIdoption +E2E_TEST_PROJECT_IDenv var, mirroring Python'sexpected_project_id. - TS runner: SHA-256
token_hashverification and runtimeis_dev_servercheck inrequireTestMode— closes parity gap with Python'srequire_test_mode. - TS Vite plugin: cached token auto-refreshes on observed HTTP
403 and after a configurable TTL (default 1 h). New option
refreshIntervalMs(0 disables TTL refresh; 403 path always on). - TS fixtures: worker-scoped
_viurTestingStatusreads.auth/token.jsononce per worker;serverStatus/context/backendApiconsume it (was 3× per test).
Changed¶
- Python: namespace
""→Nonenormalisation now applied inactivate(),ConfigModule.set_active(), andrequire_test_mode()— was previously only insetup(). - Python:
closed_system_allowed_pathsuses renderer-agnostic wildcards*/_test/*+_test/*(wasjson/_test/*only). - Python:
TokenValidatorbootstrap-path check is now an exact segment-shape match (/<renderer>?/_test/config/<action>) instead of a permissivepath.endswith(). - Python:
register_test_submoduleenforces^[a-z][a-z0-9_-]*$and rejects names shadowing existingTestModule/Moduleattributes (e.g.json,handler). - Python: banner injection detects viur-core's banner width at runtime (falls back to 80 if detection misses).
- Python:
_load_project_apiwalks past everyviur.testingframe instead of hard-codinginspect.stack()[2]. - TS:
expectedNamespace=""is normalised tonull, matching the server-sideVIUR_TESTING_NAMESPACE=convention. - TS:
assertNoDirectPlaywrightImportsskipsnode_modules,.git,dist,build,coverage,playwright-report,test-results,.next; strips line + block comments before scanning; clear error whentestsDirdoes not exist. - TS:
finishTestModesendsX-Viur-Test-Token(symmetry with Python'sfinish()). - TS:
callTestModuleRawcookies carry the full PlaywrightCookieshape (sameSite,secure,httpOnly,expiresadded), derived fromAPIRequestContext.storageState(). - TS:
viur-testing-initpins generatedpackage.jsonto^<own-version>instead of"*". - TS:
viur-testing-initvite.e2e.config.tstemplate is now stand-alone; the Sprengplatz-specificappConfigoverlay moved into a commentedOVERLAYblock at the bottom. Thedev:frontendscript is dropped from the default scripts.
Removed¶
- TS:
tokenFileoption oncreateGlobalSetup/createGlobalTeardown. The path was a footgun — changing it silently broke the fixtures that hard-coded.auth/token.json. A single internaltokenFilePath()helper is now the source of truth across globalSetup, globalTeardown, fixtures, and test-module helpers.
Fixed¶
- Python:
_patch_key_factory+_patch_legacy_urlsafeare now idempotent — repeatedactivate()no longer stacks wrapper layers (relevant for test re-entry).
Documentation¶
- README clarifies token persistence: server side never writes
to disk; runner side caches under
.auth/token.json+process.env.E2E_TEST_TOKEN. - Playwright README documents Vite token refresh and the
E2E_TEST_PROJECT_IDenv var; init-template description updated for the stand-alone Vite config.
Quality¶
- 212 pytest cases (was 187), 100% branch coverage held.
- TS smoke-test harness covers the preflight branches, the forbidden-imports walker, the Vite refresh paths, and the init scaffolder's version pin.
0.1.0 — initial design¶
The initial design pre-dates the audit round. Entries below document the design as shipped before 0.2.0.
Distribution¶
- The Python package ships on PyPI as
spltz-viur-testing(the experimentalspltz-prefix marks it pre-1.0). The Python import path staysviur.testing— namespace package, no rename in host code. Install withpip install spltz-viur-testing. - A companion npm package
@spltz/viur-testinglives inplaywright/next to the Python sources. It bundles Playwright fixtures, the global-setup / global-teardown factories, the forbidden-import guard, test-module helpers, the Vite plugin and aviur-testing-initCLI for scaffolding new e2e suites.
Test-mode activation¶
viur.testing.setup()andviur.testing.register_modules()— two one-liner host wrappers around the underlying primitives.main.pyis reduced toimport viur.testing; viur.testing.setup()andmodules/__init__.pytoviur.testing.register_modules(globals()). Both are no-ops in production (env var unset / state inactive).viur.testing.activate()— atomic test-mode activation. Order of checks:viur.core.db.transportnot yet imported, thenconf.instance.is_dev_servertrue, then builds adatastore.Client(database=…, namespace=…)against the test database, runs a synchronous probe roundtrip, patchestransport.__client__, monkey-patchesviur.core.db.types.Key.__init__to injectdatabase=/namespace=on every newly constructed Key, monkey-patchesgoogle.cloud.datastore.Key.to_legacy_urlsafeso it tolerates named databases, extendsconf.security.closed_system_allowed_pathswith broad_test/*wildcards, primesConfigModule, installs the request validator and wrapsviur.core.setupso the dev-server boot banner shows the test-mode parameters. No on-disk state.viur.testing.protect()andProductionGuardValidator— host installs the guard viaprotect()in every environment. On a non-dev server, any request carrying anX-Viur-Test-Tokenheader is rejected with 403 regardless of the header's value. In dev the guard is a no-op (the fullTokenValidatoralready handles the header).setup(api_dir="testing")— registers the project-side test API package as importable top-levelapiviaimportlib.util. The directory lives outsidedeploy/(sogcloud app deploynever uploads it) and is wired in by resolving<dirname(main.py)>/../<api_dir>/api/__init__.py. Walksinspect.stack()to anchor the path at the caller'smain.pyso the host needs nothing butviur.testing.setup(). Prints a one-line info message when the directory does not exist rather than crashing.
Datastore namespace isolation¶
activate(database=…, namespace=…)and theVIUR_TESTING_NAMESPACEenv var — partition writes inside oneviur-testsdatabase so several engineers (or CI runs) can share the database without colliding on each other's entities. Empty string or unset = no namespace (default Datastore namespace)._patch_key_factoryinjects bothdatabase=andnamespace=defaults intoviur.core.db.Key.__init__, so every Key viur-core builds during test mode points at the same slice as the active client._patch_legacy_urlsafemonkey-patchesgoogle.cloud.datastore.Key.to_legacy_urlsafeto temporarily clearself._databasearound the legacy serialisation. Without this every successful login crashed the server (viur-core usesstr(key)in session save + JSON renders, which callsto_legacy_urlsafewhich refuses named databases). The patch restores_databaseinfinallyso the key stays consistent even on exception paths.ConfigModule.current_namespace()and_namespaceclass state — reported back to runners via/json/_test/config/statusso the preflight can assert the expected namespace.require_test_mode(expected_namespace=…)— runner-side check with_UNSETsentinel: pass a string for an exact namespace,Noneto assert the default namespace, omit the field to skip the check entirely.
Bilateral session handshake¶
viur.testing._test.TestModule— host-mountable container module under/_test. Carriesjson = Trueso viur-core's__build_appregisters it under the JSON renderer. Refuses to instantiate outside a local dev server or whenactivate()has not run yet — structural last line of defence against accidental production mounts and silent "mounted-but-unactivated" states.viur.testing._test.config.ConfigModule— the bootstrap config submodule mounted as/_test/config. Carries the per-process class-level state (_database,_namespace,_project_id,_token,_status_hooks,_finish_hooks) and the same two mount guards asTestModuleso a host that bypasses the container and mountsConfigModuledirectly is still caught. Endpoint bodies are emitted as JSON strings (Content-Type: application/json+json.dumps) so viur-core's WSGI layer forwards a proper JSON body rather than a Pythonreprof a dict. Exposes two endpoints:POST /json/_test/config/status— re-verifies dev-server + datastore database, then reads/creates the session token entity in the test database (kindviur-tests, entityauth-token) and returns it to the runner. Response carriestoken,token_hash,database,namespace,project_id,version, plus any extra keys returned from status hooks. Idempotent. POST-only so a parallel browser tab cannot drive-by trigger the endpoint via a simple GET (CORS preflight stops the cross-origin POST).POST /json/_test/config/finish— re-verifies, deletes the token entity, clears the in-process token. Response includes extra keys returned from finish hooks. Test-mode itself stays armed.viur.testing.validator.TokenValidator—RequestValidatorrejecting every non-bootstrap request that lacks a matchingX-Viur-Test-Tokenheader (constant-time compare). Auto-installed byactivate(). Paths ending in/_test/config/statusor/_test/config/finishbypass the token check so the runner can bootstrap a session before one exists.viur.testing.require_test_mode()— runner preflight: calls/json/_test/config/status, verifies test-mode + dev-server + database (and optionally namespace + project_id), checks the token's sha256 matches the server-reportedtoken_hash, returns aServerStatuswith all session info.viur.testing.finish()— runner cleanup: deletes the token entity viaPOST /json/_test/config/finish.
Host-registered test fixtures¶
viur.testing.register_test_submodule(name, cls)— registers a project-specific submodule that mounts under/_test/<name>/…alongside the built-inconfig. Names are normalised to lowercase (viur-core lower-cases URL segments at request time),configis reserved, empty names are refused. Late registration via class-level dict onTestModule, consumed at mount time.viur.testing.register_status_hook(hook)andregister_finish_hook(hook)— let project code attach callbacks that run inside the/json/_test/config/statusand…/finishendpoints. Hook signature is() -> dict | None; returned dicts are merged into the JSON response (later hooks win on key conflicts). Use for project-specific test-mode prep (feature flags, seed-data references) and for surfacing extra info to runners. Side effects onviur.core.confare allowed.
Dev-server boot banner¶
viur.testing.banner.install_banner_patch(database, namespace)— wrapsviur.core.setup()so itsLOCAL DEVELOPMENT SERVER IS UP AND RUNNINGASCII banner gains two extra lines:database = …(always) andnamespace = …(rendered as(default)when not set). Pattern-matches the banner title and trailer instead of hard-counting line indices, so a future viur-core banner format change degrades gracefully. Idempotent — re-entry fromactivate()does not stack wrappers.
Top-level package surface¶
viur.testing/__init__.pyre-exports only the viur-core-free surface:activate,protect,setup,register_modules,register_test_submodule,register_status_hook,register_finish_hook,require_test_mode,finish, plus dataclasses (ServerStatus), errors (TestModePreflightError) and constants (TOKEN_HEADER,DEFAULT_DATABASE). The heavy classes (TestModule,ConfigModule,TokenValidator,ProductionGuardValidator) live in their concrete submodules and must be imported from there — soimport viur.testingdoes not triggerviur.corebefore the datastore client swap.
Quality¶
- pytest suite against
viur-light-mock+ local stubs, 100% line + branch coverage required. 182 tests at the time of writing. smoke_test.py— end-to-end script that walks every refuse path in a fresh subprocess and additionally exercisesactivate()all the way through to a realdatastore.Client(database= "viur-tests")+ probe roundtrip when run against a workstation with valid GCP credentials.
Lessons learned from booting against a real viur-core project¶
These surprises only surfaced when the package was wired into the
real deploy/ project and not into mocks. Each one is now built
in:
- Reserved Datastore kind prefix. Original
PROBE_KINDwas__viur_test_probe__. Google Cloud Datastore reserves__*__for system-internal use and 400s the write withThe kind … is reserved. Final name isviur-test-probe. - Multi-database Key construction. viur-core's
Keyclass forwardsproject=togoogle.cloud.datastore.Keybut notdatabase=ornamespace=. With a named-DB client every Key viur-core builds is for the default database, and Datastore rejects the call withmismatched databases within request. Solved by_patch_key_factorywrappingKey.__init__. to_legacy_urlsaferefuses named databases. viur-core'sKey.__str__callsto_legacy_urlsafe(), which raises on any key withdatabaseset. Triggers on session-save and login-success JSON render. Solved by_patch_legacy_urlsafe.- viur-core lower-cases URL segments at routing time.
register_submodule("userLogin", …)would register under the mixed-case key but the router looks upuserlogin, so the route silently 404'd.register_submodulenow lower-cases the key. - Closed-system gate. Many host projects set
conf.security.closed_system = True. Once on, every URL not inclosed_system_allowed_pathsreturns 401 before the route is resolved.activate()extends the allow-list with broad_test/*/*/_test/*wildcards so both the built-in bootstrap and host-registered fixture submodules pass through; theTokenValidatoris the actual access control. - Render-name opt-in.
__build_apponly registers a module class for a given renderer ifgetattr(cls, render_name, False)is truthy. WithoutTestModule.json = Truethe routes are silently not mounted, and the actual HTTP URL becomes/json/_test/config/…(JSON renderer prefix) — not/_test/config/…as one might expect from the module hierarchy. - Class vs. instance in modules namespace.
__build_appiteratesvars(modules)and only picks up subclasses ofModule(or already-instancedInstancedModule). A bare module instance is silently skipped. The host-side wiring registersTestModuleas a class, not as an instance.