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:
cedry2k1992
2026-07-20 10:22:05 +03:00
co-authored by Claude Sonnet 5
commit 873699e23e
165 changed files with 18507 additions and 0 deletions
+54
View File
@@ -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();
}
}
+73
View File
@@ -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;
});
}
}
+51
View File
@@ -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');
}
}
+31
View File
@@ -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));
}
}
+19
View File
@@ -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);
}
}
+76
View File
@@ -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();
}
}
+123
View File
@@ -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]);
}
}
+84
View File
@@ -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');
}
}
+99
View File
@@ -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());
}
}
+53
View File
@@ -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') . ' (');
}
}
+105
View File
@@ -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]);
}
}
+10
View File
@@ -0,0 +1,10 @@
<?php
namespace Tests;
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
abstract class TestCase extends BaseTestCase
{
//
}
+16
View File
@@ -0,0 +1,16 @@
<?php
namespace Tests\Unit;
use PHPUnit\Framework\TestCase;
class ExampleTest extends TestCase
{
/**
* A basic test example.
*/
public function test_that_true_is_true(): void
{
$this->assertTrue(true);
}
}
+64
View File
@@ -0,0 +1,64 @@
<?php
namespace Tests\Unit\Osint;
use App\Models\Target;
use App\Osint\Runners\CrtShRunner;
use App\Osint\RunnerException;
use Illuminate\Support\Facades\Http;
use Tests\TestCase;
class CrtShRunnerTest extends TestCase
{
public function test_parses_and_deduplicates_subdomains(): void
{
Http::fake([
'crt.sh/*' => Http::response(json_encode([
['name_value' => "www.example.com\nexample.com"],
['name_value' => "*.example.com\nwww.example.com"],
])),
]);
$target = new Target(['type' => 'domain', 'value' => 'example.com']);
$result = (new CrtShRunner)->run($target);
$this->assertSame(
['*.example.com', 'example.com', 'www.example.com'],
$result->raw['subdomains']
);
$this->assertStringContainsString('3 unique subdomains', $result->summary);
}
public function test_handles_empty_response(): void
{
Http::fake(['crt.sh/*' => Http::response('')]);
$target = new Target(['type' => 'domain', 'value' => 'nonexistent-domain-xyz.com']);
$result = (new CrtShRunner)->run($target);
$this->assertSame([], $result->raw['subdomains']);
$this->assertSame('No certificates found.', $result->summary);
}
public function test_rejects_non_domain_targets(): void
{
$target = new Target(['type' => 'ip', 'value' => '8.8.8.8']);
$this->expectException(RunnerException::class);
(new CrtShRunner)->run($target);
}
public function test_throws_on_http_failure(): void
{
Http::fake(['crt.sh/*' => Http::response('', 500)]);
$target = new Target(['type' => 'domain', 'value' => 'example.com']);
$this->expectException(RunnerException::class);
(new CrtShRunner)->run($target);
}
}
+61
View File
@@ -0,0 +1,61 @@
<?php
namespace Tests\Unit\Osint;
use App\Models\Target;
use App\Osint\Runners\IpApiRunner;
use App\Osint\RunnerException;
use Illuminate\Support\Facades\Http;
use Tests\TestCase;
class IpApiRunnerTest extends TestCase
{
public function test_parses_successful_lookup(): void
{
Http::fake([
'ip-api.com/*' => Http::response([
'status' => 'success',
'country' => 'United States',
'regionName' => 'Virginia',
'city' => 'Ashburn',
'isp' => 'Google LLC',
'org' => 'Google Public DNS',
'query' => '8.8.8.8',
]),
]);
$target = new Target(['type' => 'ip', 'value' => '8.8.8.8']);
$result = (new IpApiRunner)->run($target);
$this->assertSame('Ashburn, Virginia, United States (Google LLC)', $result->summary);
$this->assertSame('8.8.8.8', $result->raw['query']);
}
public function test_throws_when_lookup_reports_failure(): void
{
Http::fake([
'ip-api.com/*' => Http::response([
'status' => 'fail',
'message' => 'private range',
'query' => '10.0.0.1',
]),
]);
$target = new Target(['type' => 'ip', 'value' => '10.0.0.1']);
$this->expectException(RunnerException::class);
$this->expectExceptionMessage('private range');
(new IpApiRunner)->run($target);
}
public function test_rejects_non_ip_targets(): void
{
$target = new Target(['type' => 'domain', 'value' => 'example.com']);
$this->expectException(RunnerException::class);
(new IpApiRunner)->run($target);
}
}