Skip to content

Commit 99f94ca

Browse files
committed
test: add coverage for HasFrequencies, getCommands, TaskRequest validation, and notifications
- HasFrequenciesTest: frequency creation, removal on update, deletion on type change - TotemCommandsTest: no-filter passthrough, whitelist, blacklist filtering - TaskRequestTest: required fields, cron validation, frequency, email/phone format - VonageNotificationTest: mail/slack channels and toMail()/toSlack() content - Add laravel/slack-notification-channel to require-dev and suggest (toSlack() requires this package, same pattern as Vonage)
1 parent def06e4 commit 99f94ca

5 files changed

Lines changed: 246 additions & 0 deletions

File tree

composer.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,13 @@
3030
"illuminate/notifications": "^10.0|^11.0|^12.0"
3131
},
3232
"require-dev": {
33+
"laravel/slack-notification-channel": "^3.7",
3334
"laravel/vonage-notification-channel": "^3.3",
3435
"orchestra/testbench": "^8.0|^9.0|^10.0",
3536
"phpunit/phpunit": "^10.0|^11.0|^12.0"
3637
},
3738
"suggest": {
39+
"laravel/slack-notification-channel": "Required for Slack notifications.",
3840
"laravel/vonage-notification-channel": "Required for SMS notifications via Vonage."
3941
},
4042
"autoload": {
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
<?php
2+
3+
namespace Studio\Totem\Tests\Feature;
4+
5+
use Studio\Totem\Task;
6+
use Studio\Totem\Tests\TestCase;
7+
8+
class HasFrequenciesTest extends TestCase
9+
{
10+
public function test_frequencies_are_created_when_type_is_frequency(): void
11+
{
12+
$task = Task::factory()->create(['expression' => null]);
13+
14+
$task->afterSave([
15+
'type' => 'frequency',
16+
'frequencies' => [
17+
['interval' => 'everyMinute', 'label' => 'Every Minute'],
18+
],
19+
]);
20+
21+
$this->assertDatabaseHas('task_frequencies', [
22+
'task_id' => $task->id,
23+
'interval' => 'everyMinute',
24+
'label' => 'Every Minute',
25+
]);
26+
}
27+
28+
public function test_removed_frequency_is_deleted_on_update(): void
29+
{
30+
$task = Task::factory()->create(['expression' => null]);
31+
32+
$task->afterSave([
33+
'type' => 'frequency',
34+
'frequencies' => [
35+
['interval' => 'everyMinute', 'label' => 'Every Minute'],
36+
['interval' => 'hourly', 'label' => 'Hourly'],
37+
],
38+
]);
39+
40+
$task->load('frequencies');
41+
42+
$task->afterSave([
43+
'type' => 'frequency',
44+
'frequencies' => [
45+
['interval' => 'everyMinute', 'label' => 'Every Minute'],
46+
],
47+
]);
48+
49+
$this->assertDatabaseHas('task_frequencies', [
50+
'task_id' => $task->id,
51+
'interval' => 'everyMinute',
52+
]);
53+
$this->assertDatabaseMissing('task_frequencies', [
54+
'task_id' => $task->id,
55+
'interval' => 'hourly',
56+
]);
57+
}
58+
59+
public function test_frequencies_deleted_when_type_changes_to_expression(): void
60+
{
61+
$task = Task::factory()->create(['expression' => null]);
62+
63+
$task->afterSave([
64+
'type' => 'frequency',
65+
'frequencies' => [
66+
['interval' => 'everyMinute', 'label' => 'Every Minute'],
67+
],
68+
]);
69+
70+
$task->load('frequencies');
71+
72+
$task->afterSave(['type' => 'expression']);
73+
74+
$this->assertDatabaseMissing('task_frequencies', ['task_id' => $task->id]);
75+
}
76+
}

tests/Feature/TaskRequestTest.php

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
<?php
2+
3+
namespace Studio\Totem\Tests\Feature;
4+
5+
use Studio\Totem\Tests\TestCase;
6+
7+
class TaskRequestTest extends TestCase
8+
{
9+
private function validPayload(): array
10+
{
11+
return [
12+
'description' => 'Run cache:clear',
13+
'command' => 'cache:clear',
14+
'type' => 'expression',
15+
'expression' => '* * * * *',
16+
];
17+
}
18+
19+
public function test_description_is_required(): void
20+
{
21+
$this->signIn();
22+
23+
$this->post(route('totem.task.create'), array_merge($this->validPayload(), ['description' => '']))
24+
->assertSessionHasErrors('description');
25+
}
26+
27+
public function test_command_is_required(): void
28+
{
29+
$this->signIn();
30+
31+
$this->post(route('totem.task.create'), array_merge($this->validPayload(), ['command' => '']))
32+
->assertSessionHasErrors('command');
33+
}
34+
35+
public function test_expression_required_when_type_is_expression(): void
36+
{
37+
$this->signIn();
38+
39+
$this->post(route('totem.task.create'), array_merge($this->validPayload(), ['expression' => '']))
40+
->assertSessionHasErrors('expression');
41+
}
42+
43+
public function test_invalid_cron_expression_fails(): void
44+
{
45+
$this->signIn();
46+
47+
$this->post(route('totem.task.create'), array_merge($this->validPayload(), ['expression' => 'not-a-cron']))
48+
->assertSessionHasErrors('expression');
49+
}
50+
51+
public function test_frequencies_required_when_type_is_frequency(): void
52+
{
53+
$this->signIn();
54+
55+
$this->post(route('totem.task.create'), [
56+
'description' => 'Run cache:clear',
57+
'command' => 'cache:clear',
58+
'type' => 'frequency',
59+
])->assertSessionHasErrors('frequencies');
60+
}
61+
62+
public function test_invalid_notification_email_fails(): void
63+
{
64+
$this->signIn();
65+
66+
$this->post(route('totem.task.create'), array_merge($this->validPayload(), [
67+
'notification_email_address' => 'not-an-email',
68+
]))->assertSessionHasErrors('notification_email_address');
69+
}
70+
71+
public function test_notification_phone_too_short_fails(): void
72+
{
73+
$this->signIn();
74+
75+
$this->post(route('totem.task.create'), array_merge($this->validPayload(), [
76+
'notification_phone_number' => '1234567890', // 10 digits — below minimum of 11
77+
]))->assertSessionHasErrors('notification_phone_number');
78+
}
79+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php
2+
3+
namespace Studio\Totem\Tests\Feature;
4+
5+
use Studio\Totem\Tests\TestCase;
6+
use Studio\Totem\Totem;
7+
8+
class TotemCommandsTest extends TestCase
9+
{
10+
public function test_all_commands_returned_when_no_filter(): void
11+
{
12+
config(['totem.artisan.command_filter' => []]);
13+
14+
$commands = Totem::getCommands();
15+
16+
$this->assertGreaterThan(1, $commands->count());
17+
}
18+
19+
public function test_whitelist_filter_returns_only_matching_commands(): void
20+
{
21+
config([
22+
'totem.artisan.command_filter' => ['totem:*'],
23+
'totem.artisan.whitelist' => true,
24+
]);
25+
26+
$commands = Totem::getCommands();
27+
28+
$this->assertNotEmpty($commands);
29+
foreach ($commands as $command) {
30+
$this->assertStringStartsWith('totem:', $command->getName());
31+
}
32+
}
33+
34+
public function test_blacklist_filter_excludes_matching_commands(): void
35+
{
36+
config([
37+
'totem.artisan.command_filter' => ['totem:*'],
38+
'totem.artisan.whitelist' => false,
39+
]);
40+
41+
$commands = Totem::getCommands();
42+
43+
foreach ($commands as $command) {
44+
$this->assertStringNotContainsString('totem:', $command->getName());
45+
}
46+
}
47+
}

tests/Feature/VonageNotificationTest.php

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
namespace Studio\Totem\Tests\Feature;
44

5+
use Illuminate\Notifications\Messages\MailMessage;
6+
use Illuminate\Notifications\Messages\SlackMessage;
57
use Studio\Totem\Notifications\TaskCompleted;
68
use Studio\Totem\Task;
79
use Studio\Totem\Tests\TestCase;
@@ -39,4 +41,44 @@ public function test_to_vonage_returns_correct_content(): void
3941

4042
$this->assertStringContainsString('My Task', $message->content);
4143
}
44+
45+
public function test_mail_channel_used_when_email_set(): void
46+
{
47+
$task = Task::factory()->create(['notification_email_address' => 'test@example.com']);
48+
$notification = new TaskCompleted('output');
49+
50+
$this->assertContains('mail', $notification->via($task));
51+
}
52+
53+
public function test_slack_channel_used_when_webhook_set(): void
54+
{
55+
$task = Task::factory()->create(['notification_slack_webhook' => 'https://hooks.slack.com/test']);
56+
$notification = new TaskCompleted('output');
57+
58+
$this->assertContains('slack', $notification->via($task));
59+
}
60+
61+
public function test_to_mail_uses_task_description_as_subject_and_includes_output(): void
62+
{
63+
$task = Task::factory()->create(['description' => 'My Task']);
64+
$notification = new TaskCompleted('Task ran successfully');
65+
66+
$message = $notification->toMail($task);
67+
68+
$this->assertInstanceOf(MailMessage::class, $message);
69+
$this->assertSame('My Task', $message->subject);
70+
$this->assertStringContainsString('Task ran successfully', implode(' ', $message->introLines));
71+
}
72+
73+
public function test_to_slack_includes_task_description_in_attachment(): void
74+
{
75+
$task = Task::factory()->create(['description' => 'My Task']);
76+
$notification = new TaskCompleted('output');
77+
78+
$message = $notification->toSlack($task);
79+
80+
$this->assertInstanceOf(SlackMessage::class, $message);
81+
$attachment = $message->attachments[0];
82+
$this->assertStringContainsString('My Task', $attachment->content);
83+
}
4284
}

0 commit comments

Comments
 (0)