fxi first test round

This commit is contained in:
root
2026-05-30 01:08:24 -04:00
parent f55213a3df
commit 6a40de4937
11 changed files with 1303 additions and 5 deletions
@@ -0,0 +1,333 @@
<?php
declare(strict_types=1);
namespace Tests\Feature\Integration;
use Illuminate\Auth\GenericUser;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Tests\TestCase;
final class AuthTenantIsolationIntegrationTest extends TestCase
{
use RefreshDatabase;
public function test_user_can_access_own_school_workflows(): void
{
$school = $this->createSchool('Own School', 'OWN');
$user = $this->createActorForSchool($school);
$this->actingAs($user);
$this->getJson("/api/schools/{$school}")
->assertOk();
$studentResponse = $this->postJson("/api/schools/{$school}/students", [
'first_name' => 'Maya',
'last_name' => 'Patel',
'student_number' => 'OWN-1001',
'grade_level' => '7',
]);
$studentResponse->assertCreated();
$studentId = $studentResponse->json('data.id');
$this->getJson("/api/schools/{$school}/students/{$studentId}")
->assertOk()
->assertJsonPath('data.student_number', 'OWN-1001');
$this->postJson("/api/schools/{$school}/attendance/scans", [
'student_number' => 'OWN-1001',
'scanned_at' => '2026-05-29T08:15:00Z',
'source' => 'kiosk',
])->assertCreated();
$this->createTemplate(
$school,
'attendance_absence',
'email',
'Hello, {{ student.first_name }} was marked absent.'
);
$this->postJson("/api/schools/{$school}/communications/preview", [
'template_key' => 'attendance_absence',
'student_id' => $studentId,
'channel' => 'email',
])->assertOk();
$this->postJson("/api/schools/{$school}/communications/send", [
'template_key' => 'attendance_absence',
'student_id' => $studentId,
'channel' => 'email',
])->assertAccepted();
$file = UploadedFile::fake()->createWithContent(
'payments.csv',
"student_number,amount,paid_at,reference\nOWN-1001,125.00,2026-05-29,PAY-OWN-001\n"
);
$this->postJson("/api/schools/{$school}/finance/payment-files", [
'file' => $file,
])->assertCreated();
$this->getJson("/api/schools/{$school}/reports/daily?date=2026-05-29")
->assertOk();
}
public function test_user_cannot_access_another_schools_data(): void
{
$schoolA = $this->createSchool('School A', 'S-A');
$schoolB = $this->createSchool('School B', 'S-B');
$userA = $this->createActorForSchool($schoolA);
$studentB = $this->createStudent($schoolB, 'OTHER-2001', 'Other');
$this->createTemplate(
$schoolB,
'attendance_absence',
'email',
'Hello, {{ student.first_name }} was marked absent.'
);
$this->actingAs($userA);
$this->assertTenantBlocked(
$this->getJson("/api/schools/{$schoolB}")
);
$this->assertTenantBlocked(
$this->postJson("/api/schools/{$schoolB}/students", [
'first_name' => 'Intruder',
'last_name' => 'Student',
'student_number' => 'BAD-1001',
'grade_level' => '8',
])
);
$this->assertTenantBlocked(
$this->getJson("/api/schools/{$schoolB}/students/{$studentB}")
);
$this->assertTenantBlocked(
$this->postJson("/api/schools/{$schoolB}/attendance/scans", [
'student_number' => 'OTHER-2001',
'scanned_at' => '2026-05-29T08:15:00Z',
'source' => 'kiosk',
])
);
$this->assertTenantBlocked(
$this->postJson("/api/schools/{$schoolB}/communications/preview", [
'template_key' => 'attendance_absence',
'student_id' => $studentB,
'channel' => 'email',
])
);
$this->assertTenantBlocked(
$this->postJson("/api/schools/{$schoolB}/communications/send", [
'template_key' => 'attendance_absence',
'student_id' => $studentB,
'channel' => 'email',
])
);
$file = UploadedFile::fake()->createWithContent(
'payments.csv',
"student_number,amount,paid_at,reference\nOTHER-2001,125.00,2026-05-29,PAY-BAD-001\n"
);
$this->assertTenantBlocked(
$this->postJson("/api/schools/{$schoolB}/finance/payment-files", [
'file' => $file,
])
);
$this->assertTenantBlocked(
$this->getJson("/api/schools/{$schoolB}/reports/daily?date=2026-05-29")
);
$this->assertDatabaseMissing('students', [
'school_id' => $schoolB,
'student_identifier' => 'BAD-1001',
]);
$this->assertDatabaseMissing('payments', [
'school_id' => $schoolB,
'reference_number' => 'PAY-BAD-001',
]);
}
public function test_guest_cannot_access_school_workflows(): void
{
$school = $this->createSchool('Guest Block School', 'GST');
$student = $this->createStudent($school, 'GUEST-1001', 'Guest');
$this->createTemplate(
$school,
'attendance_absence',
'email',
'Hello, {{ student.first_name }} was marked absent.'
);
$this->getJson("/api/schools/{$school}")
->assertUnauthorized();
$this->postJson("/api/schools/{$school}/students", [
'first_name' => 'Guest',
'last_name' => 'Student',
'student_number' => 'GUEST-2001',
'grade_level' => '7',
])->assertUnauthorized();
$this->getJson("/api/schools/{$school}/students/{$student}")
->assertUnauthorized();
$this->postJson("/api/schools/{$school}/attendance/scans", [
'student_number' => 'GUEST-1001',
'scanned_at' => '2026-05-29T08:15:00Z',
'source' => 'kiosk',
])->assertUnauthorized();
$this->postJson("/api/schools/{$school}/communications/preview", [
'template_key' => 'attendance_absence',
'student_id' => $student,
'channel' => 'email',
])->assertUnauthorized();
$this->postJson("/api/schools/{$school}/communications/send", [
'template_key' => 'attendance_absence',
'student_id' => $student,
'channel' => 'email',
])->assertUnauthorized();
$file = UploadedFile::fake()->createWithContent(
'payments.csv',
"student_number,amount,paid_at,reference\nGUEST-1001,125.00,2026-05-29,PAY-GUEST-001\n"
);
$this->postJson("/api/schools/{$school}/finance/payment-files", [
'file' => $file,
])->assertUnauthorized();
$this->getJson("/api/schools/{$school}/reports/daily?date=2026-05-29")
->assertUnauthorized();
}
private function createSchool(string $name = 'Test School', ?string $code = null): int
{
return (int) DB::table('schools')->insertGetId([
'name' => $name,
'code' => $code ?? 'SCH-'.uniqid(),
'domain_profile' => 'standard_school',
'timezone' => 'UTC',
'locale' => 'en',
'currency' => 'USD',
'active' => true,
'created_at' => now(),
'updated_at' => now(),
]);
}
private function createStudent(int $school, string $studentNumber, string $firstName = 'Maya'): int
{
return (int) DB::table('students')->insertGetId([
'school_id' => $school,
'school_id_number' => $studentNumber,
'student_identifier' => $studentNumber,
'first_name' => $firstName,
'last_name' => 'Patel',
'status' => 'active',
'profile_metadata' => json_encode(['grade_level' => '7'], JSON_THROW_ON_ERROR),
'metadata' => json_encode(['grade_level' => '7'], JSON_THROW_ON_ERROR),
'created_at' => now(),
'updated_at' => now(),
]);
}
private function createTemplate(int $school, string $key, string $channel, string $body): int
{
return (int) DB::table('communication_templates')->insertGetId([
'school_id' => $school,
'template_key' => $key,
'version' => '1',
'status' => 'active',
'channel' => $channel,
'message_category' => 'general',
'language' => 'en',
'subject' => 'Notice for {{ student.first_name }}',
'body' => $body,
'created_at' => now(),
'updated_at' => now(),
]);
}
private function createActorForSchool(int $school): Authenticatable
{
$this->ensureAuthTestTablesExist();
$userId = (int) DB::table('users')->insertGetId([
'name' => 'Test User '.$school,
'email' => 'user'.$school.'@example.test',
'password' => bcrypt('password'),
'created_at' => now(),
'updated_at' => now(),
]);
DB::table('school_user')->insert([
'school_id' => $school,
'user_id' => $userId,
'role' => 'admin',
'created_at' => now(),
'updated_at' => now(),
]);
return new GenericUser([
'id' => $userId,
'name' => 'Test User '.$school,
'email' => 'user'.$school.'@example.test',
]);
}
private function ensureAuthTestTablesExist(): void
{
if (! Schema::hasTable('users')) {
Schema::create('users', function ($table): void {
$table->id();
$table->string('name');
$table->string('email')->unique();
$table->string('password');
$table->rememberToken();
$table->timestamps();
});
}
if (! Schema::hasTable('school_user')) {
Schema::create('school_user', function ($table): void {
$table->id();
$table->unsignedBigInteger('school_id')->index();
$table->unsignedBigInteger('user_id')->index();
$table->string('role')->default('admin');
$table->timestamps();
$table->unique(['school_id', 'user_id']);
});
}
}
private function assertTenantBlocked($response): void
{
$this->assertContains(
$response->getStatusCode(),
[403, 404],
'Cross-tenant access must return either 403 or 404.'
);
}
}