I want to generate code- coverage for rust source code

Must-share information (formatted with Markdown):

  • which versions are you using (SonarQube Server / Community Build, Scanner, Plugin, and any relevant extension)
  • how is SonarQube deployed: Docker
  • what are you trying to achieve - TO generate code coverage for rust source code
  • what have you tried so far to achieve this - I have generated code-coverage using LLVM/GCOV for rust code . But in sonarqube dashboard code coverage says Not computed

Do not share screenshots of logs – share the text itself (bonus points for being well-formatted)!

Logs?

/home/sonar/libs/common/src/error.rs:
1| |use serde::{Deserialize, Serialize};
2| |use thiserror::Error;
3| |
4| |/// Application-wide error types
5| |#[derive(Error, Debug, Clone, Serialize, Deserialize)]
6| |pub enum AppError {
7| | #[error(“Validation error: {message}”)]
8| | Validation { message: String },
9| |
10| | #[error(“Authentication failed: {reason}”)]
11| | Authentication { reason: String },
12| |
13| | #[error(“Authorization failed: {resource}”)]
14| | Authorization { resource: String },
15| |
16| | #[error(“Resource not found: {resource_type} with id {id}”)]
17| | NotFound { resource_type: String, id: String },
18| |
19| | #[error(“Database error: {message}”)]
20| | Database { message: String },
21| |
22| | #[error(“External service error: {service} - {message}”)]
23| | ExternalService { service: String, message: String },
24| |
25| | #[error(“Internal server error: {message}”)]
26| | Internal { message: String },
27| |}
28| |
29| |impl AppError {
30| 11| pub fn validation(message: impl Into) → Self {
31| 11| Self::Validation {
32| 11| message: message.into(),
33| 11| }
34| 11| }
35| |
36| 0| pub fn authentication(reason: impl Into) → Self {
37| 0| Self::Authentication {
38| 0| reason: reason.into(),
39| 0| }
40| 0| }
41| |
42| 0| pub fn authorization(resource: impl Into) → Self {
43| 0| Self::Authorization {
44| 0| resource: resource.into(),
45| 0| }
46| 0| }
47| |
48| 1| pub fn not_found(resource_type: impl Into, id: impl Into) → Self {
49| 1| Self::NotFound {
50| 1| resource_type: resource_type.into(),
51| 1| id: id.into(),
52| 1| }
53| 1| }
54| |
55| 0| pub fn database(message: impl Into) → Self {
56| 0| Self::Database {
57| 0| message: message.into(),
58| 0| }
59| 0| }
60| |
61| 0| pub fn external_service(service: impl Into, message: impl Into) → Self {
62| 0| Self::ExternalService {
63| 0| service: service.into(),
64| 0| message: message.into(),
65| 0| }
66| 0| }
67| |
68| 0| pub fn internal(message: impl Into) → Self {
69| 0| Self::Internal {
70| 0| message: message.into(),
71| 0| }
72| 0| }
73| |
74| | /// Get the HTTP status code for this error
75| 1| pub fn status_code(&self) → u16 {
76| 1| match self {
77| 1| AppError::Validation { .. } => 400,
78| 0| AppError::Authentication { .. } => 401,
79| 0| AppError::Authorization { .. } => 403,
80| 0| AppError::NotFound { .. } => 404,
81| 0| AppError::Database { .. } => 500,
82| 0| AppError::ExternalService { .. } => 502,
83| 0| AppError::Internal { .. } => 500,
84| | }
85| 1| }
86| |}
87| |
88| |/// Result type alias for application errors
89| |pub type AppResult = Result<T, AppError>;
90| |
91| |#[cfg(test)]
92| |mod tests {
93| | use super::*;
94| |
95| | #[test]
96| 1| fn test_error_creation() {
97| 1| let error = AppError::validation(“Invalid input”);
98| 1| assert_eq!(error.status_code(), 400);
99| 1| assert!(error.to_string().contains(“Validation error”));
100| 1| }
101| |
102| | #[test]
103| 1| fn test_error_serialization() {
104| 1| let error = AppError::not_found(“User”, “123”);
105| 1| let json = serde_json::to_string(&error).unwrap();
106| 1| let deserialized: AppError = serde_json::from_str(&json).unwrap();
107| |
108| 1| match deserialized {
109| 1| AppError::NotFound { resource_type, id } => {
110| 1| assert_eq!(resource_type, “User”);
111| 1| assert_eq!(id, “123”);
112| | }
113| 0| _ => panic!(“Wrong error type”),
114| | }
115| 1| }
116| |}

/home/sonar/libs/common/src/response.rs:
1| |use crate::error::AppError;
2| |use serde::{Deserialize, Serialize};
3| |
4| |/// Standard API response wrapper
5| |#[derive(Debug, Clone, Serialize, Deserialize)]
6| |pub struct ApiResponse {
7| | pub success: bool,
8| | pub data: Option,
9| | pub error: Option,
10| | pub timestamp: chrono::DateTimechrono::Utc,
11| |}
12| |
13| |impl ApiResponse {
14| | /// Create a successful response
15| 1| pub fn success(data: T) → Self {
16| 1| Self {
17| 1| success: true,
18| 1| data: Some(data),
19| 1| error: None,
20| 1| timestamp: chrono::Utc::now(),
21| 1| }
22| 1| }
23| |
24| | /// Create an error response
25| 1| pub fn error(error: AppError) → ApiResponse<()> {
26| 1| ApiResponse {
27| 1| success: false,
28| 1| data: None,
29| 1| error: Some(error.to_string()),
30| 1| timestamp: chrono::Utc::now(),
31| 1| }
32| 1| }
33| |}
34| |
35| |/// Paginated response wrapper
36| |#[derive(Debug, Clone, Serialize, Deserialize)]
37| |pub struct PaginatedResponse {
38| | pub items: Vec,
39| | pub total: usize,
40| | pub page: usize,
41| | pub per_page: usize,
42| | pub total_pages: usize,
43| |}
44| |
45| |impl PaginatedResponse {
46| 0| pub fn new(items: Vec, total: usize, page: usize, per_page: usize) → Self {
47| 0| let total_pages = total.div_ceil(per_page);
48| |
49| 0| Self {
50| 0| items,
51| 0| total,
52| 0| page,
53| 0| per_page,
54| 0| total_pages,
55| 0| }
56| 0| }
57| |}
58| |
59| |/// Pagination parameters
60| |#[derive(Debug, Clone, Serialize, Deserialize)]
61| |pub struct PaginationParams {
62| | pub page: Option,
63| | pub per_page: Option,
64| |}
65| |
66| |impl Default for PaginationParams {
67| 1| fn default() → Self {
68| 1| Self {
69| 1| page: Some(1),
70| 1| per_page: Some(20),
71| 1| }
72| 1| }
73| |}
74| |
75| |impl PaginationParams {
76| 4| pub fn page(&self) → usize {
77| 4| self.page.unwrap_or(1).max(1)
78| 4| }
79| |
80| 4| pub fn per_page(&self) → usize {
81| 4| self.per_page.unwrap_or(20).clamp(1, 100)
82| 4| }
83| |
84| 2| pub fn offset(&self) → usize {
85| 2| (self.page() - 1) * self.per_page()
86| 2| }
87| |}
88| |
89| |#[cfg(test)]
90| |mod tests {
91| | use super::*;
92| | use crate::error::AppError;
93| |
94| | #[test]
95| 1| fn test_success_response() {
96| 1| let response = ApiResponse::success(“test data”);
97| 1| assert!(response.success);
98| 1| assert_eq!(response.data, Some(“test data”));
99| 1| assert!(response.error.is_none());
100| 1| }
101| |
102| | #[test]
103| 1| fn test_error_response() {
104| 1| let error = AppError::validation(“Invalid input”);
105| 1| let response = ApiResponse::<()>::error(error);
106| 1| assert!(!response.success);
107| 1| assert!(response.data.is_none());
108| 1| assert!(response.error.is_some());
109| 1| }
110| |
111| | #[test]
112| 1| fn test_pagination() {
113| 1| let params = PaginationParams {
114| 1| page: Some(2),
115| 1| per_page: Some(10),
116| 1| };
117| |
118| 1| assert_eq!(params.page(), 2);
119| 1| assert_eq!(params.per_page(), 10);
120| 1| assert_eq!(params.offset(), 10);
121| 1| }
122| |
123| | #[test]
124| 1| fn test_pagination_defaults() {
125| 1| let params = PaginationParams::default();
126| 1| assert_eq!(params.page(), 1);
127| 1| assert_eq!(params.per_page(), 20);
128| 1| assert_eq!(params.offset(), 0);
129| 1| }
130| |}

