Skip to content

Commit affcc3e

Browse files
authored
Merge pull request #9946 from Turbo87/github-mock
github: Use mockall to generate `MockGitHubClient` implementation
2 parents dd03001 + 778477f commit affcc3e

File tree

7 files changed

+76
-78
lines changed

7 files changed

+76
-78
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ unicode-xid = "=0.2.6"
122122

123123
[dev-dependencies]
124124
bytes = "=1.8.0"
125+
crates_io_github = { path = "crates/crates_io_github", features = ["mock"] }
125126
crates_io_index = { path = "crates/crates_io_index", features = ["testing"] }
126127
crates_io_tarball = { path = "crates/crates_io_tarball", features = ["builder"] }
127128
crates_io_team_repo = { path = "crates/crates_io_team_repo", features = ["mock"] }

crates/crates_io_github/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,13 @@ edition = "2021"
77
[lints]
88
workspace = true
99

10+
[features]
11+
mock = ["dep:mockall"]
12+
1013
[dependencies]
1114
anyhow = "=1.0.93"
1215
async-trait = "=0.1.83"
16+
mockall = { version = "=0.13.0", optional = true }
1317
oauth2 = { version = "=4.4.2", default-features = false }
1418
reqwest = { version = "=0.12.9", features = ["json"] }
1519
serde = { version = "=1.0.215", features = ["derive"] }

crates/crates_io_github/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ use serde::Deserialize;
1616

1717
type Result<T> = std::result::Result<T, GitHubError>;
1818

