feat: complete Epic 0 — foundation migration & infrastructure setup
Stories 0.2-0.5: rename folders→declarations (backend+frontend), configure Redis for cache/queue/sessions, add foundation database migrations (permissions, archived_at), replace DeclarationStatus enum with architecture lifecycle values, create DeclarationObserver for status transition validation and auto-archive, fix controller status transitions to respect observer rules. 93 tests pass (240 assertions). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -81,4 +81,4 @@ test('users are rate limited', function () {
|
||||
]);
|
||||
|
||||
$response->assertTooManyRequests();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -92,4 +92,4 @@ test('already verified user visiting verification link is redirected without fir
|
||||
|
||||
Event::assertNotDispatched(Verified::class);
|
||||
expect($user->fresh()->hasVerifiedEmail())->toBeTrue();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,4 +19,4 @@ test('password confirmation requires authentication', function () {
|
||||
$response = $this->get(route('password.confirm'));
|
||||
|
||||
$response->assertRedirect(route('login'));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -70,4 +70,4 @@ test('password cannot be reset with invalid token', function () {
|
||||
]);
|
||||
|
||||
$response->assertSessionHasErrors('email');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,4 +16,4 @@ test('new users can register', function () {
|
||||
|
||||
$this->assertAuthenticated();
|
||||
$response->assertRedirect(route('dashboard', absolute: false));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -42,4 +42,4 @@ test('two factor challenge can be rendered', function () {
|
||||
->assertInertia(fn (Assert $page) => $page
|
||||
->component('auth/TwoFactorChallenge'),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -26,4 +26,4 @@ test('does not send verification notification if email is verified', function ()
|
||||
->assertRedirect(route('dashboard', absolute: false));
|
||||
|
||||
Notification::assertNothingSent();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Client;
|
||||
use App\Models\ClientContact;
|
||||
|
||||
test('primary_contact_email returns principal contact email', function () {
|
||||
$client = Client::factory()->create();
|
||||
|
||||
@@ -13,4 +13,4 @@ test('authenticated users can visit the dashboard', function () {
|
||||
|
||||
$response = $this->get(route('dashboard'));
|
||||
$response->assertOk();
|
||||
});
|
||||
});
|
||||
|
||||
11
tests/Feature/Database/FoundationMigrationsTest.php
Normal file
11
tests/Feature/Database/FoundationMigrationsTest.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
test('workspace_user table has permissions column', function () {
|
||||
expect(Schema::hasColumn('workspace_user', 'permissions'))->toBeTrue();
|
||||
});
|
||||
|
||||
test('declarations table has archived_at column', function () {
|
||||
expect(Schema::hasColumn('declarations', 'archived_at'))->toBeTrue();
|
||||
});
|
||||
@@ -36,8 +36,8 @@ test('composite index exists on messages declaration_id and created_at', functio
|
||||
});
|
||||
|
||||
test('migration is reversible and rollback restores folder tables', function () {
|
||||
// Rollback the rename migration (last one applied)
|
||||
$this->artisan('migrate:rollback', ['--step' => 1]);
|
||||
// Rollback foundation migrations (3) + polymorphic update + rename migration
|
||||
$this->artisan('migrate:rollback', ['--step' => 5]);
|
||||
|
||||
expect(Schema::hasTable('folders'))->toBeTrue();
|
||||
expect(Schema::hasTable('declarations'))->toBeFalse();
|
||||
|
||||
108
tests/Feature/Declaration/DeclarationStatusFlowTest.php
Normal file
108
tests/Feature/Declaration/DeclarationStatusFlowTest.php
Normal file
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
use App\Enums\DeclarationStatus;
|
||||
use App\Models\Declaration;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
test('valid transition: created to en_cours', function () {
|
||||
$declaration = Declaration::factory()->create(['status' => DeclarationStatus::Created]);
|
||||
|
||||
$declaration->update(['status' => DeclarationStatus::EnCours]);
|
||||
|
||||
expect($declaration->fresh()->status->value)->toBe('en_cours');
|
||||
});
|
||||
|
||||
test('valid transition: en_cours to en_attente_client', function () {
|
||||
$declaration = Declaration::factory()->create(['status' => DeclarationStatus::Created]);
|
||||
$declaration->update(['status' => DeclarationStatus::EnCours]);
|
||||
|
||||
$declaration->update(['status' => DeclarationStatus::EnAttenteClient]);
|
||||
|
||||
expect($declaration->fresh()->status->value)->toBe('en_attente_client');
|
||||
});
|
||||
|
||||
test('valid transition: en_attente_client to en_cours', function () {
|
||||
$declaration = Declaration::factory()->create(['status' => DeclarationStatus::Created]);
|
||||
$declaration->update(['status' => DeclarationStatus::EnCours]);
|
||||
$declaration->update(['status' => DeclarationStatus::EnAttenteClient]);
|
||||
|
||||
$declaration->update(['status' => DeclarationStatus::EnCours]);
|
||||
|
||||
expect($declaration->fresh()->status->value)->toBe('en_cours');
|
||||
});
|
||||
|
||||
test('valid transition: en_cours to termine', function () {
|
||||
$declaration = Declaration::factory()->create(['status' => DeclarationStatus::Created]);
|
||||
$declaration->update(['status' => DeclarationStatus::EnCours]);
|
||||
|
||||
$declaration->update(['status' => DeclarationStatus::Termine]);
|
||||
|
||||
expect($declaration->fresh()->status->value)->toBe('termine');
|
||||
});
|
||||
|
||||
test('valid transition: termine to ferme', function () {
|
||||
$declaration = Declaration::factory()->create(['status' => DeclarationStatus::Created]);
|
||||
$declaration->update(['status' => DeclarationStatus::EnCours]);
|
||||
$declaration->update(['status' => DeclarationStatus::Termine]);
|
||||
|
||||
$declaration->update(['status' => DeclarationStatus::Ferme]);
|
||||
|
||||
expect($declaration->fresh()->status->value)->toBe('ferme');
|
||||
});
|
||||
|
||||
test('invalid transition: created to ferme throws validation exception', function () {
|
||||
$declaration = Declaration::factory()->create(['status' => DeclarationStatus::Created]);
|
||||
|
||||
$declaration->update(['status' => DeclarationStatus::Ferme]);
|
||||
})->throws(ValidationException::class);
|
||||
|
||||
test('invalid transition: created to termine throws validation exception', function () {
|
||||
$declaration = Declaration::factory()->create(['status' => DeclarationStatus::Created]);
|
||||
|
||||
$declaration->update(['status' => DeclarationStatus::Termine]);
|
||||
})->throws(ValidationException::class);
|
||||
|
||||
test('auto-archive: ferme status sets archived_at', function () {
|
||||
$declaration = Declaration::factory()->create(['status' => DeclarationStatus::Created]);
|
||||
$declaration->update(['status' => DeclarationStatus::EnCours]);
|
||||
$declaration->update(['status' => DeclarationStatus::Termine]);
|
||||
|
||||
expect($declaration->fresh()->archived_at)->toBeNull();
|
||||
|
||||
$declaration->update(['status' => DeclarationStatus::Ferme]);
|
||||
|
||||
$fresh = $declaration->fresh();
|
||||
expect($fresh->archived_at)->not->toBeNull();
|
||||
});
|
||||
|
||||
test('scope active excludes archived declarations', function () {
|
||||
$active = Declaration::factory()->create([
|
||||
'status' => DeclarationStatus::Created,
|
||||
'archived_at' => null,
|
||||
]);
|
||||
$archived = Declaration::factory()->create([
|
||||
'status' => DeclarationStatus::Created,
|
||||
'archived_at' => now(),
|
||||
]);
|
||||
|
||||
$activeIds = Declaration::active()->pluck('id')->all();
|
||||
|
||||
expect($activeIds)->toContain($active->id);
|
||||
expect($activeIds)->not->toContain($archived->id);
|
||||
});
|
||||
|
||||
test('scope archived includes only archived declarations', function () {
|
||||
$active = Declaration::factory()->create([
|
||||
'status' => DeclarationStatus::Created,
|
||||
'archived_at' => null,
|
||||
]);
|
||||
$archived = Declaration::factory()->create([
|
||||
'status' => DeclarationStatus::Created,
|
||||
'archived_at' => now(),
|
||||
]);
|
||||
|
||||
$archivedIds = Declaration::archived()->pluck('id')->all();
|
||||
|
||||
expect($archivedIds)->toContain($archived->id);
|
||||
expect($archivedIds)->not->toContain($active->id);
|
||||
});
|
||||
@@ -4,28 +4,28 @@ use App\Models\Client;
|
||||
use App\Models\User;
|
||||
use App\Models\Workspace;
|
||||
|
||||
test('can create vat_monthly folder requiring month', function () {
|
||||
test('can create vat_monthly declaration requiring month', function () {
|
||||
$user = User::factory()->create();
|
||||
$workspace = Workspace::factory()->create();
|
||||
$workspace->users()->attach($user, ['role' => 'owner']);
|
||||
$client = Client::factory()->create(['workspace_id' => $workspace->id]);
|
||||
session(['current_workspace_id' => $workspace->id]);
|
||||
|
||||
$response = $this->actingAs($user)->post(route('folders.store'), [
|
||||
$response = $this->actingAs($user)->post(route('declarations.store'), [
|
||||
'client_id' => $client->id,
|
||||
'title' => 'TVA Mensuelle Mars 2026',
|
||||
'type' => 'vat_monthly',
|
||||
'period_year' => 2026,
|
||||
'period_month' => 3,
|
||||
'status' => 'draft',
|
||||
'status' => 'created',
|
||||
'priority' => 'medium',
|
||||
]);
|
||||
|
||||
$response->assertRedirect();
|
||||
$folder = $client->folders()->where('type', 'vat_monthly')->first();
|
||||
expect($folder)->not->toBeNull();
|
||||
expect($folder->period_month)->toBe(3);
|
||||
expect($folder->period_quarter)->toBeNull();
|
||||
$declaration = $client->declarations()->where('type', 'vat_monthly')->first();
|
||||
expect($declaration)->not->toBeNull();
|
||||
expect($declaration->period_month)->toBe(3);
|
||||
expect($declaration->period_quarter)->toBeNull();
|
||||
});
|
||||
|
||||
test('vat_monthly validation fails without month', function () {
|
||||
@@ -35,40 +35,40 @@ test('vat_monthly validation fails without month', function () {
|
||||
$client = Client::factory()->create(['workspace_id' => $workspace->id]);
|
||||
session(['current_workspace_id' => $workspace->id]);
|
||||
|
||||
$response = $this->actingAs($user)->post(route('folders.store'), [
|
||||
$response = $this->actingAs($user)->post(route('declarations.store'), [
|
||||
'client_id' => $client->id,
|
||||
'title' => 'TVA Mensuelle Sans Mois',
|
||||
'type' => 'vat_monthly',
|
||||
'period_year' => 2026,
|
||||
'status' => 'draft',
|
||||
'status' => 'created',
|
||||
'priority' => 'medium',
|
||||
]);
|
||||
|
||||
$response->assertSessionHasErrors('period_month');
|
||||
});
|
||||
|
||||
test('can create vat_quarterly folder requiring quarter', function () {
|
||||
test('can create vat_quarterly declaration requiring quarter', function () {
|
||||
$user = User::factory()->create();
|
||||
$workspace = Workspace::factory()->create();
|
||||
$workspace->users()->attach($user, ['role' => 'owner']);
|
||||
$client = Client::factory()->create(['workspace_id' => $workspace->id]);
|
||||
session(['current_workspace_id' => $workspace->id]);
|
||||
|
||||
$response = $this->actingAs($user)->post(route('folders.store'), [
|
||||
$response = $this->actingAs($user)->post(route('declarations.store'), [
|
||||
'client_id' => $client->id,
|
||||
'title' => 'TVA Trimestrielle T1 2026',
|
||||
'type' => 'vat_quarterly',
|
||||
'period_year' => 2026,
|
||||
'period_quarter' => 1,
|
||||
'status' => 'draft',
|
||||
'status' => 'created',
|
||||
'priority' => 'medium',
|
||||
]);
|
||||
|
||||
$response->assertRedirect();
|
||||
$folder = $client->folders()->where('type', 'vat_quarterly')->first();
|
||||
expect($folder)->not->toBeNull();
|
||||
expect($folder->period_quarter)->toBe(1);
|
||||
expect($folder->period_month)->toBeNull();
|
||||
$declaration = $client->declarations()->where('type', 'vat_quarterly')->first();
|
||||
expect($declaration)->not->toBeNull();
|
||||
expect($declaration->period_quarter)->toBe(1);
|
||||
expect($declaration->period_month)->toBeNull();
|
||||
});
|
||||
|
||||
test('vat_quarterly validation fails without quarter', function () {
|
||||
@@ -78,12 +78,12 @@ test('vat_quarterly validation fails without quarter', function () {
|
||||
$client = Client::factory()->create(['workspace_id' => $workspace->id]);
|
||||
session(['current_workspace_id' => $workspace->id]);
|
||||
|
||||
$response = $this->actingAs($user)->post(route('folders.store'), [
|
||||
$response = $this->actingAs($user)->post(route('declarations.store'), [
|
||||
'client_id' => $client->id,
|
||||
'title' => 'TVA Trimestrielle Sans Trimestre',
|
||||
'type' => 'vat_quarterly',
|
||||
'period_year' => 2026,
|
||||
'status' => 'draft',
|
||||
'status' => 'created',
|
||||
'priority' => 'medium',
|
||||
]);
|
||||
|
||||
@@ -97,12 +97,12 @@ test('server rejects old vat type', function () {
|
||||
$client = Client::factory()->create(['workspace_id' => $workspace->id]);
|
||||
session(['current_workspace_id' => $workspace->id]);
|
||||
|
||||
$response = $this->actingAs($user)->post(route('folders.store'), [
|
||||
$response = $this->actingAs($user)->post(route('declarations.store'), [
|
||||
'client_id' => $client->id,
|
||||
'title' => 'Old VAT',
|
||||
'type' => 'vat',
|
||||
'period_year' => 2026,
|
||||
'status' => 'draft',
|
||||
'status' => 'created',
|
||||
'priority' => 'medium',
|
||||
]);
|
||||
|
||||
@@ -116,19 +116,19 @@ test('vat_monthly nulls quarter field server-side', function () {
|
||||
$client = Client::factory()->create(['workspace_id' => $workspace->id]);
|
||||
session(['current_workspace_id' => $workspace->id]);
|
||||
|
||||
$response = $this->actingAs($user)->post(route('folders.store'), [
|
||||
$response = $this->actingAs($user)->post(route('declarations.store'), [
|
||||
'client_id' => $client->id,
|
||||
'title' => 'TVA Mensuelle With Quarter',
|
||||
'type' => 'vat_monthly',
|
||||
'period_year' => 2026,
|
||||
'period_month' => 6,
|
||||
'period_quarter' => 2,
|
||||
'status' => 'draft',
|
||||
'status' => 'created',
|
||||
'priority' => 'medium',
|
||||
]);
|
||||
|
||||
$response->assertRedirect();
|
||||
$folder = $client->folders()->where('title', 'TVA Mensuelle With Quarter')->first();
|
||||
expect($folder->period_quarter)->toBeNull();
|
||||
expect($folder->period_month)->toBe(6);
|
||||
$declaration = $client->declarations()->where('title', 'TVA Mensuelle With Quarter')->first();
|
||||
expect($declaration->period_quarter)->toBeNull();
|
||||
expect($declaration->period_month)->toBe(6);
|
||||
});
|
||||
@@ -1,37 +1,37 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Client;
|
||||
use App\Models\Folder;
|
||||
use App\Models\Declaration;
|
||||
use App\Models\MediaDownload;
|
||||
use App\Models\User;
|
||||
use App\Models\Workspace;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
function setupFolderWithMedia(): array
|
||||
function setupDeclarationWithMedia(): array
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$workspace = Workspace::factory()->create();
|
||||
$workspace->users()->attach($user, ['role' => 'owner']);
|
||||
$client = Client::factory()->create(['workspace_id' => $workspace->id]);
|
||||
$folder = Folder::factory()->create([
|
||||
$declaration = Declaration::factory()->create([
|
||||
'workspace_id' => $workspace->id,
|
||||
'client_id' => $client->id,
|
||||
]);
|
||||
|
||||
Storage::fake('public');
|
||||
$file = UploadedFile::fake()->create('document.pdf', 100, 'application/pdf');
|
||||
$media = $folder->addMedia($file)->toMediaCollection('documents');
|
||||
$media = $declaration->addMedia($file)->toMediaCollection('documents');
|
||||
|
||||
return [$user, $workspace, $folder, $media];
|
||||
return [$user, $workspace, $declaration, $media];
|
||||
}
|
||||
|
||||
test('downloading creates a media download record', function () {
|
||||
[$user, $workspace, $folder, $media] = setupFolderWithMedia();
|
||||
[$user, $workspace, $declaration, $media] = setupDeclarationWithMedia();
|
||||
session(['current_workspace_id' => $workspace->id]);
|
||||
|
||||
$this->actingAs($user)->get(route('folders.media.download', [
|
||||
'folder' => $folder,
|
||||
$this->actingAs($user)->get(route('declarations.media.download', [
|
||||
'declaration' => $declaration,
|
||||
'mediaId' => $media->id,
|
||||
]));
|
||||
|
||||
@@ -45,11 +45,11 @@ test('downloading creates a media download record', function () {
|
||||
});
|
||||
|
||||
test('re-downloading updates timestamp without creating duplicates', function () {
|
||||
[$user, $workspace, $folder, $media] = setupFolderWithMedia();
|
||||
[$user, $workspace, $declaration, $media] = setupDeclarationWithMedia();
|
||||
session(['current_workspace_id' => $workspace->id]);
|
||||
|
||||
$this->actingAs($user)->get(route('folders.media.download', [
|
||||
'folder' => $folder,
|
||||
$this->actingAs($user)->get(route('declarations.media.download', [
|
||||
'declaration' => $declaration,
|
||||
'mediaId' => $media->id,
|
||||
]));
|
||||
|
||||
@@ -61,8 +61,8 @@ test('re-downloading updates timestamp without creating duplicates', function ()
|
||||
|
||||
$this->travel(5)->minutes();
|
||||
|
||||
$this->actingAs($user)->get(route('folders.media.download', [
|
||||
'folder' => $folder,
|
||||
$this->actingAs($user)->get(route('declarations.media.download', [
|
||||
'declaration' => $declaration,
|
||||
'mediaId' => $media->id,
|
||||
]));
|
||||
|
||||
@@ -78,7 +78,7 @@ test('re-downloading updates timestamp without creating duplicates', function ()
|
||||
});
|
||||
|
||||
test('download status is per-user in show endpoint', function () {
|
||||
[$user, $workspace, $folder, $media] = setupFolderWithMedia();
|
||||
[$user, $workspace, $declaration, $media] = setupDeclarationWithMedia();
|
||||
$otherUser = User::factory()->create();
|
||||
$workspace->users()->attach($otherUser, ['role' => 'member']);
|
||||
session(['current_workspace_id' => $workspace->id]);
|
||||
@@ -89,13 +89,13 @@ test('download status is per-user in show endpoint', function () {
|
||||
'downloaded_at' => now(),
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($user)->get(route('folders.show', $folder));
|
||||
$response = $this->actingAs($user)->get(route('declarations.show', $declaration));
|
||||
$response->assertOk();
|
||||
$documents = $response->original->getData()['page']['props']['documents'];
|
||||
$doc = collect($documents)->firstWhere('id', $media->id);
|
||||
expect($doc['is_downloaded'])->toBeTrue();
|
||||
|
||||
$response2 = $this->actingAs($otherUser)->get(route('folders.show', $folder));
|
||||
$response2 = $this->actingAs($otherUser)->get(route('declarations.show', $declaration));
|
||||
$response2->assertOk();
|
||||
$documents2 = $response2->original->getData()['page']['props']['documents'];
|
||||
$doc2 = collect($documents2)->firstWhere('id', $media->id);
|
||||
@@ -4,4 +4,4 @@ test('returns a successful response', function () {
|
||||
$response = $this->get(route('home'));
|
||||
|
||||
$response->assertOk();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Client;
|
||||
use App\Models\Folder;
|
||||
use App\Models\Declaration;
|
||||
use App\Models\User;
|
||||
use App\Models\Workspace;
|
||||
use App\Notifications\FolderMentionNotification;
|
||||
use App\Notifications\DeclarationMentionNotification;
|
||||
use Illuminate\Support\Facades\Notification;
|
||||
|
||||
function setupMentionScenario(string $role = 'owner'): array
|
||||
@@ -16,50 +16,50 @@ function setupMentionScenario(string $role = 'owner'): array
|
||||
$workspace->users()->attach($target, ['role' => 'member']);
|
||||
|
||||
$client = Client::factory()->create(['workspace_id' => $workspace->id]);
|
||||
$folder = Folder::factory()->create([
|
||||
$declaration = Declaration::factory()->create([
|
||||
'workspace_id' => $workspace->id,
|
||||
'client_id' => $client->id,
|
||||
]);
|
||||
|
||||
return [$sender, $target, $workspace, $folder];
|
||||
return [$sender, $target, $workspace, $declaration];
|
||||
}
|
||||
|
||||
test('owner can mention a workspace user', function () {
|
||||
Notification::fake();
|
||||
[$sender, $target, $workspace, $folder] = setupMentionScenario('owner');
|
||||
[$sender, $target, $workspace, $declaration] = setupMentionScenario('owner');
|
||||
session(['current_workspace_id' => $workspace->id]);
|
||||
|
||||
$response = $this->actingAs($sender)->post(route('folders.mentions.store', $folder), [
|
||||
$response = $this->actingAs($sender)->post(route('declarations.mentions.store', $declaration), [
|
||||
'user_id' => $target->id,
|
||||
'message' => 'Please check this folder.',
|
||||
'message' => 'Please check this declaration.',
|
||||
]);
|
||||
|
||||
$response->assertRedirect();
|
||||
Notification::assertSentTo($target, FolderMentionNotification::class);
|
||||
Notification::assertSentTo($target, DeclarationMentionNotification::class);
|
||||
});
|
||||
|
||||
test('manager can mention a workspace user', function () {
|
||||
Notification::fake();
|
||||
[$sender, $target, $workspace, $folder] = setupMentionScenario('manager');
|
||||
[$sender, $target, $workspace, $declaration] = setupMentionScenario('manager');
|
||||
session(['current_workspace_id' => $workspace->id]);
|
||||
|
||||
$response = $this->actingAs($sender)->post(route('folders.mentions.store', $folder), [
|
||||
$response = $this->actingAs($sender)->post(route('declarations.mentions.store', $declaration), [
|
||||
'user_id' => $target->id,
|
||||
'message' => 'Please check this folder.',
|
||||
'message' => 'Please check this declaration.',
|
||||
]);
|
||||
|
||||
$response->assertRedirect();
|
||||
Notification::assertSentTo($target, FolderMentionNotification::class);
|
||||
Notification::assertSentTo($target, DeclarationMentionNotification::class);
|
||||
});
|
||||
|
||||
test('member cannot mention a workspace user', function () {
|
||||
Notification::fake();
|
||||
[$sender, $target, $workspace, $folder] = setupMentionScenario('member');
|
||||
[$sender, $target, $workspace, $declaration] = setupMentionScenario('member');
|
||||
session(['current_workspace_id' => $workspace->id]);
|
||||
|
||||
$response = $this->actingAs($sender)->post(route('folders.mentions.store', $folder), [
|
||||
$response = $this->actingAs($sender)->post(route('declarations.mentions.store', $declaration), [
|
||||
'user_id' => $target->id,
|
||||
'message' => 'Please check this folder.',
|
||||
'message' => 'Please check this declaration.',
|
||||
]);
|
||||
|
||||
$response->assertForbidden();
|
||||
@@ -68,11 +68,11 @@ test('member cannot mention a workspace user', function () {
|
||||
|
||||
test('cannot mention user from another workspace', function () {
|
||||
Notification::fake();
|
||||
[$sender, , $workspace, $folder] = setupMentionScenario('owner');
|
||||
[$sender, , $workspace, $declaration] = setupMentionScenario('owner');
|
||||
$outsider = User::factory()->create();
|
||||
session(['current_workspace_id' => $workspace->id]);
|
||||
|
||||
$response = $this->actingAs($sender)->post(route('folders.mentions.store', $folder), [
|
||||
$response = $this->actingAs($sender)->post(route('declarations.mentions.store', $declaration), [
|
||||
'user_id' => $outsider->id,
|
||||
'message' => 'Hello',
|
||||
]);
|
||||
@@ -82,16 +82,16 @@ test('cannot mention user from another workspace', function () {
|
||||
});
|
||||
|
||||
test('notification is persisted in database', function () {
|
||||
[$sender, $target, $workspace, $folder] = setupMentionScenario('owner');
|
||||
[$sender, $target, $workspace, $declaration] = setupMentionScenario('owner');
|
||||
session(['current_workspace_id' => $workspace->id]);
|
||||
|
||||
$this->actingAs($sender)->post(route('folders.mentions.store', $folder), [
|
||||
$this->actingAs($sender)->post(route('declarations.mentions.store', $declaration), [
|
||||
'user_id' => $target->id,
|
||||
'message' => 'Check this.',
|
||||
]);
|
||||
|
||||
expect($target->notifications()->count())->toBe(1);
|
||||
$notif = $target->notifications()->first();
|
||||
expect($notif->data['folder_id'])->toBe($folder->id);
|
||||
expect($notif->data['declaration_id'])->toBe($declaration->id);
|
||||
expect($notif->data['message'])->toBe('Check this.');
|
||||
});
|
||||
@@ -1,17 +1,26 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Client;
|
||||
use App\Models\Declaration;
|
||||
use App\Models\User;
|
||||
use App\Notifications\FolderMentionNotification;
|
||||
use App\Models\Workspace;
|
||||
use App\Notifications\DeclarationMentionNotification;
|
||||
|
||||
test('user can mark own notification as read', function () {
|
||||
$user = User::factory()->create();
|
||||
$user->notify(new FolderMentionNotification(
|
||||
folderId: 1,
|
||||
folderTitle: 'Test Folder',
|
||||
mentionedById: 999,
|
||||
mentionedByName: 'Admin',
|
||||
message: 'Please review.',
|
||||
url: '/folders/1',
|
||||
$workspace = Workspace::factory()->create();
|
||||
$workspace->users()->attach($user, ['role' => 'owner']);
|
||||
$client = Client::factory()->create(['workspace_id' => $workspace->id]);
|
||||
$declaration = Declaration::factory()->create([
|
||||
'workspace_id' => $workspace->id,
|
||||
'client_id' => $client->id,
|
||||
]);
|
||||
$mentionedBy = User::factory()->create();
|
||||
|
||||
$user->notify(new DeclarationMentionNotification(
|
||||
$declaration,
|
||||
$mentionedBy,
|
||||
'Please review.',
|
||||
));
|
||||
|
||||
$notification = $user->notifications()->first();
|
||||
@@ -27,13 +36,19 @@ test('user can mark own notification as read', function () {
|
||||
test('cannot mark another user notification as read', function () {
|
||||
$user = User::factory()->create();
|
||||
$other = User::factory()->create();
|
||||
$other->notify(new FolderMentionNotification(
|
||||
folderId: 1,
|
||||
folderTitle: 'Test',
|
||||
mentionedById: 999,
|
||||
mentionedByName: 'Admin',
|
||||
message: 'Hey.',
|
||||
url: '/folders/1',
|
||||
$workspace = Workspace::factory()->create();
|
||||
$workspace->users()->attach($other, ['role' => 'owner']);
|
||||
$client = Client::factory()->create(['workspace_id' => $workspace->id]);
|
||||
$declaration = Declaration::factory()->create([
|
||||
'workspace_id' => $workspace->id,
|
||||
'client_id' => $client->id,
|
||||
]);
|
||||
$mentionedBy = User::factory()->create();
|
||||
|
||||
$other->notify(new DeclarationMentionNotification(
|
||||
$declaration,
|
||||
$mentionedBy,
|
||||
'Hey.',
|
||||
));
|
||||
|
||||
$notification = $other->notifications()->first();
|
||||
@@ -44,15 +59,20 @@ test('cannot mark another user notification as read', function () {
|
||||
|
||||
test('user can mark all notifications as read', function () {
|
||||
$user = User::factory()->create();
|
||||
$workspace = Workspace::factory()->create();
|
||||
$workspace->users()->attach($user, ['role' => 'owner']);
|
||||
$client = Client::factory()->create(['workspace_id' => $workspace->id]);
|
||||
$mentionedBy = User::factory()->create();
|
||||
|
||||
for ($i = 0; $i < 3; $i++) {
|
||||
$user->notify(new FolderMentionNotification(
|
||||
folderId: $i,
|
||||
folderTitle: "Folder $i",
|
||||
mentionedById: 999,
|
||||
mentionedByName: 'Admin',
|
||||
message: "Message $i",
|
||||
url: "/folders/$i",
|
||||
$declaration = Declaration::factory()->create([
|
||||
'workspace_id' => $workspace->id,
|
||||
'client_id' => $client->id,
|
||||
]);
|
||||
$user->notify(new DeclarationMentionNotification(
|
||||
$declaration,
|
||||
$mentionedBy,
|
||||
"Message $i",
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
35
tests/Feature/RedisConnectivityTest.php
Normal file
35
tests/Feature/RedisConnectivityTest.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
test('redis cache driver can store and retrieve values', function () {
|
||||
Cache::store('redis')->put('test-key', 'test-value', 60);
|
||||
|
||||
expect(Cache::store('redis')->get('test-key'))->toBe('test-value');
|
||||
|
||||
Cache::store('redis')->forget('test-key');
|
||||
});
|
||||
|
||||
test('redis queue connection is configured in env', function () {
|
||||
$env = collect(file(base_path('.env'), FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES))
|
||||
->filter(fn ($line) => ! str_starts_with(trim($line), '#'))
|
||||
->mapWithKeys(function ($line) {
|
||||
[$key, $value] = explode('=', $line, 2);
|
||||
|
||||
return [trim($key) => trim($value)];
|
||||
});
|
||||
|
||||
expect($env->get('QUEUE_CONNECTION'))->toBe('redis');
|
||||
});
|
||||
|
||||
test('redis session driver is configured in env', function () {
|
||||
$env = collect(file(base_path('.env'), FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES))
|
||||
->filter(fn ($line) => ! str_starts_with(trim($line), '#'))
|
||||
->mapWithKeys(function ($line) {
|
||||
[$key, $value] = explode('=', $line, 2);
|
||||
|
||||
return [trim($key) => trim($value)];
|
||||
});
|
||||
|
||||
expect($env->get('SESSION_DRIVER'))->toBe('redis');
|
||||
});
|
||||
@@ -47,4 +47,4 @@ test('correct password must be provided to update password', function () {
|
||||
$response
|
||||
->assertSessionHasErrors('current_password')
|
||||
->assertRedirect(route('user-password.edit'));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -64,7 +64,7 @@ test('user can delete their account', function () {
|
||||
->assertRedirect(route('home'));
|
||||
|
||||
$this->assertGuest();
|
||||
expect($user->fresh())->toBeNull();
|
||||
expect($user->fresh()->deleted_at)->not->toBeNull();
|
||||
});
|
||||
|
||||
test('correct password must be provided to delete account', function () {
|
||||
@@ -82,4 +82,4 @@ test('correct password must be provided to delete account', function () {
|
||||
->assertRedirect(route('profile.edit'));
|
||||
|
||||
expect($user->fresh())->not->toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -76,4 +76,4 @@ test('two factor settings page returns forbidden response when two factor is dis
|
||||
->withSession(['auth.password_confirmed_at' => time()])
|
||||
->get(route('two-factor.show'))
|
||||
->assertForbidden();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
|
||||
test('that true is true', function () {
|
||||
expect(true)->toBeTrue();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user