tmc_testmycode_client/
response.rs

1//! Contains types which model the JSON responses from tmc-server
2
3use chrono::{DateTime, FixedOffset};
4use once_cell::sync::Lazy;
5use regex::Regex;
6use schemars::JsonSchema;
7use serde::{
8    Deserialize, Deserializer, Serialize, Serializer,
9    de::{self, Visitor},
10};
11use std::{collections::HashMap, fmt, path::PathBuf, str::FromStr};
12use thiserror::Error;
13
14/// Represents an error response from tmc-server
15#[derive(Debug, Error, Deserialize)]
16#[error("Response contained errors: {error:?}, {errors:#?}, obsolete client: {obsolete_client}")]
17#[serde(deny_unknown_fields)] // prevents responses with an errors field from being parsed as an error
18pub struct ErrorResponse {
19    pub status: Option<String>,
20    pub error: Option<String>,
21    pub errors: Option<Vec<String>>,
22    #[serde(default)]
23    pub obsolete_client: bool,
24}
25
26/// OAuth2 credentials.
27/// get /api/v8/application/{client_name}/credentials
28#[derive(Debug, Deserialize)]
29pub struct Credentials {
30    pub application_id: String,
31    pub secret: String,
32}
33
34/// get /api/v8/users/{user_id}
35/// get /api/v8/users/current
36/// post /api/v8/users/basic_info_by_usernames
37/// post /api/v8/users/basic_info_by_emails
38#[derive(Debug, Deserialize)]
39pub struct User {
40    pub id: u32,
41    pub username: String,
42    pub email: String,
43    pub administrator: bool,
44}
45
46/// get /api/v8/org.json
47/// get /api/v8/org/{organization_slug}.json
48#[derive(Debug, Serialize, Deserialize, JsonSchema)]
49#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
50pub struct Organization {
51    pub name: String,
52    pub information: String,
53    pub slug: String,
54    pub logo_path: String,
55    pub pinned: bool,
56}
57
58/// get /api/v8/core/org/{organization_slug}/courses
59#[derive(Debug, Deserialize, Serialize, JsonSchema)]
60#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
61pub struct Course {
62    pub id: u32,
63    pub name: String,
64    pub title: String,
65    pub description: Option<String>,
66    /// /api/v8/core/courses/{course_id}
67    pub details_url: String,
68    /// /api/v8/core/courses/{course_id}/unlock
69    pub unlock_url: String,
70    /// /api/v8/core/courses/{course_id}/reviews
71    pub reviews_url: String,
72    /// Typically empty.
73    pub comet_url: String,
74    pub spyware_urls: Vec<String>,
75}
76
77/// get /api/v8/courses/{course_id}
78/// get /api/v8/org/{organization_slug}/courses/{course_name}
79#[derive(Debug, Deserialize, Serialize, JsonSchema)]
80#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
81pub struct CourseData {
82    pub name: String,
83    pub hide_after: Option<String>,
84    pub hidden: bool,
85    pub cache_version: Option<u32>,
86    pub spreadsheet_key: Option<String>,
87    pub hidden_if_registered_after: Option<String>,
88    #[cfg_attr(feature = "ts-rs", ts(type = "string | null"))]
89    pub refreshed_at: Option<DateTime<FixedOffset>>,
90    pub locked_exercise_points_visible: bool,
91    pub description: Option<String>,
92    pub paste_visibility: Option<u32>,
93    pub formal_name: Option<String>,
94    pub certificate_downloadable: Option<bool>,
95    pub certificate_unlock_spec: Option<String>,
96    pub organization_id: Option<u32>,
97    pub disabled_status: Option<String>,
98    pub title: Option<String>,
99    /// Typically empty.
100    pub material_url: Option<String>,
101    pub course_template_id: Option<u32>,
102    pub hide_submission_results: bool,
103    /// Typically empty.
104    pub external_scoreboard_url: Option<String>,
105    pub organization_slug: Option<String>,
106}
107
108/// Represents a course details response from tmc-server,
109/// converted to the more convenient CourseDetails during deserialization
110#[derive(Debug, Deserialize, JsonSchema)]
111struct CourseDetailsWrapper {
112    pub course: CourseDetailsInner,
113}
114
115// TODO: improve
116#[derive(Debug, Deserialize, JsonSchema)]
117struct CourseDetailsInner {
118    #[serde(flatten)]
119    pub course: Course,
120    pub unlockables: Vec<String>,
121    pub exercises: Vec<Exercise>,
122}
123
124/// get /api/v8/core/courses/{course_id}
125#[derive(Debug, Deserialize, Serialize, JsonSchema)]
126#[serde(from = "CourseDetailsWrapper")]
127#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
128// we never take these structs as inputs from TS so it's ok to ignore from
129#[cfg_attr(feature = "ts-rs", ts(ignore_serde_attr = "from"))]
130pub struct CourseDetails {
131    #[serde(flatten)]
132    pub course: Course,
133    pub unlockables: Vec<String>,
134    pub exercises: Vec<Exercise>,
135}
136
137impl From<CourseDetailsWrapper> for CourseDetails {
138    fn from(value: CourseDetailsWrapper) -> Self {
139        Self {
140            course: value.course.course,
141            unlockables: value.course.unlockables,
142            exercises: value.course.exercises,
143        }
144    }
145}
146
147#[derive(Debug, Deserialize, Serialize, JsonSchema)]
148#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
149pub struct Exercise {
150    pub id: u32,
151    pub name: String,
152    pub locked: bool,
153    pub deadline_description: Option<String>,
154    pub deadline: Option<String>,
155    pub soft_deadline: Option<String>,
156    pub soft_deadline_description: Option<String>,
157    pub checksum: String,
158    /// /api/v8/core/exercises/{exercise_id}/submissions
159    pub return_url: String,
160    /// /api/v8/core/exercises/{exercise_id}/download
161    pub zip_url: String,
162    pub returnable: bool,
163    pub requires_review: bool,
164    pub attempted: bool,
165    pub completed: bool,
166    pub reviewed: bool,
167    pub all_review_points_given: bool,
168    pub memory_limit: Option<u32>,
169    pub runtime_params: Vec<String>,
170    pub valgrind_strategy: Option<String>,
171    pub code_review_requests_enabled: bool,
172    pub run_tests_locally_action_enabled: bool,
173    /// Typically null.
174    pub latest_submission_url: Option<String>,
175    pub latest_submission_id: Option<u32>,
176    /// /api/v8/core/exercises/{exercise_id}/solution/download
177    pub solution_zip_url: Option<String>,
178}
179
180/// get /api/v8/courses/{course_id}/exercises
181#[derive(Debug, Deserialize, Serialize, JsonSchema)]
182#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
183pub struct CourseExercise {
184    pub id: u32,
185    pub available_points: Vec<ExercisePoint>,
186    pub awarded_points: Vec<String>,
187    pub name: String,
188    pub publish_time: Option<String>,
189    pub solution_visible_after: Option<String>,
190    pub deadline: Option<String>,
191    pub soft_deadline: Option<String>,
192    pub disabled: bool,
193    pub unlocked: bool,
194}
195
196/// get /api/v8/org/{organization_slug}/courses/{course_name}/exercises
197#[derive(Debug, Deserialize)]
198pub struct CourseDataExercise {
199    pub id: u32,
200    pub available_points: Vec<ExercisePoint>,
201    pub name: String,
202    pub publish_time: Option<String>,
203    pub solution_visible_after: Option<String>,
204    pub deadline: Option<String>,
205    pub disabled: bool,
206}
207
208#[derive(Debug, Deserialize, Serialize, JsonSchema)]
209#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
210pub struct ExercisePoint {
211    pub id: u32,
212    pub exercise_id: u32,
213    pub name: String,
214    pub requires_review: bool,
215}
216
217/// get /api/v8/courses/{course_id}/points
218/// get /api/v8/courses/{course_id}/exercises/{exercise_name}/points
219/// get /api/v8/courses/{course_id}/exercises/{exercise_name}/users/{user_id}/
220/// get /api/v8/courses/{course_id}/exercises/{exercise_name}/users/current/points
221/// get /api/v8/courses/{course_id}/users/{user_id}/points
222/// get /api/v8/courses/{course_id}/users/current/points
223/// get /api/v8/org/{organization_slug}/courses/{course_name}/points
224/// get /api/v8/org/{organization_slug}/courses/{course_name}/exercises/{exercise_name}/points
225/// get /api/v8/org/{organization_slug}/courses/{course_name}/users/{user_id}/points
226/// get /api/v8/org/{organization_slug}/courses/{course_name}/users/current/points
227#[derive(Debug, Deserialize)]
228pub struct CourseDataExercisePoint {
229    pub awarded_point: AwardedPoint,
230    pub exercise_id: u32,
231}
232
233#[derive(Debug, Deserialize)]
234pub struct AwardedPoint {
235    pub id: u32,
236    pub course_id: u32,
237    pub user_id: u32,
238    pub submission_id: u32,
239    pub name: String,
240    pub created_at: DateTime<FixedOffset>,
241}
242
243/// get /api/v8/core/exercises/{exercise_id}
244#[derive(Debug, Deserialize, Serialize, JsonSchema)]
245#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
246pub struct ExerciseDetails {
247    pub course_name: String,
248    pub course_id: u32,
249    pub code_review_requests_enabled: bool,
250    pub run_tests_locally_action_enabled: bool,
251    pub exercise_name: String,
252    pub exercise_id: u32,
253    pub unlocked_at: Option<String>,
254    pub deadline: Option<String>,
255    pub submissions: Vec<ExerciseSubmission>,
256}
257
258/// get /api/v8/core/exercises/details
259#[derive(Debug, Deserialize)]
260pub(crate) struct ExercisesDetailsWrapper {
261    pub exercises: Vec<ExercisesDetails>,
262}
263
264#[derive(Debug, Deserialize, Serialize, JsonSchema)]
265pub struct ExercisesDetails {
266    pub id: u32,
267    pub course_name: String,
268    pub exercise_name: String,
269    pub checksum: String,
270    pub hide_submission_results: bool,
271}
272
273/// get /api/v8/courses/{course_id}/submissions
274/// get /api/v8/courses/{course_id}/users/{user_id}/submissions
275/// get /api/v8/courses/{course_id}/users/current/submissions
276/// get api/v8/exercises/{exercise_id}/users/{user_id}/submissions
277/// get api/v8/exercises/{exercise_id}/users/current/submissions
278/// get /api/v8/org/{organization_slug}/courses/{course_name}/submissions
279/// get /api/v8/org/{organization_slug}/courses/{course_name}/users/{user_id}/submissions
280/// get /api/v8/org/{organization_slug}/courses/{course_name}/users/current/submissions
281/// get api/v8/exercises/{exercise_id}/users/{user_id}/submissions
282/// get api/v8/exercises/{exercise_id}/users/current/submissions
283#[derive(Debug, Deserialize, Serialize, JsonSchema)]
284#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
285pub struct Submission {
286    pub id: u32,
287    pub user_id: u32,
288    pub pretest_error: Option<String>,
289    #[cfg_attr(feature = "ts-rs", ts(type = "string"))]
290    pub created_at: DateTime<FixedOffset>,
291    pub exercise_name: String,
292    pub course_id: u32,
293    pub processed: bool,
294    pub all_tests_passed: bool,
295    pub points: Option<String>,
296    #[cfg_attr(feature = "ts-rs", ts(type = "string | null"))]
297    pub processing_tried_at: Option<DateTime<FixedOffset>>,
298    #[cfg_attr(feature = "ts-rs", ts(type = "string | null"))]
299    pub processing_began_at: Option<DateTime<FixedOffset>>,
300    #[cfg_attr(feature = "ts-rs", ts(type = "string | null"))]
301    pub processing_completed_at: Option<DateTime<FixedOffset>>,
302    pub times_sent_to_sandbox: u32,
303    #[cfg_attr(feature = "ts-rs", ts(type = "string"))]
304    pub processing_attempts_started_at: DateTime<FixedOffset>,
305    pub params_json: Option<String>,
306    pub requires_review: bool,
307    pub requests_review: bool,
308    pub reviewed: bool,
309    pub message_for_reviewer: String,
310    pub newer_submission_reviewed: bool,
311    pub review_dismissed: bool,
312    pub paste_available: bool,
313    pub message_for_paste: String,
314    pub paste_key: Option<String>,
315}
316
317#[derive(Debug, Deserialize, Serialize, JsonSchema)]
318#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
319pub struct ExerciseSubmission {
320    pub exercise_name: String,
321    pub id: u32,
322    pub user_id: u32,
323    pub course_id: u32,
324    #[cfg_attr(feature = "ts-rs", ts(type = "string"))]
325    pub created_at: DateTime<FixedOffset>,
326    pub all_tests_passed: bool,
327    pub points: Option<String>,
328    /// /api/v8/core/submissions/{submission_id}/download
329    pub submitted_zip_url: String,
330    /// https://tmc.mooc.fi/paste/{paste_code}
331    pub paste_url: Option<String>,
332    pub processing_time: Option<u32>,
333    pub reviewed: bool,
334    pub requests_review: bool,
335}
336
337/// post /api/v8/core/exercises/{exercise_id}/submissions
338#[derive(Debug, Deserialize, Serialize, JsonSchema, Clone)]
339#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
340pub struct NewSubmission {
341    /// https://tmc.mooc.fi/api/v8/core/submissions/{submission_id}
342    pub show_submission_url: String,
343    /// https://tmc.mooc.fi/paste/{paste_code}
344    pub paste_url: String, // use Option and serde_with::string_empty_as_none ?
345    /// https://tmc.mooc.fi/submissions/{submission_id}
346    pub submission_url: String,
347}
348
349/// get /api/v8/core/submissions/{submission_id}
350#[derive(Debug, Deserialize, Serialize)]
351#[serde(untagged)] // TODO: tag
352pub enum SubmissionProcessingStatus {
353    Processing(SubmissionProcessing),
354    Finished(Box<SubmissionFinished>),
355}
356
357#[derive(Debug, Deserialize, Serialize)]
358pub struct SubmissionProcessing {
359    pub status: SubmissionStatus,
360    pub sandbox_status: SandboxStatus,
361}
362
363#[derive(Debug, Deserialize, PartialEq, Eq, Serialize)]
364#[serde(rename_all = "snake_case")]
365pub enum SandboxStatus {
366    Created,
367    SendingToSandbox,
368    ProcessingOnSandbox,
369}
370
371#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, JsonSchema)]
372#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
373pub struct SubmissionFinished {
374    pub api_version: u32,
375    pub all_tests_passed: Option<bool>,
376    pub user_id: u32,
377    pub login: String,
378    pub course: String,
379    pub exercise_name: String,
380    pub status: SubmissionStatus,
381    pub points: Vec<String>,
382    pub valgrind: Option<String>,
383    /// https://tmc.mooc.fi/submissions/{submission_id}}
384    pub submission_url: String,
385    /// https://tmc.mooc.fi/exercises/{exercise_id}/solution
386    pub solution_url: Option<String>,
387    pub submitted_at: String,
388    pub processing_time: Option<u32>,
389    pub reviewed: bool,
390    pub requests_review: bool,
391    /// https://tmc.mooc.fi/paste/{paste_code}
392    pub paste_url: Option<String>,
393    pub message_for_paste: Option<String>,
394    pub missing_review_points: Vec<String>,
395    pub test_cases: Option<Vec<TestCase>>,
396    pub feedback_questions: Option<Vec<SubmissionFeedbackQuestion>>,
397    /// /api/v8/core/submissions/{submission_id}/feedback
398    pub feedback_answer_url: Option<String>,
399    pub error: Option<String>,
400    pub validations: Option<TmcStyleValidationResult>,
401}
402
403#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, JsonSchema)]
404#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
405#[serde(rename_all = "lowercase")]
406pub enum SubmissionStatus {
407    Processing,
408    Fail,
409    Ok,
410    Error,
411    Hidden,
412}
413
414/// post /api/v8/core/submissions/{submission_id}/feedback
415#[derive(Debug, Deserialize, Serialize, JsonSchema)]
416#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
417pub struct SubmissionFeedbackResponse {
418    pub api_version: u32,
419    pub status: SubmissionStatus,
420}
421
422#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, JsonSchema)]
423#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
424pub struct TestCase {
425    pub name: String,
426    pub successful: bool,
427    pub message: Option<String>,
428    pub exception: Option<Vec<String>>,
429    pub detailed_message: Option<String>,
430}
431
432#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, JsonSchema)]
433#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
434pub struct SubmissionFeedbackQuestion {
435    pub id: u32,
436    pub question: String,
437    pub kind: SubmissionFeedbackKind,
438}
439
440#[derive(Debug, Clone, Copy, PartialEq, Eq, JsonSchema)]
441#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
442pub enum SubmissionFeedbackKind {
443    Text,
444    IntRange { lower: u32, upper: u32 },
445}
446
447impl<'de> Deserialize<'de> for SubmissionFeedbackKind {
448    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
449    where
450        D: Deserializer<'de>,
451    {
452        let wrapper = SubmissionFeedbackKindWrapper::deserialize(deserializer)?;
453        let kind = match wrapper {
454            SubmissionFeedbackKindWrapper::String(string) => match string {
455                SubmissionFeedbackKindString::Text => Self::Text,
456                SubmissionFeedbackKindString::IntRange { lower, upper } => {
457                    Self::IntRange { lower, upper }
458                }
459            },
460            SubmissionFeedbackKindWrapper::Derived(derived) => match derived {
461                SubmissionFeedbackKindDerived::Text => Self::Text,
462                SubmissionFeedbackKindDerived::IntRange { lower, upper } => {
463                    Self::IntRange { lower, upper }
464                }
465            },
466        };
467        Ok(kind)
468    }
469}
470
471impl Serialize for SubmissionFeedbackKind {
472    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
473    where
474        S: Serializer,
475    {
476        let derived = match self {
477            Self::Text => SubmissionFeedbackKindDerived::Text,
478            Self::IntRange { lower, upper } => SubmissionFeedbackKindDerived::IntRange {
479                lower: *lower,
480                upper: *upper,
481            },
482        };
483        derived.serialize(serializer)
484    }
485}
486
487// wraps the two stringly typed and rusty versions of the kind
488#[derive(Debug, Clone, Copy, Deserialize)]
489#[serde(untagged)]
490enum SubmissionFeedbackKindWrapper {
491    Derived(SubmissionFeedbackKindDerived),
492    String(SubmissionFeedbackKindString),
493}
494
495// uses derived serde impls
496#[derive(Debug, Clone, Copy, Deserialize, Serialize)]
497enum SubmissionFeedbackKindDerived {
498    Text,
499    IntRange { lower: u32, upper: u32 },
500}
501
502// the stringly typed "text" or "intrange" that comes from the server
503#[derive(Debug, Clone, Copy)]
504enum SubmissionFeedbackKindString {
505    Text,
506    IntRange { lower: u32, upper: u32 },
507}
508
509impl<'de> Deserialize<'de> for SubmissionFeedbackKindString {
510    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
511    where
512        D: Deserializer<'de>,
513    {
514        deserializer.deserialize_string(SubmissionFeedbackKindStringVisitor {})
515    }
516}
517
518struct SubmissionFeedbackKindStringVisitor {}
519
520// parses "text" into Text, and "intrange[x..y]" into IntRange {lower: x, upper: y}
521impl Visitor<'_> for SubmissionFeedbackKindStringVisitor {
522    type Value = SubmissionFeedbackKindString;
523
524    fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
525        formatter.write_str("\"text\" or \"intrange[x..y]\"")
526    }
527
528    fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
529    where
530        E: de::Error,
531    {
532        #[allow(clippy::unwrap_used)]
533        static RANGE: Lazy<Regex> =
534            Lazy::new(|| Regex::new(r#"intrange\[(\d+)\.\.(\d+)\]"#).unwrap());
535
536        if value == "text" {
537            Ok(SubmissionFeedbackKindString::Text)
538        } else if let Some(captures) = RANGE.captures(value) {
539            let lower = &captures[1];
540            let lower = u32::from_str(lower).map_err(|e| {
541                E::custom(format!("error parsing intrange lower bound {lower}: {e}"))
542            })?;
543            let upper = &captures[2];
544            let upper = u32::from_str(upper).map_err(|e| {
545                E::custom(format!("error parsing intrange upper bound {upper}: {e}"))
546            })?;
547            Ok(SubmissionFeedbackKindString::IntRange { lower, upper })
548        } else {
549            Err(E::custom("expected \"text\" or \"intrange[x..y]\""))
550        }
551    }
552}
553
554/// get /api/v8/core/courses/{course_id}/reviews
555#[derive(Debug, Deserialize, Serialize, JsonSchema)]
556#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
557pub struct Review {
558    pub submission_id: u32,
559    pub exercise_name: String,
560    pub id: u32,
561    pub marked_as_read: bool,
562    pub reviewer_name: String,
563    pub review_body: String,
564    pub points: Vec<String>,
565    pub points_not_awarded: Vec<String>,
566    /// https://tmc.mooc.fi/submissions/{submission_id}/reviews
567    pub url: String,
568    /// /api/v8/core/courses/{course_id}/reviews/{review_id}
569    pub update_url: String,
570    #[cfg_attr(feature = "ts-rs", ts(type = "string"))]
571    pub created_at: DateTime<FixedOffset>,
572    #[cfg_attr(feature = "ts-rs", ts(type = "string"))]
573    pub updated_at: DateTime<FixedOffset>,
574}
575
576/// The result of a style check.
577#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, JsonSchema)]
578#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
579#[serde(rename_all = "camelCase")]
580pub struct TmcStyleValidationResult {
581    pub strategy: TmcStyleValidationStrategy,
582    pub validation_errors: Option<HashMap<PathBuf, Vec<TmcStyleValidationError>>>,
583}
584
585/// Determines how style errors are handled.
586#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, JsonSchema)]
587#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
588#[serde(rename_all = "UPPERCASE")]
589pub enum TmcStyleValidationStrategy {
590    Fail,
591    Warn,
592    Disabled,
593}
594
595/// A style validation error.
596#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, JsonSchema)]
597#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
598#[serde(rename_all = "camelCase")]
599pub struct TmcStyleValidationError {
600    pub column: u32,
601    pub line: u32,
602    pub message: String,
603    pub source_name: String,
604}
605
606#[cfg(test)]
607#[allow(clippy::unwrap_used)]
608mod test {
609    use super::*;
610    use tmc_langs_util::deserialize;
611
612    fn init() {
613        use log::*;
614        use simple_logger::*;
615        // the module levels must be set here too for some reason,
616        // even though this module does not use mockito etc.
617        let _ = SimpleLogger::new()
618            .with_level(LevelFilter::Debug)
619            // mockito does some logging
620            .with_module_level("mockito", LevelFilter::Warn)
621            // reqwest does a lot of logging
622            .with_module_level("reqwest", LevelFilter::Warn)
623            // hyper does a lot of logging
624            .with_module_level("hyper", LevelFilter::Warn)
625            .init();
626    }
627
628    #[test]
629    fn course_details_de() {
630        init();
631
632        let details = serde_json::json!(
633            {
634                "course": {
635                    "comet_url": "c",
636                    "description": "d",
637                    "details_url": "du",
638                    "id": 1,
639                    "name": "n",
640                    "reviews_url": "r",
641                    "spyware_urls": [
642                        "s"
643                    ],
644                    "title": "t",
645                    "unlock_url": "u",
646                    "unlockables": ["a"],
647                    "exercises": []
648                }
649            }
650        );
651        assert!(deserialize::json_from_value::<CourseDetails>(details).is_ok());
652    }
653
654    #[test]
655    fn feedback_kind_de_server() {
656        init();
657
658        let text = serde_json::json!("text");
659        let text: SubmissionFeedbackKind = deserialize::json_from_value(text).unwrap();
660        if let SubmissionFeedbackKind::Text = text {
661        } else {
662            panic!("wrong type")
663        }
664
665        let intrange = serde_json::json!("intrange[1..5]");
666        let intrange: SubmissionFeedbackKind = deserialize::json_from_value(intrange).unwrap();
667        if let SubmissionFeedbackKind::IntRange { lower: 1, upper: 5 } = intrange {
668        } else {
669            panic!("wrong type")
670        }
671    }
672
673    #[test]
674    fn feedback_kind_de_rust() {
675        init();
676
677        let original = SubmissionFeedbackKind::Text;
678        let json = serde_json::to_string(&original).unwrap();
679        let deserialized: SubmissionFeedbackKind = deserialize::json_from_str(&json).unwrap();
680        assert_eq!(deserialized, original);
681
682        let original = SubmissionFeedbackKind::IntRange { lower: 1, upper: 5 };
683        let json = serde_json::to_string(&original).unwrap();
684        let deserialized: SubmissionFeedbackKind = deserialize::json_from_str(&json).unwrap();
685        assert_eq!(deserialized, original);
686    }
687
688    #[test]
689    fn feedback_kind_se() {
690        init();
691
692        let original = SubmissionFeedbackKind::Text;
693        let json = serde_json::to_string(&original).unwrap();
694        assert_eq!(json, r#""Text""#);
695
696        let original = SubmissionFeedbackKind::IntRange { lower: 1, upper: 5 };
697        let json = serde_json::to_string(&original).unwrap();
698        assert_eq!(json, r#"{"IntRange":{"lower":1,"upper":5}}"#);
699    }
700
701    #[test]
702    fn submission_response_with_validation_errors() {
703        init();
704
705        let submission = serde_json::json!(
706            {
707                "api_version": 7,
708                "all_tests_passed": false,
709                "user_id": 12345,
710                "login": "12345",
711                "course": "course",
712                "exercise_name": "exercise",
713                "status": "fail",
714                "points": [],
715                "validations": {
716                  "strategy": "FAIL",
717                  "validationErrors": {
718                    "Main.java": [
719                      {
720                        "column": 1,
721                        "line": 1,
722                        "message": "Indentation incorrect. Expected 8, but was 12.",
723                        "sourceName": "com.puppycrawl.tools.checkstyle.checks.indentation.IndentationCheck"
724                      },
725                      {
726                        "column": 2,
727                        "line": 2,
728                        "message": "Indentation incorrect. Expected 8, but was 12.",
729                        "sourceName": "com.puppycrawl.tools.checkstyle.checks.indentation.IndentationCheck"
730                      }
731                    ]
732                  }
733                },
734                "valgrind": "",
735                "submission_url": "https://tmc.mooc.fi/submissions/12345",
736                "solution_url": "https://tmc.mooc.fi/exercises/12345/solution",
737                "submitted_at": "2025-01-01T01:01:01.001+01:01",
738                "processing_time": 12345,
739                "reviewed": false,
740                "requests_review": false,
741                "paste_url": null,
742                "message_for_paste": null,
743                "missing_review_points": [],
744                "test_cases": [
745                  {
746                    "name": "FakeTest test",
747                    "successful": true,
748                    "message": "",
749                    "exception": [],
750                    "detailed_message": null
751                  },
752                  {
753                    "name": "FakeTest testTwo",
754                    "successful": true,
755                    "message": "",
756                    "exception": [],
757                    "detailed_message": null
758                  },
759                  {
760                    "name": "FakeTest testThree",
761                    "successful": true,
762                    "message": "",
763                    "exception": [],
764                    "detailed_message": null
765                  }
766                ]
767              }
768        );
769        let submission = deserialize::json_from_value::<SubmissionFinished>(submission).unwrap();
770        assert!(
771            !submission
772                .validations
773                .unwrap()
774                .validation_errors
775                .unwrap()
776                .is_empty()
777        );
778    }
779}