Files
pdfcraft/crates/ui-egui/tests/it/trust_signer.rs
T
aiongg 64e0868b84 feat(ui): menus from craft-menu, native on macOS (#4752)
* feat(ui): menus from craft-menu, native on macOS

One menu definition (crates/ui-egui/src/menus.rs) built from the command
registry drives the native macOS menu bar (craft-menu's muda backend) and
an in-window egui bar on Windows, Linux and the web, which takes the place
of the "Menu" button at the start of the top bar.

- File, Edit, View, Sign, Window and Help, with separators and submenus.
  Edit gains Cut, Paste, Delete and Select All (they act as their keys
  do) and a Pages submenu; View gains a Tools submenu built from the All
  tools groups; Sign holds Fill & Sign and the certificate commands;
  Window lists the open documents.
- Toggles are check items and the page layouts, side panels and themes
  radio items, with their state from the app.
- File > Open Recent lists the recent files (file.open_recent with an
  index).
- A menu click and its shortcut run the same dispatch, including the
  form-field deferral (#166) and macOS Cmd+W closing the window (#869).
  Dialogs and the palette grey the menus through craft-menu's gate.
- Quit goes through craft-quit, so the unsaved-changes prompt runs.
- craft-ui, craft-quit, craft-menu and craft-keymap pinned to
  storytold/craft-libs 70ade1fb.

Menu state and dispatch follow #2610 by @hajdukste; the menu tree and the
view and panel items follow #3870 by @RivanJarjes.

* test(ui): the menu model, native dispatch and the in-window bar

- Unit tests for the menu definition: every registry menu command is
  listed once with its registry shortcut; check and radio items follow
  the app; a dialog greys document commands and drops their clicks; the
  recent files and open documents are listed once each; a native menu
  click (through a fake craft-menu backend) runs its command once.
- The kittests that opened the old "Menu" button use the menu bar, and
  the ones that look up a label the bar's titles share (Edit, File,
  Sign) skip the bar.
- The in-window bar takes its titles' width, so the mode tabs, search
  and buttons keep their place in the top bar.
- Edit menu clicks that stand for keys (Copy, Cut, Delete, Select All)
  reach this frame's input from `logic`, where test hosts see them too.

* test(ui): the language switch test opens File in the menu bar

Translated titles and rows carry their mnemonic ("ファイル(F)", "開く(O)…").

* feat(ui): the menu bars run Quit themselves, the bar shrinks to its titles, and AppKit's window list stays off

craft-libs main edc41f6 (craft-libs#354): MenuBarUi runs Quit and Minimize, shrink_to_fit sizes the in-window bar, Key::parse reads the registry's arrow and minus glyphs, and mac::install_with(.., window_list(false)) keeps the Window menu to PdfCraft's own document list.

* fix(ui): the menu command filter is an if, for clippy

* test(ui): drop a needless borrow in the edit-tab helper

* test(ui): two tests follow the menu bar

The tests were wrong, not the app. The Preferences language test clicked the old Menu button; it opens Edit in the menu bar and its Preferences row. The prepare Actions test counted the Edit labels: the narrower menu bar leaves room for the Edit tab, so the rows' Edit buttons come third and fourth.
2026-10-26 00:57:46 -04:00

89 lines
4.2 KiB
Rust

//! The Signatures panel's one-click "Add to trusted certificates" must store the signer shown in
//! the row, not the chain top the document embedded in the CMS (audit v3-098). Signing with a
//! certificate whose CMS carries its issuer chain gives a signature whose `chain.last()` (the
//! self-signed root) differs from its `certificate` (the signer); the click must trust the signer.
use crate::support;
use egui_kittest::Harness;
use egui_kittest::kittest::Queryable;
use pdfcraft_engine::SignatureStatus;
use pdfcraft_ui_egui::{PdfCraftApp, SignStep};
const FIXTURE: &[u8] = b"%PDF-1.7
1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj
2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj
3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 300 200] /Contents 4 0 R /Resources << /Font << /F1 5 0 R >> >> >> endobj
4 0 obj << /Length 45 >> stream
BT /F1 14 Tf 20 150 Td (Please sign below) Tj ET
endstream endobj
5 0 obj << /Type /Font /Subtype /Type1 /BaseFont /Helvetica >> endobj
trailer << /Root 1 0 R >>
%%EOF";
fn dir() -> std::path::PathBuf {
static NEXT: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
let n = NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let d = std::env::temp_dir().join(format!("pdfcraft-trust-signer-{}-{n}", std::process::id()));
let _ = std::fs::remove_dir_all(&d);
std::fs::create_dir_all(&d).unwrap();
d
}
fn harness(dir: std::path::PathBuf) -> Harness<'static, PdfCraftApp> {
let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(move |_cc| {
let mut app = PdfCraftApp::new();
app.set_option("language", "en").unwrap();
app.open_bytes("contract.pdf", None, FIXTURE.to_vec()).expect("opens");
app.set_option("left", "closed").unwrap();
app.export_dir_override = Some(dir.to_string_lossy().into_owned());
app.save_override = Some(dir.join("contract_signed.pdf").to_string_lossy().into_owned());
app
});
h.run_steps(6);
h
}
#[test]
fn one_click_trust_stores_the_displayed_signer_not_the_embedded_root() {
let dir = dir();
let mut h = harness(dir);
// Sign with chain.p12, whose CMS embeds the signer's issuer chain (signer + self-signed root).
let p12 = concat!(env!("CARGO_MANIFEST_DIR"), "/../sign/tests/data/chain.p12");
let id = pdfcraft_engine::sign::pkcs12::open(&std::fs::read(p12).unwrap(), "test").unwrap();
let entry = h.state_mut().add_digital_id(p12, &id.certificate);
h.state_mut().start_signing(0, None, None, None);
{
let d = h.state_mut().sign_draft.as_mut().unwrap();
d.selected = Some(entry);
d.step = SignStep::SignAs;
d.password = "test".into();
}
h.run_steps(2);
support::outside_menus(&h, "Sign").click();
h.run_steps(6);
// The signature validates as Unknown and carries a two-certificate chain whose top (the root)
// is a different certificate from the displayed signer.
let (signer, root) = {
let s = h.state();
let sig = s.session.get(s.views[0].id).unwrap().signatures.iter().find(|x| x.signed).cloned().expect("a signed field");
assert_eq!(sig.status, SignatureStatus::Unknown, "untrusted before the click");
assert_eq!(sig.chain.len(), 2, "the CMS embeds the signer and its root: {:?}", sig.chain.len());
let signer = sig.certificate.clone().expect("a signer certificate");
let root = sig.chain.last().cloned().expect("a chain top");
assert_ne!(signer.raw, root.raw, "the embedded chain top must differ from the signer for this test to be meaningful");
(signer, root)
};
// Expand the signature row and use the one-click trust button.
h.get_by_label_contains("Signed by").click();
h.run_steps(2);
h.get_by_label("Add to trusted certificates").click();
h.run_steps(3);
// The trust store holds exactly the displayed signer, never the document-supplied root.
let trusted = h.state().session.trusted_certificates().to_vec();
assert!(trusted.iter().any(|c| c.raw == signer.raw), "the displayed signer should be trusted");
assert!(!trusted.iter().any(|c| c.raw == root.raw), "the embedded root must not be trusted by the one-click action");
}