chore(v1.1.5): version bumps, CI workflow, schema-snapshot un-ignore
- Workspace 1.1.4 → 1.1.5; SDK 1.5 → 1.6; dashboard 0.10.0 → 0.11.0. - CHANGELOG v1.1.5 entry; CLAUDE.md runtime-config table gains PICLOUD_FILES_ROOT + PICLOUD_FILES_MAX_FILE_SIZE_BYTES. - schema_snapshot test: drop #[ignore] + #[sqlx::test]; run against DATABASE_URL when set, skip cleanly when absent. Re-blessed golden picks up files / files_trigger_details / pubsub_trigger_details, the two widened CHECKs, and the pubsub partial index. - First CI workflow (.github/workflows/ci.yml): postgres:15 service + fmt + clippy + cargo test --workspace; separate dashboard check job. - Add files/pubsub admin-trigger reject-coverage tests (module + cross-app + bad-pattern), mirroring the v1.1.3 regression set. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1739,4 +1739,258 @@ mod tests {
|
||||
.expect("endpoint target should succeed");
|
||||
assert_eq!(status, StatusCode::CREATED);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// v1.1.5: files + pubsub trigger create (Layout-E reject coverage).
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
fn files_req(script_id: ScriptId, glob: &str) -> CreateFilesTriggerRequest {
|
||||
CreateFilesTriggerRequest {
|
||||
script_id,
|
||||
collection_glob: glob.into(),
|
||||
ops: vec![FilesEventOp::Create],
|
||||
dispatch_mode: TriggerDispatchMode::Async,
|
||||
retry_max_attempts: None,
|
||||
retry_backoff: None,
|
||||
retry_base_ms: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn files_trigger_create_succeeds() {
|
||||
let app_id = AppId::new();
|
||||
let script_id = ScriptId::new();
|
||||
let state = state_with_endpoint(Arc::new(AlwaysAllowAuthzRepo), app_id, script_id);
|
||||
let (status, Json(trigger)) = create_files_trigger(
|
||||
State(state),
|
||||
Extension(member_principal()),
|
||||
Path(app_id),
|
||||
Json(files_req(script_id, "avatars")),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(status, StatusCode::CREATED);
|
||||
assert!(matches!(
|
||||
trigger.kind,
|
||||
crate::trigger_repo::TriggerKind::Files
|
||||
));
|
||||
match trigger.details {
|
||||
TriggerDetails::Files {
|
||||
collection_glob,
|
||||
ops,
|
||||
} => {
|
||||
assert_eq!(collection_glob, "avatars");
|
||||
assert_eq!(ops, vec![FilesEventOp::Create]);
|
||||
}
|
||||
other => panic!("expected Files details, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn files_trigger_empty_glob_rejected() {
|
||||
let app_id = AppId::new();
|
||||
let state = state_with(Arc::new(AlwaysAllowAuthzRepo), app_id);
|
||||
let res = create_files_trigger(
|
||||
State(state),
|
||||
Extension(member_principal()),
|
||||
Path(app_id),
|
||||
Json(files_req(ScriptId::new(), " ")),
|
||||
)
|
||||
.await;
|
||||
assert!(matches!(
|
||||
res.expect_err("empty glob"),
|
||||
TriggersApiError::Invalid(_)
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn files_trigger_rejects_module_target() {
|
||||
let app_id = AppId::new();
|
||||
let script_id = ScriptId::new();
|
||||
let state = TriggersState {
|
||||
triggers: Arc::new(InMemoryTriggerRepo::default()),
|
||||
apps: InMemoryAppRepo::with(app_id),
|
||||
authz: Arc::new(AlwaysAllowAuthzRepo),
|
||||
scripts: InMemoryScriptRepo::with_module(app_id, script_id),
|
||||
config: TriggerConfig::conservative(),
|
||||
};
|
||||
let res = create_files_trigger(
|
||||
State(state),
|
||||
Extension(member_principal()),
|
||||
Path(app_id),
|
||||
Json(files_req(script_id, "avatars")),
|
||||
)
|
||||
.await;
|
||||
let msg = match res.expect_err("module rejected") {
|
||||
TriggersApiError::Invalid(m) => m,
|
||||
other => panic!("expected Invalid, got {other:?}"),
|
||||
};
|
||||
assert!(msg.to_lowercase().contains("module"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn files_trigger_rejects_cross_app_script() {
|
||||
let app_a = AppId::new();
|
||||
let app_b = AppId::new();
|
||||
let script_id = ScriptId::new();
|
||||
let state = TriggersState {
|
||||
triggers: Arc::new(InMemoryTriggerRepo::default()),
|
||||
apps: InMemoryAppRepo::with(app_a),
|
||||
authz: Arc::new(AlwaysAllowAuthzRepo),
|
||||
scripts: InMemoryScriptRepo::with_endpoint(app_b, script_id),
|
||||
config: TriggerConfig::conservative(),
|
||||
};
|
||||
let res = create_files_trigger(
|
||||
State(state),
|
||||
Extension(member_principal()),
|
||||
Path(app_a),
|
||||
Json(files_req(script_id, "avatars")),
|
||||
)
|
||||
.await;
|
||||
let msg = match res.expect_err("cross-app rejected") {
|
||||
TriggersApiError::Invalid(m) => m,
|
||||
other => panic!("expected Invalid, got {other:?}"),
|
||||
};
|
||||
assert!(msg.to_lowercase().contains("does not belong"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn files_trigger_member_without_role_is_forbidden() {
|
||||
let app_id = AppId::new();
|
||||
let state = state_with(Arc::new(AlwaysDenyAuthzRepo), app_id);
|
||||
let res = create_files_trigger(
|
||||
State(state),
|
||||
Extension(member_principal()),
|
||||
Path(app_id),
|
||||
Json(files_req(ScriptId::new(), "avatars")),
|
||||
)
|
||||
.await;
|
||||
assert!(matches!(
|
||||
res.expect_err("forbidden"),
|
||||
TriggersApiError::Forbidden
|
||||
));
|
||||
}
|
||||
|
||||
fn pubsub_req(script_id: ScriptId, pattern: &str) -> CreatePubsubTriggerRequest {
|
||||
CreatePubsubTriggerRequest {
|
||||
script_id,
|
||||
topic_pattern: pattern.into(),
|
||||
dispatch_mode: TriggerDispatchMode::Async,
|
||||
retry_max_attempts: None,
|
||||
retry_backoff: None,
|
||||
retry_base_ms: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn pubsub_trigger_create_succeeds() {
|
||||
let app_id = AppId::new();
|
||||
let script_id = ScriptId::new();
|
||||
let state = state_with_endpoint(Arc::new(AlwaysAllowAuthzRepo), app_id, script_id);
|
||||
let (status, Json(trigger)) = create_pubsub_trigger(
|
||||
State(state),
|
||||
Extension(member_principal()),
|
||||
Path(app_id),
|
||||
Json(pubsub_req(script_id, "user.*")),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(status, StatusCode::CREATED);
|
||||
match trigger.details {
|
||||
TriggerDetails::Pubsub { topic_pattern } => assert_eq!(topic_pattern, "user.*"),
|
||||
other => panic!("expected Pubsub details, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn pubsub_trigger_rejects_bad_pattern() {
|
||||
let app_id = AppId::new();
|
||||
let script_id = ScriptId::new();
|
||||
let state = state_with_endpoint(Arc::new(AlwaysAllowAuthzRepo), app_id, script_id);
|
||||
for bad in ["*.created", "a.*.b", "**"] {
|
||||
let res = create_pubsub_trigger(
|
||||
State(state.clone()),
|
||||
Extension(member_principal()),
|
||||
Path(app_id),
|
||||
Json(pubsub_req(script_id, bad)),
|
||||
)
|
||||
.await;
|
||||
let msg = match res.expect_err("bad pattern") {
|
||||
TriggersApiError::Invalid(m) => m,
|
||||
other => panic!("expected Invalid, got {other:?}"),
|
||||
};
|
||||
assert!(
|
||||
msg.contains("unsupported pubsub topic pattern"),
|
||||
"got {msg} for {bad}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn pubsub_trigger_rejects_module_target() {
|
||||
let app_id = AppId::new();
|
||||
let script_id = ScriptId::new();
|
||||
let state = TriggersState {
|
||||
triggers: Arc::new(InMemoryTriggerRepo::default()),
|
||||
apps: InMemoryAppRepo::with(app_id),
|
||||
authz: Arc::new(AlwaysAllowAuthzRepo),
|
||||
scripts: InMemoryScriptRepo::with_module(app_id, script_id),
|
||||
config: TriggerConfig::conservative(),
|
||||
};
|
||||
let res = create_pubsub_trigger(
|
||||
State(state),
|
||||
Extension(member_principal()),
|
||||
Path(app_id),
|
||||
Json(pubsub_req(script_id, "user.*")),
|
||||
)
|
||||
.await;
|
||||
let msg = match res.expect_err("module rejected") {
|
||||
TriggersApiError::Invalid(m) => m,
|
||||
other => panic!("expected Invalid, got {other:?}"),
|
||||
};
|
||||
assert!(msg.to_lowercase().contains("module"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn pubsub_trigger_rejects_cross_app_script() {
|
||||
let app_a = AppId::new();
|
||||
let app_b = AppId::new();
|
||||
let script_id = ScriptId::new();
|
||||
let state = TriggersState {
|
||||
triggers: Arc::new(InMemoryTriggerRepo::default()),
|
||||
apps: InMemoryAppRepo::with(app_a),
|
||||
authz: Arc::new(AlwaysAllowAuthzRepo),
|
||||
scripts: InMemoryScriptRepo::with_endpoint(app_b, script_id),
|
||||
config: TriggerConfig::conservative(),
|
||||
};
|
||||
let res = create_pubsub_trigger(
|
||||
State(state),
|
||||
Extension(member_principal()),
|
||||
Path(app_a),
|
||||
Json(pubsub_req(script_id, "user.*")),
|
||||
)
|
||||
.await;
|
||||
let msg = match res.expect_err("cross-app rejected") {
|
||||
TriggersApiError::Invalid(m) => m,
|
||||
other => panic!("expected Invalid, got {other:?}"),
|
||||
};
|
||||
assert!(msg.to_lowercase().contains("does not belong"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn pubsub_trigger_member_without_role_is_forbidden() {
|
||||
let app_id = AppId::new();
|
||||
let state = state_with(Arc::new(AlwaysDenyAuthzRepo), app_id);
|
||||
let res = create_pubsub_trigger(
|
||||
State(state),
|
||||
Extension(member_principal()),
|
||||
Path(app_id),
|
||||
Json(pubsub_req(ScriptId::new(), "user.*")),
|
||||
)
|
||||
.await;
|
||||
assert!(matches!(
|
||||
res.expect_err("forbidden"),
|
||||
TriggersApiError::Forbidden
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user