Переглянути джерело

Isolate test app to shared framework only

master
Open Claw 2 тижднів тому
джерело
коміт
e6866dccd5
15 змінених файлів з 155 додано та 79 видалено
  1. +4
    -3
      Framework/Application.wsc
  2. +4
    -3
      README.md
  3. +7
    -6
      SPEC.md
  4. +11
    -9
      docs/ARCHITECTURE.md
  5. +8
    -0
      docs/DECISIONS.md
  6. +41
    -0
      docs/TEST-RESULTS.md
  7. +3
    -2
      public/Default.asp
  8. +5
    -6
      public/web.config
  9. +7
    -32
      test-app/Controllers/SelfTestController.wsc
  10. +7
    -9
      test-app/public/Default.asp
  11. +2
    -2
      tests/Test-Components.vbs
  12. +37
    -1
      tests/Test-Http.ps1
  13. +13
    -4
      tests/run-self-test.sh
  14. +5
    -1
      tools/Register-Components.ps1
  15. +1
    -1
      tools/Unregister-Components.ps1

+ 4
- 3
Framework/Application.wsc Переглянути файл

@@ -11,6 +11,7 @@
<public>
<method name="Run">
<parameter name="ctx"/>
<parameter name="applicationName"/>
<parameter name="statusLine"/>
<parameter name="contentType"/>
<parameter name="body"/>
@@ -25,12 +26,12 @@ Option Explicit
' (COM/method failure -> 500) outcomes, and one place logs them. ctx is our
' own WscMvc.RequestContext object (not an ASP intrinsic), carrying only
' 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

path = ctx.Path

If path = "/hello" Then
If applicationName = "production" And path = "/hello" Then
Set ctrl = Nothing
On Error Resume Next
Set ctrl = CreateObject("WscMvc.HomeController")
@@ -64,7 +65,7 @@ Sub Run(ctx, statusLine, contentType, body)
contentType = "text/html; charset=utf-8"
body = helloBody
Set ctrl = Nothing
ElseIf path = "/self-test" Then
ElseIf applicationName = "tests" And path = "/self-test" Then
Set ctrl = Nothing
On Error Resume Next
Set ctrl = CreateObject("WscMvc.SelfTestController")


+ 4
- 3
README.md Переглянути файл

@@ -4,11 +4,12 @@ A small, WSC-first MVC framework for Classic ASP on IIS: VBScript Windows Script

## 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`.
- `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.
- `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
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
./tests/run-self-test.sh http://<host>:8091 # test-app site, plain curl+JSON, any CLI


+ 7
- 6
SPEC.md Переглянути файл

@@ -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.

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/
public/ # production site's IIS physical path
Default.asp
Default.asp # selects only production routes
web.config
test-app/
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
Controllers/
SelfTestController.wsc # test-app-specific; NOT in the shared Controllers/ below
logs/
app.log # this site's own log, separate from the production one
Framework/
Framework/ # the ONLY folder shared between both sites
Application.wsc
Router.wsc # phase 3
RequestContext.wsc
ResponseResult.wsc # phase 2; optional if simpler contract works
ViewRenderer.wsc # phase 4
Controllers/
Controllers/ # production's own controllers, not shared with test-app/
HomeController.wsc
SelfTestController.wsc
Views/
Home.html # phase 4
Layout.html # phase 4


+ 11
- 9
docs/ARCHITECTURE.md Переглянути файл

@@ -8,7 +8,7 @@ GET /hello
-> /Default.asp?route=/hello
-> Server.CreateObject("WscMvc.RequestContext"); ctx.Initialize path, httpMethod, logDir
-> 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")
-> HomeController.Hello(body) [Controllers/HomeController.wsc]
-> LogOutcome ctx, statusLine (best-effort append to logs/app.log)
@@ -18,9 +18,9 @@ GET /hello
## 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.
- `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.
- 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.

## 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`).
- `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`).

**"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).

