소스 검색

Implement M5: Data (ADODB contract, real SQL Server integration)

Adds Framework/Database.wsc (WscMvc.Database): explicit, request-scoped
ADODB connection/transaction/command lifecycle, parameterized commands
via ADO positional placeholders (never string-concatenated SQL), and
Null-safe value handling. Proven via tests/Test-Components.vbs against a
real database, including a single-quote-containing value round-tripped
through a parameterized INSERT/SELECT to demonstrate no injection risk.

LocalDB was tried first (zero-install) but failed under the real IIS
ApplicationPoolIdentity ("SQL Server Network Interfaces" - LocalDB isn't
supported for IIS-hosted use per Microsoft's own guidance, confirmed
experimentally via a dedicated SelfTestController check, not assumed).
Pivoted to a real network-reachable SQL Server with SQL auth, which has
no dependency on the caller's Windows identity and was verified to work
under the real app-pool identity via the same check.

The live connection string lives only in an untracked db.connectionstring
file (.gitignore'd before it was ever written to disk) and is threaded
through Application.Run exactly like the existing logDir/viewsDir
pattern - Database.wsc itself never touches the filesystem or ASP
intrinsics. No new production route/view was added, per SPEC's
"database-backed example" non-goal - proven the same way M1-M4 proved
their components, via direct component tests plus one diagnostic-only
self-test check.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
master
부모
커밋
bb473a6c0c
14개의 변경된 파일과 766개의 추가작업 그리고 18개의 파일을 삭제
  1. +9
    -0
      .gitignore
  2. +5
    -4
      Framework/Application.wsc
  3. +253
    -0
      Framework/Database.wsc
  4. +3
    -3
      IMPLEMENTATION_PLAN.md
  5. +15
    -4
      docs/ARCHITECTURE.md
  6. +14
    -0
      docs/DECISIONS.md
  7. +66
    -0
      docs/TEST-RESULTS.md
  8. +17
    -2
      public/Default.asp
  9. +76
    -2
      test-app/Controllers/SelfTestController.wsc
  10. +21
    -2
      test-app/public/Default.asp
  11. +231
    -1
      tests/Test-Components.vbs
  12. +1
    -0
      tools/Register-Components.ps1
  13. +54
    -0
      tools/Setup-TestDatabase.ps1
  14. +1
    -0
      tools/Unregister-Components.ps1

+ 9
- 0
.gitignore 파일 보기

@@ -1 +1,10 @@
logs/

# Live credentials - never committed. See docs/DECISIONS.md for the M5
# database connection contract; the actual secret lives only in these
# untracked files (and, on the deployment host, wherever it is provisioned
# from there - never hardcoded into a .wsc/.asp/test file).
db
db.local
db.connectionstring
*.secrets

+ 5
- 4
Framework/Application.wsc 파일 보기

@@ -13,6 +13,7 @@
<parameter name="ctx"/>
<parameter name="applicationName"/>
<parameter name="viewsDir"/>
<parameter name="dbConnectionString"/>
<parameter name="statusLine"/>
<parameter name="contentType"/>
<parameter name="body"/>
@@ -28,7 +29,7 @@ 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, viewsDir, statusLine, contentType, body, allowHeader)
Sub Run(ctx, applicationName, viewsDir, dbConnectionString, statusLine, contentType, body, allowHeader)
Dim router, handlerKey

