Deploy HickLab3D Dashboard / Deploy to production (push) Failing after 23s
Replace the old touchmark/PNG identity with the supplied lockup and monogram, segmented role-aware navigation, Live/Draft listing filters with generation-based cache invalidation, and admin Projects covering immutable file revisions, listing live pointers, and exact-decimal filament/packaging/shipping costing. Additive schema only; no Etsy publication. Isolated clone tests: 301 tests, 1162 assertions.
244 lines
12 KiB
PHP
244 lines
12 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature;
|
|
|
|
use App\Models\Project;
|
|
use App\Models\ProjectCostSnapshot;
|
|
use App\Models\ProjectFile;
|
|
use App\Models\ProjectListingLink;
|
|
use App\Models\ProjectRelease;
|
|
use App\Models\ProjectRevision;
|
|
use App\Models\User;
|
|
use Illuminate\Foundation\Testing\LazilyRefreshDatabase;
|
|
use Illuminate\Support\Facades\Schema;
|
|
use Tests\TestCase;
|
|
|
|
class ProjectReleaseTest extends TestCase
|
|
{
|
|
use LazilyRefreshDatabase;
|
|
|
|
private function approvedRevisionWithCost(Project $project, int $revisionNumber = 1): array
|
|
{
|
|
$revision = ProjectRevision::factory()->approved()->for($project)->create(['revision_number' => $revisionNumber]);
|
|
ProjectFile::factory()->for($revision, 'revision')->create();
|
|
$snapshot = ProjectCostSnapshot::factory()->for($project)->for($revision, 'revision')->create([
|
|
'grand_total' => '10.0000', 'filament_total' => '10.0000',
|
|
'packaging_total' => '0.0000', 'shipping_total' => '0.0000',
|
|
]);
|
|
|
|
return [$revision, $snapshot];
|
|
}
|
|
|
|
public function test_admin_can_release_an_approved_revision_to_a_listing(): void
|
|
{
|
|
$admin = User::factory()->create(['role' => 'admin']);
|
|
$project = Project::factory()->create();
|
|
[$revision, $snapshot] = $this->approvedRevisionWithCost($project);
|
|
$link = ProjectListingLink::factory()->for($project)->create(['lock_version' => 0, 'live_release_id' => null]);
|
|
|
|
$response = $this->actingAs($admin)->post(route('projects.listings.release', [$project, $link]), [
|
|
'revision_id' => $revision->id, 'cost_snapshot_id' => $snapshot->id, 'lock_version' => 0,
|
|
]);
|
|
|
|
$response->assertRedirect();
|
|
$link->refresh();
|
|
$this->assertNotNull($link->live_release_id);
|
|
$this->assertSame(1, $link->lock_version);
|
|
$this->assertDatabaseHas('project_releases', ['project_id' => $project->id, 'revision_id' => $revision->id, 'action' => 'release']);
|
|
}
|
|
|
|
public function test_uploading_v2_does_not_move_the_live_pointer(): void
|
|
{
|
|
$admin = User::factory()->create(['role' => 'admin']);
|
|
$project = Project::factory()->create();
|
|
[$v1, $snapshot1] = $this->approvedRevisionWithCost($project, 1);
|
|
$link = ProjectListingLink::factory()->for($project)->create(['lock_version' => 0, 'live_release_id' => null]);
|
|
$this->actingAs($admin)->post(route('projects.listings.release', [$project, $link]), [
|
|
'revision_id' => $v1->id, 'cost_snapshot_id' => $snapshot1->id, 'lock_version' => 0,
|
|
]);
|
|
$link->refresh();
|
|
$firstReleaseId = $link->live_release_id;
|
|
|
|
[$v2, $snapshot2] = $this->approvedRevisionWithCost($project, 2);
|
|
|
|
$link->refresh();
|
|
$this->assertSame($firstReleaseId, $link->live_release_id);
|
|
}
|
|
|
|
public function test_releasing_v2_then_reverting_restores_v1_with_new_history_entry(): void
|
|
{
|
|
$admin = User::factory()->create(['role' => 'admin']);
|
|
$project = Project::factory()->create();
|
|
[$v1, $snapshot1] = $this->approvedRevisionWithCost($project, 1);
|
|
$link = ProjectListingLink::factory()->for($project)->create(['lock_version' => 0, 'live_release_id' => null]);
|
|
$this->actingAs($admin)->post(route('projects.listings.release', [$project, $link]), [
|
|
'revision_id' => $v1->id, 'cost_snapshot_id' => $snapshot1->id, 'lock_version' => 0,
|
|
]);
|
|
$link->refresh();
|
|
$firstRelease = $link->live_release_id;
|
|
|
|
[$v2, $snapshot2] = $this->approvedRevisionWithCost($project, 2);
|
|
$this->actingAs($admin)->post(route('projects.listings.release', [$project, $link]), [
|
|
'revision_id' => $v2->id, 'cost_snapshot_id' => $snapshot2->id, 'lock_version' => $link->lock_version,
|
|
]);
|
|
$link->refresh();
|
|
|
|
$this->actingAs($admin)->post(route('projects.listings.revert', [$project, $link]), [
|
|
'release_id' => $firstRelease, 'lock_version' => $link->lock_version,
|
|
])->assertRedirect();
|
|
|
|
$link->refresh();
|
|
$revertRelease = ProjectRelease::find($link->live_release_id);
|
|
$this->assertSame('revert', $revertRelease->action);
|
|
$this->assertSame($v1->id, $revertRelease->revision_id);
|
|
$this->assertDatabaseCount('project_releases', 3);
|
|
}
|
|
|
|
public function test_optimistic_concurrency_rejects_stale_lock_version(): void
|
|
{
|
|
$admin = User::factory()->create(['role' => 'admin']);
|
|
$project = Project::factory()->create();
|
|
[$v1, $snapshot1] = $this->approvedRevisionWithCost($project, 1);
|
|
$link = ProjectListingLink::factory()->for($project)->create(['lock_version' => 0, 'live_release_id' => null]);
|
|
$this->actingAs($admin)->post(route('projects.listings.release', [$project, $link]), [
|
|
'revision_id' => $v1->id, 'cost_snapshot_id' => $snapshot1->id, 'lock_version' => 0,
|
|
]);
|
|
|
|
[$v2, $snapshot2] = $this->approvedRevisionWithCost($project, 2);
|
|
$response = $this->actingAs($admin)->post(route('projects.listings.release', [$project, $link]), [
|
|
'revision_id' => $v2->id, 'cost_snapshot_id' => $snapshot2->id, 'lock_version' => 0,
|
|
]);
|
|
|
|
$response->assertStatus(409);
|
|
}
|
|
|
|
public function test_cannot_release_a_revision_from_a_different_project(): void
|
|
{
|
|
$admin = User::factory()->create(['role' => 'admin']);
|
|
$projectA = Project::factory()->create();
|
|
$projectB = Project::factory()->create();
|
|
[$revisionOfB] = $this->approvedRevisionWithCost($projectB);
|
|
$link = ProjectListingLink::factory()->for($projectA)->create(['lock_version' => 0]);
|
|
|
|
$this->actingAs($admin)->post(route('projects.listings.release', [$projectA, $link]), [
|
|
'revision_id' => $revisionOfB->id, 'cost_snapshot_id' => 999999, 'lock_version' => 0,
|
|
])->assertStatus(422);
|
|
}
|
|
|
|
public function test_cannot_release_an_unapproved_revision(): void
|
|
{
|
|
$admin = User::factory()->create(['role' => 'admin']);
|
|
$project = Project::factory()->create();
|
|
$draft = ProjectRevision::factory()->for($project)->create(['status' => 'draft']);
|
|
$link = ProjectListingLink::factory()->for($project)->create(['lock_version' => 0]);
|
|
|
|
$this->actingAs($admin)->post(route('projects.listings.release', [$project, $link]), [
|
|
'revision_id' => $draft->id, 'cost_snapshot_id' => 999999, 'lock_version' => 0,
|
|
])->assertStatus(422);
|
|
}
|
|
|
|
public function test_cannot_release_to_a_listing_link_from_another_project(): void
|
|
{
|
|
$admin = User::factory()->create(['role' => 'admin']);
|
|
$projectA = Project::factory()->create();
|
|
$projectB = Project::factory()->create();
|
|
[$revisionOfA] = $this->approvedRevisionWithCost($projectA);
|
|
$linkOfB = ProjectListingLink::factory()->for($projectB)->create(['lock_version' => 0]);
|
|
|
|
$this->actingAs($admin)->post(route('projects.listings.release', [$projectA, $linkOfB]), [
|
|
'revision_id' => $revisionOfA->id, 'lock_version' => 0,
|
|
])->assertNotFound();
|
|
}
|
|
|
|
public function test_non_admin_cannot_release(): void
|
|
{
|
|
$project = Project::factory()->create();
|
|
$link = ProjectListingLink::factory()->for($project)->create(['lock_version' => 0]);
|
|
|
|
$this->actingAs(User::factory()->create())->post(route('projects.listings.release', [$project, $link]), [
|
|
'revision_id' => 1, 'lock_version' => 0,
|
|
])->assertForbidden();
|
|
}
|
|
|
|
public function test_release_requires_a_cost_snapshot(): void
|
|
{
|
|
$admin = User::factory()->create(['role' => 'admin']);
|
|
$project = Project::factory()->create();
|
|
$revision = ProjectRevision::factory()->approved()->for($project)->create(['revision_number' => 1]);
|
|
ProjectFile::factory()->for($revision, 'revision')->create();
|
|
$link = ProjectListingLink::factory()->for($project)->create(['lock_version' => 0]);
|
|
|
|
$this->actingAs($admin)->post(route('projects.listings.release', [$project, $link]), [
|
|
'revision_id' => $revision->id, 'lock_version' => 0,
|
|
])->assertSessionHasErrors('cost_snapshot_id');
|
|
$this->assertDatabaseCount('project_releases', 0);
|
|
}
|
|
|
|
public function test_release_rejects_an_incomplete_cost_snapshot(): void
|
|
{
|
|
$admin = User::factory()->create(['role' => 'admin']);
|
|
$project = Project::factory()->create();
|
|
$revision = ProjectRevision::factory()->approved()->for($project)->create(['revision_number' => 1]);
|
|
ProjectFile::factory()->for($revision, 'revision')->create();
|
|
$incomplete = ProjectCostSnapshot::factory()->for($project)->for($revision, 'revision')->create([
|
|
'grand_total' => '10.0000', 'filament_total' => '10.0000',
|
|
'packaging_total' => null, 'shipping_total' => null,
|
|
]);
|
|
$link = ProjectListingLink::factory()->for($project)->create(['lock_version' => 0]);
|
|
|
|
$this->actingAs($admin)->post(route('projects.listings.release', [$project, $link]), [
|
|
'revision_id' => $revision->id, 'cost_snapshot_id' => $incomplete->id, 'lock_version' => 0,
|
|
])->assertSessionHasErrors('cost_snapshot_id');
|
|
$this->assertDatabaseCount('project_releases', 0);
|
|
}
|
|
|
|
public function test_release_rejects_a_detached_listing_link(): void
|
|
{
|
|
$admin = User::factory()->create(['role' => 'admin']);
|
|
$project = Project::factory()->create();
|
|
[$revision, $snapshot] = $this->approvedRevisionWithCost($project);
|
|
$link = ProjectListingLink::factory()->for($project)->create(['lock_version' => 0, 'detached_at' => now()]);
|
|
|
|
$this->actingAs($admin)->post(route('projects.listings.release', [$project, $link]), [
|
|
'revision_id' => $revision->id, 'cost_snapshot_id' => $snapshot->id, 'lock_version' => 0,
|
|
])->assertSessionHasErrors('listing_link');
|
|
$this->assertDatabaseCount('project_releases', 0);
|
|
}
|
|
|
|
public function test_revert_rejects_a_detached_listing_link(): void
|
|
{
|
|
$admin = User::factory()->create(['role' => 'admin']);
|
|
$project = Project::factory()->create();
|
|
[$v1, $snapshot1] = $this->approvedRevisionWithCost($project);
|
|
$link = ProjectListingLink::factory()->for($project)->create(['lock_version' => 0, 'live_release_id' => null]);
|
|
$this->actingAs($admin)->post(route('projects.listings.release', [$project, $link]), [
|
|
'revision_id' => $v1->id, 'cost_snapshot_id' => $snapshot1->id, 'lock_version' => 0,
|
|
]);
|
|
$link->refresh();
|
|
$firstRelease = $link->live_release_id;
|
|
$link->update(['detached_at' => now()]);
|
|
|
|
$this->actingAs($admin)->post(route('projects.listings.revert', [$project, $link]), [
|
|
'release_id' => $firstRelease, 'lock_version' => $link->lock_version,
|
|
])->assertSessionHasErrors('listing_link');
|
|
}
|
|
|
|
public function test_release_is_recorded_atomically_with_its_audit_event(): void
|
|
{
|
|
$admin = User::factory()->create(['role' => 'admin']);
|
|
$project = Project::factory()->create();
|
|
[$revision, $snapshot] = $this->approvedRevisionWithCost($project);
|
|
$link = ProjectListingLink::factory()->for($project)->create(['lock_version' => 0, 'live_release_id' => null]);
|
|
Schema::drop('project_events');
|
|
|
|
$this->actingAs($admin)->post(route('projects.listings.release', [$project, $link]), [
|
|
'revision_id' => $revision->id, 'cost_snapshot_id' => $snapshot->id, 'lock_version' => 0,
|
|
]);
|
|
|
|
$this->assertDatabaseCount('project_releases', 0);
|
|
$link->refresh();
|
|
$this->assertNull($link->live_release_id);
|
|
$this->assertSame(0, $link->lock_version);
|
|
}
|
|
}
|