diff --git a/app/Actions/Post/CreatePost.php b/app/Actions/Post/CreatePost.php index 7950339d..1462f425 100644 --- a/app/Actions/Post/CreatePost.php +++ b/app/Actions/Post/CreatePost.php @@ -12,6 +12,7 @@ use App\Models\Workspace; use Carbon\Carbon; use Illuminate\Support\Facades\DB; +use InvalidArgumentException; class CreatePost { @@ -39,18 +40,26 @@ class CreatePost * platforms?: array}>, * label_ids?: array * } $data + * + * @throws InvalidArgumentException when created_via is missing or not a CreatedVia case */ public static function execute(Workspace $workspace, User $user, array $data): Post { + $createdVia = data_get($data, 'created_via'); + + if (! $createdVia instanceof CreatedVia) { + throw new InvalidArgumentException('created_via must be a CreatedVia enum case.'); + } + $scheduledAt = self::resolveScheduledAt($data); - $post = DB::transaction(function () use ($workspace, $user, $data, $scheduledAt): Post { + $post = DB::transaction(function () use ($workspace, $user, $data, $scheduledAt, $createdVia): Post { $post = $workspace->posts()->create([ 'user_id' => $user->id, 'content' => data_get($data, 'content', ''), 'media' => data_get($data, 'media', []), 'status' => PostStatus::Draft, - 'created_via' => data_get($data, 'created_via'), + 'created_via' => $createdVia, 'scheduled_at' => $scheduledAt, ]); diff --git a/app/Actions/Post/DuplicatePost.php b/app/Actions/Post/DuplicatePost.php index 3fdd67ec..1e40854e 100644 --- a/app/Actions/Post/DuplicatePost.php +++ b/app/Actions/Post/DuplicatePost.php @@ -4,6 +4,7 @@ namespace App\Actions\Post; +use App\Enums\Post\CreatedVia; use App\Enums\Post\Status as PostStatus; use App\Enums\PostPlatform\Status as PostPlatformStatus; use App\Models\Post; @@ -25,6 +26,7 @@ public static function execute(Post $original, User $user): Post 'content' => $original->content, 'media' => $original->media, 'status' => PostStatus::Draft, + 'created_via' => CreatedVia::Web, 'scheduled_at' => null, 'published_at' => null, ]); diff --git a/tests/Feature/Actions/Post/CreatePostTest.php b/tests/Feature/Actions/Post/CreatePostTest.php index 690f3bb9..e0d2d7f4 100644 --- a/tests/Feature/Actions/Post/CreatePostTest.php +++ b/tests/Feature/Actions/Post/CreatePostTest.php @@ -43,3 +43,12 @@ 'api' => CreatedVia::Api, 'automation' => CreatedVia::Automation, ]); + +test('execute requires created_via', function () { + $user = User::factory()->create(); + $workspace = Workspace::factory()->create(['user_id' => $user->id]); + + CreatePost::execute($workspace, $user, [ + 'content' => 'Hello world', + ]); +})->throws(InvalidArgumentException::class, 'created_via must be a CreatedVia enum case.'); diff --git a/tests/Feature/Actions/Post/DuplicatePostTest.php b/tests/Feature/Actions/Post/DuplicatePostTest.php new file mode 100644 index 00000000..628c6943 --- /dev/null +++ b/tests/Feature/Actions/Post/DuplicatePostTest.php @@ -0,0 +1,31 @@ +create(); + $workspace = Workspace::factory()->create(['user_id' => $user->id]); + $original = Post::factory()->create([ + 'workspace_id' => $workspace->id, + 'user_id' => $user->id, + 'content' => 'Original content', + 'created_via' => CreatedVia::Api, + 'status' => PostStatus::Published, + ]); + + $copy = DuplicatePost::execute($original, $user); + + expect($copy->id)->not->toBe($original->id) + ->and($copy->content)->toBe('Original content') + ->and($copy->status)->toBe(PostStatus::Draft) + ->and($copy->created_via)->toBe(CreatedVia::Web) + ->and($copy->scheduled_at)->toBeNull() + ->and($copy->published_at)->toBeNull(); +}); diff --git a/tests/Feature/Ai/StreamPostCreationTest.php b/tests/Feature/Ai/StreamPostCreationTest.php index 513da29b..457d1f64 100644 --- a/tests/Feature/Ai/StreamPostCreationTest.php +++ b/tests/Feature/Ai/StreamPostCreationTest.php @@ -4,6 +4,7 @@ use App\Ai\Agents\PostContentGenerator; use App\Ai\Agents\PostContentHumanizer; +use App\Enums\Post\CreatedVia; use App\Enums\PostPlatform\ContentType; use App\Enums\UserWorkspace\Role; use App\Jobs\Ai\StreamPostCreation; @@ -111,7 +112,8 @@ function runStreamPostCreation(string $format, SocialAccount $account, int $imag $post = $this->user->currentWorkspace->posts()->latest()->first(); expect($post->content)->toBe('Hello world\n\nSecond para.') - ->and($post->media)->toHaveCount(1); + ->and($post->media)->toHaveCount(1) + ->and($post->created_via)->toBe(CreatedVia::Web); $platform = PostPlatform::where('social_account_id', $this->account->id)->firstOrFail(); diff --git a/tests/Feature/Api/PostApiTest.php b/tests/Feature/Api/PostApiTest.php index 444bcbfd..567607a6 100644 --- a/tests/Feature/Api/PostApiTest.php +++ b/tests/Feature/Api/PostApiTest.php @@ -78,6 +78,23 @@ expect($post->created_via)->toBe(CreatedVia::Api); }); +it('ignores a client-supplied created_via and always records api', function () { + $this->withHeaders(['Authorization' => 'Bearer '.$this->plainToken]) + ->postJson(route('api.posts.store'), [ + 'created_via' => 'web', + 'platforms' => [ + [ + 'social_account_id' => $this->socialAccount->id, + 'content_type' => 'linkedin_post', + ], + ], + ]) + ->assertCreated(); + + $post = Post::where('workspace_id', $this->workspace->id)->first(); + expect($post->created_via)->toBe(CreatedVia::Api); +}); + it('creates a post with content, media, and labels', function () { $label = WorkspaceLabel::factory()->create(['workspace_id' => $this->workspace->id]); diff --git a/tests/Feature/Automation/Node/GenerateNodeTest.php b/tests/Feature/Automation/Node/GenerateNodeTest.php index f8fa5673..368a99de 100644 --- a/tests/Feature/Automation/Node/GenerateNodeTest.php +++ b/tests/Feature/Automation/Node/GenerateNodeTest.php @@ -7,6 +7,7 @@ use App\Ai\Agents\PostContentHumanizer; use App\Enums\Ai\ContentStyle; use App\Enums\Ai\GeneratorFormat; +use App\Enums\Post\CreatedVia; use App\Enums\Post\Status as PostStatus; use App\Enums\PostPlatform\ContentType; use App\Models\Automation; @@ -47,6 +48,7 @@ $post = Post::find($run->generated_post_id); expect($post)->not->toBeNull(); expect($post->status)->toBe(PostStatus::Draft); + expect($post->created_via)->toBe(CreatedVia::Automation); }); it('applies brand voice by default', function () { diff --git a/tests/Feature/PostTemplateControllerTest.php b/tests/Feature/PostTemplateControllerTest.php index be4b34bd..7e20d520 100644 --- a/tests/Feature/PostTemplateControllerTest.php +++ b/tests/Feature/PostTemplateControllerTest.php @@ -2,6 +2,7 @@ declare(strict_types=1); +use App\Enums\Post\CreatedVia; use App\Enums\UserWorkspace\Role; use App\Models\Account; use App\Models\SocialAccount; @@ -72,6 +73,9 @@ expect($response->json('post_id'))->toBeString(); expect($response->json('redirect_url'))->toContain('edit'); + + $post = $this->workspace->posts()->find($response->json('post_id')); + expect($post->created_via)->toBe(CreatedVia::Web); }); test('apply interpolates brand_name in content', function () {