cloudreve-api 0.8.4

A Rust library for interacting with Cloudreve API
Documentation
use cloudreve_api::api::v3::models::*;

#[cfg(test)]
mod v3_models_tests {
    use super::*;

    #[test]
    fn test_user_struct() {
        let user = User {
            id: "l6hY".to_string(),
            user_name: "admin@cloudreve.org".to_string(),
            nickname: "admin".to_string(),
            status: 0,
            avatar: "".to_string(),
            created_at: "2024-05-01T11:04:25.490486+08:00".to_string(),
            preferred_theme: "".to_string(),
            anonymous: false,
            group: UserGroup {
                id: 1,
                name: "Admin".to_string(),
                allow_share: true,
                allow_remote_download: true,
                allow_archive_download: true,
                share_download: true,
                compress: true,
                webdav: true,
                source_batch: 1000,
                advance_delete: true,
                allow_web_dav_proxy: false,
            },
            tags: vec![],
        };

        assert_eq!(user.id, "l6hY");
        assert_eq!(user.user_name, "admin@cloudreve.org");
        assert_eq!(user.nickname, "admin");
        assert_eq!(user.status, 0);
    }

    #[test]
    fn test_object_struct() {
        let obj = Object {
            id: "bbTL".to_string(),
            name: "my_folder".to_string(),
            path: "/".to_string(),
            thumb: false,
            size: 0,
            object_type: "dir".to_string(),
            date: "2024-05-01T11:19:12.1733916+08:00".to_string(),
            create_date: "2024-05-01T11:19:12.1733916+08:00".to_string(),
            source_enabled: false,
        };

        assert_eq!(obj.id, "bbTL");
        assert_eq!(obj.name, "my_folder");
        assert_eq!(obj.object_type, "dir");
        assert_eq!(obj.size, 0);
    }

    #[test]
    fn test_policy_struct() {
        let policy = Policy {
            id: "z3hJ".to_string(),
            name: "Default storage policy".to_string(),
            policy_type: "local".to_string(),
            max_size: 0,
            file_type: None,
        };

        assert_eq!(policy.id, "z3hJ");
        assert_eq!(policy.name, "Default storage policy");
        assert_eq!(policy.policy_type, "local");
    }

    #[test]
    fn test_property_struct() {
        let property = Property {
            created_at: "2024-05-01T11:05:21.8852154+08:00".to_string(),
            updated_at: "2024-04-30T15:25:46.424+08:00".to_string(),
            policy: "Default storage policy".to_string(),
            size: 1597,
            child_folder_num: 0,
            child_file_num: 0,
            path: "".to_string(),
            query_date: "2024-05-01T11:20:07.6083989+08:00".to_string(),
        };

        assert_eq!(property.size, 1597);
        assert_eq!(property.child_folder_num, 0);
        assert_eq!(property.child_file_num, 0);
    }

    #[test]
    fn test_directory_list_struct() {
        let objects = vec![Object {
            id: "bbTL".to_string(),
            name: "my_folder".to_string(),
            path: "/".to_string(),
            thumb: false,
            size: 0,
            object_type: "dir".to_string(),
            date: "2024-05-01T11:19:12.1733916+08:00".to_string(),
            create_date: "2024-05-01T11:19:12.1733916+08:00".to_string(),
            source_enabled: false,
        }];

        let dir_list = DirectoryList {
            parent: "9zh3".to_string(),
            objects,
            policy: Policy {
                id: "z3hJ".to_string(),
                name: "Default storage policy".to_string(),
                policy_type: "local".to_string(),
                max_size: 0,
                file_type: None,
            },
        };

        assert_eq!(dir_list.parent, "9zh3");
        assert_eq!(dir_list.objects.len(), 1);
    }

    #[test]
    fn test_upload_session_struct() {
        let session = UploadSession {
            session_id: "abc123".to_string(),
            chunk_size: 1024,
            expires: 1234567890,
        };

        assert_eq!(session.session_id, "abc123");
        assert_eq!(session.chunk_size, 1024);
        assert_eq!(session.expires, 1234567890);
    }

