create(['role' => 'admin', 'disabled' => true]); $this->actingAs($admin)->get(route('projects.index')) ->assertRedirect(route('login')); $this->assertGuest(); } public function test_project_notes_with_script_tags_are_rendered_escaped_not_executed(): void { $admin = User::factory()->create(['role' => 'admin']); $project = Project::factory()->create(['notes' => '']); $response = $this->actingAs($admin)->get(route('projects.show', $project)); $response->assertOk() ->assertDontSee('', false) ->assertSee('<script>alert(1)</script>', false); } public function test_revision_change_notes_with_html_are_escaped_in_the_project_view(): void { $admin = User::factory()->create(['role' => 'admin']); $project = Project::factory()->create(); ProjectRevision::factory()->for($project)->create(['change_notes' => '']); $this->actingAs($admin)->get(route('projects.show', $project))->assertOk() ->assertDontSee('', false); } public function test_every_mutating_project_endpoint_rejects_guests(): void { $project = Project::factory()->create(); $revision = ProjectRevision::factory()->for($project)->create(); $file = ProjectFile::factory()->for($revision, 'revision')->create(); $listingLink = ProjectListingLink::factory()->for($project)->create(); $release = ProjectRelease::factory()->for($project)->create(); $this->post(route('projects.store'), [])->assertRedirect(route('login')); $this->patch(route('projects.update', $project), [])->assertRedirect(route('login')); $this->post(route('projects.archive', $project))->assertRedirect(route('login')); $this->post(route('projects.revisions.store', $project), [])->assertRedirect(route('login')); $this->post(route('projects.revisions.approve', [$project, $revision]))->assertRedirect(route('login')); $this->get(route('projects.files.download', [$project, $file]))->assertRedirect(route('login')); $this->post(route('projects.listings.store', $project), [])->assertRedirect(route('login')); $this->delete(route('projects.listings.destroy', [$project, $listingLink]))->assertRedirect(route('login')); $this->post(route('projects.cost-snapshots.store', $project), [])->assertRedirect(route('login')); $this->post(route('projects.listings.release', [$project, $listingLink]), [])->assertRedirect(route('login')); $this->post(route('projects.listings.revert', [$project, $listingLink]), ['release_id' => $release->id])->assertRedirect(route('login')); } public function test_every_mutating_project_endpoint_rejects_non_admin_users(): void { $user = User::factory()->create(); $project = Project::factory()->create(); $revision = ProjectRevision::factory()->for($project)->create(); $file = ProjectFile::factory()->for($revision, 'revision')->create(); $listingLink = ProjectListingLink::factory()->for($project)->create(); $release = ProjectRelease::factory()->for($project)->create(); $this->actingAs($user)->post(route('projects.store'), [])->assertForbidden(); $this->actingAs($user)->patch(route('projects.update', $project), [])->assertForbidden(); $this->actingAs($user)->post(route('projects.archive', $project))->assertForbidden(); $this->actingAs($user)->post(route('projects.revisions.store', $project), [])->assertForbidden(); $this->actingAs($user)->post(route('projects.revisions.approve', [$project, $revision]))->assertForbidden(); $this->actingAs($user)->get(route('projects.files.download', [$project, $file]))->assertForbidden(); $this->actingAs($user)->post(route('projects.listings.store', $project), [])->assertForbidden(); $this->actingAs($user)->delete(route('projects.listings.destroy', [$project, $listingLink]))->assertForbidden(); $this->actingAs($user)->post(route('projects.cost-snapshots.store', $project), [])->assertForbidden(); $this->actingAs($user)->post(route('projects.listings.release', [$project, $listingLink]), [])->assertForbidden(); $this->actingAs($user)->post(route('projects.listings.revert', [$project, $listingLink]), ['release_id' => $release->id])->assertForbidden(); } public function test_disabled_admin_cannot_use_any_mutating_project_endpoint(): void { $admin = User::factory()->create(['role' => 'admin', 'disabled' => true]); $project = Project::factory()->create(); $listingLink = ProjectListingLink::factory()->for($project)->create(); $this->actingAs($admin)->post(route('projects.store'), [])->assertRedirect(route('login')); $this->actingAs($admin)->post(route('projects.listings.release', [$project, $listingLink]), [])->assertRedirect(route('login')); } public function test_cross_project_revision_id_is_rejected_rather_than_silently_creating_a_new_revision(): void { $admin = User::factory()->create(['role' => 'admin']); $projectA = Project::factory()->create(); $projectB = Project::factory()->create(); $foreignRevision = ProjectRevision::factory()->for($projectB)->create(['revision_number' => 1]); Storage::fake('project_files'); $stl = UploadedFile::fake()->createWithContent( 'part.stl', "solid cube\nfacet normal 0 0 0\nouter loop\nvertex 0 0 0\nvertex 1 0 0\nvertex 0 1 0\nendloop\nendfacet\nendsolid cube\n", ); $this->actingAs($admin)->post(route('projects.revisions.store', $projectA), [ 'files' => [$stl], 'roles' => ['body'], 'revision_id' => $foreignRevision->id, ])->assertRedirect(); // A revision_id that doesn't belong to this project must never be written to; // it must fall through to creating a fresh revision scoped to this project. $this->assertDatabaseHas('project_revisions', ['project_id' => $projectA->id, 'revision_number' => 1]); $this->assertSame(0, $foreignRevision->fresh()->files()->count()); } public function test_missing_csrf_token_is_rejected_when_verification_actually_runs(): void { $this->app['env'] = 'local'; $admin = User::factory()->create(['role' => 'admin']); $project = Project::factory()->create(); $response = $this->actingAs($admin)->post(route('projects.archive', $project)); $response->assertStatus(419); } }