/home/sonar/libs/common/src/types.rs:
1| |use serde::{Deserialize, Serialize};
2| |use uuid::Uuid;
3| |
4| |/// User ID type
5| |#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
6| |pub struct UserId(pub Uuid);
7| |
8| |impl UserId {
9| 5| pub fn new() → Self {
10| 5| Self(Uuid::new_v4())
11| 5| }
12| |
13| 0| pub fn from_uuid(uuid: Uuid) → Self {
14| 0| Self(uuid)
15| 0| }
16| |
17| 3| pub fn inner(&self) → Uuid {
18| 3| self.0
19| 3| }
20| |}
21| |
22| |impl Default for UserId {
23| 0| fn default() → Self {
24| 0| Self::new()
25| 0| }
26| |}
27| |
28| |impl std::fmt::Display for UserId {
29| 0| fn fmt(&self, f: &mut std::fmt::Formatter<'_>) → std::fmt::Result {
30| 0| write!(f, “{}”, self.0)
31| 0| }
32| |}
33| |
34| |impl From for UserId {
35| 0| fn from(uuid: Uuid) → Self {
36| 0| Self(uuid)
37| 0| }
38| |}
39| |
40| |impl From for Uuid {
41| 0| fn from(user_id: UserId) → Self {
42| 0| user_id.0
43| 0| }
44| |}
45| |
46| |/// User role enumeration
47| |#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
48| |pub enum UserRole {
49| | Admin,
50| | User,
51| | Moderator,
52| | Guest,
53| |}
54| |
55| |impl Default for UserRole {
56| 0| fn default() → Self {
57| 0| Self::Guest
58| 0| }
59| |}
60| |
61| |impl UserRole {
62| 2| pub fn can_access_admin(&self) → bool {
63| 2| matches!(self, UserRole::Admin)
^1
64| 2| }
65| |
66| 2| pub fn can_moderate(&self) → bool {
67| 2| matches!(self, UserRole::Admin | UserRole::Moderator)
^1
68| 2| }
69| |
70| 2| pub fn can_write(&self) → bool {
71| 2| matches!(self, UserRole::Admin | UserRole::Moderator | UserRole::User)
^1
72| 2| }
73| |}
74| |
75| |/// Authentication token type
76| |#[derive(Debug, Clone, Serialize, Deserialize)]
77| |pub struct AuthToken {
78| | pub token: String,
79| | pub expires_at: chrono::DateTimechrono::Utc,
80| | pub user_id: UserId,
81| |}
82| |
83| |impl AuthToken {
84| 2| pub fn new(token: String, expires_at: chrono::DateTimechrono::Utc, user_id: UserId) → Self {
85| 2| Self {
86| 2| token,
87| 2| expires_at,
88| 2| user_id,
89| 2| }
90| 2| }
91| |
92| 2| pub fn is_expired(&self) → bool {
93| 2| chrono::Utc::now() > self.expires_at
94| 2| }
95| |}
96| |
97| |#[cfg(test)]
98| |mod tests {
99| | use super::*;
100| |
101| | #[test]
102| 1| fn test_user_id_creation() {
103| 1| let id1 = UserId::new();
104| 1| let id2 = UserId::new();
105| 1| assert_ne!(id1, id2);
106| 1| }
107| |
108| | #[test]
109| 1| fn test_user_role_permissions() {
110| 1| let admin = UserRole::Admin;
111| 1| assert!(admin.can_access_admin());
112| 1| assert!(admin.can_moderate());
113| 1| assert!(admin.can_write());
114| |
115| 1| let guest = UserRole::Guest;
116| 1| assert!(!guest.can_access_admin());
117| 1| assert!(!guest.can_moderate());
118| 1| assert!(!guest.can_write());
119| 1| }
120| |
121| | #[test]
122| 1| fn test_auth_token_expiry() {
123| 1| let user_id = UserId::new();
124| 1| let expired_token = AuthToken::new(
125| 1| “test_token”.to_string(),
126| 1| chrono::Utc::now() - chrono::Duration::hours(1),
127| 1| user_id,
128| | );
129| |
130| 1| assert!(expired_token.is_expired());
131| |
132| 1| let valid_token = AuthToken::new(
133| 1| “test_token”.to_string(),
134| 1| chrono::Utc::now() + chrono::Duration::hours(1),
135| 1| user_id,
136| | );
137| |
138| 1| assert!(!valid_token.is_expired());
139| 1| }
140| |}

/home/sonar/libs/common/src/utils.rs:
1| |use std::time::{Duration, SystemTime, UNIX_EPOCH};
2| |
3| |/// Generate a random string of given length
4| 2|pub fn generate_random_string(length: usize) → String {
5| | use rand::Rng;
6| | const CHARSET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ
7| | abcdefghijklmnopqrstuvwxyz
8| | 0123456789";
9| 2| let mut rng = rand::thread_rng();
10| |
11| 2| (0..length)
12| 20| .map(|| {
^2
13| 20| let idx = rng.gen_range(0..CHARSET.len());
14| 20| CHARSET[idx] as char
15| 20| })
16| 2| .collect()
17| 2|}
18| |
19| |/// Convert timestamp to human readable format
20| 0|pub fn format_timestamp(timestamp: i64) → String {
21| 0| let datetime = chrono::DateTime::from_timestamp(timestamp, 0).unwrap_or_else(chrono::Utc::now);
22| 0| datetime.format(“%Y-%m-%d %H:%M:%S UTC”).to_string()
23| 0|}
24| |
25| |/// Get current timestamp in seconds
26| 1|pub fn current_timestamp() → i64 {
27| 1| SystemTime::now()
28| 1| .duration_since(UNIX_EPOCH)
29| 1| .unwrap_or(Duration::from_secs(0))
30| 1| .as_secs() as i64
31| 1|}
32| |
33| |/// Hash password using a simple algorithm (in production, use bcrypt or similar)
34| 3|pub fn hash_password(password: &str) → String {
35| | use std::collections::hash_map::DefaultHasher;
36| | use std::hash::{Hash, Hasher};
37| |
38| 3| let mut hasher = DefaultHasher::new();
39| 3| password.hash(&mut hasher);
40| 3| format!(“{:x}”, hasher.finish())
41| 3|}
42| |
43| |/// Verify password against hash
44| 2|pub fn verify_password(password: &str, hash: &str) → bool {
45| 2| hash_password(password) == hash
46| 2|}
47| |
48| |/// Truncate string to given length with ellipsis
49| 3|pub fn truncate_string(s: &str, max_len: usize) → String {
50| 3| if s.len() <= max_len {
51| 2| s.to_string()
52| 1| } else if max_len < 3 {
53| 0| s.chars().take(max_len).collect()
54| | } else {
55| 1| format!(“{}…”, s.chars().take(max_len - 3).collect::())
56| | }
57| 3|}
58| |
59| |/// Sanitize string for safe usage (remove dangerous characters)
60| 0|pub fn sanitize_string(input: &str) → String {
61| 0| input
62| 0| .chars()
63| 0| .filter(|c| {
64| 0| c.is_alphanumeric() || c.is_whitespace() || ".,!?-
@#$%^&*()+={}|:;"'<>".contains(c)
65| 0| })
66| 0| .collect()
67| 0|}
68| |
69| |/// Convert snake_case to camelCase
70| 3|pub fn snake_to_camel(snake_str: &str) → String {
71| 3| let mut result = String::new();
72| 3| let mut capitalize_next = false;
73| |
74| 24| for c in snake_str.chars() {
^3 ^3
75| 24| if c == ‘_’ {
76| 2| capitalize_next = true;
77| 22| } else if capitalize_next {
78| 2| result.push(c.to_uppercase().next().unwrap_or(c));
79| 2| capitalize_next = false;
80| 20| } else {
81| 20| result.push(c);
82| 20| }
83| | }
84| |
85| 3| result
86| 3|}
87| |
88| |#[cfg(test)]
89| |mod tests {
90| | use super::
;
91| |
92| | #[test]
93| 1| fn test_generate_random_string() {
94| 1| let s1 = generate_random_string(10);
95| 1| let s2 = generate_random_string(10);
96| |
97| 1| assert_eq!(s1.len(), 10);
98| 1| assert_eq!(s2.len(), 10);
99| 1| assert_ne!(s1, s2); // Very unlikely to be the same
100| 1| }
101| |
102| | #[test]
103| 1| fn test_password_hashing() {
104| 1| let password = “test_password”;
105| 1| let hash = hash_password(password);
106| |
107| 1| assert!(verify_password(password, &hash));
108| 1| assert!(!verify_password(“wrong_password”, &hash));
109| 1| }
110| |
111| | #[test]
112| 1| fn test_truncate_string() {
113| 1| assert_eq!(truncate_string(“hello”, 10), “hello”);
114| 1| assert_eq!(truncate_string(“hello world”, 8), “hello…”);
115| 1| assert_eq!(truncate_string(“hi”, 2), “hi”);
116| 1| }
117| |
118| | #[test]
119| 1| fn test_snake_to_camel() {
120| 1| assert_eq!(snake_to_camel(“hello_world”), “helloWorld”);
121| 1| assert_eq!(snake_to_camel(“user_id”), “userId”);
122| 1| assert_eq!(snake_to_camel(“simple”), “simple”);
123| 1| }
124| |
125| | #[test]
126| 1| fn test_current_timestamp() {
127| 1| let ts = current_timestamp();
128| 1| assert!(ts > 0);
129| 1| }
130| |}

