From a07d9773e927b30ab19dc89d8db5fb8c775e0843 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Ber=C3=A1nek?= Date: Thu, 11 Jan 2024 13:30:36 +0100 Subject: [PATCH] Add basic test infrastructure and two simple team tests --- Cargo.lock | 218 +++++++++++++++++++++++++++++++-- Cargo.toml | 2 + src/github/api/mod.rs | 2 +- src/github/mod.rs | 7 ++ src/github/tests/mod.rs | 43 +++++++ src/github/tests/test_utils.rs | 193 +++++++++++++++++++++++++++++ 6 files changed, 454 insertions(+), 11 deletions(-) create mode 100644 src/github/tests/mod.rs create mode 100644 src/github/tests/test_utils.rs diff --git a/Cargo.lock b/Cargo.lock index 8515842..7e5b336 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -145,6 +145,18 @@ dependencies = [ "generic-array", ] +[[package]] +name = "console" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" +dependencies = [ + "encode_unicode", + "lazy_static", + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "core-foundation" version = "0.9.3" @@ -170,6 +182,78 @@ dependencies = [ "libc", ] +[[package]] +name = "darling" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "109c1ca6e6b7f82cc233a97004ea8ed7ca123a9af07a8230878fcfda9b158bf0" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "derive_builder" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d67778784b508018359cbc8696edb3db78160bab2c2a28ba7f56ef6932997f8" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c11bdc11a0c47bc7d37d582b5285da6849c96681023680b906673c5707af7b0f" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "derive_builder_macro" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebcda35c7a396850a55ffeac740804b40ffec779b98fffbb1738f4033f0ee79e" +dependencies = [ + "derive_builder_core", + "syn", +] + +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + [[package]] name = "encoding_rs" version = "0.8.23" @@ -447,6 +531,12 @@ dependencies = [ "tokio-native-tls", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "0.2.0" @@ -479,6 +569,19 @@ dependencies = [ "serde", ] +[[package]] +name = "insta" +version = "1.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d64600be34b2fcfc267740a243fa7744441bb4947a619ac4e5bb6507f35fbfc" +dependencies = [ + "console", + "lazy_static", + "linked-hash-map", + "similar", + "yaml-rust", +] + [[package]] name = "instant" version = "0.1.12" @@ -542,6 +645,12 @@ version = "0.2.132" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5" +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + [[package]] name = "log" version = "0.4.8" @@ -578,7 +687,7 @@ dependencies = [ "libc", "log", "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys", + "windows-sys 0.36.1", ] [[package]] @@ -709,18 +818,18 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.43" +version = "1.0.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab" +checksum = "95fc56cda0b5c3325f5fbbd7ff9fda9e02bb00bb3dac51252d2f1bfa1cb8cc8c" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.7" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" dependencies = [ "proc-macro2", ] @@ -897,6 +1006,12 @@ dependencies = [ "serde", ] +[[package]] +name = "similar" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32fea41aca09ee824cc9724996433064c89f7777e60762749a4170a14abbfa21" + [[package]] name = "slab" version = "0.4.2" @@ -919,6 +1034,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + [[package]] name = "subtle" version = "2.4.0" @@ -942,9 +1063,11 @@ version = "0.1.0" dependencies = [ "anyhow", "base64 0.13.0", + "derive_builder", "env_logger", "hyper-old-types", "indexmap 2.1.0", + "insta", "log", "reqwest", "rust_team_data", @@ -1267,43 +1390,109 @@ version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" dependencies = [ - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_msvc", + "windows_aarch64_msvc 0.36.1", + "windows_i686_gnu 0.36.1", + "windows_i686_msvc 0.36.1", + "windows_x86_64_gnu 0.36.1", + "windows_x86_64_msvc 0.36.1", ] +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc 0.52.0", + "windows_i686_gnu 0.52.0", + "windows_i686_msvc 0.52.0", + "windows_x86_64_gnu 0.52.0", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc 0.52.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" + [[package]] name = "windows_aarch64_msvc" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" + [[package]] name = "windows_i686_gnu" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" +[[package]] +name = "windows_i686_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" + [[package]] name = "windows_i686_msvc" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" +[[package]] +name = "windows_i686_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" + [[package]] name = "windows_x86_64_gnu" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" + [[package]] name = "windows_x86_64_msvc" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" + [[package]] name = "winreg" version = "0.10.1" @@ -1313,6 +1502,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] + [[package]] name = "zeroize" version = "1.2.0" diff --git a/Cargo.toml b/Cargo.toml index 1e54cb2..690d7ee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,3 +18,5 @@ serde_json = "1.0" [dev-dependencies] indexmap = "2.1.0" +derive_builder = "0.12.0" +insta = "1.34.0" diff --git a/src/github/api/mod.rs b/src/github/api/mod.rs index 7f9890d..3def026 100644 --- a/src/github/api/mod.rs +++ b/src/github/api/mod.rs @@ -196,7 +196,7 @@ impl GraphPageInfo { } } -#[derive(serde::Deserialize, Debug)] +#[derive(serde::Deserialize, Debug, Clone)] pub(crate) struct Team { /// The ID returned by the GitHub API can't be empty, but the None marks teams "created" during /// a dry run and not actually present on GitHub, so other methods can avoid acting on them. diff --git a/src/github/mod.rs b/src/github/mod.rs index 8cadb67..9130b3c 100644 --- a/src/github/mod.rs +++ b/src/github/mod.rs @@ -1,4 +1,6 @@ mod api; +#[cfg(test)] +mod tests; use self::api::{BranchProtectionOp, TeamPrivacy, TeamRole}; use crate::github::api::{GithubRead, RepoPermission}; @@ -802,6 +804,7 @@ enum BranchProtectionDiffOperation { Delete(String), } +#[derive(Debug)] enum TeamDiff { Create(CreateTeamDiff), Edit(EditTeamDiff), @@ -830,6 +833,7 @@ impl std::fmt::Display for TeamDiff { } } +#[derive(Debug)] struct CreateTeamDiff { org: String, name: String, @@ -871,6 +875,7 @@ impl std::fmt::Display for CreateTeamDiff { } } +#[derive(Debug)] struct EditTeamDiff { org: String, name: String, @@ -947,6 +952,7 @@ impl std::fmt::Display for EditTeamDiff { } } +#[derive(Debug)] enum MemberDiff { Create(TeamRole), ChangeRole((TeamRole, TeamRole)), @@ -972,6 +978,7 @@ impl MemberDiff { } } +#[derive(Debug)] struct DeleteTeamDiff { org: String, name: String, diff --git a/src/github/tests/mod.rs b/src/github/tests/mod.rs new file mode 100644 index 0000000..b6dc90a --- /dev/null +++ b/src/github/tests/mod.rs @@ -0,0 +1,43 @@ +use crate::github::tests::test_utils::{DataModel, TeamData}; + +mod test_utils; + +#[test] +fn team_noop() { + let model = DataModel::default(); + let gh = model.gh_model(); + let team_diff = model.diff_teams(gh); + assert!(team_diff.is_empty()); +} + +#[test] +fn team_create() { + let mut model = DataModel::default(); + let user = model.add_user("mark"); + let user2 = model.add_user("jan"); + let gh = model.gh_model(); + model.add_team(TeamData::new("admins").gh_team("admins", &[user, user2])); + let team_diff = model.diff_teams(gh); + insta::assert_debug_snapshot!(team_diff, @r###" + [ + Create( + CreateTeamDiff { + org: "rust-lang", + name: "admins", + description: "Managed by the rust-lang/team repository.", + privacy: Closed, + members: [ + ( + "mark", + Member, + ), + ( + "jan", + Member, + ), + ], + }, + ), + ] + "###); +} diff --git a/src/github/tests/test_utils.rs b/src/github/tests/test_utils.rs new file mode 100644 index 0000000..0b668a4 --- /dev/null +++ b/src/github/tests/test_utils.rs @@ -0,0 +1,193 @@ +use std::collections::{HashMap, HashSet}; +use std::rc::Rc; + +use derive_builder::Builder; +use rust_team_data::v1::{GitHubTeam, Person, TeamGitHub, TeamKind}; + +use crate::github::api::{ + BranchProtection, GithubRead, Repo, RepoTeam, RepoUser, Team, TeamMember, TeamPrivacy, +}; +use crate::github::{api, SyncGitHub, TeamDiff}; + +const DEFAULT_ORG: &str = "rust-lang"; + +/// Represents the contents of rust_team_data state. +/// In tests, you should fill the model with repos, teams, people etc., +/// and then call `gh_model` to construct a corresponding GitHubModel. +/// After that, you can modify the data model further, then generate a diff +/// and assert that it has the expected value. +#[derive(Default, Clone)] +pub struct DataModel { + people: Vec, + teams: Vec, +} + +impl DataModel { + pub fn add_user(&mut self, name: &str) -> usize { + let github_id = self.people.len(); + self.people.push(Person { + name: name.to_string(), + email: Some(format!("{name}@rust.com")), + github_id, + }); + github_id + } + + pub fn gh_model(&self) -> GithubMock { + GithubMock { + users: self + .people + .iter() + .map(|user| (user.github_id, user.name.clone())) + .collect(), + owners: Default::default(), + teams: self + .teams + .clone() + .into_iter() + .enumerate() + .map(|(id, team)| api::Team { + id: Some(id), + name: team.name.clone(), + description: None, + privacy: TeamPrivacy::Closed, + slug: team.name, + }) + .collect(), + } + } + + pub fn add_team(&mut self, team: TeamDataBuilder) { + let team = team.build().expect("Cannot build team"); + self.teams.push(team); + } + + pub fn diff_teams(&self, github: GithubMock) -> Vec { + let teams = self.teams.iter().map(|r| r.to_data()).collect(); + let repos = vec![]; + + let read = Rc::new(github); + let sync = SyncGitHub::new(read, teams, repos).expect("Cannot create SyncGitHub"); + sync.diff_teams().expect("Cannot diff teams") + } +} + +#[derive(Clone, Builder)] +#[builder(pattern = "owned")] +pub struct TeamData { + #[builder(default = "TeamKind::Team")] + kind: TeamKind, + name: String, + #[builder(default)] + gh_teams: Vec, +} + +impl TeamData { + pub fn new(name: &str) -> TeamDataBuilder { + TeamDataBuilder::default().name(name.to_string()) + } + + fn to_data(&self) -> rust_team_data::v1::Team { + let TeamData { + name, + kind, + gh_teams, + } = self.clone(); + rust_team_data::v1::Team { + name: name.clone(), + kind, + subteam_of: None, + members: vec![], + alumni: vec![], + github: (!gh_teams.is_empty()).then(|| TeamGitHub { teams: gh_teams }), + website_data: None, + discord: vec![], + } + } +} + +impl TeamDataBuilder { + pub fn gh_team(mut self, name: &str, members: &[usize]) -> Self { + let mut gh_teams = self.gh_teams.unwrap_or_default(); + gh_teams.push(GitHubTeam { + org: DEFAULT_ORG.to_string(), + name: name.to_string(), + members: members.to_vec(), + }); + self.gh_teams = Some(gh_teams); + self + } +} + +/// Represents the state of GitHub repositories, teams and users. +#[derive(Default, Clone)] +pub struct GithubMock { + users: HashMap, + owners: HashMap>, + teams: Vec, +} + +impl GithubRead for GithubMock { + fn usernames(&self, ids: &[usize]) -> anyhow::Result> { + Ok(self + .users + .iter() + .filter(|(k, _)| ids.contains(k)) + .map(|(k, v)| (*k, v.clone())) + .collect()) + } + + fn org_owners(&self, org: &str) -> anyhow::Result> { + Ok(self + .owners + .get(org) + .cloned() + .unwrap_or_default() + .into_iter() + .collect()) + } + + fn org_teams(&self, _org: &str) -> anyhow::Result> { + Ok(self + .teams + .iter() + .map(|team| (team.name.clone(), team.slug.clone())) + .collect()) + } + + fn team(&self, _org: &str, team: &str) -> anyhow::Result> { + Ok(self.teams.iter().find(|t| t.name == team).cloned()) + } + + fn team_memberships(&self, _team: &Team) -> anyhow::Result> { + todo!() + } + + fn team_membership_invitations( + &self, + _org: &str, + _team: &str, + ) -> anyhow::Result> { + todo!() + } + + fn repo(&self, _org: &str, _repo: &str) -> anyhow::Result> { + todo!() + } + + fn repo_teams(&self, _org: &str, _repo: &str) -> anyhow::Result> { + todo!() + } + + fn repo_collaborators(&self, _org: &str, _repo: &str) -> anyhow::Result> { + todo!() + } + + fn branch_protections( + &self, + _org: &str, + _repo: &str, + ) -> anyhow::Result> { + todo!() + } +}