artisan('posts:publish', ['post' => 999]) ->assertExitCode(1); }); test('it fails when the post type is not blog', function () { $site = Site::factory()->wordpress()->create(); $post = Post::factory()->create(['site_id' => $site->id, 'type' => 'twitter', 'title' => 'Hi']); $this->artisan('posts:publish', ['post' => $post->id]) ->expectsOutputToContain("expected 'blog'") ->assertExitCode(1); expect($post->fresh()->status)->toBe($post->status); }); test('it fails when the post has no title', function () { $site = Site::factory()->wordpress()->create(); $post = Post::factory()->create(['site_id' => $site->id, 'type' => 'blog', 'title' => null]); $this->artisan('posts:publish', ['post' => $post->id]) ->expectsOutputToContain('no title') ->assertExitCode(1); }); test('it fails when the post has no image', function () { $site = Site::factory()->wordpress()->create(); $post = Post::factory()->create(['site_id' => $site->id, 'type' => 'blog', 'title' => 'Hi', 'image_url' => null]); $this->artisan('posts:publish', ['post' => $post->id]) ->expectsOutputToContain('no image') ->assertExitCode(1); expect($post->fresh()->status)->toBe($post->status); }); test('it fails when the site is not configured as WordPress', function () { $site = Site::factory()->create(['cms_type' => 'custom']); $post = Post::factory()->create(['site_id' => $site->id, 'type' => 'blog', 'title' => 'Hi', 'image_url' => 'https://example.com/image.jpg']); $this->artisan('posts:publish', ['post' => $post->id]) ->expectsOutputToContain('not configured as WordPress') ->assertExitCode(1); }); test('it fails when the WordPress site is missing credentials', function () { $site = Site::factory()->create(['cms_type' => 'wordpress', 'wp_username' => null, 'wp_app_password' => null]); $post = Post::factory()->create(['site_id' => $site->id, 'type' => 'blog', 'title' => 'Hi', 'image_url' => 'https://example.com/image.jpg']); $this->artisan('posts:publish', ['post' => $post->id]) ->expectsOutputToContain('missing WordPress credentials') ->assertExitCode(1); }); test('it publishes successfully, uploading the image as the featured media', function () { Http::fake([ 'https://images.example.com/*' => Http::response('fake-image-bytes', 200, ['Content-Type' => 'image/jpeg']), '*/wp-json/wp/v2/media' => Http::response(['id' => 99], 201), '*/wp-json/wp/v2/posts' => Http::response([ 'id' => 42, 'link' => 'https://wp.example.com/2026/07/hello-world/', ], 201), ]); $site = Site::factory()->wordpress()->create(['url' => 'https://wp.example.com']); $post = Post::factory()->create([ 'site_id' => $site->id, 'type' => 'blog', 'title' => 'Hello World', 'body' => 'Post content', 'image_url' => 'https://images.example.com/photo.jpg', 'status' => 'scheduled', ]); $this->artisan('posts:publish', ['post' => $post->id]) ->expectsOutputToContain('"success":true') ->assertExitCode(0); expect($post->fresh()) ->status->toBe('published') ->published_url->toBe('https://wp.example.com/2026/07/hello-world/'); Http::assertSent(function ($request) { return $request->url() === 'https://wp.example.com/wp-json/wp/v2/media' && $request->hasHeader('Content-Disposition', 'attachment; filename="photo.jpg"') && $request->body() === 'fake-image-bytes'; }); Http::assertSent(function ($request) use ($site) { return $request->url() === "{$site->url}/wp-json/wp/v2/posts" && $request['title'] === 'Hello World' && $request['content'] === 'Post content' && $request['status'] === 'publish' && $request['featured_media'] === 99; }); }); test('it marks the post as failed when the image cannot be downloaded', function () { Http::fake([ 'https://images.example.com/*' => Http::response('not found', 404), '*/wp-json/*' => Http::response([], 500), ]); $site = Site::factory()->wordpress()->create(); $post = Post::factory()->create([ 'site_id' => $site->id, 'type' => 'blog', 'title' => 'Hi', 'image_url' => 'https://images.example.com/missing.jpg', 'status' => 'draft', ]); $this->artisan('posts:publish', ['post' => $post->id]) ->expectsOutputToContain('could not download image') ->assertExitCode(1); expect($post->fresh()->status)->toBe('failed'); }); test('it marks the post as failed when WordPress rejects the image upload', function () { Http::fake([ 'https://images.example.com/*' => Http::response('fake-image-bytes', 200, ['Content-Type' => 'image/jpeg']), '*/wp-json/wp/v2/media' => Http::response(['message' => 'Sorry, you are not allowed to upload files.'], 401), ]); $site = Site::factory()->wordpress()->create(); $post = Post::factory()->create([ 'site_id' => $site->id, 'type' => 'blog', 'title' => 'Hi', 'image_url' => 'https://images.example.com/photo.jpg', 'status' => 'draft', ]); $this->artisan('posts:publish', ['post' => $post->id]) ->expectsOutputToContain('not allowed to upload files') ->assertExitCode(1); expect($post->fresh()->status)->toBe('failed'); }); test('it marks the post as failed when WordPress returns an error', function () { Http::fake([ 'https://images.example.com/*' => Http::response('fake-image-bytes', 200, ['Content-Type' => 'image/jpeg']), '*/wp-json/wp/v2/media' => Http::response(['id' => 99], 201), '*/wp-json/wp/v2/posts' => Http::response([ 'code' => 'rest_cannot_create', 'message' => 'Sorry, you are not allowed to create posts.', ], 401), ]); $site = Site::factory()->wordpress()->create(); $post = Post::factory()->create([ 'site_id' => $site->id, 'type' => 'blog', 'title' => 'Hi', 'image_url' => 'https://images.example.com/photo.jpg', 'status' => 'draft', ]); $this->artisan('posts:publish', ['post' => $post->id]) ->expectsOutputToContain('not allowed to create posts') ->assertExitCode(1); expect($post->fresh()->status)->toBe('failed'); }); test('it marks the post as failed when the site is unreachable', function () { Http::fake([ 'https://images.example.com/*' => Http::response('fake-image-bytes', 200, ['Content-Type' => 'image/jpeg']), '*/wp-json/*' => function () { throw new \Illuminate\Http\Client\ConnectionException('Could not connect'); }, ]); $site = Site::factory()->wordpress()->create(); $post = Post::factory()->create([ 'site_id' => $site->id, 'type' => 'blog', 'title' => 'Hi', 'image_url' => 'https://images.example.com/photo.jpg', 'status' => 'draft', ]); $this->artisan('posts:publish', ['post' => $post->id]) ->expectsOutputToContain('could not connect') ->assertExitCode(1); expect($post->fresh()->status)->toBe('failed'); }); test('it correctly builds the WordPress endpoint URL when the site URL has a trailing slash', function () { Http::fake([ 'https://images.example.com/*' => Http::response('fake-image-bytes', 200, ['Content-Type' => 'image/jpeg']), '*/wp-json/wp/v2/media' => Http::response(['id' => 99], 201), '*/wp-json/wp/v2/posts' => Http::response([ 'id' => 42, 'link' => 'https://wp.example.com/2026/07/hello-world/', ], 201), ]); $site = Site::factory()->wordpress()->create(['url' => 'https://wp.example.com/']); $post = Post::factory()->create([ 'site_id' => $site->id, 'type' => 'blog', 'title' => 'Hello World', 'body' => 'Post content', 'image_url' => 'https://images.example.com/photo.jpg', 'status' => 'draft', ]); $this->artisan('posts:publish', ['post' => $post->id]) ->assertExitCode(0); Http::assertSent(function ($request) { return $request->url() === 'https://wp.example.com/wp-json/wp/v2/posts'; }); Http::assertSent(function ($request) { return $request->url() === 'https://wp.example.com/wp-json/wp/v2/media'; }); });