Licence is MIT, set solution-wide rather than only on the packable project:
DodoSSH.Contracts is published so clients can build against it, and a
package with no licence expression is one a corporate policy scanner
rejects outright.
The SSH.NET spike is the M1 client gate and it passes. SSH.NET 2025.1.0
exposes ShellStream.ChangeWindowSize, but a method existing is not the
remote observing it, so the tests read `stty size` back from a real sshd
after resizing rather than asserting the call did not throw. Repeated
resizes each take effect too, which matters because dragging a window edge
produces a stream of them. The IChannelSession fallback is not needed.
Also verified against a real sshd: password and public-key auth, that the
host key arrives as a raw blob we can fingerprint ourselves rather than
reading SSH.NET's MD5 property, and that refusing the key via CanTrust
actually aborts the connection -- without which the TOFU dialog would be
decoration.
Kept as a permanent suite, not deleted after the spike. An upgrade that
silently stopped sending the request would present as wrapped output only
after a resize, which is easy to misattribute to the terminal emulator.
Two bugs in the test itself, both worth naming because either would have
been read as "resize does not work":
- A PTY emits CRLF, and the anchored regex rejected the CR. The output
visibly contained `24 80` while the match failed.
- Each read can begin with output still buffered from the previous command,
including its size line. Taking the first match would have reported the
pre-resize size.
platform-flags.md now records window-change as resolved rather than
unverified -- a stale flag is worse than none -- plus the three real SSH.NET
limits found on the way: ShellStream does not override ReadAsync so every
idle session parks a pool thread, one connection cannot serve both
SshClient and SftpClient, and agent forwarding needs an upstream change.
27 end-to-end tests over the real HTTP pipeline, against a PostgreSQL container and a
stubbed identity provider. This closes the gap the previous commit flagged.
Authentication is genuinely exercised, not bypassed. StubIdentityProvider serves real OIDC
discovery and JWKS via WireMock and signs tokens with a real RSA key, so the application's
own JwtBearer pipeline validates issuer, audience, signature, lifetime and claims. A
TestAuthHandler that short-circuits authentication would hide exactly the claim-mapping
mistakes that cause real authorization holes. Proven by rejecting: no token, a foreign
signing key, the wrong audience, the wrong issuer, and an expired token.
Authorization denials — the tests that matter most:
- Another user's vault is 404, not 403, for both pull and push. A distinct
"exists but forbidden" answer is an existence oracle for other tenants' vault ids.
- A denied push writes nothing: no host row and no change-log entry. A denial that still
mutated state would be worse than no check at all.
- A team vault is denied until M3 rather than falling through to a permissive default.
Behaviour covered: push/pull round trip, cursor advance (and that an empty pull does not
rewind the cursor, which would replay history), tampered cursor rejection, stale-version
conflict returning server state without overwriting, operation-id replay reported Duplicate
and applied once, a mixed batch applying the good and reporting the bad, relay field
enforcement both ways, delete clearing the relay address, tombstones carrying no payload,
and JIT provisioning happening exactly once.
Two configuration problems found by running it:
- appsettings.json carried empty-string placeholders for the connection string and OIDC
authority. Under minimal hosting those beat anything a test registers via
ConfigureAppConfiguration, because Program.cs adds its own sources after that callback
runs. Removed them outright — an empty placeholder turns "not configured" into
"configured as empty", which defeats failing fast. Tests now use DODOSSH_ environment
variables, which Program.cs adds last.
- My first fix for minting an expired test token derived notBefore from the expiry, which
put nbf fourteen minutes in the future for normal tokens and made every valid token 401.
It needs the earlier of now-1min and exp-1min.
Verified: 0 warnings on a clean rebuild, 173 tests pass (up from 146), format clean.