    #[test]
    fn test_storage_info_struct() {
        let storage = StorageInfo {
            used: 1597,
            free: 1073740227,
            total: 1073741824,
        };

        assert_eq!(storage.used, 1597);
        assert_eq!(storage.free, 1073740227);
        assert_eq!(storage.total, 1073741824);
    }

    #[test]
    fn test_api_response_struct() {
        let response: ApiResponse<String> = ApiResponse {
            code: 0,
            msg: "".to_string(),
            data: Some("success".to_string()),
        };

        assert_eq!(response.code, 0);
        assert!(response.data.is_some());
    }

    #[test]
    fn test_login_request_struct() {
        let request = LoginRequest {
            user_name: "admin@cloudreve.org",
            password: "password",
            captcha_code: None,
        };

        assert_eq!(request.user_name, "admin@cloudreve.org");
        assert_eq!(request.password, "password");
    }

    #[test]
    fn test_upload_file_request_struct() {
        let request = UploadFileRequest {
            path: "/test/b",
            size: 188,
            name: "1.py",
            policy_id: "kVfW",
            last_modified: 1714481625683,
            mime_type: "",
        };

        assert_eq!(request.path, "/test/b");
        assert_eq!(request.size, 188);
        assert_eq!(request.name, "1.py");
    }

    #[test]
    fn test_share_request_struct() {
        let request = ShareRequest {
            id: "dmLxcN".to_string(),
            is_dir: false,
            password: "".to_string(),
            downloads: -1,
            expire: 86400,
            preview: true,
        };

        assert_eq!(request.id, "dmLxcN");
        assert_eq!(request.downloads, -1);
        assert_eq!(request.expire, 86400);
    }

