@@ -168,14 +168,15 @@ fn vector_store_add_file_response() -> serde_json::Value {
168168/// Test helper: wraps the new streaming `upload_file` with the old simple interface.
169169///
170170/// Calls `get_upload_context`, validates MIME, converts bytes to stream, and
171- /// calls `upload_file` with the full parameter set .
172- async fn test_upload_file (
171+ /// calls `upload_file` with the given `size_hint` .
172+ async fn test_upload_file_inner (
173173 svc : & TestAttachmentService ,
174174 ctx : & modkit_security:: SecurityContext ,
175175 chat_id : Uuid ,
176176 filename : & str ,
177177 content_type : & str ,
178178 data : Bytes ,
179+ size_hint : Option < u64 > ,
179180) -> Result < crate :: infra:: db:: entity:: attachment:: Model , crate :: domain:: error:: DomainError > {
180181 use crate :: domain:: mime_validation:: {
181182 infer_mime_from_extension, normalize_mime, remap_csv_to_plain, validate_mime,
@@ -196,7 +197,6 @@ async fn test_upload_file(
196197 } ;
197198 let validated = validate_mime ( effective_ct) ?;
198199
199- let size = data. len ( ) as u64 ;
200200 let stream = bytes_to_stream ( data) ;
201201
202202 svc. upload_file (
@@ -207,11 +207,36 @@ async fn test_upload_file(
207207 validated. mime ,
208208 validated. kind ,
209209 stream,
210- Some ( size ) ,
210+ size_hint ,
211211 )
212212 . await
213213}
214214
215+ /// Upload with known `Content-Length`.
216+ async fn test_upload_file (
217+ svc : & TestAttachmentService ,
218+ ctx : & modkit_security:: SecurityContext ,
219+ chat_id : Uuid ,
220+ filename : & str ,
221+ content_type : & str ,
222+ data : Bytes ,
223+ ) -> Result < crate :: infra:: db:: entity:: attachment:: Model , crate :: domain:: error:: DomainError > {
224+ let size = data. len ( ) as u64 ;
225+ test_upload_file_inner ( svc, ctx, chat_id, filename, content_type, data, Some ( size) ) . await
226+ }
227+
228+ /// Upload without `Content-Length` (simulates chunked transfer encoding).
229+ async fn test_upload_file_chunked (
230+ svc : & TestAttachmentService ,
231+ ctx : & modkit_security:: SecurityContext ,
232+ chat_id : Uuid ,
233+ filename : & str ,
234+ content_type : & str ,
235+ data : Bytes ,
236+ ) -> Result < crate :: infra:: db:: entity:: attachment:: Model , crate :: domain:: error:: DomainError > {
237+ test_upload_file_inner ( svc, ctx, chat_id, filename, content_type, data, None ) . await
238+ }
239+
215240// ── P5-B1: Upload document full lifecycle ──
216241
217242#[ tokio:: test]
@@ -593,6 +618,62 @@ async fn test_upload_storage_limit_exceeded() {
593618 ) ;
594619}
595620
621+ // ── P5-C5: Post-upload storage limit for chunked uploads (no Content-Length) ──
622+
623+ #[ tokio:: test]
624+ async fn test_upload_storage_limit_exceeded_chunked ( ) {
625+ let db = inmem_db ( ) . await ;
626+ let tenant_id = Uuid :: new_v4 ( ) ;
627+ let chat_id = Uuid :: new_v4 ( ) ;
628+ let user_id = Uuid :: new_v4 ( ) ;
629+ let db_prov = mock_db_provider ( db. clone ( ) ) ;
630+ insert_chat_for_user ( & db_prov, tenant_id, chat_id, user_id) . await ;
631+
632+ let config = RagConfig {
633+ max_documents_per_chat : 50 ,
634+ max_total_upload_mb_per_chat : 1 , // 1 MB limit
635+ ..RagConfig :: default ( )
636+ } ;
637+
638+ // Insert a large existing attachment (close to limit)
639+ let mut params =
640+ crate :: domain:: service:: test_helpers:: InsertTestAttachmentParams :: ready_document (
641+ tenant_id, chat_id,
642+ ) ;
643+ params. uploaded_by_user_id = user_id;
644+ params. size_bytes = 900_000 ; // ~0.86 MB
645+ crate :: domain:: service:: test_helpers:: insert_test_attachment ( & db_prov, params) . await ;
646+
647+ let ctx = crate :: domain:: service:: test_helpers:: test_security_ctx_with_id ( tenant_id, user_id) ;
648+
649+ // OAGW returns file upload success (the provider accepts the file)
650+ let oagw = MockOagwGateway :: with_responses ( vec ! [ Ok ( file_upload_response( "file-chunked-001" ) ) ] ) ;
651+ let outbox = Arc :: new ( NoopOutboxEnqueuer ) ;
652+ let svc = build_service ( db, Arc :: clone ( & oagw) as _ , outbox, config) ;
653+
654+ // Upload 200KB via chunked encoding (no Content-Length → size_hint = None).
655+ // The preflight check is skipped, but the post-upload check should reject.
656+ let result = test_upload_file_chunked (
657+ & svc,
658+ & ctx,
659+ chat_id,
660+ "big.pdf" ,
661+ "application/pdf" ,
662+ Bytes :: from ( vec ! [ 0u8 ; 200_000 ] ) ,
663+ )
664+ . await ;
665+
666+ assert ! ( result. is_err( ) ) ;
667+ let err = result. unwrap_err ( ) ;
668+ assert ! (
669+ matches!(
670+ err,
671+ crate :: domain:: error:: DomainError :: StorageLimitExceeded { .. }
672+ ) ,
673+ "expected StorageLimitExceeded for chunked upload, got: {err:?}"
674+ ) ;
675+ }
676+
596677// ── P5-D1: Provider upload failure sets attachment to failed ──
597678
598679#[ tokio:: test]
0 commit comments