Sync your own workout data from your "Strong" app
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

bumped version && cargo fmt

tolik518 4020b8e7 e1976acb

+281 -78
+2 -2
Cargo.lock
··· 1270 1270 1271 1271 [[package]] 1272 1272 name = "strong-api-fetch" 1273 - version = "0.2.0" 1273 + version = "0.2.1" 1274 1274 dependencies = [ 1275 1275 "clickhouse", 1276 1276 "dotenvy", ··· 1285 1285 1286 1286 [[package]] 1287 1287 name = "strong-api-lib" 1288 - version = "0.3.0" 1288 + version = "0.3.1" 1289 1289 dependencies = [ 1290 1290 "reqwest", 1291 1291 "serde",
+136
fetch_test_fixtures.sh
··· 1 + #!/usr/bin/env bash 2 + # --------------------------------------------------------------------------- 3 + # fetch_test_fixtures.sh 4 + # 5 + # Fetches real API responses from the Strong backend and saves them as 6 + # fixture files in strong-api-lib/tests/fixtures/. 7 + # 8 + # Usage: 9 + # STRONG_USER=you@example.com STRONG_PASS=yourpassword ./fetch_test_fixtures.sh 10 + # 11 + # Optionally set BASE_URL if you are pointing at a different backend: 12 + # BASE_URL=https://api.strong.app ./fetch_test_fixtures.sh 13 + # --------------------------------------------------------------------------- 14 + 15 + set -uo pipefail 16 + 17 + # Load .env from the project root if it exists (so STRONG_USER, STRONG_PASS, 18 + # and STRONG_BACKEND don't have to be passed manually every time). 19 + SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" 20 + if [[ -f "${SCRIPT_DIR}/.env" ]]; then 21 + echo "==> Loading ${SCRIPT_DIR}/.env" 22 + set -a 23 + # shellcheck source=/dev/null 24 + source "${SCRIPT_DIR}/.env" 25 + set +a 26 + fi 27 + 28 + BASE_URL="${BASE_URL:-${STRONG_BACKEND:-https://back.strong.app}}" 29 + FIXTURES_DIR="$(dirname "$0")/strong-api-lib/tests/fixtures" 30 + 31 + : "${STRONG_USER:?Set STRONG_USER to your Strong username or e-mail}" 32 + : "${STRONG_PASS:?Set STRONG_PASS to your Strong password}" 33 + 34 + echo "==> Logging in..." 35 + LOGIN_JSON=$(curl -s --max-time 15 --connect-timeout 10 -X POST "${BASE_URL}/auth/login" \ 36 + -H "User-Agent: Strong Android" \ 37 + -H "Content-Type: application/json" \ 38 + -H "Accept: application/json" \ 39 + -H "x-client-build: 600013" \ 40 + -H "x-client-platform: android" \ 41 + -d "{\"usernameOrEmail\": \"${STRONG_USER}\", \"password\": \"${STRONG_PASS}\"}" \ 42 + || { echo "ERROR: curl request failed (timeout or network issue? Check BASE_URL=${BASE_URL})"; exit 1; }) 43 + 44 + echo " Raw login response: $LOGIN_JSON" 45 + 46 + ACCESS_TOKEN=$(echo "$LOGIN_JSON" | python3 -c " 47 + import sys, json 48 + try: 49 + data = json.load(sys.stdin) 50 + except json.JSONDecodeError as e: 51 + print(f'ERROR: response is not valid JSON: {e}', file=sys.stderr) 52 + sys.exit(1) 53 + token = data.get('accessToken') 54 + if not token: 55 + print(f'ERROR: no accessToken in response. Keys present: {list(data.keys())}', file=sys.stderr) 56 + sys.exit(1) 57 + print(token) 58 + ") || exit 1 59 + 60 + USER_ID=$(echo "$LOGIN_JSON" | python3 -c " 61 + import sys, json 62 + data = json.load(sys.stdin) 63 + uid = data.get('userId') 64 + if not uid: 65 + print(f'ERROR: no userId in response. Keys present: {list(data.keys())}', file=sys.stderr) 66 + sys.exit(1) 67 + print(uid) 68 + ") || exit 1 69 + 70 + echo "$LOGIN_JSON" | python3 -m json.tool > "${FIXTURES_DIR}/login_response.json" 71 + echo " Saved login_response.json" 72 + 73 + echo " Logged in as user ${USER_ID}" 74 + 75 + # --------------------------------------------------------------------------- 76 + # GET /api/measurements (page 1) 77 + # --------------------------------------------------------------------------- 78 + echo "==> Fetching measurements (page 1)..." 79 + curl -s "${BASE_URL}/api/measurements?page=1" \ 80 + -H "User-Agent: Strong Android" \ 81 + -H "Accept: application/json" \ 82 + -H "x-client-build: 600013" \ 83 + -H "x-client-platform: android" \ 84 + | python3 -m json.tool > "${FIXTURES_DIR}/measurements_response.json" 85 + echo " Saved measurements_response.json" 86 + 87 + # --------------------------------------------------------------------------- 88 + # GET /api/users/{userId} with logs + measurements embedded 89 + # Uses a small limit so the fixture stays manageable. 90 + # --------------------------------------------------------------------------- 91 + echo "==> Fetching user (with logs embedded)..." 92 + curl -s "${BASE_URL}/api/users/${USER_ID}?limit=5&continuation=&include=log&include=measurement" \ 93 + -H "User-Agent: Strong Android" \ 94 + -H "Accept: application/json" \ 95 + -H "Authorization: Bearer ${ACCESS_TOKEN}" \ 96 + -H "x-client-build: 600013" \ 97 + -H "x-client-platform: android" \ 98 + | python3 -m json.tool > "${FIXTURES_DIR}/user_response.json" 99 + echo " Saved user_response.json" 100 + 101 + # --------------------------------------------------------------------------- 102 + # GET /api/users/{userId} (all include types, for a wider integration test) 103 + # --------------------------------------------------------------------------- 104 + echo "==> Fetching user (all includes)..." 105 + curl -s "${BASE_URL}/api/users/${USER_ID}?limit=5&continuation=&include=log&include=measurement&include=tag&include=template&include=folder&include=widget&include=measuredValue" \ 106 + -H "User-Agent: Strong Android" \ 107 + -H "Accept: application/json" \ 108 + -H "Authorization: Bearer ${ACCESS_TOKEN}" \ 109 + -H "x-client-build: 600013" \ 110 + -H "x-client-platform: android" \ 111 + | python3 -m json.tool > "${FIXTURES_DIR}/user_response_all_includes.json" 112 + echo " Saved user_response_all_includes.json" 113 + 114 + # --------------------------------------------------------------------------- 115 + # Simulate a failed login to capture ApiErrorResponse shape 116 + # --------------------------------------------------------------------------- 117 + echo "==> Fetching error response (bad credentials)..." 118 + curl -s -X POST "${BASE_URL}/auth/login" \ 119 + -H "User-Agent: Strong Android" \ 120 + -H "Content-Type: application/json" \ 121 + -H "Accept: application/json" \ 122 + -H "x-client-build: 600013" \ 123 + -H "x-client-platform: android" \ 124 + -d '{"usernameOrEmail": "invalid@example.com", "password": "wrongpassword"}' \ 125 + | python3 -m json.tool > "${FIXTURES_DIR}/error_response.json" 126 + echo " Saved error_response.json" 127 + 128 + echo "" 129 + echo "All fixtures saved to ${FIXTURES_DIR}" 130 + echo "Replace the synthetic fixture files with these real responses to make" 131 + echo "the integration tests run against actual API data." 132 + 133 + 134 + 135 + 136 +
+1 -1
strong-api-fetch/Cargo.toml
··· 1 1 [package] 2 2 name = "strong-api-fetch" 3 - version = "0.2.0" 3 + version = "0.2.1" 4 4 edition = "2024" 5 5 6 6 [dependencies]
+8 -6
strong-api-fetch/src/clickhouse_saver.rs
··· 65 65 let mut insert = self.client.insert(&self.table_name)?; 66 66 67 67 for exercise in &workout.exercises { 68 - let exercise_nr = workout.exercises.iter().position(|x| x.id == exercise.id).unwrap() as u32; 68 + let exercise_nr = workout 69 + .exercises 70 + .iter() 71 + .position(|x| x.id == exercise.id) 72 + .unwrap() as u32; 69 73 for set in &exercise.sets { 70 74 let start_dt = OffsetDateTime::parse( 71 75 &workout.start_date.clone().unwrap_or_default(), 72 76 &Rfc3339, 73 77 )?; 74 78 75 - let end_dt = OffsetDateTime::parse( 76 - &workout.end_date.clone().unwrap_or_default(), 77 - &Rfc3339, 78 - )?; 79 + let end_dt = 80 + OffsetDateTime::parse(&workout.end_date.clone().unwrap_or_default(), &Rfc3339)?; 79 81 80 82 let set_nr = exercise.sets.iter().position(|x| x.id == set.id).unwrap() as u32; 81 83 ··· 109 111 println!("Workout {} imported successfully", workout.id); 110 112 Ok(()) 111 113 } 112 - } 114 + }
+11 -17
strong-api-fetch/src/main.rs
··· 21 21 22 22 // Load configuration from environment variables. 23 23 let config = load_config()?; 24 - let url = Url::parse(&config.strong_backend) 25 - .expect("STRONG_BACKEND is not a valid URL"); 24 + let url = Url::parse(&config.strong_backend).expect("STRONG_BACKEND is not a valid URL"); 26 25 27 26 // Initialize the API and ClickHouse saver. 28 27 let mut strong_api = StrongApi::new(url); 29 28 let clickhouse_saver = create_clickhouse_saver(&config); 30 29 31 30 // Log in to the API. 32 - strong_api.login(config.username.as_str(), config.password.as_str()).await?; 31 + strong_api 32 + .login(config.username.as_str(), config.password.as_str()) 33 + .await?; 33 34 34 35 // Get the measurements (either from file or API). 35 36 let measurements_response = get_measurements_response(&mut strong_api).await?; ··· 72 73 /// Load configuration values from environment variables. 73 74 fn load_config() -> Result<Config, Box<dyn std::error::Error>> { 74 75 Ok(Config { 75 - username: env::var("STRONG_USER") 76 - .expect("STRONG_USER must be set"), 77 - password: env::var("STRONG_PASS") 78 - .expect("STRONG_PASS must be set"), 79 - strong_backend: env::var("STRONG_BACKEND") 80 - .expect("STRONG_BACKEND must be set"), 81 - clickhouse_url: env::var("CLICKHOUSE_URL") 82 - .expect("CLICKHOUSE_URL must be set"), 83 - clickhouse_user: env::var("CLICKHOUSE_USER") 84 - .expect("CLICKHOUSE_USER must be set"), 85 - clickhouse_pass: env::var("CLICKHOUSE_PASS") 86 - .expect("CLICKHOUSE_PASS must be set"), 76 + username: env::var("STRONG_USER").expect("STRONG_USER must be set"), 77 + password: env::var("STRONG_PASS").expect("STRONG_PASS must be set"), 78 + strong_backend: env::var("STRONG_BACKEND").expect("STRONG_BACKEND must be set"), 79 + clickhouse_url: env::var("CLICKHOUSE_URL").expect("CLICKHOUSE_URL must be set"), 80 + clickhouse_user: env::var("CLICKHOUSE_USER").expect("CLICKHOUSE_USER must be set"), 81 + clickhouse_pass: env::var("CLICKHOUSE_PASS").expect("CLICKHOUSE_PASS must be set"), 87 82 clickhouse_database: env::var("CLICKHOUSE_DATABASE") 88 83 .expect("CLICKHOUSE_DATABASE must be set"), 89 - clickhouse_table: env::var("CLICKHOUSE_TABLE") 90 - .expect("CLICKHOUSE_TABLE must be set"), 84 + clickhouse_table: env::var("CLICKHOUSE_TABLE").expect("CLICKHOUSE_TABLE must be set"), 91 85 }) 92 86 } 93 87
+1 -1
strong-api-lib/Cargo.toml
··· 1 1 [package] 2 2 name = "strong-api-lib" 3 - version = "0.3.0" 3 + version = "0.3.1" 4 4 edition = "2024" 5 5 6 6 [dependencies]
+6 -9
strong-api-lib/src/data_transformer.rs
··· 169 169 .and_then(|cell| cell.value.as_ref()) 170 170 .and_then(|s| s.parse::<f32>().ok()); 171 171 172 - Some( 173 - Set { 174 - id: cell_set.id.clone(), 175 - weight, 176 - reps, 177 - rpe, 178 - } 179 - ) 172 + Some(Set { 173 + id: cell_set.id.clone(), 174 + weight, 175 + reps, 176 + rpe, 177 + }) 180 178 } 181 179 182 180 fn get_measurement_id_from_link(links: &CellSetGroupLinks) -> String { ··· 189 187 parts[parts.len() - 1].to_string() 190 188 } 191 189 } 192 -
+30 -7
strong-api-lib/src/strong_api.rs
··· 90 90 username: &str, 91 91 password: &str, 92 92 ) -> Result<(), Box<dyn std::error::Error>> { 93 - let url = self.url.join("auth/login").expect("joining auth/login has failed, check the base URL"); 93 + let url = self 94 + .url 95 + .join("auth/login") 96 + .expect("joining auth/login has failed, check the base URL"); 94 97 let body = json!({ 95 98 "usernameOrEmail": username, 96 99 "password": password ··· 116 119 117 120 /// Refreshes the access token using tokens obtained during login. 118 121 pub async fn refresh(&mut self) -> Result<(), Box<dyn std::error::Error>> { 119 - let url = self.url.join("auth/login/refresh").expect("joining auth/login/refresh has failed, check the base URL"); 122 + let url = self 123 + .url 124 + .join("auth/login/refresh") 125 + .expect("joining auth/login/refresh has failed, check the base URL"); 120 126 let body = json!({ 121 127 "accessToken": self.access_token, 122 128 "refreshToken": self.refresh_token ··· 149 155 access_token: String, 150 156 refresh_token: String, 151 157 ) -> Result<(), Box<dyn std::error::Error>> { 152 - let url = self.url.join("auth/login/refresh").expect("joining auth/login/refresh has failed, check the base URL"); 158 + let url = self 159 + .url 160 + .join("auth/login/refresh") 161 + .expect("joining auth/login/refresh has failed, check the base URL"); 153 162 let body = json!({ 154 163 "accessToken": access_token.clone(), 155 164 "refreshToken": refresh_token, ··· 186 195 .user_id 187 196 .as_ref() 188 197 .ok_or("Missing user id. Use `login` before calling `get_user`")?; 189 - let mut url = self.url.join(&format!("api/users/{user_id}")).expect("joining api/users/{user_id} has failed, check the base URL"); 198 + let mut url = self 199 + .url 200 + .join(&format!("api/users/{user_id}")) 201 + .expect("joining api/users/{user_id} has failed, check the base URL"); 190 202 191 203 { 192 204 // Use query_pairs_mut to build the query string. ··· 227 239 &self, 228 240 page: i8, 229 241 ) -> Result<MeasurementsResponse, Box<dyn std::error::Error>> { 230 - let mut url = self.url.join("api/measurements").expect("joining api/measurements has failed, check the base URL"); 242 + let mut url = self 243 + .url 244 + .join("api/measurements") 245 + .expect("joining api/measurements has failed, check the base URL"); 231 246 232 247 { 233 248 let mut query_pairs = url.query_pairs_mut(); 234 249 query_pairs.append_pair("page", &page.to_string()); 235 250 } 236 251 237 - let response = self.client.get(url).headers(Self::default_headers()).send().await?; 252 + let response = self 253 + .client 254 + .get(url) 255 + .headers(Self::default_headers()) 256 + .send() 257 + .await?; 238 258 let response_text = response.text().await.expect("failed to read response body"); 239 259 240 260 let response: MeasurementsResponse = serde_json::from_str(&response_text)?; ··· 244 264 245 265 pub async fn get_logs_raw(&self) -> Result<String, Box<dyn std::error::Error>> { 246 266 let user_id = self.user_id.as_ref().ok_or("Missing user id")?; 247 - let url = self.url.join(&format!("api/logs/{user_id}")).expect("joining api/logs/{user_id} has failed, check the base URL"); 267 + let url = self 268 + .url 269 + .join(&format!("api/logs/{user_id}")) 270 + .expect("joining api/logs/{user_id} has failed, check the base URL"); 248 271 let response = self 249 272 .client 250 273 .get(url)
+46 -25
strong-api-lib/tests/data_transformer_tests.rs
··· 45 45 46 46 #[test] 47 47 fn test_transform_with_measurements_resolves_names() { 48 - let transformer = DataTransformer::new() 49 - .with_measurements_response(measurements_from_fixture()); 48 + let transformer = 49 + DataTransformer::new().with_measurements_response(measurements_from_fixture()); 50 50 let logs = logs_from_fixture(); 51 51 let workouts = transformer.get_measurements_from_logs(&logs).unwrap(); 52 52 ··· 55 55 56 56 // Every exercise whose measurement was found in the lookup has a non-empty name. 57 57 let named = exercises.iter().filter(|e| !e.name.is_empty()).count(); 58 - assert!(named > 0, "at least one exercise should have a resolved name"); 58 + assert!( 59 + named > 0, 60 + "at least one exercise should have a resolved name" 61 + ); 59 62 } 60 63 61 64 // --------------------------------------------------------------------------- ··· 64 67 65 68 #[test] 66 69 fn test_only_groups_with_real_sets_become_exercises() { 67 - let transformer = DataTransformer::new() 68 - .with_measurements_response(measurements_from_fixture()); 70 + let transformer = 71 + DataTransformer::new().with_measurements_response(measurements_from_fixture()); 69 72 let logs = logs_from_fixture(); 70 73 let workouts = transformer.get_measurements_from_logs(&logs).unwrap(); 71 74 72 75 // Count CSGs in the raw fixture log 73 76 let json = load_fixture("user_response.json"); 74 77 let user: UserResponse = serde_json::from_str(&json).unwrap(); 75 - let raw_csg_count = user.embedded.log.unwrap()[0] 76 - .embedded 77 - .cell_set_group 78 - .len(); 78 + let raw_csg_count = user.embedded.log.unwrap()[0].embedded.cell_set_group.len(); 79 79 80 80 // Transformed exercise count can be at most the number of raw CSGs 81 81 assert!(workouts[0].exercises.len() <= raw_csg_count); ··· 87 87 88 88 #[test] 89 89 fn test_sets_have_weight_and_reps() { 90 - let transformer = DataTransformer::new() 91 - .with_measurements_response(measurements_from_fixture()); 90 + let transformer = 91 + DataTransformer::new().with_measurements_response(measurements_from_fixture()); 92 92 let logs = logs_from_fixture(); 93 93 let workouts = transformer.get_measurements_from_logs(&logs).unwrap(); 94 94 95 95 let exercises = &workouts[0].exercises; 96 96 for exercise in exercises { 97 - assert!(!exercise.sets.is_empty(), "exercise {} should have sets", exercise.id); 97 + assert!( 98 + !exercise.sets.is_empty(), 99 + "exercise {} should have sets", 100 + exercise.id 101 + ); 98 102 for set in &exercise.sets { 99 - let _ = set.reps; // always present 103 + let _ = set.reps; // always present 100 104 let _ = set.weight; // optional (None for bodyweight) 101 105 } 102 106 } ··· 125 129 // the name lookup succeeds; if the link is absent the name is empty. 126 130 // --------------------------------------------------------------------------- 127 131 128 - fn make_log_with_measurement_link(measurement_href: Option<&str>) -> Vec<strong_api_lib::models::workout::Log> { 132 + fn make_log_with_measurement_link( 133 + measurement_href: Option<&str>, 134 + ) -> Vec<strong_api_lib::models::workout::Log> { 129 135 use serde_json::json; 130 136 use strong_api_lib::models::common::Link; 131 137 use strong_api_lib::models::workout::{ ··· 138 144 cell_set_group: vec![CellSetGroup { 139 145 id: "csg-link-test".to_string(), 140 146 links: CellSetGroupLinks { 141 - measurement: measurement_href.map(|href| Link { href: href.to_string() }), 147 + measurement: measurement_href.map(|href| Link { 148 + href: href.to_string(), 149 + }), 142 150 }, 143 151 embedded: CellSetGroupEmbedded {}, 144 152 cell_sets: vec![CellSet { 145 153 id: "cs-link-test".to_string(), 146 154 is_completed: Some(true), 147 155 cells: vec![ 148 - Cell { id: "c1".to_string(), cell_type: "BARBELL_WEIGHT".to_string(), value: Some("80".to_string()) }, 149 - Cell { id: "c2".to_string(), cell_type: "REPS".to_string(), value: Some("5".to_string()) }, 156 + Cell { 157 + id: "c1".to_string(), 158 + cell_type: "BARBELL_WEIGHT".to_string(), 159 + value: Some("80".to_string()), 160 + }, 161 + Cell { 162 + id: "c2".to_string(), 163 + cell_type: "REPS".to_string(), 164 + value: Some("5".to_string()), 165 + }, 150 166 ], 151 167 }], 152 168 }], ··· 184 200 fn test_missing_measurement_link_gives_empty_name() { 185 201 let logs = make_log_with_measurement_link(None); 186 202 187 - let transformer = DataTransformer::new().with_measurements_response(measurements_from_fixture()); 203 + let transformer = 204 + DataTransformer::new().with_measurements_response(measurements_from_fixture()); 188 205 let workouts = transformer.get_measurements_from_logs(&Some(logs)).unwrap(); 189 206 190 207 assert_eq!(workouts[0].exercises[0].name, ""); ··· 194 211 // Cell type coverage — NOTE filter, all weight variants, RPE, missing REPS 195 212 // --------------------------------------------------------------------------- 196 213 197 - fn make_log_with_cells(cells: Vec<(String, Option<String>)>) -> Vec<strong_api_lib::models::workout::Log> { 214 + fn make_log_with_cells( 215 + cells: Vec<(String, Option<String>)>, 216 + ) -> Vec<strong_api_lib::models::workout::Log> { 198 217 use serde_json::json; 199 218 use strong_api_lib::models::workout::{ 200 219 Cell, CellSet, CellSetGroup, CellSetGroupEmbedded, CellSetGroupLinks, Log, LogEmbedded, ··· 244 263 ]); 245 264 let transformer = DataTransformer::new(); 246 265 let workouts = transformer.get_measurements_from_logs(&Some(logs)).unwrap(); 247 - assert!(workouts[0].exercises.is_empty(), "NOTE cell should cause the group to be filtered out"); 266 + assert!( 267 + workouts[0].exercises.is_empty(), 268 + "NOTE cell should cause the group to be filtered out" 269 + ); 248 270 } 249 271 250 272 #[test] ··· 283 305 #[test] 284 306 fn test_no_weight_cell_gives_none() { 285 307 // Only REPS, no weight cell at all 286 - let logs = make_log_with_cells(vec![ 287 - ("REPS".to_string(), Some("10".to_string())), 288 - ]); 308 + let logs = make_log_with_cells(vec![("REPS".to_string(), Some("10".to_string()))]); 289 309 let transformer = DataTransformer::new(); 290 310 let workouts = transformer.get_measurements_from_logs(&Some(logs)).unwrap(); 291 311 assert_eq!(workouts[0].exercises[0].sets[0].weight, None); ··· 317 337 #[test] 318 338 fn test_empty_logs_vec_returns_empty_workouts() { 319 339 let transformer = DataTransformer::new(); 320 - let workouts = transformer.get_measurements_from_logs(&Some(vec![])).unwrap(); 340 + let workouts = transformer 341 + .get_measurements_from_logs(&Some(vec![])) 342 + .unwrap(); 321 343 assert!(workouts.is_empty()); 322 344 } 323 345 ··· 335 357 let workouts = by_default.get_measurements_from_logs(&None).unwrap(); 336 358 assert!(workouts.is_empty()); 337 359 } 338 -
-1
strong-api-lib/tests/error_tests.rs
··· 27 27 assert_eq!(err.code, "NOT_FOUND"); 28 28 assert_eq!(err.description, "Resource not found"); 29 29 } 30 -
+24 -6
strong-api-lib/tests/measurement_tests.rs
··· 22 22 23 23 #[test] 24 24 fn test_name_display_en() { 25 - let name = Name { en: Some("Bench Press".to_string()), custom: None }; 25 + let name = Name { 26 + en: Some("Bench Press".to_string()), 27 + custom: None, 28 + }; 26 29 assert_eq!(name.to_string(), "Bench Press"); 27 30 } 28 31 29 32 #[test] 30 33 fn test_name_display_custom_fallback() { 31 - let name = Name { en: None, custom: Some("My Exercise".to_string()) }; 34 + let name = Name { 35 + en: None, 36 + custom: Some("My Exercise".to_string()), 37 + }; 32 38 assert_eq!(name.to_string(), "My Exercise"); 33 39 } 34 40 35 41 #[test] 36 42 fn test_name_display_unknown_fallback() { 37 - let name = Name { en: None, custom: None }; 43 + let name = Name { 44 + en: None, 45 + custom: None, 46 + }; 38 47 assert_eq!(name.to_string(), "Unknown"); 39 48 } 40 49 41 50 #[test] 42 51 fn test_name_from_name_en() { 43 - let name = Name { en: Some("Squat".to_string()), custom: None }; 52 + let name = Name { 53 + en: Some("Squat".to_string()), 54 + custom: None, 55 + }; 44 56 assert_eq!(String::from(name), "Squat"); 45 57 } 46 58 47 59 #[test] 48 60 fn test_name_from_name_custom_fallback() { 49 - let name = Name { en: None, custom: Some("Custom".to_string()) }; 61 + let name = Name { 62 + en: None, 63 + custom: Some("Custom".to_string()), 64 + }; 50 65 assert_eq!(String::from(name), "Custom"); 51 66 } 52 67 53 68 #[test] 54 69 fn test_name_from_name_unknown_fallback() { 55 - let name = Name { en: None, custom: None }; 70 + let name = Name { 71 + en: None, 72 + custom: None, 73 + }; 56 74 assert_eq!(String::from(name), "Unknown"); 57 75 } 58 76
+16 -3
strong-api-lib/tests/strong_api_tests.rs
··· 97 97 api.login("user", "pass").await.unwrap(); 98 98 assert_eq!(api.access_token.as_deref(), Some("test-access-token")); 99 99 assert_eq!(api.refresh_token.as_deref(), Some("test-refresh-token")); 100 - assert_eq!(api.user_id.as_deref(), Some("00000000-0000-0000-0000-000000000001")); 100 + assert_eq!( 101 + api.user_id.as_deref(), 102 + Some("00000000-0000-0000-0000-000000000001") 103 + ); 101 104 } 102 105 #[tokio::test] 103 106 async fn test_login_invalid_json_returns_error() { ··· 142 145 let mut api = refused_api(); 143 146 let result = api.refresh().await; 144 147 assert!(result.is_err()); 145 - assert!(result.unwrap_err().to_string().contains("Missing access token")); 148 + assert!( 149 + result 150 + .unwrap_err() 151 + .to_string() 152 + .contains("Missing access token") 153 + ); 146 154 } 147 155 #[tokio::test] 148 156 async fn test_refresh_invalid_json_returns_error() { ··· 310 318 api.user_id = Some("00000000-0000-0000-0000-000000000001".to_string()); 311 319 let result = api.get_logs_raw().await; 312 320 assert!(result.is_err()); 313 - assert!(result.unwrap_err().to_string().contains("Missing access token")); 321 + assert!( 322 + result 323 + .unwrap_err() 324 + .to_string() 325 + .contains("Missing access token") 326 + ); 314 327 } 315 328 #[tokio::test] 316 329 async fn test_get_logs_raw_send_failure_returns_error() {