statusLine = ""
@@ -72,7 +73,7 @@ Sub Run(ctx, applicationName, viewsDir, statusLine, contentType, body, allowHead
Case "Home.Hello"
RunHomeHello ctx, viewsDir, statusLine, contentType, body, allowHeader
Case "SelfTest.RunSelfTest"
RunSelfTest ctx, statusLine, contentType, body, allowHeader
RunSelfTest ctx, dbConnectionString, statusLine, contentType, body, allowHeader
Case Else
InternalServerError statusLine, contentType, body, allowHeader
End Select
@@ -133,7 +134,7 @@ Sub RunHomeHello(ctx, viewsDir, statusLine, contentType, body, allowHeader)
allowHeader = ""
End Sub

Sub RunSelfTest(ctx, statusLine, contentType, body, allowHeader)
Sub RunSelfTest(ctx, dbConnectionString, statusLine, contentType, body, allowHeader)
Dim ctrl, selfTestBody

Set ctrl = Nothing
@@ -149,7 +150,7 @@ Sub RunSelfTest(ctx, statusLine, contentType, body, allowHeader)

selfTestBody = ""
On Error Resume Next
ctrl.RunSelfTest ctx.LogDir, selfTestBody
ctrl.RunSelfTest ctx.LogDir, dbConnectionString, selfTestBody
If Err.Number <> 0 Then
Err.Clear
On Error Goto 0


+ 253
- 0
Framework/Database.wsc 파일 보기

@@ -0,0 +1,253 @@
<?xml version="1.0"?>
<?component error="true" debug="false"?>
<component>
<registration
description="WscMvc Database"
progid="WscMvc.Database"
version="1.00"
classid="{E3F7C1A2-9B4D-4E6F-8C3A-2D5B7A9E1F04}">
</registration>

<public>
<method name="Open">
<parameter name="connectionString"/>
</method>
<method name="Close"/>
<method name="BeginTransaction"/>
<method name="CommitTransaction"/>
<method name="RollbackTransaction"/>
<method name="ExecuteNonQuery">
<parameter name="sql"/>
<parameter name="paramOrder"/>
<parameter name="paramsDict"/>
<parameter name="rowsAffected"/>
</method>
<method name="ExecuteScalar">
<parameter name="sql"/>
<parameter name="paramOrder"/>
<parameter name="paramsDict"/>
<parameter name="result"/>
</method>
</public>

<script language="VBScript">
<![CDATA[
Option Explicit

' Explicit, request-scoped ADODB connection/transaction/command lifecycle -
' never cached in Session/Application (SPEC SS10). Never references ASP
' intrinsics. sql uses ADO's positional "?" placeholders; paramOrder is a
' plain comma-separated string (not an array) giving the placeholder order,
' deliberately avoiding an untested VBScript-array-through-WSC-<public>
' marshaling question for zero real benefit - paramsDict (a Scripting.
' Dictionary, the same object-parameter pattern already proven by
' ViewRenderer.wsc) supplies each named value. Parameter type/size are
' inferred from VBScript VarType()/Len() - a deliberate minimal v0.1 policy,
' not a general ORM type system (SPEC SS3 non-goal).

' ADODB type/property constants (adovbs.inc is not included in a WSC script
' block; these are ADODB's own stable, documented numeric values).
Const adVarWChar = 202
Const adInteger = 3
Const adBigInt = 20
Const adDouble = 5
Const adDate = 7
Const adBoolean = 11
Const adParamInput = 1

Dim m_conn, m_isOpen

m_isOpen = False

Sub Open(connectionString)
Set m_conn = Nothing
On Error Resume Next
Set m_conn = CreateObject("ADODB.Connection")
If Err.Number = 0 Then m_conn.Open connectionString
If Err.Number <> 0 Then
Err.Clear
On Error Goto 0
Set m_conn = Nothing
Err.Raise vbObjectError + 101, "WscMvc.Database", "Unable to open database connection"
End If
On Error Goto 0
m_isOpen = True
End Sub

' Idempotent: safe to call even if Open was never called or Close already ran.
Sub Close()
If m_isOpen And Not m_conn Is Nothing Then
On Error Resume Next
m_conn.Close
Err.Clear
On Error Goto 0
End If
Set m_conn = Nothing
m_isOpen = False
End Sub

Sub BeginTransaction()
RequireOpen
On Error Resume Next
m_conn.BeginTrans
If Err.Number <> 0 Then
Err.Clear
On Error Goto 0
Err.Raise vbObjectError + 102, "WscMvc.Database", "Unable to begin transaction"
End If
On Error Goto 0
End Sub

Sub CommitTransaction()
RequireOpen
On Error Resume Next
m_conn.CommitTrans
If Err.Number <> 0 Then
Err.Clear
On Error Goto 0
Err.Raise vbObjectError + 103, "WscMvc.Database", "Unable to commit transaction"
End If
On Error Goto 0
End Sub

Sub RollbackTransaction()
RequireOpen
On Error Resume Next
m_conn.RollbackTrans
If Err.Number <> 0 Then
Err.Clear
On Error Goto 0
Err.Raise vbObjectError + 104, "WscMvc.Database", "Unable to roll back transaction"
End If
On Error Goto 0
End Sub

Sub ExecuteNonQuery(sql, paramOrder, paramsDict, rowsAffected)
Dim cmd, affected
RequireOpen

Set cmd = Nothing
On Error Resume Next
Set cmd = BuildCommand(sql, paramOrder, paramsDict)
If Err.Number <> 0 Then
Err.Clear
On Error Goto 0
Set cmd = Nothing
Err.Raise vbObjectError + 105, "WscMvc.Database", "Unable to prepare command"
End If
On Error Goto 0

On Error Resume Next
cmd.Execute affected
If Err.Number <> 0 Then
Err.Clear
On Error Goto 0
Set cmd = Nothing
Err.Raise vbObjectError + 106, "WscMvc.Database", "Command execution failed"
End If
On Error Goto 0

rowsAffected = affected
Set cmd = Nothing
End Sub

Sub ExecuteScalar(sql, paramOrder, paramsDict, result)
Dim cmd, rs
RequireOpen

Set cmd = Nothing
On Error Resume Next
Set cmd = BuildCommand(sql, paramOrder, paramsDict)
If Err.Number <> 0 Then
Err.Clear
On Error Goto 0
Set cmd = Nothing
Err.Raise vbObjectError + 105, "WscMvc.Database", "Unable to prepare command"
End If
On Error Goto 0

Set rs = Nothing
On Error Resume Next
Set rs = cmd.Execute()
If Err.Number <> 0 Then
Err.Clear
On Error Goto 0
Set rs = Nothing
Set cmd = Nothing
Err.Raise vbObjectError + 106, "WscMvc.Database", "Command execution failed"
End If
On Error Goto 0

If rs.EOF Then
result = Null
Else
result = rs.Fields(0).Value
End If

On Error Resume Next
rs.Close
Err.Clear
On Error Goto 0
Set rs = Nothing
Set cmd = Nothing
End Sub

Sub RequireOpen()
If Not m_isOpen Or m_conn Is Nothing Then
Err.Raise vbObjectError + 100, "WscMvc.Database", "Database is not open"
End If
End Sub

Function BuildCommand(sql, paramOrder, paramsDict)
Dim cmd, names, i, key, value

Set cmd = CreateObject("ADODB.Command")
cmd.ActiveConnection = m_conn
cmd.CommandText = sql
cmd.CommandType = 1 ' adCmdText

If Len(Trim(paramOrder)) > 0 Then
names = Split(paramOrder, ",")
For i = 0 To UBound(names)
key = Trim(names(i))
If paramsDict Is Nothing Or Not paramsDict.Exists(key) Then
Err.Raise vbObjectError + 107, "WscMvc.Database", "Missing parameter value: " & key
End If
value = paramsDict.Item(key)
cmd.Parameters.Append BuildParameter(cmd, key, value)
Next
End If

Set BuildCommand = cmd
End Function

Function BuildParameter(cmd, name, value)
Dim p

If IsNull(value) Or IsEmpty(value) Then
Set p = cmd.CreateParameter(name, adVarWChar, adParamInput, 1)
p.Value = Null
Else
Select Case VarType(value)
Case 2, 3 ' vbInteger, vbLong
Set p = cmd.CreateParameter(name, adInteger, adParamInput, 0, CLng(value))
Case 5, 4, 6 ' vbDouble, vbSingle, vbCurrency
Set p = cmd.CreateParameter(name, adDouble, adParamInput, 0, CDbl(value))
Case 7 ' vbDate
Set p = cmd.CreateParameter(name, adDate, adParamInput, 0, CDate(value))
Case 11 ' vbBoolean
Set p = cmd.CreateParameter(name, adBoolean, adParamInput, 0, CBool(value))
Case Else
Dim s, sz
s = CStr(value)
sz = Len(s)
If sz = 0 Then sz = 1
Set p = cmd.CreateParameter(name, adVarWChar, adParamInput, sz, s)
End Select
End If

Set BuildParameter = p
End Function
]]>
</script>
</component>

+ 3
- 3
IMPLEMENTATION_PLAN.md 파일 보기

