| @@ -0,0 +1 @@ | |||
| *.sh text eol=lf | |||
| @@ -36,7 +36,9 @@ User instructions > `SPEC.md` accepted decisions > this file > implementation pl | |||
| - No implicit web or filesystem access from a service merely because a host object happens to be available. | |||
| ## Design and implementation workflow | |||
| For each change: explain a small intended behavior -> write/extend a test -> implement -> execute available tests -> review security and cleanup -> update docs. Keep one milestone in flight at a time. Prefer the simplest viable component contracts over prematurely generic abstractions. Keep `Default.asp` small and free from business rules. | |||
| For each change: define the contract first, including preconditions, postconditions, invariants, return/object ownership, and expected failure behavior; write or extend the closest failing test; implement the smallest production change; execute available tests; review security and cleanup; update docs. Keep one milestone in flight at a time. Prefer the simplest viable component contracts over prematurely generic abstractions. Keep `Default.asp` small and free from business rules. | |||
| Use TDD for new behavior and bug fixes. When practical, observe the test fail for the intended reason before implementation, then rerun it after the fix. If Windows/IIS access is unavailable for a host-dependent contract, mark that check NOT RUN and provide exact commands rather than treating review or parsing as a pass. | |||
| ## Registration and deployment | |||
| - Each WSC class has unique, immutable CLSID and documented ProgID. Never reuse GUIDs or quietly rename registered interfaces. | |||
| @@ -50,6 +52,7 @@ For each change: explain a small intended behavior -> write/extend a test -> imp | |||
| - Test both WSH component creation and IIS HTTP behavior where applicable. | |||
| - For M1 verify `/hello` status/content-type/exact body; denied direct `.wsc` access; safe failure; reversible registration; basic concurrent requests. | |||
| - Add regression tests before fixing discovered bugs. Flag concurrency and cross-request state risks. | |||
| - Include contract tests for public APIs and trust boundaries: valid input, representative invalid input, Null/Empty/Nothing handling, object/scalar return semantics, and error paths. | |||
| - Do not claim production readiness until the relevant security/performance phases and deployment tests pass. | |||
| ## Agent autonomy and self-improvement | |||
| @@ -9,12 +9,14 @@ Implement WSC-MVC milestone by milestone: IIS/Classic ASP host, registered VBScr | |||
| - At the start of each session, determine current milestone from implementation plan and actual tests, not from filenames alone. | |||
| - Inspect changed files and prior decisions; preserve user changes. | |||
| - For a task crossing architectural boundaries, first write a brief plan referencing the exact SPEC acceptance criteria. | |||
| - Before implementation, state the Design by Contract shape for the change: preconditions, postconditions, invariants, ownership, and expected failure behavior. | |||
| - Use small edits and run the closest available test after each meaningful increment. | |||
| - Follow TDD: write or update the failing test first where practical, confirm the failure is meaningful, then make the minimum implementation change and rerun it. | |||
| - If tool access lacks Windows or IIS, implement only what can be responsibly checked and clearly report Windows integration as NOT RUN. | |||
| - Do not turn guesses about WSC registration, COM marshaling, WSC XML semantics, IIS rewrite, or hot reload into established facts. | |||
| ## Code review checklist | |||
| Confirm: VBScript `Option Explicit`; WSC `<public>` and script procedure agreement; stable COM identity; object/scalar assignment semantics; checked error paths; request-scoped state; safe response ownership; no URL-controlled arbitrary ProgID/method; no publicly accessible source/config/templates; tests updated; docs accurate. | |||
| Confirm: contract defined and tested; VBScript `Option Explicit`; WSC `<public>` and script procedure agreement; stable COM identity; object/scalar assignment semantics; checked error paths; request-scoped state; safe response ownership; no URL-controlled arbitrary ProgID/method; no publicly accessible source/config/templates; tests updated; docs accurate. | |||
| ## Before concluding | |||
| Summarize modified files, what actually ran, proof of acceptance or blocked tests, and the single next milestone. Never say 'done' for a host-dependent milestone without IIS evidence. Propose rule improvements with evidence; do not silently edit architectural guardrails. | |||
| @@ -15,6 +15,7 @@ | |||
| <parameter name="statusLine"/> | |||
| <parameter name="contentType"/> | |||
| <parameter name="body"/> | |||
| <parameter name="allowHeader"/> | |||
| </method> | |||
| </public> | |||
| @@ -26,93 +27,134 @@ 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, applicationName, statusLine, contentType, body) | |||
| Dim ctrl, helloBody, path | |||
| Sub Run(ctx, applicationName, statusLine, contentType, body, allowHeader) | |||
| Dim router, handlerKey | |||
| path = ctx.Path | |||
| statusLine = "" | |||
| contentType = "" | |||
| body = "" | |||
| allowHeader = "" | |||
| handlerKey = "" | |||
| If applicationName = "production" And path = "/hello" Then | |||
| Set ctrl = Nothing | |||
| On Error Resume Next | |||
| Set ctrl = CreateObject("WscMvc.HomeController") | |||
| If Err.Number <> 0 Then | |||
| Err.Clear | |||
| On Error Goto 0 | |||
| statusLine = "500 Internal Server Error" | |||
| contentType = "text/plain; charset=utf-8" | |||
| body = "Internal Server Error" | |||
| LogOutcome ctx, statusLine | |||
| Exit Sub | |||
| End If | |||
| Set router = Nothing | |||
| On Error Resume Next | |||
| Set router = CreateObject("WscMvc.Router") | |||
| If Err.Number <> 0 Then | |||
| Err.Clear | |||
| On Error Goto 0 | |||
| InternalServerError statusLine, contentType, body, allowHeader | |||
| LogOutcome ctx, statusLine | |||
| Exit Sub | |||
| End If | |||
| On Error Goto 0 | |||
| helloBody = "" | |||
| On Error Resume Next | |||
| ctrl.Hello helloBody | |||
| If Err.Number <> 0 Then | |||
| Err.Clear | |||
| On Error Goto 0 | |||
| statusLine = "500 Internal Server Error" | |||
| contentType = "text/plain; charset=utf-8" | |||
| body = "Internal Server Error" | |||
| Set ctrl = Nothing | |||
| LogOutcome ctx, statusLine | |||
| Exit Sub | |||
| End If | |||
| On Error Resume Next | |||
| router.Match ctx, applicationName, statusLine, contentType, body, allowHeader, handlerKey | |||
| If Err.Number <> 0 Then | |||
| Err.Clear | |||
| On Error Goto 0 | |||
| Set router = Nothing | |||
| InternalServerError statusLine, contentType, body, allowHeader | |||
| LogOutcome ctx, statusLine | |||
| Exit Sub | |||
| End If | |||
| On Error Goto 0 | |||
| statusLine = "200 OK" | |||
| contentType = "text/html; charset=utf-8" | |||
| body = helloBody | |||
| Set ctrl = Nothing | |||
| ElseIf applicationName = "tests" And path = "/self-test" Then | |||
| Set ctrl = Nothing | |||
| On Error Resume Next | |||
| Set ctrl = CreateObject("WscMvc.SelfTestController") | |||
| If Err.Number <> 0 Then | |||
| Err.Clear | |||
| On Error Goto 0 | |||
| statusLine = "500 Internal Server Error" | |||
| contentType = "text/plain; charset=utf-8" | |||
| body = "Internal Server Error" | |||
| LogOutcome ctx, statusLine | |||
| Exit Sub | |||
| End If | |||
| Set router = Nothing | |||
| If Len(handlerKey) = 0 Then | |||
| LogOutcome ctx, statusLine | |||
| Exit Sub | |||
| End If | |||
| Select Case handlerKey | |||
| Case "Home.Hello" | |||
| RunHomeHello ctx, statusLine, contentType, body, allowHeader | |||
| Case "SelfTest.RunSelfTest" | |||
| RunSelfTest ctx, statusLine, contentType, body, allowHeader | |||
| Case Else | |||
| InternalServerError statusLine, contentType, body, allowHeader | |||
| End Select | |||
| LogOutcome ctx, statusLine | |||
| End Sub | |||
| Sub RunHomeHello(ctx, statusLine, contentType, body, allowHeader) | |||
| Dim ctrl, helloBody | |||
| Set ctrl = Nothing | |||
| On Error Resume Next | |||
| Set ctrl = CreateObject("WscMvc.HomeController") | |||
| If Err.Number <> 0 Then | |||
| Err.Clear | |||
| On Error Goto 0 | |||
| InternalServerError statusLine, contentType, body, allowHeader | |||
| Exit Sub | |||
| End If | |||
| On Error Goto 0 | |||
| Dim selfTestBody | |||
| selfTestBody = "" | |||
| On Error Resume Next | |||
| ctrl.RunSelfTest ctx.LogDir, selfTestBody | |||
| If Err.Number <> 0 Then | |||
| Err.Clear | |||
| On Error Goto 0 | |||
| statusLine = "500 Internal Server Error" | |||
| contentType = "text/plain; charset=utf-8" | |||
| body = "Internal Server Error" | |||
| Set ctrl = Nothing | |||
| LogOutcome ctx, statusLine | |||
| Exit Sub | |||
| End If | |||
| helloBody = "" | |||
| On Error Resume Next | |||
| ctrl.Hello helloBody | |||
| If Err.Number <> 0 Then | |||
| Err.Clear | |||
| On Error Goto 0 | |||
| Set ctrl = Nothing | |||
| InternalServerError statusLine, contentType, body, allowHeader | |||
| Exit Sub | |||
| End If | |||
| On Error Goto 0 | |||
| statusLine = "200 OK" | |||
| contentType = "text/html; charset=utf-8" | |||
| body = helloBody | |||
| allowHeader = "" | |||
| Set ctrl = Nothing | |||
| End Sub | |||
| Sub RunSelfTest(ctx, statusLine, contentType, body, allowHeader) | |||
| Dim ctrl, selfTestBody | |||
| Set ctrl = Nothing | |||
| On Error Resume Next | |||
| Set ctrl = CreateObject("WscMvc.SelfTestController") | |||
| If Err.Number <> 0 Then | |||
| Err.Clear | |||
| On Error Goto 0 | |||
| InternalServerError statusLine, contentType, body, allowHeader | |||
| Exit Sub | |||
| End If | |||
| On Error Goto 0 | |||
| ' Always 200: the self-test HTTP call itself succeeded. Whether the | |||
| ' underlying checks passed is reported in the JSON body's "ok" field | |||
| ' and per-check "pass" fields, not the HTTP status - this matches | |||
| ' conventional health-check endpoint design (5xx is reserved for the | |||
| ' diagnostics mechanism itself being broken, handled above). | |||
| statusLine = "200 OK" | |||
| contentType = "application/json; charset=utf-8" | |||
| body = selfTestBody | |||
| selfTestBody = "" | |||
| On Error Resume Next | |||
| ctrl.RunSelfTest ctx.LogDir, selfTestBody | |||
| If Err.Number <> 0 Then | |||
| Err.Clear | |||
| On Error Goto 0 | |||
| Set ctrl = Nothing | |||
| Else | |||
| ' Expected outcome, not a failure: no matching route yet (M3 adds a real table). | |||
| statusLine = "404 Not Found" | |||
| contentType = "text/plain; charset=utf-8" | |||
| body = "Not Found" | |||
| InternalServerError statusLine, contentType, body, allowHeader | |||
| Exit Sub | |||
| End If | |||
| On Error Goto 0 | |||
| LogOutcome ctx, statusLine | |||
| ' Always 200: the self-test HTTP call itself succeeded. Whether the | |||
| ' underlying checks passed is reported in the JSON body's "ok" field | |||
| ' and per-check "pass" fields, not the HTTP status - this matches | |||
| ' conventional health-check endpoint design (5xx is reserved for the | |||
| ' diagnostics mechanism itself being broken, handled above). | |||
| statusLine = "200 OK" | |||
| contentType = "application/json; charset=utf-8" | |||
| body = selfTestBody | |||
| allowHeader = "" | |||
| Set ctrl = Nothing | |||
| End Sub | |||
| Sub InternalServerError(statusLine, contentType, body, allowHeader) | |||
| statusLine = "500 Internal Server Error" | |||
| contentType = "text/plain; charset=utf-8" | |||
| body = "Internal Server Error" | |||
| allowHeader = "" | |||
| End Sub | |||
| ' Best-effort diagnostics only: a logging failure must never affect the | |||
| @@ -0,0 +1,141 @@ | |||
| <?xml version="1.0"?> | |||
| <?component error="true" debug="false"?> | |||
| <component> | |||
| <registration | |||
| description="WscMvc explicit route table" | |||
| progid="WscMvc.Router" | |||
| version="1.00" | |||
| classid="{C92F9338-B478-4EAD-B865-892FFB1E1C51}"> | |||
| </registration> | |||
| <public> | |||
| <method name="Match"> | |||
| <parameter name="ctx"/> | |||
| <parameter name="applicationName"/> | |||
| <parameter name="statusLine"/> | |||
| <parameter name="contentType"/> | |||
| <parameter name="body"/> | |||
| <parameter name="allowHeader"/> | |||
| <parameter name="handlerKey"/> | |||
| </method> | |||
| </public> | |||
| <script language="VBScript"> | |||
| <![CDATA[ | |||
| Option Explicit | |||
| ' Explicit allowlisted route table. This component decides only route | |||
| ' metadata; Application.wsc performs the corresponding hardcoded handler | |||
| ' calls. No URL segment is ever treated as a ProgID or method name. | |||
| Sub Match(ctx, applicationName, statusLine, contentType, body, allowHeader, handlerKey) | |||
| Dim rawPath, normalizedPath, httpMethod | |||
| statusLine = "" | |||
| contentType = "" | |||
| body = "" | |||
| allowHeader = "" | |||
| handlerKey = "" | |||
| rawPath = ctx.Path | |||
| httpMethod = UCase(Trim(CStr(ctx.HttpMethod))) | |||
| If Not TryNormalizePath(rawPath, normalizedPath) Then | |||
| BadRequest statusLine, contentType, body | |||
| Exit Sub | |||
| End If | |||
| ' Literal routes are intentionally listed directly. Parameterized routes, | |||
| ' when added, must be checked after these literal comparisons. | |||
| If applicationName = "production" Then | |||
| If normalizedPath = "/hello" Then | |||
| MatchMethod httpMethod, "GET", "Home.Hello", statusLine, contentType, body, allowHeader, handlerKey | |||
| Exit Sub | |||
| End If | |||
| ElseIf applicationName = "tests" Then | |||
| If normalizedPath = "/self-test" Then | |||
| MatchMethod httpMethod, "GET, POST", "SelfTest.RunSelfTest", statusLine, contentType, body, allowHeader, handlerKey | |||
| Exit Sub | |||
| End If | |||
| End If | |||
| statusLine = "404 Not Found" | |||
| contentType = "text/plain; charset=utf-8" | |||
| body = "Not Found" | |||
| End Sub | |||
| Function TryNormalizePath(rawPath, normalizedPath) | |||
| Dim path | |||
| TryNormalizePath = False | |||
| normalizedPath = "" | |||
| If IsNull(rawPath) Or IsEmpty(rawPath) Then | |||
| path = "/" | |||
| Else | |||
| path = Trim(CStr(rawPath)) | |||
| If Len(path) = 0 Then path = "/" | |||
| End If | |||
| If Left(path, 1) <> "/" Then Exit Function | |||
| If InStr(path, "\") > 0 Then Exit Function | |||
| If InStr(path, "%") > 0 Then Exit Function | |||
| If InStr(path, "?") > 0 Then Exit Function | |||
| If InStr(path, "#") > 0 Then Exit Function | |||
| If InStr(path, "//") > 0 Then Exit Function | |||
| If InStr(path, "/./") > 0 Then Exit Function | |||
| If InStr(path, "/../") > 0 Then Exit Function | |||
| If Right(path, 2) = "/." Then Exit Function | |||
| If Right(path, 3) = "/.." Then Exit Function | |||
| If ContainsControlCharacter(path) Then Exit Function | |||
| If Len(path) > 1 And Right(path, 1) = "/" Then | |||
| path = Left(path, Len(path) - 1) | |||
| End If | |||
| normalizedPath = LCase(path) | |||
| TryNormalizePath = True | |||
| End Function | |||
| Function ContainsControlCharacter(value) | |||
| Dim i, code | |||
| ContainsControlCharacter = False | |||
| For i = 1 To Len(value) | |||
| code = Asc(Mid(value, i, 1)) | |||
| If code < 32 Or code = 127 Then | |||
| ContainsControlCharacter = True | |||
| Exit Function | |||
| End If | |||
| Next | |||
| End Function | |||
| Sub MatchMethod(httpMethod, allowedMethods, matchedHandlerKey, statusLine, contentType, body, allowHeader, handlerKey) | |||
| If IsAllowedMethod(httpMethod, allowedMethods) Then | |||
| handlerKey = matchedHandlerKey | |||
| Else | |||
| statusLine = "405 Method Not Allowed" | |||
| contentType = "text/plain; charset=utf-8" | |||
| body = "Method Not Allowed" | |||
| allowHeader = allowedMethods | |||
| End If | |||
| End Sub | |||
| Function IsAllowedMethod(httpMethod, allowedMethods) | |||
| Dim methods, i | |||
| methods = Split(allowedMethods, ",") | |||
| IsAllowedMethod = False | |||
| For i = 0 To UBound(methods) | |||
| If httpMethod = Trim(methods(i)) Then | |||
| IsAllowedMethod = True | |||
| Exit Function | |||
| End If | |||
| Next | |||
| End Function | |||
| Sub BadRequest(statusLine, contentType, body) | |||
| statusLine = "400 Bad Request" | |||
| contentType = "text/plain; charset=utf-8" | |||
| body = "Bad Request" | |||
| End Sub | |||
| ]]> | |||
| </script> | |||
| </component> | |||
| @@ -26,10 +26,10 @@ Gate: all M1 SPEC acceptance tests pass on IIS, or mark blocked with evidence; d | |||
| Gate: repeated/concurrent requests and failure cases behave predictably. **MET** — see `docs/TEST-RESULTS.md`. HTTP response correctness holds unconditionally under concurrency (8/8 correct every run); best-effort logging completeness under heavy concurrency is an accepted, documented tradeoff, not a gate failure. | |||
| ## M3 — Routing | |||
| - [ ] Explicit route map with GET/POST, literal routes first. | |||
| - [ ] Safe decoding/validation, 400/404/405, Allow header. | |||
| - [ ] No arbitrary URL-to-ProgID or URL-to-method dispatch. | |||
| Gate: route matrix and malformed URL tests pass. | |||
| - [x] Explicit route map with GET/POST, literal routes first. | |||
| - [x] Safe decoding/validation, 400/404/405, Allow header. | |||
| - [x] No arbitrary URL-to-ProgID or URL-to-method dispatch. | |||
| Gate: route matrix and malformed URL tests pass. **MET** — see `docs/TEST-RESULTS.md`. WSH covers full Router/Application routing contract; IIS HTTP covers GET/POST-routed behavior and malformed route handling. PUT/DELETE can be intercepted by IIS before Classic ASP on the tested host, so those are not treated as framework HTTP-route assertions. | |||
| ## M4 — Views | |||
| - [ ] Safe separate template loading, text-context encoding, default-deny for private templates. | |||
| @@ -35,4 +35,4 @@ powershell -File tests\Test-Http.ps1 -BaseUrl http://localhost:8090 -TestBaseUrl | |||
| ## Status | |||
| M0–M2 gated PASS with real test evidence on Windows/IIS; see `docs/TEST-RESULTS.md`. Next milestone: M3 (explicit route table) per `IMPLEMENTATION_PLAN.md`. | |||
| M0–M3 gated PASS with real test evidence on Windows/IIS; see `docs/TEST-RESULTS.md`. Next milestone: M4 (safe separate HTML templates and encoding) per `IMPLEMENTATION_PLAN.md`. | |||
| @@ -6,6 +6,8 @@ Status: Proposed; implementation behavior must be verified on the target Windows | |||
| ## 1. Mission | |||
| Build a small, maintainable, WSC-first MVC framework for Classic ASP. IIS receives HTTP traffic; a single ASP bootstrap dispatches requests to VBScript Windows Script Components registered as COM objects. Application behavior lives in WSC files; presentation lives in separate HTML templates. Ship verified increments rather than an elaborate untested framework. | |||
| Development uses Design by Contract and test-driven development: define the observable contract first, write or extend the failing test that proves the contract, then implement the smallest change that makes the test pass. | |||
| ## 2. Fixed decisions | |||
| - Runtime: Windows, IIS, Classic ASP, VBScript; no ASP.NET, PHP, Node.js, or JScript runtime dependency. | |||
| - Object architecture: registered `.wsc` COM components; stable ProgIDs and CLSIDs. | |||
| @@ -123,6 +125,14 @@ The tree is a target, not an instruction to create placeholder classes before th | |||
| - Define a testable strategy for already-committed responses. Never pretend an exception can undo a sent body. | |||
| - Distinguish an expected not-found/validation result from a programmer/COM failure. | |||
| ## 11A. Development method contract | |||
| - Use Design by Contract for every public component/API change: document the caller obligations (preconditions), promised outcomes (postconditions), stable assumptions across calls (invariants), error behavior, and object/scalar ownership before or alongside implementation. | |||
| - Contracts must be concrete enough to test in VBScript/WSH, IIS HTTP tests, or a documented manual host check. Avoid vague promises such as "handles invalid input" without named cases. | |||
| - Use TDD for new behavior and bug fixes: add or update the closest failing test first, confirm it fails for the intended reason where practical, then implement the minimum production change and rerun the test. | |||
| - Keep contract checks explicit at trust boundaries: route input, COM public methods, template data, database parameters, filesystem paths, and deployment scripts. | |||
| - If a host-dependent contract cannot be executed in the current environment, mark it NOT RUN with the exact Windows/IIS command that must verify it; do not treat static inspection as a passing test. | |||
| - Do not weaken an existing contract to make a test pass unless the SPEC or `docs/DECISIONS.md` is deliberately updated with the reason and evidence. | |||
| ## 12. Definition of done for each component | |||
| 1. Contract and source file exist; XML is well-formed. | |||
| 2. Public declarations match implemented functions/procedures and documented names. | |||
| @@ -133,6 +143,7 @@ The tree is a target, not an instruction to create placeholder classes before th | |||
| 7. Security review covers direct HTTP exposure and untrusted input. | |||
| 8. README or architecture docs reflect actual implementation, not intentions. | |||
| 9. Tests are reported as PASS, FAIL, or NOT RUN with commands, host details, and evidence. | |||
| 10. Preconditions, postconditions, invariants, and representative invalid inputs have automated tests or an explicit NOT RUN/manual verification note. | |||
| ## 13. Milestone sequence and gates | |||
| M0: Inspect repository/environment; record OS/IIS/ASP/bitness/permissions and missing tools. No claim of execution on Linux/macOS. | |||
| @@ -1,4 +1,4 @@ | |||
| # WSC-MVC — Architecture (as implemented through M2) | |||
| # WSC-MVC — Architecture (as implemented through M3) | |||
| ## Request flow | |||
| @@ -8,19 +8,22 @@ GET / or GET /hello | |||
| -> /Default.asp?route=/hello | |||
| -> Server.CreateObject("WscMvc.RequestContext"); ctx.Initialize path, httpMethod, logDir | |||
| -> Server.CreateObject("WscMvc.Application") | |||
| -> Application.Run(ctx, "production", statusLine, contentType, body) [Framework/Application.wsc] | |||
| -> Application.Run(ctx, "production", statusLine, contentType, body, allowHeader) [Framework/Application.wsc] | |||
| -> CreateObject("WscMvc.Router") | |||
| -> Router.Match(ctx, "production", ..., handlerKey) [Framework/Router.wsc] | |||
| -> CreateObject("WscMvc.HomeController") | |||
| -> HomeController.Hello(body) [Controllers/HomeController.wsc] | |||
| -> LogOutcome ctx, statusLine (best-effort append to logs/app.log) | |||
| -> Default.asp sets Response.Status/ContentType, writes body | |||
| -> Default.asp sets Response.Status/ContentType, optional Allow header, writes body | |||
| ``` | |||
| ## 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`, `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. | |||
| - `Framework/RequestContext.wsc`, `Framework/Router.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 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`. | |||
| - Routing is an explicit literal allowlist in `Framework/Router.wsc`. 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 normalized path. Literal routes are checked directly before any future parameterized route support. Router decisions return either a hardcoded handler key for `Application.wsc` to dispatch with explicit `Select Case` branches, or a completed expected response (`400`, `404`, `405` with `Allow`). No URL segment is ever treated as a ProgID, method name, template name, or filesystem path. | |||
| - Route path handling is deliberately conservative in M3: ASP/IIS has already decoded the `route` query parameter once, so `Router.wsc` does not decode again. It rejects remaining `%` escapes, backslashes, query/fragment markers, doubled slashes, dot-segments, and control characters, then lowercases and trims one trailing slash for literal matching. The original path remains available through `RequestContext.Path` for logging. | |||
| - `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 | |||
| @@ -31,7 +34,7 @@ GET / or GET /hello | |||
| ## Test harness: GET /self-test — its own app, only Framework/ is shared | |||
| `test-app/Controllers/SelfTestController.wsc` (`WscMvc.SelfTestController`) exposes framework checks over plain HTTP and returns JSON — no PowerShell, cscript, or SSH access to the VM required. Both the test site's root (`GET /`) and `GET /self-test` rewrite to the test app's `/self-test` route. Production similarly maps both `GET /` and `GET /hello` to `/hello`. | |||
| `test-app/Controllers/SelfTestController.wsc` (`WscMvc.SelfTestController`) exposes framework checks over plain HTTP and returns JSON — no PowerShell, cscript, or SSH access to the VM required. Both the test site's root (`GET /`) and `GET /self-test` rewrite to the test app's `/self-test` route. The route table allows both `GET` and `POST` for `/self-test`. Production similarly maps both `GET /` and `GET /hello` to `/hello`, which is `GET` only. | |||
| **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`). | |||
| @@ -55,11 +58,12 @@ curl http://100.127.62.31:8091/self-test | |||
| | Component | ProgID | CLSID | | |||
| |---|---|---| | |||
| | `Framework/RequestContext.wsc` | `WscMvc.RequestContext` | `{1C36FA55-34DF-4974-94B9-D657389362B2}` | | |||
| | `Framework/Router.wsc` | `WscMvc.Router` | `{C92F9338-B478-4EAD-B865-892FFB1E1C51}` | | |||
| | `Framework/Application.wsc` | `WscMvc.Application` | `{851C7763-1638-42FE-A166-BF3DD3A96A88}` | | |||
| | `Controllers/HomeController.wsc` | `WscMvc.HomeController` | `{87488446-60BE-4068-8368-0B709BB68F3F}` | | |||
| | `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 during pre-release development (M2 added a leading `ctx` parameter; M3 added an `allowHeader` output parameter) under the same CLSID; acceptable because this is active pre-release (v0.1) development with exactly one caller shape (`Default.asp`, updated in lockstep) — not a claim that live interface changes are safe for a published/external client. | |||
| ## IIS sites (test host: win2025test, 100.127.62.31) | |||
| @@ -83,4 +87,4 @@ Both are 64-bit, no managed code, anonymous auth identity `IUSR`. Each site's `p | |||
| ## Deferred to later milestones (do not implement early) | |||
| Per SPEC §3 non-goals and IMPLEMENTATION_PLAN M3+: explicit route table, HTML views/templates, ADODB, auth. A more robust logging mechanism (if complete coverage under concurrent load is ever required) is deferred to M6 — see the concurrency finding in `docs/DECISIONS.md`. | |||
| Per SPEC §3 non-goals and IMPLEMENTATION_PLAN M4+: HTML views/templates, ADODB, auth. A more robust logging mechanism (if complete coverage under concurrent load is ever required) is deferred to M6 — see the concurrency finding in `docs/DECISIONS.md`. | |||
| @@ -107,3 +107,13 @@ Follow-up direction from Daniel right after the previous entry: "Test app should | |||
| **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. | |||
| ## M3 — Explicit route table and method handling (2026-09-19) | |||
| Added `Framework/Router.wsc` (`WscMvc.Router`) as the explicit route allowlist. It receives the per-request `RequestContext` and fixed application selector (`production` or `tests`), then returns either a hardcoded handler key or a completed expected response. `Application.wsc` dispatches only through explicit `Select Case` branches for those handler keys; no URL text is ever used as a ProgID or method name. | |||
| Route path validation is intentionally conservative for M3. ASP/IIS has already decoded the `route` query parameter once, so the router does not decode it again. Any remaining `%` escape, backslash, query/fragment marker, doubled slash, dot-segment, or control character is rejected with `400 Bad Request`; otherwise the route is lowercased and one trailing slash is trimmed for literal matching. The original path remains preserved in `RequestContext.Path` and logs. | |||
| The response contract gained an `allowHeader` out parameter from `Application.Run`. `Default.asp` is still the only layer that touches ASP `Response`; it emits the `Allow` header only when the framework returns one. This keeps routing/framework code ASP-intrinsic-free while still allowing correct HTTP behavior for app-routed `405 Method Not Allowed` responses. | |||
| IIS/Classic ASP on the local Windows 11 test host intercepted `PUT`/`DELETE` requests before they reached the application, returning IIS's own `Allow: GET, HEAD, OPTIONS, TRACE`. Therefore HTTP integration tests assert framework 405 behavior using `POST /hello`, which Classic ASP does deliver to the app and which returns the framework's `Allow: GET`. The full Router/Application unsupported-method contract, including `DELETE /self-test -> Allow: GET, POST`, is covered by WSH component tests and the self-test controller's direct `Router.Match` checks. This matches the M3 scope: support GET and POST initially; distinguish unsupported methods where the request reaches the framework. | |||
| @@ -277,3 +277,80 @@ http://100.127.62.31:8091/self-test -> 200, JSON ok=true | |||
| ``` | |||
| `Test-Http.ps1 -BaseUrl http://localhost:8090 -TestBaseUrl http://localhost:8091` and `run-self-test.sh http://100.127.62.31:8091`: **PASS**, including existing cross-app direct-query isolation checks. | |||
| ## M3 — Explicit route table, 400/404/405, GET/POST dispatch (2026-09-19) | |||
| Host under test for this M3 run: `DESKTOP-80D128R`, Microsoft Windows 11 Pro 10.0.26200 build 26200, 64-bit, Intel Core i7-8750H. Local IIS sites created from this checkout: | |||
| ``` | |||
| WscMvc Started C:\Development\wsc-mvc\public WscMvc | |||
| WscMvcTests Started C:\Development\wsc-mvc\test-app\public WscMvcTests | |||
| ``` | |||
| App pools: `WscMvc` and `WscMvcTests`, 64-bit (`enable32BitAppOnWin64=False`), no managed runtime. Components registered from `C:\Development\wsc-mvc` using: | |||
| ``` | |||
| powershell -ExecutionPolicy Bypass -File tools\Register-Components.ps1 -ProjectRoot C:\Development\wsc-mvc | |||
| ``` | |||
| Result: **PASS** — registered `RequestContext`, new `Router`, `Application`, `HomeController`, and test-app `SelfTestController`. | |||
| Test-first check before implementation: | |||
| ``` | |||
| cscript //nologo tests\Test-Components.vbs | |||
| ``` | |||
| Result before M3 implementation: **FAIL**, as expected for the new contract: | |||
| ``` | |||
| FAIL: Application.Run raised error on /hello - Wrong number of arguments or invalid property assignment | |||
| RESULT: FAILURE | |||
| ``` | |||
| WSH component contract after implementation: | |||
| ``` | |||
| cscript //nologo tests\Test-Components.vbs | |||
| ``` | |||
| Result: **PASS**. Covered `GET /hello -> 200`, unknown route -> `404`, `POST /hello -> 405` with `Allow: GET`, `POST /self-test` in the test app -> `200` JSON, malformed `/hello/../secret -> 400`, and logging. | |||
| HTTP integration: | |||
| ``` | |||
| powershell -ExecutionPolicy Bypass -File tests\Test-Http.ps1 -BaseUrl http://localhost:8090 -TestBaseUrl http://localhost:8091 | |||
| ``` | |||
| Result: **PASS**: | |||
| ``` | |||
| PASS: GET /hello status 200 | |||
| PASS: GET /hello content-type | |||
| PASS: GET /hello body | |||
| PASS: POST /hello returns 405 | |||
| PASS: POST /hello Allow header | |||
| PASS: malformed route returns 400 | |||
| PASS: GET / status 200 | |||
| PASS: GET / maps to /hello | |||
| PASS: GET /self-test not exposed on production | |||
| PASS: direct Default.asp cannot cross into test routes | |||
| PASS: test app cannot cross into production routes | |||
| PASS: POST /self-test on test app status 200 | |||
| PASS: POST /self-test content-type | |||
| PASS: GET /Framework/Application.wsc unreachable | |||
| RESULT: ALL PASS | |||
| ``` | |||
| Self-test JSON endpoint: | |||
| ``` | |||
| $resp = Invoke-WebRequest -Uri http://localhost:8091/self-test -UseBasicParsing | |||
| $json = $resp.Content | ConvertFrom-Json | |||
| ``` | |||
| Result: **PASS** — `ok=true`, checks included `request_context_contract`, `correlation_id_uniqueness`, `test_app_rejects_production_route`, `post_self_test_route`, and `delete_self_test_allow_header`. | |||
| `tests/run-self-test.sh` local note: first failed under local bash because the Windows checkout had CRLF line endings in the `.sh` file (`set: pipefail\r: invalid option name`). Added `.gitattributes` (`*.sh text eol=lf`) and normalized the script. After that, local WSL/bash still could not reach Windows IIS on `localhost:8091`/gateway IP from this environment (`curl: (7) Failed to connect`), so the bash wrapper is **NOT RUN/PASS locally**. The same HTTP JSON contract was verified with PowerShell above; the bash wrapper remains intended for a CLI that can reach the test-app URL, as in earlier VM/tailnet evidence. | |||
| M3 gate status: **PASS** for framework and IIS GET/POST behavior. PUT/DELETE HTTP probes are not used as app-route assertions on this host because IIS intercepts them before Classic ASP; WSH/self-test Router checks cover the unsupported-method framework contract directly. | |||
| @@ -1,7 +1,7 @@ | |||
| <%@ Language="VBScript" %> | |||
| <% Option Explicit %> | |||
| <% | |||
| Dim route, httpMethod, logDir, ctx, app, applicationName, statusLine, contentType, body | |||
| Dim route, httpMethod, logDir, ctx, app, applicationName, statusLine, contentType, body, allowHeader | |||
| route = Request.QueryString("route") | |||
| httpMethod = Request.ServerVariables("REQUEST_METHOD") | |||
| @@ -54,9 +54,10 @@ On Error Goto 0 | |||
| statusLine = "" | |||
| contentType = "" | |||
| body = "" | |||
| allowHeader = "" | |||
| On Error Resume Next | |||
| app.Run ctx, applicationName, statusLine, contentType, body | |||
| app.Run ctx, applicationName, statusLine, contentType, body, allowHeader | |||
| If Err.Number <> 0 Then | |||
| Err.Clear | |||
| On Error Goto 0 | |||
| @@ -71,6 +72,9 @@ On Error Goto 0 | |||
| Response.Status = statusLine | |||
| Response.ContentType = contentType | |||
| If Len(allowHeader) > 0 Then | |||
| Response.AddHeader "Allow", allowHeader | |||
| End If | |||
| Response.Write body | |||
| Set app = Nothing | |||
| @@ -79,15 +79,15 @@ Sub RunSelfTest(logDir, body) | |||
| ' --- 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 | |||
| Dim app, ctxUnknown, unkStatus, unkType, unkBody, unkAllow, unkOk, unkDetail | |||
| unkOk = False | |||
| unkDetail = "" | |||
| On Error Resume Next | |||
| Set app = CreateObject("WscMvc.Application") | |||
| Set ctxUnknown = CreateObject("WscMvc.RequestContext") | |||
| ctxUnknown.Initialize "/hello", "GET", logDir | |||
| unkStatus = "" : unkType = "" : unkBody = "" | |||
| app.Run ctxUnknown, "tests", unkStatus, unkType, unkBody | |||
| unkStatus = "" : unkType = "" : unkBody = "" : unkAllow = "" | |||
| app.Run ctxUnknown, "tests", unkStatus, unkType, unkBody, unkAllow | |||
| If Err.Number <> 0 Then | |||
| unkDetail = Err.Description | |||
| Err.Clear | |||
| @@ -100,9 +100,59 @@ Sub RunSelfTest(logDir, body) | |||
| checks = AppendCheck(checks, "test_app_rejects_production_route", unkOk, unkDetail) | |||
| allPass = allPass And unkOk | |||
| ' --- Method dispatch contract: test app self-test route supports both | |||
| ' GET and POST, and reports an Allow header for unsupported methods. --- | |||
| Dim router, ctxPost, postStatus, postType, postBody, postAllow, postHandler, postOk, postDetail | |||
| postOk = False | |||
| postDetail = "" | |||
| On Error Resume Next | |||
| Set router = CreateObject("WscMvc.Router") | |||
| Set ctxPost = CreateObject("WscMvc.RequestContext") | |||
| ctxPost.Initialize "/self-test", "POST", logDir | |||
| postStatus = "" : postType = "" : postBody = "" : postAllow = "" : postHandler = "" | |||
| router.Match ctxPost, "tests", postStatus, postType, postBody, postAllow, postHandler | |||
| If Err.Number <> 0 Then | |||
| postDetail = Err.Description | |||
| Err.Clear | |||
| ElseIf postHandler <> "SelfTest.RunSelfTest" Then | |||
| postDetail = "Expected SelfTest.RunSelfTest handler, got [" & postHandler & "]" | |||
| ElseIf postStatus <> "" Then | |||
| postDetail = "Expected empty status for matched route, got [" & postStatus & "]" | |||
| Else | |||
| postOk = True | |||
| End If | |||
| On Error Goto 0 | |||
| checks = AppendCheck(checks, "post_self_test_route", postOk, postDetail) | |||
| allPass = allPass And postOk | |||
| Dim ctxDelete, delStatus, delType, delBody, delAllow, delHandler, delOk, delDetail | |||
| delOk = False | |||
| delDetail = "" | |||
| On Error Resume Next | |||
| Set ctxDelete = CreateObject("WscMvc.RequestContext") | |||
| ctxDelete.Initialize "/self-test", "DELETE", logDir | |||
| delStatus = "" : delType = "" : delBody = "" : delAllow = "" : delHandler = "" | |||
| router.Match ctxDelete, "tests", delStatus, delType, delBody, delAllow, delHandler | |||
| If Err.Number <> 0 Then | |||
| delDetail = Err.Description | |||
| Err.Clear | |||
| ElseIf delStatus <> "405 Method Not Allowed" Then | |||
| delDetail = "Expected 405 Method Not Allowed, got [" & delStatus & "]" | |||
| ElseIf delAllow <> "GET, POST" Then | |||
| delDetail = "Expected Allow [GET, POST], got [" & delAllow & "]" | |||
| Else | |||
| delOk = True | |||
| End If | |||
| On Error Goto 0 | |||
| checks = AppendCheck(checks, "delete_self_test_allow_header", delOk, delDetail) | |||
| allPass = allPass And delOk | |||
| Set ctx1 = Nothing | |||
| Set ctx2 = Nothing | |||
| Set ctxUnknown = Nothing | |||
| Set ctxPost = Nothing | |||
| Set ctxDelete = Nothing | |||
| Set router = Nothing | |||
| Set app = Nothing | |||
| body = "{""ok"":" & LCase(CStr(allPass)) & ",""checks"":[" & checks & "]}" | |||
| @@ -5,7 +5,7 @@ | |||
| ' 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 | |||
| Dim route, httpMethod, logDir, ctx, app, applicationName, statusLine, contentType, body, allowHeader | |||
| route = Request.QueryString("route") | |||
| httpMethod = Request.ServerVariables("REQUEST_METHOD") | |||
| @@ -58,9 +58,10 @@ On Error Goto 0 | |||
| statusLine = "" | |||
| contentType = "" | |||
| body = "" | |||
| allowHeader = "" | |||
| On Error Resume Next | |||
| app.Run ctx, applicationName, statusLine, contentType, body | |||
| app.Run ctx, applicationName, statusLine, contentType, body, allowHeader | |||
| If Err.Number <> 0 Then | |||
| Err.Clear | |||
| On Error Goto 0 | |||
| @@ -75,6 +76,9 @@ On Error Goto 0 | |||
| Response.Status = statusLine | |||
| Response.ContentType = contentType | |||
| If Len(allowHeader) > 0 Then | |||
| Response.AddHeader "Allow", allowHeader | |||
| End If | |||
| Response.Write body | |||
| Set app = Nothing | |||
| @@ -16,6 +16,14 @@ Function NewContext(path, httpMethod) | |||
| Set NewContext = ctx | |||
| End Function | |||
| Sub RunApplication(app, path, httpMethod, applicationName, statusLine, contentType, body, allowHeader) | |||
| Dim ctx | |||
| Set ctx = NewContext(path, httpMethod) | |||
| statusLine = "" : contentType = "" : body = "" : allowHeader = "" | |||
| app.Run ctx, applicationName, statusLine, contentType, body, allowHeader | |||
| Set ctx = Nothing | |||
| End Sub | |||
| Sub CheckEqual(actual, expected, label) | |||
| If actual = expected Then | |||
| WScript.Echo "PASS: " & label | |||
| @@ -66,7 +74,7 @@ If pass Then | |||
| End If | |||
| ' --- Application.Run happy path via ctx --- | |||
| Dim app, statusLine, contentType, body | |||
| Dim app, statusLine, contentType, body, allowHeader | |||
| If pass Then | |||
| On Error Resume Next | |||
| Set app = CreateObject("WscMvc.Application") | |||
| @@ -79,9 +87,8 @@ If pass Then | |||
| End If | |||
| If pass Then | |||
| statusLine = "" : contentType = "" : body = "" | |||
| On Error Resume Next | |||
| app.Run ctx1, "production", statusLine, contentType, body | |||
| RunApplication app, "/hello", "GET", "production", statusLine, contentType, body, allowHeader | |||
| If Err.Number <> 0 Then | |||
| WScript.Echo "FAIL: Application.Run raised error on /hello - " & Err.Description | |||
| pass = False | |||
| @@ -94,13 +101,13 @@ If pass Then | |||
| CheckEqual statusLine, "200 OK", "Application.Run(/hello) statusLine" | |||
| CheckEqual contentType, "text/html; charset=utf-8", "Application.Run(/hello) contentType" | |||
| CheckEqual body, "Hello from WSC-MVC!", "Application.Run(/hello) body" | |||
| CheckEqual allowHeader, "", "Application.Run(/hello) Allow header" | |||
| End If | |||
| ' --- Application.Run unknown route via ctx (expected 404, not an error) --- | |||
| If pass Then | |||
| statusLine = "" : contentType = "" : body = "" | |||
| On Error Resume Next | |||
| app.Run ctx2, "production", statusLine, contentType, body | |||
| RunApplication app, "/does-not-exist", "GET", "production", statusLine, contentType, body, allowHeader | |||
| If Err.Number <> 0 Then | |||
| WScript.Echo "FAIL: Application.Run raised error on unknown route - " & Err.Description | |||
| pass = False | |||
| @@ -111,6 +118,63 @@ End If | |||
| If pass Then | |||
| CheckEqual statusLine, "404 Not Found", "Application.Run(unknown route) statusLine" | |||
| CheckEqual allowHeader, "", "Application.Run(unknown route) Allow header" | |||
| End If | |||
| ' --- Router method contract: existing route with unsupported method is 405 + Allow --- | |||
| If pass Then | |||
| On Error Resume Next | |||
| RunApplication app, "/hello", "POST", "production", statusLine, contentType, body, allowHeader | |||
| If Err.Number <> 0 Then | |||
| WScript.Echo "FAIL: Application.Run raised error on method mismatch - " & Err.Description | |||
| pass = False | |||
| Err.Clear | |||
| End If | |||
| On Error Goto 0 | |||
| End If | |||
| If pass Then | |||
| CheckEqual statusLine, "405 Method Not Allowed", "Application.Run(POST /hello) statusLine" | |||
| CheckEqual contentType, "text/plain; charset=utf-8", "Application.Run(POST /hello) contentType" | |||
| CheckEqual body, "Method Not Allowed", "Application.Run(POST /hello) body" | |||
| CheckEqual allowHeader, "GET", "Application.Run(POST /hello) Allow header" | |||
| End If | |||
| ' --- Router app isolation and POST support for test app route --- | |||
| If pass Then | |||
| On Error Resume Next | |||
| RunApplication app, "/self-test", "POST", "tests", statusLine, contentType, body, allowHeader | |||
| If Err.Number <> 0 Then | |||
| WScript.Echo "FAIL: Application.Run raised error on POST /self-test - " & Err.Description | |||
| pass = False | |||
| Err.Clear | |||
| End If | |||
| On Error Goto 0 | |||
| End If | |||
| If pass Then | |||
| CheckEqual statusLine, "200 OK", "Application.Run(POST /self-test tests) statusLine" | |||
| CheckEqual contentType, "application/json; charset=utf-8", "Application.Run(POST /self-test tests) contentType" | |||
| CheckEqual allowHeader, "", "Application.Run(POST /self-test tests) Allow header" | |||
| End If | |||
| ' --- Router malformed path contract --- | |||
| If pass Then | |||
| On Error Resume Next | |||
| RunApplication app, "/hello/../secret", "GET", "production", statusLine, contentType, body, allowHeader | |||
| If Err.Number <> 0 Then | |||
| WScript.Echo "FAIL: Application.Run raised error on malformed path - " & Err.Description | |||
| pass = False | |||
| Err.Clear | |||
| End If | |||
| On Error Goto 0 | |||
| End If | |||
| If pass Then | |||
| CheckEqual statusLine, "400 Bad Request", "Application.Run(malformed path) statusLine" | |||
| CheckEqual contentType, "text/plain; charset=utf-8", "Application.Run(malformed path) contentType" | |||
| CheckEqual body, "Bad Request", "Application.Run(malformed path) body" | |||
| CheckEqual allowHeader, "", "Application.Run(malformed path) Allow header" | |||
| End If | |||
| ' --- Logging: best-effort log file was written with both outcomes --- | |||
| @@ -26,6 +26,37 @@ try { | |||
| Report $false "GET /hello request" $_.Exception.Message | |||
| } | |||
| try { | |||
| $postHelloResp = Invoke-WebRequest -Uri "$BaseUrl/hello" -Method Post -UseBasicParsing | |||
| Report $false "POST /hello returns 405" "got $($postHelloResp.StatusCode)" | |||
| } catch [System.Net.WebException] { | |||
| $webResp = $_.Exception.Response | |||
| if ($webResp) { | |||
| $code = [int]$webResp.StatusCode | |||
| Report ($code -eq 405) "POST /hello returns 405" "got $code" | |||
| Report ($webResp.Headers['Allow'] -eq 'GET') "POST /hello Allow header" "got '$($webResp.Headers['Allow'])'" | |||
| } else { | |||
| Report $false "POST /hello returns 405" $_.Exception.Message | |||
| } | |||
| } catch { | |||
| Report $false "POST /hello returns 405" $_.Exception.Message | |||
| } | |||
| try { | |||
| $badPathResp = Invoke-WebRequest -Uri "$BaseUrl/Default.asp?route=/hello/../secret" -UseBasicParsing | |||
| Report $false "malformed route returns 400" "got $($badPathResp.StatusCode)" | |||
| } catch [System.Net.WebException] { | |||
| $webResp = $_.Exception.Response | |||
| if ($webResp) { | |||
| $code = [int]$webResp.StatusCode | |||
| Report ($code -eq 400) "malformed route returns 400" "got $code" | |||
| } else { | |||
| Report $false "malformed route returns 400" $_.Exception.Message | |||
| } | |||
| } catch { | |||
| Report $false "malformed route returns 400" $_.Exception.Message | |||
| } | |||
| try { | |||
| $rootResp = Invoke-WebRequest -Uri "$BaseUrl/" -UseBasicParsing | |||
| Report ($rootResp.StatusCode -eq 200) "GET / status 200" "got $($rootResp.StatusCode)" | |||
| @@ -86,6 +117,15 @@ if ($TestBaseUrl) { | |||
| } catch { | |||
| Report $false "test app cannot cross into production routes" $_.Exception.Message | |||
| } | |||
| try { | |||
| $postSelfTestResp = Invoke-WebRequest -Uri "$TestBaseUrl/self-test" -Method Post -UseBasicParsing | |||
| Report ($postSelfTestResp.StatusCode -eq 200) "POST /self-test on test app status 200" "got $($postSelfTestResp.StatusCode)" | |||
| Report ($postSelfTestResp.Headers['Content-Type'] -eq 'application/json; charset=utf-8') "POST /self-test content-type" "got $($postSelfTestResp.Headers['Content-Type'])" | |||
| } catch { | |||
| Report $false "POST /self-test on test app" $_.Exception.Message | |||
| } | |||
| } | |||
| # Framework/ is a sibling of the "public" webroot, not a rule-denied | |||
| @@ -3,10 +3,10 @@ | |||
| # no cscript, no SSH access to the VM required. Runnable from any CLI that | |||
| # can reach the site over HTTP. | |||
| # | |||
| # Usage: ./run-self-test.sh http://100.127.62.31:8090 | |||
| # Usage: ./run-self-test.sh http://100.127.62.31:8091 | |||
| set -euo pipefail | |||
| BASE_URL="${1:?Usage: run-self-test.sh <base-url>, e.g. http://100.127.62.31:8090}" | |||
| BASE_URL="${1:?Usage: run-self-test.sh <base-url>, e.g. http://100.127.62.31:8091}" | |||
| response=$(curl -sf "$BASE_URL/self-test") | |||
| @@ -13,6 +13,7 @@ $components = @( | |||
| # is test-app-specific and lives under test-app/ instead. See | |||
| # docs/ARCHITECTURE.md. | |||
| (Join-Path $ProjectRoot 'Framework\RequestContext.wsc'), | |||
| (Join-Path $ProjectRoot 'Framework\Router.wsc'), | |||
| (Join-Path $ProjectRoot 'Framework\Application.wsc'), | |||
| (Join-Path $ProjectRoot 'Controllers\HomeController.wsc'), | |||
| (Join-Path $ProjectRoot 'test-app\Controllers\SelfTestController.wsc') | |||
| @@ -18,6 +18,7 @@ $components = @( | |||
| @{ 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\Router.wsc'); ProgId = 'WscMvc.Router'; ClassId = '{C92F9338-B478-4EAD-B865-892FFB1E1C51}' }, | |||
| @{ Path = (Join-Path $ProjectRoot 'Framework\RequestContext.wsc'); ProgId = 'WscMvc.RequestContext'; ClassId = '{1C36FA55-34DF-4974-94B9-D657389362B2}' } | |||
| ) | |||
Powered by TurnKey Linux.