code-review: 2026-05-28 baseline re-review of all 23 modules at 1eb6e97
Re-applies the full 10-category checklist to every src/ project — including
first-time reviews of the four newer components (AuditLog, NotificationOutbox,
SiteCallAudit, Transport) — so the code-reviews/ index reflects today's
codebase rather than the 2026-05-16 baseline. 172 new Open findings (0
Critical, 18 High, 62 Medium, 92 Low); 481 findings total across 23 modules.
regen-readme.py now derives each module's Last reviewed + Commit from its
findings.md header instead of hard-coding 2026-05-16 / 9c60592, so future
single-module re-reviews show their own date in the Module Status table.
This commit is contained in:
@@ -5,10 +5,10 @@
|
||||
| Module | `src/ScadaLink.CentralUI` |
|
||||
| Design doc | `docs/requirements/Component-CentralUI.md` |
|
||||
| Status | Reviewed |
|
||||
| Last reviewed | 2026-05-17 |
|
||||
| Last reviewed | 2026-05-28 |
|
||||
| Reviewer | claude-agent |
|
||||
| Commit reviewed | `39d737e` |
|
||||
| Open findings | 0 |
|
||||
| Commit reviewed | `1eb6e97` |
|
||||
| Open findings | 8 |
|
||||
|
||||
## Summary
|
||||
|
||||
@@ -73,6 +73,55 @@ cross-thread `Dictionary`; CentralUI-022 unguarded `InvokeAsync`), category 4
|
||||
claims), category 9 (CentralUI-025 untested `SessionExpiry` poll). Categories
|
||||
1, 2, 5, 6, 7, 10 produced no new findings.
|
||||
|
||||
_Re-review (2026-05-28, `1eb6e97`):_
|
||||
|
||||
| # | Category | Examined | Notes |
|
||||
|---|----------|----------|-------|
|
||||
| 1 | Correctness & logic bugs | ☑ | CentralUI-026 (AuditFilterBar UTC), CentralUI-027 (3 other pages with same UTC bug). |
|
||||
| 2 | Akka.NET conventions | ☑ | No new findings — module is presentation; `DebugStreamService` actor usage unchanged. |
|
||||
| 3 | Concurrency & thread safety | ☑ | CentralUI-030 (StringWriter capture buffer not thread-safe under intra-script `Task.WhenAll`). |
|
||||
| 4 | Error handling & resilience | ☑ | No new findings — the prior CentralUI-018/023 patterns hold. |
|
||||
| 5 | Security | ☑ | CentralUI-028 (NotificationReport + SiteCallsReport not site-scoped — CentralUI-002 regression on new pages). |
|
||||
| 6 | Performance & resource management | ☑ | CentralUI-031 (TransportImport buffers full bundle bytes in component state). |
|
||||
| 7 | Design-document adherence | ☑ | CentralUI-032 (AuditResultsGrid forward-only paging diverges from "keyset paginated" implied bi-directional). |
|
||||
| 8 | Code organization & conventions | ☑ | CentralUI-029 (`JS.InvokeAsync<int>("eval", ...)` in ConfigurationAuditLog vs the `_content/.../BrowserTime` module pattern). |
|
||||
| 9 | Testing coverage | ☑ | CentralUI-033 (TransportImport / SiteCallsReport query-string drill-in code paths untested). |
|
||||
| 10 | Documentation & comments | ☑ | No new findings — code comments accurately describe intent. |
|
||||
|
||||
#### Re-review 2026-05-28 (commit `1eb6e97`)
|
||||
|
||||
All 25 prior findings remain closed. This re-review re-examined the full
|
||||
module against the 10-category checklist with attention to the
|
||||
recently-added Transport export/import wizards (`TransportExport`,
|
||||
`TransportImport`) and the operational Audit Log page (Bundle B..G). The
|
||||
most consequential pattern in this pass is that the **CentralUI-008
|
||||
local-input-treated-as-UTC** bug, fixed for the legacy
|
||||
`AuditLog.razor` via the `BrowserTime.LocalInputToUtc` helper, has been
|
||||
silently recreated on every other page that exposes a
|
||||
`<input type="datetime-local">` filter — `AuditFilterBar` (the new
|
||||
operational Audit Log filter, CentralUI-026), `SiteCallsReport`,
|
||||
`NotificationReport`, and `EventLogs` (CentralUI-027). The Audit Log
|
||||
page CSV export URL therefore mis-shifts the From/To filter window by
|
||||
the operator's UTC offset, and the same offset bug silently corrupts
|
||||
audit-style queries on Site Calls / Notification Report / Event Logs.
|
||||
Second-most consequential is **CentralUI-028**: the new `NotificationReport`
|
||||
and `SiteCallsReport` pages (both `[Authorize(RequireDeployment)]`) do
|
||||
NOT filter their site dropdown or row data through `SiteScopeService`,
|
||||
and the relay actions (`RetryNotification`/`DiscardNotification`,
|
||||
`RetrySiteCall`/`DiscardSiteCall`) issue no server-side site-scope
|
||||
re-check before relaying to the owning site — so a site-scoped Deployment
|
||||
user can read and act on notifications and cached calls for sites
|
||||
outside their grant, replicating the original CentralUI-002 defect on
|
||||
the two pages added after the CentralUI-002 fix landed. The remaining
|
||||
new findings (CentralUI-029..CentralUI-033) cover a residual `JS.InvokeAsync<int>("eval", ...)`
|
||||
in `ConfigurationAuditLog`, a single-thread `StringWriter` capture buffer
|
||||
in the Test Run sandbox (a sandboxed script that uses `Task.WhenAll` can
|
||||
write concurrently), a `using var` `MemoryStream` followed by `ms.ToArray()`
|
||||
buffering the full bundle in memory in `TransportImport`, the
|
||||
`AuditResultsGrid` having no Previous-page control (forward-only navigation,
|
||||
a UX/design adherence gap), and the un-tested `TransportImport` /
|
||||
`SiteCallsReport` query-string drill-in code paths.
|
||||
|
||||
## Findings
|
||||
|
||||
### CentralUI-001 — Test Run sandbox executes arbitrary C# with no trust-model enforcement
|
||||
@@ -1216,3 +1265,278 @@ also forces the CentralUI-020 fix.
|
||||
**Resolution**
|
||||
|
||||
2026-05-17 — added `SessionExpiryComponentTests` (bUnit): an expired ping (401) redirects to `/login`, a live ping (200) and a transient failure (status 0) do not, and on the `/login` route the component neither pings nor redirects; also added `AuthPingEndpointTests` covering the `/auth/ping` endpoint contract.
|
||||
|
||||
### CentralUI-026 — `AuditFilterBar` From/To filters treat browser-local datetimes as UTC
|
||||
|
||||
| | |
|
||||
|--|--|
|
||||
| Severity | Medium |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Open |
|
||||
| Location | `src/ScadaLink.CentralUI/Components/Audit/AuditFilterBar.razor:97-104`; `src/ScadaLink.CentralUI/Components/Audit/AuditQueryModel.cs:56-58,150-178,203-213` |
|
||||
|
||||
**Description**
|
||||
|
||||
The new operational Audit Log filter bar binds two `<input type="datetime-local">` controls
|
||||
straight to `AuditQueryModel.CustomFromUtc` / `CustomToUtc` (`DateTime?`), and `ToFilter`
|
||||
emits those values as `AuditLogQueryFilter.FromUtc` / `ToUtc` without converting from
|
||||
the browser's local time zone. A `datetime-local` input yields the user's *browser-local*
|
||||
wall-clock value, so for any non-UTC user the audit query window is shifted by their UTC
|
||||
offset — returning the wrong rows from the central `AuditLog` table and producing a
|
||||
mis-shifted CSV export through `AuditLogPage.BuildExportUrl`, which round-trips the
|
||||
filter's `FromUtc`/`ToUtc` straight into `?from=`/`?to=` query params. This is the same
|
||||
defect CentralUI-008 fixed for the legacy `Components/Pages/Monitoring/AuditLog.razor`
|
||||
via the `BrowserTime.LocalInputToUtc(value, _browserUtcOffsetMinutes)` helper — but the
|
||||
new Audit Log v2 filter bar does not use that helper, so a Bundle B/C/D/E/F regression
|
||||
re-introduced the bug for the page-replacement target. The CLAUDE.md "all timestamps are
|
||||
UTC throughout" decision is satisfied at the wire level but violated at the input
|
||||
boundary, exactly as the original finding called out.
|
||||
|
||||
**Recommendation**
|
||||
|
||||
Fetch the browser offset once via JS interop (mirroring `ConfigurationAuditLog.OnAfterRenderAsync`
|
||||
and `AuditLog.razor`'s implementation), pipe both `CustomFromUtc` and `CustomToUtc` through
|
||||
`BrowserTime.LocalInputToUtc(value, offsetMinutes)` inside `AuditQueryModel.ToFilter`
|
||||
(or in the filter-bar Apply path before calling `ToFilter`), and add a regression test
|
||||
that pins the non-UTC behaviour (mirroring `BrowserTimeTests.LocalInputToUtc_NonUtcBrowser_DoesNotEqualNaiveRelabelling`).
|
||||
The label "Custom From / To" should also be clarified ("UTC" vs "local") in the UI.
|
||||
|
||||
### CentralUI-027 — Same UTC misinterpretation in `SiteCallsReport`, `NotificationReport`, and `EventLogs`
|
||||
|
||||
| | |
|
||||
|--|--|
|
||||
| Severity | Medium |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Open |
|
||||
| Location | `src/ScadaLink.CentralUI/Components/Pages/SiteCalls/SiteCallsReport.razor:74-80`; `src/ScadaLink.CentralUI/Components/Pages/SiteCalls/SiteCallsReport.razor.cs:421-425`; `src/ScadaLink.CentralUI/Components/Pages/Notifications/NotificationReport.razor:75-81,639-640`; `src/ScadaLink.CentralUI/Components/Pages/Monitoring/EventLogs.razor:62-73,261-262` |
|
||||
|
||||
**Description**
|
||||
|
||||
The same `datetime-local`-treated-as-UTC bug from CentralUI-008 and CentralUI-026 is
|
||||
present on three other pages:
|
||||
|
||||
- `SiteCallsReport.ToUtc` stamps `DateTimeKind.Utc` on the local-input value
|
||||
(`DateTime.SpecifyKind(value.Value, DateTimeKind.Utc)`).
|
||||
- `NotificationReport.ToUtc` does the same — `new DateTimeOffset(DateTime.SpecifyKind(local.Value, DateTimeKind.Utc))`.
|
||||
- `EventLogs.FetchPage` emits `new DateTimeOffset(_filterFrom.Value, TimeSpan.Zero)`,
|
||||
which labels the browser-local wall-clock value as UTC (the exact pre-fix shape of
|
||||
CentralUI-008).
|
||||
|
||||
For any non-UTC operator, every Site-Calls / Notification / Event-Log query is silently
|
||||
shifted by their UTC offset. The bug is mass-recreated on every page added after
|
||||
CentralUI-008 landed — the `BrowserTime` helper exists but is only used by the legacy
|
||||
Audit Log page and `ConfigurationAuditLog`.
|
||||
|
||||
**Recommendation**
|
||||
|
||||
Plumb the browser offset (via `eval` interop or a dedicated JS module, mirroring
|
||||
`ConfigurationAuditLog`/`AuditLog.razor`) into each of these pages and route every
|
||||
local-input value through `BrowserTime.LocalInputToUtc(value, offsetMinutes)` before
|
||||
constructing the wire filter. Add regression tests pinning the non-UTC behaviour for
|
||||
at least one representative page so the helper's continued use is enforced.
|
||||
|
||||
### CentralUI-028 — `NotificationReport` and `SiteCallsReport` bypass `SiteScopeService` — Deployment role site-scoping defeated on the two new central-mirror pages
|
||||
|
||||
| | |
|
||||
|--|--|
|
||||
| Severity | High |
|
||||
| Category | Security |
|
||||
| Status | Open |
|
||||
| Location | `src/ScadaLink.CentralUI/Components/Pages/Notifications/NotificationReport.razor:2,434,472,502`; `src/ScadaLink.CentralUI/Components/Pages/SiteCalls/SiteCallsReport.razor:2,52-59`; `src/ScadaLink.CentralUI/Components/Pages/SiteCalls/SiteCallsReport.razor.cs:97-110,201,250-251,278-279` |
|
||||
|
||||
**Description**
|
||||
|
||||
Both pages are `[Authorize(Policy = RequireDeployment)]` and, per CLAUDE.md "Security &
|
||||
Auth", the Deployment role must be site-scoped. CentralUI-002 fixed this for every
|
||||
Deployment/Monitoring page that existed at the time by introducing `SiteScopeService`
|
||||
and threading `FilterSitesAsync` / `IsSiteAllowedAsync` through the site dropdowns and
|
||||
mutating calls. The two new central-mirror pages — Notification Report (Notification
|
||||
Outbox queryable list) and Site Calls Report (Site Call Audit queryable list) — do NOT
|
||||
inject `SiteScopeService`, do NOT filter their Source-Site `<select>` lists (they
|
||||
enumerate `await SiteRepository.GetAllSitesAsync()` straight to the dropdown), do NOT
|
||||
narrow the query results by permitted site, and do NOT re-check the user's grant
|
||||
before relaying Retry/Discard to the owning site. `NotificationReport.RetryNotificationAsync`,
|
||||
`NotificationReport.DiscardNotificationAsync`, `SiteCallsReport.RetrySiteCallAsync`,
|
||||
and `SiteCallsReport.DiscardSiteCallAsync` all dispatch with the row's `SourceSiteId` /
|
||||
`SourceSite` unchecked. A scoped Deployment user can therefore (a) browse every row in
|
||||
the central `Notifications` / `SiteCalls` table including those for sites outside their
|
||||
grant, (b) submit Retry/Discard URLs hand-crafted from the row metadata, and (c) the
|
||||
site relay completes successfully because the CommunicationService only sees the
|
||||
row's source-site identifier, not the user's grant. This is a direct regression of the
|
||||
CentralUI-002 contract on the two pages that landed after CentralUI-002 was closed.
|
||||
|
||||
**Recommendation**
|
||||
|
||||
Inject `SiteScopeService` into both pages; filter the source-site dropdown through
|
||||
`FilterSitesAsync`; default the filter to the permitted-site set so a scoped user sees
|
||||
only their own rows (or push the predicate into the central query — preferred, so the
|
||||
filter cannot be bypassed by URL manipulation); and re-check `IsSiteAllowedAsync` in
|
||||
`RetryNotificationAsync`/`DiscardNotificationAsync`/`RetrySiteCallAsync`/`DiscardSiteCallAsync`
|
||||
before the CommunicationService call, surfacing a "not permitted for this site" toast
|
||||
on failure (mirroring `ParkedMessages.razor`'s `SelectedSiteIsPermitted` guard).
|
||||
Add `Site_ScopedDeploymentUser_OnlySeesPermittedRows` and
|
||||
`Site_ScopedDeploymentUser_CannotRetryRowOnNonPermittedSite` regression tests modelled
|
||||
on `TopologyPageTests.SiteScoping_*`.
|
||||
|
||||
### CentralUI-029 — `ConfigurationAuditLog` uses `JS.InvokeAsync<int>("eval", ...)` instead of a dedicated JS module
|
||||
|
||||
| | |
|
||||
|--|--|
|
||||
| Severity | Low |
|
||||
| Category | Code organization & conventions |
|
||||
| Status | Open |
|
||||
| Location | `src/ScadaLink.CentralUI/Components/Pages/Audit/ConfigurationAuditLog.razor:248-263` |
|
||||
|
||||
**Description**
|
||||
|
||||
`OnAfterRenderAsync` fetches the browser's UTC offset with
|
||||
`JS.InvokeAsync<int>("eval", "new Date().getTimezoneOffset()")`. Calling `eval` over
|
||||
JS interop is a code-smell: it widens the JS-interop attack surface (any future
|
||||
attacker who can influence the second argument runs arbitrary JS), it is brittle
|
||||
under stricter Content-Security-Policy headers (CSP `script-src` directives commonly
|
||||
forbid `unsafe-eval`), and it bypasses the existing module-import pattern the rest
|
||||
of the module follows (`session-expiry.js`, `audit-grid.js`, `nav-state.js`,
|
||||
`transport.js` are all loaded as `IJSObjectReference` modules). The legacy
|
||||
`AuditLog.razor` (CentralUI-008 fix) and the planned helper exist precisely to avoid
|
||||
this. Today the eval text is a static string so there is no live bug; the issue is
|
||||
that the pattern invites a future caller to compose the argument from page state.
|
||||
|
||||
**Recommendation**
|
||||
|
||||
Move the offset lookup into a small wwwroot JS module (e.g.
|
||||
`wwwroot/js/browser-time.js` exporting `getTimezoneOffsetMinutes()`) and `import` it
|
||||
via `IJSObjectReference` like the other helpers. Replace the `eval` call with
|
||||
`module.InvokeAsync<int>("getTimezoneOffsetMinutes")`. The fix is local and removes
|
||||
a residual eval surface; the same module can host the rest of the `BrowserTime`
|
||||
plumbing CentralUI-027 will need.
|
||||
|
||||
### CentralUI-030 — `SandboxConsoleCapture`'s per-call `StringWriter` is not thread-safe under intra-script concurrency
|
||||
|
||||
| | |
|
||||
|--|--|
|
||||
| Severity | Low |
|
||||
| Category | Concurrency & thread safety |
|
||||
| Status | Open |
|
||||
| Location | `src/ScadaLink.CentralUI/ScriptAnalysis/SandboxConsoleCapture.cs:31-118`; `src/ScadaLink.CentralUI/ScriptAnalysis/ScriptAnalysisService.cs:401-404` |
|
||||
|
||||
**Description**
|
||||
|
||||
CentralUI-003 correctly routed console capture through an `AsyncLocal<StringWriter?>`
|
||||
so concurrent Test Runs cannot cross-contaminate. `BeginCapture` flows the capture
|
||||
buffer through the call-tree, and `Target` reads it on every `Write`. But a single
|
||||
script execution can still write to its captured `StringWriter` from multiple threads
|
||||
within one call-tree: the script trust model allows `System.Threading.Tasks`, so a
|
||||
user script can `await Task.WhenAll(t1, t2, t3)` where each task is `Task.Run(() => Console.WriteLine(...))`,
|
||||
and `_current.Value` flows into each `Task.Run`. The capture buffer is a plain
|
||||
`StringWriter` (`captured = new StringWriter()` in `RunInSandboxAsync`), which is
|
||||
**not** thread-safe — concurrent `WriteLine` calls can throw or interleave
|
||||
character-level. The Akka/gRPC-thread race fixed by CentralUI-003 is gone, but the
|
||||
intra-script-concurrency race is a residual hazard for any script that exercises
|
||||
parallel tasks (a realistic shape for a Test Run that calls multiple `External.Call`s
|
||||
concurrently). Severity is Low because the symptom is a corrupted ConsoleOutput
|
||||
string, not a security/data-loss issue, and the script must opt into Task-based
|
||||
concurrency to trigger it.
|
||||
|
||||
**Recommendation**
|
||||
|
||||
Wrap the capture buffer with `TextWriter.Synchronized(new StringWriter())` (the
|
||||
BCL's purpose-built thread-safe wrapper), or hold a lock inside `SandboxConsoleCapture.Write*`
|
||||
on the current scope's `StringWriter`. Add a focused test that runs `await Task.WhenAll(...)`
|
||||
with `Console.WriteLine` in each task and asserts the resulting `ConsoleOutput` has
|
||||
the expected line count regardless of thread interleaving.
|
||||
|
||||
### CentralUI-031 — `TransportImport` buffers the full bundle bytes in component state
|
||||
|
||||
| | |
|
||||
|--|--|
|
||||
| Severity | Low |
|
||||
| Category | Performance & resource management |
|
||||
| Status | Open |
|
||||
| Location | `src/ScadaLink.CentralUI/Components/Pages/Design/TransportImport.razor.cs:72,104-142,160-161` |
|
||||
|
||||
**Description**
|
||||
|
||||
`OnFileSelectedAsync` reads the uploaded `.scadabundle` into a `MemoryStream`,
|
||||
calls `ms.ToArray()`, and stores the byte array on the component as
|
||||
`private byte[]? _bundleBytes`. The bytes live on the Blazor circuit for the
|
||||
lifetime of the wizard — through the passphrase step, the diff step (which can
|
||||
take an arbitrary amount of operator time on a large bundle), the confirm step,
|
||||
and the apply step — and are only cleared in `ResetSessionState` (Done /
|
||||
re-upload). For an operator who walks away from the diff step mid-review, the
|
||||
configured `MaxBundleSizeMb` (default not enforced here; only the file-size
|
||||
check on read) worth of bytes stays pinned on the central node's heap per
|
||||
open circuit. The page has no `IDisposable` to clear the bytes on tear-down
|
||||
either. Severity is Low because the cap is checked at upload time and Import
|
||||
is Admin-only (limited concurrent users), but the lifetime is longer than the
|
||||
strictly-needed retention.
|
||||
|
||||
**Recommendation**
|
||||
|
||||
Stream the bundle to a temp file (or to the `IBundleImporter`'s session store)
|
||||
rather than caching it on the component. Failing that, implement `IDisposable`
|
||||
on `TransportImport` and clear `_bundleBytes` (`Array.Clear` for sensitivity)
|
||||
on dispose; also clear the cached passphrase string. Tighten `MaxBundleSizeMb`
|
||||
docs to call out the in-memory cost per concurrent import session.
|
||||
|
||||
### CentralUI-032 — `AuditResultsGrid` paging is forward-only, no Previous button
|
||||
|
||||
| | |
|
||||
|--|--|
|
||||
| Severity | Low |
|
||||
| Category | Design-document adherence |
|
||||
| Status | Open |
|
||||
| Location | `src/ScadaLink.CentralUI/Components/Audit/AuditResultsGrid.razor:76-82`; `src/ScadaLink.CentralUI/Components/Audit/AuditResultsGrid.razor.cs:65,196-197,219-220` |
|
||||
|
||||
**Description**
|
||||
|
||||
The Audit Log results grid (Bundle B / M7-T3) renders a single "Next page" button
|
||||
and a `Page N · M rows` label, with no Previous control. The design doc says
|
||||
"Keyset pagination ordered by `(OccurredAtUtc desc, EventId desc)`. Default page
|
||||
size 100." — keyset paging is naturally forward-only, but a usable audit-triage
|
||||
workflow needs to step back to the previous page (the `SiteCallsReport` keyset
|
||||
implementation correctly maintains a `Stack<(...)> _cursorStack` for exactly this).
|
||||
An operator who clicks Next once and misses a row on the first page cannot return
|
||||
without re-applying the filter to start a fresh first page. The current shape
|
||||
also makes the "Page N" label slightly misleading — there is no in-grid affordance
|
||||
to use it as a navigation target.
|
||||
|
||||
**Recommendation**
|
||||
|
||||
Mirror the `SiteCallsReport.razor.cs` keyset-paging shape: maintain a
|
||||
`Stack<(DateTime?, Guid?)> _cursorStack` of previous-page cursors, add a Previous
|
||||
button gated on `_cursorStack.Count > 0`, push the current cursor on Next and pop
|
||||
on Previous. Either implement this or update the design doc to acknowledge
|
||||
forward-only paging on the Audit Log grid.
|
||||
|
||||
### CentralUI-033 — Drill-in / query-string code paths for the new Transport + SiteCalls pages are untested
|
||||
|
||||
| | |
|
||||
|--|--|
|
||||
| Severity | Low |
|
||||
| Category | Testing coverage |
|
||||
| Status | Open |
|
||||
| Location | `src/ScadaLink.CentralUI/Components/Pages/Design/TransportImport.razor.cs:97-238,267-319`; `src/ScadaLink.CentralUI/Components/Pages/SiteCalls/SiteCallsReport.razor.cs:107-148`; `tests/ScadaLink.CentralUI.Tests/Pages/Design/TransportImportPageTests.cs`; `tests/ScadaLink.CentralUI.Tests/Pages/SiteCallsReportPageTests.cs` |
|
||||
|
||||
**Description**
|
||||
|
||||
The CentralUI-025 lesson — "a critical drill-in/redirect path was untested, so the
|
||||
CentralUI-020 defect was not caught" — applies again to the two newest pages.
|
||||
`SiteCallsReport.ApplyQueryStringFilters` parses `?status=` and `?stuck=true` to
|
||||
seed the filters from a Health-dashboard KPI tile drill-in; there is no test that
|
||||
pins this seeding (an unrecognised status, a missing param, the case-insensitive
|
||||
match). `TransportImport` has a 5-step state machine and a 3-strike passphrase
|
||||
lockout, both with intricate transition logic
|
||||
(`GoFromUploadAsync` re-trying `LoadAsync`, the `_failedUnlockAttempts` reset on
|
||||
success, the audit-row write on failure) — none of the step-machine transition
|
||||
paths or the lockout reset / lockout-trip behaviours are pinned by tests. The
|
||||
existing `TransportImportPageTests` exercise rendering shapes, not the lifecycle.
|
||||
|
||||
**Recommendation**
|
||||
|
||||
Add bUnit tests for `SiteCallsReport.ApplyQueryStringFilters` covering valid /
|
||||
invalid / case-mismatched `?status=` values and the `?stuck=true` toggle, and
|
||||
add `TransportImport` lifecycle tests covering: an encrypted-bundle upload
|
||||
advances to Step 2 without opening a session; a wrong passphrase increments the
|
||||
counter and writes the `BundleImportUnlockFailed` audit row; the lockout resets
|
||||
the wizard to Step 1 once `MaxUnlockAttemptsPerSession` is reached; a successful
|
||||
unlock resets the counter and advances to Step 3.
|
||||
|
||||
Reference in New Issue
Block a user