@@ -37,9 +37,9 @@ Gate: route matrix and malformed URL tests pass. **MET** — see `docs/TEST-RESU
Gate: output is deterministic and untrusted text does not become HTML. **MET** for the vertical slice shipped — see `docs/TEST-RESULTS.md`. `Views/Layout.html` (a layout convention) is not yet added since there is still only one view; add it when a second view needs shared chrome, not speculatively.

## M5 — Data
- [ ] ADODB contract and provider-specific integration test database.
- [ ] Parameterized operations, transaction ownership, cleanup on error.
Gate: real DB integration tests pass and input is not concatenated into SQL values.
- [x] ADODB contract and provider-specific integration test database.
- [x] Parameterized operations, transaction ownership, cleanup on error.
Gate: real DB integration tests pass and input is not concatenated into SQL values. **MET** — see `docs/TEST-RESULTS.md`. Includes one real defect found and fixed during testing (LocalDB is unreachable from the IIS `ApplicationPoolIdentity` worker process; pivoted to a network-reachable SQL Server with SQL auth) and one caught in the diagnostic code itself (a chained `On Error Resume Next` masked the real failure reason). Identifier allowlisting (SPEC SS9) is not yet exercised — no route takes a table/column name from input in this vertical slice; deferred until one does, not implemented speculatively.

## M6 — Hardening
- [ ] Authentication/authorization design and security regression tests.


+ 15
- 4
docs/ARCHITECTURE.md 파일 보기

@@ -1,4 +1,4 @@
# WSC-MVC — Architecture (as implemented through M3)
# WSC-MVC — Architecture (as implemented through M5)

## Request flow

@@ -8,7 +8,7 @@ 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, allowHeader) [Framework/Application.wsc]
-> Application.Run(ctx, "production", viewsDir, dbConnectionString, statusLine, contentType, body, allowHeader) [Framework/Application.wsc]
-> CreateObject("WscMvc.Router")
-> Router.Match(ctx, "production", ..., handlerKey) [Framework/Router.wsc]
-> CreateObject("WscMvc.HomeController")
@@ -63,10 +63,11 @@ curl http://100.127.62.31:8091/self-test
| `Framework/Router.wsc` | `WscMvc.Router` | `{C92F9338-B478-4EAD-B865-892FFB1E1C51}` |
| `Framework/Application.wsc` | `WscMvc.Application` | `{851C7763-1638-42FE-A166-BF3DD3A96A88}` |
| `Framework/ViewRenderer.wsc` | `WscMvc.ViewRenderer` | `{4948DF84-5DC6-448A-9F1B-EB596C28842B}` |
| `Framework/Database.wsc` | `WscMvc.Database` | `{E3F7C1A2-9B4D-4E6F-8C3A-2D5B7A9E1F04}` |
| `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 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.
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; M4 added `viewsDir`; M5 added `dbConnectionString`) 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)

@@ -98,6 +99,16 @@ Substitution is a single deterministic left-to-right scan; template content is n

`Views/Home.html` is exactly `{{Message}}`; `HomeController` supplies `message = "Hello from WSC-MVC!"`, a string with no HTML-special characters, so `/hello`'s response body is byte-identical to the pre-M4 (M1-era) contract — the rendering pipeline was proven end-to-end without changing observable behavior. `Views/Layout.html` and a layout convention are deferred until a second view actually needs shared chrome (IMPLEMENTATION_PLAN M4 explicitly sequences "add layout after renderer works").

## Data (M5)

`Framework/Database.wsc` (`WscMvc.Database`) owns an explicit, request-scoped ADODB connection/transaction/command lifecycle — never cached in Session/Application (SPEC §10), never referencing ASP intrinsics. `Open`/`Close` are idempotent; `BeginTransaction`/`CommitTransaction`/`RollbackTransaction` give the caller explicit transaction ownership. `ExecuteNonQuery(sql, paramOrder, paramsDict, rowsAffected)` and `ExecuteScalar(sql, paramOrder, paramsDict, result)` use ADO's positional `?` placeholders: `paramOrder` is a plain comma-separated *string* naming the placeholder order (not an array — deliberately avoiding an untested VBScript-array-through-WSC-`<public>` marshaling question), and `paramsDict` is a `Scripting.Dictionary` of name→value, the same object-parameter pattern already proven by `ViewRenderer.wsc`. ADODB parameter type/size are inferred from VBScript `VarType()`/`Len()` — a deliberate minimal v0.1 policy (not a general ORM type system, SPEC §3 non-goal). `Null` values pass through as a real SQL `NULL`. Every failure path (`Open`, command prep, execution) releases any `ADODB.Command`/`Recordset` it created and raises a generic, connection-string-free error — `Database.wsc` never leaks credentials or internals, matching SPEC §10.

