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.

at master 359 lines 13 kB view raw
1use strong_api_lib::data_transformer::DataTransformer; 2use strong_api_lib::models::measurement::MeasurementsResponse; 3use strong_api_lib::models::workout::UserResponse; 4 5fn load_fixture(name: &str) -> String { 6 std::fs::read_to_string(format!( 7 "{}/tests/fixtures/{}", 8 env!("CARGO_MANIFEST_DIR"), 9 name 10 )) 11 .unwrap_or_else(|_| panic!("fixture '{name}' not found")) 12} 13 14fn measurements_from_fixture() -> MeasurementsResponse { 15 let json = load_fixture("measurements_response.json"); 16 serde_json::from_str(&json).unwrap() 17} 18 19fn logs_from_fixture() -> Option<Vec<strong_api_lib::models::workout::Log>> { 20 let json = load_fixture("user_response.json"); 21 let user: UserResponse = serde_json::from_str(&json).unwrap(); 22 user.embedded.log 23} 24 25// --------------------------------------------------------------------------- 26// Basic transformation — no measurements loaded, names fall back to empty 27// --------------------------------------------------------------------------- 28 29#[test] 30fn test_transform_without_measurements_produces_workouts() { 31 let transformer = DataTransformer::new(); 32 let logs = logs_from_fixture(); 33 let workouts = transformer.get_measurements_from_logs(&logs).unwrap(); 34 35 assert!(!workouts.is_empty(), "should produce at least one workout"); 36 // Without a measurement lookup every exercise name is empty string 37 for exercise in &workouts[0].exercises { 38 assert_eq!(exercise.name, ""); 39 } 40} 41 42// --------------------------------------------------------------------------- 43// With measurements: names are resolved and exercises are non-empty 44// --------------------------------------------------------------------------- 45 46#[test] 47fn test_transform_with_measurements_resolves_names() { 48 let transformer = 49 DataTransformer::new().with_measurements_response(measurements_from_fixture()); 50 let logs = logs_from_fixture(); 51 let workouts = transformer.get_measurements_from_logs(&logs).unwrap(); 52 53 let exercises = &workouts[0].exercises; 54 assert!(!exercises.is_empty(), "should have at least one exercise"); 55 56 // Every exercise whose measurement was found in the lookup has a non-empty name. 57 let named = exercises.iter().filter(|e| !e.name.is_empty()).count(); 58 assert!( 59 named > 0, 60 "at least one exercise should have a resolved name" 61 ); 62} 63 64// --------------------------------------------------------------------------- 65// Exercises come only from groups that have at least one valid (non-rest) set 66// --------------------------------------------------------------------------- 67 68#[test] 69fn test_only_groups_with_real_sets_become_exercises() { 70 let transformer = 71 DataTransformer::new().with_measurements_response(measurements_from_fixture()); 72 let logs = logs_from_fixture(); 73 let workouts = transformer.get_measurements_from_logs(&logs).unwrap(); 74 75 // Count CSGs in the raw fixture log 76 let json = load_fixture("user_response.json"); 77 let user: UserResponse = serde_json::from_str(&json).unwrap(); 78 let raw_csg_count = user.embedded.log.unwrap()[0].embedded.cell_set_group.len(); 79 80 // Transformed exercise count can be at most the number of raw CSGs 81 assert!(workouts[0].exercises.len() <= raw_csg_count); 82} 83 84// --------------------------------------------------------------------------- 85// Sets: weight and reps fields are always present on every set 86// --------------------------------------------------------------------------- 87 88#[test] 89fn test_sets_have_weight_and_reps() { 90 let transformer = 91 DataTransformer::new().with_measurements_response(measurements_from_fixture()); 92 let logs = logs_from_fixture(); 93 let workouts = transformer.get_measurements_from_logs(&logs).unwrap(); 94 95 let exercises = &workouts[0].exercises; 96 for exercise in exercises { 97 assert!( 98 !exercise.sets.is_empty(), 99 "exercise {} should have sets", 100 exercise.id 101 ); 102 for set in &exercise.sets { 103 let _ = set.reps; // always present 104 let _ = set.weight; // optional (None for bodyweight) 105 } 106 } 107} 108 109// --------------------------------------------------------------------------- 110// Workout metadata is preserved 111// --------------------------------------------------------------------------- 112 113#[test] 114fn test_workout_metadata_is_present() { 115 let transformer = DataTransformer::new(); 116 let logs = logs_from_fixture(); 117 let workouts = transformer.get_measurements_from_logs(&logs).unwrap(); 118 119 let workout = &workouts[0]; 120 assert!(!workout.id.is_empty()); 121 assert!(workout.timezone.is_some(), "timezone should be present"); 122 assert!(workout.start_date.is_some(), "start_date should be present"); 123 assert!(workout.end_date.is_some(), "end_date should be present"); 124} 125 126// --------------------------------------------------------------------------- 127// Measurement ID extraction from link (replaces former inline unit tests) 128// Tested indirectly through the public API: if the ID is extracted correctly 129// the name lookup succeeds; if the link is absent the name is empty. 130// --------------------------------------------------------------------------- 131 132fn make_log_with_measurement_link( 133 measurement_href: Option<&str>, 134) -> Vec<strong_api_lib::models::workout::Log> { 135 use serde_json::json; 136 use strong_api_lib::models::common::Link; 137 use strong_api_lib::models::workout::{ 138 Cell, CellSet, CellSetGroup, CellSetGroupEmbedded, CellSetGroupLinks, Log, LogEmbedded, 139 }; 140 141 vec![Log { 142 id: "log-link-test".to_string(), 143 embedded: LogEmbedded { 144 cell_set_group: vec![CellSetGroup { 145 id: "csg-link-test".to_string(), 146 links: CellSetGroupLinks { 147 measurement: measurement_href.map(|href| Link { 148 href: href.to_string(), 149 }), 150 }, 151 embedded: CellSetGroupEmbedded {}, 152 cell_sets: vec![CellSet { 153 id: "cs-link-test".to_string(), 154 is_completed: Some(true), 155 cells: vec![ 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 }, 166 ], 167 }], 168 }], 169 }, 170 links: json!({}), 171 timezone_id: None, 172 created: "2024-01-01T00:00:00Z".to_string(), 173 last_changed: "2024-01-01T00:00:00Z".to_string(), 174 name: None, 175 access: "private".to_string(), 176 start_date: None, 177 end_date: None, 178 log_type: "WORKOUT".to_string(), 179 }] 180} 181 182#[test] 183fn test_measurement_id_extracted_from_link_resolves_name() { 184 // Use a known measurement ID from the fixture 185 let measurements = measurements_from_fixture(); 186 let known = &measurements.embedded.measurements[0]; 187 let known_id = known.id.clone(); 188 let expected_name = known.name.to_string(); 189 190 let href = format!("/api/users/00000000-0000-0000-0000-000000000001/measurements/{known_id}"); 191 let logs = make_log_with_measurement_link(Some(&href)); 192 193 let transformer = DataTransformer::new().with_measurements_response(measurements); 194 let workouts = transformer.get_measurements_from_logs(&Some(logs)).unwrap(); 195 196 assert_eq!(workouts[0].exercises[0].name, expected_name); 197} 198 199#[test] 200fn test_missing_measurement_link_gives_empty_name() { 201 let logs = make_log_with_measurement_link(None); 202 203 let transformer = 204 DataTransformer::new().with_measurements_response(measurements_from_fixture()); 205 let workouts = transformer.get_measurements_from_logs(&Some(logs)).unwrap(); 206 207 assert_eq!(workouts[0].exercises[0].name, ""); 208} 209 210// --------------------------------------------------------------------------- 211// Cell type coverage — NOTE filter, all weight variants, RPE, missing REPS 212// --------------------------------------------------------------------------- 213 214fn make_log_with_cells( 215 cells: Vec<(String, Option<String>)>, 216) -> Vec<strong_api_lib::models::workout::Log> { 217 use serde_json::json; 218 use strong_api_lib::models::workout::{ 219 Cell, CellSet, CellSetGroup, CellSetGroupEmbedded, CellSetGroupLinks, Log, LogEmbedded, 220 }; 221 222 let cells: Vec<Cell> = cells 223 .into_iter() 224 .enumerate() 225 .map(|(i, (cell_type, value))| Cell { 226 id: format!("c{i}"), 227 cell_type, 228 value, 229 }) 230 .collect(); 231 232 vec![Log { 233 id: "log-cell-test".to_string(), 234 embedded: LogEmbedded { 235 cell_set_group: vec![CellSetGroup { 236 id: "csg-cell-test".to_string(), 237 links: CellSetGroupLinks { measurement: None }, 238 embedded: CellSetGroupEmbedded {}, 239 cell_sets: vec![CellSet { 240 id: "cs-cell-test".to_string(), 241 is_completed: Some(true), 242 cells, 243 }], 244 }], 245 }, 246 links: json!({}), 247 timezone_id: None, 248 created: "2024-01-01T00:00:00Z".to_string(), 249 last_changed: "2024-01-01T00:00:00Z".to_string(), 250 name: None, 251 access: "private".to_string(), 252 start_date: None, 253 end_date: None, 254 log_type: "WORKOUT".to_string(), 255 }] 256} 257 258#[test] 259fn test_note_cell_type_is_excluded() { 260 let logs = make_log_with_cells(vec![ 261 ("NOTE".to_string(), Some("Good session".to_string())), 262 ("REPS".to_string(), Some("10".to_string())), 263 ]); 264 let transformer = DataTransformer::new(); 265 let workouts = transformer.get_measurements_from_logs(&Some(logs)).unwrap(); 266 assert!( 267 workouts[0].exercises.is_empty(), 268 "NOTE cell should cause the group to be filtered out" 269 ); 270} 271 272#[test] 273fn test_dumbbell_weight_cell_type() { 274 let logs = make_log_with_cells(vec![ 275 ("DUMBBELL_WEIGHT".to_string(), Some("20".to_string())), 276 ("REPS".to_string(), Some("12".to_string())), 277 ]); 278 let transformer = DataTransformer::new(); 279 let workouts = transformer.get_measurements_from_logs(&Some(logs)).unwrap(); 280 assert_eq!(workouts[0].exercises[0].sets[0].weight, Some(20.0)); 281} 282 283#[test] 284fn test_other_weight_cell_type() { 285 let logs = make_log_with_cells(vec![ 286 ("OTHER_WEIGHT".to_string(), Some("15.5".to_string())), 287 ("REPS".to_string(), Some("8".to_string())), 288 ]); 289 let transformer = DataTransformer::new(); 290 let workouts = transformer.get_measurements_from_logs(&Some(logs)).unwrap(); 291 assert_eq!(workouts[0].exercises[0].sets[0].weight, Some(15.5)); 292} 293 294#[test] 295fn test_weighted_bodyweight_cell_type() { 296 let logs = make_log_with_cells(vec![ 297 ("WEIGHTED_BODYWEIGHT".to_string(), Some("10".to_string())), 298 ("REPS".to_string(), Some("15".to_string())), 299 ]); 300 let transformer = DataTransformer::new(); 301 let workouts = transformer.get_measurements_from_logs(&Some(logs)).unwrap(); 302 assert_eq!(workouts[0].exercises[0].sets[0].weight, Some(10.0)); 303} 304 305#[test] 306fn test_no_weight_cell_gives_none() { 307 // Only REPS, no weight cell at all 308 let logs = make_log_with_cells(vec![("REPS".to_string(), Some("10".to_string()))]); 309 let transformer = DataTransformer::new(); 310 let workouts = transformer.get_measurements_from_logs(&Some(logs)).unwrap(); 311 assert_eq!(workouts[0].exercises[0].sets[0].weight, None); 312} 313 314#[test] 315fn test_rpe_cell_type() { 316 let logs = make_log_with_cells(vec![ 317 ("BARBELL_WEIGHT".to_string(), Some("100".to_string())), 318 ("REPS".to_string(), Some("5".to_string())), 319 ("RPE".to_string(), Some("9".to_string())), 320 ]); 321 let transformer = DataTransformer::new(); 322 let workouts = transformer.get_measurements_from_logs(&Some(logs)).unwrap(); 323 assert_eq!(workouts[0].exercises[0].sets[0].rpe, Some(9.0)); 324} 325 326#[test] 327fn test_missing_reps_value_defaults_to_zero() { 328 let logs = make_log_with_cells(vec![ 329 ("BARBELL_WEIGHT".to_string(), Some("60".to_string())), 330 ("REPS".to_string(), None), // value is None 331 ]); 332 let transformer = DataTransformer::new(); 333 let workouts = transformer.get_measurements_from_logs(&Some(logs)).unwrap(); 334 assert_eq!(workouts[0].exercises[0].sets[0].reps, 0); 335} 336 337#[test] 338fn test_empty_logs_vec_returns_empty_workouts() { 339 let transformer = DataTransformer::new(); 340 let workouts = transformer 341 .get_measurements_from_logs(&Some(vec![])) 342 .unwrap(); 343 assert!(workouts.is_empty()); 344} 345 346#[test] 347fn test_logs_option_none_returns_empty_workouts() { 348 let transformer = DataTransformer::new(); 349 let workouts = transformer.get_measurements_from_logs(&None).unwrap(); 350 assert!(workouts.is_empty()); 351} 352 353#[test] 354fn test_data_transformer_default_equals_new() { 355 // Exercises the Default impl (derived via `impl Default for DataTransformer`) 356 let by_default: DataTransformer = Default::default(); 357 let workouts = by_default.get_measurements_from_logs(&None).unwrap(); 358 assert!(workouts.is_empty()); 359}