Skip to content

Commit 79e10eb

Browse files
unhappychoiceclaude
andcommitted
test: mock file storage and HTTP clients for repository tests
Add proper mocking for all repository tests: - Mock OssInsightClient using test-mocks feature (same pattern as GitHubApiClient) - Add new_test() methods to ChallengeRepository, TrendingRepository, and VersionRepository - Update all repository tests to use test instances - Remove ignore attributes from trending repository tests This ensures no real file system or HTTP requests during tests. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 15d8939 commit 79e10eb

File tree

8 files changed

+191
-112
lines changed

8 files changed

+191
-112
lines changed

src/domain/repositories/challenge_repository.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,12 @@ impl ChallengeRepository {
5050
}
5151
}
5252

53+
/// Create a new repository for testing
54+
#[doc(hidden)]
55+
pub fn new_test() -> Self {
56+
Self::with_cache_dir(PathBuf::from("/mock/cache"))
57+
}
58+
5359
pub fn save_challenges(
5460
&self,
5561
repo: &GitRepository,

src/domain/repositories/session_repository.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,16 @@ impl SessionRepository {
2727
})
2828
}
2929

30+
/// Create a new repository with an in-memory database for testing
31+
#[doc(hidden)]
32+
pub fn new_test() -> Result<Self> {
33+
let database = Database::new_test()?;
34+
database.init()?;
35+
Ok(Self {
36+
database: Arc::new(Mutex::new(database)),
37+
})
38+
}
39+
3040
/// Record a completed session to the database
3141
pub fn record_session(
3242
&self,

src/domain/repositories/trending_repository.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,12 @@ impl TrendingRepository {
6060
}
6161
}
6262

63+
/// Create a new repository for testing that doesn't make HTTP requests
64+
#[doc(hidden)]
65+
pub fn new_test() -> Self {
66+
Self::with_cache_dir(PathBuf::from("/mock/trending_cache"))
67+
}
68+
6369
/// Get trending repositories with caching and fallback to fresh data
6470
pub async fn get_trending_repositories(
6571
&self,

src/domain/repositories/version_repository.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,15 @@ impl VersionRepository {
2020
})
2121
}
2222

