docs(code-review): record SMS-feature review findings + reconcile NotificationService doc
Per-module code review of the SMS notifications feature (reviewed at d6ead8ae) following
code-reviews/REVIEW-PROCESS.md. 19 findings across 7 modules — 1 High, 5 Medium, 13 Low:
- ManagementService-024 (High): provider-config updates Admin-gated to match the UI.
- ConfigurationDatabase-025, CentralUI-034/035, ManagementService-025 (Medium): migration
data-safety guard, type-aware recipient badge, FromNumber-optional (Messaging-Service-only),
empty-token-clear guard.
- Remaining Low: secret-encryption + diff + dispatch + factory + contract tests, truncation,
ctor guard, reserved retry-field docs.
- Won't Fix: Transport-015/016 (shared repo-wide import patterns, not SMS-specific),
Commons-026 (breaking ergonomics-only change). Deferred: ConfigurationDatabase-027 (live-SQL
migration test).
All findings closed (0 pending). README.md regenerated; Component-NotificationService.md
updated for the FromNumber-optional + reserved-retry-fields outcomes.
This commit is contained in:
@@ -5,9 +5,9 @@
|
||||
| Module | `src/ZB.MOM.WW.ScadaBridge.CLI` |
|
||||
| Design doc | `docs/requirements/Component-CLI.md` |
|
||||
| Status | Reviewed |
|
||||
| Last reviewed | 2026-05-28 |
|
||||
| Last reviewed | 2026-06-19 |
|
||||
| Reviewer | claude-agent |
|
||||
| Commit reviewed | `1eb6e97` |
|
||||
| Commit reviewed | `d6ead8ae` |
|
||||
| Open findings | 1 |
|
||||
|
||||
## Summary
|
||||
@@ -1082,3 +1082,36 @@ authoritative source.
|
||||
**Resolution**
|
||||
|
||||
_Unresolved._
|
||||
|
||||
#### Re-review 2026-06-19 (commit `d6ead8ae`) — SMS notifications feature
|
||||
|
||||
Per-module review of the CLI SMS surface: `--type email|sms` / `--phones` on `notification list`,
|
||||
the `notification sms list|update` subcommands, and `bundle export --sms-configs`. Channel-aware
|
||||
validation and the AuthToken-never-printed projection are correct and tested. One Low test-coverage
|
||||
finding; Resolved.
|
||||
|
||||
### CLI-024 — No contract test pinning `notification sms update` required fields
|
||||
|
||||
| | |
|
||||
|--|--|
|
||||
| Severity | Low |
|
||||
| Category | Testing coverage |
|
||||
| Status | Resolved |
|
||||
| Location | `tests/ZB.MOM.WW.ScadaBridge.CLI.Tests/UpdateCommandContractTests.cs` |
|
||||
|
||||
**Description**
|
||||
|
||||
`UpdateCommandContractTests` pins the `Required`/optional contract for every other update command's
|
||||
core flags, but had no entry for `notification sms update` — a future regression making `--id` or
|
||||
`--account-sid` optional (silently sending null) would go uncaught.
|
||||
|
||||
**Recommendation**
|
||||
|
||||
Add a contract test asserting the required/optional flags for `notification sms update`.
|
||||
|
||||
**Resolution**
|
||||
|
||||
Resolved 2026-06-19 (commit `a9393c89`): added `SmsUpdate_CoreFieldsRequired`. (Updated in commit
|
||||
`33e1802e` when `--from-number` became conditionally validated rather than `Required` — see
|
||||
CentralUI-035 — so the test now pins `--id`/`--account-sid` Required and `--from-number`/`--auth-token`
|
||||
optional, with the either-or validation covered in `NotificationSmsCommandTests`.)
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
| Module | `src/ZB.MOM.WW.ScadaBridge.CentralUI` |
|
||||
| Design doc | `docs/requirements/Component-CentralUI.md` |
|
||||
| Status | Reviewed |
|
||||
| Last reviewed | 2026-05-28 |
|
||||
| Last reviewed | 2026-06-19 |
|
||||
| Reviewer | claude-agent |
|
||||
| Commit reviewed | `1eb6e97` |
|
||||
| Commit reviewed | `d6ead8ae` |
|
||||
| Open findings | 0 |
|
||||
|
||||
## Summary
|
||||
@@ -1595,3 +1595,93 @@ 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.
|
||||
|
||||
#### Re-review 2026-06-19 (commit `d6ead8ae`) — SMS notifications feature
|
||||
|
||||
Per-module review of the CentralUI SMS surface: the `/notifications/sms` Admin-only config page, the
|
||||
adapter-gated Type selector + per-type recipient input on the list form, the Type column, and the
|
||||
Transport-export SMS-config selection. Security is solid — the page is `RequireAdmin`, the AuthToken is
|
||||
never rendered (presence-only) and never pre-filled, preserve-if-blank is correct. Two Medium
|
||||
correctness findings and one Low test gap; all Resolved.
|
||||
|
||||
### CentralUI-034 — Notification Lists recipient badge renders "Name <>" for SMS recipients
|
||||
|
||||
| | |
|
||||
|--|--|
|
||||
| Severity | Medium |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Notifications/NotificationLists.razor:70` |
|
||||
|
||||
**Description**
|
||||
|
||||
The recipient badge rendered `@r.Name <@r.EmailAddress>` unconditionally. An SMS recipient carries a
|
||||
PhoneNumber and a null EmailAddress, so an SMS list rendered "Jane <>" — an empty contact field. The
|
||||
same commit added the Type column to this page but did not make the badge type-aware (the list form's
|
||||
recipients table WAS made type-aware; only this summary page was missed).
|
||||
|
||||
**Recommendation**
|
||||
|
||||
Make the badge type-aware: show PhoneNumber for SMS lists, EmailAddress otherwise.
|
||||
|
||||
**Resolution**
|
||||
|
||||
Resolved 2026-06-19 (commit `cd8e4872`): the badge now selects the contact field by `list.Type` (phone
|
||||
for SMS, email otherwise). Locked by `SmsListRecipientBadge_ShowsPhoneNumber_NotEmptyAngleBrackets`
|
||||
(commit `a9393c89`).
|
||||
|
||||
### CentralUI-035 — SMS config Save hard-required a From Number, contradicting the either-or contract
|
||||
|
||||
| | |
|
||||
|--|--|
|
||||
| Severity | Medium |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Notifications/SmsConfiguration.razor:219` |
|
||||
|
||||
**Description**
|
||||
|
||||
`Save()` required a From Number unconditionally, but the design doc and the Twilio delivery adapter
|
||||
treat FromNumber and MessagingServiceSid as either-or (the adapter only errors when BOTH are blank, and
|
||||
uses the Messaging Service SID instead of From when set). An admin could not create a Twilio
|
||||
Messaging-Service-only configuration through the UI, despite the entity and adapter supporting it.
|
||||
Investigation found the drift ran deeper than the UI: the entity ctor and the EF `FromNumber` column
|
||||
were also non-null, so a pure UI fix was insufficient.
|
||||
|
||||
**Recommendation**
|
||||
|
||||
Reconcile the implementation with the spec — make FromNumber optional end-to-end (entity, schema, UI,
|
||||
CLI) so a Messaging-Service-only config is valid, with at-least-one-of (FromNumber, MessagingServiceSid)
|
||||
validated at the boundaries.
|
||||
|
||||
**Resolution**
|
||||
|
||||
Resolved 2026-06-19 (commit `33e1802e`): made FromNumber optional end-to-end — `SmsConfiguration.FromNumber`
|
||||
→ nullable (ctor param optional), EF `IsRequired(false)` + migration `SmsFromNumberOptional` (ALTER
|
||||
COLUMN nullable), `SmsConfigDto.FromNumber` → nullable, `UpdateSmsConfigCommand.FromNumber` → nullable,
|
||||
UI validation now requires AccountSid + at-least-one-of(FromNumber, MessagingServiceSid), and CLI
|
||||
`--from-number` is conditionally validated. Direction chosen by the design owner.
|
||||
|
||||
### CentralUI-036 — No test for the From-Number/Messaging-Service-SID either-or rule
|
||||
|
||||
| | |
|
||||
|--|--|
|
||||
| Severity | Low |
|
||||
| Category | Testing coverage |
|
||||
| Status | Resolved |
|
||||
| Location | `tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Pages/SmsConfigurationPageTests.cs` |
|
||||
|
||||
**Description**
|
||||
|
||||
No test pinned the sender-identity validation, so the divergence in CentralUI-035 (UI stricter than the
|
||||
adapter contract) was not caught.
|
||||
|
||||
**Recommendation**
|
||||
|
||||
Add a test that saving succeeds with a Messaging Service SID and no From Number.
|
||||
|
||||
**Resolution**
|
||||
|
||||
Resolved 2026-06-19 (commit `33e1802e`): added `SavingNewConfig_MessagingServiceSidOnly_NoFromNumber_Saves`,
|
||||
which asserts the config persists with a null FromNumber. Complemented by ManagementActor + CLI
|
||||
either-or tests in the same commit.
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
| Module | `src/ZB.MOM.WW.ScadaBridge.Commons` |
|
||||
| Design doc | `docs/requirements/Component-Commons.md` |
|
||||
| Status | Reviewed |
|
||||
| Last reviewed | 2026-05-28 |
|
||||
| Last reviewed | 2026-06-19 |
|
||||
| Reviewer | claude-agent |
|
||||
| Commit reviewed | `1eb6e97` |
|
||||
| Commit reviewed | `d6ead8ae` |
|
||||
| Open findings | 2 |
|
||||
|
||||
## Summary
|
||||
@@ -1155,3 +1155,87 @@ fields on positional records MUST be added at the end of the parameter list AND
|
||||
carry a `= null` (or other safe) default value, so existing positional construction
|
||||
sites keep compiling. Apply that rule retroactively to `TrackingStatusSnapshot` and any
|
||||
other recent record that did not adopt it. No behavioral change required.
|
||||
|
||||
#### Re-review 2026-06-19 (commit `d6ead8ae`) — SMS notifications feature
|
||||
|
||||
Per-module review of the SMS (Twilio) notifications feature (#21 Notification Outbox / #8
|
||||
Notification Service slice). Three Low-severity findings in Commons; all closed. The new
|
||||
`SmsConfiguration` POCO, `NotificationType.Sms`, the `NotificationRecipient` `ForEmail`/`ForSms`
|
||||
factories, and the additive message-contract changes all follow the established conventions.
|
||||
|
||||
### Commons-024 — `NotificationRecipient` public constructor name-guard weaker than the factories
|
||||
|
||||
| | |
|
||||
|--|--|
|
||||
| Severity | Low |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Commons/Entities/Notifications/NotificationRecipient.cs:21-25` |
|
||||
|
||||
**Description**
|
||||
|
||||
The public `NotificationRecipient(string name, string emailAddress)` constructor validated `name`
|
||||
with a null-only check (`name ?? throw`), while the `ForEmail`/`ForSms` factories reject
|
||||
null-or-whitespace names. A caller using the public ctor directly could produce a recipient with a
|
||||
whitespace name, an invariant the factories forbid. (EF materializes via the private parameterless
|
||||
ctor + property injection, so this ctor is reached only by intentional email-path callers.)
|
||||
|
||||
**Recommendation**
|
||||
|
||||
Tighten the public ctor's name guard to `IsNullOrWhiteSpace` to match the factories.
|
||||
|
||||
**Resolution**
|
||||
|
||||
Resolved 2026-06-19 (commit `cd8e4872`): the public ctor now throws `ArgumentException` on a
|
||||
null-or-whitespace name, matching `ForEmail`. Locked by `NotificationRecipientTests` (commit `a9393c89`).
|
||||
|
||||
### Commons-025 — No unit tests for `NotificationRecipient.ForEmail`/`ForSms` factory invariants
|
||||
|
||||
| | |
|
||||
|--|--|
|
||||
| Severity | Low |
|
||||
| Category | Testing coverage |
|
||||
| Status | Resolved |
|
||||
| Location | `tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Entities/NotificationRecipientTests.cs` |
|
||||
|
||||
**Description**
|
||||
|
||||
The recipient construction invariants (email-only for `ForEmail`, phone-only for `ForSms`,
|
||||
non-blank name on every path) were exercised only transitively via ManagementActor tests, with no
|
||||
focused Commons-layer unit tests.
|
||||
|
||||
**Recommendation**
|
||||
|
||||
Add a `NotificationRecipientTests` covering both factories' happy paths, the null/whitespace guards,
|
||||
and the contact-field exclusivity.
|
||||
|
||||
**Resolution**
|
||||
|
||||
Resolved 2026-06-19 (commit `a9393c89`): added `NotificationRecipientTests` (14 cases) covering
|
||||
`ForEmail`/`ForSms` happy paths, contact-field exclusivity, null/whitespace name guards, and the
|
||||
public-ctor guards.
|
||||
|
||||
### Commons-026 — `Create/UpdateNotificationListCommand.RecipientEmails` non-nullable even for SMS lists
|
||||
|
||||
| | |
|
||||
|--|--|
|
||||
| Severity | Low |
|
||||
| Category | Code organization & conventions |
|
||||
| Status | Won't Fix |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Management/NotificationCommands.cs:7-8` |
|
||||
|
||||
**Description**
|
||||
|
||||
`CreateNotificationListCommand`/`UpdateNotificationListCommand` carry `RecipientEmails` as a
|
||||
required positional parameter even when `Type = Sms`, where it is semantically irrelevant (the
|
||||
handler ignores it and reads `RecipientPhones`). This is an ergonomics wart, not a correctness bug.
|
||||
|
||||
**Recommendation**
|
||||
|
||||
Optionally default `RecipientEmails` to an empty list, or normalize null→empty in the handler.
|
||||
|
||||
**Resolution**
|
||||
|
||||
Won't Fix (2026-06-19): not a correctness defect — the handler already ignores `RecipientEmails`
|
||||
for SMS lists. Changing a required positional parameter to defaulted/nullable is a breaking
|
||||
contract change for existing callers for a pure-ergonomics gain; deferred indefinitely.
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
| Module | `src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase` |
|
||||
| Design doc | `docs/requirements/Component-ConfigurationDatabase.md` |
|
||||
| Status | Reviewed |
|
||||
| Last reviewed | 2026-05-28 |
|
||||
| Last reviewed | 2026-06-19 |
|
||||
| Reviewer | claude-agent |
|
||||
| Commit reviewed | `1eb6e97` |
|
||||
| Commit reviewed | `d6ead8ae` |
|
||||
| Open findings | 0 |
|
||||
|
||||
## Summary
|
||||
@@ -1373,3 +1373,91 @@ later months. This pins down the resolution of CD-019. (2) Add a
|
||||
`DeploymentRecord`, clear the change tracker, call `DeleteDeploymentRecordAsync`,
|
||||
and assert the row is gone — pinning the resolution of CD-018. Both tests should be
|
||||
`[SkippableFact]` so the suite still passes when no MS SQL Server is available.
|
||||
|
||||
#### Re-review 2026-06-19 (commit `d6ead8ae`) — SMS notifications feature
|
||||
|
||||
Per-module review of the SMS (Twilio) notifications data layer: the `SmsConfiguration` EF mapping +
|
||||
encrypted `AuthToken`, the `AddSmsNotifications` migration, and the `NotificationRecipient` nullable
|
||||
contact change. The encryption wiring (`EncryptedStringConverter` on `AuthToken` via
|
||||
`ApplySecretColumnEncryption` + `GuardSecretWritesHaveAKeyRing`) mirrors `SmtpConfiguration.Credentials`
|
||||
and is correct. Two Medium findings (one data-safety, one test gap) and one Low test gap; the two
|
||||
Mediums are Resolved.
|
||||
|
||||
### ConfigurationDatabase-025 — `AddSmsNotifications.Down()` silently corrupts SMS-only recipients
|
||||
|
||||
| | |
|
||||
|--|--|
|
||||
| Severity | Medium |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Migrations/20260619135543_AddSmsNotifications.cs:52-66` |
|
||||
|
||||
**Description**
|
||||
|
||||
The `Down()` migration drops the `PhoneNumber` column and backfills every NULL `EmailAddress` to `''`
|
||||
so the column can revert to NOT NULL. An SMS-only recipient (PhoneNumber set, EmailAddress
|
||||
legitimately NULL) is therefore corrupted on rollback: its phone is dropped and its email becomes an
|
||||
invalid empty string. In a database where SMS lists have been created, the rollback destroys contact data.
|
||||
|
||||
**Recommendation**
|
||||
|
||||
Before dropping `PhoneNumber`, guard the rollback: throw if any SMS-only recipient (PhoneNumber set,
|
||||
EmailAddress NULL) exists, so the operator must reassign/remove them first instead of losing data silently.
|
||||
|
||||
**Resolution**
|
||||
|
||||
Resolved 2026-06-19 (commit `cd8e4872`): `Down()` now runs a `THROW`-on-`IF EXISTS` guard for
|
||||
SMS-only recipients as its first statement (while `PhoneNumber` is still queryable), refusing the
|
||||
rollback instead of corrupting data. Comments document the invariant.
|
||||
|
||||
### ConfigurationDatabase-026 — No encryption-at-rest regression test for `SmsConfiguration.AuthToken`
|
||||
|
||||
| | |
|
||||
|--|--|
|
||||
| Severity | Medium |
|
||||
| Category | Testing coverage |
|
||||
| Status | Resolved |
|
||||
| Location | `tests/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Tests/SecretEncryptionTests.cs` |
|
||||
|
||||
**Description**
|
||||
|
||||
`SecretEncryptionTests` covered the database-connection, SMTP-credentials, and external-system secrets
|
||||
with "stored-encrypted / raw != plaintext / round-trips" tests, but had no parallel test for the
|
||||
Twilio `AuthToken`. A refactor dropping `SmsConfiguration` from `ApplySecretColumnEncryption` would go
|
||||
uncaught.
|
||||
|
||||
**Recommendation**
|
||||
|
||||
Add `SmsConfiguration_AuthToken_StoredEncrypted_RoundTrips` and a null-round-trip test mirroring the
|
||||
SMTP shape.
|
||||
|
||||
**Resolution**
|
||||
|
||||
Resolved 2026-06-19 (commit `a9393c89`): added both tests; the encrypted test also asserts the raw
|
||||
column is ciphertext and that `AccountSid` stays plaintext.
|
||||
|
||||
### ConfigurationDatabase-027 — No MS SQL migration integration test for `AddSmsNotifications`
|
||||
|
||||
| | |
|
||||
|--|--|
|
||||
| Severity | Low |
|
||||
| Category | Testing coverage |
|
||||
| Status | Deferred |
|
||||
| Location | `tests/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Tests/Migrations/` |
|
||||
|
||||
**Description**
|
||||
|
||||
Prior structural migrations have MS SQL integration tests asserting the resulting columns/types/nullability.
|
||||
`AddSmsNotifications` (and the follow-up `SmsFromNumberOptional`) have none, so the column shapes and the
|
||||
idempotency of the `ALTER COLUMN` statements are unverified against a real SQL Server.
|
||||
|
||||
**Recommendation**
|
||||
|
||||
Add an `AddSmsNotificationsMigrationTests` using the `MsSqlMigrationFixture` pattern.
|
||||
|
||||
**Resolution**
|
||||
|
||||
Deferred (2026-06-19): the migrations are standard idempotent `ALTER`/guarded-`CREATE` statements
|
||||
verified by the model snapshot and the build, and `AddSmsNotifications` was applied to the real
|
||||
docker MS SQL during the feature's integration pass. A live-SQL migration test is tracked for a
|
||||
future ConfigurationDatabase test-stability pass.
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
| Module | `src/ZB.MOM.WW.ScadaBridge.ManagementService` |
|
||||
| Design doc | `docs/requirements/Component-ManagementService.md` |
|
||||
| Status | Reviewed |
|
||||
| Last reviewed | 2026-05-28 |
|
||||
| Last reviewed | 2026-06-19 |
|
||||
| Reviewer | claude-agent |
|
||||
| Commit reviewed | `1eb6e97` |
|
||||
| Commit reviewed | `d6ead8ae` |
|
||||
| Open findings | 0 (1 Deferred — see ManagementService-012) |
|
||||
|
||||
## Summary
|
||||
@@ -1131,3 +1131,88 @@ entirely.
|
||||
|
||||
Defer until a noticeable hot path emerges, but track it: this is the only N+1 in
|
||||
`ManagementActor` once 002 / 014 are folded in.
|
||||
|
||||
#### Re-review 2026-06-19 (commit `d6ead8ae`) — SMS notifications feature
|
||||
|
||||
Per-module review of the `ManagementActor` SMS surface: the SMS/SMTP provider-config command gating,
|
||||
`HandleUpdateSmsConfig`, and the `SmsConfigPublicShape` secret projection. The secret-redaction chain
|
||||
(`SmsConfigPublicShape` exposes `HasAuthToken` only; the AuthToken never reaches the response or audit
|
||||
afterState) is correct and tested. One High authorization finding, one Medium secret-clear footgun, one
|
||||
Low stale comment; all Resolved.
|
||||
|
||||
### ManagementService-024 — SMS/SMTP provider-config updates were Designer-gated despite Admin-only UI
|
||||
|
||||
| | |
|
||||
|--|--|
|
||||
| Severity | High |
|
||||
| Category | Security |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementActor.cs:189-190` |
|
||||
|
||||
**Description**
|
||||
|
||||
`UpdateSmsConfigCommand` (and the pre-existing `UpdateSmtpConfigCommand`) were placed in the Designer
|
||||
arm of `GetRequiredRole`, but both the `/notifications/sms` and `/notifications/smtp` Central UI pages
|
||||
enforce `RequireAdmin`. This split-brain let a Designer who is blocked in the UI still rotate a
|
||||
production credential (the Twilio Auth Token / SMTP password) via the CLI / Management API. The design
|
||||
(D8) specifies SMS config management as Admin-only.
|
||||
|
||||
**Recommendation**
|
||||
|
||||
Move `UpdateSmsConfigCommand` (and `UpdateSmtpConfigCommand`, whose UI page is also RequireAdmin) to the
|
||||
Administrator arm so the actor gate matches the UI.
|
||||
|
||||
**Resolution**
|
||||
|
||||
Resolved 2026-06-19 (commit `cd8e4872`): both update commands moved to the Administrator arm. Locked by
|
||||
`UpdateSmsConfig_WithDesignerRole_ReturnsUnauthorized` and updated success-case tests (commit `a9393c89`).
|
||||
|
||||
### ManagementService-025 — Empty-string `--auth-token` silently clears the stored Twilio token
|
||||
|
||||
| | |
|
||||
|--|--|
|
||||
| Severity | Medium |
|
||||
| Category | Security |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementActor.cs:1859` |
|
||||
|
||||
**Description**
|
||||
|
||||
`HandleUpdateSmsConfig` used `if (cmd.AuthToken is not null)` for preserve-if-omitted. An explicit empty
|
||||
/ whitespace `--auth-token ""` passes the null check and overwrites the stored token with empty, silently
|
||||
clearing the credential and 401-ing every subsequent send. A Twilio Auth Token is always required, so an
|
||||
empty value is never a valid rotation.
|
||||
|
||||
**Recommendation**
|
||||
|
||||
Guard on `IsNullOrWhiteSpace` so an empty token is treated as omitted (preserve). (SMTP `Credentials`
|
||||
keeps its null-only guard, since empty may be valid for anonymous SMTP.)
|
||||
|
||||
**Resolution**
|
||||
|
||||
Resolved 2026-06-19 (commit `cd8e4872`): the guard is now `!string.IsNullOrWhiteSpace(cmd.AuthToken)`.
|
||||
Locked by `UpdateSmsConfig_WithEmptyAuthToken_PreservesExistingToken` (commit `a9393c89`).
|
||||
|
||||
### ManagementService-026 — Stale "Admin-only" comment on `HandleListSmtpConfigs`
|
||||
|
||||
| | |
|
||||
|--|--|
|
||||
| Severity | Low |
|
||||
| Category | Documentation & comments |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementActor.cs:1788-1790` |
|
||||
|
||||
**Description**
|
||||
|
||||
The comment on `HandleListSmtpConfigs` called the `UpdateSmtpConfig` path "Admin-only", which did not
|
||||
match its (then) Designer gating; the SMS counterpart comment said "Designer-gated". The two were
|
||||
inconsistent.
|
||||
|
||||
**Recommendation**
|
||||
|
||||
Make the comments accurate after fixing the gating.
|
||||
|
||||
**Resolution**
|
||||
|
||||
Resolved 2026-06-19 (commit `cd8e4872`): with both update commands moved to Administrator (MS-024), the
|
||||
SMTP "Admin-only" comment is now accurate and the SMS list comment was updated to "Admin-only".
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
| Module | `src/ZB.MOM.WW.ScadaBridge.NotificationOutbox` |
|
||||
| Design doc | `docs/requirements/Component-NotificationOutbox.md` |
|
||||
| Status | Reviewed |
|
||||
| Last reviewed | 2026-05-28 |
|
||||
| Last reviewed | 2026-06-19 |
|
||||
| Reviewer | claude-agent |
|
||||
| Commit reviewed | `1eb6e97` |
|
||||
| Commit reviewed | `d6ead8ae` |
|
||||
| Open findings | 7 |
|
||||
|
||||
## Summary
|
||||
@@ -486,3 +486,89 @@ task-construction throw and is otherwise unreachable."
|
||||
**Resolution**
|
||||
|
||||
_Unresolved._
|
||||
|
||||
#### Re-review 2026-06-19 (commit `d6ead8ae`) — SMS notifications feature
|
||||
|
||||
Per-module review of the Twilio `SmsNotificationDeliveryAdapter`, `SmsErrorClassifier`, `SmsOptions`,
|
||||
the DI registration, and the `NotificationOutboxActor` ingest type-stamping. This is the risk centre of
|
||||
the feature; it is well-built. The Twilio request shape, the transient/permanent error classification,
|
||||
the per-recipient rollup, and — critically — the AuthToken redaction on every error path
|
||||
(`CredentialRedactor.Scrub`) are all correct and well-tested. Three Low findings; all closed.
|
||||
|
||||
### NotificationOutbox-011 — `SmsConfiguration.MaxRetries`/`RetryDelay` are operator-settable but never honored
|
||||
|
||||
| | |
|
||||
|--|--|
|
||||
| Severity | Low |
|
||||
| Category | Design-document adherence |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.NotificationOutbox/NotificationOutboxActor.cs:370-404` |
|
||||
|
||||
**Description**
|
||||
|
||||
The dispatcher's `ResolveRetryPolicyAsync` derives the retry policy (max-retries + interval) from the
|
||||
central SMTP configuration for all notification types. `SmsConfiguration.MaxRetries`/`RetryDelay` are
|
||||
operator-editable (management, UI, Transport) but never read at dispatch time — they are dead config,
|
||||
which misleads an operator who sets them. (`ConnectionTimeoutSeconds`, by contrast, IS honored by the
|
||||
adapter.)
|
||||
|
||||
**Recommendation**
|
||||
|
||||
Either honor the per-SMS values type-aware in `ResolveRetryPolicyAsync`, or document the fields as
|
||||
reserved so operators are not misled.
|
||||
|
||||
**Resolution**
|
||||
|
||||
Resolved 2026-06-19 (commit `cd8e4872`): the fields are now documented as RESERVED on the entity
|
||||
(XML doc) and in `Component-NotificationService.md` — the dispatcher reuses the shared SMTP-derived
|
||||
retry policy per the documented "retry reuses central SMTP" decision. Honoring them per-type would
|
||||
supersede that decision and is recorded as a deferred enhancement (not changed unilaterally for a Low finding).
|
||||
|
||||
### NotificationOutbox-012 — SMS body truncation can split a surrogate pair at the cap boundary
|
||||
|
||||
| | |
|
||||
|--|--|
|
||||
| Severity | Low |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.NotificationOutbox/Delivery/SmsNotificationDeliveryAdapter.cs:359-367` |
|
||||
|
||||
**Description**
|
||||
|
||||
`ComposeBody` truncates by UTF-16 code-unit index. If `MaxMessageLength` falls on a surrogate-pair
|
||||
boundary (e.g. an emoji in the alarm subject/body), the cut emits a lone surrogate — one malformed
|
||||
character at the boundary. Cosmetic and only at the exact cap.
|
||||
|
||||
**Recommendation**
|
||||
|
||||
Back off one code unit if the cut would split a surrogate pair.
|
||||
|
||||
**Resolution**
|
||||
|
||||
Resolved 2026-06-19 (commit `cd8e4872`): the truncation now backs off by one code unit when the cut
|
||||
index lands immediately after a high surrogate, keeping the body well-formed and within the cap.
|
||||
|
||||
### NotificationOutbox-013 — No actor-level dispatch test for SMS-typed routing
|
||||
|
||||
| | |
|
||||
|--|--|
|
||||
| Severity | Low |
|
||||
| Category | Testing coverage |
|
||||
| Status | Resolved |
|
||||
| Location | `tests/ZB.MOM.WW.ScadaBridge.NotificationOutbox.Tests/NotificationOutboxActorDispatchTests.cs` |
|
||||
|
||||
**Description**
|
||||
|
||||
The adapter, ingest type-stamping, and DI registration were independently tested, but no actor-level
|
||||
test exercised the Type-keyed adapter selection landing an SMS-typed notification on the SMS adapter
|
||||
(vs the Email adapter).
|
||||
|
||||
**Recommendation**
|
||||
|
||||
Add a dispatch test that routes an SMS-typed notification through an SMS stub adapter and asserts the
|
||||
outcome.
|
||||
|
||||
**Resolution**
|
||||
|
||||
Resolved 2026-06-19 (commit `a9393c89`): made the dispatch-test stub adapter Type-configurable and
|
||||
added `Dispatch_SmsTypedNotification_RoutesToSmsAdapter_NotEmailAdapter`.
|
||||
|
||||
@@ -50,20 +50,20 @@ module file and counted in **Total**.
|
||||
| Module | Last reviewed | Commit | Open (C/H/M/L) | Open | Total |
|
||||
|--------|---------------|--------|----------------|------|-------|
|
||||
| [AuditLog](AuditLog/findings.md) | 2026-05-28 | `1eb6e97` | 0/0/0/0 | 0 | 11 |
|
||||
| [CLI](CLI/findings.md) | 2026-05-28 | `1eb6e97` | 0/0/0/0 | 0 | 23 |
|
||||
| [CentralUI](CentralUI/findings.md) | 2026-05-28 | `1eb6e97` | 0/0/0/0 | 0 | 33 |
|
||||
| [CLI](CLI/findings.md) | 2026-06-19 | `d6ead8ae` | 0/0/0/0 | 0 | 24 |
|
||||
| [CentralUI](CentralUI/findings.md) | 2026-06-19 | `d6ead8ae` | 0/0/0/0 | 0 | 36 |
|
||||
| [ClusterInfrastructure](ClusterInfrastructure/findings.md) | 2026-05-28 | `1eb6e97` | 0/0/0/0 | 0 | 14 |
|
||||
| [Commons](Commons/findings.md) | 2026-05-28 | `1eb6e97` | 0/0/0/0 | 0 | 23 |
|
||||
| [Commons](Commons/findings.md) | 2026-06-19 | `d6ead8ae` | 0/0/0/0 | 0 | 26 |
|
||||
| [Communication](Communication/findings.md) | 2026-05-28 | `1eb6e97` | 0/0/0/0 | 0 | 22 |
|
||||
| [ConfigurationDatabase](ConfigurationDatabase/findings.md) | 2026-05-28 | `1eb6e97` | 0/0/0/0 | 0 | 24 |
|
||||
| [ConfigurationDatabase](ConfigurationDatabase/findings.md) | 2026-06-19 | `d6ead8ae` | 0/0/0/0 | 0 | 27 |
|
||||
| [DataConnectionLayer](DataConnectionLayer/findings.md) | 2026-05-28 | `1eb6e97` | 0/0/0/0 | 0 | 22 |
|
||||
| [DeploymentManager](DeploymentManager/findings.md) | 2026-05-28 | `1eb6e97` | 0/0/0/0 | 0 | 24 |
|
||||
| [ExternalSystemGateway](ExternalSystemGateway/findings.md) | 2026-05-28 | `1eb6e97` | 0/0/0/0 | 0 | 23 |
|
||||
| [HealthMonitoring](HealthMonitoring/findings.md) | 2026-05-28 | `1eb6e97` | 0/0/0/0 | 0 | 23 |
|
||||
| [Host](Host/findings.md) | 2026-05-28 | `1eb6e97` | 0/0/0/0 | 0 | 22 |
|
||||
| [InboundAPI](InboundAPI/findings.md) | 2026-05-28 | `1eb6e97` | 0/0/0/0 | 0 | 25 |
|
||||
| [ManagementService](ManagementService/findings.md) | 2026-05-28 | `1eb6e97` | 0/0/0/0 | 0 | 23 |
|
||||
| [NotificationOutbox](NotificationOutbox/findings.md) | 2026-05-28 | `1eb6e97` | 0/0/0/0 | 0 | 10 |
|
||||
| [ManagementService](ManagementService/findings.md) | 2026-06-19 | `d6ead8ae` | 0/0/0/0 | 0 | 26 |
|
||||
| [NotificationOutbox](NotificationOutbox/findings.md) | 2026-06-19 | `d6ead8ae` | 0/0/0/0 | 0 | 13 |
|
||||
| [NotificationService](NotificationService/findings.md) | 2026-05-28 | `1eb6e97` | 0/0/0/0 | 0 | 25 |
|
||||
| [Security](Security/findings.md) | 2026-05-28 | `1eb6e97` | 0/0/0/0 | 0 | 21 |
|
||||
| [SiteCallAudit](SiteCallAudit/findings.md) | 2026-05-28 | `1eb6e97` | 0/0/0/0 | 0 | 6 |
|
||||
@@ -71,7 +71,7 @@ module file and counted in **Total**.
|
||||
| [SiteRuntime](SiteRuntime/findings.md) | 2026-05-28 | `1eb6e97` | 0/0/0/0 | 0 | 26 |
|
||||
| [StoreAndForward](StoreAndForward/findings.md) | 2026-05-28 | `1eb6e97` | 0/0/0/0 | 0 | 24 |
|
||||
| [TemplateEngine](TemplateEngine/findings.md) | 2026-05-28 | `1eb6e97` | 0/0/0/0 | 0 | 22 |
|
||||
| [Transport](Transport/findings.md) | 2026-05-28 | `1eb6e97` | 0/0/0/0 | 0 | 12 |
|
||||
| [Transport](Transport/findings.md) | 2026-06-19 | `d6ead8ae` | 0/0/0/0 | 0 | 15 |
|
||||
|
||||
## Pending Findings
|
||||
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
| Module | `src/ZB.MOM.WW.ScadaBridge.Transport` |
|
||||
| Design doc | `docs/requirements/Component-Transport.md` |
|
||||
| Status | Reviewed |
|
||||
| Last reviewed | 2026-05-28 |
|
||||
| Last reviewed | 2026-06-19 |
|
||||
| Reviewer | claude-agent |
|
||||
| Commit reviewed | `1eb6e97` |
|
||||
| Open findings | 7 |
|
||||
| Commit reviewed | `d6ead8ae` |
|
||||
| Open findings | 0 |
|
||||
|
||||
## Summary
|
||||
|
||||
@@ -527,3 +527,90 @@ but the Audit-Log-Viewer UI surface — the dropdown + `BundleImported` hyperlin
|
||||
is a deferred UI follow-up. Operators have a workaround via the existing
|
||||
`audit query --bundle-import-id` CLI flag. The UI work belongs in the CentralUI
|
||||
backlog; implementing it here would expand scope beyond a doc fix.
|
||||
|
||||
#### Re-review 2026-06-19 (commit `d6ead8ae`) — SMS notifications feature
|
||||
|
||||
Per-module review of the Transport SMS surface: `SmsConfigDto`, the entity serializer round-trip,
|
||||
`ApplySmsConfigsAsync` (AccountSid-keyed upsert), and `CompareSmsConfiguration`. The secret isolation is
|
||||
correct — the AuthToken rides only the encrypted `SecretsBlock`, never the plaintext DTO/diff, and the
|
||||
diff is presence-only. Backward-compat with pre-SMS bundles holds (`SmsConfigs` defaults to empty). Two
|
||||
findings carried over as Won't Fix (shared/inherited patterns), one Low test gap Resolved.
|
||||
|
||||
### Transport-015 — `ApplySmsConfigsAsync` Rename branch falls back to the original key when `RenameTo` is null
|
||||
|
||||
| | |
|
||||
|--|--|
|
||||
| Severity | Medium |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Won't Fix |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Transport/Import/BundleImporter.cs:2573` |
|
||||
|
||||
**Description**
|
||||
|
||||
The Rename branch uses `resolution.RenameTo ?? dto.AccountSid`; a null `RenameTo` on a Rename action
|
||||
would add a config under the original key, risking a duplicate. Flagged Medium by the reviewer.
|
||||
|
||||
**Recommendation**
|
||||
|
||||
Guard the Rename branch against a null `RenameTo`.
|
||||
|
||||
**Resolution**
|
||||
|
||||
Won't Fix (2026-06-19): on investigation this is the established repo-wide import pattern — every entity
|
||||
type's apply method uses the identical `resolution.RenameTo ?? <originalKey>` fallback (templates,
|
||||
folders, sites, data connections, instances, SMTP at line 2497, SMS at line 2573, ...). It is not
|
||||
SMS-specific, the import-wizard / CLI map step supplies `RenameTo` for a Rename action (the `??` is
|
||||
belt-and-suspenders), and the reviewer's proposed guard would merely relabel the duplicate as an Add. If
|
||||
the null-`RenameTo` edge is a real concern it is a Transport-wide question outside the SMS review scope;
|
||||
changing only the SMS branch would break the cross-entity consistency.
|
||||
|
||||
### Transport-016 — New `SmsConfiguration` audit rows log entity id "0"
|
||||
|
||||
| | |
|
||||
|--|--|
|
||||
| Severity | Low |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Won't Fix |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Transport/Import/BundleImporter.cs:2576,2594` |
|
||||
|
||||
**Description**
|
||||
|
||||
Audit rows for newly created `SmsConfiguration` entities log `"0"` as the entity id (the surrogate key
|
||||
is unknown before EF assigns it), mirroring the SMTP path. Correlation is still possible by name
|
||||
(AccountSid), so the id is misleading but not lost.
|
||||
|
||||
**Recommendation**
|
||||
|
||||
Read back the assigned id after save and use it in the audit `LogAsync` call.
|
||||
|
||||
**Resolution**
|
||||
|
||||
Won't Fix (2026-06-19): this is the established inherited pattern (SMTP at lines 2500/2518 does the same)
|
||||
and is not a regression introduced by the SMS work. The audit row carries the AccountSid as
|
||||
`entityName`, so the row is correlatable. A cross-entity cleanup to log real ids is out of the SMS
|
||||
review scope.
|
||||
|
||||
### Transport-017 — No unit tests for `CompareSmsConfiguration`
|
||||
|
||||
| | |
|
||||
|--|--|
|
||||
| Severity | Low |
|
||||
| Category | Testing coverage |
|
||||
| Status | Resolved |
|
||||
| Location | `tests/ZB.MOM.WW.ScadaBridge.Transport.Tests/Import/ArtifactDiffTests.cs` |
|
||||
|
||||
**Description**
|
||||
|
||||
`CompareSmsConfiguration` (the new diff method) had no dedicated unit tests; in particular the
|
||||
secret presence-only invariant (the AuthToken value must never be echoed) relied on parallelism with the
|
||||
SMTP/DataConnection patterns rather than a direct assertion.
|
||||
|
||||
**Recommendation**
|
||||
|
||||
Add unit tests for New/Identical/Modified plus the AuthToken presence-only invariant.
|
||||
|
||||
**Resolution**
|
||||
|
||||
Resolved 2026-06-19 (commit `a9393c89`): added five `CompareSmsConfiguration` tests — existing-null→New,
|
||||
all-match→Identical, FromNumber-differs→Modified, and the two secret cases (value-changes-but-presence-
|
||||
preserved → no echo; presence-flip → `<present>` marker only, token never echoed).
|
||||
|
||||
Reference in New Issue
Block a user