@@ -44,7 +46,7 @@ curl http://100.127.62.31:8091/self-test
{"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.

@@ -55,7 +57,7 @@ curl http://100.127.62.31:8091/self-test
| `Framework/RequestContext.wsc` | `WscMvc.RequestContext` | `{1C36FA55-34DF-4974-94B9-D657389362B2}` |
| `Framework/Application.wsc` | `WscMvc.Application` | `{851C7763-1638-42FE-A166-BF3DD3A96A88}` |
| `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.

@@ -72,7 +74,7 @@ Two separate IIS sites now, both set up/reconciled by the same `tools/Setup-Site
| Routes | `/hello` | `/self-test` |
| 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.
- `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`.


+ 8
- 0
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.

**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.

+ 41
- 0
docs/TEST-RESULTS.md Переглянути файл

@@ -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**

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**.

+ 3
- 2
public/Default.asp Переглянути файл

@@ -1,10 +1,11 @@
<%@ Language="VBScript" %>
<% 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")
httpMethod = Request.ServerVariables("REQUEST_METHOD")
applicationName = "production"
' logs/ deliberately lives outside the served "public" webroot (IIS site
' physical path = public/), so a parent-relative MapPath is required here -
' requires enableParentPaths=true for this site. See docs/DECISIONS.md.
@@ -55,7 +56,7 @@ contentType = ""
body = ""

On Error Resume Next
app.Run ctx, statusLine, contentType, body
app.Run ctx, applicationName, statusLine, contentType, body
If Err.Number <> 0 Then
Err.Clear
On Error Goto 0


+ 5
- 6
public/web.config Переглянути файл

@@ -22,12 +22,11 @@
</requestFiltering>
</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>
<rules>


Controllers/SelfTestController.wsc → test-app/Controllers/SelfTestController.wsc Переглянути файл

@@ -77,41 +77,17 @@ Sub RunSelfTest(logDir, body)
checks = AppendCheck(checks, "correlation_id_uniqueness", distinctOk, distinctDetail)
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
unkDetail = ""
On Error Resume Next
Set app = CreateObject("WscMvc.Application")
Set ctxUnknown = CreateObject("WscMvc.RequestContext")
ctxUnknown.Initialize "/self-test-does-not-exist", "GET", logDir
ctxUnknown.Initialize "/hello", "GET", logDir
unkStatus = "" : unkType = "" : unkBody = ""
app.Run ctxUnknown, unkStatus, unkType, unkBody
app.Run ctxUnknown, "tests", unkStatus, unkType, unkBody
If Err.Number <> 0 Then
unkDetail = Err.Description
Err.Clear
@@ -121,12 +97,11 @@ Sub RunSelfTest(logDir, body)
unkOk = True
End If
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

Set ctx1 = Nothing
Set ctx2 = Nothing
Set ctxHello = Nothing
Set ctxUnknown = Nothing
Set app = Nothing


+ 7
- 9
test-app/public/Default.asp Переглянути файл

@@ -1,17 +1,15 @@
<%@ Language="VBScript" %>
<% 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")
httpMethod = Request.ServerVariables("REQUEST_METHOD")
applicationName = "tests"
' logs/ deliberately lives outside the served "public" webroot (IIS site
' physical path = public/), so a parent-relative MapPath is required here -
' requires enableParentPaths=true for this site. See docs/DECISIONS.md.
@@ -62,7 +60,7 @@ contentType = ""
body = ""

On Error Resume Next
app.Run ctx, statusLine, contentType, body
app.Run ctx, applicationName, statusLine, contentType, body
If Err.Number <> 0 Then
Err.Clear
On Error Goto 0


+ 2
- 2
tests/Test-Components.vbs Переглянути файл

@@ -81,7 +81,7 @@ End If
If pass Then
statusLine = "" : contentType = "" : body = ""
On Error Resume Next
app.Run ctx1, statusLine, contentType, body
app.Run ctx1, "production", statusLine, contentType, body
If Err.Number <> 0 Then
WScript.Echo "FAIL: Application.Run raised error on /hello - " & Err.Description
pass = False
@@ -100,7 +100,7 @@ End If
If pass Then
statusLine = "" : contentType = "" : body = ""
On Error Resume Next
app.Run ctx2, statusLine, contentType, body
app.Run ctx2, "production", statusLine, contentType, body
If Err.Number <> 0 Then
WScript.Echo "FAIL: Application.Run raised error on unknown route - " & Err.Description
pass = False


+ 37
- 1
tests/Test-Http.ps1 Переглянути файл

@@ -1,7 +1,8 @@
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$BaseUrl
[string]$BaseUrl,
[string]$TestBaseUrl
)

$script:failures = 0
@@ -44,6 +45,41 @@ try {
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
# subfolder within it - this checks it's genuinely unreachable, not just
# filtered.


+ 13
- 4
tests/run-self-test.sh Переглянути файл

@@ -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")')

if [ "$ok" = "true" ]; then
echo "RESULT: ALL PASS"
exit 0
else
if [ "$ok" != "true" ]; then
echo "RESULT: FAILURE"
exit 1
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"

+ 5
- 1
tools/Register-Components.ps1 Переглянути файл

@@ -8,10 +8,14 @@ if (-not $ProjectRoot) {
}

$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\Application.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) {


+ 1
- 1
tools/Unregister-Components.ps1 Переглянути файл

@@ -15,7 +15,7 @@ if (-not $ProjectRoot) {
# can never be touched even if it happened to reuse a ProgID string.

$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 '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}' }


Завантаження…
Відмінити
Зберегти

Powered by TurnKey Linux.