'video/mp4']; } function wp_check_filetype_and_ext(string $file, string $name, array $mimes = []): array { $ext = strtolower(pathinfo($name, PATHINFO_EXTENSION)); return ['ext' => $ext, 'type' => $mimes[$ext] ?? '']; } class WP_Error { private array $errors; public function __construct($code = '', $message = '') { $this->errors = [$code => $message]; } public function get_error_message(): string { return (string) reset($this->errors); } public function get_error_code() { return (string) array_key_first($this->errors); } } class WP_REST_Server { const READABLE = 'GET'; } class WP_REST_Response { private array $data; private int $status; public function __construct($data = [], int $status = 200) { $this->data = $data; $this->status = $status; } public function get_data(): array { return $this->data; } public function get_status(): int { return $this->status; } } class WP_REST_Request { } class FakeRestRequest extends \WP_REST_Request { private array $params; private array $files; public function __construct(array $params = [], array $files = []) { $this->params = $params; $this->files = $files; } public function get_param(string $key) { return $this->params[$key] ?? null; } public function get_file_params(): array { return $this->files; } } function split_upload_check(bool $condition, string $message): void { if (!$condition) throw new \RuntimeException($message); } require dirname(__DIR__, 2) . '/src/Core/FrontSubmitChunk.php'; } namespace AKSRT\Core { final class FakeFrontSubmit { use \AKSRT\Core\FrontSubmitChunk; public static int $limit_bytes = 10 * 1048576; public static array $persisted = []; public static function upload_limit_bytes(string $kind): int { return self::$limit_bytes; } /** Stand-in for FrontSubmit::persist_uploaded_file(): records and echoes merged content. */ public function persist_uploaded_file(string $kind, array $file, bool $sideload = false) { self::$persisted[] = ['kind' => $kind, 'file' => $file, 'sideload' => $sideload]; return 'persisted:' . (string) file_get_contents((string) $file['tmp_name']); } } } namespace { use AKSRT\Core\FakeFrontSubmit; $submit = new FakeFrontSubmit(); $temp_dir = WP_CONTENT_DIR . '/aksrt-temp'; // 1) Chunk parameter validation. $err = $submit->rest_upload_chunk(new FakeRestRequest(['name' => '', 'chunks' => 3, 'chunk' => 0, 'size' => 10])); split_upload_check($err instanceof \WP_Error, 'empty name should be rejected'); $err = $submit->rest_upload_chunk(new FakeRestRequest(['name' => 'a.mp4', 'chunks' => 0, 'chunk' => 0, 'size' => 10])); split_upload_check($err instanceof \WP_Error, 'zero chunk count should be rejected'); $err = $submit->rest_upload_chunk(new FakeRestRequest(['name' => 'a.mp4', 'chunks' => 3, 'chunk' => 5, 'size' => 10])); split_upload_check($err instanceof \WP_Error, 'out-of-range chunk index should be rejected'); // 2) Chunk upload lands per-index .part files, oversized total is refused. $make_file = static function (string $content): string { $path = tempnam(sys_get_temp_dir(), 'chunk'); file_put_contents($path, $content); return $path; }; $params = static fn (int $chunk, int $chunks, int $size): array => [ 'kind' => 'video', 'chunk' => $chunk, 'chunks' => $chunks, 'name' => 'movie.mp4', 'size' => $size, ]; $response = $submit->rest_upload_chunk(new FakeRestRequest($params(0, 3, 6), ['file' => ['tmp_name' => $make_file('AAA'), 'error' => 0]])); split_upload_check($response instanceof \WP_REST_Response, 'first chunk should upload'); split_upload_check($response->get_data()['uploaded_size'] === 3, 'uploaded_size should count the stored chunk'); $submit->rest_upload_chunk(new FakeRestRequest($params(2, 3, 6), ['file' => ['tmp_name' => $make_file('CCC'), 'error' => 0]])); $parts = glob($temp_dir . '/*.part'); split_upload_check(count($parts) === 2, 'two part files should exist'); foreach ($parts as $part) { split_upload_check(strpos(basename($part), '1-') === 0, 'part name should be blog-id prefixed'); } // 3) Status reports the uploaded indexes (resume contract). $response = $submit->rest_upload_chunk_status(new FakeRestRequest(['kind' => 'video', 'name' => 'movie.mp4', 'chunks' => 3])); split_upload_check($response instanceof \WP_REST_Response, 'status should respond'); split_upload_check($response->get_data()['uploaded'] === [0, 2], 'status should list uploaded chunk indexes'); // 4) Merge refuses when a chunk is missing. $err = $submit->rest_upload_chunk_merge(new FakeRestRequest(['kind' => 'video', 'name' => 'movie.mp4', 'chunks' => 3, 'size' => 9])); split_upload_check($err instanceof \WP_Error && $err->get_error_code() === 'aksrt_chunk_missing', 'merge should refuse with missing chunks'); // 5) Merge with a size mismatch is refused. $submit->rest_upload_chunk(new FakeRestRequest($params(1, 3, 6), ['file' => ['tmp_name' => $make_file('BBB'), 'error' => 0]])); $err = $submit->rest_upload_chunk_merge(new FakeRestRequest(['kind' => 'video', 'name' => 'movie.mp4', 'chunks' => 3, 'size' => 999])); split_upload_check($err instanceof \WP_Error && $err->get_error_code() === 'aksrt_chunk_size_mismatch', 'size mismatch should refuse the merge'); // 6) Happy-path merge concatenates in order, flags sideload, and cleans up. $response = $submit->rest_upload_chunk_merge(new FakeRestRequest(['kind' => 'video', 'name' => 'movie.mp4', 'chunks' => 3, 'size' => 9])); split_upload_check(is_string($response) && $response === 'persisted:AAABBBCCC', 'merge should concatenate chunks in order'); split_upload_check(count(FakeFrontSubmit::$persisted) === 1, 'merge should persist exactly once'); $call = FakeFrontSubmit::$persisted[0]; split_upload_check($call['kind'] === 'video' && $call['sideload'] === true, 'merge should persist as a sideloaded video'); split_upload_check($call['file']['name'] === 'movie.mp4' && $call['file']['size'] === 9, 'merge should rebuild the $_FILES-style payload'); split_upload_check(glob($temp_dir . '/*.part') === [], 'merge should clean up all parts'); // 7) Per-user keying: a different file name never sees another name's parts. $response = $submit->rest_upload_chunk_status(new FakeRestRequest(['kind' => 'video', 'name' => 'other.mp4', 'chunks' => 3])); split_upload_check($response instanceof \WP_REST_Response && $response->get_data()['uploaded'] === [], 'a different name should start with no uploaded chunks'); // 8) Chunk size cap refuses oversized chunk blobs. $big = $make_file(str_repeat('x', 3 * 1048576)); $err = $submit->rest_upload_chunk(new FakeRestRequest($params(0, 1, 3 * 1048576), ['file' => ['tmp_name' => $big, 'error' => 0]])); split_upload_check($err instanceof \WP_Error && $err->get_error_code() === 'aksrt_chunk_invalid_size', 'oversized chunk blobs should be refused'); // 9) Kind size limit caps cumulative uploads. FakeFrontSubmit::$limit_bytes = 4; $err = $submit->rest_upload_chunk(new FakeRestRequest($params(0, 2, 9), ['file' => ['tmp_name' => $make_file('AAAAA'), 'error' => 0]])); split_upload_check($err instanceof \WP_Error && $err->get_error_code() === 'aksrt_upload_too_large', 'cumulative size over the kind limit should be refused'); // Cleanup. FakeFrontSubmit::$persisted = []; foreach ((array) glob(WP_CONTENT_DIR . '/*.part') as $leftover) { @unlink((string) $leftover); } @unlink(WP_CONTENT_DIR . '/aksrt-temp/index.php'); @rmdir(WP_CONTENT_DIR . '/aksrt-temp'); @rmdir(WP_CONTENT_DIR); echo "Split upload harness passed\n"; }