/home/sonar/libs/common/src/validation.rs:
1| |use crate::error::{AppError, AppResult};
2| |use regex::Regex;
3| |use std::collections::HashMap;
4| |
5| |/// Email validation utility
6| 5|pub fn validate_email(email: &str) → AppResult<()> {
7| 5| let email_regex = Regex::new(r"[1]+@[a-zA-Z0-9.-]+.[a-zA-Z]{2,}$“)
8| 5| .map_err(|| AppError::internal(“Failed to compile email regex”))?;
^0 ^0
9| |
10| 5| if email_regex.is_match(email) {
11| 2| Ok(())
12| | } else {
13| 3| Err(AppError::validation(“Invalid email format”))
14| | }
15| 5|}
16| |
17| |/// Password strength validation
18| 6|pub fn validate_password(password: &str) → AppResult<()> {
19| 6| let mut errors = Vec::new();
20| |
21| 6| if password.len() < 8 {
22| 1| errors.push(“Password must be at least 8 characters long”);
23| 5| }
24| |
25| 23| if !password.chars().any(|c| c.is_uppercase()) {
^6 ^6
26| 2| errors.push(“Password must contain at least one uppercase letter”);
27| 4| }
28| |
29| 23| if !password.chars().any(|c| c.is_lowercase()) {
^6 ^6
30| 1| errors.push(“Password must contain at least one lowercase letter”);
31| 5| }
32| |
33| 64| if !password.chars().any(|c| c.is_numeric()) {
^6 ^6
34| 2| errors.push(“Password must contain at least one number”);
35| 4| }
36| |
37| 6| if !password
38| 6| .chars()
39| 75| .any(|c| "!@#$%^&*()
±={}|;:,.<>?”.contains(c))
^6
40| 2| {
41| 2| errors.push(“Password must contain at least one special character”);
42| 4| }
43| |
44| 6| if errors.is_empty() {
45| 1| Ok(())
46| | } else {
47| 5| Err(AppError::validation(errors.join(“, “)))
48| | }
49| 6|}
50| |
51| |/// Generic field validation
52| |pub struct Validator {
53| | errors: HashMap<String, Vec>,
54| |}
55| |
56| |impl Validator {
57| 1| pub fn new() → Self {
58| 1| Self {
59| 1| errors: HashMap::new(),
60| 1| }
61| 1| }
62| |
63| 2| pub fn validate_required(&mut self, field: &str, value: Option<&str>) → &mut Self {
64| 2| if value.is_none() || value.unwrap().trim().is_empty() {
^1 ^1
65| 1| self.add_error(field, “This field is required”);
66| 1| }
67| 2| self
68| 2| }
69| |
70| 1| pub fn validate_min_length(
71| 1| &mut self,
72| 1| field: &str,
73| 1| value: &str,
74| 1| min_length: usize,
75| 1| ) → &mut Self {
76| 1| if value.len() < min_length {
77| 1| self.add_error(
78| 1| field,
79| 1| &format!(“Must be at least {min_length} characters long”),
80| 1| );
81| 1| }
^0
82| 1| self
83| 1| }
84| |
85| 0| pub fn validate_max_length(
86| 0| &mut self,
87| 0| field: &str,
88| 0| value: &str,
89| 0| max_length: usize,
90| 0| ) → &mut Self {
91| 0| if value.len() > max_length {
92| 0| self.add_error(
93| 0| field,
94| 0| &format!(“Must be no more than {max_length} characters long”),
95| 0| );
96| 0| }
97| 0| self
98| 0| }
99| |
100| 0| pub fn validate_email_field(&mut self, field: &str, email: &str) → &mut Self {
101| 0| if let Err(error) = validate_email(email) {
102| 0| self.add_error(field, &error.to_string());
103| 0| }
104| 0| self
105| 0| }
106| |
107| 2| fn add_error(&mut self, field: &str, message: &str) {
108| 2| self.errors
109| 2| .entry(field.to_string())
110| 2| .or_default()
111| 2| .push(message.to_string());
112| 2| }
113| |
114| 2| pub fn is_valid(&self) → bool {
115| 2| self.errors.is_empty()
116| 2| }
117| |
118| 1| pub fn into_result(self) → AppResult<()> {
119| 1| if self.is_valid() {
120| 0| Ok(())
121| | } else {
122| 1| let error_message = self
123| 1| .errors
124| 1| .iter()
125| 2| .map(|(field, errors)| format!(”{}: {}”, field, errors.join(“, “)))
^1
126| 1| .collect::<Vec<_>>()
127| 1| .join(”; “);
128| |
129| 1| Err(AppError::validation(error_message))
130| | }
131| 1| }
132| |}
133| |
134| |impl Default for Validator {
135| 0| fn default() → Self {
136| 0| Self::new()
137| 0| }
138| |}
139| |
140| |#[cfg(test)]
141| |mod tests {
142| | use super::*;
143| |
144| | #[test]
145| 1| fn test_valid_email() {
146| 1| assert!(validate_email(“test@example.com”).is_ok());
147| 1| assert!(validate_email(“user.name+tag@domain.co.uk”).is_ok());
148| 1| }
149| |
150| | #[test]
151| 1| fn test_invalid_email() {
152| 1| assert!(validate_email(“invalid-email”).is_err());
153| 1| assert!(validate_email(”@domain.com”).is_err());
154| 1| assert!(validate_email(“test@”).is_err());
155| 1| }
156| |
157| | #[test]
158| 1| fn test_valid_password() {
159| 1| assert!(validate_password(“SecurePass123!”).is_ok());
160| 1| }
161| |
162| | #[test]
163| 1| fn test_invalid_password() {
164| 1| assert!(validate_password(“weak”).is_err());
165| 1| assert!(validate_password(“nouppercase123!”).is_err());
166| 1| assert!(validate_password(“NOLOWERCASE123!”).is_err());
167| 1| assert!(validate_password(“NoNumbers!”).is_err());
168| 1| assert!(validate_password(“NoSpecialChars123”).is_err());
169| 1| }
170| |
171| | #[test]
172| 1| fn test_validator() {
173| 1| let mut validator = Validator::new();
174| |
175| 1| validator
176| 1| .validate_required(“name”, Some(“John”))
177| 1| .validate_required(“email”, None)
178| 1| .validate_min_length(“password”, “123”, 8);
179| |
180| 1| assert!(!validator.is_valid());
181| 1| assert!(validator.into_result().is_err());
182| 1| }
183| |}

/home/sonar/libs/config/src/auth.rs:
1| |use serde::{Deserialize, Serialize};
2| |
3| |/// Authentication service configuration
4| |#[derive(Debug, Clone, Serialize, Deserialize, Default)]
5| |pub struct AuthConfig {
6| | pub server: ServerConfig,
7| | pub jwt: JwtConfig,
8| | pub database: DatabaseConfig,
9| | pub security: SecurityConfig,
10| |}
11| |
12| |/// Server configuration
13| |#[derive(Debug, Clone, Serialize, Deserialize)]
14| |pub struct ServerConfig {
15| | pub host: String,
16| | pub port: u16,
17| | pub workers: Option,
18| | pub timeout_seconds: u64,
19| |}
20| |
21| |impl Default for ServerConfig {
22| 1| fn default() → Self {
23| 1| Self {
24| 1| host: “0.0.0.0”.to_string(),
25| 1| port: 8001,
26| 1| workers: None,
27| 1| timeout_seconds: 30,
28| 1| }
29| 1| }
30| |}
31| |
32| |/// JWT configuration
33| |#[derive(Debug, Clone, Serialize, Deserialize)]
34| |pub struct JwtConfig {
35| | pub secret: String,
36| | pub expiration_hours: i64,
37| | pub refresh_expiration_days: i64,
38| | pub algorithm: String,
39| |}
40| |
41| |impl Default for JwtConfig {
42| 1| fn default() → Self {
43| 1| Self {
44| 1| secret: “your-secret-key-change-in-production”.to_string(),
45| 1| expiration_hours: 24,
46| 1| refresh_expiration_days: 7,
47| 1| algorithm: “HS256”.to_string(),
48| 1| }
49| 1| }
50| |}
51| |
52| |/// Database configuration for auth service
53| |#[derive(Debug, Clone, Serialize, Deserialize)]
54| |pub struct DatabaseConfig {
55| | pub url: Option,
56| | pub host: String,
57| | pub port: u16,
58| | pub username: String,
59| | pub password: String,
60| | pub database_name: String,
61| | pub max_connections: u32,
62| | pub min_connections: u32,
63| | pub connection_timeout_seconds: u64,
64| |}
65| |
66| |impl Default for DatabaseConfig {
67| 2| fn default() → Self {
68| 2| Self {
69| 2| url: None,
70| 2| host: “localhost”.to_string(),
71| 2| port: 5432,
72| 2| username: “auth_user”.to_string(),
73| 2| password: “auth_password”.to_string(),
74| 2| database_name: “auth_db”.to_string(),
75| 2| max_connections: 10,
76| 2| min_connections: 1,
77| 2| connection_timeout_seconds: 30,
78| 2| }
79| 2| }
80| |}
81| |
82| |impl DatabaseConfig {
83| 1| pub fn database_url(&self) → String {
84| 1| self.url.clone().unwrap_or_else(|| {
85| 1| format!(
86| 1| “postgresql://{}:{}@{}:{}/{}”,
87| | self.username, self.password, self.host, self.port, self.database_name
88| | )
89| 1| })
90| 1| }
91| |}
92| |
93| |/// Security configuration
94| |#[derive(Debug, Clone, Serialize, Deserialize)]
95| |pub struct SecurityConfig {
96| | pub password_min_length: usize,
97| | pub password_require_uppercase: bool,
98| | pub password_require_lowercase: bool,
99| | pub password_require_numbers: bool,
100| | pub password_require_special: bool,
101| | pub max_login_attempts: u32,
102| | pub lockout_duration_minutes: u64,
103| | pub session_timeout_hours: i64,
104| |}
105| |
106| |impl Default for SecurityConfig {
107| 2| fn default() → Self {
108| 2| Self {
109| 2| password_min_length: 8,
110| 2| password_require_uppercase: true,
111| 2| password_require_lowercase: true,
112| 2| password_require_numbers: true,
113| 2| password_require_special: true,
114| 2| max_login_attempts: 5,
115| 2| lockout_duration_minutes: 15,
116| 2| session_timeout_hours: 24,
117| 2| }
118| 2| }
119| |}
120| |
121| |impl AuthConfig {
122| | /// Load from environment variables with fallback to defaults
123| 0| pub fn from_env() → Self {
124| | Self {
125| | server: ServerConfig {
126| 0| host: std::env::var(“AUTH_HOST”).unwrap_or_else(|| “0.0.0.0”.to_string()),
127| 0| port: std::env::var(“AUTH_PORT”)
128| 0| .unwrap_or_else(|
| “8001”.to_string())
129| 0| .parse()
130| 0| .unwrap_or(8001),
131| 0| workers: std::env::var(“AUTH_WORKERS”)
132| 0| .ok()
133| 0| .and_then(|w| w.parse().ok()),
134| 0| timeout_seconds: std::env::var(“AUTH_TIMEOUT”)
135| 0| .unwrap_or_else(|| “30”.to_string())
136| 0| .parse()
137| 0| .unwrap_or(30),
138| | },
139| | jwt: JwtConfig {
140| 0| secret: std::env::var(“JWT_SECRET”)
141| 0| .unwrap_or_else(|
| “your-secret-key-change-in-production”.to_string()),
142| 0| expiration_hours: std::env::var(“JWT_EXPIRATION_HOURS”)
143| 0| .unwrap_or_else(|| “24”.to_string())
144| 0| .parse()
145| 0| .unwrap_or(24),
146| 0| refresh_expiration_days: std::env::var(“JWT_REFRESH_EXPIRATION_DAYS”)
147| 0| .unwrap_or_else(|
| “7”.to_string())
148| 0| .parse()
149| 0| .unwrap_or(7),
150| 0| algorithm: std::env::var(“JWT_ALGORITHM”).unwrap_or_else(|| “HS256”.to_string()),
151| | },
152| | database: DatabaseConfig {
153| 0| url: std::env::var(“DATABASE_URL”).ok(),
154| 0| host: std::env::var(“DATABASE_HOST”).unwrap_or_else(|
| “localhost”.to_string()),
155| 0| port: std::env::var(“DATABASE_PORT”)
156| 0| .unwrap_or_else(|| “5432”.to_string())
157| 0| .parse()
158| 0| .unwrap_or(5432),
159| 0| username: std::env::var(“DATABASE_USERNAME”)
160| 0| .unwrap_or_else(|
| “auth_user”.to_string()),
161| 0| password: std::env::var(“DATABASE_PASSWORD”)
162| 0| .unwrap_or_else(|| “auth_password”.to_string()),
163| 0| database_name: std::env::var(“DATABASE_NAME”)
164| 0| .unwrap_or_else(|
| “auth_db”.to_string()),
165| 0| max_connections: std::env::var(“DATABASE_MAX_CONNECTIONS”)
166| 0| .unwrap_or_else(|| “10”.to_string())
167| 0| .parse()
168| 0| .unwrap_or(10),
169| 0| min_connections: std::env::var(“DATABASE_MIN_CONNECTIONS”)
170| 0| .unwrap_or_else(|
| “1”.to_string())
171| 0| .parse()
172| 0| .unwrap_or(1),
173| 0| connection_timeout_seconds: std::env::var(“DATABASE_TIMEOUT”)
174| 0| .unwrap_or_else(|_| “30”.to_string())
175| 0| .parse()
176| 0| .unwrap_or(30),
177| | },
178| 0| security: SecurityConfig::default(),
179| | }
180| 0| }
181| |}
182| |
183| |#[cfg(test)]
184| |mod tests {
185| | use super::*;
186| |
187| | #[test]
188| 1| fn test_default_auth_config() {
189| 1| let config = AuthConfig::default();
190| 1| assert_eq!(config.server.port, 8001);
191| 1| assert_eq!(config.jwt.expiration_hours, 24);
192| 1| assert_eq!(config.security.password_min_length, 8);
193| 1| }
194| |
195| | #[test]
196| 1| fn test_database_url_generation() {
197| 1| let config = DatabaseConfig::default();
198| 1| let url = config.database_url();
199| 1| assert!(url.contains(“postgresql://”));
200| 1| assert!(url.contains(“auth_user:auth_password”));
201| 1| }
202| |
203| | #[test]
204| 1| fn test_security_defaults() {
205| 1| let security = SecurityConfig::default();
206| 1| assert!(security.password_require_uppercase);
207| 1| assert!(security.password_require_lowercase);
208| 1| assert!(security.password_require_numbers);
209| 1| assert!(security.password_require_special);
210| 1| assert_eq!(security.max_login_attempts, 5);
211| 1| }
212| |}

/home/sonar/libs/config/src/database.rs:
1| |use serde::{Deserialize, Serialize};
2| |use std::collections::HashMap;
3| |
4| |/// Database configuration settings
5| |#[derive(Debug, Clone, Serialize, Deserialize)]
6| |pub struct DatabaseConfig {
7| | /// Database URL (e.g., postgres://user:pass@host:port/db)
8| | pub url: String,
9| |
10| | /// Maximum number of connections in the pool
11| | pub max_connections: u32,
12| |
13| | /// Minimum number of connections in the pool
14| | pub min_connections: u32,
15| |
16| | /// Connection timeout in seconds
17| | pub connect_timeout: u64,
18| |
19| | /// Query timeout in seconds
20| | pub query_timeout: u64,
21| |
22| | /// Whether to run migrations on startup
23| | pub run_migrations: bool,
24| |
25| | /// Additional connection parameters
26| | pub extra_params: HashMap<String, String>,
27| |}
28| |
29| |impl Default for DatabaseConfig {
30| 5| fn default() → Self {
31| 5| Self {
32| 5| url: “sqlite://data.db”.to_string(),
33| 5| max_connections: 10,
34| 5| min_connections: 1,
35| 5| connect_timeout: 30,
36| 5| query_timeout: 30,
37| 5| run_migrations: true,
38| 5| extra_params: HashMap::new(),
39| 5| }
40| 5| }
41| |}
42| |
43| |impl DatabaseConfig {
44| | /// Create a new database configuration
45| 1| pub fn new(url: String) → Self {
46| 1| Self {
47| 1| url,
48| 1| ..Default::default()
49| 1| }
50| 1| }
51| |
52| | /// Set maximum connections
53| 1| pub fn with_max_connections(mut self, max: u32) → Self {
54| 1| self.max_connections = max;
55| 1| self
56| 1| }
57| |
58| | /// Set minimum connections
59| 1| pub fn with_min_connections(mut self, min: u32) → Self {
60| 1| self.min_connections = min;
61| 1| self
62| 1| }
63| |
64| | /// Set connection timeout
65| 1| pub fn with_connect_timeout(mut self, timeout: u64) → Self {
66| 1| self.connect_timeout = timeout;
67| 1| self
68| 1| }
69| |
70| | /// Set query timeout
71| 0| pub fn with_query_timeout(mut self, timeout: u64) → Self {
72| 0| self.query_timeout = timeout;
73| 0| self
74| 0| }
75| |
76| | /// Enable or disable migrations
77| 0| pub fn with_migrations(mut self, run: bool) → Self {
78| 0| self.run_migrations = run;
79| 0| self
80| 0| }
81| |
82| | /// Add extra connection parameter
83| 0| pub fn with_extra_param(mut self, key: String, value: String) → Self {
84| 0| self.extra_params.insert(key, value);
85| 0| self
86| 0| }
87| |
88| | /// Validate the configuration
89| 3| pub fn validate(&self) → Result<(), String> {
90| 3| if self.url.is_empty() {
91| 1| return Err(“Database URL cannot be empty”.to_string());
92| 2| }
93| |
94| 2| if self.max_connections == 0 {
95| 0| return Err(“Max connections must be greater than 0”.to_string());
96| 2| }
97| |
98| 2| if self.min_connections > self.max_connections {
99| 1| return Err(“Min connections cannot be greater than max connections”.to_string());
100| 1| }
101| |
102| 1| Ok(())
103| 3| }
104| |}
105| |
106| |#[cfg(test)]
107| |mod tests {
108| | use super::*;
109| |
110| | #[test]
111| 1| fn test_default_config() {
112| 1| let config = DatabaseConfig::default();
113| 1| assert_eq!(config.url, “sqlite://data.db”);
114| 1| assert_eq!(config.max_connections, 10);
115| 1| assert_eq!(config.min_connections, 1);
116| 1| assert!(config.run_migrations);
117| 1| }
118| |
119| | #[test]
120| 1| fn test_builder_pattern() {
121| 1| let config = DatabaseConfig::new(“postgres://localhost/test”.to_string())
122| 1| .with_max_connections(20)
123| 1| .with_min_connections(5)
124| 1| .with_connect_timeout(60);
125| |
126| 1| assert_eq!(config.url, “postgres://localhost/test”);
127| 1| assert_eq!(config.max_connections, 20);
128| 1| assert_eq!(config.min_connections, 5);
129| 1| assert_eq!(config.connect_timeout, 60);
130| 1| }
131| |
132| | #[test]
133| 1| fn test_validation() {
134| 1| let valid_config = DatabaseConfig::default();
135| 1| assert!(valid_config.validate().is_ok());
136| |
137| 1| let invalid_config = DatabaseConfig {
138| 1| url: “”.to_string(),
139| 1| ..Default::default()
140| 1| };
141| 1| assert!(invalid_config.validate().is_err());
142| |
143| 1| let invalid_config2 = DatabaseConfig {
144| 1| min_connections: 15,
145| 1| max_connections: 10,
146| 1| ..Default::default()
147| 1| };
148| 1| assert!(invalid_config2.validate().is_err());
149| 1| }
150| |}

/home/sonar/libs/config/src/loader.rs:
1| |use anyhow::{Context, Result};
2| |use serde::de::DeserializeOwned;
3| |use std::path::Path;
4| |use tracing::{info, warn};
5| |
6| |/// Configuration source types
7| |#[derive(Debug, Clone)]
8| |pub enum ConfigSource {
9| | File(String),
10| | Environment,
11| | Default,
12| |}
13| |
14| |/// Configuration loader with support for multiple sources
15| |pub struct ConfigLoader;
16| |
17| |impl ConfigLoader {
18| | /// Load configuration from a YAML file
19| 1| pub fn from_yaml<T: DeserializeOwned>(path: &str) → Result {
20| 1| info!(“Loading configuration from YAML file: {}”, path);
^0
21| |
22| 1| let content = std::fs::read_to_string(path)
23| 1| .with_context(|| format!(“Failed to read config file: {path}”))?;
^0 ^0
24| |
25| 1| serde_yaml::from_str(&content)
26| 1| .with_context(|| format!(“Failed to parse YAML config: {path}”))
^0
27| 1| }
28| |
29| | /// Load configuration from a TOML file
30| 0| pub fn from_toml<T: DeserializeOwned>(path: &str) → Result {
31| 0| info!(“Loading configuration from TOML file: {}”, path);
32| |
33| 0| let content = std::fs::read_to_string(path)
34| 0| .with_context(|| format!(“Failed to read config file: {path}”))?;
35| |
36| 0| toml::from_str(&content).with_context(|| format!(“Failed to parse TOML config: {path}”))
37| 0| }
38| |
39| | /// Load configuration from a JSON file
40| 0| pub fn from_json<T: DeserializeOwned>(path: &str) → Result {
41| 0| info!(“Loading configuration from JSON file: {}”, path);
42| |
43| 0| let content = std::fs::read_to_string(path)
44| 0| .with_context(|| format!(“Failed to read config file: {path}”))?;
45| |
46| 0| serde_json::from_str(&content)
47| 0| .with_context(|| format!(“Failed to parse JSON config: {path}”))
48| 0| }
49| |
50| | /// Load configuration with multiple fallback sources
51| 1| pub fn load_with_fallback<T: DeserializeOwned + Default>(
52| 1| sources: &[ConfigSource],
53| 1| ) → Result {
54| 2| for source in sources {
55| 2| match source {
56| 1| ConfigSource::File(path) => {
57| 1| if Path::new(path).exists() {
58| 0| return Self::from_file(path);
59| | } else {
60| 1| warn!(“Config file not found: {}”, path);
^0
61| | }
62| | }
63| | ConfigSource::Environment => {
64| | // In a real implementation, you’d deserialize from env vars
65| | // For now, we’ll skip this
66| 0| continue;
67| | }
68| | ConfigSource::Default => {
69| 1| info!(“Using default configuration”);
^0
70| 1| return Ok(T::default());
71| | }
72| | }
73| | }
74| |
75| | // If all sources fail, use default
76| 0| Ok(T::default())
77| 1| }
78| |
79| | /// Auto-detect file format and load
80| 0| fn from_file<T: DeserializeOwned>(path: &str) → Result {
81| 0| let path_obj = Path::new(path);
82| |
83| 0| match path_obj.extension().and_then(|ext| ext.to_str()) {
84| 0| Some(“yaml”) | Some(“yml”) => Self::from_yaml(path),
85| 0| Some(“toml”) => Self::from_toml(path),
86| 0| Some(“json”) => Self::from_json(path),
87| 0| _ => Err(anyhow::anyhow!(
88| 0| “Unsupported config file format. Supported: yaml, yml, toml, json”
89| 0| )),
90| | }
91| 0| }
92| |
93| | /// Get configuration directory path
94| 2| pub fn config_dir() → String {
95| 2| std::env::var(“CONFIG_DIR”).unwrap_or_else(|| “config”.to_string())
96| 2| }
97| |
98| | /// Get environment-specific config file path
99| 1| pub fn env_config_path(service_name: &str) → String {
100| 1| let env = std::env::var(“ENVIRONMENT”).unwrap_or_else(|
| “development”.to_string());
101| 1| let config_dir = Self::config_dir();
102| 1| format!(“{config_dir}/{service_name}-{env}.yaml”)
103| 1| }
104| |}
105| |
106| |#[cfg(test)]
107| |mod tests {
108| | use super::*;
109| | use serde::{Deserialize, Serialize};
110| | use std::fs;
111| |
112| | #[derive(Debug, Serialize, Deserialize, PartialEq, Default)]
113| | struct TestConfig {
114| | name: String,
115| | port: u16,
116| | debug: bool,
117| | }
118| |
119| | #[test]
120| 1| fn test_yaml_loading() {
121| 1| let yaml_content = r#"
122| 1|name: “test-service”
123| 1|port: 8080
124| 1|debug: true
125| 1|“#;
126| |
127| 1| let temp_file = “test_config.yaml”;
128| 1| fs::write(temp_file, yaml_content).unwrap();
129| |
130| 1| let config: TestConfig = ConfigLoader::from_yaml(temp_file).unwrap();
131| |
132| 1| assert_eq!(config.name, “test-service”);
133| 1| assert_eq!(config.port, 8080);
134| 1| assert!(config.debug);
135| |
136| 1| fs::remove_file(temp_file).unwrap();
137| 1| }
138| |
139| | #[test]
140| 1| fn test_fallback_to_default() {
141| 1| let sources = vec![
142| 1| ConfigSource::File(“non_existent.yaml”.to_string()),
143| 1| ConfigSource::Default,
144| | ];
145| |
146| 1| let config: TestConfig = ConfigLoader::load_with_fallback(&sources).unwrap();
147| 1| assert_eq!(config, TestConfig::default());
148| 1| }
149| |
150| | #[test]
151| 1| fn test_config_dir() {
152| 1| let dir = ConfigLoader::config_dir();
153| 1| assert!(!dir.is_empty());
154| 1| }
155| |
156| | #[test]
157| 1| fn test_env_config_path() {
158| 1| let path = ConfigLoader::env_config_path(“auth-service”);
159| 1| assert!(path.contains(“auth-service”));
160| 1| assert!(path.ends_with(”.yaml"));
161| 1| }
162| |}

/home/sonar/libs/config/src/service.rs:
1| |use serde::{Deserialize, Serialize};
2| |use std::collections::HashMap;
3| |
4| |/// Service configuration settings
5| |#[derive(Debug, Clone, Serialize, Deserialize)]
6| |pub struct ServiceConfig {
7| | /// Service name
8| | pub name: String,
9| |
10| | /// Service version
11| | pub version: String,
12| |
13| | /// Host to bind to
14| | pub host: String,
15| |
16| | /// Port to listen on
17| | pub port: u16,
18| |
19| | /// Environment (dev, test, prod)
20| | pub environment: String,
21| |
22| | /// Log level (trace, debug, info, warn, error)
23| | pub log_level: String,
24| |
25| | /// Request timeout in seconds
26| | pub request_timeout: u64,
27| |
28| | /// Enable metrics collection
29| | pub enable_metrics: bool,
30| |
31| | /// Enable health checks
32| | pub enable_health_checks: bool,
33| |
34| | /// CORS allowed origins
35| | pub cors_origins: Vec,
36| |
37| | /// Rate limiting (requests per minute)
38| | pub rate_limit: Option,
39| |
40| | /// Additional service-specific settings
41| | pub extra_settings: HashMap<String, String>,
42| |}
43| |
44| |impl Default for ServiceConfig {
45| 6| fn default() → Self {
46| 6| Self {
47| 6| name: “unknown-service”.to_string(),
48| 6| version: “0.1.0”.to_string(),
49| 6| host: “0.0.0.0”.to_string(),
50| 6| port: 8080,
51| 6| environment: “dev”.to_string(),
52| 6| log_level: “info”.to_string(),
53| 6| request_timeout: 30,
54| 6| enable_metrics: true,
55| 6| enable_health_checks: true,
56| 6| cors_origins: vec!["“.to_string()],
57| 6| rate_limit: None,
58| 6| extra_settings: HashMap::new(),
59| 6| }
60| 6| }
61| |}
62| |
63| |impl ServiceConfig {
64| | /// Create a new service configuration
65| 2| pub fn new(name: String, port: u16) → Self {
66| 2| Self {
67| 2| name,
68| 2| port,
69| 2| ..Default::default()
70| 2| }
71| 2| }
72| |
73| | /// Set the service version
74| 1| pub fn with_version(mut self, version: String) → Self {
75| 1| self.version = version;
76| 1| self
77| 1| }
78| |
79| | /// Set the host
80| 1| pub fn with_host(mut self, host: String) → Self {
81| 1| self.host = host;
82| 1| self
83| 1| }
84| |
85| | /// Set the environment
86| 2| pub fn with_environment(mut self, env: String) → Self {
87| 2| self.environment = env;
88| 2| self
89| 2| }
90| |
91| | /// Set the log level
92| 1| pub fn with_log_level(mut self, level: String) → Self {
93| 1| self.log_level = level;
94| 1| self
95| 1| }
96| |
97| | /// Set request timeout
98| 0| pub fn with_request_timeout(mut self, timeout: u64) → Self {
99| 0| self.request_timeout = timeout;
100| 0| self
101| 0| }
102| |
103| | /// Enable or disable metrics
104| 0| pub fn with_metrics(mut self, enable: bool) → Self {
105| 0| self.enable_metrics = enable;
106| 0| self
107| 0| }
108| |
109| | /// Enable or disable health checks
110| 0| pub fn with_health_checks(mut self, enable: bool) → Self {
111| 0| self.enable_health_checks = enable;
112| 0| self
113| 0| }
114| |
115| | /// Set CORS origins
116| 0| pub fn with_cors_origins(mut self, origins: Vec) → Self {
117| 0| self.cors_origins = origins;
118| 0| self
119| 0| }
120| |
121| | /// Set rate limit
122| 1| pub fn with_rate_limit(mut self, limit: u32) → Self {
123| 1| self.rate_limit = Some(limit);
124| 1| self
125| 1| }
126| |
127| | /// Add extra setting
128| 0| pub fn with_extra_setting(mut self, key: String, value: String) → Self {
129| 0| self.extra_settings.insert(key, value);
130| 0| self
131| 0| }
132| |
133| | /// Validate the configuration
134| 3| pub fn validate(&self) → Result<(), String> {
135| 3| if self.name.is_empty() {
136| 1| return Err(“Service name cannot be empty”.to_string());
137| 2| }
138| |
139| 2| if self.port == 0 {
140| 0| return Err(“Port must be greater than 0”.to_string());
141| 2| }
142| |
143| 2| if ![“trace”, “debug”, “info”, “warn”, “error”].contains(&self.log_level.as_str()) {
144| 1| return Err(“Invalid log level”.to_string());
145| 1| }
146| |
147| 1| if ![“dev”, “test”, “prod”].contains(&self.environment.as_str()) {
148| 0| return Err(“Environment must be one of: dev, test, prod”.to_string());
149| 1| }
150| |
151| 1| Ok(())
152| 3| }
153| |
154| | /// Get the service address
155| 1| pub fn address(&self) → String {
156| 1| format!(”{}:{}", self.host, self.port)
157| 1| }
158| |
159| | /// Check if running in production
160| 2| pub fn is_production(&self) → bool {
161| 2| self.environment == “prod”
162| 2| }
163| |
164| | /// Check if running in development
165| 2| pub fn is_development(&self) → bool {
166| 2| self.environment == “dev”
167| 2| }
168| |}
169| |
170| |#[cfg(test)]
171| |mod tests {
172| | use super::
;
173| |
174| | #[test]
175| 1| fn test_default_config() {
176| 1| let config = ServiceConfig::default();
177| 1| assert_eq!(config.name, “unknown-service”);
178| 1| assert_eq!(config.port, 8080);
179| 1| assert_eq!(config.environment, “dev”);
180| 1| assert_eq!(config.log_level, “info”);
181| 1| assert!(config.enable_metrics);
182| 1| assert!(config.enable_health_checks);
183| 1| }
184| |
185| | #[test]
186| 1| fn test_builder_pattern() {
187| 1| let config = ServiceConfig::new(“test-service”.to_string(), 3000)
188| 1| .with_version(“1.0.0”.to_string())
189| 1| .with_environment(“prod”.to_string())
190| 1| .with_log_level(“warn”.to_string())
191| 1| .with_rate_limit(100);
192| |
193| 1| assert_eq!(config.name, “test-service”);
194| 1| assert_eq!(config.port, 3000);
195| 1| assert_eq!(config.version, “1.0.0”);
196| 1| assert_eq!(config.environment, “prod”);
197| 1| assert_eq!(config.log_level, “warn”);
198| 1| assert_eq!(config.rate_limit, Some(100));
199| 1| }
200| |
201| | #[test]
202| 1| fn test_validation() {
203| 1| let valid_config = ServiceConfig::default();
204| 1| assert!(valid_config.validate().is_ok());
205| |
206| 1| let invalid_config = ServiceConfig {
207| 1| name: “”.to_string(),
208| 1| ..Default::default()
209| 1| };
210| 1| assert!(invalid_config.validate().is_err());
211| |
212| 1| let invalid_config2 = ServiceConfig {
213| 1| log_level: “invalid”.to_string(),
214| 1| ..Default::default()
215| 1| };
216| 1| assert!(invalid_config2.validate().is_err());
217| 1| }
218| |
219| | #[test]
220| 1| fn test_utility_methods() {
221| 1| let config =
222| 1| ServiceConfig::new(“test”.to_string(), 8080).with_host(“localhost”.to_string());
223| |
224| 1| assert_eq!(config.address(), “localhost:8080”);
225| 1| assert!(config.is_development());
226| 1| assert!(!config.is_production());
227| |
228| 1| let prod_config = config.with_environment(“prod”.to_string());
229| 1| assert!(prod_config.is_production());
230| 1| assert!(!prod_config.is_development());
231| 1| }
232| |}

/home/sonar/libs/database/src/connection.rs:
1| |use common::AppResult;
2| |use sqlx::PgPool;
3| |use std::sync::Arc;
4| |use tracing::{info, warn};
5| |
6| |/// Type alias for database pool
7| |pub type DatabasePool = Arc;
8| |
9| |/// Database connection configuration
10| |#[derive(Debug, Clone)]
11| |pub struct DatabaseConfig {
12| | pub host: String,
13| | pub port: u16,
14| | pub username: String,
15| | pub password: String,
16| | pub database_name: String,
17| | pub max_connections: u32,
18| | pub min_connections: u32,
19| |}
20| |
21| |impl Default for DatabaseConfig {
22| 1| fn default() → Self {
23| 1| Self {
24| 1| host: “localhost”.to_string(),
25| 1| port: 5432,
26| 1| username: “postgres”.to_string(),
27| 1| password: “password”.to_string(),
28| 1| database_name: “rust_monolith”.to_string(),
29| 1| max_connections: 10,
30| 1| min_connections: 1,
31| 1| }
32| 1| }
33| |}
34| |
35| |impl DatabaseConfig {
36| | /// Create database URL from configuration
37| 2| pub fn database_url(&self) → String {
38| 2| format!(
39| 2| “postgresql://{}:{}@{}:{}/{}”,
40| | self.username, self.password, self.host, self.port, self.database_name
41| | )
42| 2| }
43| |}
44| |
45| |/// Create and configure database connection pool
46| 0|pub async fn create_pool(config: &DatabaseConfig) → AppResult {
47| 0| info!(“Creating database connection pool”);
48| |
49| 0| let pool = sqlx::postgres::PgPoolOptions::new()
50| 0| .max_connections(config.max_connections)
51| 0| .min_connections(config.min_connections)
52| 0| .connect(&config.database_url())
53| 0| .await
54| 0| .map_err(|e| common::AppError::database(format!(“Failed to create pool: {e}”)))?;
55| |
56| 0| info!(“Database connection pool created successfully”);
57| 0| Ok(Arc::new(pool))
58| 0|}
59| |
60| |/// Get database pool from environment or default configuration
61| 0|pub async fn get_database_pool() → AppResult {
62| 0| let config = DatabaseConfig {
63| 0| host: std::env::var(“DATABASE_HOST”).unwrap_or_else(|| “localhost”.to_string()),
64| 0| port: std::env::var(“DATABASE_PORT”)
65| 0| .unwrap_or_else(|
| “5432”.to_string())
66| 0| .parse()
67| 0| .unwrap_or(5432),
68| 0| username: std::env::var(“DATABASE_USERNAME”).unwrap_or_else(|| “postgres”.to_string()),
69| 0| password: std::env::var(“DATABASE_PASSWORD”).unwrap_or_else(|
| “password”.to_string()),
70| 0| database_name: std::env::var(“DATABASE_NAME”)
71| 0| .unwrap_or_else(|| “rust_monolith”.to_string()),
72| 0| max_connections: std::env::var(“DATABASE_MAX_CONNECTIONS”)
73| 0| .unwrap_or_else(|
| “10”.to_string())
74| 0| .parse()
75| 0| .unwrap_or(10),
76| 0| min_connections: std::env::var(“DATABASE_MIN_CONNECTIONS”)
77| 0| .unwrap_or_else(|_| “1”.to_string())
78| 0| .parse()
79| 0| .unwrap_or(1),
80| | };
81| |
82| 0| create_pool(&config).await
83| 0|}
84| |
85| |/// Health check for database connection
86| 0|pub async fn health_check(pool: &PgPool) → AppResult<()> {
87| 0| sqlx::query(“SELECT 1”)
88| 0| .execute(pool)
89| 0| .await
90| 0| .map_err(|e| common::AppError::database(format!(“Health check failed: {e}”)))?;
91| |
92| 0| Ok(())
93| 0|}
94| |
95| |/// Close database pool gracefully
96| 0|pub async fn close_pool(pool: DatabasePool) {
97| 0| info!(“Closing database connection pool”);
98| |
99| | // Extract the inner PgPool from Arc
100| 0| if let Ok(pool) = Arc::try_unwrap(pool) {
101| 0| pool.close().await;
102| 0| info!(“Database connection pool closed successfully”);
103| | } else {
104| 0| warn!(“Could not close pool - multiple references exist”);
105| | }
106| 0|}
107| |
108| |#[cfg(test)]
109| |mod tests {
110| | use super::*;
111| |
112| | #[test]
113| 1| fn test_database_config_url() {
114| 1| let config = DatabaseConfig::default();
115| 1| let url = config.database_url();
116| 1| assert!(url.contains(“postgresql://”));
117| 1| assert!(url.contains(“postgres:password”));
118| 1| assert!(url.contains(“localhost:5432”));
119| 1| assert!(url.contains(“rust_monolith”));
120| 1| }
121| |
122| | #[test]
123| 1| fn test_custom_database_config() {
124| 1| let config = DatabaseConfig {
125| 1| host: “example.com”.to_string(),
126| 1| port: 3306,
127| 1| username: “user”.to_string(),
128| 1| password: “pass”.to_string(),
129| 1| database_name: “test_db”.to_string(),
130| 1| max_connections: 20,
131| 1| min_connections: 2,
132| 1| };
133| |
134| 1| let url = config.database_url();
135| 1| assert!(url.contains(“user:pass@example.com:3306/test_db”));
136| 1| }
137| |
138| | // Note: Integration tests would require a running database
139| | // These would be better placed in tests/ directory
140| |}

/home/sonar/libs/database/src/migrations.rs:
1| |use anyhow::Result;
2| |use sqlx::Row;
3| |
4| |/// Database migration utilities
5| |pub struct MigrationRunner {
6| | pool: sqlx::PgPool,
7| |}
8| |
9| |impl MigrationRunner {
10| | /// Create a new migration runner
11| 0| pub fn new(pool: sqlx::PgPool) → Self {
12| 0| Self { pool }
13| 0| }
14| |
15| | /// Run all pending migrations
16| 0| pub async fn run_migrations(&self) → Result<()> {
17| | // Create migrations table if it doesn’t exist
18| 0| self.create_migrations_table().await?;
19| |
20| | // Get list of applied migrations
21| 0| let applied = self.get_applied_migrations().await?;
22| |
23| | // Get list of available migrations
24| 0| let available = self.get_available_migrations();
25| |
26| | // Apply pending migrations
27| 0| for migration in available {
28| 0| if !applied.contains(&migration.id) {
29| 0| tracing::info!(“Applying migration: {}”, migration.id);
30| 0| self.apply_migration(&migration).await?;
31| 0| self.record_migration(&migration).await?;
32| 0| tracing::info!(“Migration {} applied successfully”, migration.id);
33| 0| }
34| | }
35| |
36| 0| Ok(())
37| 0| }
38| |
39| | /// Create the migrations tracking table
40| 0| async fn create_migrations_table(&self) → Result<()> {
41| 0| sqlx::query(
42| 0| r#"
43| 0| CREATE TABLE IF NOT EXISTS _migrations (
44| 0| id VARCHAR(255) PRIMARY KEY,
45| 0| applied_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
46| 0| )
47| 0| “#,
48| 0| )
49| 0| .execute(&self.pool)
50| 0| .await?;
51| |
52| 0| Ok(())
53| 0| }
54| |
55| | /// Get list of applied migrations
56| 0| async fn get_applied_migrations(&self) → Result<Vec> {
57| 0| let rows = sqlx::query(“SELECT id FROM _migrations ORDER BY applied_at”)
58| 0| .fetch_all(&self.pool)
59| 0| .await?;
60| |
61| 0| Ok(rows
62| 0| .into_iter()
63| 0| .map(|row| row.get::<String, _>(“id”))
64| 0| .collect())
65| 0| }
66| |
67| | /// Get list of available migrations
68| 0| fn get_available_migrations(&self) → Vec {
69| | // In a real implementation, this would scan the migrations directory
70| | // For now, return a basic set of migrations
71| 0| vec![Migration {
72| 0| id: “001_initial_schema”.to_string(),
73| 0| description: “Initial database schema”.to_string(),
74| 0| sql: include_str!(”../migrations/001_initial_schema.sql").to_string(),
75| 0| }]
76| 0| }
77| |
78| | /// Apply a migration
79| 0| async fn apply_migration(&self, migration: &Migration) → Result<()> {
80| 0| sqlx::query(&migration.sql).execute(&self.pool).await?;
81| 0| Ok(())
82| 0| }
83| |
84| | /// Record that a migration has been applied
85| 0| async fn record_migration(&self, migration: &Migration) → Result<()> {
86| 0| sqlx::query(“INSERT INTO _migrations (id) VALUES ($1)”)
87| 0| .bind(&migration.id)
88| 0| .execute(&self.pool)
89| 0| .await?;
90| 0| Ok(())
91| 0| }
92| |}
93| |
94| |/// Represents a database migration
95| |#[derive(Debug, Clone)]
96| |pub struct Migration {
97| | pub id: String,
98| | pub description: String,
99| | pub sql: String,
100| |}
101| |
102| |#[cfg(test)]
103| |mod tests {
104| |
105| | #[tokio::test]
106| 1| async fn test_migration_runner_creation() {
107| | // This would need a test database connection in a real test
108| | // For now, just test that the struct can be created
109| | // let pool = get_test_pool().await;
110| | // let runner = MigrationRunner::new(pool);
111| | // assert!(runner.get_available_migrations().len() > 0);
112| 1| }
113| |}

/home/sonar/libs/database/src/models.rs:
1| |use common::{UserId, UserRole};
2| |use serde::{Deserialize, Serialize};
3| |use sqlx::FromRow;
4| |use uuid::Uuid;
5| |
6| |/// User model for database operations
7| |#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
8| |pub struct User {
9| | pub id: Uuid,
10| | pub email: String,
11| | pub username: String,
12| | pub password_hash: String,
13| | pub role: String, // Stored as string in DB, converted to/from UserRole
14| | pub is_active: bool,
15| | pub created_at: chrono::DateTimechrono::Utc,
16| | pub updated_at: chrono::DateTimechrono::Utc,
17| |}
18| |
19| |impl User {
20| 1| pub fn new(email: String, username: String, password_hash: String, role: UserRole) → Self {
21| 1| let now = chrono::Utc::now();
22| |
23| 1| Self {
24| 1| id: Uuid::new_v4(),
25| 1| email,
26| 1| username,
27| 1| password_hash,
28| 1| role: role_to_string(&role),
29| 1| is_active: true,
30| 1| created_at: now,
31| 1| updated_at: now,
32| 1| }
33| 1| }
34| |
35| 0| pub fn user_id(&self) → UserId {
36| 0| UserId::from_uuid(self.id)
37| 0| }
38| |
39| 1| pub fn role(&self) → UserRole {
40| 1| string_to_role(&self.role)
41| 1| }
42| |
43| 0| pub fn set_role(&mut self, role: UserRole) {
44| 0| self.role = role_to_string(&role);
45| 0| self.updated_at = chrono::Utc::now();
46| 0| }
47| |
48| 0| pub fn deactivate(&mut self) {
49| 0| self.is_active = false;
50| 0| self.updated_at = chrono::Utc::now();
51| 0| }
52| |
53| 0| pub fn activate(&mut self) {
54| 0| self.is_active = true;
55| 0| self.updated_at = chrono::Utc::now();
56| 0| }
57| |}
58| |
59| |/// Convert UserRole to string for database storage
60| 2|fn role_to_string(role: &UserRole) → String {
61| 2| match role {
62| 1| UserRole::Admin => “admin”.to_string(),
63| 1| UserRole::User => “user”.to_string(),
64| 0| UserRole::Moderator => “moderator”.to_string(),
65| 0| UserRole::Guest => “guest”.to_string(),
66| | }
67| 2|}
68| |
69| |/// Convert string from database to UserRole
70| 3|fn string_to_role(role: &str) → UserRole {
71| 3| match role.to_lowercase().as_str() {
72| 3| “admin” => UserRole::Admin,
^1
73| 2| “user” => UserRole::User,
^1
74| 1| “moderator” => UserRole::Moderator,
^0
75| 1| “guest” => UserRole::Guest,
^0
76| 1| _ => UserRole::Guest, // Default fallback
77| | }
78| 3|}
79| |
80| |/// Session model for authentication
81| |#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
82| |pub struct Session {
83| | pub id: Uuid,
84| | pub user_id: Uuid,
85| | pub token: String,
86| | pub expires_at: chrono::DateTimechrono::Utc,
87| | pub created_at: chrono::DateTimechrono::Utc,
88| | pub last_accessed: chrono::DateTimechrono::Utc,
89| | pub is_active: bool,
90| |}
91| |
92| |impl Session {
93| 2| pub fn new(user_id: UserId, token: String, expires_at: chrono::DateTimechrono::Utc) → Self {
94| 2| let now = chrono::Utc::now();
95| |
96| 2| Self {
97| 2| id: Uuid::new_v4(),
98| 2| user_id: user_id.inner(),
99| 2| token,
100| 2| expires_at,
101| 2| created_at: now,
102| 2| last_accessed: now,
103| 2| is_active: true,
104| 2| }
105| 2| }
106| |
107| 0| pub fn user_id(&self) → UserId {
108| 0| UserId::from_uuid(self.user_id)
109| 0| }
110| |
111| 2| pub fn is_expired(&self) → bool {
112| 2| chrono::Utc::now() > self.expires_at
113| 2| }
114| |
115| 0| pub fn touch(&mut self) {
116| 0| self.last_accessed = chrono::Utc::now();
117| 0| }
118| |
119| 0| pub fn revoke(&mut self) {
120| 0| self.is_active = false;
121| 0| }
122| |}
123| |
124| |/// Notification model
125| |#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
126| |pub struct Notification {
127| | pub id: Uuid,
128| | pub user_id: Uuid,
129| | pub title: String,
130| | pub message: String,
131| | pub notification_type: String,
132| | pub is_read: bool,
133| | pub created_at: chrono::DateTimechrono::Utc,
134| | pub read_at: Option<chrono::DateTimechrono::Utc>,
135| |}
136| |
137| |impl Notification {
138| 1| pub fn new(user_id: UserId, title: String, message: String, notification_type: String) → Self {
139| 1| Self {
140| 1| id: Uuid::new_v4(),
141| 1| user_id: user_id.inner(),
142| 1| title,
143| 1| message,
144| 1| notification_type,
145| 1| is_read: false,
146| 1| created_at: chrono::Utc::now(),
147| 1| read_at: None,
148| 1| }
149| 1| }
150| |
151| 0| pub fn user_id(&self) → UserId {
152| 0| UserId::from_uuid(self.user_id)
153| 0| }
154| |
155| 1| pub fn mark_as_read(&mut self) {
156| 1| if !self.is_read {
157| 1| self.is_read = true;
158| 1| self.read_at = Some(chrono::Utc::now());
159| 1| }
^0
160| 1| }
161| |}
162| |
163| |#[cfg(test)]
164| |mod tests {
165| | use super::*;
166| |
167| | #[test]
168| 1| fn test_user_creation() {
169| 1| let user = User::new(
170| 1| “test@example.com”.to_string(),
171| 1| “testuser”.to_string(),
172| 1| “hashed_password”.to_string(),
173| 1| UserRole::User,
174| | );
175| |
176| 1| assert_eq!(user.email, “test@example.com”);
177| 1| assert_eq!(user.username, “testuser”);
178| 1| assert_eq!(user.role(), UserRole::User);
179| 1| assert!(user.is_active);
180| 1| }
181| |
182| | #[test]
183| 1| fn test_user_role_conversion() {
184| 1| assert_eq!(role_to_string(&UserRole::Admin), “admin”);
185| 1| assert_eq!(string_to_role(“admin”), UserRole::Admin);
186| 1| assert_eq!(string_to_role(“invalid”), UserRole::Guest);
187| 1| }
188| |
189| | #[test]
190| 1| fn test_session_expiry() {
191| 1| let user_id = UserId::new();
192| 1| let expired_session = Session::new(
193| 1| user_id,
194| 1| “token”.to_string(),
195| 1| chrono::Utc::now() - chrono::Duration::hours(1),
196| | );
197| |
198| 1| assert!(expired_session.is_expired());
199| |
200| 1| let valid_session = Session::new(
201| 1| user_id,
202| 1| “token”.to_string(),
203| 1| chrono::Utc::now() + chrono::Duration::hours(1),
204| | );
205| |
206| 1| assert!(!valid_session.is_expired());
207| 1| }
208| |
209| | #[test]
210| 1| fn test_notification_read_status() {
211| 1| let user_id = UserId::new();
212| 1| let mut notification = Notification::new(
213| 1| user_id,
214| 1| “Test”.to_string(),
215| 1| “Test message”.to_string(),
216| 1| “info”.to_string(),
217| | );
218| |
219| 1| assert!(!notification.is_read);
220| 1| assert!(notification.read_at.is_none());
221| |
222| 1| notification.mark_as_read();
223| |
224| 1| assert!(notification.is_read);
225| 1| assert!(notification.read_at.is_some());
226| 1| }
227| |}

/home/sonar/libs/database/src/repository.rs:
1| |use anyhow::Result;
2| |use async_trait::async_trait;
3| |use sqlx::PgPool;
4| |use std::fmt::Debug;
5| |
6| |/// Generic repository trait for database operations
7| |#[async_trait]
8| |pub trait Repository<T, ID>
9| |where
10| | T: Send + Sync + Debug,
11| | ID: Send + Sync + Debug,
12| |{
13| | /// Find entity by ID
14| | async fn find_by_id(&self, id: &ID) → Result<Option>;
15| |
16| | /// Find all entities
17| | async fn find_all(&self) → Result<Vec>;
18| |
19| | /// Save entity (insert or update)
20| | async fn save(&self, entity: &T) → Result;
21| |
22| | /// Delete entity by ID
23| | async fn delete_by_id(&self, id: &ID) → Result;
24| |
25| | /// Check if entity exists by ID
26| | async fn exists_by_id(&self, id: &ID) → Result;
27| |
28| | /// Count total entities
29| | async fn count(&self) → Result;
30| |}
31| |
32| |/// Base repository implementation with common database operations
33| |pub struct BaseRepository {
34| | pool: PgPool,
35| |}
36| |
37| |impl BaseRepository {
38| | /// Create a new base repository
39| 0| pub fn new(pool: PgPool) → Self {
40| 0| Self { pool }
41| 0| }
42| |
43| | /// Get the database pool
44| 0| pub fn pool(&self) → &PgPool {
45| 0| &self.pool
46| 0| }
47| |
48| | /// Execute a simple query without parameters
49| 0| pub async fn execute_simple(&self, query: &str) → Result {
50| 0| let result = sqlx::query(query).execute(&self.pool).await?;
51| 0| Ok(result.rows_affected())
52| 0| }
53| |
54| | /// Execute a query with a single parameter
55| 0| pub async fn execute_with_param(&self, query: &str, param: T) → Result
56| 0| where
57| 0| T: for<'q> sqlx::Encode<‘q, sqlx::Postgres> + sqlx::Typesqlx::Postgres + Send,
58| 0| {
59| 0| let result = sqlx::query(query).bind(param).execute(&self.pool).await?;
60| 0| Ok(result.rows_affected())
61| 0| }
62| |
63| | /// Execute a query and return a single row
64| 0| pub async fn query_one_simple(&self, query: &str) → Resultsqlx::postgres::PgRow {
65| 0| let row = sqlx::query(query).fetch_one(&self.pool).await?;
66| 0| Ok(row)
67| 0| }
68| |
69| | /// Execute a query and return multiple rows
70| 0| pub async fn query_all_simple(&self, query: &str) → Result<Vecsqlx::postgres::PgRow> {
71| 0| let rows = sqlx::query(query).fetch_all(&self.pool).await?;
72| 0| Ok(rows)
73| 0| }
74| |
75| | /// Execute a query and return an optional row
76| 0| pub async fn query_optional_simple(
77| 0| &self,
78| 0| query: &str,
79| 0| ) → Result<Optionsqlx::postgres::PgRow> {
80| 0| let row = sqlx::query(query).fetch_optional(&self.pool).await?;
81| 0| Ok(row)
82| 0| }
83| |
84| | /// Begin a database transaction
85| 0| pub async fn begin_transaction(&self) → Result<sqlx::Transaction<’_, sqlx::Postgres>> {
86| 0| let tx = self.pool.begin().await?;
87| 0| Ok(tx)
88| 0| }
89| |}
90| |
91| |/// Pagination parameters for repository queries
92| |#[derive(Debug, Clone)]
93| |pub struct Pagination {
94| | pub page: u32,
95| | pub page_size: u32,
96| |}
97| |
98| |impl Pagination {
99| | /// Create new pagination parameters
100| 7| pub fn new(page: u32, page_size: u32) → Self {
101| 7| let page_size = if page_size > 100 {
102| 1| 100
103| | } else {
104| 6| page_size.max(1)
105| | };
106| 7| let page = page.max(1);
107| 7| Self { page, page_size }
108| 7| }
109| |
110| | /// Get the SQL OFFSET value
111| 2| pub fn offset(&self) → u32 {
112| 2| (self.page - 1) * self.page_size
113| 2| }
114| |
115| | /// Get the SQL LIMIT value
116| 2| pub fn limit(&self) → u32 {
117| 2| self.page_size
118| 2| }
119| |}
120| |
121| |/// Paginated result wrapper
122| |#[derive(Debug, Clone)]
123| |pub struct PaginatedResult {
124| | pub data: Vec,
125| | pub page: u32,
126| | pub page_size: u32,
127| | pub total_pages: u32,
128| | pub total_items: i64,
129| |}
130| |
131| |impl PaginatedResult {
132| | /// Create a new paginated result
133| 2| pub fn new(data: Vec, pagination: &Pagination, total_items: i64) → Self {
134| 2| let total_pages = ((total_items as f64) / (pagination.page_size as f64)).ceil() as u32;
135| |
136| 2| Self {
137| 2| data,
138| 2| page: pagination.page,
139| 2| page_size: pagination.page_size,
140| 2| total_pages,
141| 2| total_items,
142| 2| }
143| 2| }
144| |
145| | /// Check if there are more pages
146| 2| pub fn has_next_page(&self) → bool {
147| 2| self.page < self.total_pages
148| 2| }
149| |
150| | /// Check if there are previous pages
151| 2| pub fn has_previous_page(&self) → bool {
152| 2| self.page > 1
153| 2| }
154| |}
155| |
156| |/// Repository error types
157| |#[derive(Debug, thiserror::Error)]
158| |pub enum RepositoryError {
159| | #[error(“Entity not found”)]
160| | NotFound,
161| |
162| | #[error(“Database error: {0}”)]
163| | Database(#[from] sqlx::Error),
164| |
165| | #[error(“Validation error: {message}”)]
166| | Validation { message: String },
167| |
168| | #[error(“Conflict error: {message}”)]
169| | Conflict { message: String },
170| |}
171| |
172| |#[cfg(test)]
173| |mod tests {
174| | use super::*;
175| |
176| | #[test]
177| 1| fn test_pagination() {
178| 1| let p = Pagination::new(1, 20);
179| 1| assert_eq!(p.page, 1);
180| 1| assert_eq!(p.page_size, 20);
181| 1| assert_eq!(p.offset(), 0);
182| 1| assert_eq!(p.limit(), 20);
183| |
184| 1| let p2 = Pagination::new(3, 10);
185| 1| assert_eq!(p2.offset(), 20);
186| 1| assert_eq!(p2.limit(), 10);
187| 1| }
188| |
189| | #[test]
190| 1| fn test_pagination_limits() {
191| | // Test maximum page size limit
192| 1| let p = Pagination::new(1, 200);
193| 1| assert_eq!(p.page_size, 100);
194| |
195| | // Test minimum page size
196| 1| let p2 = Pagination::new(1, 0);
197| 1| assert_eq!(p2.page_size, 1);
198| |
199| | // Test minimum page
200| 1| let p3 = Pagination::new(0, 20);
201| 1| assert_eq!(p3.page, 1);
202| 1| }
203| |
204| | #[test]
205| 1| fn test_paginated_result() {
206| 1| let data = vec![1, 2, 3, 4, 5];
207| 1| let pagination = Pagination::new(1, 5);
208| 1| let result = PaginatedResult::new(data, &pagination, 23);
209| |
210| 1| assert_eq!(result.total_pages, 5);
211| 1| assert_eq!(result.total_items, 23);
212| 1| assert!(result.has_next_page());
213| 1| assert!(!result.has_previous_page());
214| |
215| 1| let pagination2 = Pagination::new(3, 5);
216| 1| let result2 = PaginatedResult::new(vec![1, 2], &pagination2, 23);
217| 1| assert!(result2.has_next_page());
218| 1| assert!(result2.has_previous_page());
219| 1| }
220| |}

/home/sonar/services/auth-service/src/main.rs:
1| |use anyhow::Result;
2| |use axum::{response::Json, routing::get, Router};
3| |use clap::Parser;
4| |use serde_json::{json, Value};
5| |use tracing::info;
6| |use tracing_subscriber::{self, EnvFilter};
7| |
8| |/// Auth Service - Handles authentication and authorization
9| |#[derive(Parser, Debug)]
10| |#[command(name = “auth-service”)]
11| |#[command(about = “An authentication microservice”)]
12| |struct Args {
13| | /// Port to listen on
14| | #[arg(short, long, default_value = “8001”)]
15| | port: u16,
16| |
17| | /// Log level
18| | #[arg(short, long, default_value = “info”)]
19| | log_level: String,
20| |}
21| |
22| |/// Health check endpoint
23| 0|async fn health_check() → Json {
24| 0| Json(json!({
25| 0| “status”: “healthy”,
26| 0| “service”: “auth-service”
27| 0| }))
28| 0|}
29| |
30| |#[tokio::main]
31| 0|async fn main() → Result<()> {
32| 0| let args = Args::parse();
33| |
34| | // Initialize tracing
35| 0| tracing_subscriber::fmt()
36| 0| .with_env_filter(EnvFilter::new(&args.log_level))
37| 0| .init();
38| |
39| 0| info!(“Starting auth-service on port {}”, args.port);
40| |
41| | // Build our application with a route
42| 0| let app = Router::new().route(“/health”, get(health_check));
43| |
44| | // Run the server
45| 0| let listener = tokio::net::TcpListener::bind(format!(“0.0.0.0:{}”, args.port)).await?;
46| 0| info!(“Auth service listening on port {}”, args.port);
47| |
48| 0| axum::serve(listener, app).await?;
49| |
50| 0| Ok(())
51| 0|}

/home/sonar/services/notification-service/src/main.rs:
1| |//! Notification Service
2| |//!
3| |//! A microservice responsible for handling notifications, emails,
4| |//! and messaging in the Rust monolithic architecture.
5| |
6| |use anyhow::Result;
7| |use axum::{response::Json, routing::get, Router};
8| |use clap::Parser;
9| |use serde_json::{json, Value};
10| |use tracing::info;
11| |use tracing_subscriber::{self, EnvFilter};
12| |
13| |/// Notification Service - Handles notifications and messaging
14| |#[derive(Parser, Debug)]
15| |#[command(name = “notification-service”)]
16| |#[command(about = “A notification microservice”)]
17| |struct Args {
18| | /// Port to listen on
19| | #[arg(short, long, default_value = “8003”)]
20| | port: u16,
21| |
22| | /// Log level
23| | #[arg(short, long, default_value = “info”)]
24| | log_level: String,
25| |}
26| |
27| |/// Health check endpoint
28| 0|async fn health_check() → Json {
29| 0| Json(json!({
30| 0| “status”: “healthy”,
31| 0| “service”: “notification-service”
32| 0| }))
33| 0|}
34| |
35| |#[tokio::main]
36| 0|async fn main() → Result<()> {
37| 0| let args = Args::parse();
38| |
39| | // Initialize tracing
40| 0| tracing_subscriber::fmt()
41| 0| .with_env_filter(EnvFilter::new(&args.log_level))
42| 0| .init();
43| |
44| 0| info!(“Starting notification-service on port {}”, args.port);
45| |
46| | // Build our application with a route
47| 0| let app = Router::new().route(“/health”, get(health_check));
48| |
49| | // Run the server
50| 0| let listener = tokio::net::TcpListener::bind(format!(“0.0.0.0:{}”, args.port)).await?;
51| 0| info!(“Notification service listening on port {}”, args.port);
52| |
53| 0| axum::serve(listener, app).await?;
54| |
55| 0| Ok(())
56| 0|}

/home/sonar/services/user-service/src/main.rs:
1| |use anyhow::Result;
2| |use clap::Parser;
3| |use tracing::{info, warn};
4| |use tracing_subscriber::{self, EnvFilter};
5| |
6| |/// User Service - Handles user management operations
7| |#[derive(Parser, Debug)]
8| |#[command(name = “user-service”)]
9| |#[command(about = “A user management microservice”)]
10| |struct Args {
11| | /// Port to listen on
12| | #[arg(short, long, default_value = “8002”)]
13| | port: u16,
14| |
15| | /// Log level
16| | #[arg(short, long, default_value = “info”)]
17| | log_level: String,
18| |}
19| |
20| |#[tokio::main]
21| 0|async fn main() → Result<()> {
22| 0| let args = Args::parse();
23| |
24| | // Initialize tracing
25| 0| tracing_subscriber::fmt()
26| 0| .with_env_filter(EnvFilter::new(&args.log_level))
27| 0| .init();
28| |
29| 0| info!(“Starting user-service on port {}”, args.port);
30| |
31| | // TODO: Implement user service functionality
32| | // - User registration
33| | // - User profile management
34| | // - User authentication integration
35| | // - User data validation
36| |
37| 0| warn!(“User service is not yet implemented - placeholder service”);
38| |
39| | // Keep the service running
40| 0| tokio::select! {
41| 0| _ = tokio::signal::ctrl_c() => {
42| 0| info!(“Received shutdown signal”);
43| | }
44| | }
45| |
46| 0| info!(“User service shutting down”);
47| 0| Ok(())
48| 0|}


  1. a-zA-Z0-9._%± ↩︎

Hey there.

Have you passed your covearge report to either sonar.rust.lcov.reportPaths or sonar.rust.cobertura.reportPaths?

I am assuming your coverage reports are already in a supported format. If not, they need to be in either LCOV or Cobertura format.

1 Like