Line data Source code
1 : /**
2 : * @file test_upload_download.c
3 : * @brief FT-06 — upload / download functional tests.
4 : *
5 : * Drives the full end-to-end flow of:
6 : * - upload.saveFilePart (small), upload.saveBigFilePart (>= 10 MiB),
7 : * followed by messages.sendMedia (InputMediaUploadedDocument /
8 : * InputMediaUploadedPhoto).
9 : * - upload.getFile chunked download with EOF detection on short chunk.
10 : * - FILE_MIGRATE_X surface via the `wrong_dc` out-parameter.
11 : *
12 : * Real AES-IGE + SHA-256 on both sides. The files live in /tmp and are
13 : * recreated per-test so concurrent runs don't collide.
14 : */
15 :
16 : #include "test_helpers.h"
17 :
18 : #include "mock_socket.h"
19 : #include "mock_tel_server.h"
20 :
21 : #include "api_call.h"
22 : #include "mtproto_session.h"
23 : #include "transport.h"
24 : #include "app/session_store.h"
25 : #include "tl_registry.h"
26 : #include "tl_serial.h"
27 :
28 : #include "domain/write/upload.h"
29 : #include "domain/read/media.h"
30 :
31 : #include <stdio.h>
32 : #include <stdlib.h>
33 : #include <string.h>
34 : #include <sys/stat.h>
35 : #include <unistd.h>
36 :
37 : #define CRC_upload_saveFilePart 0xb304a621U
38 : #define CRC_upload_saveBigFilePart 0xde7b673dU
39 : #define CRC_messages_sendMedia 0x7547c966U
40 : #define CRC_upload_getFile 0xbe5335beU
41 : #define CRC_upload_file 0x096a18d5U
42 : #define CRC_storage_filePartial 0x40bc6f52U /* storage.filePartial */
43 : #define CRC_storage_fileJpeg 0x7efe0e /* storage.fileJpeg */
44 :
45 42 : static void with_tmp_home(const char *tag) {
46 : char tmp[256];
47 42 : snprintf(tmp, sizeof(tmp), "/tmp/tg-cli-ft-media-%s", tag);
48 : char bin[512];
49 42 : snprintf(bin, sizeof(bin), "%s/.config/tg-cli/session.bin", tmp);
50 42 : (void)unlink(bin);
51 42 : setenv("HOME", tmp, 1);
52 42 : }
53 :
54 42 : static void connect_mock(Transport *t) {
55 42 : transport_init(t);
56 42 : ASSERT(transport_connect(t, "127.0.0.1", 443) == 0, "connect");
57 : }
58 :
59 42 : static void init_cfg(ApiConfig *cfg) {
60 42 : api_config_init(cfg);
61 42 : cfg->api_id = 12345;
62 42 : cfg->api_hash = "deadbeefcafebabef00dbaadfeedc0de";
63 42 : }
64 :
65 42 : static void load_session(MtProtoSession *s) {
66 42 : ASSERT(mt_server_seed_session(2, NULL, NULL, NULL) == 0, "seed");
67 42 : mtproto_session_init(s);
68 42 : int dc = 0;
69 42 : ASSERT(session_store_load(s, &dc) == 0, "load");
70 : }
71 :
72 : /* Write a tempfile of @p size bytes filled with a deterministic byte
73 : * pattern and return its path (owned by a static buffer, caller must
74 : * unlink when done). */
75 18 : static const char *make_tempfile(const char *name, size_t size) {
76 : static char path[256];
77 18 : snprintf(path, sizeof(path), "/tmp/tg-cli-fixture-%s.bin", name);
78 18 : FILE *fp = fopen(path, "wb");
79 18 : if (!fp) return NULL;
80 18 : uint8_t *buf = (uint8_t *)malloc(size);
81 18 : if (!buf) { fclose(fp); return NULL; }
82 22038546 : for (size_t i = 0; i < size; ++i) buf[i] = (uint8_t)(i & 0xFFu);
83 18 : size_t written = fwrite(buf, 1, size, fp);
84 18 : free(buf);
85 18 : fclose(fp);
86 18 : return (written == size) ? path : NULL;
87 : }
88 :
89 : /* ================================================================ */
90 : /* Counters & state used by responders */
91 : /* ================================================================ */
92 :
93 : static int g_save_file_part_calls = 0;
94 : static int g_save_big_file_part_calls = 0;
95 : static int g_get_file_calls = 0;
96 : static int g_send_media_calls = 0;
97 :
98 42 : static void reset_counters(void) {
99 42 : g_save_file_part_calls = 0;
100 42 : g_save_big_file_part_calls = 0;
101 42 : g_get_file_calls = 0;
102 42 : g_send_media_calls = 0;
103 42 : }
104 :
105 : /* ================================================================ */
106 : /* Responders */
107 : /* ================================================================ */
108 :
109 60 : static void reply_bool_true(MtRpcContext *ctx) {
110 : TlWriter w;
111 60 : tl_writer_init(&w);
112 60 : tl_write_uint32(&w, TL_boolTrue);
113 60 : mt_server_reply_result(ctx, w.data, w.len);
114 60 : tl_writer_free(&w);
115 60 : }
116 :
117 14 : static void on_save_file_part(MtRpcContext *ctx) {
118 14 : g_save_file_part_calls++;
119 14 : reply_bool_true(ctx);
120 14 : }
121 :
122 42 : static void on_save_big_file_part(MtRpcContext *ctx) {
123 42 : g_save_big_file_part_calls++;
124 42 : reply_bool_true(ctx);
125 42 : }
126 :
127 : /* messages.sendMedia → minimal updates envelope. */
128 14 : static void on_send_media(MtRpcContext *ctx) {
129 14 : g_send_media_calls++;
130 : TlWriter w;
131 14 : tl_writer_init(&w);
132 14 : tl_write_uint32(&w, TL_updates);
133 14 : tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 0); /* updates */
134 14 : tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 0); /* users */
135 14 : tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 0); /* chats */
136 14 : tl_write_int32 (&w, 0); tl_write_int32 (&w, 0);
137 14 : mt_server_reply_result(ctx, w.data, w.len);
138 14 : tl_writer_free(&w);
139 14 : }
140 :
141 : /* upload.file with a caller-supplied payload length. */
142 16 : static void reply_upload_file(MtRpcContext *ctx,
143 : const uint8_t *bytes, size_t len) {
144 : TlWriter w;
145 16 : tl_writer_init(&w);
146 16 : tl_write_uint32(&w, CRC_upload_file);
147 : /* storage.FileType — pick storage.filePartial so no extension lookup */
148 16 : tl_write_uint32(&w, CRC_storage_filePartial);
149 16 : tl_write_int32 (&w, 0); /* mtime */
150 16 : tl_write_bytes (&w, bytes, len);
151 16 : mt_server_reply_result(ctx, w.data, w.len);
152 16 : tl_writer_free(&w);
153 16 : }
154 :
155 : /* Single-shot download: always returns a short chunk (EOF immediately). */
156 8 : static void on_get_file_short(MtRpcContext *ctx) {
157 8 : g_get_file_calls++;
158 : uint8_t payload[128];
159 1032 : for (size_t i = 0; i < sizeof(payload); ++i)
160 1024 : payload[i] = (uint8_t)(i ^ 0xA5u);
161 8 : reply_upload_file(ctx, payload, sizeof(payload));
162 8 : }
163 :
164 : /* Two-chunk download: first call returns exactly CHUNK_SIZE (128 KiB)
165 : * of data (signals "more to come"), second call returns a short chunk
166 : * (signals EOF). */
167 8 : static void on_get_file_two_chunks(MtRpcContext *ctx) {
168 8 : g_get_file_calls++;
169 8 : if (g_get_file_calls == 1) {
170 : /* Full chunk — 128 KiB of deterministic data. */
171 4 : uint8_t *full = (uint8_t *)malloc(128 * 1024);
172 4 : if (!full) return;
173 524292 : for (size_t i = 0; i < 128 * 1024; ++i) full[i] = (uint8_t)(i & 0xFFu);
174 4 : reply_upload_file(ctx, full, 128 * 1024);
175 4 : free(full);
176 : } else {
177 : uint8_t tail[64];
178 4 : memset(tail, 0x5A, sizeof(tail));
179 4 : reply_upload_file(ctx, tail, sizeof(tail));
180 : }
181 : }
182 :
183 : /* upload.getFile always returns FILE_MIGRATE_3. */
184 4 : static void on_get_file_migrate(MtRpcContext *ctx) {
185 4 : g_get_file_calls++;
186 4 : mt_server_reply_error(ctx, 303, "FILE_MIGRATE_3");
187 4 : }
188 :
189 : /* ================================================================ */
190 : /* Tests */
191 : /* ================================================================ */
192 :
193 2 : static void test_upload_small_document(void) {
194 2 : with_tmp_home("up-small");
195 2 : mt_server_init(); mt_server_reset();
196 2 : reset_counters();
197 2 : MtProtoSession s; load_session(&s);
198 2 : mt_server_expect(CRC_upload_saveFilePart, on_save_file_part, NULL);
199 2 : mt_server_expect(CRC_messages_sendMedia, on_send_media, NULL);
200 :
201 : /* 1 KiB file — one saveFilePart call. */
202 2 : const char *path = make_tempfile("up-small", 1024);
203 2 : ASSERT(path != NULL, "tempfile created");
204 :
205 2 : ApiConfig cfg; init_cfg(&cfg);
206 2 : Transport t; connect_mock(&t);
207 :
208 2 : HistoryPeer self = { .kind = HISTORY_PEER_SELF };
209 2 : RpcError err = {0};
210 2 : ASSERT(domain_send_file(&cfg, &s, &t, &self, path,
211 : "a tiny file", "text/plain", &err) == 0,
212 : "domain_send_file ok");
213 2 : ASSERT(g_save_file_part_calls == 1, "exactly one saveFilePart call");
214 :
215 2 : unlink(path);
216 2 : transport_close(&t);
217 2 : mt_server_reset();
218 : }
219 :
220 2 : static void test_upload_multi_chunk_document(void) {
221 2 : with_tmp_home("up-multi");
222 2 : mt_server_init(); mt_server_reset();
223 2 : reset_counters();
224 2 : MtProtoSession s; load_session(&s);
225 2 : mt_server_expect(CRC_upload_saveFilePart, on_save_file_part, NULL);
226 2 : mt_server_expect(CRC_messages_sendMedia, on_send_media, NULL);
227 :
228 : /* 513 KiB → 2 saveFilePart calls (chunk = 512 KiB). */
229 2 : const char *path = make_tempfile("up-multi", 513 * 1024);
230 2 : ASSERT(path != NULL, "tempfile created");
231 :
232 2 : ApiConfig cfg; init_cfg(&cfg);
233 2 : Transport t; connect_mock(&t);
234 :
235 2 : HistoryPeer self = { .kind = HISTORY_PEER_SELF };
236 2 : RpcError err = {0};
237 2 : ASSERT(domain_send_file(&cfg, &s, &t, &self, path,
238 : NULL, NULL, &err) == 0,
239 : "send_file 513 KiB ok");
240 2 : ASSERT(g_save_file_part_calls == 2, "two saveFilePart calls");
241 :
242 2 : unlink(path);
243 2 : transport_close(&t);
244 2 : mt_server_reset();
245 : }
246 :
247 2 : static void test_upload_big_file_uses_big_part(void) {
248 2 : with_tmp_home("up-big");
249 2 : mt_server_init(); mt_server_reset();
250 2 : reset_counters();
251 2 : MtProtoSession s; load_session(&s);
252 2 : mt_server_expect(CRC_upload_saveBigFilePart, on_save_big_file_part, NULL);
253 2 : mt_server_expect(CRC_messages_sendMedia, on_send_media, NULL);
254 :
255 : /* UPLOAD_BIG_THRESHOLD = 10 MiB — go just past it. */
256 2 : size_t sz = (size_t)UPLOAD_BIG_THRESHOLD + 1024;
257 2 : const char *path = make_tempfile("up-big", sz);
258 2 : ASSERT(path != NULL, "tempfile created");
259 :
260 2 : ApiConfig cfg; init_cfg(&cfg);
261 2 : Transport t; connect_mock(&t);
262 :
263 2 : HistoryPeer self = { .kind = HISTORY_PEER_SELF };
264 2 : RpcError err = {0};
265 2 : ASSERT(domain_send_file(&cfg, &s, &t, &self, path,
266 : NULL, NULL, &err) == 0,
267 : "send_file >=10 MiB ok");
268 : /* 10 MiB + 1 KiB ÷ 512 KiB chunk = 21 parts. */
269 2 : ASSERT(g_save_big_file_part_calls >= 21,
270 : ">= 21 saveBigFilePart calls");
271 2 : ASSERT(g_save_file_part_calls == 0, "no small saveFilePart for big file");
272 :
273 2 : unlink(path);
274 2 : transport_close(&t);
275 2 : mt_server_reset();
276 : }
277 :
278 2 : static void test_upload_photo_uses_saveFilePart(void) {
279 2 : with_tmp_home("up-photo");
280 2 : mt_server_init(); mt_server_reset();
281 2 : reset_counters();
282 2 : MtProtoSession s; load_session(&s);
283 2 : mt_server_expect(CRC_upload_saveFilePart, on_save_file_part, NULL);
284 2 : mt_server_expect(CRC_messages_sendMedia, on_send_media, NULL);
285 :
286 2 : const char *path = make_tempfile("up-photo", 2048);
287 2 : ASSERT(path != NULL, "tempfile created");
288 :
289 2 : ApiConfig cfg; init_cfg(&cfg);
290 2 : Transport t; connect_mock(&t);
291 :
292 2 : HistoryPeer self = { .kind = HISTORY_PEER_SELF };
293 2 : RpcError err = {0};
294 2 : ASSERT(domain_send_photo(&cfg, &s, &t, &self, path, "pic", &err) == 0,
295 : "send_photo ok");
296 2 : ASSERT(g_save_file_part_calls == 1, "photo uploaded as one small part");
297 :
298 2 : unlink(path);
299 2 : transport_close(&t);
300 2 : mt_server_reset();
301 : }
302 :
303 : /* Populate a MediaInfo struct with values the downloader expects. */
304 8 : static void make_media_info(MediaInfo *mi) {
305 8 : memset(mi, 0, sizeof(*mi));
306 8 : mi->kind = MEDIA_PHOTO;
307 8 : mi->photo_id = 0xDEADBEEFLL;
308 8 : mi->access_hash = 0xCAFEBABELL;
309 8 : mi->dc_id = 2;
310 8 : mi->file_reference_len = 4;
311 8 : mi->file_reference[0] = 0x11;
312 8 : mi->file_reference[1] = 0x22;
313 8 : mi->file_reference[2] = 0x33;
314 8 : mi->file_reference[3] = 0x44;
315 8 : strcpy(mi->thumb_type, "y");
316 8 : }
317 :
318 : /* Populate a MediaInfo struct for a document download. */
319 8 : static void make_doc_media_info(MediaInfo *mi) {
320 8 : memset(mi, 0, sizeof(*mi));
321 8 : mi->kind = MEDIA_DOCUMENT;
322 8 : mi->document_id = 0xFEEDC0FFEE1234LL;
323 8 : mi->access_hash = 0xABCDEF0123456789LL;
324 8 : mi->dc_id = 2;
325 8 : mi->file_reference_len = 4;
326 8 : mi->file_reference[0] = 0xAA;
327 8 : mi->file_reference[1] = 0xBB;
328 8 : mi->file_reference[2] = 0xCC;
329 8 : mi->file_reference[3] = 0xDD;
330 8 : strcpy(mi->document_filename, "hello.bin");
331 8 : }
332 :
333 2 : static void test_download_photo_short_chunk(void) {
334 2 : with_tmp_home("dl-short");
335 2 : mt_server_init(); mt_server_reset();
336 2 : reset_counters();
337 2 : MtProtoSession s; load_session(&s);
338 2 : mt_server_expect(CRC_upload_getFile, on_get_file_short, NULL);
339 :
340 2 : ApiConfig cfg; init_cfg(&cfg);
341 2 : Transport t; connect_mock(&t);
342 :
343 2 : MediaInfo mi; make_media_info(&mi);
344 2 : const char *out = "/tmp/tg-cli-ft-media-dl-short.bin";
345 2 : int wrong = -1;
346 2 : ASSERT(domain_download_photo(&cfg, &s, &t, &mi, out, &wrong) == 0,
347 : "download ok");
348 2 : ASSERT(wrong == 0, "no wrong_dc");
349 2 : ASSERT(g_get_file_calls == 1,
350 : "single call — EOF from short chunk on first try");
351 :
352 : struct stat st;
353 2 : ASSERT(stat(out, &st) == 0, "output exists");
354 2 : ASSERT(st.st_size == 128, "128 bytes written");
355 :
356 2 : unlink(out);
357 2 : transport_close(&t);
358 2 : mt_server_reset();
359 : }
360 :
361 2 : static void test_download_photo_two_chunks(void) {
362 2 : with_tmp_home("dl-two");
363 2 : mt_server_init(); mt_server_reset();
364 2 : reset_counters();
365 2 : MtProtoSession s; load_session(&s);
366 2 : mt_server_expect(CRC_upload_getFile, on_get_file_two_chunks, NULL);
367 :
368 2 : ApiConfig cfg; init_cfg(&cfg);
369 2 : Transport t; connect_mock(&t);
370 :
371 2 : MediaInfo mi; make_media_info(&mi);
372 2 : const char *out = "/tmp/tg-cli-ft-media-dl-two.bin";
373 2 : int wrong = -1;
374 2 : ASSERT(domain_download_photo(&cfg, &s, &t, &mi, out, &wrong) == 0,
375 : "download ok");
376 2 : ASSERT(wrong == 0, "no wrong_dc");
377 2 : ASSERT(g_get_file_calls == 2,
378 : "two calls: first full chunk, second EOF");
379 :
380 : struct stat st;
381 2 : ASSERT(stat(out, &st) == 0, "output exists");
382 2 : ASSERT(st.st_size == 128 * 1024 + 64, "128 KiB + 64 bytes written");
383 :
384 2 : unlink(out);
385 2 : transport_close(&t);
386 2 : mt_server_reset();
387 : }
388 :
389 2 : static void test_download_photo_file_migrate(void) {
390 2 : with_tmp_home("dl-mig");
391 2 : mt_server_init(); mt_server_reset();
392 2 : reset_counters();
393 2 : MtProtoSession s; load_session(&s);
394 2 : mt_server_expect(CRC_upload_getFile, on_get_file_migrate, NULL);
395 :
396 2 : ApiConfig cfg; init_cfg(&cfg);
397 2 : Transport t; connect_mock(&t);
398 :
399 2 : MediaInfo mi; make_media_info(&mi);
400 2 : const char *out = "/tmp/tg-cli-ft-media-dl-mig.bin";
401 2 : int wrong = -1;
402 2 : ASSERT(domain_download_photo(&cfg, &s, &t, &mi, out, &wrong) == -1,
403 : "download fails with migrate");
404 2 : ASSERT(wrong == 3, "wrong_dc surfaced as 3");
405 :
406 2 : unlink(out);
407 2 : transport_close(&t);
408 2 : mt_server_reset();
409 : }
410 :
411 : /* ================================================================ */
412 : /* Document download tests (TEST-07) */
413 : /* ================================================================ */
414 :
415 2 : static void test_download_document_single_chunk(void) {
416 2 : with_tmp_home("dl-doc-short");
417 2 : mt_server_init(); mt_server_reset();
418 2 : reset_counters();
419 2 : MtProtoSession s; load_session(&s);
420 2 : mt_server_expect(CRC_upload_getFile, on_get_file_short, NULL);
421 :
422 2 : ApiConfig cfg; init_cfg(&cfg);
423 2 : Transport t; connect_mock(&t);
424 :
425 2 : MediaInfo mi; make_doc_media_info(&mi);
426 : /* Use document_filename as the output filename path. */
427 2 : const char *out = "/tmp/tg-cli-ft-media-dl-doc-short.bin";
428 2 : int wrong = -1;
429 2 : ASSERT(domain_download_document(&cfg, &s, &t, &mi, out, &wrong) == 0,
430 : "document download ok");
431 2 : ASSERT(wrong == 0, "no wrong_dc");
432 2 : ASSERT(g_get_file_calls == 1, "single chunk → EOF");
433 :
434 : struct stat st;
435 2 : ASSERT(stat(out, &st) == 0, "output file exists");
436 2 : ASSERT(st.st_size == 128, "128 bytes written");
437 :
438 : /* Verify content: on_get_file_short uses (i ^ 0xA5). */
439 2 : FILE *fp = fopen(out, "rb");
440 2 : ASSERT(fp != NULL, "can open output");
441 : uint8_t buf[128];
442 2 : ASSERT(fread(buf, 1, 128, fp) == 128, "read 128 bytes");
443 2 : fclose(fp);
444 2 : int content_ok = 1;
445 258 : for (int i = 0; i < 128; ++i) {
446 256 : if (buf[i] != (uint8_t)(i ^ 0xA5u)) { content_ok = 0; break; }
447 : }
448 2 : ASSERT(content_ok, "document content matches deterministic pattern");
449 :
450 2 : unlink(out);
451 2 : transport_close(&t);
452 2 : mt_server_reset();
453 : }
454 :
455 2 : static void test_download_document_two_chunks(void) {
456 2 : with_tmp_home("dl-doc-two");
457 2 : mt_server_init(); mt_server_reset();
458 2 : reset_counters();
459 2 : MtProtoSession s; load_session(&s);
460 2 : mt_server_expect(CRC_upload_getFile, on_get_file_two_chunks, NULL);
461 :
462 2 : ApiConfig cfg; init_cfg(&cfg);
463 2 : Transport t; connect_mock(&t);
464 :
465 2 : MediaInfo mi; make_doc_media_info(&mi);
466 2 : const char *out = "/tmp/tg-cli-ft-media-dl-doc-two.bin";
467 2 : int wrong = -1;
468 2 : ASSERT(domain_download_document(&cfg, &s, &t, &mi, out, &wrong) == 0,
469 : "document two-chunk download ok");
470 2 : ASSERT(wrong == 0, "no wrong_dc");
471 2 : ASSERT(g_get_file_calls == 2, "two calls: first full chunk, second EOF");
472 :
473 : struct stat st;
474 2 : ASSERT(stat(out, &st) == 0, "output file exists");
475 2 : ASSERT(st.st_size == 128 * 1024 + 64, "128 KiB + 64 bytes written");
476 :
477 2 : unlink(out);
478 2 : transport_close(&t);
479 2 : mt_server_reset();
480 : }
481 :
482 2 : static void test_download_document_file_migrate(void) {
483 2 : with_tmp_home("dl-doc-mig");
484 2 : mt_server_init(); mt_server_reset();
485 2 : reset_counters();
486 2 : MtProtoSession s; load_session(&s);
487 2 : mt_server_expect(CRC_upload_getFile, on_get_file_migrate, NULL);
488 :
489 2 : ApiConfig cfg; init_cfg(&cfg);
490 2 : Transport t; connect_mock(&t);
491 :
492 2 : MediaInfo mi; make_doc_media_info(&mi);
493 2 : const char *out = "/tmp/tg-cli-ft-media-dl-doc-mig.bin";
494 2 : int wrong = -1;
495 2 : ASSERT(domain_download_document(&cfg, &s, &t, &mi, out, &wrong) == -1,
496 : "document download fails with FILE_MIGRATE");
497 2 : ASSERT(wrong == 3, "wrong_dc surfaced as 3");
498 :
499 2 : unlink(out);
500 2 : transport_close(&t);
501 2 : mt_server_reset();
502 : }
503 :
504 2 : static void test_path_is_image(void) {
505 : /* Pure helper — no server — but lives here for coupling with the
506 : * upload module. */
507 2 : ASSERT(domain_path_is_image("foo.jpg"), "jpg → image");
508 2 : ASSERT(domain_path_is_image("foo.PNG"), "png → image (case)");
509 2 : ASSERT(domain_path_is_image("a/b.webp"), "webp → image");
510 2 : ASSERT(!domain_path_is_image("x.txt"), "txt → not image");
511 2 : ASSERT(!domain_path_is_image("x"), "no dot → not image");
512 2 : ASSERT(!domain_path_is_image(NULL), "NULL → not image");
513 : }
514 :
515 : /* ================================================================ */
516 : /* Caption propagation tests (TEST-16) */
517 : /* ================================================================ */
518 :
519 : /* Stores a copy of the sendMedia body for inspection. */
520 : static uint8_t g_send_media_body[4096];
521 : static size_t g_send_media_body_len = 0;
522 :
523 4 : static void on_send_media_capture(MtRpcContext *ctx) {
524 : /* Save a copy of the full request body so the test can inspect it. */
525 4 : size_t cap = ctx->req_body_len;
526 4 : if (cap > sizeof(g_send_media_body))
527 0 : cap = sizeof(g_send_media_body);
528 4 : memcpy(g_send_media_body, ctx->req_body, cap);
529 4 : g_send_media_body_len = cap;
530 :
531 : /* Reply with a minimal updates envelope. */
532 : TlWriter w;
533 4 : tl_writer_init(&w);
534 4 : tl_write_uint32(&w, TL_updates);
535 4 : tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 0);
536 4 : tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 0);
537 4 : tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 0);
538 4 : tl_write_int32 (&w, 0); tl_write_int32 (&w, 0);
539 4 : mt_server_reply_result(ctx, w.data, w.len);
540 4 : tl_writer_free(&w);
541 4 : }
542 :
543 : /* Return 1 if @p needle (len @p nlen) appears anywhere inside
544 : * [body, body+blen). Simple byte-scan, no dependency on <string.h>
545 : * memmem (which is a GNU extension). */
546 4 : static int body_contains(const uint8_t *body, size_t blen,
547 : const char *needle, size_t nlen) {
548 4 : if (nlen == 0 || blen < nlen) return 0;
549 502 : for (size_t i = 0; i <= blen - nlen; ++i) {
550 500 : if (memcmp(body + i, needle, nlen) == 0) return 1;
551 : }
552 2 : return 0;
553 : }
554 :
555 : /**
556 : * domain_send_file with caption "final version" → the exact byte sequence
557 : * "final version" must appear inside the messages.sendMedia wire body.
558 : */
559 2 : static void test_send_file_caption_propagates(void) {
560 2 : with_tmp_home("cap-set");
561 2 : mt_server_init(); mt_server_reset();
562 2 : reset_counters();
563 2 : g_send_media_body_len = 0;
564 :
565 2 : MtProtoSession s; load_session(&s);
566 2 : mt_server_expect(CRC_upload_saveFilePart, on_save_file_part, NULL);
567 2 : mt_server_expect(CRC_messages_sendMedia, on_send_media_capture, NULL);
568 :
569 2 : const char *path = make_tempfile("cap-set", 512);
570 2 : ASSERT(path != NULL, "tempfile created");
571 :
572 2 : ApiConfig cfg; init_cfg(&cfg);
573 2 : Transport t; connect_mock(&t);
574 :
575 2 : HistoryPeer self = { .kind = HISTORY_PEER_SELF };
576 2 : RpcError err = {0};
577 2 : ASSERT(domain_send_file(&cfg, &s, &t, &self, path,
578 : "final version", "text/plain", &err) == 0,
579 : "domain_send_file with caption ok");
580 :
581 2 : const char *cap = "final version";
582 2 : ASSERT(body_contains(g_send_media_body, g_send_media_body_len,
583 : cap, strlen(cap)),
584 : "caption 'final version' found in sendMedia wire bytes");
585 :
586 2 : unlink(path);
587 2 : transport_close(&t);
588 2 : mt_server_reset();
589 : }
590 :
591 : /**
592 : * domain_send_file with no caption (NULL) → empty TL string on the wire.
593 : * An empty TL string is encoded as a single 0x00 byte (length=0, no padding
594 : * needed since 1+0=1, padded to 4 → 3 zero pad bytes). We check that the
595 : * literal string "final version" does NOT appear in the request.
596 : */
597 2 : static void test_send_file_no_caption_empty_string(void) {
598 2 : with_tmp_home("cap-empty");
599 2 : mt_server_init(); mt_server_reset();
600 2 : reset_counters();
601 2 : g_send_media_body_len = 0;
602 :
603 2 : MtProtoSession s; load_session(&s);
604 2 : mt_server_expect(CRC_upload_saveFilePart, on_save_file_part, NULL);
605 2 : mt_server_expect(CRC_messages_sendMedia, on_send_media_capture, NULL);
606 :
607 2 : const char *path = make_tempfile("cap-empty", 512);
608 2 : ASSERT(path != NULL, "tempfile created");
609 :
610 2 : ApiConfig cfg; init_cfg(&cfg);
611 2 : Transport t; connect_mock(&t);
612 :
613 2 : HistoryPeer self = { .kind = HISTORY_PEER_SELF };
614 2 : RpcError err = {0};
615 2 : ASSERT(domain_send_file(&cfg, &s, &t, &self, path,
616 : NULL, "text/plain", &err) == 0,
617 : "domain_send_file without caption ok");
618 :
619 : /* The request body must NOT contain a non-empty caption string. */
620 2 : const char *cap = "final version";
621 2 : ASSERT(!body_contains(g_send_media_body, g_send_media_body_len,
622 : cap, strlen(cap)),
623 : "no spurious caption in sendMedia wire bytes when caption is NULL");
624 :
625 2 : unlink(path);
626 2 : transport_close(&t);
627 2 : mt_server_reset();
628 : }
629 :
630 : /* ================================================================ */
631 : /* Cache-reuse tests (TEST-08) */
632 : /* ================================================================ */
633 :
634 : /**
635 : * Download a photo twice with the same media_id and output path.
636 : * The second call must not issue any upload.getFile RPC — it returns
637 : * the cached file instead.
638 : */
639 2 : static void test_download_photo_cache_reuse(void) {
640 2 : with_tmp_home("dl-cache-photo");
641 2 : mt_server_init(); mt_server_reset();
642 2 : reset_counters();
643 2 : MtProtoSession s; load_session(&s);
644 2 : mt_server_expect(CRC_upload_getFile, on_get_file_short, NULL);
645 :
646 2 : ApiConfig cfg; init_cfg(&cfg);
647 2 : Transport t; connect_mock(&t);
648 :
649 2 : MediaInfo mi; make_media_info(&mi);
650 2 : const char *out = "/tmp/tg-cli-ft-media-dl-cache-photo.bin";
651 2 : unlink(out);
652 :
653 : /* First download — must hit the server. */
654 2 : int wrong = -1;
655 2 : ASSERT(domain_download_photo(&cfg, &s, &t, &mi, out, &wrong) == 0,
656 : "first download ok");
657 2 : ASSERT(wrong == 0, "no wrong_dc on first call");
658 2 : ASSERT(g_get_file_calls == 1, "first call fires one upload.getFile RPC");
659 :
660 : /* Second download with identical media_id and out_path — cache hit. */
661 2 : int calls_before = g_get_file_calls;
662 2 : wrong = -1;
663 2 : ASSERT(domain_download_photo(&cfg, &s, &t, &mi, out, &wrong) == 0,
664 : "second download ok (from cache)");
665 2 : ASSERT(g_get_file_calls == calls_before,
666 : "second call does NOT issue upload.getFile (cache hit)");
667 :
668 : /* File content must be identical to what the first download wrote. */
669 : struct stat st;
670 2 : ASSERT(stat(out, &st) == 0, "output file still exists");
671 2 : ASSERT(st.st_size == 128, "cached file size unchanged (128 bytes)");
672 :
673 2 : unlink(out);
674 2 : transport_close(&t);
675 2 : mt_server_reset();
676 : }
677 :
678 : /**
679 : * Download a document twice with the same document_id.
680 : * The second call must return the cached file without any RPC.
681 : */
682 2 : static void test_download_document_cache_reuse(void) {
683 2 : with_tmp_home("dl-cache-doc");
684 2 : mt_server_init(); mt_server_reset();
685 2 : reset_counters();
686 2 : MtProtoSession s; load_session(&s);
687 2 : mt_server_expect(CRC_upload_getFile, on_get_file_short, NULL);
688 :
689 2 : ApiConfig cfg; init_cfg(&cfg);
690 2 : Transport t; connect_mock(&t);
691 :
692 2 : MediaInfo mi; make_doc_media_info(&mi);
693 2 : const char *out = "/tmp/tg-cli-ft-media-dl-cache-doc.bin";
694 2 : unlink(out);
695 :
696 : /* First download — server must be called. */
697 2 : int wrong = -1;
698 2 : ASSERT(domain_download_document(&cfg, &s, &t, &mi, out, &wrong) == 0,
699 : "first document download ok");
700 2 : ASSERT(wrong == 0, "no wrong_dc on first call");
701 2 : ASSERT(g_get_file_calls == 1, "first call fires one upload.getFile RPC");
702 :
703 : /* Second download — cache hit, no new RPC. */
704 2 : int calls_before = g_get_file_calls;
705 2 : wrong = -1;
706 2 : ASSERT(domain_download_document(&cfg, &s, &t, &mi, out, &wrong) == 0,
707 : "second document download ok (from cache)");
708 2 : ASSERT(g_get_file_calls == calls_before,
709 : "second document call does NOT issue upload.getFile (cache hit)");
710 :
711 : struct stat st;
712 2 : ASSERT(stat(out, &st) == 0, "cached document file still exists");
713 2 : ASSERT(st.st_size == 128, "cached document size unchanged (128 bytes)");
714 :
715 2 : unlink(out);
716 2 : transport_close(&t);
717 2 : mt_server_reset();
718 : }
719 :
720 : /* ================================================================ */
721 : /* Invalid-path tests (TEST-17) */
722 : /* ================================================================ */
723 :
724 : /**
725 : * NULL path: domain_send_file must return -1 immediately without
726 : * touching the wire.
727 : */
728 2 : static void test_upload_null_path(void) {
729 2 : with_tmp_home("up-null");
730 2 : mt_server_init(); mt_server_reset();
731 2 : reset_counters();
732 2 : MtProtoSession s; load_session(&s);
733 : /* No responders registered — any RPC would cause the mock to fail. */
734 :
735 2 : ApiConfig cfg; init_cfg(&cfg);
736 2 : Transport t; connect_mock(&t);
737 :
738 2 : HistoryPeer self = { .kind = HISTORY_PEER_SELF };
739 2 : RpcError err = {0};
740 2 : ASSERT(domain_send_file(&cfg, &s, &t, &self, NULL,
741 : NULL, NULL, &err) == -1,
742 : "NULL path returns -1");
743 2 : ASSERT(mt_server_rpc_call_count() == 0,
744 : "NULL path: no RPC fired");
745 :
746 2 : transport_close(&t);
747 2 : mt_server_reset();
748 : }
749 :
750 : /**
751 : * Non-existent path: stat() fails with ENOENT so upload_chunk_phase
752 : * returns -1 before any RPC.
753 : */
754 2 : static void test_upload_nonexistent_path(void) {
755 2 : with_tmp_home("up-noent");
756 2 : mt_server_init(); mt_server_reset();
757 2 : reset_counters();
758 2 : MtProtoSession s; load_session(&s);
759 : /* No responders registered. */
760 :
761 2 : ApiConfig cfg; init_cfg(&cfg);
762 2 : Transport t; connect_mock(&t);
763 :
764 2 : HistoryPeer self = { .kind = HISTORY_PEER_SELF };
765 2 : RpcError err = {0};
766 2 : const char *missing = "/tmp/tg-cli-this-file-does-not-exist-TEST17.bin";
767 2 : unlink(missing); /* ensure it really is absent */
768 2 : ASSERT(domain_send_file(&cfg, &s, &t, &self, missing,
769 : NULL, NULL, &err) == -1,
770 : "non-existent path returns -1");
771 2 : ASSERT(mt_server_rpc_call_count() == 0,
772 : "non-existent path: no RPC fired");
773 :
774 2 : transport_close(&t);
775 2 : mt_server_reset();
776 : }
777 :
778 : /**
779 : * File exceeding UPLOAD_MAX_SIZE (1.5 GiB): upload_chunk_phase rejects
780 : * the file before any RPC is issued. We use a sparse file created with
781 : * truncate(2) — it reports a size of 2 GiB but consumes no disk space.
782 : */
783 2 : static void test_upload_over_max_size_rejected(void) {
784 2 : with_tmp_home("up-overmax");
785 2 : mt_server_init(); mt_server_reset();
786 2 : reset_counters();
787 2 : MtProtoSession s; load_session(&s);
788 : /* No responders — any RPC would cause the mock to fail. */
789 :
790 : /* Create a sparse file whose reported size is 2 GiB (0x80000000 bytes),
791 : * comfortably above UPLOAD_MAX_SIZE = 1.5 GiB. truncate(2) extends the
792 : * file without writing actual blocks, so this costs ~0 disk space. */
793 2 : const char *big_path = "/tmp/tg-cli-fixture-overmax-TEST18.bin";
794 : {
795 2 : FILE *fp = fopen(big_path, "wb");
796 2 : ASSERT(fp != NULL, "create sparse file placeholder");
797 2 : fclose(fp);
798 : }
799 : /* 2 GiB = 2 * 1024 * 1024 * 1024 */
800 2 : off_t two_gib = (off_t)2 * 1024 * 1024 * 1024;
801 2 : ASSERT(truncate(big_path, two_gib) == 0, "truncate to 2 GiB (sparse)");
802 :
803 2 : ApiConfig cfg; init_cfg(&cfg);
804 2 : Transport t; connect_mock(&t);
805 :
806 2 : HistoryPeer self = { .kind = HISTORY_PEER_SELF };
807 2 : RpcError err = {0};
808 2 : ASSERT(domain_send_file(&cfg, &s, &t, &self, big_path,
809 : NULL, NULL, &err) == -1,
810 : "2 GiB file returns -1 (over UPLOAD_MAX_SIZE)");
811 2 : ASSERT(mt_server_rpc_call_count() == 0,
812 : "over-max-size: no RPC fired");
813 :
814 2 : unlink(big_path);
815 2 : transport_close(&t);
816 2 : mt_server_reset();
817 : }
818 :
819 : /**
820 : * File just under UPLOAD_MAX_SIZE proceeds normally. We use a small real
821 : * file (1 KiB) — the point is that the size-cap branch is NOT taken.
822 : * (The existing test_upload_small_document already covers this path, but
823 : * having it adjacent to the over-limit test makes the boundary explicit.)
824 : */
825 2 : static void test_upload_under_max_size_proceeds(void) {
826 2 : with_tmp_home("up-undermax");
827 2 : mt_server_init(); mt_server_reset();
828 2 : reset_counters();
829 2 : MtProtoSession s; load_session(&s);
830 2 : mt_server_expect(CRC_upload_saveFilePart, on_save_file_part, NULL);
831 2 : mt_server_expect(CRC_messages_sendMedia, on_send_media, NULL);
832 :
833 2 : const char *path = make_tempfile("up-undermax", 1024);
834 2 : ASSERT(path != NULL, "1 KiB tempfile created");
835 :
836 2 : ApiConfig cfg; init_cfg(&cfg);
837 2 : Transport t; connect_mock(&t);
838 :
839 2 : HistoryPeer self = { .kind = HISTORY_PEER_SELF };
840 2 : RpcError err = {0};
841 2 : ASSERT(domain_send_file(&cfg, &s, &t, &self, path,
842 : NULL, NULL, &err) == 0,
843 : "1 KiB file (under UPLOAD_MAX_SIZE) succeeds");
844 2 : ASSERT(mt_server_rpc_call_count() >= 1,
845 : "under-max-size: at least one RPC fired");
846 :
847 2 : unlink(path);
848 2 : transport_close(&t);
849 2 : mt_server_reset();
850 : }
851 :
852 : /**
853 : * Empty file (0 bytes): upload_chunk_phase rejects st_size == 0 before
854 : * any RPC is issued.
855 : */
856 2 : static void test_upload_empty_file(void) {
857 2 : with_tmp_home("up-empty");
858 2 : mt_server_init(); mt_server_reset();
859 2 : reset_counters();
860 2 : MtProtoSession s; load_session(&s);
861 : /* No responders registered. */
862 :
863 : /* Create a genuine 0-byte file. */
864 2 : const char *empty_path = "/tmp/tg-cli-fixture-empty-TEST17.bin";
865 2 : FILE *fp = fopen(empty_path, "wb");
866 2 : ASSERT(fp != NULL, "empty tempfile created");
867 2 : fclose(fp);
868 :
869 2 : ApiConfig cfg; init_cfg(&cfg);
870 2 : Transport t; connect_mock(&t);
871 :
872 2 : HistoryPeer self = { .kind = HISTORY_PEER_SELF };
873 2 : RpcError err = {0};
874 2 : ASSERT(domain_send_file(&cfg, &s, &t, &self, empty_path,
875 : NULL, NULL, &err) == -1,
876 : "empty file returns -1");
877 2 : ASSERT(mt_server_rpc_call_count() == 0,
878 : "empty file: no RPC fired");
879 :
880 2 : unlink(empty_path);
881 2 : transport_close(&t);
882 2 : mt_server_reset();
883 : }
884 :
885 : /* ================================================================ */
886 : /* NETWORK_MIGRATE upload retry test (TEST-19) */
887 : /* ================================================================ */
888 :
889 : /**
890 : * upload.saveFilePart responder for the NETWORK_MIGRATE test.
891 : *
892 : * Call 1 (home DC): arms the reconnect detector and returns
893 : * rpc_error(303, "NETWORK_MIGRATE_4") so the client switches to DC 4.
894 : * Call 2+ (DC 4 session, same mock server): returns boolTrue so the
895 : * retried upload succeeds.
896 : */
897 4 : static void on_save_file_part_migrate(MtRpcContext *ctx) {
898 4 : g_save_file_part_calls++;
899 4 : if (g_save_file_part_calls == 1) {
900 : /* Arm reconnect detection so the DC 4 transport's 0xEF marker is
901 : * parsed as a new connection rather than a frame length prefix. */
902 2 : mt_server_arm_reconnect();
903 2 : mt_server_reply_error(ctx, 303, "NETWORK_MIGRATE_4");
904 : } else {
905 2 : reply_bool_true(ctx);
906 : }
907 4 : }
908 :
909 : /**
910 : * FT-19 — NETWORK_MIGRATE_4 retry path for upload.saveFilePart.
911 : *
912 : * Scenario:
913 : * 1. Home DC (DC 2) returns NETWORK_MIGRATE_4 on the first saveFilePart.
914 : * 2. upload_chunk_phase opens DC 4 (pre-seeded session → fast path, no DH).
915 : * 3. Retried saveFilePart on DC 4 succeeds (boolTrue).
916 : * 4. messages.sendMedia fires on the home transport (DC 2).
917 : *
918 : * Verifications:
919 : * - domain_send_file returns 0 (full success).
920 : * - saveFilePart was called exactly twice: once home, once on DC 4.
921 : * - sendMedia was called exactly once (home DC).
922 : */
923 2 : static void test_upload_savefilepart_network_migrate(void) {
924 2 : with_tmp_home("up-mig");
925 2 : mt_server_init(); mt_server_reset();
926 2 : reset_counters();
927 :
928 2 : MtProtoSession s; load_session(&s); /* seeds DC 2 */
929 2 : ASSERT(mt_server_seed_extra_dc(4) == 0, "seed DC4 session");
930 :
931 2 : mt_server_expect(CRC_upload_saveFilePart, on_save_file_part_migrate, NULL);
932 2 : mt_server_expect(CRC_messages_sendMedia, on_send_media, NULL);
933 :
934 2 : const char *path = make_tempfile("up-mig", 1024);
935 2 : ASSERT(path != NULL, "tempfile created");
936 :
937 2 : ApiConfig cfg; init_cfg(&cfg);
938 2 : Transport t; connect_mock(&t);
939 :
940 2 : HistoryPeer self = { .kind = HISTORY_PEER_SELF };
941 2 : RpcError err = {0};
942 2 : ASSERT(domain_send_file(&cfg, &s, &t, &self, path,
943 : "migrate test", "text/plain", &err) == 0,
944 : "domain_send_file succeeds after NETWORK_MIGRATE_4 retry");
945 :
946 : /* saveFilePart fired once on home DC (→ NETWORK_MIGRATE_4) and
947 : * once on DC 4 (→ boolTrue). */
948 2 : ASSERT(g_save_file_part_calls == 2,
949 : "saveFilePart called twice: once home DC, once DC 4");
950 :
951 : /* sendMedia fires exactly once on the home transport. */
952 2 : ASSERT(g_send_media_calls == 1,
953 : "sendMedia fired once on home DC after cross-DC upload");
954 :
955 2 : unlink(path);
956 2 : transport_close(&t);
957 2 : mt_server_reset();
958 : }
959 :
960 : /* ================================================================ */
961 : /* FILE_MIGRATE upload retry test (TEST-20) */
962 : /* ================================================================ */
963 :
964 : /**
965 : * upload.saveFilePart responder for the FILE_MIGRATE test.
966 : *
967 : * Call 1 (home DC): arms the reconnect detector and returns
968 : * rpc_error(303, "FILE_MIGRATE_3") so the client switches to DC 3.
969 : * Call 2+ (DC 3 session, same mock server): returns boolTrue so the
970 : * retried upload succeeds.
971 : */
972 4 : static void on_save_file_part_file_migrate(MtRpcContext *ctx) {
973 4 : g_save_file_part_calls++;
974 4 : if (g_save_file_part_calls == 1) {
975 2 : mt_server_arm_reconnect();
976 2 : mt_server_reply_error(ctx, 303, "FILE_MIGRATE_3");
977 : } else {
978 2 : reply_bool_true(ctx);
979 : }
980 4 : }
981 :
982 : /**
983 : * FT-20 — FILE_MIGRATE_3 retry path for upload.saveFilePart.
984 : *
985 : * Scenario:
986 : * 1. Home DC (DC 2) returns FILE_MIGRATE_3 on the first saveFilePart.
987 : * 2. upload_chunk_phase opens DC 3 (pre-seeded session → fast path, no DH).
988 : * 3. Retried saveFilePart on DC 3 succeeds (boolTrue).
989 : * 4. messages.sendMedia fires on the home transport (DC 2).
990 : *
991 : * Verifications:
992 : * - domain_send_file returns 0 (full success).
993 : * - saveFilePart was called exactly twice: once home, once on DC 3.
994 : * - sendMedia was called exactly once (home DC).
995 : */
996 2 : static void test_upload_savefilepart_file_migrate(void) {
997 2 : with_tmp_home("up-fmig");
998 2 : mt_server_init(); mt_server_reset();
999 2 : reset_counters();
1000 :
1001 2 : MtProtoSession s; load_session(&s); /* seeds DC 2 */
1002 2 : ASSERT(mt_server_seed_extra_dc(3) == 0, "seed DC3 session");
1003 :
1004 2 : mt_server_expect(CRC_upload_saveFilePart, on_save_file_part_file_migrate, NULL);
1005 2 : mt_server_expect(CRC_messages_sendMedia, on_send_media, NULL);
1006 :
1007 2 : const char *path = make_tempfile("up-fmig", 1024);
1008 2 : ASSERT(path != NULL, "tempfile created");
1009 :
1010 2 : ApiConfig cfg; init_cfg(&cfg);
1011 2 : Transport t; connect_mock(&t);
1012 :
1013 2 : HistoryPeer self = { .kind = HISTORY_PEER_SELF };
1014 2 : RpcError err = {0};
1015 2 : ASSERT(domain_send_file(&cfg, &s, &t, &self, path,
1016 : "file migrate test", "text/plain", &err) == 0,
1017 : "domain_send_file succeeds after FILE_MIGRATE_3 retry");
1018 :
1019 : /* saveFilePart fired once on home DC (→ FILE_MIGRATE_3) and
1020 : * once on DC 3 (→ boolTrue). */
1021 2 : ASSERT(g_save_file_part_calls == 2,
1022 : "saveFilePart called twice: once home DC, once DC 3");
1023 :
1024 : /* sendMedia fires exactly once on the home transport. */
1025 2 : ASSERT(g_send_media_calls == 1,
1026 : "sendMedia fired once on home DC after cross-DC FILE_MIGRATE upload");
1027 :
1028 2 : unlink(path);
1029 2 : transport_close(&t);
1030 2 : mt_server_reset();
1031 : }
1032 :
1033 2 : void run_upload_download_tests(void) {
1034 2 : RUN_TEST(test_upload_small_document);
1035 2 : RUN_TEST(test_upload_multi_chunk_document);
1036 2 : RUN_TEST(test_upload_big_file_uses_big_part);
1037 2 : RUN_TEST(test_upload_photo_uses_saveFilePart);
1038 2 : RUN_TEST(test_download_photo_short_chunk);
1039 2 : RUN_TEST(test_download_photo_two_chunks);
1040 2 : RUN_TEST(test_download_photo_file_migrate);
1041 2 : RUN_TEST(test_download_document_single_chunk);
1042 2 : RUN_TEST(test_download_document_two_chunks);
1043 2 : RUN_TEST(test_download_document_file_migrate);
1044 2 : RUN_TEST(test_path_is_image);
1045 2 : RUN_TEST(test_download_photo_cache_reuse);
1046 2 : RUN_TEST(test_download_document_cache_reuse);
1047 2 : RUN_TEST(test_send_file_caption_propagates);
1048 2 : RUN_TEST(test_send_file_no_caption_empty_string);
1049 2 : RUN_TEST(test_upload_null_path);
1050 2 : RUN_TEST(test_upload_nonexistent_path);
1051 2 : RUN_TEST(test_upload_empty_file);
1052 2 : RUN_TEST(test_upload_over_max_size_rejected);
1053 2 : RUN_TEST(test_upload_under_max_size_proceeds);
1054 2 : RUN_TEST(test_upload_savefilepart_network_migrate);
1055 2 : RUN_TEST(test_upload_savefilepart_file_migrate);
1056 2 : }
|