whisper-money/tests/Feature/AutomationRuleTest.php

431 lines
16 KiB
PHP

<?php
use App\Models\AutomationRule;
use App\Models\Category;
use App\Models\Label;
use App\Models\User;
test('user can view their automation rules', function () {
$user = User::factory()->create();
$rule = AutomationRule::factory()->create(['user_id' => $user->id]);
$response = $this->actingAs($user)->get(route('automation-rules.index'));
$response->assertSuccessful();
$response->assertInertia(
fn ($page) => $page
->component('settings/automation-rules')
->has('automationRules', 1)
);
});
test('user can create an automation rule with category action', function () {
$user = User::factory()->create();
$category = Category::factory()->create(['user_id' => $user->id]);
$response = $this->actingAs($user)
->from(route('automation-rules.index'))
->post(route('automation-rules.store'), [
'title' => 'Grocery Rule',
'priority' => 10,
'rules_json' => json_encode(['in' => ['grocery', ['var' => 'description']]]),
'action_category_id' => $category->id,
'action_note' => null,
'action_note_iv' => null,
]);
$response->assertRedirect(route('automation-rules.index'));
$this->assertDatabaseHas('automation_rules', [
'user_id' => $user->id,
'title' => 'Grocery Rule',
'priority' => 10,
'action_category_id' => $category->id,
]);
});
test('user can create an automation rule with labels action only', function () {
$user = User::factory()->create();
$label = Label::factory()->create(['user_id' => $user->id]);
$response = $this->actingAs($user)
->from(route('automation-rules.index'))
->post(route('automation-rules.store'), [
'title' => 'Label Only Rule',
'priority' => 5,
'rules_json' => json_encode(['==' => [['var' => 'amount'], 100]]),
'action_category_id' => null,
'action_note' => null,
'action_note_iv' => null,
'action_label_ids' => [$label->id],
]);
$response->assertRedirect(route('automation-rules.index'));
$this->assertDatabaseHas('automation_rules', [
'user_id' => $user->id,
'title' => 'Label Only Rule',
]);
$rule = AutomationRule::where('title', 'Label Only Rule')->first();
expect($rule->labels)->toHaveCount(1);
expect($rule->labels->first()->id)->toBe($label->id);
});
test('user can create an automation rule with both actions', function () {
$user = User::factory()->create();
$category = Category::factory()->create(['user_id' => $user->id]);
$response = $this->actingAs($user)
->from(route('automation-rules.index'))
->post(route('automation-rules.store'), [
'title' => 'Combined Rule',
'priority' => 0,
'rules_json' => json_encode(['>' => [['var' => 'amount'], 50]]),
'action_category_id' => $category->id,
'action_note' => 'encrypted_note',
'action_note_iv' => 'test_iv',
]);
$response->assertRedirect(route('automation-rules.index'));
$this->assertDatabaseHas('automation_rules', [
'user_id' => $user->id,
'title' => 'Combined Rule',
'action_category_id' => $category->id,
'action_note' => 'encrypted_note',
]);
});
test('user cannot create rule without at least one action', function () {
$user = User::factory()->create();
$response = $this->actingAs($user)->post(route('automation-rules.store'), [
'title' => 'Invalid Rule',
'priority' => 0,
'rules_json' => json_encode(['>' => [['var' => 'amount'], 50]]),
'action_category_id' => null,
'action_note' => null,
'action_note_iv' => null,
]);
$response->assertSessionHasErrors('action_category_id');
});
test('user cannot create rule with invalid json logic', function () {
$user = User::factory()->create();
$category = Category::factory()->create(['user_id' => $user->id]);
$response = $this->actingAs($user)->post(route('automation-rules.store'), [
'title' => 'Invalid JSON Rule',
'priority' => 0,
'rules_json' => 'invalid json',
'action_category_id' => $category->id,
]);
$response->assertSessionHasErrors('rules_json');
});
test('user can update their automation rule', function () {
$user = User::factory()->create();
$category = Category::factory()->create(['user_id' => $user->id]);
$rule = AutomationRule::factory()->create([
'user_id' => $user->id,
'title' => 'Old Title',
]);
$response = $this->actingAs($user)
->from(route('automation-rules.index'))
->patch(route('automation-rules.update', $rule), [
'title' => 'New Title',
'priority' => 20,
'rules_json' => json_encode(['==' => [['var' => 'bank_name'], 'Chase']]),
'action_category_id' => $category->id,
'action_note' => null,
'action_note_iv' => null,
]);
$response->assertRedirect(route('automation-rules.index'));
$this->assertDatabaseHas('automation_rules', [
'id' => $rule->id,
'title' => 'New Title',
'priority' => 20,
]);
});
test('user cannot update another users automation rule', function () {
$user = User::factory()->create();
$otherUser = User::factory()->create();
$category = Category::factory()->create(['user_id' => $user->id]);
$rule = AutomationRule::factory()->create(['user_id' => $otherUser->id]);
$response = $this->actingAs($user)->patch(route('automation-rules.update', $rule), [
'title' => 'Hacked Title',
'priority' => 0,
'rules_json' => json_encode(['==' => [1, 1]]),
'action_category_id' => $category->id,
]);
$response->assertForbidden();
});
test('user can soft delete their automation rule', function () {
$user = User::factory()->create();
$rule = AutomationRule::factory()->create(['user_id' => $user->id]);
$response = $this->actingAs($user)
->from(route('automation-rules.index'))
->delete(route('automation-rules.destroy', $rule));
$response->assertRedirect(route('automation-rules.index'));
$this->assertSoftDeleted('automation_rules', ['id' => $rule->id]);
});
test('user cannot delete another users automation rule', function () {
$user = User::factory()->create();
$otherUser = User::factory()->create();
$rule = AutomationRule::factory()->create(['user_id' => $otherUser->id]);
$response = $this->actingAs($user)->delete(route('automation-rules.destroy', $rule));
$response->assertForbidden();
$this->assertDatabaseHas('automation_rules', ['id' => $rule->id]);
});
test('rules are ordered by priority', function () {
$user = User::factory()->create();
AutomationRule::factory()->create(['user_id' => $user->id, 'priority' => 30]);
AutomationRule::factory()->create(['user_id' => $user->id, 'priority' => 10]);
AutomationRule::factory()->create(['user_id' => $user->id, 'priority' => 20]);
$response = $this->actingAs($user)->get(route('automation-rules.index'));
$response->assertSuccessful();
$response->assertInertia(
fn ($page) => $page
->has('automationRules', 3)
->where('automationRules.0.priority', 10)
->where('automationRules.1.priority', 20)
->where('automationRules.2.priority', 30)
);
});
test('user cannot use another users category in rule', function () {
$user = User::factory()->create();
$otherUser = User::factory()->create();
$otherCategory = Category::factory()->create(['user_id' => $otherUser->id]);
$response = $this->actingAs($user)->post(route('automation-rules.store'), [
'title' => 'Invalid Category Rule',
'priority' => 0,
'rules_json' => json_encode(['==' => [1, 1]]),
'action_category_id' => $otherCategory->id,
]);
$response->assertSessionHasErrors('action_category_id');
});
test('rules with description filter are case insensitive with lowercase rule', function () {
$user = User::factory()->create();
$category = Category::factory()->create(['user_id' => $user->id]);
$response = $this->actingAs($user)
->from(route('automation-rules.index'))
->post(route('automation-rules.store'), [
'title' => 'Case Insensitive Description Rule (lowercase)',
'priority' => 10,
'rules_json' => json_encode(['in' => ['m3 sport', ['var' => 'description']]]),
'action_category_id' => $category->id,
'action_note' => null,
'action_note_iv' => null,
]);
$response->assertRedirect(route('automation-rules.index'));
$this->assertDatabaseHas('automation_rules', [
'user_id' => $user->id,
'title' => 'Case Insensitive Description Rule (lowercase)',
]);
});
test('rules with description filter are case insensitive with uppercase rule', function () {
$user = User::factory()->create();
$category = Category::factory()->create(['user_id' => $user->id]);
$response = $this->actingAs($user)
->from(route('automation-rules.index'))
->post(route('automation-rules.store'), [
'title' => 'Case Insensitive Description Rule (uppercase)',
'priority' => 10,
'rules_json' => json_encode(['in' => ['M3 SPORT', ['var' => 'description']]]),
'action_category_id' => $category->id,
'action_note' => null,
'action_note_iv' => null,
]);
$response->assertRedirect(route('automation-rules.index'));
$this->assertDatabaseHas('automation_rules', [
'user_id' => $user->id,
'title' => 'Case Insensitive Description Rule (uppercase)',
]);
});
test('rules with description filter are case insensitive with mixed case rule', function () {
$user = User::factory()->create();
$category = Category::factory()->create(['user_id' => $user->id]);
$response = $this->actingAs($user)
->from(route('automation-rules.index'))
->post(route('automation-rules.store'), [
'title' => 'Case Insensitive Description Rule (mixed)',
'priority' => 10,
'rules_json' => json_encode(['in' => ['M3 Sport Academy', ['var' => 'description']]]),
'action_category_id' => $category->id,
'action_note' => null,
'action_note_iv' => null,
]);
$response->assertRedirect(route('automation-rules.index'));
$this->assertDatabaseHas('automation_rules', [
'user_id' => $user->id,
'title' => 'Case Insensitive Description Rule (mixed)',
]);
});
test('rules with notes filter are case insensitive', function () {
$user = User::factory()->create();
$category = Category::factory()->create(['user_id' => $user->id]);
$response = $this->actingAs($user)
->from(route('automation-rules.index'))
->post(route('automation-rules.store'), [
'title' => 'Case Insensitive Notes Rule',
'priority' => 10,
'rules_json' => json_encode(['in' => ['IMPORTANT NOTE', ['var' => 'notes']]]),
'action_category_id' => $category->id,
'action_note' => null,
'action_note_iv' => null,
]);
$response->assertRedirect(route('automation-rules.index'));
$this->assertDatabaseHas('automation_rules', [
'user_id' => $user->id,
'title' => 'Case Insensitive Notes Rule',
]);
});
test('user can create an automation rule with labels action', function () {
$user = User::factory()->create();
$label1 = Label::factory()->create(['user_id' => $user->id]);
$label2 = Label::factory()->create(['user_id' => $user->id]);
$response = $this->actingAs($user)
->from(route('automation-rules.index'))
->post(route('automation-rules.store'), [
'title' => 'Labels Rule',
'priority' => 10,
'rules_json' => json_encode(['in' => ['grocery', ['var' => 'description']]]),
'action_category_id' => null,
'action_note' => null,
'action_note_iv' => null,
'action_label_ids' => [$label1->id, $label2->id],
]);
$response->assertRedirect(route('automation-rules.index'));
$rule = AutomationRule::where('title', 'Labels Rule')->first();
expect($rule)->not->toBeNull();
expect($rule->labels)->toHaveCount(2);
expect($rule->labels->pluck('id')->toArray())->toContain($label1->id, $label2->id);
});
test('user can create an automation rule with category and labels', function () {
$user = User::factory()->create();
$category = Category::factory()->create(['user_id' => $user->id]);
$label = Label::factory()->create(['user_id' => $user->id]);
$response = $this->actingAs($user)
->from(route('automation-rules.index'))
->post(route('automation-rules.store'), [
'title' => 'Combined Category and Labels Rule',
'priority' => 5,
'rules_json' => json_encode(['>' => [['var' => 'amount'], 100]]),
'action_category_id' => $category->id,
'action_note' => null,
'action_note_iv' => null,
'action_label_ids' => [$label->id],
]);
$response->assertRedirect(route('automation-rules.index'));
$rule = AutomationRule::where('title', 'Combined Category and Labels Rule')->first();
expect($rule)->not->toBeNull();
expect($rule->action_category_id)->toBe($category->id);
expect($rule->labels)->toHaveCount(1);
expect($rule->labels->first()->id)->toBe($label->id);
});
test('user can update automation rule labels', function () {
$user = User::factory()->create();
$label1 = Label::factory()->create(['user_id' => $user->id]);
$label2 = Label::factory()->create(['user_id' => $user->id]);
$rule = AutomationRule::factory()->create(['user_id' => $user->id]);
$rule->labels()->attach($label1->id);
$response = $this->actingAs($user)
->from(route('automation-rules.index'))
->patch(route('automation-rules.update', $rule), [
'title' => $rule->title,
'priority' => $rule->priority,
'rules_json' => json_encode($rule->rules_json),
'action_category_id' => null,
'action_note' => null,
'action_note_iv' => null,
'action_label_ids' => [$label2->id],
]);
$response->assertRedirect(route('automation-rules.index'));
$rule->refresh();
expect($rule->labels)->toHaveCount(1);
expect($rule->labels->first()->id)->toBe($label2->id);
});
test('user cannot use another users label in rule', function () {
$user = User::factory()->create();
$otherUser = User::factory()->create();
$otherLabel = Label::factory()->create(['user_id' => $otherUser->id]);
$response = $this->actingAs($user)->post(route('automation-rules.store'), [
'title' => 'Invalid Label Rule',
'priority' => 0,
'rules_json' => json_encode(['==' => [1, 1]]),
'action_category_id' => null,
'action_note' => null,
'action_note_iv' => null,
'action_label_ids' => [$otherLabel->id],
]);
$response->assertSessionHasErrors('action_label_ids.0');
});
test('updating labels touches automation rule updated_at timestamp', function () {
$user = User::factory()->create();
$label1 = Label::factory()->create(['user_id' => $user->id]);
$label2 = Label::factory()->create(['user_id' => $user->id]);
$rule = AutomationRule::factory()->create(['user_id' => $user->id]);
$rule->labels()->attach($label1->id);
$originalUpdatedAt = $rule->updated_at;
$this->travel(2)->seconds();
$response = $this->actingAs($user)
->from(route('automation-rules.index'))
->patch(route('automation-rules.update', $rule), [
'title' => $rule->title,
'priority' => $rule->priority,
'rules_json' => json_encode($rule->rules_json),
'action_category_id' => $rule->action_category_id,
'action_note' => null,
'action_note_iv' => null,
'action_label_ids' => [$label2->id],
]);
$response->assertRedirect(route('automation-rules.index'));
$rule->refresh();
expect($rule->updated_at->isAfter($originalUpdatedAt))->toBeTrue();
});