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(); });