feat(kanban): authorize canonical sync reads
req: sync/007 req: auth/005 req: security/004
This commit is contained in:
@@ -598,6 +598,129 @@ async fn account_partition_hides_replay_and_export_until_owner_returns() -> WebD
|
||||
result.and(quit)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn canonical_snapshot_and_history_are_tenant_scoped() -> WebDriverResult<()> {
|
||||
// test req: sync/007 req: security/004 req: auth/005
|
||||
let app_port = available_port();
|
||||
let app_addr = format!("127.0.0.1:{app_port}");
|
||||
let mut app_command = Command::new(env!("CARGO_BIN_EXE_hemx-kanban-example"));
|
||||
app_command
|
||||
.env("HEMX_KANBAN_ADDR", &app_addr)
|
||||
.env(
|
||||
"HEMX_KANBAN_SESSION_ALICE_ALPHA_EDITOR",
|
||||
"test-token-alice-alpha-editor",
|
||||
)
|
||||
.env(
|
||||
"HEMX_KANBAN_SESSION_BOB_ALPHA_VIEWER",
|
||||
"test-token-bob-alpha-viewer",
|
||||
)
|
||||
.env(
|
||||
"HEMX_KANBAN_SESSION_CAROL_BETA_EDITOR",
|
||||
"test-token-carol-beta-editor",
|
||||
);
|
||||
let _app = TestProcess::start(app_command, "hemx-kanban", &app_addr, STARTUP_TIMEOUT)
|
||||
.expect("start ready hemx-kanban");
|
||||
|
||||
let webdriver_port = available_port();
|
||||
let webdriver_addr = format!("127.0.0.1:{webdriver_port}");
|
||||
let mut webdriver = Command::new("geckodriver");
|
||||
webdriver.arg("--port").arg(webdriver_port.to_string());
|
||||
let _webdriver = TestProcess::start(webdriver, "geckodriver", &webdriver_addr, STARTUP_TIMEOUT)
|
||||
.expect("start ready geckodriver");
|
||||
let mut caps = DesiredCapabilities::firefox();
|
||||
caps.set_headless()?;
|
||||
let driver = WebDriver::new(&format!("http://{webdriver_addr}"), caps).await?;
|
||||
|
||||
let result = async {
|
||||
driver.goto(&format!("http://{app_addr}/")).await?;
|
||||
let proof = driver
|
||||
.execute_async(
|
||||
r#"
|
||||
const done = arguments[arguments.length - 1];
|
||||
const session = (token) => { document.cookie = `hemx_kanban_session=${token}; Path=/; SameSite=Strict`; };
|
||||
const command = (id, card) => fetch(`/sync/commands?command_id=${encodeURIComponent(id)}&card_id=${card}&column=done`, { method: 'POST' });
|
||||
(async () => {
|
||||
session('test-token-alice-alpha-editor');
|
||||
const aliceWrite = await command('alice-history:1', 1);
|
||||
session('test-token-carol-beta-editor');
|
||||
const carolWrite = await command('carol-history:1', 2);
|
||||
const betaSnapshotResponse = await fetch('/sync/snapshot', { cache: 'no-store' });
|
||||
const betaSnapshot = await betaSnapshotResponse.json();
|
||||
const betaHistoryResponse = await fetch('/sync/acknowledgements?after=0', { headers: { Accept: 'text/event-stream' }, cache: 'no-store' });
|
||||
const betaHistory = await betaHistoryResponse.text();
|
||||
session('test-token-bob-alpha-viewer');
|
||||
const alphaSnapshotResponse = await fetch('/sync/snapshot', { cache: 'no-store' });
|
||||
const alphaSnapshot = await alphaSnapshotResponse.json();
|
||||
const alphaHistoryResponse = await fetch('/sync/acknowledgements?after=0', { headers: { Accept: 'text/event-stream' }, cache: 'no-store' });
|
||||
const alphaHistory = await alphaHistoryResponse.text();
|
||||
document.cookie = 'hemx_kanban_session=; Path=/; Max-Age=0; SameSite=Strict';
|
||||
const signedOutSnapshot = await fetch('/sync/snapshot', { cache: 'no-store' });
|
||||
const signedOutHistory = await fetch('/sync/acknowledgements?after=0', { headers: { Accept: 'text/event-stream' }, cache: 'no-store' });
|
||||
done({
|
||||
aliceWrite: aliceWrite.status,
|
||||
carolWrite: carolWrite.status,
|
||||
betaSnapshotStatus: betaSnapshotResponse.status,
|
||||
betaSnapshot,
|
||||
betaHistoryStatus: betaHistoryResponse.status,
|
||||
betaHistory,
|
||||
alphaSnapshotStatus: alphaSnapshotResponse.status,
|
||||
alphaSnapshot,
|
||||
alphaHistoryStatus: alphaHistoryResponse.status,
|
||||
alphaHistory,
|
||||
signedOutSnapshotStatus: signedOutSnapshot.status,
|
||||
signedOutHistoryStatus: signedOutHistory.status,
|
||||
});
|
||||
})().catch((error) => done({ error: String(error), stack: error.stack }));
|
||||
"#,
|
||||
Vec::new(),
|
||||
)
|
||||
.await?
|
||||
.json()
|
||||
.clone();
|
||||
assert!(proof.get("error").is_none(), "browser proof failed: {proof}");
|
||||
assert_eq!(proof["aliceWrite"], 200);
|
||||
assert_eq!(proof["carolWrite"], 200);
|
||||
assert_eq!(proof["betaSnapshotStatus"], 200);
|
||||
assert_eq!(
|
||||
proof["betaSnapshot"],
|
||||
serde_json::json!({
|
||||
"schemaVersion": 1,
|
||||
"serverSequence": 1,
|
||||
"cards": [{ "id": 2, "column": "done" }]
|
||||
})
|
||||
);
|
||||
assert_eq!(proof["betaHistoryStatus"], 200);
|
||||
let beta_history = proof["betaHistory"].as_str().unwrap();
|
||||
assert!(beta_history.contains("carol-history:1"));
|
||||
assert!(!beta_history.contains("alice-history:1"));
|
||||
assert!(!beta_history.contains("\"cardId\":1"));
|
||||
|
||||
assert_eq!(proof["alphaSnapshotStatus"], 200);
|
||||
assert_eq!(
|
||||
proof["alphaSnapshot"],
|
||||
serde_json::json!({
|
||||
"schemaVersion": 1,
|
||||
"serverSequence": 1,
|
||||
"cards": [
|
||||
{ "id": 1, "column": "done" },
|
||||
{ "id": 3, "column": "done" }
|
||||
]
|
||||
})
|
||||
);
|
||||
assert_eq!(proof["alphaHistoryStatus"], 200);
|
||||
let alpha_history = proof["alphaHistory"].as_str().unwrap();
|
||||
assert!(alpha_history.contains("alice-history:1"));
|
||||
assert!(!alpha_history.contains("carol-history:1"));
|
||||
assert!(!alpha_history.contains("\"cardId\":2"));
|
||||
assert_eq!(proof["signedOutSnapshotStatus"], 401);
|
||||
assert_eq!(proof["signedOutHistoryStatus"], 401);
|
||||
Ok(())
|
||||
}
|
||||
.await;
|
||||
let quit = driver.quit().await;
|
||||
result.and(quit)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn schema_upgrade_preserves_queued_order_and_local_intent() -> WebDriverResult<()> {
|
||||
// test req: sync/004 req: sync/010 req: sync/014
|
||||
@@ -1592,7 +1715,8 @@ async fn canonical_acknowledgement_survives_server_restart() -> WebDriverResult<
|
||||
assert!(store.is_file(), "server did not materialize sync store");
|
||||
let persisted = fs::read_to_string(&store).expect("read sync store");
|
||||
assert!(persisted.contains("restart-proof:1"));
|
||||
assert!(persisted.contains("\"schemaVersion\": 1"));
|
||||
assert!(persisted.contains("\"schemaVersion\": 2"));
|
||||
assert!(persisted.contains("\"tenant\": \"demo\""));
|
||||
|
||||
drop(first_app);
|
||||
let mut second_app_command = Command::new(env!("CARGO_BIN_EXE_hemx-kanban-example"));
|
||||
|
||||
Reference in New Issue
Block a user