    /// Real `/aria2/downloading` payload sample. Catches regressions where Aria2Task got
    /// remodeled with fictional fields like `progress: f64` that don't exist in the
    /// Cloudreve v3 response.
    #[test]
    fn test_aria2_downloading_deserializes_real_payload() {
        let payload = r#"{
            "code": 0,
            "msg": "",
            "data": [{
                "update": "2025-06-11T12:00:00+08:00",
                "interval": 5,
                "name": "ubuntu-22.04.iso",
                "status": 1,
                "dst": "/Downloads",
                "total": 4929355776,
                "downloaded": 524288000,
                "speed": 1048576,
                "node": "master",
                "info": {
                    "gid": "2089b05ecca3d829",
                    "status": "active",
                    "totalLength": "4929355776",
                    "completedLength": "524288000",
                    "downloadSpeed": "1048576",
                    "numPieces": "1175",
                    "files": [{
                        "index": "1",
                        "path": "/data/aria2/ubuntu-22.04.iso",
                        "length": "4929355776",
                        "completedLength": "524288000",
                        "selected": true,
                        "uris": [{"uri": "https://releases.ubuntu.com/22.04/ubuntu.iso", "status": "used"}]
                    }]
                }
            }]
        }"#;
        let resp: ApiResponse<Vec<Aria2DownloadingTask>> =
            serde_json::from_str(payload).expect("downloading payload should deserialize");
        let tasks = resp.data.expect("data");
        assert_eq!(tasks.len(), 1);
        let t = &tasks[0];
        assert_eq!(t.name, "ubuntu-22.04.iso");
        assert_eq!(t.status, 1);
        assert_eq!(t.total, 4929355776);
        assert_eq!(t.downloaded, 524288000);
        assert_eq!(t.info.gid, "2089b05ecca3d829");
        assert_eq!(t.info.total_length, "4929355776");
        assert_eq!(t.info.files.len(), 1);
        assert_eq!(t.info.files[0].uris.len(), 1);
        assert_eq!(t.info.files[0].uris[0].status, "used");
    }

    /// Real `/aria2/finished` payload sample. Includes the dual status fields and the
    /// nested `files` array Cloudreve v3 emits.
    #[test]
    fn test_aria2_finished_deserializes_real_payload() {
        let payload = r#"{
            "code": 0,
            "msg": "",
            "data": [{
                "name": "demo.zip",
                "gid": "abcd1234",
                "status": 4,
                "dst": "/Downloads",
                "error": "",
                "total": 1048576,
                "files": [{
                    "index": "0",
                    "path": "/data/aria2/demo.zip",
                    "length": "1048576",
                    "completedLength": "1048576",
                    "selected": true,
                    "uris": []
                }],
                "task_status": 4,
                "task_error": "",
                "create": "2025-06-10T18:00:00+08:00",
                "update": "2025-06-10T18:05:00+08:00",
                "node": "master"
            }]
        }"#;
        let resp: ApiResponse<Vec<Aria2FinishedTask>> =
            serde_json::from_str(payload).expect("finished payload should deserialize");
        let tasks = resp.data.expect("data");
        assert_eq!(tasks.len(), 1);
        let t = &tasks[0];
        assert_eq!(t.name, "demo.zip");
        assert_eq!(t.gid, "abcd1234");
        assert_eq!(t.status, 4);
        assert_eq!(t.task_status, 4);
        assert_eq!(t.total, 1048576);
        assert_eq!(t.files.len(), 1);
        assert_eq!(t.files[0].completed_length, "1048576");
    }

    /// Cloudreve v3 echoes aria2c's wire format for the `selected` field, which uses the
    /// string `"true"` / `"false"` rather than a JSON boolean. Make sure the deserializer
    /// accepts both spellings.
    #[test]
    fn test_aria2_file_selected_accepts_string_bool() {
        let payload_string = r#"{"code":0,"msg":"","data":[{
            "name":"demo.zip","gid":"g1","status":4,"dst":"/d","error":"","total":1,
            "files":[{"index":"0","path":"/d/demo.zip","length":"1","completedLength":"1","selected":"true","uris":[]}],
            "task_status":4,"task_error":"","create":"","update":"","node":""
        }]}"#;
        let resp: ApiResponse<Vec<Aria2FinishedTask>> =
            serde_json::from_str(payload_string).expect("string-bool should be accepted");
        assert!(resp.data.unwrap()[0].files[0].selected);

        let payload_bool = r#"{"code":0,"msg":"","data":[{
            "name":"demo.zip","gid":"g1","status":4,"dst":"/d","error":"","total":1,
            "files":[{"index":"0","path":"/d/demo.zip","length":"1","completedLength":"1","selected":false,"uris":[]}],
            "task_status":4,"task_error":"","create":"","update":"","node":""
        }]}"#;
        let resp: ApiResponse<Vec<Aria2FinishedTask>> =
            serde_json::from_str(payload_bool).expect("native bool should still work");
        assert!(!resp.data.unwrap()[0].files[0].selected);
    }

    /// `/aria2/finished` may return `data: null` when there are no finished tasks.
    /// `list_finished()` should treat that as an empty list, not a parse error.
    #[test]
    fn test_aria2_finished_handles_null_data() {
        let payload = r#"{"code": 0, "msg": "", "data": null}"#;
        let resp: ApiResponse<Vec<Aria2FinishedTask>> =
            serde_json::from_str(payload).expect("null-data payload should deserialize");
        assert!(resp.data.is_none());
    }

    /// `/user/setting/tasks` paginated payload — distinct from aria2 endpoints.
    #[test]
    fn test_user_task_list_deserializes_real_payload() {
        let payload = r#"{
            "code": 0,
            "msg": "",
            "data": {
                "total": 2,
                "tasks": [
                    {
                        "id": "x1",
                        "name": "archive.zip",
                        "status": 4,
                        "type": 2,
                        "create_date": "2025-06-09T10:00:00+08:00",
                        "progress": 100,
                        "error": ""
                    },
                    {
                        "id": "x2",
                        "name": "import.csv",
                        "status": 2,
                        "type": 4,
                        "create_date": "2025-06-09T11:00:00+08:00",
                        "progress": 33,
                        "error": "{\"msg\":\"failed\",\"error\":\"timeout\"}"
                    }
                ]
            }
        }"#;
        let resp: ApiResponse<UserTaskList> =
            serde_json::from_str(payload).expect("user tasks payload should deserialize");
        let list = resp.data.expect("data");
        assert_eq!(list.total, 2);
        assert_eq!(list.tasks.len(), 2);
        assert_eq!(list.tasks[0].id, "x1");
        assert_eq!(list.tasks[0].task_type, 2);
        assert_eq!(list.tasks[1].progress, 33);
        assert!(list.tasks[1].error.contains("timeout"));
    }
}