19+
#[cfg_attr(feature = "mock", mockall::automock)]
1920
#[async_trait]
2021
pub trait GitHubClient: Send + Sync {
2122
async fn current_user(&self, auth: &AccessToken) -> Result<GithubUser>;

src/tests/github_secret_scanning.rs

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use crate::tests::util::MockRequestExt;
22
use crate::tests::{RequestHelper, TestApp};
33
use crate::util::token::HashedToken;
44
use crate::{models::ApiToken, schema::api_tokens};
5+
use crates_io_github::{GitHubPublicKey, MockGitHubClient};
56
use diesel::prelude::*;
67
use diesel_async::RunQueryDsl;
78
use googletest::prelude::*;
@@ -13,13 +14,34 @@ static URL: &str = "/api/github/secret-scanning/verify";
1314
// Test request and signature from https://docs.github.com/en/developers/overview/secret-scanning-partner-program#create-a-secret-alert-service
1415
static GITHUB_ALERT: &[u8] =
1516
br#"[{"token":"some_token","type":"some_type","url":"some_url","source":"some_source"}]"#;
17+
1618
static GITHUB_PUBLIC_KEY_IDENTIFIER: &str =
1719
"f9525bf080f75b3506ca1ead061add62b8633a346606dc5fe544e29231c6ee0d";
20+
21+
/// Test key from https://docs.github.com/en/developers/overview/secret-scanning-partner-program#create-a-secret-alert-service
22+
static GITHUB_PUBLIC_KEY: &str = "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEsz9ugWDj5jK5ELBK42ynytbo38gP\nHzZFI03Exwz8Lh/tCfL3YxwMdLjB+bMznsanlhK0RwcGP3IDb34kQDIo3Q==\n-----END PUBLIC KEY-----";
23+
1824
static GITHUB_PUBLIC_KEY_SIGNATURE: &str = "MEUCIFLZzeK++IhS+y276SRk2Pe5LfDrfvTXu6iwKKcFGCrvAiEAhHN2kDOhy2I6eGkOFmxNkOJ+L2y8oQ9A2T9GGJo6WJY=";
1925

26+
fn github_mock() -> MockGitHubClient {
27+
let mut mock = MockGitHubClient::new();
28+
29+
mock.expect_public_keys().returning(|_, _| {
30+
let key = GitHubPublicKey {
31+
key_identifier: GITHUB_PUBLIC_KEY_IDENTIFIER.to_string(),
32+
key: GITHUB_PUBLIC_KEY.to_string(),
33+
is_current: true,
34+
};
35+
36+
Ok(vec![key])
37+
});
38+
39+
mock
40+
}
41+
2042
#[tokio::test(flavor = "multi_thread")]
2143
async fn github_secret_alert_revokes_token() {
22-
let (app, anon, user, token) = TestApp::init().with_token();
44+
let (app, anon, user, token) = TestApp::init().with_github(github_mock()).with_token();
2345
let mut conn = app.async_db_conn().await;
2446

2547
// Ensure no emails were sent up to this point
@@ -77,7 +99,7 @@ async fn github_secret_alert_revokes_token() {
7799

78100
#[tokio::test(flavor = "multi_thread")]
79101
async fn github_secret_alert_for_revoked_token() {
80-
let (app, anon, user, token) = TestApp::init().with_token();
102+
let (app, anon, user, token) = TestApp::init().with_github(github_mock()).with_token();
81103
let mut conn = app.async_db_conn().await;
82104

83105
// Ensure no emails were sent up to this point
@@ -138,7 +160,7 @@ async fn github_secret_alert_for_revoked_token() {
138160

139161
#[tokio::test(flavor = "multi_thread")]
140162
async fn github_secret_alert_for_unknown_token() {
141-
let (app, anon, user, token) = TestApp::init().with_token();
163+
let (app, anon, user, token) = TestApp::init().with_github(github_mock()).with_token();
142164
let mut conn = app.async_db_conn().await;
143165

144166
// Ensure no emails were sent up to this point
@@ -180,7 +202,7 @@ async fn github_secret_alert_for_unknown_token() {
180202

181203
#[tokio::test(flavor = "multi_thread")]
182204
async fn github_secret_alert_invalid_signature_fails() {
183-
let (_, anon) = TestApp::init().empty();
205+
let (_, anon) = TestApp::init().with_github(github_mock()).empty();
184206

185207
// No headers or request body
186208
let request = anon.post_request(URL);

src/tests/util/github.rs

Lines changed: 30 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
use anyhow::anyhow;
2-
use async_trait::async_trait;
32
use crates_io_github::{
4-
GitHubClient, GitHubError, GitHubOrgMembership, GitHubOrganization, GitHubPublicKey,
5-
GitHubTeam, GitHubTeamMembership, GithubUser,
3+
GitHubError, GitHubOrgMembership, GitHubOrganization, GitHubTeam, GitHubTeamMembership,
4+
GithubUser, MockGitHubClient,
65
};
7-
use oauth2::AccessToken;
86
use std::sync::atomic::{AtomicUsize, Ordering};
97

108
static NEXT_GH_ID: AtomicUsize = AtomicUsize::new(0);
@@ -51,30 +49,34 @@ pub(crate) const MOCK_GITHUB_DATA: MockData = MockData {
5149
5250
},
5351
],
54-
// Test key from https://docs.github.com/en/developers/overview/secret-scanning-partner-program#create-a-secret-alert-service
55-
public_keys: &[
56-
MockPublicKey {
57-
key_identifier: "f9525bf080f75b3506ca1ead061add62b8633a346606dc5fe544e29231c6ee0d",
58-
key: "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEsz9ugWDj5jK5ELBK42ynytbo38gP\nHzZFI03Exwz8Lh/tCfL3YxwMdLjB+bMznsanlhK0RwcGP3IDb34kQDIo3Q==\n-----END PUBLIC KEY-----",
59-
is_current: true,
60-
},
61-
],
6252
};
6353

64-
pub(crate) struct MockGitHubClient {
65-
data: &'static MockData,
66-
}
54+
impl MockData {
55+
pub fn as_mock_client(&'static self) -> MockGitHubClient {
56+
let mut mock = MockGitHubClient::new();
57+
58+
mock.expect_current_user()
59+
.returning(|_auth| self.current_user());
60+
61+
mock.expect_org_by_name()
62+
.returning(|org_name, _auth| self.org_by_name(org_name));
6763

68-
impl MockGitHubClient {
69-
pub(crate) fn new(data: &'static MockData) -> Self {
70-
Self { data }
64+
mock.expect_team_by_name()
65+
.returning(|org_name, team_name, _auth| self.team_by_name(org_name, team_name));
66+
67+
mock.expect_team_membership()
68+
.returning(|org_id, team_id, username, _auth| {
69+
self.team_membership(org_id, team_id, username)
70+
});
71+
72+
mock.expect_org_membership()
73+
.returning(|org_id, username, _auth| self.org_membership(org_id, username));
74+
75+
mock
7176
}
72-
}
7377

74-
#[async_trait]
75-
impl GitHubClient for MockGitHubClient {
76-
async fn current_user(&self, _auth: &AccessToken) -> Result<GithubUser, GitHubError> {
77-
let user = &self.data.users[0];
78+
fn current_user(&self) -> Result<GithubUser, GitHubError> {
79+
let user = &self.users[0];
7880
Ok(GithubUser {
7981
id: user.id,
8082
login: user.login.into(),
@@ -84,13 +86,8 @@ impl GitHubClient for MockGitHubClient {
8486
})
8587
}
8688

87-
async fn org_by_name(
88-
&self,
89-
org_name: &str,
90-
_auth: &AccessToken,
91-
) -> Result<GitHubOrganization, GitHubError> {
89+
fn org_by_name(&self, org_name: &str) -> Result<GitHubOrganization, GitHubError> {
9290
let org = self
93-
.data
9491
.orgs
9592
.iter()
9693
.find(|org| org.name == org_name.to_lowercase())
@@ -101,14 +98,8 @@ impl GitHubClient for MockGitHubClient {
10198
})
10299
}
103100

104-
async fn team_by_name(
105-
&self,
106-
org_name: &str,
107-
team_name: &str,
108-
auth: &AccessToken,
109-
) -> Result<GitHubTeam, GitHubError> {
101+
fn team_by_name(&self, org_name: &str, team_name: &str) -> Result<GitHubTeam, GitHubError> {
110102
let team = self
111-
.data
112103
.orgs
113104
.iter()
114105
.find(|org| org.name == org_name.to_lowercase())
@@ -120,19 +111,17 @@ impl GitHubClient for MockGitHubClient {
120111
Ok(GitHubTeam {
121112
id: team.id,
122113
name: Some(team.name.into()),
123-
organization: self.org_by_name(org_name, auth).await?,
114+
organization: self.org_by_name(org_name)?,
124115
})
125116
}
126117

127-
async fn team_membership(
118+
fn team_membership(
128119
&self,
129120
org_id: i32,
130121
team_id: i32,
131122
username: &str,
132-
_auth: &AccessToken,
133123
) -> Result<GitHubTeamMembership, GitHubError> {
134124
let team = self
135-
.data
136125
.orgs
137126
.iter()
138127
.find(|org| org.id == org_id)
@@ -150,14 +139,12 @@ impl GitHubClient for MockGitHubClient {
150139
}
151140
}
152141

153-
async fn org_membership(
142+
fn org_membership(
154143
&self,
155144
org_id: i32,
156145
username: &str,
157-
_auth: &AccessToken,
158146
) -> Result<GitHubOrgMembership, GitHubError> {
159147
let org = self
160-
.data
161148
.orgs
162149
.iter()
163150
.find(|org| org.id == org_id)
@@ -180,14 +167,6 @@ impl GitHubClient for MockGitHubClient {
180167
Err(not_found())
181168
}
182169
}
183-
184-
async fn public_keys(
185-
&self,
186-
_username: &str,
187-
_password: &str,
188-
) -> Result<Vec<GitHubPublicKey>, GitHubError> {
189-
Ok(self.data.public_keys.iter().map(Into::into).collect())
190-
}
191170
}
192171

193172
fn not_found() -> GitHubError {
@@ -197,7 +176,6 @@ fn not_found() -> GitHubError {
197176
pub(crate) struct MockData {
198177
orgs: &'static [MockOrg],
199178
users: &'static [MockUser],
200-
public_keys: &'static [MockPublicKey],
201179
}
202180

203181
struct MockUser {
@@ -219,19 +197,3 @@ struct MockTeam {
219197
name: &'static str,
220198
members: &'static [&'static str],
221199
}
222-
223-
struct MockPublicKey {
224-
key_identifier: &'static str,
225-
key: &'static str,
226-
is_current: bool,
227-
}
228-
229-
impl From<&'static MockPublicKey> for GitHubPublicKey {
230-
fn from(k: &'static MockPublicKey) -> Self {
231-
Self {
232-
key_identifier: k.key_identifier.to_string(),
233-
key: k.key.to_string(),
234-
is_current: k.is_current,
235-
}
236-
}
237-
}

src/tests/util/test_app.rs

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,10 @@ use crate::rate_limiter::{LimitedAction, RateLimiterConfig};
99
use crate::schema::users;
1010
use crate::storage::StorageConfig;
1111
use crate::tests::util::chaosproxy::ChaosProxy;
12-
use crate::tests::util::github::{MockGitHubClient, MOCK_GITHUB_DATA};
12+
use crate::tests::util::github::MOCK_GITHUB_DATA;
1313
use crate::worker::{Environment, RunnerExt};
1414
use crate::{App, Emails, Env};
15+
use crates_io_github::MockGitHubClient;
1516
use crates_io_index::testing::UpstreamIndex;
1617
use crates_io_index::{Credentials, RepositoryConfig};
1718
use crates_io_team_repo::MockTeamRepo;
@@ -103,6 +104,7 @@ impl TestApp {
103104
build_job_runner: false,
104105
use_chaos_proxy: false,
105106
team_repo: MockTeamRepo::new(),
107+
github: None,
106108
}
107109
}
108110

@@ -252,6 +254,7 @@ pub struct TestAppBuilder {
252254
build_job_runner: bool,
253255
use_chaos_proxy: bool,
254256
team_repo: MockTeamRepo,
257+
github: Option<MockGitHubClient>,
255258
}
256259

257260
impl TestAppBuilder {
@@ -296,7 +299,7 @@ impl TestAppBuilder {
296299
(primary_proxy, replica_proxy)
297300
};
298301

299-
let (app, router) = build_app(self.config);
302+
let (app, router) = build_app(self.config, self.github);
300303

301304
let runner = if self.build_job_runner {
302305
let index = self
@@ -397,6 +400,11 @@ impl TestAppBuilder {
397400
self
398401
}
399402

403+
pub fn with_github(mut self, github: MockGitHubClient) -> Self {
404+
self.github = Some(github);
405+
self
406+
}
407+
400408
pub fn with_team_repo(mut self, team_repo: MockTeamRepo) -> Self {
401409
self.team_repo = team_repo;
402410
self
@@ -487,14 +495,13 @@ fn simple_config() -> config::Server {
487495
}
488496
}
489497

490-
fn build_app(config: config::Server) -> (Arc<App>, axum::Router) {
498+
fn build_app(config: config::Server, github: Option<MockGitHubClient>) -> (Arc<App>, axum::Router) {
491499
// Use the in-memory email backend for all tests, allowing tests to analyze the emails sent by
492500
// the application. This will also prevent cluttering the filesystem.
493501
let emails = Emails::new_in_memory();
494502

495-
// Use a custom mock for the GitHub client, allowing to define the GitHub users and
496-
// organizations without actually having to create GitHub accounts.
497-
let github = Box::new(MockGitHubClient::new(&MOCK_GITHUB_DATA));
503+
let github = github.unwrap_or_else(|| MOCK_GITHUB_DATA.as_mock_client());
504+
let github = Box::new(github);
498505

499506
let app = App::new(config, emails, github);
500507

0 commit comments

Comments
 (0)