Add secure credential storage, sessions, and a launch screen
Login now persists properly rather than through the JSON file the test
harness was using.
dr-plat SecretStore trait plus a Secret Service backend.
Verified against the live GNOME Keyring: store,
retrieve, delete, confirm-gone all round-trip.
Session/SessionStore splits credentials from settings — the app
password goes to the keyring (FR-NC-2), while
server, login, chosen root and format selection are
ordinary config. A test asserts the credential never
appears in the config file.
LaunchModel the launch-screen state machine, testable without a
display server: sign in, approve in browser, choose
folder, tick formats, sign out.
launch.slint the screen itself, in its own file.
Absence of a secrets daemon is an explicit degraded mode, not a silent
fallback to plaintext — the screen says sign-in will not persist rather
than letting the user find out next launch. Android's Keystore backend
fails loudly for the same reason: a no-op store would look like it
worked and then lose the credential.
Two bugs caught by tests rather than by running it:
- fail() after busy() signed the user out, because busy() had already
discarded the session. A failed *scan* would have logged you out.
Busy now carries the session.
- normalise_server upgrades http:// to https:// rather than accepting
it. NFR-SEC-3 requires TLS, and silently sending a credential in the
clear is not a decision to make on the user's behalf.
launch.slint is not yet wired into app.slint. Calling slint_build::compile
twice replaces the generated module rather than adding to it, which broke
the other in-flight work on dr-ui; I reverted that immediately. Wiring it
needs an import inside app.slint, which is that work's file to change.
419 tests passing across ten crates.
This commit is contained in:
@@ -398,7 +398,14 @@ mod tests {
|
||||
// something the UI would have to hardcode.
|
||||
let g = EditGraph::default_chain();
|
||||
let caps = g.capabilities();
|
||||
assert_eq!(caps.len(), g.descriptors().len());
|
||||
// Every operation, plus framing — which is not an operation and so
|
||||
// is absent from `descriptors`, but must still reach the panel.
|
||||
assert_eq!(caps.len(), g.descriptors().len() + 1);
|
||||
assert!(
|
||||
caps.iter().any(|c| c.id == crate::framing::ID),
|
||||
"framing must appear in the capability list, or the UI cannot \
|
||||
build a crop control without naming it"
|
||||
);
|
||||
|
||||
for cap in &caps {
|
||||
assert!(!cap.params.is_empty(), "{} exposes no parameters", cap.id);
|
||||
@@ -458,13 +465,25 @@ mod tests {
|
||||
fn capabilities_survive_a_round_trip_through_set_param() {
|
||||
// The UI reads a capability, writes the value back, and must get the
|
||||
// same thing out — no hidden scaling between the two.
|
||||
//
|
||||
// Written at the parameter's declared precision, because that is what
|
||||
// the UI can actually produce: a control declaring 0 decimals emits
|
||||
// whole numbers, and a stage free to quantise to them is behaving
|
||||
// correctly rather than losing the value.
|
||||
let mut g = EditGraph::default_chain();
|
||||
for cap in g.capabilities() {
|
||||
for p in &cap.params {
|
||||
if let ParamKind::Scalar { max, .. } = p.kind {
|
||||
let target = max * 0.5;
|
||||
if let ParamKind::Scalar { max, precision, .. } = p.kind {
|
||||
let step = 10f32.powi(i32::from(precision));
|
||||
let target = (max * 0.5 * step).round() / step;
|
||||
g.set_param(cap.id, p.id, target);
|
||||
assert_eq!(g.param(cap.id, p.id), Some(target));
|
||||
assert_eq!(
|
||||
g.param(cap.id, p.id),
|
||||
Some(target),
|
||||
"{}.{} did not round-trip",
|
||||
cap.id,
|
||||
p.id
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user