Skip to content

Commit 4a9be9b

Browse files
committed
test: add comprehensive test coverage for input validation handlers
1 parent f37a0ac commit 4a9be9b

5 files changed

Lines changed: 147 additions & 4 deletions

File tree

src/helpers/events/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ impl std::fmt::Display for SubscriptionName {
4949
}
5050
}
5151

52+
#[allow(dead_code)]
5253
#[derive(Debug, Clone, Builder)]
5354
#[builder(setter(into))]
5455
pub struct SubscribeConfig {

src/helpers/events/subscribe.rs

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,30 @@
11
use super::*;
22

3+
#[derive(Debug, Clone, Default, Builder)]
4+
#[builder(setter(into))]
5+
pub struct SubscribeConfig {
6+
#[builder(default)]
7+
target: Option<String>,
8+
#[builder(default)]
9+
event_types: Vec<String>,
10+
#[builder(default)]
11+
project: Option<ProjectId>,
12+
#[builder(default)]
13+
subscription: Option<SubscriptionName>,
14+
#[builder(default = "10")]
15+
max_messages: u32,
16+
#[builder(default = "2")]
17+
poll_interval: u64,
18+
#[builder(default)]
19+
once: bool,
20+
#[builder(default)]
21+
cleanup: bool,
22+
#[builder(default)]
23+
no_ack: bool,
24+
#[builder(default)]
25+
output_dir: Option<String>,
26+
}
27+
328
fn parse_subscribe_args(matches: &ArgMatches) -> Result<SubscribeConfig, GwsError> {
429
let mut builder = SubscribeConfigBuilder::default();
530

@@ -97,7 +122,9 @@ pub(super) async fn handle_subscribe(
97122
} else {
98123
// Full setup: create Pub/Sub topic + subscription + Workspace Events subscription
99124
let target = config.target.clone().unwrap();
100-
let project = config.project.clone().unwrap().0;
125+
let project =
126+
crate::helpers::validate_resource_name(&config.project.clone().unwrap().0)?
127+
.to_string();
101128
let event_types_str: Vec<&str> =
102129
config.event_types.iter().map(|s| s.as_str()).collect();
103130

@@ -515,6 +542,15 @@ mod tests {
515542
cmd.try_get_matches_from(args).unwrap()
516543
}
517544

545+
#[test]
546+
fn test_parse_subscribe_args_invalid_output_dir() {
547+
let matches = make_matches_subscribe(&["test", "--output-dir", "../../etc"]);
548+
let result = parse_subscribe_args(&matches);
549+
assert!(result.is_err());
550+
let msg = result.unwrap_err().to_string();
551+
assert!(msg.contains("outside the current directory"));
552+
}
553+
518554
#[test]
519555
fn test_parse_subscribe_args() {
520556
let matches = make_matches_subscribe(&[

src/helpers/gmail/watch.rs

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,9 @@ pub(super) async fn handle_watch(
3737

3838
let suffix = format!("{:08x}", rand::random::<u32>());
3939
let topic = if let Some(ref t) = config.topic {
40-
t.clone()
40+
crate::helpers::validate_resource_name(t)?.to_string()
4141
} else {
42+
let project = crate::helpers::validate_resource_name(&project)?;
4243
let t = format!("projects/{project}/topics/gws-gmail-watch-{suffix}");
4344
// Create Pub/Sub topic
4445
eprintln!("Creating Pub/Sub topic: {t}");
@@ -97,6 +98,7 @@ pub(super) async fn handle_watch(
9798
t
9899
};
99100

101+
let project = crate::helpers::validate_resource_name(&project)?;
100102
let sub = format!("projects/{project}/subscriptions/gws-gmail-watch-{suffix}");
101103

102104
// 3. Create Pub/Sub subscription
@@ -499,7 +501,7 @@ fn extract_message_ids_from_history(history_body: &Value) -> Vec<String> {
499501
result
500502
}
501503

502-
#[derive(Clone)]
504+
#[derive(Debug, Clone)]
503505
struct WatchConfig {
504506
project: Option<String>,
505507
subscription: Option<String>,
@@ -630,7 +632,25 @@ mod tests {
630632
}
631633

632634
#[test]
633-
fn test_parse_watch_args() {
635+
fn test_parse_watch_args_invalid_format() {
636+
let matches = make_matches_watch(&["test", "--msg-format", "invalid-format"]);
637+
let result = parse_watch_args(&matches);
638+
assert!(result.is_err());
639+
let msg = result.unwrap_err().to_string();
640+
assert!(msg.contains("Invalid message format"));
641+
}
642+
643+
#[test]
644+
fn test_parse_watch_args_invalid_output_dir() {
645+
let matches = make_matches_watch(&["test", "--output-dir", "../../etc"]);
646+
let result = parse_watch_args(&matches);
647+
assert!(result.is_err());
648+
let msg = result.unwrap_err().to_string();
649+
assert!(msg.contains("outside the current directory"));
650+
}
651+
652+
#[test]
653+
fn test_parse_watch_args_full() {
634654
let matches = make_matches_watch(&[
635655
"test",
636656
"--project",

src/helpers/workflows.rs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -719,4 +719,61 @@ mod tests {
719719
fn test_helper_only() {
720720
assert!(WorkflowHelper.helper_only());
721721
}
722+
723+
#[test]
724+
fn test_epoch_to_rfc3339() {
725+
assert_eq!(epoch_to_rfc3339(0), "1970-01-01T00:00:00+00:00");
726+
assert_eq!(epoch_to_rfc3339(1710000000), "2024-03-09T16:00:00+00:00");
727+
}
728+
729+
#[test]
730+
fn test_build_standup_report_cmd() {
731+
let cmd = build_standup_report_cmd();
732+
assert_eq!(cmd.get_name(), "+standup-report");
733+
}
734+
735+
#[test]
736+
fn test_build_meeting_prep_cmd() {
737+
let cmd = build_meeting_prep_cmd();
738+
assert_eq!(cmd.get_name(), "+meeting-prep");
739+
}
740+
741+
#[test]
742+
fn test_build_email_to_task_cmd() {
743+
let cmd = build_email_to_task_cmd();
744+
assert_eq!(cmd.get_name(), "+email-to-task");
745+
746+
// message-id is required
747+
let args = cmd
748+
.clone()
749+
.try_get_matches_from(vec!["+email-to-task", "--message-id", "123"]);
750+
assert!(args.is_ok());
751+
752+
let args_err = cmd.try_get_matches_from(vec!["+email-to-task"]);
753+
assert!(args_err.is_err());
754+
}
755+
756+
#[test]
757+
fn test_build_weekly_digest_cmd() {
758+
let cmd = build_weekly_digest_cmd();
759+
assert_eq!(cmd.get_name(), "+weekly-digest");
760+
}
761+
762+
#[test]
763+
fn test_build_file_announce_cmd() {
764+
let cmd = build_file_announce_cmd();
765+
assert_eq!(cmd.get_name(), "+file-announce");
766+
767+
let args = cmd.clone().try_get_matches_from(vec![
768+
"+file-announce",
769+
"--file-id",
770+
"123",
771+
"--space",
772+
"spaces/test",
773+
]);
774+
assert!(args.is_ok());
775+
776+
let args_err = cmd.try_get_matches_from(vec!["+file-announce"]);
777+
assert!(args_err.is_err());
778+
}
722779
}

src/validate.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,35 @@ mod tests {
243243
assert!(result.is_ok(), "expected Ok, got: {result:?}");
244244
}
245245

246+
#[test]
247+
#[serial]
248+
fn test_output_dir_rejects_symlink_traversal() {
249+
let dir = tempdir().unwrap();
250+
let canonical_dir = dir.path().canonicalize().unwrap();
251+
252+
// Create a directory inside the tempdir
253+
let allowed_dir = canonical_dir.join("allowed");
254+
fs::create_dir(&allowed_dir).unwrap();
255+
256+
// Create a symlink pointing OUTSIDE the tempdir (e.g. to /tmp)
257+
let symlink_path = canonical_dir.join("sneaky_link");
258+
#[cfg(unix)]
259+
std::os::unix::fs::symlink("/tmp", &symlink_path).unwrap();
260+
#[cfg(windows)]
261+
return; // Skip on Windows due to privilege requirements for symlinks
262+
263+
let saved_cwd = std::env::current_dir().unwrap();
264+
std::env::set_current_dir(&canonical_dir).unwrap();
265+
266+
// Try to validate the symlink resolving outside CWD
267+
let result = validate_safe_output_dir("sneaky_link");
268+
std::env::set_current_dir(&saved_cwd).unwrap();
269+
270+
assert!(result.is_err());
271+
let msg = result.unwrap_err().to_string();
272+
assert!(msg.contains("outside the current directory"), "got: {msg}");
273+
}
274+
246275
#[test]
247276
#[serial]
248277
fn test_output_dir_rejects_traversal() {

0 commit comments

Comments
 (0)