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