| @@ -11,6 +11,7 @@ | |||||
| <public> | <public> | ||||
| <method name="Run"> | <method name="Run"> | ||||
| <parameter name="ctx"/> | <parameter name="ctx"/> | ||||
| <parameter name="applicationName"/> | |||||
| <parameter name="statusLine"/> | <parameter name="statusLine"/> | ||||
| <parameter name="contentType"/> | <parameter name="contentType"/> | ||||
| <parameter name="body"/> | <parameter name="body"/> | ||||
| @@ -25,12 +26,12 @@ Option Explicit | |||||
| ' (COM/method failure -> 500) outcomes, and one place logs them. ctx is our | ' (COM/method failure -> 500) outcomes, and one place logs them. ctx is our | ||||
| ' own WscMvc.RequestContext object (not an ASP intrinsic), carrying only | ' own WscMvc.RequestContext object (not an ASP intrinsic), carrying only | ||||
| ' primitive request data. No ASP intrinsics are referenced here. | ' primitive request data. No ASP intrinsics are referenced here. | ||||
| Sub Run(ctx, statusLine, contentType, body) | |||||
| Sub Run(ctx, applicationName, statusLine, contentType, body) | |||||
| Dim ctrl, helloBody, path | Dim ctrl, helloBody, path | ||||
| path = ctx.Path | path = ctx.Path | ||||
| If path = "/hello" Then | |||||
| If applicationName = "production" And path = "/hello" Then | |||||
| Set ctrl = Nothing | Set ctrl = Nothing | ||||
| On Error Resume Next | On Error Resume Next | ||||
| Set ctrl = CreateObject("WscMvc.HomeController") | Set ctrl = CreateObject("WscMvc.HomeController") | ||||
| @@ -64,7 +65,7 @@ Sub Run(ctx, statusLine, contentType, body) | |||||
| contentType = "text/html; charset=utf-8" | contentType = "text/html; charset=utf-8" | ||||
| body = helloBody | body = helloBody | ||||
| Set ctrl = Nothing | Set ctrl = Nothing | ||||
| ElseIf path = "/self-test" Then | |||||
| ElseIf applicationName = "tests" And path = "/self-test" Then | |||||
| Set ctrl = Nothing | Set ctrl = Nothing | ||||
| On Error Resume Next | On Error Resume Next | ||||
| Set ctrl = CreateObject("WscMvc.SelfTestController") | Set ctrl = CreateObject("WscMvc.SelfTestController") | ||||
| @@ -4,11 +4,12 @@ A small, WSC-first MVC framework for Classic ASP on IIS: VBScript Windows Script | |||||
| ## Layout | ## Layout | ||||
| Two separate IIS sites, sharing one set of framework/controller COM components: | |||||
| Two separate IIS sites sharing only the framework components: | |||||
| - `public/` — the production site's IIS physical path. Contains only `Default.asp` and `web.config`. | - `public/` — the production site's IIS physical path. Contains only `Default.asp` and `web.config`. | ||||
| - `test-app/public/` — a second, separate site exposing `GET /self-test` (JSON test harness), so diagnostics aren't reachable on the production site/port. See `docs/ARCHITECTURE.md`. | - `test-app/public/` — a second, separate site exposing `GET /self-test` (JSON test harness), so diagnostics aren't reachable on the production site/port. See `docs/ARCHITECTURE.md`. | ||||
| - `Framework/`, `Controllers/` — the actual WSC components (`.wsc`), registered once via COM and used by both sites. Never served over HTTP by either site. | |||||
| - `Framework/` — shared WSC components, registered once via COM and used by both sites. | |||||
| - `Controllers/` — production-owned controllers; `test-app/Controllers/` — test-app-owned controllers. Neither app can activate the other's routes, including through direct `Default.asp?route=...` requests. None of these source folders is served over HTTP. | |||||
| - `logs/`, `test-app/logs/` — each site's own runtime log, written by the app, never served over HTTP. | - `logs/`, `test-app/logs/` — each site's own runtime log, written by the app, never served over HTTP. | ||||
| - `tests/`, `tools/`, `docs/` — test scripts, deployment/registration tooling, and documentation. Never served over HTTP. | - `tests/`, `tools/`, `docs/` — test scripts, deployment/registration tooling, and documentation. Never served over HTTP. | ||||
| @@ -26,7 +27,7 @@ Both `Setup-Site.ps1` invocations are idempotent — safe to re-run after any de | |||||
| ```powershell | ```powershell | ||||
| cscript //nologo tests\Test-Components.vbs # WSH smoke test, no IIS needed | cscript //nologo tests\Test-Components.vbs # WSH smoke test, no IIS needed | ||||
| powershell -File tests\Test-Http.ps1 -BaseUrl http://localhost:8090 # production site | |||||
| powershell -File tests\Test-Http.ps1 -BaseUrl http://localhost:8090 -TestBaseUrl http://localhost:8091 | |||||
| ``` | ``` | ||||
| ```bash | ```bash | ||||
| ./tests/run-self-test.sh http://<host>:8091 # test-app site, plain curl+JSON, any CLI | ./tests/run-self-test.sh http://<host>:8091 # test-app site, plain curl+JSON, any CLI | ||||
| @@ -28,28 +28,29 @@ Prefer a bootstrap with `Option Explicit`, no business logic, no includes, no em | |||||
| Revised 2026-09-19 (explicit user direction, superseding the original v0.1 tree): IIS's site physical path is a `public/` folder **only** — never the project root. Every other project directory is a sibling of the folder actually served, entirely outside the served tree — not reachable by IIS regardless of `web.config`, which is a stronger guarantee than request-filtering rules over a shared directory. `Default.asp` reaches sibling directories (e.g. `logs/`) via a parent-relative `Server.MapPath`, which requires `enableParentPaths=true` for the site. See `docs/DECISIONS.md` for the full rationale and `docs/ARCHITECTURE.md` for how it's wired up. | Revised 2026-09-19 (explicit user direction, superseding the original v0.1 tree): IIS's site physical path is a `public/` folder **only** — never the project root. Every other project directory is a sibling of the folder actually served, entirely outside the served tree — not reachable by IIS regardless of `web.config`, which is a stronger guarantee than request-filtering rules over a shared directory. `Default.asp` reaches sibling directories (e.g. `logs/`) via a parent-relative `Server.MapPath`, which requires `enableParentPaths=true` for the site. See `docs/DECISIONS.md` for the full rationale and `docs/ARCHITECTURE.md` for how it's wired up. | ||||
| Further revised 2026-09-19 (same day, same direction): the diagnostics/test harness (`GET /self-test`) is its own separate IIS site/app (`test-app/`), not a route on the production site. Both sites share the exact same `Framework/`/`Controllers/` COM components (registered once, globally) — nothing about the framework or business logic is duplicated, only the thin per-site `Default.asp`+`web.config` wiring exists twice (and `Default.asp` is intentionally byte-identical in both places; see `docs/DECISIONS.md`). | |||||
| Further revised 2026-09-19 (same day, same direction): the diagnostics/test harness (`GET /self-test`) is its own separate IIS site/app (`test-app/`), not a route on the production site. **Only `Framework/` is shared** between the two sites (registered once, globally via COM) — `Controllers/` is *not* shared: it holds application-specific business logic, so production's `Controllers/HomeController.wsc` and the test-app's own `test-app/Controllers/SelfTestController.wsc` are kept apart. Each site also owns its thin `Default.asp` bootstrap and passes its fixed application name into the shared framework, so a direct `Default.asp?route=...` request cannot activate the other app's routes; see `docs/DECISIONS.md`. | |||||
| ``` | ``` | ||||
| WSC-MVC/ | WSC-MVC/ | ||||
| public/ # production site's IIS physical path | public/ # production site's IIS physical path | ||||
| Default.asp | |||||
| Default.asp # selects only production routes | |||||
| web.config | web.config | ||||
| test-app/ | test-app/ | ||||
| public/ # test-app site's IIS physical path (separate site/port) | public/ # test-app site's IIS physical path (separate site/port) | ||||
| Default.asp # intentionally identical to public/Default.asp | |||||
| Default.asp # selects only test routes | |||||
| web.config # only routes /self-test | web.config # only routes /self-test | ||||
| Controllers/ | |||||
| SelfTestController.wsc # test-app-specific; NOT in the shared Controllers/ below | |||||
| logs/ | logs/ | ||||
| app.log # this site's own log, separate from the production one | app.log # this site's own log, separate from the production one | ||||
| Framework/ | |||||
| Framework/ # the ONLY folder shared between both sites | |||||
| Application.wsc | Application.wsc | ||||
| Router.wsc # phase 3 | Router.wsc # phase 3 | ||||
| RequestContext.wsc | RequestContext.wsc | ||||
| ResponseResult.wsc # phase 2; optional if simpler contract works | ResponseResult.wsc # phase 2; optional if simpler contract works | ||||
| ViewRenderer.wsc # phase 4 | ViewRenderer.wsc # phase 4 | ||||
| Controllers/ | |||||
| Controllers/ # production's own controllers, not shared with test-app/ | |||||
| HomeController.wsc | HomeController.wsc | ||||
| SelfTestController.wsc | |||||
| Views/ | Views/ | ||||
| Home.html # phase 4 | Home.html # phase 4 | ||||
| Layout.html # phase 4 | Layout.html # phase 4 | ||||
| @@ -8,7 +8,7 @@ GET /hello | |||||
| -> /Default.asp?route=/hello | -> /Default.asp?route=/hello | ||||
| -> Server.CreateObject("WscMvc.RequestContext"); ctx.Initialize path, httpMethod, logDir | -> Server.CreateObject("WscMvc.RequestContext"); ctx.Initialize path, httpMethod, logDir | ||||
| -> Server.CreateObject("WscMvc.Application") | -> Server.CreateObject("WscMvc.Application") | ||||
| -> Application.Run(ctx, statusLine, contentType, body) [Framework/Application.wsc] | |||||
| -> Application.Run(ctx, "production", statusLine, contentType, body) [Framework/Application.wsc] | |||||
| -> CreateObject("WscMvc.HomeController") | -> CreateObject("WscMvc.HomeController") | ||||
| -> HomeController.Hello(body) [Controllers/HomeController.wsc] | -> HomeController.Hello(body) [Controllers/HomeController.wsc] | ||||
| -> LogOutcome ctx, statusLine (best-effort append to logs/app.log) | -> LogOutcome ctx, statusLine (best-effort append to logs/app.log) | ||||
| @@ -18,9 +18,9 @@ GET /hello | |||||
| ## Component boundary contract | ## Component boundary contract | ||||
| - `Default.asp` is the only file that touches ASP intrinsic objects (`Request`, `Response`, `Server`). It contains no business logic — only reading the `route` query parameter and `REQUEST_METHOD`, resolving `logs/`'s physical path via `Server.MapPath`, invoking `WscMvc.RequestContext` and `WscMvc.Application`, and writing the response. | - `Default.asp` is the only file that touches ASP intrinsic objects (`Request`, `Response`, `Server`). It contains no business logic — only reading the `route` query parameter and `REQUEST_METHOD`, resolving `logs/`'s physical path via `Server.MapPath`, invoking `WscMvc.RequestContext` and `WscMvc.Application`, and writing the response. | ||||
| - `Framework/RequestContext.wsc`, `Framework/Application.wsc`, and `Controllers/HomeController.wsc` never reference `Request`/`Response`/`Server`/`Session`. All data crosses the ASP-to-WSC and WSC-to-WSC boundaries as either VBScript scalars (strings) or our own `RequestContext` COM object (not an ASP intrinsic) — never an ASP host object. See `docs/DECISIONS.md` for why the original SPEC §15 open question about passing ASP intrinsics into a WSC never needed a direct experiment: the architecture never crosses that boundary by design. | |||||
| - `Framework/RequestContext.wsc`, `Framework/Application.wsc`, `Controllers/HomeController.wsc`, and `test-app/Controllers/SelfTestController.wsc` never reference `Request`/`Response`/`Server`/`Session`. All data crosses the ASP-to-WSC and WSC-to-WSC boundaries as either VBScript scalars (strings) or our own `RequestContext` COM object (not an ASP intrinsic) — never an ASP host object. See `docs/DECISIONS.md` for why the original SPEC §15 open question about passing ASP intrinsics into a WSC never needed a direct experiment: the architecture never crosses that boundary by design. | |||||
| - WSC public members are exposed as plain `<method>` entries backed by ordinary `Sub`/`Function` procedures, never `<property>`/`Property Get`. VBScript's `Property Get/Let/Set` requires a `Class...End Class` block and cannot appear at a WSC's top-level script scope — confirmed experimentally (see `docs/DECISIONS.md`), not assumed from general WSC documentation. | - WSC public members are exposed as plain `<method>` entries backed by ordinary `Sub`/`Function` procedures, never `<property>`/`Property Get`. VBScript's `Property Get/Let/Set` requires a `Class...End Class` block and cannot appear at a WSC's top-level script scope — confirmed experimentally (see `docs/DECISIONS.md`), not assumed from general WSC documentation. | ||||
| - Routing in M1/M2 is still a single hardcoded `If path = "/hello"` check inside `Application.Run`. This is intentionally minimal and will be replaced by the explicit allowlisted route table in M3 (`Router.wsc`); do not extend it ad hoc before that milestone. | |||||
| - Routing in M1/M2 is still a minimal hardcoded allowlist inside `Application.Run`. Every call includes a fixed application name (`production` or `tests`) supplied by that app's own bootstrap; a route must match both the application and path. This prevents direct `Default.asp?route=...` requests from crossing between apps. M3 will replace these checks with the explicit route table in `Router.wsc`. | |||||
| - `Application.Run` is the single central point that decides expected (404, ordinary control flow) vs. unexpected (COM/method failure, 500) outcomes, and the single point that logs every outcome. `Default.asp` still independently guards its own three sequential calls (`RequestContext` creation, `Initialize`, `Application` creation, `Run`) since a WSC failing to even instantiate happens outside `Application.Run`'s reach. | - `Application.Run` is the single central point that decides expected (404, ordinary control flow) vs. unexpected (COM/method failure, 500) outcomes, and the single point that logs every outcome. `Default.asp` still independently guards its own three sequential calls (`RequestContext` creation, `Initialize`, `Application` creation, `Run`) since a WSC failing to even instantiate happens outside `Application.Run`'s reach. | ||||
| ## Per-request lifetime and diagnostics | ## Per-request lifetime and diagnostics | ||||
| @@ -29,13 +29,15 @@ GET /hello | |||||
| - `RequestContext.Initialize` generates a correlation id from `Fix(Timer)` plus `Scripting.FileSystemObject.GetTempName()` — not `Randomize`/`Rnd()`, which was tried first and shown experimentally to collide when two contexts are created within the same clock tick (see `docs/DECISIONS.md`). | - `RequestContext.Initialize` generates a correlation id from `Fix(Timer)` plus `Scripting.FileSystemObject.GetTempName()` — not `Randomize`/`Rnd()`, which was tried first and shown experimentally to collide when two contexts are created within the same clock tick (see `docs/DECISIONS.md`). | ||||
| - `Application.LogOutcome` appends one line per request (timestamp, correlation id, method, path, status line, elapsed ms) to `logs/app.log`, guarded end-to-end by `On Error Resume Next` so a logging failure can never affect the HTTP response. Concurrent writers are serialized with a manual lock-file mutex (`logs/app.log.lock`, via `CreateTextFile(..., OverwriteExisting:=False)`) since no ASP intrinsic locking primitive (`Application.Lock`) is available to code that must not reference ASP intrinsics. The retry budget is deliberately short (see `docs/DECISIONS.md`): logging is explicitly best-effort and may drop lines under heavy concurrency without affecting correctness of the response. | - `Application.LogOutcome` appends one line per request (timestamp, correlation id, method, path, status line, elapsed ms) to `logs/app.log`, guarded end-to-end by `On Error Resume Next` so a logging failure can never affect the HTTP response. Concurrent writers are serialized with a manual lock-file mutex (`logs/app.log.lock`, via `CreateTextFile(..., OverwriteExisting:=False)`) since no ASP intrinsic locking primitive (`Application.Lock`) is available to code that must not reference ASP intrinsics. The retry budget is deliberately short (see `docs/DECISIONS.md`): logging is explicitly best-effort and may drop lines under heavy concurrency without affecting correctness of the response. | ||||
| ## Test harness: GET /self-test — its own app, same shared framework | |||||
| ## Test harness: GET /self-test — its own app, only Framework/ is shared | |||||
| `Controllers/SelfTestController.wsc` (`WscMvc.SelfTestController`) exposes the same checks as `tests/Test-Components.vbs`, but callable over plain HTTP and returning JSON — no PowerShell, cscript, or SSH access to the VM required. `Application.Run` routes `path = "/self-test"` to it. | |||||
| `test-app/Controllers/SelfTestController.wsc` (`WscMvc.SelfTestController`) exposes the same checks as `tests/Test-Components.vbs`, but callable over plain HTTP and returning JSON — no PowerShell, cscript, or SSH access to the VM required. `Application.Run` routes `path = "/self-test"` to it. | |||||
| **This lives on its own IIS site (`test-app/`), not the production site.** Changed 2026-09-19 at Daniel's explicit direction: a diagnostics endpoint permanently reachable on the same site/port as real traffic was the wrong shape. `test-app/public/` is a second, separate IIS site's physical path (own site name, own app pool, own port, own `logs/`), whose `web.config` routes `/self-test` and nothing else. The production site's `web.config` no longer has a `/self-test` rewrite rule at all — `GET /self-test` against the production site now returns a plain `404` (nothing rewrites that path to `Default.asp`). | **This lives on its own IIS site (`test-app/`), not the production site.** Changed 2026-09-19 at Daniel's explicit direction: a diagnostics endpoint permanently reachable on the same site/port as real traffic was the wrong shape. `test-app/public/` is a second, separate IIS site's physical path (own site name, own app pool, own port, own `logs/`), whose `web.config` routes `/self-test` and nothing else. The production site's `web.config` no longer has a `/self-test` rewrite rule at all — `GET /self-test` against the production site now returns a plain `404` (nothing rewrites that path to `Default.asp`). | ||||
| **"Own app, same framework" is achieved without duplicating any business logic**: `Application.wsc`'s `Run` method still has (and needs) its `path = "/self-test"` branch — that's shared framework code, registered once globally via COM, used by whichever site's `web.config` chooses to route a URL to it. The only genuinely duplicated file is `Default.asp` itself (also present at `test-app/public/Default.asp`), and only because IIS requires each site to have its own physical files — the bootstrap logic in that file is fully generic (doesn't hardcode routes or reference which site is calling it), so the two copies are intentionally byte-identical. If `Default.asp`'s bootstrap logic ever needs to change, change both copies the same way (see the comment at the top of each file). | |||||
| **Only `Framework/` is genuinely shared between the two sites — `Controllers/` is not.** `Application.wsc`/`RequestContext.wsc` in `Framework/` are the reusable dispatch/context engine, registered once globally via COM and used by both sites. `Controllers/` holds *application-specific* business logic: production owns `Controllers/HomeController.wsc`, while the test app owns `test-app/Controllers/SelfTestController.wsc`. COM registration for a `.wsc` records its exact file path, so registration tooling points each controller at its app-owned directory. | |||||
| Each site also owns its thin `Default.asp`. The files differ only in the fixed application name passed to the shared `Application.Run`: production passes `"production"`; the test app passes `"tests"`. That selector is part of the route match, so neither ordinary URL Rewrite nor a direct `Default.asp?route=...` request can activate a controller belonging to the other app. `SelfTestController` no longer invokes `/hello` or `HomeController`; its framework checks use the `tests` route set and explicitly verify that `/hello` is rejected. | |||||
| The HTTP status is always `200` if the self-test mechanism itself ran (a broken `SelfTestController`/`Application` still degrades to the normal `500` path via the same central error handling); the JSON body's top-level `"ok"` field and each check's `"pass"` field carry the actual test results — conventional health-check design (5xx is reserved for the diagnostics mechanism being broken, not for a failed assertion inside it). | The HTTP status is always `200` if the self-test mechanism itself ran (a broken `SelfTestController`/`Application` still degrades to the normal `500` path via the same central error handling); the JSON body's top-level `"ok"` field and each check's `"pass"` field carry the actual test results — conventional health-check design (5xx is reserved for the diagnostics mechanism being broken, not for a failed assertion inside it). | ||||
| @@ -44,7 +46,7 @@ curl http://100.127.62.31:8091/self-test | |||||
| {"ok":true,"checks":[{"name":"request_context_contract","pass":true,"detail":""}, ...]} | {"ok":true,"checks":[{"name":"request_context_contract","pass":true,"detail":""}, ...]} | ||||
| ``` | ``` | ||||
| `tests/run-self-test.sh` wraps this in `curl` + `python3 -m json.tool`, runnable from any CLI (verified working directly from the Linux OpenClaw host, not just from the VM) — point it at the test-app site's URL. `tests/Test-Http.ps1` tests the production site only (`/hello`, and confirms `/self-test` is genuinely unreachable there). | |||||
| `tests/run-self-test.sh` wraps this in `curl` + `python3 -m json.tool`, runnable from any CLI — point it at the test-app site's URL. It also probes direct `Default.asp?route=/hello` and requires a 404. `tests/Test-Http.ps1` tests production and, when `-TestBaseUrl` is supplied, verifies direct-query isolation in both directions. | |||||
| `SelfTestController`'s error details intentionally include raw `Err.Description` text — unlike every other route, which must never leak internals to an arbitrary caller, this route's entire purpose is diagnostics. It is a dev/test-milestone tool; reconsider whether the test-app site should be gated or removed before any production deployment during the M6 hardening pass. | `SelfTestController`'s error details intentionally include raw `Err.Description` text — unlike every other route, which must never leak internals to an arbitrary caller, this route's entire purpose is diagnostics. It is a dev/test-milestone tool; reconsider whether the test-app site should be gated or removed before any production deployment during the M6 hardening pass. | ||||
| @@ -55,7 +57,7 @@ curl http://100.127.62.31:8091/self-test | |||||
| | `Framework/RequestContext.wsc` | `WscMvc.RequestContext` | `{1C36FA55-34DF-4974-94B9-D657389362B2}` | | | `Framework/RequestContext.wsc` | `WscMvc.RequestContext` | `{1C36FA55-34DF-4974-94B9-D657389362B2}` | | ||||
| | `Framework/Application.wsc` | `WscMvc.Application` | `{851C7763-1638-42FE-A166-BF3DD3A96A88}` | | | `Framework/Application.wsc` | `WscMvc.Application` | `{851C7763-1638-42FE-A166-BF3DD3A96A88}` | | ||||
| | `Controllers/HomeController.wsc` | `WscMvc.HomeController` | `{87488446-60BE-4068-8368-0B709BB68F3F}` | | | `Controllers/HomeController.wsc` | `WscMvc.HomeController` | `{87488446-60BE-4068-8368-0B709BB68F3F}` | | ||||
| | `Controllers/SelfTestController.wsc` | `WscMvc.SelfTestController` | `{D2634944-4646-4C55-956E-4C05E7E10904}` | | |||||
| | `test-app/Controllers/SelfTestController.wsc` | `WscMvc.SelfTestController` | `{D2634944-4646-4C55-956E-4C05E7E10904}` | | |||||
| CLSIDs are fixed at creation and must never be recycled for a different component (AGENTS.md). `Application`'s public `Run` signature changed between M1 and M2 (added a leading `ctx` parameter) under the same CLSID; acceptable because this is active pre-release (v0.1) development with exactly one caller (`Default.asp`, updated in lockstep) — not a claim that live interface changes are safe for a published/external client. | CLSIDs are fixed at creation and must never be recycled for a different component (AGENTS.md). `Application`'s public `Run` signature changed between M1 and M2 (added a leading `ctx` parameter) under the same CLSID; acceptable because this is active pre-release (v0.1) development with exactly one caller (`Default.asp`, updated in lockstep) — not a claim that live interface changes are safe for a published/external client. | ||||
| @@ -72,7 +74,7 @@ Two separate IIS sites now, both set up/reconciled by the same `tools/Setup-Site | |||||
| | Routes | `/hello` | `/self-test` | | | Routes | `/hello` | `/self-test` | | ||||
| | Logs | `C:\Projects\wsc-mvc\logs\app.log` | `C:\Projects\wsc-mvc\test-app\logs\app.log` | | | Logs | `C:\Projects\wsc-mvc\logs\app.log` | `C:\Projects\wsc-mvc\test-app\logs\app.log` | | ||||
| Both are 64-bit, no managed code, anonymous auth identity `IUSR`. Each site's `public/` contains only `Default.asp` and `web.config`; every other directory (`Framework/`, `Controllers/`, `tests/`, `tools/`, `docs/`, both `logs/` folders) is a sibling of whichever `public/` is actually served, outside both sites' physical paths entirely. Changed 2026-09-19 at Daniel's explicit direction (see `docs/DECISIONS.md`); previously there was one site with the whole project directory as its root and those folders hidden via `hiddenSegments`, and `/self-test` was a route on that same site. | |||||
| Both are 64-bit, no managed code, anonymous auth identity `IUSR`. Each site's `public/` contains only `Default.asp` and `web.config`; every other directory (`Framework/`, `Controllers/`, `test-app/Controllers/`, `tests/`, `tools/`, `docs/`, both `logs/` folders) is a sibling of whichever `public/` is actually served, outside both sites' physical paths entirely — see the "only Framework/ is shared" note above for which of those directories are truly cross-site versus app-specific. Changed 2026-09-19 at Daniel's explicit direction (see `docs/DECISIONS.md`); previously there was one site with the whole project directory as its root and those folders hidden via `hiddenSegments`, and `/self-test` was a route on that same site. | |||||
| - Each site's `web.config` keeps only a `.wsc/.vbs/.ps1/.md` extension denylist as defense-in-depth (in case a stray file ever lands directly in its `public/`); no `hiddenSegments` are needed since the directories they used to hide no longer exist under either served root at all. | - Each site's `web.config` keeps only a `.wsc/.vbs/.ps1/.md` extension denylist as defense-in-depth (in case a stray file ever lands directly in its `public/`); no `hiddenSegments` are needed since the directories they used to hide no longer exist under either served root at all. | ||||
| - `enableParentPaths=true` is set for **both** sites (scoped via `appcmd ... /commit:apphost`, a separate `<location>` block per site in `applicationHost.config` — not a machine-wide unlock of the locked `system.webServer/asp` section) so each site's `Default.asp` can `Server.MapPath("../logs")` to reach its own `logs/`. This setting affects only server-side script `MapPath`/`#include` resolution, not client-supplied URL paths — IIS's own URL normalization independently rejects `..`-traversal in an incoming request regardless of this setting; verified with a direct request attempt, see `docs/DECISIONS.md`. | - `enableParentPaths=true` is set for **both** sites (scoped via `appcmd ... /commit:apphost`, a separate `<location>` block per site in `applicationHost.config` — not a machine-wide unlock of the locked `system.webServer/asp` section) so each site's `Default.asp` can `Server.MapPath("../logs")` to reach its own `logs/`. This setting affects only server-side script `MapPath`/`#include` resolution, not client-supplied URL paths — IIS's own URL normalization independently rejects `..`-traversal in an incoming request regardless of this setting; verified with a direct request attempt, see `docs/DECISIONS.md`. | ||||
| @@ -99,3 +99,11 @@ Daniel: "the tests need to be its own app in its own folder but still uses the s | |||||
| **Real bug found and fixed while doing this, in my own tooling**: my first attempt to invoke `Setup-Site.ps1 -PhysicalPath "C:\Projects\wsc-mvc\test-app\public"` over a chained SSH/cmd/PowerShell command failed with `New-Website : Parameter 'PhysicalPath' should point to existing path` — nested shell-quoting layers had passed the literal string `'C:\Projects\wsc-mvc\test-app\public'` (with the single quotes as literal characters) as the parameter value. The script's own `Write-Output "Created site..."` line printed unconditionally regardless of whether `New-Website` actually succeeded (it hadn't - `$ErrorActionPreference` was the default `Continue`, so the error was non-fatal and the script kept going, prompting a misleading "success" message followed by a real `IIS:\Sites\WscMvcTests` not-found error on the next line). Fixed two ways: (1) stopped fighting nested quoting and instead wrote the actual invocation into a small script file copied to the VM and run with `-File`, which sidesteps the quoting problem entirely; (2) added `$ErrorActionPreference = 'Stop'` and an explicit `Test-Path $PhysicalPath` pre-check to `Setup-Site.ps1` itself, so a bad path now fails loudly and immediately instead of printing a false-success message and failing confusingly two lines later. Re-verified both sites set up correctly and idempotently after the fix. | **Real bug found and fixed while doing this, in my own tooling**: my first attempt to invoke `Setup-Site.ps1 -PhysicalPath "C:\Projects\wsc-mvc\test-app\public"` over a chained SSH/cmd/PowerShell command failed with `New-Website : Parameter 'PhysicalPath' should point to existing path` — nested shell-quoting layers had passed the literal string `'C:\Projects\wsc-mvc\test-app\public'` (with the single quotes as literal characters) as the parameter value. The script's own `Write-Output "Created site..."` line printed unconditionally regardless of whether `New-Website` actually succeeded (it hadn't - `$ErrorActionPreference` was the default `Continue`, so the error was non-fatal and the script kept going, prompting a misleading "success" message followed by a real `IIS:\Sites\WscMvcTests` not-found error on the next line). Fixed two ways: (1) stopped fighting nested quoting and instead wrote the actual invocation into a small script file copied to the VM and run with `-File`, which sidesteps the quoting problem entirely; (2) added `$ErrorActionPreference = 'Stop'` and an explicit `Test-Path $PhysicalPath` pre-check to `Setup-Site.ps1` itself, so a bad path now fails loudly and immediately instead of printing a false-success message and failing confusingly two lines later. Re-verified both sites set up correctly and idempotently after the fix. | ||||
| **Verified after the split**: production `GET /hello` → `200`; production `GET /self-test` → `404` (genuinely unreachable now); test-app `GET /self-test` → `200` with correct JSON; test-app `GET /hello` → `404` (not routed there, expected); both sites' `Framework/Application.wsc` → `404`; both sites write to their own separate `logs/app.log` (confirmed distinct file paths, distinct content); re-running `Setup-Site.ps1` for both sites a second time is a true no-op. Full `tests/Test-Components.vbs`, `tests/Test-Http.ps1` (now production-only), and `tests/run-self-test.sh` (now pointed at the test-app site) all pass. | **Verified after the split**: production `GET /hello` → `200`; production `GET /self-test` → `404` (genuinely unreachable now); test-app `GET /self-test` → `200` with correct JSON; test-app `GET /hello` → `404` (not routed there, expected); both sites' `Framework/Application.wsc` → `404`; both sites write to their own separate `logs/app.log` (confirmed distinct file paths, distinct content); re-running `Setup-Site.ps1` for both sites a second time is a true no-op. Full `tests/Test-Components.vbs`, `tests/Test-Http.ps1` (now production-only), and `tests/run-self-test.sh` (now pointed at the test-app site) all pass. | ||||
| ## Correction: test-app should only share Framework/, not Controllers/ (2026-09-19) | |||||
| Follow-up direction from Daniel right after the previous entry: "Test app should only share /framework folder." The just-shipped version had `SelfTestController.wsc` sitting in the shared root `Controllers/` folder alongside `HomeController.wsc` — meaning both apps' registration tooling pointed into the same `Controllers/` directory, blurring the intended boundary. `Framework/` (`Application.wsc`, `RequestContext.wsc`) is the genuinely reusable dispatch/context engine, meant to be shared; `Controllers/` is *application-specific business logic*, and `SelfTestController` is test-app-specific, not production business logic, so it doesn't belong there. | |||||
| **Fix**: moved `Controllers/SelfTestController.wsc` -> `test-app/Controllers/SelfTestController.wsc` and updated registration tooling to reference the app-owned path. Then closed a subtler boundary bypass: removing rewrite rules alone was insufficient because callers can address `Default.asp?route=...` directly. Each app's bootstrap now passes a fixed application name into shared `Application.Run`; the route allowlist matches both application and path. Production therefore cannot activate `/self-test`, and the test app cannot activate `/hello`, even through direct `Default.asp` requests. `SelfTestController` no longer invokes production's `/hello` route or `HomeController`; it verifies that the `tests` route set rejects `/hello`. | |||||
| COM registration for a `.wsc` records the exact file path in the registry (`HKLM:\SOFTWARE\Classes\CLSID\{guid}\ScriptletURL`), so moving the file requires re-registration. `tools/Register-Components.ps1` and `tools/Unregister-Components.ps1` now point to `test-app/Controllers/SelfTestController.wsc`; verification includes checking that registry value and exercising both HTTP boundaries. | |||||
| @@ -223,3 +223,44 @@ All: **PASS** | |||||
| Confirmed the two sites write to genuinely separate log files (`C:\Projects\wsc-mvc\logs\app.log` vs `C:\Projects\wsc-mvc\test-app\logs\app.log`), read back over SSH with distinct content. **PASS** | Confirmed the two sites write to genuinely separate log files (`C:\Projects\wsc-mvc\logs\app.log` vs `C:\Projects\wsc-mvc\test-app\logs\app.log`), read back over SSH with distinct content. **PASS** | ||||
| Full regression: `tests/Test-Components.vbs` (WSH, unaffected by the site split — doesn't go through IIS), `tests/Test-Http.ps1` (updated to test the production site only: `/hello` plus confirming `/self-test` is genuinely unreachable there), and `tests/run-self-test.sh` (now pointed at the `WscMvcTests` site, `http://100.127.62.31:8091`). All: **PASS**. | Full regression: `tests/Test-Components.vbs` (WSH, unaffected by the site split — doesn't go through IIS), `tests/Test-Http.ps1` (updated to test the production site only: `/hello` plus confirming `/self-test` is genuinely unreachable there), and `tests/run-self-test.sh` (now pointed at the `WscMvcTests` site, `http://100.127.62.31:8091`). All: **PASS**. | ||||
| ## Correction: SelfTestController moved to test-app/Controllers/ (2026-09-19) | |||||
| Commands run after moving `Controllers/SelfTestController.wsc` -> `test-app/Controllers/SelfTestController.wsc` and updating `tools/Register-Components.ps1`/`Unregister-Components.ps1`: | |||||
| ``` | |||||
| powershell -File tools\Register-Components.ps1 | |||||
| ``` | |||||
| Result: **PASS** — registered all four components (`Framework\RequestContext.wsc`, `Framework\Application.wsc`, `Controllers\HomeController.wsc`, `test-app\Controllers\SelfTestController.wsc`), no errors. | |||||
| Verified the registry actually updated, not just assumed: read `HKLM:\SOFTWARE\Classes\CLSID\{D2634944-4646-4C55-956E-4C05E7E10904}\ScriptletURL` directly — value is `file:///C:/Projects/wsc-mvc/test-app/Controllers/SelfTestController.wsc`, confirming the re-registration genuinely repointed the CLSID to the new file location. **PASS** | |||||
| ``` | |||||
| curl http://100.127.62.31:8091/self-test -> 200, correct JSON (unchanged) | |||||
| curl http://100.127.62.31:8090/hello -> 200 (unaffected) | |||||
| curl http://100.127.62.31:8090/Controllers/SelfTestController.wsc -> 404 (old location, production site) | |||||
| curl http://100.127.62.31:8091/Controllers/SelfTestController.wsc -> 404 (new location, test-app site) | |||||
| ``` | |||||
| All: **PASS** | |||||
| The move exposed a direct-entry boundary gap: URL Rewrite isolation alone did not cover `Default.asp?route=...`. Added a fixed application selector to `Application.Run`, made each app-owned bootstrap pass its own selector, removed the self-test's dependency on production `/hello`, and added direct-query regressions. | |||||
| Windows Server 2025 / IIS, 64-bit app pools: | |||||
| ``` | |||||
| cscript //nologo tests\Test-Components.vbs | |||||
| powershell -File tests\Test-Http.ps1 -BaseUrl http://localhost:8090 -TestBaseUrl http://localhost:8091 | |||||
| ``` | |||||
| Both: **PASS**. HTTP boundary probes: | |||||
| ``` | |||||
| production /hello -> 200 | |||||
| production /self-test -> 404 | |||||
| production /Default.asp?route=/self-test -> 404 | |||||
| test app /self-test -> 200, JSON ok=true | |||||
| test app /hello -> 404 | |||||
| test app /Default.asp?route=/hello -> 404 | |||||
| ``` | |||||
| `tests/run-self-test.sh http://100.127.62.31:8091`: **PASS**, including JSON checks and the direct production-route rejection. Both `Setup-Site.ps1` invocations remained idempotent and reported the correct physical paths. Registry inspection showed the SelfTestController `ScriptletURL` at `file:///C:/Projects/wsc-mvc/test-app/Controllers/SelfTestController.wsc`; the old production controller file was absent. Full regression: **PASS**. | |||||
| @@ -1,10 +1,11 @@ | |||||
| <%@ Language="VBScript" %> | <%@ Language="VBScript" %> | ||||
| <% Option Explicit %> | <% Option Explicit %> | ||||
| <% | <% | ||||
| Dim route, httpMethod, logDir, ctx, app, statusLine, contentType, body | |||||
| Dim route, httpMethod, logDir, ctx, app, applicationName, statusLine, contentType, body | |||||
| route = Request.QueryString("route") | route = Request.QueryString("route") | ||||
| httpMethod = Request.ServerVariables("REQUEST_METHOD") | httpMethod = Request.ServerVariables("REQUEST_METHOD") | ||||
| applicationName = "production" | |||||
| ' logs/ deliberately lives outside the served "public" webroot (IIS site | ' logs/ deliberately lives outside the served "public" webroot (IIS site | ||||
| ' physical path = public/), so a parent-relative MapPath is required here - | ' physical path = public/), so a parent-relative MapPath is required here - | ||||
| ' requires enableParentPaths=true for this site. See docs/DECISIONS.md. | ' requires enableParentPaths=true for this site. See docs/DECISIONS.md. | ||||
| @@ -55,7 +56,7 @@ contentType = "" | |||||
| body = "" | body = "" | ||||
| On Error Resume Next | On Error Resume Next | ||||
| app.Run ctx, statusLine, contentType, body | |||||
| app.Run ctx, applicationName, statusLine, contentType, body | |||||
| If Err.Number <> 0 Then | If Err.Number <> 0 Then | ||||
| Err.Clear | Err.Clear | ||||
| On Error Goto 0 | On Error Goto 0 | ||||
| @@ -22,12 +22,11 @@ | |||||
| </requestFiltering> | </requestFiltering> | ||||
| </security> | </security> | ||||
| <!-- | <!-- | ||||
| No /self-test rewrite rule on this (production) site on purpose: the | |||||
| diagnostics endpoint is only exposed on the separate test-app/ site | |||||
| (see docs/ARCHITECTURE.md). Application.wsc's Run method still | |||||
| technically has a "/self-test" case - that's shared framework code, | |||||
| used by whichever site's web.config chooses to route to it - but | |||||
| nothing on this site's rewrite rules can ever reach it. | |||||
| No /self-test rewrite rule on this production site: diagnostics are | |||||
| exposed only by the separate test app. Defense in depth lives in the | |||||
| bootstrap too: Default.asp always passes applicationName="production", | |||||
| so even a direct Default.asp?route=/self-test request is rejected by | |||||
| the shared framework route allowlist. | |||||
| --> | --> | ||||
| <rewrite> | <rewrite> | ||||
| <rules> | <rules> | ||||
| @@ -77,41 +77,17 @@ Sub RunSelfTest(logDir, body) | |||||
| checks = AppendCheck(checks, "correlation_id_uniqueness", distinctOk, distinctDetail) | checks = AppendCheck(checks, "correlation_id_uniqueness", distinctOk, distinctDetail) | ||||
| allPass = allPass And distinctOk | allPass = allPass And distinctOk | ||||
| ' --- Application.Run happy path --- | |||||
| Dim app, ctxHello, helloStatus, helloType, helloBody, helloOk, helloDetail | |||||
| helloOk = False | |||||
| helloDetail = "" | |||||
| On Error Resume Next | |||||
| Set app = CreateObject("WscMvc.Application") | |||||
| Set ctxHello = CreateObject("WscMvc.RequestContext") | |||||
| ctxHello.Initialize "/hello", "GET", logDir | |||||
| helloStatus = "" : helloType = "" : helloBody = "" | |||||
| app.Run ctxHello, helloStatus, helloType, helloBody | |||||
| If Err.Number <> 0 Then | |||||
| helloDetail = Err.Description | |||||
| Err.Clear | |||||
| ElseIf helloStatus <> "200 OK" Then | |||||
| helloDetail = "Expected 200 OK, got [" & helloStatus & "]" | |||||
| ElseIf helloType <> "text/html; charset=utf-8" Then | |||||
| helloDetail = "Unexpected content type [" & helloType & "]" | |||||
| ElseIf helloBody <> "Hello from WSC-MVC!" Then | |||||
| helloDetail = "Unexpected body [" & helloBody & "]" | |||||
| Else | |||||
| helloOk = True | |||||
| End If | |||||
| On Error Goto 0 | |||||
| checks = AppendCheck(checks, "application_run_hello", helloOk, helloDetail) | |||||
| allPass = allPass And helloOk | |||||
| ' --- Application.Run unknown route (expected 404, not an error) --- | |||||
| Dim ctxUnknown, unkStatus, unkType, unkBody, unkOk, unkDetail | |||||
| ' --- Shared Application must isolate route sets. The test app must not | |||||
| ' activate or test production's HomeController. --- | |||||
| Dim app, ctxUnknown, unkStatus, unkType, unkBody, unkOk, unkDetail | |||||
| unkOk = False | unkOk = False | ||||
| unkDetail = "" | unkDetail = "" | ||||
| On Error Resume Next | On Error Resume Next | ||||
| Set app = CreateObject("WscMvc.Application") | |||||
| Set ctxUnknown = CreateObject("WscMvc.RequestContext") | Set ctxUnknown = CreateObject("WscMvc.RequestContext") | ||||
| ctxUnknown.Initialize "/self-test-does-not-exist", "GET", logDir | |||||
| ctxUnknown.Initialize "/hello", "GET", logDir | |||||
| unkStatus = "" : unkType = "" : unkBody = "" | unkStatus = "" : unkType = "" : unkBody = "" | ||||
| app.Run ctxUnknown, unkStatus, unkType, unkBody | |||||
| app.Run ctxUnknown, "tests", unkStatus, unkType, unkBody | |||||
| If Err.Number <> 0 Then | If Err.Number <> 0 Then | ||||
| unkDetail = Err.Description | unkDetail = Err.Description | ||||
| Err.Clear | Err.Clear | ||||
| @@ -121,12 +97,11 @@ Sub RunSelfTest(logDir, body) | |||||
| unkOk = True | unkOk = True | ||||
| End If | End If | ||||
| On Error Goto 0 | On Error Goto 0 | ||||
| checks = AppendCheck(checks, "application_run_unknown_route", unkOk, unkDetail) | |||||
| checks = AppendCheck(checks, "test_app_rejects_production_route", unkOk, unkDetail) | |||||
| allPass = allPass And unkOk | allPass = allPass And unkOk | ||||
| Set ctx1 = Nothing | Set ctx1 = Nothing | ||||
| Set ctx2 = Nothing | Set ctx2 = Nothing | ||||
| Set ctxHello = Nothing | |||||
| Set ctxUnknown = Nothing | Set ctxUnknown = Nothing | ||||
| Set app = Nothing | Set app = Nothing | ||||
| @@ -1,17 +1,15 @@ | |||||
| <%@ Language="VBScript" %> | <%@ Language="VBScript" %> | ||||
| <% Option Explicit %> | <% Option Explicit %> | ||||
| <% | <% | ||||
| ' Intentionally byte-identical to /public/Default.asp. This is the test | |||||
| ' app's own bootstrap (separate IIS site/physical path from production), | |||||
| ' but the bootstrap logic itself is fully generic - it doesn't hardcode | |||||
| ' which routes exist (that lives in Framework/Application.wsc, shared by | |||||
| ' both sites via COM registration) or which site is calling it. If you | |||||
| ' change this file's logic, change public/Default.asp the same way, and | |||||
| ' vice versa. See docs/ARCHITECTURE.md for why there are two IIS sites. | |||||
| Dim route, httpMethod, logDir, ctx, app, statusLine, contentType, body | |||||
| ' This test app owns its bootstrap and explicitly selects only the test route | |||||
| ' set in the shared framework. Production's bootstrap selects "production". | |||||
| ' Keeping this value inside each app prevents direct Default.asp?route=... | |||||
| ' requests from crossing the application boundary. | |||||
| Dim route, httpMethod, logDir, ctx, app, applicationName, statusLine, contentType, body | |||||
| route = Request.QueryString("route") | route = Request.QueryString("route") | ||||
| httpMethod = Request.ServerVariables("REQUEST_METHOD") | httpMethod = Request.ServerVariables("REQUEST_METHOD") | ||||
| applicationName = "tests" | |||||
| ' logs/ deliberately lives outside the served "public" webroot (IIS site | ' logs/ deliberately lives outside the served "public" webroot (IIS site | ||||
| ' physical path = public/), so a parent-relative MapPath is required here - | ' physical path = public/), so a parent-relative MapPath is required here - | ||||
| ' requires enableParentPaths=true for this site. See docs/DECISIONS.md. | ' requires enableParentPaths=true for this site. See docs/DECISIONS.md. | ||||
| @@ -62,7 +60,7 @@ contentType = "" | |||||
| body = "" | body = "" | ||||
| On Error Resume Next | On Error Resume Next | ||||
| app.Run ctx, statusLine, contentType, body | |||||
| app.Run ctx, applicationName, statusLine, contentType, body | |||||
| If Err.Number <> 0 Then | If Err.Number <> 0 Then | ||||
| Err.Clear | Err.Clear | ||||
| On Error Goto 0 | On Error Goto 0 | ||||
| @@ -81,7 +81,7 @@ End If | |||||
| If pass Then | If pass Then | ||||
| statusLine = "" : contentType = "" : body = "" | statusLine = "" : contentType = "" : body = "" | ||||
| On Error Resume Next | On Error Resume Next | ||||
| app.Run ctx1, statusLine, contentType, body | |||||
| app.Run ctx1, "production", statusLine, contentType, body | |||||
| If Err.Number <> 0 Then | If Err.Number <> 0 Then | ||||
| WScript.Echo "FAIL: Application.Run raised error on /hello - " & Err.Description | WScript.Echo "FAIL: Application.Run raised error on /hello - " & Err.Description | ||||
| pass = False | pass = False | ||||
| @@ -100,7 +100,7 @@ End If | |||||
| If pass Then | If pass Then | ||||
| statusLine = "" : contentType = "" : body = "" | statusLine = "" : contentType = "" : body = "" | ||||
| On Error Resume Next | On Error Resume Next | ||||
| app.Run ctx2, statusLine, contentType, body | |||||
| app.Run ctx2, "production", statusLine, contentType, body | |||||
| If Err.Number <> 0 Then | If Err.Number <> 0 Then | ||||
| WScript.Echo "FAIL: Application.Run raised error on unknown route - " & Err.Description | WScript.Echo "FAIL: Application.Run raised error on unknown route - " & Err.Description | ||||
| pass = False | pass = False | ||||
| @@ -1,7 +1,8 @@ | |||||
| [CmdletBinding()] | [CmdletBinding()] | ||||
| param( | param( | ||||
| [Parameter(Mandatory = $true)] | [Parameter(Mandatory = $true)] | ||||
| [string]$BaseUrl | |||||
| [string]$BaseUrl, | |||||
| [string]$TestBaseUrl | |||||
| ) | ) | ||||
| $script:failures = 0 | $script:failures = 0 | ||||
| @@ -44,6 +45,41 @@ try { | |||||
| Report $false "GET /self-test not exposed on production" $_.Exception.Message | Report $false "GET /self-test not exposed on production" $_.Exception.Message | ||||
| } | } | ||||
| # A caller can request Default.asp directly and supply the internal route | |||||
| # query string, bypassing URL Rewrite. The per-app route-set argument must | |||||
| # still prevent production from activating the test controller. | |||||
| try { | |||||
| $directSelfTestResp = Invoke-WebRequest -Uri "$BaseUrl/Default.asp?route=/self-test" -UseBasicParsing | |||||
| Report $false "direct Default.asp cannot cross into test routes" "got $($directSelfTestResp.StatusCode)" | |||||
| } catch [System.Net.WebException] { | |||||
| $webResp = $_.Exception.Response | |||||
| if ($webResp) { | |||||
| $code = [int]$webResp.StatusCode | |||||
| Report ($code -eq 404) "direct Default.asp cannot cross into test routes" "got $code" | |||||
| } else { | |||||
| Report $false "direct Default.asp cannot cross into test routes" $_.Exception.Message | |||||
| } | |||||
| } catch { | |||||
| Report $false "direct Default.asp cannot cross into test routes" $_.Exception.Message | |||||
| } | |||||
| if ($TestBaseUrl) { | |||||
| try { | |||||
| $directHelloResp = Invoke-WebRequest -Uri "$TestBaseUrl/Default.asp?route=/hello" -UseBasicParsing | |||||
| Report $false "test app cannot cross into production routes" "got $($directHelloResp.StatusCode)" | |||||
| } catch [System.Net.WebException] { | |||||
| $webResp = $_.Exception.Response | |||||
| if ($webResp) { | |||||
| $code = [int]$webResp.StatusCode | |||||
| Report ($code -eq 404) "test app cannot cross into production routes" "got $code" | |||||
| } else { | |||||
| Report $false "test app cannot cross into production routes" $_.Exception.Message | |||||
| } | |||||
| } catch { | |||||
| Report $false "test app cannot cross into production routes" $_.Exception.Message | |||||
| } | |||||
| } | |||||
| # Framework/ is a sibling of the "public" webroot, not a rule-denied | # Framework/ is a sibling of the "public" webroot, not a rule-denied | ||||
| # subfolder within it - this checks it's genuinely unreachable, not just | # subfolder within it - this checks it's genuinely unreachable, not just | ||||
| # filtered. | # filtered. | ||||
| @@ -14,10 +14,19 @@ echo "$response" | python3 -m json.tool | |||||
| ok=$(echo "$response" | python3 -c 'import json, sys; print("true" if json.load(sys.stdin)["ok"] else "false")') | ok=$(echo "$response" | python3 -c 'import json, sys; print("true" if json.load(sys.stdin)["ok"] else "false")') | ||||
| if [ "$ok" = "true" ]; then | |||||
| echo "RESULT: ALL PASS" | |||||
| exit 0 | |||||
| else | |||||
| if [ "$ok" != "true" ]; then | |||||
| echo "RESULT: FAILURE" | echo "RESULT: FAILURE" | ||||
| exit 1 | exit 1 | ||||
| fi | fi | ||||
| # URL Rewrite is not the only entry path: Default.asp is directly addressable. | |||||
| # Prove this test app still cannot activate a production route through its | |||||
| # internal route query string. | |||||
| hello_status=$(curl -sS -o /dev/null -w '%{http_code}' "$BASE_URL/Default.asp?route=/hello") | |||||
| if [ "$hello_status" != "404" ]; then | |||||
| echo "FAIL: test app direct Default.asp activated production route (HTTP $hello_status)" >&2 | |||||
| exit 1 | |||||
| fi | |||||
| echo "PASS: test app rejects production route through direct Default.asp" | |||||
| echo "RESULT: ALL PASS" | |||||
| @@ -8,10 +8,14 @@ if (-not $ProjectRoot) { | |||||
| } | } | ||||
| $components = @( | $components = @( | ||||
| # Framework/ is the only thing genuinely shared between the production | |||||
| # and test-app sites. Controllers/ is production's own; SelfTestController | |||||
| # is test-app-specific and lives under test-app/ instead. See | |||||
| # docs/ARCHITECTURE.md. | |||||
| (Join-Path $ProjectRoot 'Framework\RequestContext.wsc'), | (Join-Path $ProjectRoot 'Framework\RequestContext.wsc'), | ||||
| (Join-Path $ProjectRoot 'Framework\Application.wsc'), | (Join-Path $ProjectRoot 'Framework\Application.wsc'), | ||||
| (Join-Path $ProjectRoot 'Controllers\HomeController.wsc'), | (Join-Path $ProjectRoot 'Controllers\HomeController.wsc'), | ||||
| (Join-Path $ProjectRoot 'Controllers\SelfTestController.wsc') | |||||
| (Join-Path $ProjectRoot 'test-app\Controllers\SelfTestController.wsc') | |||||
| ) | ) | ||||
| foreach ($path in $components) { | foreach ($path in $components) { | ||||
| @@ -15,7 +15,7 @@ if (-not $ProjectRoot) { | |||||
| # can never be touched even if it happened to reuse a ProgID string. | # can never be touched even if it happened to reuse a ProgID string. | ||||
| $components = @( | $components = @( | ||||
| @{ Path = (Join-Path $ProjectRoot 'Controllers\SelfTestController.wsc'); ProgId = 'WscMvc.SelfTestController'; ClassId = '{D2634944-4646-4C55-956E-4C05E7E10904}' }, | |||||
| @{ Path = (Join-Path $ProjectRoot 'test-app\Controllers\SelfTestController.wsc'); ProgId = 'WscMvc.SelfTestController'; ClassId = '{D2634944-4646-4C55-956E-4C05E7E10904}' }, | |||||
| @{ Path = (Join-Path $ProjectRoot 'Controllers\HomeController.wsc'); ProgId = 'WscMvc.HomeController'; ClassId = '{87488446-60BE-4068-8368-0B709BB68F3F}' }, | @{ Path = (Join-Path $ProjectRoot 'Controllers\HomeController.wsc'); ProgId = 'WscMvc.HomeController'; ClassId = '{87488446-60BE-4068-8368-0B709BB68F3F}' }, | ||||
| @{ Path = (Join-Path $ProjectRoot 'Framework\Application.wsc'); ProgId = 'WscMvc.Application'; ClassId = '{851C7763-1638-42FE-A166-BF3DD3A96A88}' }, | @{ Path = (Join-Path $ProjectRoot 'Framework\Application.wsc'); ProgId = 'WscMvc.Application'; ClassId = '{851C7763-1638-42FE-A166-BF3DD3A96A88}' }, | ||||
| @{ Path = (Join-Path $ProjectRoot 'Framework\RequestContext.wsc'); ProgId = 'WscMvc.RequestContext'; ClassId = '{1C36FA55-34DF-4974-94B9-D657389362B2}' } | @{ Path = (Join-Path $ProjectRoot 'Framework\RequestContext.wsc'); ProgId = 'WscMvc.RequestContext'; ClassId = '{1C36FA55-34DF-4974-94B9-D657389362B2}' } | ||||
Powered by TurnKey Linux.