**Connection string wiring**: the live connection string (with real credentials) lives only in an untracked `db.connectionstring` file at the project root — `.gitignore`d, never hardcoded into any tracked `.wsc`/`.asp`/test file. Each `Default.asp` reads it via `Server.MapPath` exactly like `logDir`/`viewsDir` (production: `"../db.connectionstring"`; test-app: `"../../db.connectionstring"`, since `test-app/public/` sits one directory level deeper than production's `public/`) and threads it through `Application.Run`'s `dbConnectionString` parameter. `tools/Setup-TestDatabase.ps1` is the explicit, manually-run deployment step that creates the test database/schema (idempotent, same pattern as `tools/Setup-Site.ps1`) — never invoked from application code, per SPEC §10's "no broad filesystem/DB write privileges from a web request."

**No new production route/view was added for this milestone.** SPEC §3 lists "database-backed example" as a v0.1 non-goal; M5 is proven the same way M1–M4 proved their components — direct WSH tests (`tests/Test-Components.vbs`) plus one diagnostic-only check added to the existing test-app `SelfTestController` (`database_connectivity_under_app_pool_identity`), which exists specifically to verify DB connectivity under the real IIS `ApplicationPoolIdentity` worker process, not just an interactive dev session. See `docs/DECISIONS.md` for why the first engine choice (LocalDB) failed that exact check and was replaced with a network-reachable SQL Server using SQL authentication (which has no dependency on the caller's Windows identity).

No route currently selects a table/column name from user input, so SPEC §9's identifier-allowlist requirement isn't yet exercised — deferred until a route actually needs it, not implemented speculatively.

## Deferred to later milestones (do not implement early)

Per SPEC §3 non-goals and IMPLEMENTATION_PLAN M5+: 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`. A layout convention (`Views/Layout.html`) and non-text-context encoding are deferred until a concrete view needs them — see the Views section above.
Per SPEC §3 non-goals and IMPLEMENTATION_PLAN M6+: auth, a least-privileged (non-`sa`) SQL login for the M5 database (see `docs/DECISIONS.md`), identifier allowlisting (no route needs it yet). 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`. A layout convention (`Views/Layout.html`) and non-text-context encoding are deferred until a concrete view needs them — see the Views section above.

+ 14
- 0
docs/DECISIONS.md 파일 보기

@@ -127,3 +127,17 @@ Added `Framework/ViewRenderer.wsc` (`WscMvc.ViewRenderer`) and `Views/Home.html`
No experimental surprises this milestone (unlike M0-M3, which each surfaced a real WSC/VBScript defect) — returning a newly-created object through a `Sub`'s `ByRef` out-parameter (`ctrl.Hello viewName, message`, both scalars in this case, but the same mechanism was also exercised for `Set data = CreateObject(...)` inside `RunHomeHello`) behaved exactly as ordinary VBScript `ByRef` semantics predict, and `Err.Raise` inside a WSC method, caught by the caller's existing `On Error Resume Next` + `Err.Number` pattern, worked identically to the naturally-thrown errors (`CreateObject` failures) already relied on elsewhere. Confirmed via the WSH suite's dedicated `ViewRenderer` contract tests (encoding, missing template, unresolved placeholder, invalid view name) before trusting it in `Application.wsc`.

Confirmed `Views/` is unreachable over HTTP purely from being a sibling of `public/` (same guarantee already used for `Framework/`/`Controllers/`) — no new `web.config` rule was needed: `GET /Views/Home.html` → `404` on both sites.

## M5 — Data: ADODB contract, and why LocalDB was abandoned for a real SQL Server (2026-09-19)

**"database-backed example" in SPEC SS3 vs. M5's own gate**: SPEC SS3's non-goals list includes "database-backed example," which reads at first glance like it contradicts M5's gate ("real DB integration tests pass"). Read against SS9 (a full ADODB contract described as a real, later-phase deliverable), the non-goal is interpreted as "no new demo feature/route/page built to show the DB off" — not "don't implement the ADODB contract." M5 was implemented and proven the same way M1-M4 proved their components: direct WSH component tests (`tests/Test-Components.vbs`) plus one check added to the existing test-app `SelfTestController` (already a diagnostics-only surface, not a production feature) — no new production controller, route, or view was added. Flagged to Daniel before implementing; no objection raised.

**Engine choice, attempt 1 (LocalDB) - failed for a real, evidenced reason, not a guess.** Daniel initially approved "SQL Server Express / LocalDB." LocalDB was tried first (already installed on this host, zero additional install) and its ADODB contract fully passed under the interactive dev account (`cscript`), via `Driver={ODBC Driver 17 for SQL Server}` - `MSOLEDBSQL` is not installed on this host and legacy `SQLOLEDB` cannot locate the LocalDB named-pipe instance at all, so ODBC Driver 17 is the only working provider found. However, SPEC SS10's "validate application pool identity... where applicable" discipline requires proving this under the *real* IIS worker identity, not just an interactive session - AGENTS.md is explicit that WSC/COM/host assumptions must be verified, not assumed. A dedicated `database_connectivity_under_app_pool_identity` check was added to `SelfTestController` (test-app's `WscMvcTests` site, `ApplicationPoolIdentity`, confirmed via `Get-WebConfigurationProperty` - no custom identity configured) and hit over real HTTP. It failed: `SQL Server Network Interfaces: ` (ADODB error -2147467259). This matches Microsoft's own documented guidance that LocalDB is a per-user, interactive-profile-dependent instance not supported for IIS-hosted use - now confirmed experimentally on this exact host/identity, not inferred from documentation alone.

**Pivot to a real SQL Server, provided by Daniel mid-session.** Daniel supplied connection details (host/port/`sa` credentials) for an existing SQL Server 2022 instance reachable over the tailnet (`100.120.114.48:1433`). SQL authentication over plain TCP has no dependency on the calling process's Windows identity at all (unlike LocalDB's `Trusted_Connection`/named-pipe model), so this sidesteps the `ApplicationPoolIdentity` problem entirely rather than working around it. Verified: TCP reachability (`Test-NetConnection` succeeded), then an ADODB connection with SQL auth (interactive session, then - critically - re-verified via the same `SelfTestController` HTTP check under the real app-pool identity, which now reports `"pass":true`). `tools/Setup-TestDatabase.ps1` was generalized from a LocalDB-only script to accept `-ServerInstance`/`-SqlLogin`/`-SqlPassword` (the last a `SecureString`, converted to plain text only in the narrow window `sqlcmd.exe` - an external process with no SecureString-aware API - actually needs it as a CLI argument; flagged by the IDE's own PSScriptAnalyzer and fixed before use, not after). It remains idempotent (same create-if-missing pattern as `Setup-Site.ps1`) and is never invoked from application code - only as an explicit, manually-run deployment step, per SPEC SS10.

**Credential handling.** The live password reached this session as a plaintext note (`db`, opened by Daniel in the IDE) and is never embedded in any tracked file. `.gitignore` now excludes `db`, `db.connectionstring`, `db.local`, and `*.secrets`, added and verified (`git check-ignore -v`) *before* the actual connection string file was written to disk - not after. `db.connectionstring` (project root, untracked) holds the one live ADO connection string; `Default.asp` (both sites) reads it via `Server.MapPath` exactly like `logDir`/`viewsDir` and threads it through `Application.Run`'s new `dbConnectionString` parameter - `Framework/Database.wsc` itself never touches the filesystem or ASP intrinsics for this. Test-app's `public/` is two directory levels below the project root (`root -> test-app -> public`, vs. production's one level, `root -> public`), so its `Default.asp` needed `"../../db.connectionstring"`, not `"../db.connectionstring"` - caught by checking the actual path depth rather than copy-pasting production's relative path. `tests/Test-Components.vbs` reads the same file directly and skips (NOT RUN, not FAIL) its entire Database contract section when the file is absent, so the rest of the suite still gives a clean verdict on a machine without this secret provisioned. The `sa` login is a real production-grade credential with full admin rights on the target server; using it directly from the app is accepted for this dev/test milestone but is a real least-privilege gap - flagged here for M6 hardening (a dedicated, minimally-privileged SQL login scoped to `WscMvcTest`), not silently treated as the final state.

**A real bug caught in my own diagnostic code, not just the framework.** The first version of the `database_connectivity_under_app_pool_identity` self-test check chained `Open`/`ExecuteScalar`/`Close` under one `On Error Resume Next` block and checked `Err.Number` only once at the end - exactly the anti-pattern AGENTS.md's VBScript rules forbid ("no `On Error Resume Next` across an entire function; check `Err.Number` immediately"). Because `On Error Resume Next` lets execution continue to the next statement after an error, `ExecuteScalar`'s later "Database is not open" error silently overwrote `Open`'s real, more informative failure - masking the actual root cause (`SQL Server Network Interfaces`) behind a misleading message. Fixed by checking `Err.Number` after each call individually, matching the rest of the codebase's established discipline, before trusting the result to diagnose the LocalDB failure above.

**`Framework/Database.wsc` (`WscMvc.Database`) contract summary**: explicit `Open`/`Close` (idempotent) and `BeginTransaction`/`CommitTransaction`/`RollbackTransaction`; `ExecuteNonQuery`/`ExecuteScalar` take `sql` (ADO positional `?` placeholders), `paramOrder` (a comma-separated *string*, not an array - deliberately avoiding an untested VBScript-array-through-WSC-`<public>`-method marshaling question for zero real benefit), and `paramsDict` (a `Scripting.Dictionary`, the same object-parameter pattern already proven by `ViewRenderer.wsc` in M4). Parameter ADODB type/size are inferred from VBScript `VarType()`/`Len()` - a deliberate minimal v0.1 policy (not a general ORM type system, per SPEC SS3). `Null` values pass through as a real SQL `NULL` via `Parameter.Value = Null`, verified round-tripping correctly, not assumed. Verified via `tests/Test-Components.vbs`: a parameterized INSERT/SELECT round-trip of a value containing a literal single quote (proving no string-concatenation injection risk and no manual escaping needed), explicit commit and rollback (each checked against a `COUNT(*)` read-back, not just "no error"), and safe failure on an unopened/closed connection.

+ 66
- 0
docs/TEST-RESULTS.md 파일 보기

@@ -420,6 +420,72 @@ curl -o /dev/null -w "%{http_code}" http://localhost:8090/Views/Home.html -> 4

M4 gate status: **PASS** for the vertical slice implemented (safe separate template loading from a physically non-served directory; deterministic `{{Key}}` substitution; default HTML-text-context encoding verified against a real escaping case; missing-template and unresolved-placeholder failures verified path-free and safe; `/hello`'s existing exact-body contract verified unchanged end-to-end through IIS). `Views/Layout.html` and a layout convention are deferred (IMPLEMENTATION_PLAN M4 sequences "add layout after renderer works" as a second step) — not yet needed since there is still only one view. Attribute/URL/JS-context encoding remains explicitly out of scope until a view needs it (documented on `ViewRenderer.wsc` and in `docs/ARCHITECTURE.md`).

## M5 — Data: ADODB contract, real DB integration (2026-09-19)

Host under test: `DESKTOP-80D128R`, same local IIS sites as M3/M4. Added `Framework/Database.wsc` (`WscMvc.Database`, `{E3F7C1A2-9B4D-4E6F-8C3A-2D5B7A9E1F04}`). Full narrative (engine-choice pivot, credential handling, a real bug caught in the diagnostic code itself) is in `docs/DECISIONS.md`; this section is the command-by-command evidence.

**Attempt 1 — LocalDB (rejected with evidence).** `(localdb)\MSSQLLocalDB` was already present on this host. WSH-level contract tests (interactive account) **PASSED** via `Driver={ODBC Driver 17 for SQL Server}` (the only working provider — `MSOLEDBSQL` isn't installed, legacy `SQLOLEDB` can't find the named-pipe instance). A dedicated self-test check under the real IIS `ApplicationPoolIdentity` (`WscMvcTests` app pool, confirmed default/no custom identity via `Get-WebConfigurationProperty`) **FAILED**:

```
curl http://localhost:8091/self-test
{"...","name":"database_connectivity_under_app_pool_identity","pass":false,"detail":"Open: -2147467259 - [Microsoft][ODBC Driver 17 for SQL Server]SQL Server Network Interfaces: "}
```

This matches Microsoft's documented guidance that LocalDB isn't supported for IIS-hosted use — confirmed experimentally on this exact host/identity, not assumed from docs alone.

**Attempt 2 — real SQL Server (works).** Daniel provided credentials for an existing SQL Server 2022 instance on the tailnet (`100.120.114.48:1433`, SQL auth). Connectivity verified in stages:

```
Test-NetConnection -ComputerName 100.120.114.48 -Port 1433 -> TcpTestSucceeded: True
cscript probe-sql.vbs -> who=sa version=Microsoft SQL Server 2022 (RTM-CU27)... / OK
```

`tools\Setup-TestDatabase.ps1 -ServerInstance '100.120.114.48,1433' -SqlLogin sa -SqlPassword <SecureString>` (idempotent; run twice, second run a no-op) created database `WscMvcTest` and table `dbo.Widgets`.

WSH component contract, connection string read from the untracked `db.connectionstring` (not hardcoded):

```
cscript //nologo tests\Test-Components.vbs
```

Result: **PASS**, Database-specific lines:

```
PASS: Database.ExecuteNonQuery(INSERT) rowsAffected
PASS: Database.ExecuteScalar round-trips quote-containing value
PASS: Database.ExecuteScalar Null parameter round-trips as SQL NULL
PASS: Database.RollbackTransaction leaves no row
PASS: Database.CommitTransaction persists the row
PASS: Database.ExecuteScalar on an unopened connection raises a safe error
PASS: Database.Close is idempotent
RESULT: ALL PASS
```

The INSERT/SELECT round-trip used the value `O'Brien` (a literal single quote) specifically to prove no string-concatenation SQL injection and no manual escaping requirement — a naive `"...VALUES ('" & name & "')"` would have broken on this exact input; the parameterized call did not.

Real IIS/app-pool-identity re-check after the pivot:

```
curl http://localhost:8091/self-test
{"ok":true,"checks":[...,{"name":"database_connectivity_under_app_pool_identity","pass":true,"detail":""}]}
```

Full regression after all M5 wiring changes (both `Default.asp` files, `Application.wsc`'s `Run` signature, `SelfTestController.wsc`):

```
cscript //nologo tests\Test-Components.vbs -> ALL PASS (34 checks)
powershell -File tests\Test-Http.ps1 -BaseUrl http://localhost:8090 -TestBaseUrl http://localhost:8091 -> ALL PASS (14 checks)
curl http://localhost:8090/hello -> 200, unchanged body
curl -o /dev/null -w "%{http_code}" http://localhost:8090/db.connectionstring -> 404
curl -o /dev/null -w "%{http_code}" http://localhost:8090/db -> 404
curl -o /dev/null -w "%{http_code}" http://localhost:8091/db.connectionstring -> 404
curl -o /dev/null -w "%{http_code}" http://localhost:8091/db -> 404
```

Secret hygiene verified directly, not assumed: `git check-ignore -v db db.connectionstring` confirmed both patterns match, and `git status --short` never showed either file as trackable, from before either file was written to disk.

M5 gate status: **PASS** — real DB integration tests pass against a real, explicitly-provisioned SQL Server (not mocked, not LocalDB-only); every value crosses the SQL boundary as a bound ADO parameter, never string concatenation. Deferred to M6: a least-privileged (non-`sa`) SQL login for the test database; identifier allowlisting (no route yet selects a table/column name from input, so there's nothing to allowlist against).

## Remote deployment tooling — Windows Server 2025 live proof (2026-09-19)

Host under test: `win2025test`, Windows Server 2025 Standard build 26100, 64-bit; production `WscMvc` on `:8090` and test `WscMvcTests` on `:8091`.


+ 17
- 2
public/Default.asp 파일 보기

@@ -1,7 +1,7 @@
<%@ Language="VBScript" %>
<% Option Explicit %>
<%
Dim route, httpMethod, logDir, viewsDir, ctx, app, applicationName, statusLine, contentType, body, allowHeader
Dim route, httpMethod, logDir, viewsDir, dbConnStringPath, dbConnStringFso, dbConnectionString, ctx, app, applicationName, statusLine, contentType, body, allowHeader

route = Request.QueryString("route")
httpMethod = Request.ServerVariables("REQUEST_METHOD")
@@ -12,6 +12,21 @@ applicationName = "production"
logDir = Server.MapPath("../logs")
viewsDir = Server.MapPath("../Views")

' db.connectionstring holds the live DB credentials and is never committed
' (see .gitignore, docs/DECISIONS.md) - read as plain framework wiring here,
' same as logDir/viewsDir, never hardcoded into any tracked file. Optional:
' an empty string means no route on this app currently needs a database.
dbConnStringPath = Server.MapPath("../db.connectionstring")
dbConnectionString = ""
On Error Resume Next
Set dbConnStringFso = Server.CreateObject("Scripting.FileSystemObject")
If dbConnStringFso.FileExists(dbConnStringPath) Then
dbConnectionString = Trim(dbConnStringFso.OpenTextFile(dbConnStringPath, 1).ReadAll)
End If
Set dbConnStringFso = Nothing
Err.Clear
On Error Goto 0

Set ctx = Nothing
On Error Resume Next
Set ctx = Server.CreateObject("WscMvc.RequestContext")
@@ -58,7 +73,7 @@ body = ""
allowHeader = ""

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


+ 76
- 2
test-app/Controllers/SelfTestController.wsc 파일 보기

@@ -11,6 +11,7 @@
<public>
<method name="RunSelfTest">
<parameter name="logDir"/>
<parameter name="dbConnectionString"/>
<parameter name="body"/>
</method>
</public>
@@ -28,7 +29,7 @@ Option Explicit
' route IS the diagnostics surface, so its whole job is to say what broke.
' This is a dev/test-milestone tool, not a production data endpoint -
' revisit whether it should be gated/removed during M6 hardening.
Sub RunSelfTest(logDir, body)
Sub RunSelfTest(logDir, dbConnectionString, body)
Dim checks, allPass
checks = ""
allPass = True
@@ -87,7 +88,7 @@ Sub RunSelfTest(logDir, body)
Set ctxUnknown = CreateObject("WscMvc.RequestContext")
ctxUnknown.Initialize "/hello", "GET", logDir
unkStatus = "" : unkType = "" : unkBody = "" : unkAllow = ""
app.Run ctxUnknown, "tests", "", unkStatus, unkType, unkBody, unkAllow
app.Run ctxUnknown, "tests", "", "", unkStatus, unkType, unkBody, unkAllow
If Err.Number <> 0 Then
unkDetail = Err.Description
Err.Clear
@@ -147,6 +148,79 @@ Sub RunSelfTest(logDir, body)
checks = AppendCheck(checks, "delete_self_test_allow_header", delOk, delDetail)
allPass = allPass And delOk

' --- Database connectivity under the real IIS app-pool identity (M5).
' This is the genuinely open question SPEC SS10/SS15's "validate app pool
' identity... where applicable" discipline calls for: WSH/direct component
' tests only prove the contract under the interactive dev account. SQL
' auth over TCP (not LocalDB's Windows-integrated named pipes) doesn't
' depend on the caller's OS identity, but this is still verified for real
' here rather than assumed, exactly because this route runs inside the
' real worker process under ApplicationPoolIdentity - see
' docs/DECISIONS.md for the LocalDB attempt that failed this exact check.
' NOTE: this probes ADODB.Connection directly, bypassing WscMvc.Database,
' solely to see the real underlying driver error - Database.Open
' deliberately discards Err.Description (SPEC SS10: never leak connection
' strings/internals to a client), which is correct for the production
' error boundary but useless for diagnosing *why* connectivity fails.
' SelfTestController is the one place intentionally exempted from that
' rule (see file header comment).
Dim rawConn, dbOk, dbDetail, dbRs, dbSkipped
dbOk = False
dbDetail = ""
dbSkipped = False

If Len(Trim(dbConnectionString)) = 0 Then
' No db.connectionstring provisioned on this deployment - an
' environment-provisioning fact, not a framework defect, so this
' reports as passing-but-skipped rather than dragging down "ok"
' (same NOT-RUN-not-FAIL discipline as tests/Test-Components.vbs).
dbSkipped = True
dbOk = True
dbDetail = "skipped: db.connectionstring not provisioned on this deployment"
End If

If Not dbSkipped Then
On Error Resume Next
Set rawConn = CreateObject("ADODB.Connection")
If Err.Number <> 0 Then
dbDetail = "CreateObject: " & Err.Description
Err.Clear
End If
On Error Goto 0
End If

If Len(dbDetail) = 0 Then
On Error Resume Next
rawConn.Open dbConnectionString
If Err.Number <> 0 Then
dbDetail = "Open: " & Err.Number & " - " & Err.Description
Err.Clear
End If
On Error Goto 0
End If

If Len(dbDetail) = 0 Then
On Error Resume Next
Set dbRs = rawConn.Execute("SELECT 1")
If Err.Number <> 0 Then
dbDetail = "Execute: " & Err.Number & " - " & Err.Description
Err.Clear
Else
dbOk = True
dbRs.Close
End If
On Error Goto 0
End If

On Error Resume Next
If Not rawConn Is Nothing Then rawConn.Close
Err.Clear
On Error Goto 0

checks = AppendCheck(checks, "database_connectivity_under_app_pool_identity", dbOk, dbDetail)
allPass = allPass And dbOk
Set rawConn = Nothing

Set ctx1 = Nothing
Set ctx2 = Nothing
Set ctxUnknown = Nothing


+ 21
- 2
test-app/public/Default.asp 파일 보기

@@ -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, viewsDir, ctx, app, applicationName, statusLine, contentType, body, allowHeader
Dim route, httpMethod, logDir, viewsDir, dbConnStringPath, dbConnStringFso, dbConnectionString, ctx, app, applicationName, statusLine, contentType, body, allowHeader

route = Request.QueryString("route")
httpMethod = Request.ServerVariables("REQUEST_METHOD")
@@ -16,6 +16,25 @@ applicationName = "tests"
logDir = Server.MapPath("../logs")
viewsDir = Server.MapPath("../Views")

' db.connectionstring holds the live DB credentials and is never committed
' (see .gitignore, docs/DECISIONS.md) - read as plain framework wiring here,
' same as logDir/viewsDir, never hardcoded into any tracked file. Optional:
' an empty string means it isn't provisioned on this deployment. Unlike
' logs/ (each site has its own), there is exactly one shared credential file
' at the project root - test-app/public/ is two levels below the project
' root (root -> test-app -> public), one level deeper than production's
' public/ (root -> public), so it needs "../../" not "../" to reach it.
dbConnStringPath = Server.MapPath("../../db.connectionstring")
dbConnectionString = ""
On Error Resume Next
Set dbConnStringFso = Server.CreateObject("Scripting.FileSystemObject")
If dbConnStringFso.FileExists(dbConnStringPath) Then
dbConnectionString = Trim(dbConnStringFso.OpenTextFile(dbConnStringPath, 1).ReadAll)
End If
Set dbConnStringFso = Nothing
Err.Clear
On Error Goto 0

Set ctx = Nothing
On Error Resume Next
Set ctx = Server.CreateObject("WscMvc.RequestContext")
@@ -62,7 +81,7 @@ body = ""
allowHeader = ""

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


+ 231
- 1
tests/Test-Components.vbs 파일 보기

@@ -1,6 +1,7 @@
Option Explicit

Dim fso, logDir, viewsDir, testViewsDir, pass
Dim dbConnStringPath, dbConnString, dbSectionAvailable
pass = True

Set fso = CreateObject("Scripting.FileSystemObject")
@@ -13,6 +14,23 @@ End If
' rendering pipeline, not just the ViewRenderer component in isolation).
viewsDir = fso.GetParentFolderName(WScript.ScriptFullName) & "\..\Views"

' The connection string (with live credentials) lives only in the untracked
' db.connectionstring file (see .gitignore, docs/DECISIONS.md) - never
' hardcoded here. Read up front so every RunApplication call (including the
' non-DB ones above the Database contract section below) sees a consistent
' value. If the file isn't present on this machine, dbSectionAvailable gates
' the Database contract tests as NOT RUN rather than FAIL further down.
dbConnStringPath = fso.GetParentFolderName(WScript.ScriptFullName) & "\..\db.connectionstring"
dbSectionAvailable = fso.FileExists(dbConnStringPath)
dbConnString = ""
If dbSectionAvailable Then
Dim dbConnStringStreamEarly
Set dbConnStringStreamEarly = fso.OpenTextFile(dbConnStringPath, 1)
dbConnString = Trim(dbConnStringStreamEarly.ReadAll)
dbConnStringStreamEarly.Close
Set dbConnStringStreamEarly = Nothing
End If

' Disposable fixture directory for direct ViewRenderer contract tests below.
testViewsDir = fso.GetParentFolderName(WScript.ScriptFullName) & "\test-views"
If fso.FolderExists(testViewsDir) Then
@@ -31,7 +49,7 @@ Sub RunApplication(app, path, httpMethod, applicationName, statusLine, contentTy
Dim ctx
Set ctx = NewContext(path, httpMethod)
statusLine = "" : contentType = "" : body = "" : allowHeader = ""
app.Run ctx, applicationName, viewsDir, statusLine, contentType, body, allowHeader
app.Run ctx, applicationName, viewsDir, dbConnString, statusLine, contentType, body, allowHeader
Set ctx = Nothing
End Sub

@@ -313,6 +331,218 @@ End If

Set renderer = Nothing

' --- Database contract (M5) ---
' Targets the dedicated integration test database created by
' tools/Setup-TestDatabase.ps1 (never auto-created by application code -
' SPEC SS10: no broad filesystem/DB write privileges from a web request).
' dbConnString/dbSectionAvailable were already read at the top of this file
' (before the first RunApplication call). If db.connectionstring isn't
' present on this machine, the DB section is skipped (NOT RUN) rather than
' failed, since the real issue is an unprovisioned environment, not a
' framework defect.
Dim db
If Not dbSectionAvailable Then
WScript.Echo "NOT RUN: Database contract tests skipped - db.connectionstring not found at " & dbConnStringPath
End If

If pass And dbSectionAvailable Then
On Error Resume Next
Set db = CreateObject("WscMvc.Database")
If Err.Number <> 0 Then
WScript.Echo "FAIL: could not create WscMvc.Database - " & Err.Description
pass = False
Err.Clear
End If
On Error Goto 0
End If

If pass And dbSectionAvailable Then
On Error Resume Next
db.Open dbConnString
If Err.Number <> 0 Then
WScript.Echo "FAIL: Database.Open raised error - " & Err.Description
pass = False
Err.Clear
End If
On Error Goto 0
End If

' Clean slate so this test is deterministic across repeated runs.
If pass And dbSectionAvailable Then
Dim rowsCleared
On Error Resume Next
db.ExecuteNonQuery "DELETE FROM dbo.Widgets", "", Nothing, rowsCleared
If Err.Number <> 0 Then
WScript.Echo "FAIL: Database.ExecuteNonQuery(DELETE) raised error - " & Err.Description
pass = False
Err.Clear
End If
On Error Goto 0
End If

' --- Parameterized INSERT with a value containing a single quote: proves no
' string-concatenation SQL injection (a naive concatenation would break the
' SQL syntax or need manual quote-escaping; the parameterized call needs
' neither). ---
If pass And dbSectionAvailable Then
Dim insertParams, rowsInserted
Set insertParams = CreateObject("Scripting.Dictionary")
insertParams.Add "Name", "O'Brien"
On Error Resume Next
db.ExecuteNonQuery "INSERT INTO dbo.Widgets (Name) VALUES (?)", "Name", insertParams, rowsInserted
If Err.Number <> 0 Then
WScript.Echo "FAIL: Database.ExecuteNonQuery(INSERT) raised error - " & Err.Description
pass = False
Err.Clear
End If
On Error Goto 0
If pass Then
CheckEqual rowsInserted, 1, "Database.ExecuteNonQuery(INSERT) rowsAffected"
End If
Set insertParams = Nothing
End If

' --- Parameterized SELECT round-trips the quote-containing value intact ---
If pass And dbSectionAvailable Then
Dim selectParams, foundName
Set selectParams = CreateObject("Scripting.Dictionary")
selectParams.Add "Name", "O'Brien"
On Error Resume Next
db.ExecuteScalar "SELECT Name FROM dbo.Widgets WHERE Name = ?", "Name", selectParams, foundName
If Err.Number <> 0 Then
WScript.Echo "FAIL: Database.ExecuteScalar(SELECT) raised error - " & Err.Description
pass = False
Err.Clear
End If
On Error Goto 0
If pass Then
CheckEqual foundName, "O'Brien", "Database.ExecuteScalar round-trips quote-containing value"
End If
Set selectParams = Nothing
End If

' --- Null parameter contract: a Null value must pass through as a real SQL
' NULL, not the string "Null" or an empty string. ---
If pass And dbSectionAvailable Then
Dim nullParams, nullResult
Set nullParams = CreateObject("Scripting.Dictionary")
nullParams.Add "v", Null
On Error Resume Next
db.ExecuteScalar "SELECT ? AS v", "v", nullParams, nullResult
If Err.Number <> 0 Then
WScript.Echo "FAIL: Database.ExecuteScalar(Null param) raised error - " & Err.Description
pass = False
Err.Clear
End If
On Error Goto 0
If pass Then
If IsNull(nullResult) Then
WScript.Echo "PASS: Database.ExecuteScalar Null parameter round-trips as SQL NULL"
Else
WScript.Echo "FAIL: Database.ExecuteScalar Null parameter -> expected Null, got [" & nullResult & "]"
pass = False
End If
End If
Set nullParams = Nothing
End If

' --- Transaction contract: rollback leaves no trace ---
If pass And dbSectionAvailable Then
Dim rollbackParams, rowsRolledBack, countAfterRollback
On Error Resume Next
db.BeginTransaction
Set rollbackParams = CreateObject("Scripting.Dictionary")
rollbackParams.Add "Name", "RolledBack"
db.ExecuteNonQuery "INSERT INTO dbo.Widgets (Name) VALUES (?)", "Name", rollbackParams, rowsRolledBack
db.RollbackTransaction
If Err.Number <> 0 Then
WScript.Echo "FAIL: Database rollback sequence raised error - " & Err.Description
pass = False
Err.Clear
End If
On Error Goto 0

If pass Then
Dim countParams
Set countParams = CreateObject("Scripting.Dictionary")
countParams.Add "Name", "RolledBack"
On Error Resume Next
db.ExecuteScalar "SELECT COUNT(*) FROM dbo.Widgets WHERE Name = ?", "Name", countParams, countAfterRollback
On Error Goto 0
CheckEqual countAfterRollback, 0, "Database.RollbackTransaction leaves no row"
Set countParams = Nothing
End If
Set rollbackParams = Nothing
End If

' --- Transaction contract: commit persists the change ---
If pass And dbSectionAvailable Then
Dim commitParams, rowsCommitted, countAfterCommit
On Error Resume Next
db.BeginTransaction
Set commitParams = CreateObject("Scripting.Dictionary")
commitParams.Add "Name", "Committed"
db.ExecuteNonQuery "INSERT INTO dbo.Widgets (Name) VALUES (?)", "Name", commitParams, rowsCommitted
db.CommitTransaction
If Err.Number <> 0 Then
WScript.Echo "FAIL: Database commit sequence raised error - " & Err.Description
pass = False
Err.Clear
End If
On Error Goto 0

If pass Then
On Error Resume Next
db.ExecuteScalar "SELECT COUNT(*) FROM dbo.Widgets WHERE Name = ?", "Name", commitParams, countAfterCommit
On Error Goto 0
CheckEqual countAfterCommit, 1, "Database.CommitTransaction persists the row"
End If
Set commitParams = Nothing
End If

' --- Failure path: operating on a closed/never-opened connection is a safe error ---
If pass And dbSectionAvailable Then
Dim db2, closedOk, closedDetail, dummyResult
closedOk = False
Set db2 = CreateObject("WscMvc.Database")
On Error Resume Next
db2.ExecuteScalar "SELECT 1", "", Nothing, dummyResult
If Err.Number <> 0 Then
closedOk = True
Err.Clear
End If
On Error Goto 0
If closedOk Then
WScript.Echo "PASS: Database.ExecuteScalar on an unopened connection raises a safe error"
Else
WScript.Echo "FAIL: Database.ExecuteScalar on an unopened connection did not raise -> got [" & dummyResult & "]"
pass = False
End If
Set db2 = Nothing
End If

' --- Close is idempotent ---
If pass And dbSectionAvailable Then
Dim closeOk
closeOk = True
On Error Resume Next
db.Close
db.Close
If Err.Number <> 0 Then
closeOk = False
Err.Clear
End If
On Error Goto 0
If closeOk Then
WScript.Echo "PASS: Database.Close is idempotent"
Else
WScript.Echo "FAIL: calling Database.Close twice raised an error"
pass = False
End If
End If

Set db = Nothing

' --- Logging: best-effort log file was written with both outcomes ---
If pass Then
Dim logPath, logContent


+ 1
- 0
tools/Register-Components.ps1 파일 보기

@@ -15,6 +15,7 @@ $components = @(
(Join-Path $ProjectRoot 'Framework\RequestContext.wsc'),
(Join-Path $ProjectRoot 'Framework\Router.wsc'),
(Join-Path $ProjectRoot 'Framework\ViewRenderer.wsc'),
(Join-Path $ProjectRoot 'Framework\Database.wsc'),
(Join-Path $ProjectRoot 'Framework\Application.wsc'),
(Join-Path $ProjectRoot 'Controllers\HomeController.wsc'),
(Join-Path $ProjectRoot 'test-app\Controllers\SelfTestController.wsc')


+ 54
- 0
tools/Setup-TestDatabase.ps1 파일 보기

@@ -0,0 +1,54 @@
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)][string]$ServerInstance,
[Parameter(Mandatory = $true)][string]$SqlLogin,
[Parameter(Mandatory = $true)][SecureString]$SqlPassword,
[string]$DatabaseName = 'WscMvcTest'
)

# Idempotent: creates the M5 integration test database and its one table if
# missing, does nothing if they already exist. This is a deliberate, explicit
# deployment step (SPEC SS10/SS9: "no broad filesystem/DB write privileges";
# a web request must never create schema) - never called from Default.asp or
# any WSC, only from this tool. Credentials are supplied as parameters at
# invocation time, never embedded in this (tracked) script - see
# docs/DECISIONS.md for where the actual secret lives. SqlPassword is a
# SecureString and is only ever converted to plain text in the narrow window
# where sqlcmd.exe (an external process with no SecureString-aware API)
# actually needs it as a command-line argument.
$ErrorActionPreference = 'Stop'

$plainPassword = [Runtime.InteropServices.Marshal]::PtrToStringUni(
[Runtime.InteropServices.Marshal]::SecureStringToGlobalAllocUnicode($SqlPassword)
)

$createDbSql = @"
IF DB_ID(N'$DatabaseName') IS NULL
BEGIN
CREATE DATABASE [$DatabaseName];
END
"@

$createTableSql = @"
IF OBJECT_ID(N'dbo.Widgets', N'U') IS NULL
BEGIN
CREATE TABLE dbo.Widgets (
Id INT IDENTITY(1,1) PRIMARY KEY,
Name NVARCHAR(100) NOT NULL
);
END
"@

try {
Write-Output "Ensuring database [$DatabaseName] exists on $ServerInstance ..."
sqlcmd -S $ServerInstance -U $SqlLogin -P $plainPassword -d master -Q $createDbSql -b
if ($LASTEXITCODE -ne 0) { throw "sqlcmd failed creating database (exit $LASTEXITCODE)" }

Write-Output "Ensuring dbo.Widgets table exists ..."
sqlcmd -S $ServerInstance -U $SqlLogin -P $plainPassword -d $DatabaseName -Q $createTableSql -b
if ($LASTEXITCODE -ne 0) { throw "sqlcmd failed creating table (exit $LASTEXITCODE)" }
} finally {
$plainPassword = $null
}

Write-Output "Test database ready: [$DatabaseName] on $ServerInstance (table dbo.Widgets)."

+ 1
- 0
tools/Unregister-Components.ps1 파일 보기

@@ -19,6 +19,7 @@ $components = @(
@{ 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\ViewRenderer.wsc'); ProgId = 'WscMvc.ViewRenderer'; ClassId = '{4948DF84-5DC6-448A-9F1B-EB596C28842B}' },
@{ Path = (Join-Path $ProjectRoot 'Framework\Database.wsc'); ProgId = 'WscMvc.Database'; ClassId = '{E3F7C1A2-9B4D-4E6F-8C3A-2D5B7A9E1F04}' },
@{ 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.