Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ tracing-subscriber = { version = "0.3.23", features = ["env-filter", "json"] }
uuid = { version = "1.23.4", features = ["serde", "v4"] }

[dev-dependencies]
aws-lc-rs = "1.17.0"
base64 = "0.22.1"
http-body = "1.0.1"
hyper = "1.10.1"
mockall = "0.15.0"
proptest = { version = "1.11.0", default-features = false, features = ["std"] }
4 changes: 3 additions & 1 deletion docs/config/.gitvote.yml
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ profiles:
#
# The percentage is calculated based on the number of votes in favor and the
# number of allowed voters (see allowed_voters field below for more details).
# It must be greater than 0 and not greater than 100.
pass_threshold: 50

# Allowed voters (optional)
Expand All @@ -103,7 +104,8 @@ profiles:
# considered allowed voters. Please note that this option only applies to
# the teams explicitly listed in `allowed_voters/teams`.
#
# Teams names must be provided without the organization prefix.
# Teams names must be provided without the organization prefix. Users are
# matched case-insensitively, as GitHub usernames are.
#
# allowed_voters:
# teams:
Expand Down
203 changes: 10 additions & 193 deletions src/cfg_repo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ use crate::github::{DynGH, File, TeamSlug, UserName};
/// Default configuration profile.
const DEFAULT_PROFILE: &str = "default";

/// Error message used when the pass threshold is not a valid percentage.
const ERR_INVALID_PASS_THRESHOLD: &str = "pass threshold must be greater than 0 and not greater than 100";

/// Error message used when teams are listed in the allowed voters section on a
/// repository that does not belong to an organization.
const ERR_TEAMS_NOT_ALLOWED: &str = "teams in allowed voters can only be used in organizations";
Expand Down Expand Up @@ -127,6 +130,12 @@ impl CfgProfile {

/// Check if the configuration profile is valid.
fn validate(&self, is_org: bool) -> Result<()> {
// The pass threshold must be a percentage in the (0, 100] range. Written
// as a negated range check so that NaN values are rejected as well.
if !(self.pass_threshold > 0.0 && self.pass_threshold <= 100.0) {
bail!(ERR_INVALID_PASS_THRESHOLD);
}

// Only repositories that belong to some organization can use teams in
// the allowed voters configuration section.
if !is_org
Expand Down Expand Up @@ -177,196 +186,4 @@ pub(crate) enum CfgError {
}

#[cfg(test)]
mod tests {
use std::sync::Arc;

use futures::future;
use mockall::predicate::eq;

use crate::github::MockGH;
use crate::testutil::*;

use super::*;

#[test]
fn automation_rule_matches() {
let rule = AutomationRule {
patterns: vec!["*.md".to_string(), "file.txt".to_string()],
profile: "default".to_string(),
};
assert!(
rule.matches(&[File {
filename: "README.md".to_string()
}])
.unwrap()
);
assert!(
rule.matches(&[File {
filename: "path/file.txt".to_string()
}])
.unwrap()
);
}

#[test]
fn automation_rule_does_not_match() {
let rule = AutomationRule {
patterns: vec!["path/image.svg".to_string()],
profile: "default".to_string(),
};
assert!(
!rule
.matches(&[File {
filename: "README.md".to_string()
}])
.unwrap()
);
assert!(
!rule
.matches(&[File {
filename: "image.svg".to_string()
}])
.unwrap()
);
}

#[tokio::test]
async fn get_cfg_profile_config_not_found() {
let mut gh = MockGH::new();
gh.expect_get_config_file()
.with(eq(INST_ID), eq(OWNER), eq(REPO))
.times(1)
.returning(|_, _, _| Box::pin(future::ready(None)));
let gh = Arc::new(gh);

assert_eq!(
CfgProfile::get(gh, INST_ID, OWNER, OWNER_IS_ORG, REPO, None).await.unwrap_err(),
CfgError::ConfigNotFound
);
}

#[tokio::test]
async fn get_cfg_profile_invalid_config_invalid_yaml() {
let mut gh = MockGH::new();
gh.expect_get_config_file()
.with(eq(INST_ID), eq(OWNER), eq(REPO))
.times(1)
.returning(|_, _, _| Box::pin(future::ready(Some(get_test_invalid_config()))));
let gh = Arc::new(gh);

assert!(matches!(
CfgProfile::get(
gh,
INST_ID,
OWNER,
OWNER_IS_ORG,
REPO,
Some(PROFILE_NAME.to_string())
)
.await
.unwrap_err(),
CfgError::InvalidConfig(_)
));
}

#[tokio::test]
async fn get_cfg_profile_invalid_config_teams_owner_not_org() {
let mut gh = MockGH::new();
gh.expect_get_config_file()
.with(eq(INST_ID), eq(OWNER), eq(REPO))
.times(1)
.returning(|_, _, _| Box::pin(future::ready(Some(get_test_valid_config()))));
let gh = Arc::new(gh);

assert_eq!(
CfgProfile::get(
gh,
INST_ID,
OWNER,
!OWNER_IS_ORG,
REPO,
Some(PROFILE_NAME.to_string())
)
.await
.unwrap_err(),
CfgError::InvalidConfig(ERR_TEAMS_NOT_ALLOWED.to_string())
);
}

#[tokio::test]
async fn get_cfg_profile_profile_not_found() {
let mut gh = MockGH::new();
gh.expect_get_config_file()
.with(eq(INST_ID), eq(OWNER), eq(REPO))
.times(1)
.returning(|_, _, _| Box::pin(future::ready(Some(get_test_valid_config()))));
let gh = Arc::new(gh);

assert_eq!(
CfgProfile::get(
gh,
INST_ID,
OWNER,
OWNER_IS_ORG,
REPO,
Some("profile9".to_string())
)
.await
.unwrap_err(),
CfgError::ProfileNotFound
);
}

#[tokio::test]
async fn get_cfg_profile_default() {
let mut gh = MockGH::new();
gh.expect_get_config_file()
.with(eq(INST_ID), eq(OWNER), eq(REPO))
.times(1)
.returning(|_, _, _| Box::pin(future::ready(Some(get_test_valid_config()))));
let gh = Arc::new(gh);

assert_eq!(
CfgProfile::get(gh, INST_ID, OWNER, OWNER_IS_ORG, REPO, None).await.unwrap(),
CfgProfile {
duration: Duration::from_mins(5),
pass_threshold: 50.0,
allowed_voters: Some(AllowedVoters::default()),
..Default::default()
}
);
}

#[tokio::test]
async fn get_cfg_profile_profile1() {
let mut gh = MockGH::new();
gh.expect_get_config_file()
.with(eq(INST_ID), eq(OWNER), eq(REPO))
.times(1)
.returning(|_, _, _| Box::pin(future::ready(Some(get_test_valid_config()))));
let gh = Arc::new(gh);

assert_eq!(
CfgProfile::get(
gh,
INST_ID,
OWNER,
OWNER_IS_ORG,
REPO,
Some(PROFILE_NAME.to_string())
)
.await
.unwrap(),
CfgProfile {
duration: Duration::from_mins(10),
pass_threshold: 75.0,
allowed_voters: Some(AllowedVoters {
teams: Some(vec![TEAM1.to_string()]),
users: Some(vec![USER1.to_string(), USER2.to_string()]),
..Default::default()
}),
..Default::default()
}
);
}
}
mod tests;
Loading
Loading