23+
/// Create a new repository for testing that doesn't make HTTP requests
24+
#[doc(hidden)]
25+
pub fn new_test() -> Result<Self> {
26+
Ok(Self {
27+
github_client: GitHubApiClient::new()?,
28+
file_storage: FileStorage::new(),
29+
})
30+
}
31+
2332
/// Fetch the latest version from cache or API
2433
pub async fn fetch_latest_version(&self) -> Result<String> {
2534
const CHECK_FREQUENCY_HOURS: u64 = 24;
Lines changed: 145 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -1,124 +1,167 @@
1-
use reqwest::Client;
2-
use serde::Deserialize;
3-
use std::time::Duration;
4-
5-
use crate::domain::error::{GitTypeError, Result};
61
use crate::domain::repositories::trending_repository::TrendingRepositoryInfo;
7-
8-
#[derive(Debug, Clone)]
9-
pub struct OssInsightClient {
10-
client: Client,
11-
}
12-
13-
impl OssInsightClient {
14-
pub fn new() -> Self {
15-
Self {
16-
client: Client::new(),
17-
}
2+
#[cfg(feature = "test-mocks")]
3+
use crate::Result;
4+
#[cfg(not(feature = "test-mocks"))]
5+
use crate::{GitTypeError, Result};
6+
7+
#[cfg(not(feature = "test-mocks"))]
8+
mod real_impl {
9+
use super::*;
10+
use reqwest::Client;
11+
use serde::Deserialize;
12+
use std::time::Duration;
13+
14+
#[derive(Debug, Clone)]
15+
pub struct OssInsightClient {
16+
client: Client,
1817
}
1918

20-
pub async fn fetch_trending_repositories(
21-
&self,
22-
language: Option<&str>,
23-
period: &str,
24-
) -> Result<Vec<TrendingRepositoryInfo>> {
25-
let api_period = match period {
26-
"daily" => "past_24_hours",
27-
"weekly" => "past_week",
28-
"monthly" => "past_month",
29-
_ => "past_24_hours",
30-
};
31-
32-
let mut url = format!(
33-
"https://api.ossinsight.io/v1/trends/repos/?period={}",
34-
api_period
35-
);
36-
37-
if let Some(lang) = language {
38-
let api_lang = self.map_language_name(lang);
39-
url = format!("{}&language={}", url, urlencoding::encode(&api_lang));
19+
impl OssInsightClient {
20+
pub fn new() -> Self {
21+
Self {
22+
client: Client::new(),
23+
}
4024
}
4125

42-
let response = self
43-
.client
44-
.get(&url)
45-
.header("User-Agent", "gittype")
46-
.header("Accept", "application/json")
47-
.timeout(Duration::from_secs(10))
48-
.send()
49-
.await?;
50-
51-
if !response.status().is_success() {
52-
return Err(GitTypeError::ApiError(format!(
53-
"OSS Insight API request failed: {}",
54-
response.status()
55-
)));
56-
}
26+
pub async fn fetch_trending_repositories(
27+
&self,
28+
language: Option<&str>,
29+
period: &str,
30+
) -> Result<Vec<TrendingRepositoryInfo>> {
31+
let api_period = match period {
32+
"daily" => "past_24_hours",
33+
"weekly" => "past_week",
34+
"monthly" => "past_month",
35+
_ => "past_24_hours",
36+
};
37+
38+
let mut url = format!(
39+
"https://api.ossinsight.io/v1/trends/repos/?period={}",
40+
api_period
41+
);
42+
43+
if let Some(lang) = language {
44+
let api_lang = self.map_language_name(lang);
45+
url = format!("{}&language={}", url, urlencoding::encode(&api_lang));
46+
}
5747

58-
let api_response: ApiResponse = response.json().await?;
59-
let repositories = self.convert_api_response(api_response);
48+
let response = self
49+
.client
50+
.get(&url)
51+
.header("User-Agent", "gittype")
52+
.header("Accept", "application/json")
53+
.timeout(Duration::from_secs(10))
54+
.send()
55+
.await?;
56+
57+
if !response.status().is_success() {
58+
return Err(GitTypeError::ApiError(format!(
59+
"OSS Insight API request failed: {}",
60+
response.status()
61+
)));
62+
}
6063

61-
Ok(repositories)
62-
}
64+
let api_response: ApiResponse = response.json().await?;
65+
let repositories = self.convert_api_response(api_response);
66+
67+
Ok(repositories)
68+
}
6369

64-
fn map_language_name(&self, lang: &str) -> String {
65-
match lang.to_lowercase().as_str() {
66-
"javascript" => "JavaScript".to_string(),
67-
"typescript" => "TypeScript".to_string(),
68-
"c++" => "C++".to_string(),
69-
"c#" => "C#".to_string(),
70-
"php" => "PHP".to_string(),
71-
_ => {
72-
let mut chars = lang.chars();
73-
match chars.next() {
74-
None => lang.to_string(),
75-
Some(first) => {
76-
first.to_uppercase().collect::<String>() + &chars.as_str().to_lowercase()
70+
fn map_language_name(&self, lang: &str) -> String {
71+
match lang.to_lowercase().as_str() {
72+
"javascript" => "JavaScript".to_string(),
73+
"typescript" => "TypeScript".to_string(),
74+
"c++" => "C++".to_string(),
75+
"c#" => "C#".to_string(),
76+
"php" => "PHP".to_string(),
77+
_ => {
78+
let mut chars = lang.chars();
79+
match chars.next() {
80+
None => lang.to_string(),
81+
Some(first) => {
82+
first.to_uppercase().collect::<String>()
83+
+ &chars.as_str().to_lowercase()
84+
}
7785
}
7886
}
7987
}
8088
}
89+
90+
fn convert_api_response(&self, api_response: ApiResponse) -> Vec<TrendingRepositoryInfo> {
91+
api_response
92+
.data
93+
.rows
94+
.into_iter()
95+
.map(|row| TrendingRepositoryInfo {
96+
repo_name: row.repo_name,
97+
primary_language: row.primary_language,
98+
description: row.description,
99+
stars: row.stars.unwrap_or_else(|| "0".to_string()),
100+
forks: row.forks.unwrap_or_else(|| "0".to_string()),
101+
total_score: row.total_score.unwrap_or_else(|| "0".to_string()),
102+
})
103+
.collect()
104+
}
81105
}
82106

83-
fn convert_api_response(&self, api_response: ApiResponse) -> Vec<TrendingRepositoryInfo> {
84-
api_response
85-
.data
86-
.rows
87-
.into_iter()
88-
.map(|row| TrendingRepositoryInfo {
89-
repo_name: row.repo_name,
90-
primary_language: row.primary_language,
91-
description: row.description,
92-
stars: row.stars.unwrap_or_else(|| "0".to_string()),
93-
forks: row.forks.unwrap_or_else(|| "0".to_string()),
94-
total_score: row.total_score.unwrap_or_else(|| "0".to_string()),
95-
})
96-
.collect()
107+
impl Default for OssInsightClient {
108+
fn default() -> Self {
109+
Self::new()
110+
}
97111
}
98-
}
99112

100-
impl Default for OssInsightClient {
101-
fn default() -> Self {
102-
Self::new()
113+
#[derive(Deserialize)]
114+
struct ApiResponse {
115+
data: ApiData,
103116
}
104-
}
105117

106-
#[derive(Deserialize)]
107-
struct ApiResponse {
108-
data: ApiData,
109-
}
118+
#[derive(Deserialize)]
119+
struct ApiData {
120+
rows: Vec<RowData>,
121+
}
110122

111-
#[derive(Deserialize)]
112-
struct ApiData {
113-
rows: Vec<RowData>,
123+
#[derive(Deserialize)]
124+
struct RowData {
125+
repo_name: String,
126+
primary_language: Option<String>,
127+
description: Option<String>,
128+
stars: Option<String>,
129+
forks: Option<String>,
130+
total_score: Option<String>,
131+
}
114132
}
115133

116-
#[derive(Deserialize)]
117-
struct RowData {
118-
repo_name: String,
119-
primary_language: Option<String>,
120-
description: Option<String>,
121-
stars: Option<String>,
122-
forks: Option<String>,
123-
total_score: Option<String>,
134+
#[cfg(feature = "test-mocks")]
135+
mod mock_impl {
136+
use super::*;
137+
138+
#[derive(Debug, Clone)]
139+
pub struct OssInsightClient;
140+
141+
impl OssInsightClient {
142+
pub fn new() -> Self {
143+
Self
144+
}
145+
146+
pub async fn fetch_trending_repositories(
147+
&self,
148+
_language: Option<&str>,
149+
_period: &str,
150+
) -> Result<Vec<TrendingRepositoryInfo>> {
151+
// Return empty vec for tests (no HTTP requests)
152+
Ok(Vec::new())
153+
}
154+
}
155+
156+
impl Default for OssInsightClient {
157+
fn default() -> Self {
158+
Self::new()
159+
}
160+
}
124161
}
162+
163+
#[cfg(not(feature = "test-mocks"))]
164+
pub use real_impl::OssInsightClient;
165+
166+
#[cfg(feature = "test-mocks")]
167+
pub use mock_impl::OssInsightClient;

tests/unit/domain/repositories/challenge_repository_tests.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ mod tests {
3232
use super::*;
3333

3434
fn create_test_cache() -> ChallengeRepository {
35-
ChallengeRepository::with_cache_dir(PathBuf::from("/mock/cache"))
35+
ChallengeRepository::new_test()
3636
}
3737

3838
#[test]

0 commit comments

Comments
 (0)