use serde_json::json; use shared_contracts::model3d::Model3dGenerationResult; use shared_contracts::model3d::common::{ Model3dGeneratedArtifact, Model3dGenerationSource, Model3dGenerationTarget, Model3dGenerationTargetRef, }; use shared_contracts::model3d::image_to_model::Model3dImageToModelRequest; use shared_contracts::model3d::text_to_model::Model3dTextToModelRequest; #[test] fn text_to_model_request_accepts_generation_and_target() { let request: Model3dTextToModelRequest = serde_json::from_value(json!({ "generation": { "prompt": "一把木椅", "model": "v3.1-20260211", "texture": true }, "target": { "kind": "assetLibrary", "folderId": "folder-1", "label": "木椅" } })) .expect("合法请求应可反序列化"); assert_eq!(request.generation.prompt, "一把木椅"); assert!(matches!( request.target, Model3dGenerationTarget::AssetLibrary { .. } )); } #[test] fn target_requires_branch_local_fields() { for invalid in [ json!({ "kind": "assetLibrary", "label": "木椅" }), json!({ "kind": "assetLibrary", "folderId": "folder-1" }), json!({ "kind": "projectResource" }), ] { let error = serde_json::from_value::(invalid.clone()) .expect_err("缺少分支必填字段应被拒绝"); assert!( error.to_string().contains("missing field"), "缺少字段的报错应指出 missing field,输入 {invalid},实际为:{error}" ); } } #[test] fn image_source_rejects_ambiguous_and_raw_input() { let both_ids = json!({ "source": { "kind": "resource", "resourceId": "r1", "assetId": "a1" }, "generation": { "model": "v3.1-20260211" }, "target": { "kind": "assetLibrary", "folderId": "folder-1", "label": "木椅" } }); let error = serde_json::from_value::(both_ids) .expect_err("同时给出资源与素材 ID 应被拒绝"); assert!( error.to_string().contains("unknown field"), "同时给出两个 ID 应报 unknown field,实际为:{error}" ); let raw_input = json!({ "source": "https://example.com/a.png", "generation": { "model": "v3.1-20260211" }, "target": { "kind": "assetLibrary", "folderId": "folder-1", "label": "木椅" } }); let error = serde_json::from_value::(raw_input) .expect_err("裸字符串图片输入应被拒绝"); // 拒绝必须发生在 source 本身:错误里点名 Model3dGenerationSource,避免将来 // generation / target 的必填变化把这条负例“顶替”成通过。 assert!( error.to_string().contains("Model3dGenerationSource"), "裸字符串 source 的报错应点名 Model3dGenerationSource,实际为:{error}" ); // 同样把 source 单独反序列化一遍,锁死被拒的是图片输入的形状本身。 serde_json::from_value::(json!("https://example.com/a.png")) .expect_err("裸字符串 source 值应被拒绝"); } #[test] fn api_request_rejects_unknown_top_level_fields() { let error = serde_json::from_value::(json!({ "generation": { "prompt": "一把木椅", "model": "v3.1-20260211" }, "target": { "kind": "assetLibrary", "folderId": "folder-1", "label": "木椅" }, "unexpected": true })) .expect_err("顶层未知字段应被拒绝"); assert!( error.to_string().contains("unknown field"), "顶层未知字段应报 unknown field,实际为:{error}" ); } #[test] fn generation_result_is_tagged_per_endpoint() { let result: Model3dGenerationResult = serde_json::from_value(json!({ "kind": "textToModel", "target": { "kind": "projectResource", "resourceId": "res-1" }, "model": { "objectKey": "models/res-1.glb", "contentType": "model/gltf-binary", "contentLength": 41_636_076_u64, "sha256": "0f1e" }, "preview": { "objectKey": "models/res-1.webp", "contentType": "image/webp", "contentLength": 20_480_u64, "sha256": "2a3b" } })) .expect("按端点的结果应可反序列化"); let Model3dGenerationResult::TextToModel(text) = result else { panic!("kind=textToModel 不应解成 imageToModel 分支"); }; assert_eq!( text.target, Model3dGenerationTargetRef::ProjectResource { resource_id: "res-1".into() } ); assert_eq!( text.model, Model3dGeneratedArtifact { object_key: "models/res-1.glb".into(), content_type: "model/gltf-binary".into(), content_length: 41_636_076, sha256: "0f1e".into(), } ); assert_eq!( text.preview, Model3dGeneratedArtifact { object_key: "models/res-1.webp".into(), content_type: "image/webp".into(), content_length: 20_480, sha256: "2a3b".into(), } ); // 对称覆盖 imageToModel 分支:tag 路由与分支字段都要单独守住。 let image_result: Model3dGenerationResult = serde_json::from_value(json!({ "kind": "imageToModel", "target": { "kind": "projectResource", "resourceId": "res-2" }, "model": { "objectKey": "models/res-2.glb", "contentType": "model/gltf-binary", "contentLength": 41_636_077_u64, "sha256": "3c4d" }, "preview": { "objectKey": "models/res-2.webp", "contentType": "image/webp", "contentLength": 20_481_u64, "sha256": "4e5f" } })) .expect("按端点的结果应可反序列化"); let Model3dGenerationResult::ImageToModel(image) = image_result else { panic!("kind=imageToModel 不应解成 textToModel 分支"); }; assert_eq!( image.target, Model3dGenerationTargetRef::ProjectResource { resource_id: "res-2".into() } ); assert_eq!( image.model, Model3dGeneratedArtifact { object_key: "models/res-2.glb".into(), content_type: "model/gltf-binary".into(), content_length: 41_636_077, sha256: "3c4d".into(), } ); assert_eq!( image.preview, Model3dGeneratedArtifact { object_key: "models/res-2.webp".into(), content_type: "image/webp".into(), content_length: 20_481, sha256: "4e5f".into(), } ); }