Initial commit: Laravel OSINT platform
Multi-user OSINT investigation tool with owner-controlled per-user and per-group access grants. Includes Sherlock, theHarvester, crt.sh, WHOIS, and ip-api.com collectors wired through a pluggable Runner/queue pipeline, investigation search, and printable/CSV/JSON reporting. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Auth;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AuthenticationTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_login_screen_can_be_rendered(): void
|
||||
{
|
||||
$response = $this->get('/login');
|
||||
|
||||
$response->assertStatus(200);
|
||||
}
|
||||
|
||||
public function test_users_can_authenticate_using_the_login_screen(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
$response = $this->post('/login', [
|
||||
'email' => $user->email,
|
||||
'password' => 'password',
|
||||
]);
|
||||
|
||||
$this->assertAuthenticated();
|
||||
$response->assertRedirect(route('dashboard', absolute: false));
|
||||
}
|
||||
|
||||
public function test_users_can_not_authenticate_with_invalid_password(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->post('/login', [
|
||||
'email' => $user->email,
|
||||
'password' => 'wrong-password',
|
||||
]);
|
||||
|
||||
$this->assertGuest();
|
||||
}
|
||||
|
||||
public function test_users_can_logout(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
$response = $this->actingAs($user)->post('/logout');
|
||||
|
||||
$this->assertGuest();
|
||||
$response->assertRedirect('/');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Auth;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Auth\Events\Verified;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Support\Facades\URL;
|
||||
use Tests\TestCase;
|
||||
|
||||
class EmailVerificationTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_email_verification_screen_can_be_rendered(): void
|
||||
{
|
||||
$user = User::factory()->unverified()->create();
|
||||
|
||||
$response = $this->actingAs($user)->get('/verify-email');
|
||||
|
||||
$response->assertStatus(200);
|
||||
}
|
||||
|
||||
public function test_email_can_be_verified(): void
|
||||
{
|
||||
$user = User::factory()->unverified()->create();
|
||||
|
||||
Event::fake();
|
||||
|
||||
$verificationUrl = URL::temporarySignedRoute(
|
||||
'verification.verify',
|
||||
now()->addMinutes(60),
|
||||
['id' => $user->id, 'hash' => sha1($user->email)]
|
||||
);
|
||||
|
||||
$response = $this->actingAs($user)->get($verificationUrl);
|
||||
|
||||
Event::assertDispatched(Verified::class);
|
||||
$this->assertTrue($user->fresh()->hasVerifiedEmail());
|
||||
$response->assertRedirect(route('dashboard', absolute: false).'?verified=1');
|
||||
}
|
||||
|
||||
public function test_email_is_not_verified_with_invalid_hash(): void
|
||||
{
|
||||
$user = User::factory()->unverified()->create();
|
||||
|
||||
$verificationUrl = URL::temporarySignedRoute(
|
||||
'verification.verify',
|
||||
now()->addMinutes(60),
|
||||
['id' => $user->id, 'hash' => sha1('wrong-email')]
|
||||
);
|
||||
|
||||
$this->actingAs($user)->get($verificationUrl);
|
||||
|
||||
$this->assertFalse($user->fresh()->hasVerifiedEmail());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Auth;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class PasswordConfirmationTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_confirm_password_screen_can_be_rendered(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
$response = $this->actingAs($user)->get('/confirm-password');
|
||||
|
||||
$response->assertStatus(200);
|
||||
}
|
||||
|
||||
public function test_password_can_be_confirmed(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
$response = $this->actingAs($user)->post('/confirm-password', [
|
||||
'password' => 'password',
|
||||
]);
|
||||
|
||||
$response->assertRedirect();
|
||||
$response->assertSessionHasNoErrors();
|
||||
}
|
||||
|
||||
public function test_password_is_not_confirmed_with_invalid_password(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
$response = $this->actingAs($user)->post('/confirm-password', [
|
||||
'password' => 'wrong-password',
|
||||
]);
|
||||
|
||||
$response->assertSessionHasErrors();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Auth;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Auth\Notifications\ResetPassword;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Notification;
|
||||
use Tests\TestCase;
|
||||
|
||||
class PasswordResetTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_reset_password_link_screen_can_be_rendered(): void
|
||||
{
|
||||
$response = $this->get('/forgot-password');
|
||||
|
||||
$response->assertStatus(200);
|
||||
}
|
||||
|
||||
public function test_reset_password_link_can_be_requested(): void
|
||||
{
|
||||
Notification::fake();
|
||||
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->post('/forgot-password', ['email' => $user->email]);
|
||||
|
||||
Notification::assertSentTo($user, ResetPassword::class);
|
||||
}
|
||||
|
||||
public function test_reset_password_screen_can_be_rendered(): void
|
||||
{
|
||||
Notification::fake();
|
||||
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->post('/forgot-password', ['email' => $user->email]);
|
||||
|
||||
Notification::assertSentTo($user, ResetPassword::class, function ($notification) {
|
||||
$response = $this->get('/reset-password/'.$notification->token);
|
||||
|
||||
$response->assertStatus(200);
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
public function test_password_can_be_reset_with_valid_token(): void
|
||||
{
|
||||
Notification::fake();
|
||||
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->post('/forgot-password', ['email' => $user->email]);
|
||||
|
||||
Notification::assertSentTo($user, ResetPassword::class, function ($notification) use ($user) {
|
||||
$response = $this->post('/reset-password', [
|
||||
'token' => $notification->token,
|
||||
'email' => $user->email,
|
||||
'password' => 'password',
|
||||
'password_confirmation' => 'password',
|
||||
]);
|
||||
|
||||
$response
|
||||
->assertSessionHasNoErrors()
|
||||
->assertRedirect(route('login'));
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Auth;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Tests\TestCase;
|
||||
|
||||
class PasswordUpdateTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_password_can_be_updated(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
$response = $this
|
||||
->actingAs($user)
|
||||
->from('/profile')
|
||||
->put('/password', [
|
||||
'current_password' => 'password',
|
||||
'password' => 'new-password',
|
||||
'password_confirmation' => 'new-password',
|
||||
]);
|
||||
|
||||
$response
|
||||
->assertSessionHasNoErrors()
|
||||
->assertRedirect('/profile');
|
||||
|
||||
$this->assertTrue(Hash::check('new-password', $user->refresh()->password));
|
||||
}
|
||||
|
||||
public function test_correct_password_must_be_provided_to_update_password(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
$response = $this
|
||||
->actingAs($user)
|
||||
->from('/profile')
|
||||
->put('/password', [
|
||||
'current_password' => 'wrong-password',
|
||||
'password' => 'new-password',
|
||||
'password_confirmation' => 'new-password',
|
||||
]);
|
||||
|
||||
$response
|
||||
->assertSessionHasErrorsIn('updatePassword', 'current_password')
|
||||
->assertRedirect('/profile');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Auth;
|
||||
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class RegistrationTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_registration_screen_can_be_rendered(): void
|
||||
{
|
||||
$response = $this->get('/register');
|
||||
|
||||
$response->assertStatus(200);
|
||||
}
|
||||
|
||||
public function test_new_users_can_register(): void
|
||||
{
|
||||
$response = $this->post('/register', [
|
||||
'name' => 'Test User',
|
||||
'email' => 'test@example.com',
|
||||
'password' => 'password',
|
||||
'password_confirmation' => 'password',
|
||||
]);
|
||||
|
||||
$this->assertAuthenticated();
|
||||
$response->assertRedirect(route('dashboard', absolute: false));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
// use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ExampleTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* A basic test example.
|
||||
*/
|
||||
public function test_the_application_returns_a_successful_response(): void
|
||||
{
|
||||
$response = $this->get('/');
|
||||
|
||||
$response->assertStatus(200);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\Group;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class GroupManagementTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_owner_can_add_a_member_by_email(): void
|
||||
{
|
||||
$owner = User::factory()->create();
|
||||
$newMember = User::factory()->create(['email' => 'member@example.com']);
|
||||
$group = Group::factory()->for($owner, 'owner')->create();
|
||||
|
||||
$this->actingAs($owner)
|
||||
->post(route('groups.members.store', $group), ['email' => 'member@example.com'])
|
||||
->assertRedirect(route('groups.show', $group));
|
||||
|
||||
$this->assertTrue($group->fresh()->members->contains($newMember));
|
||||
}
|
||||
|
||||
public function test_adding_a_member_with_unknown_email_fails_gracefully(): void
|
||||
{
|
||||
$owner = User::factory()->create();
|
||||
$group = Group::factory()->for($owner, 'owner')->create();
|
||||
|
||||
$this->actingAs($owner)
|
||||
->post(route('groups.members.store', $group), ['email' => 'nobody@example.com'])
|
||||
->assertSessionHasErrors('email');
|
||||
|
||||
$this->assertCount(0, $group->fresh()->members);
|
||||
}
|
||||
|
||||
public function test_owner_can_remove_a_member(): void
|
||||
{
|
||||
$owner = User::factory()->create();
|
||||
$member = User::factory()->create();
|
||||
$group = Group::factory()->for($owner, 'owner')->create();
|
||||
$group->members()->attach($member);
|
||||
|
||||
$this->actingAs($owner)
|
||||
->delete(route('groups.members.destroy', [$group, $member]))
|
||||
->assertRedirect(route('groups.show', $group));
|
||||
|
||||
$this->assertFalse($group->fresh()->members->contains($member));
|
||||
}
|
||||
|
||||
public function test_non_owner_member_cannot_manage_members(): void
|
||||
{
|
||||
$owner = User::factory()->create();
|
||||
$member = User::factory()->create();
|
||||
$anotherUser = User::factory()->create(['email' => 'another@example.com']);
|
||||
$group = Group::factory()->for($owner, 'owner')->create();
|
||||
$group->members()->attach($member);
|
||||
|
||||
$this->actingAs($member)
|
||||
->post(route('groups.members.store', $group), ['email' => 'another@example.com'])
|
||||
->assertForbidden();
|
||||
}
|
||||
|
||||
public function test_unrelated_user_cannot_view_group(): void
|
||||
{
|
||||
$owner = User::factory()->create();
|
||||
$stranger = User::factory()->create();
|
||||
$group = Group::factory()->for($owner, 'owner')->create();
|
||||
|
||||
$this->actingAs($stranger)
|
||||
->get(route('groups.show', $group))
|
||||
->assertForbidden();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\Group;
|
||||
use App\Models\Investigation;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class InvestigationAccessManagementTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_owner_can_grant_a_user_viewer_access(): void
|
||||
{
|
||||
$owner = User::factory()->create();
|
||||
$grantee = User::factory()->create(['email' => 'grantee@example.com']);
|
||||
$investigation = Investigation::factory()->for($owner, 'owner')->create();
|
||||
|
||||
$this->actingAs($owner)
|
||||
->post(route('investigations.access.users.store', $investigation), [
|
||||
'email' => 'grantee@example.com',
|
||||
'role' => 'viewer',
|
||||
])
|
||||
->assertRedirect(route('investigations.access.index', $investigation));
|
||||
|
||||
$this->assertSame('viewer', $investigation->fresh()->roleFor($grantee));
|
||||
}
|
||||
|
||||
public function test_owner_can_revoke_a_users_access(): void
|
||||
{
|
||||
$owner = User::factory()->create();
|
||||
$grantee = User::factory()->create();
|
||||
$investigation = Investigation::factory()->for($owner, 'owner')->create();
|
||||
$investigation->users()->attach($grantee, ['role' => 'editor']);
|
||||
|
||||
$this->actingAs($owner)
|
||||
->delete(route('investigations.access.users.destroy', [$investigation, $grantee]))
|
||||
->assertRedirect(route('investigations.access.index', $investigation));
|
||||
|
||||
$this->assertNull($investigation->fresh()->roleFor($grantee));
|
||||
}
|
||||
|
||||
public function test_owner_can_grant_a_group_editor_access(): void
|
||||
{
|
||||
$owner = User::factory()->create();
|
||||
$member = User::factory()->create();
|
||||
$group = Group::factory()->for($owner, 'owner')->create();
|
||||
$group->members()->attach($member);
|
||||
$investigation = Investigation::factory()->for($owner, 'owner')->create();
|
||||
|
||||
$this->actingAs($owner)
|
||||
->post(route('investigations.access.groups.store', $investigation), [
|
||||
'group_id' => $group->id,
|
||||
'role' => 'editor',
|
||||
])
|
||||
->assertRedirect(route('investigations.access.index', $investigation));
|
||||
|
||||
$this->assertSame('editor', $investigation->fresh()->roleFor($member));
|
||||
}
|
||||
|
||||
public function test_owner_can_revoke_a_groups_access(): void
|
||||
{
|
||||
$owner = User::factory()->create();
|
||||
$member = User::factory()->create();
|
||||
$group = Group::factory()->for($owner, 'owner')->create();
|
||||
$group->members()->attach($member);
|
||||
$investigation = Investigation::factory()->for($owner, 'owner')->create();
|
||||
$investigation->groups()->attach($group, ['role' => 'viewer']);
|
||||
|
||||
$this->actingAs($owner)
|
||||
->delete(route('investigations.access.groups.destroy', [$investigation, $group]))
|
||||
->assertRedirect(route('investigations.access.index', $investigation));
|
||||
|
||||
$this->assertNull($investigation->fresh()->roleFor($member));
|
||||
}
|
||||
|
||||
public function test_editor_cannot_manage_access(): void
|
||||
{
|
||||
$owner = User::factory()->create();
|
||||
$editor = User::factory()->create();
|
||||
$investigation = Investigation::factory()->for($owner, 'owner')->create();
|
||||
$investigation->users()->attach($editor, ['role' => 'editor']);
|
||||
|
||||
$this->actingAs($editor)
|
||||
->get(route('investigations.access.index', $investigation))
|
||||
->assertForbidden();
|
||||
}
|
||||
|
||||
public function test_owner_cannot_grant_a_group_they_have_no_relation_to(): void
|
||||
{
|
||||
$owner = User::factory()->create();
|
||||
$unrelatedGroup = Group::factory()->create();
|
||||
$investigation = Investigation::factory()->for($owner, 'owner')->create();
|
||||
|
||||
$this->actingAs($owner)
|
||||
->post(route('investigations.access.groups.store', $investigation), [
|
||||
'group_id' => $unrelatedGroup->id,
|
||||
'role' => 'viewer',
|
||||
])
|
||||
->assertForbidden();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\Group;
|
||||
use App\Models\Investigation;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class InvestigationAccessTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_owner_has_full_access(): void
|
||||
{
|
||||
$owner = User::factory()->create();
|
||||
$investigation = Investigation::factory()->for($owner, 'owner')->create();
|
||||
|
||||
$this->assertTrue($investigation->isAccessibleBy($owner));
|
||||
$this->assertTrue($investigation->isEditableBy($owner));
|
||||
$this->assertTrue($owner->can('manageAccess', $investigation));
|
||||
}
|
||||
|
||||
public function test_unrelated_user_has_no_access(): void
|
||||
{
|
||||
$owner = User::factory()->create();
|
||||
$stranger = User::factory()->create();
|
||||
$investigation = Investigation::factory()->for($owner, 'owner')->create();
|
||||
|
||||
$this->assertFalse($investigation->isAccessibleBy($stranger));
|
||||
$this->assertFalse($stranger->can('view', $investigation));
|
||||
}
|
||||
|
||||
public function test_owner_can_grant_direct_viewer_access(): void
|
||||
{
|
||||
$owner = User::factory()->create();
|
||||
$viewer = User::factory()->create();
|
||||
$investigation = Investigation::factory()->for($owner, 'owner')->create();
|
||||
|
||||
$investigation->users()->attach($viewer, ['role' => 'viewer']);
|
||||
|
||||
$this->assertTrue($investigation->isAccessibleBy($viewer));
|
||||
$this->assertFalse($investigation->isEditableBy($viewer));
|
||||
$this->assertFalse($viewer->can('manageAccess', $investigation));
|
||||
}
|
||||
|
||||
public function test_owner_can_grant_direct_editor_access(): void
|
||||
{
|
||||
$owner = User::factory()->create();
|
||||
$editor = User::factory()->create();
|
||||
$investigation = Investigation::factory()->for($owner, 'owner')->create();
|
||||
|
||||
$investigation->users()->attach($editor, ['role' => 'editor']);
|
||||
|
||||
$this->assertTrue($investigation->isAccessibleBy($editor));
|
||||
$this->assertTrue($investigation->isEditableBy($editor));
|
||||
$this->assertTrue($editor->can('update', $investigation));
|
||||
}
|
||||
|
||||
public function test_group_grant_gives_access_to_all_members(): void
|
||||
{
|
||||
$owner = User::factory()->create();
|
||||
$groupOwner = User::factory()->create();
|
||||
$memberOne = User::factory()->create();
|
||||
$memberTwo = User::factory()->create();
|
||||
|
||||
$group = Group::factory()->for($groupOwner, 'owner')->create();
|
||||
$group->members()->attach([$memberOne->id, $memberTwo->id]);
|
||||
|
||||
$investigation = Investigation::factory()->for($owner, 'owner')->create();
|
||||
$investigation->groups()->attach($group, ['role' => 'viewer']);
|
||||
|
||||
$this->assertTrue($investigation->isAccessibleBy($memberOne));
|
||||
$this->assertTrue($investigation->isAccessibleBy($memberTwo));
|
||||
$this->assertFalse($investigation->isEditableBy($memberOne));
|
||||
}
|
||||
|
||||
public function test_direct_grant_wins_over_conflicting_group_grant(): void
|
||||
{
|
||||
$owner = User::factory()->create();
|
||||
$groupOwner = User::factory()->create();
|
||||
$user = User::factory()->create();
|
||||
|
||||
$group = Group::factory()->for($groupOwner, 'owner')->create();
|
||||
$group->members()->attach($user->id);
|
||||
|
||||
$investigation = Investigation::factory()->for($owner, 'owner')->create();
|
||||
$investigation->groups()->attach($group, ['role' => 'editor']);
|
||||
$investigation->users()->attach($user, ['role' => 'viewer']);
|
||||
|
||||
$this->assertSame('viewer', $investigation->roleFor($user));
|
||||
$this->assertFalse($investigation->isEditableBy($user));
|
||||
}
|
||||
|
||||
public function test_editor_role_wins_when_user_belongs_to_multiple_groups(): void
|
||||
{
|
||||
$owner = User::factory()->create();
|
||||
$user = User::factory()->create();
|
||||
|
||||
$viewerGroup = Group::factory()->create();
|
||||
$editorGroup = Group::factory()->create();
|
||||
$viewerGroup->members()->attach($user->id);
|
||||
$editorGroup->members()->attach($user->id);
|
||||
|
||||
$investigation = Investigation::factory()->for($owner, 'owner')->create();
|
||||
$investigation->groups()->attach($viewerGroup, ['role' => 'viewer']);
|
||||
$investigation->groups()->attach($editorGroup, ['role' => 'editor']);
|
||||
|
||||
$this->assertSame('editor', $investigation->roleFor($user));
|
||||
}
|
||||
|
||||
public function test_only_owner_can_delete_investigation(): void
|
||||
{
|
||||
$owner = User::factory()->create();
|
||||
$editor = User::factory()->create();
|
||||
$investigation = Investigation::factory()->for($owner, 'owner')->create();
|
||||
$investigation->users()->attach($editor, ['role' => 'editor']);
|
||||
|
||||
$this->assertTrue($owner->can('delete', $investigation));
|
||||
$this->assertFalse($editor->can('delete', $investigation));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\Investigation;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class InvestigationDeletionTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_owner_can_delete_investigation_and_its_targets_cascade(): void
|
||||
{
|
||||
$owner = User::factory()->create();
|
||||
$investigation = Investigation::factory()->for($owner, 'owner')->create();
|
||||
$target = $investigation->targets()->create([
|
||||
'type' => 'username',
|
||||
'value' => 'someone',
|
||||
'created_by' => $owner->id,
|
||||
]);
|
||||
|
||||
$this->actingAs($owner)
|
||||
->delete(route('investigations.destroy', $investigation))
|
||||
->assertRedirect(route('investigations.index'));
|
||||
|
||||
$this->assertDatabaseMissing('investigations', ['id' => $investigation->id]);
|
||||
$this->assertDatabaseMissing('targets', ['id' => $target->id]);
|
||||
}
|
||||
|
||||
public function test_editor_cannot_delete_investigation(): void
|
||||
{
|
||||
$owner = User::factory()->create();
|
||||
$editor = User::factory()->create();
|
||||
$investigation = Investigation::factory()->for($owner, 'owner')->create();
|
||||
$investigation->users()->attach($editor, ['role' => 'editor']);
|
||||
|
||||
$this->actingAs($editor)
|
||||
->delete(route('investigations.destroy', $investigation))
|
||||
->assertForbidden();
|
||||
|
||||
$this->assertDatabaseHas('investigations', ['id' => $investigation->id]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\Investigation;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class InvestigationReportTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_owner_can_view_printable_report(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$investigation = Investigation::factory()->for($user, 'owner')->create();
|
||||
|
||||
$this->actingAs($user)
|
||||
->get(route('investigations.report', $investigation))
|
||||
->assertOk()
|
||||
->assertSee($investigation->name);
|
||||
}
|
||||
|
||||
public function test_unrelated_user_cannot_view_report(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$stranger = User::factory()->create();
|
||||
$investigation = Investigation::factory()->for($user, 'owner')->create();
|
||||
|
||||
$this->actingAs($stranger)
|
||||
->get(route('investigations.report', $investigation))
|
||||
->assertForbidden();
|
||||
}
|
||||
|
||||
public function test_csv_export_contains_finding_rows(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$investigation = Investigation::factory()->for($user, 'owner')->create();
|
||||
$target = $investigation->targets()->create(['type' => 'username', 'value' => 'someone', 'created_by' => $user->id]);
|
||||
$source = \App\Models\Source::create([
|
||||
'key' => 'test-source', 'name' => 'Test Source', 'target_types' => ['username'], 'enabled' => true,
|
||||
]);
|
||||
$target->findings()->create([
|
||||
'source_id' => $source->id, 'status' => 'completed', 'summary' => 'Found it',
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($user)->get(route('investigations.export.csv', $investigation));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertHeader('Content-Type', 'text/csv; charset=UTF-8');
|
||||
$this->assertStringContainsString('Test Source', $response->getContent());
|
||||
$this->assertStringContainsString('Found it', $response->getContent());
|
||||
}
|
||||
|
||||
public function test_csv_export_neutralizes_formula_injection_in_target_value(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$investigation = Investigation::factory()->for($user, 'owner')->create();
|
||||
$target = $investigation->targets()->create(['type' => 'person', 'value' => 'safe name', 'created_by' => $user->id]);
|
||||
$source = \App\Models\Source::create([
|
||||
'key' => 'test-source', 'name' => 'Test Source', 'target_types' => ['person'], 'enabled' => true,
|
||||
]);
|
||||
$target->findings()->create([
|
||||
'source_id' => $source->id, 'status' => 'completed', 'summary' => '=cmd|"/c calc"!A1',
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($user)->get(route('investigations.export.csv', $investigation));
|
||||
|
||||
$this->assertStringContainsString("'=cmd", $response->getContent());
|
||||
}
|
||||
|
||||
public function test_json_export_includes_targets_and_findings(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$investigation = Investigation::factory()->for($user, 'owner')->create();
|
||||
$investigation->targets()->create(['type' => 'domain', 'value' => 'example.com', 'created_by' => $user->id]);
|
||||
|
||||
$response = $this->actingAs($user)->get(route('investigations.export.json', $investigation));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJsonPath('targets.0.value', 'example.com');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ProfileTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_profile_page_is_displayed(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
$response = $this
|
||||
->actingAs($user)
|
||||
->get('/profile');
|
||||
|
||||
$response->assertOk();
|
||||
}
|
||||
|
||||
public function test_profile_information_can_be_updated(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
$response = $this
|
||||
->actingAs($user)
|
||||
->patch('/profile', [
|
||||
'name' => 'Test User',
|
||||
'email' => 'test@example.com',
|
||||
]);
|
||||
|
||||
$response
|
||||
->assertSessionHasNoErrors()
|
||||
->assertRedirect('/profile');
|
||||
|
||||
$user->refresh();
|
||||
|
||||
$this->assertSame('Test User', $user->name);
|
||||
$this->assertSame('test@example.com', $user->email);
|
||||
$this->assertNull($user->email_verified_at);
|
||||
}
|
||||
|
||||
public function test_email_verification_status_is_unchanged_when_the_email_address_is_unchanged(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
$response = $this
|
||||
->actingAs($user)
|
||||
->patch('/profile', [
|
||||
'name' => 'Test User',
|
||||
'email' => $user->email,
|
||||
]);
|
||||
|
||||
$response
|
||||
->assertSessionHasNoErrors()
|
||||
->assertRedirect('/profile');
|
||||
|
||||
$this->assertNotNull($user->refresh()->email_verified_at);
|
||||
}
|
||||
|
||||
public function test_user_can_delete_their_account(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
$response = $this
|
||||
->actingAs($user)
|
||||
->delete('/profile', [
|
||||
'password' => 'password',
|
||||
]);
|
||||
|
||||
$response
|
||||
->assertSessionHasNoErrors()
|
||||
->assertRedirect('/');
|
||||
|
||||
$this->assertGuest();
|
||||
$this->assertNull($user->fresh());
|
||||
}
|
||||
|
||||
public function test_correct_password_must_be_provided_to_delete_account(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
$response = $this
|
||||
->actingAs($user)
|
||||
->from('/profile')
|
||||
->delete('/profile', [
|
||||
'password' => 'wrong-password',
|
||||
]);
|
||||
|
||||
$response
|
||||
->assertSessionHasErrorsIn('userDeletion', 'password')
|
||||
->assertRedirect('/profile');
|
||||
|
||||
$this->assertNotNull($user->fresh());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\Investigation;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class SearchTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_finds_accessible_investigation_by_name(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$mine = Investigation::factory()->for($user, 'owner')->create(['name' => 'Operation Nightingale']);
|
||||
$other = Investigation::factory()->create(['name' => 'Operation Nightowl']);
|
||||
|
||||
$response = $this->actingAs($user)->get(route('search.index', ['q' => 'Nightingale']));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertSee('Operation Nightingale');
|
||||
$response->assertDontSee('Operation Nightowl');
|
||||
}
|
||||
|
||||
public function test_finds_target_by_value_only_within_accessible_investigations(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$mine = Investigation::factory()->for($user, 'owner')->create();
|
||||
$mine->targets()->create(['type' => 'username', 'value' => 'unique_search_handle', 'created_by' => $user->id]);
|
||||
|
||||
$stranger = User::factory()->create();
|
||||
$notMine = Investigation::factory()->for($stranger, 'owner')->create();
|
||||
$notMine->targets()->create(['type' => 'username', 'value' => 'unique_search_handle_2', 'created_by' => $stranger->id]);
|
||||
|
||||
$response = $this->actingAs($user)->get(route('search.index', ['q' => 'unique_search_handle']));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertSee('unique_search_handle', false);
|
||||
$response->assertDontSee('unique_search_handle_2');
|
||||
}
|
||||
|
||||
public function test_empty_query_shows_no_results_section(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
$response = $this->actingAs($user)->get(route('search.index'));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertDontSee(__('Investigations') . ' (');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\Investigation;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class TargetManagementTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_editor_can_add_a_valid_target(): void
|
||||
{
|
||||
$owner = User::factory()->create();
|
||||
$investigation = Investigation::factory()->for($owner, 'owner')->create();
|
||||
|
||||
$this->actingAs($owner)
|
||||
->post(route('investigations.targets.store', $investigation), [
|
||||
'type' => 'username',
|
||||
'value' => 'valid_user.name',
|
||||
])
|
||||
->assertRedirect(route('investigations.show', $investigation));
|
||||
|
||||
$this->assertDatabaseHas('targets', [
|
||||
'investigation_id' => $investigation->id,
|
||||
'type' => 'username',
|
||||
'value' => 'valid_user.name',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_username_target_rejects_unsafe_characters(): void
|
||||
{
|
||||
$owner = User::factory()->create();
|
||||
$investigation = Investigation::factory()->for($owner, 'owner')->create();
|
||||
|
||||
$this->actingAs($owner)
|
||||
->post(route('investigations.targets.store', $investigation), [
|
||||
'type' => 'username',
|
||||
'value' => '../../etc/passwd',
|
||||
])
|
||||
->assertSessionHasErrors('value');
|
||||
|
||||
$this->assertDatabaseCount('targets', 0);
|
||||
}
|
||||
|
||||
public function test_email_target_requires_valid_email_format(): void
|
||||
{
|
||||
$owner = User::factory()->create();
|
||||
$investigation = Investigation::factory()->for($owner, 'owner')->create();
|
||||
|
||||
$this->actingAs($owner)
|
||||
->post(route('investigations.targets.store', $investigation), [
|
||||
'type' => 'email',
|
||||
'value' => 'not-an-email',
|
||||
])
|
||||
->assertSessionHasErrors('value');
|
||||
}
|
||||
|
||||
public function test_ip_target_requires_valid_ip_format(): void
|
||||
{
|
||||
$owner = User::factory()->create();
|
||||
$investigation = Investigation::factory()->for($owner, 'owner')->create();
|
||||
|
||||
$this->actingAs($owner)
|
||||
->post(route('investigations.targets.store', $investigation), [
|
||||
'type' => 'ip',
|
||||
'value' => 'not-an-ip',
|
||||
])
|
||||
->assertSessionHasErrors('value');
|
||||
}
|
||||
|
||||
public function test_viewer_cannot_add_targets(): void
|
||||
{
|
||||
$owner = User::factory()->create();
|
||||
$viewer = User::factory()->create();
|
||||
$investigation = Investigation::factory()->for($owner, 'owner')->create();
|
||||
$investigation->users()->attach($viewer, ['role' => 'viewer']);
|
||||
|
||||
$this->actingAs($viewer)
|
||||
->post(route('investigations.targets.store', $investigation), [
|
||||
'type' => 'username',
|
||||
'value' => 'someone',
|
||||
])
|
||||
->assertForbidden();
|
||||
}
|
||||
|
||||
public function test_editor_can_delete_a_target(): void
|
||||
{
|
||||
$owner = User::factory()->create();
|
||||
$investigation = Investigation::factory()->for($owner, 'owner')->create();
|
||||
$target = $investigation->targets()->create([
|
||||
'type' => 'username',
|
||||
'value' => 'someone',
|
||||
'created_by' => $owner->id,
|
||||
]);
|
||||
|
||||
$this->actingAs($owner)
|
||||
->delete(route('investigations.targets.destroy', [$investigation, $target]))
|
||||
->assertRedirect(route('investigations.show', $investigation));
|
||||
|
||||
$this->assertDatabaseMissing('targets', ['id' => $target->id]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user