seed(RolePermissionSeeder::class); } protected function actingWithRole(string $role): User { $user = User::factory()->create(); $user->syncRoles([$role]); $this->withHeaders($this->apiHeadersFor($user)); return $user; } #[Test] public function an_api_change_is_logged_with_who_what_and_from_where(): void { $editor = $this->actingWithRole('editor'); $project = Project::factory()->create(['status' => 'active']); $this->patchJson("/api/v1/projects/{$project->id}", ['status' => 'paused'])->assertOk(); $entry = AuditLog::query()->where('action', 'projects.updated')->sole(); $this->assertSame($editor->id, $entry->user_id); $this->assertSame('api', $entry->via); $this->assertSame('127.0.0.1', $entry->ip); $this->assertSame((string) $project->id, $entry->subject_id); $this->assertSame(['old' => 'active', 'new' => 'paused'], $entry->changes['status']); } #[Test] public function what_the_application_does_by_itself_is_not_logged(): void { Repository::factory()->create()->forceFill(['stars' => 10])->save(); $this->assertSame(0, AuditLog::query()->count()); } #[Test] public function secrets_are_never_written_down(): void { $this->actingAs(User::factory()->create()); Webhook::factory()->create(['secret' => 'never-in-the-log-please']); $this->assertStringNotContainsString('never-in-the-log-please', (string) json_encode(AuditLog::query()->get())); $this->assertSame('••••', AuditLog::query()->where('action', 'webhooks.created')->sole()->changes['secret']); } #[Test] public function the_content_version_bump_is_not_an_entry(): void { $this->actingAs(User::factory()->create()); ContentVersion::bump(); $this->assertSame(0, AuditLog::query()->where('subject_type', 'settings')->count()); } #[Test] public function role_changes_are_logged(): void { $admin = User::factory()->create(); $admin->syncRoles(['admin']); $this->actingAs($admin); $someone = User::factory()->create(); $someone->syncRoles(['editor']); $entry = AuditLog::query()->where('action', 'users.roles_added')->where('subject_id', (string) $someone->id)->sole(); $this->assertSame(['roles' => ['editor']], $entry->changes); } #[Test] public function a_machine_token_is_named_in_what_it_did(): void { $editor = User::factory()->create(); $editor->syncRoles(['editor']); [$token, $plain] = ApiToken::issue($editor, 'CI', ['technologies.*'], now()->addDay()); $this->app['auth']->forgetGuards(); $this->withHeaders(['Authorization' => 'Bearer '.$plain, 'Accept' => 'application/json']); $this->postJson('/api/v1/technologies', ['name' => 'Vue', 'slug' => 'vue'])->assertCreated(); $entry = AuditLog::query()->where('action', 'technologies.created')->sole(); $this->assertSame('token', $entry->via); $this->assertSame($token->id, $entry->api_token_id); } #[Test] public function only_an_admin_reads_the_log(): void { $this->actingWithRole('editor'); $this->getJson('/api/v1/audit')->assertForbidden(); Project::factory()->create(); $this->actingWithRole('admin'); $this->getJson('/api/v1/audit?action=projects.') ->assertOk() ->assertJsonPath('data.0.action', 'projects.created') ->assertJsonPath('data.0.subject.type', 'projects'); } }