add api tests

This commit is contained in:
root
2026-06-08 02:08:04 -04:00
parent 8ca24ef03f
commit 79024235ef
28 changed files with 1446 additions and 130 deletions
-2
View File
@@ -1,2 +0,0 @@
*
!.gitignore
Vendored Executable
+63
View File
@@ -0,0 +1,63 @@
<?php return array (
'laravel/pail' =>
array (
'providers' =>
array (
0 => 'Laravel\\Pail\\PailServiceProvider',
),
),
'laravel/sail' =>
array (
'providers' =>
array (
0 => 'Laravel\\Sail\\SailServiceProvider',
),
),
'laravel/sanctum' =>
array (
'providers' =>
array (
0 => 'Laravel\\Sanctum\\SanctumServiceProvider',
),
),
'laravel/tinker' =>
array (
'providers' =>
array (
0 => 'Laravel\\Tinker\\TinkerServiceProvider',
),
),
'nesbot/carbon' =>
array (
'providers' =>
array (
0 => 'Carbon\\Laravel\\ServiceProvider',
),
),
'nunomaduro/collision' =>
array (
'providers' =>
array (
0 => 'NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider',
),
),
'nunomaduro/termwind' =>
array (
'providers' =>
array (
0 => 'Termwind\\Laravel\\TermwindServiceProvider',
),
),
'php-open-source-saver/jwt-auth' =>
array (
'aliases' =>
array (
'JWTAuth' => 'PHPOpenSourceSaver\\JWTAuth\\Facades\\JWTAuth',
'JWTFactory' => 'PHPOpenSourceSaver\\JWTAuth\\Facades\\JWTFactory',
),
'providers' =>
array (
0 => 'PHPOpenSourceSaver\\JWTAuth\\Providers\\LaravelServiceProvider',
),
),
);
Vendored Executable
+267
View File
@@ -0,0 +1,267 @@
<?php return array (
'providers' =>
array (
0 => 'Illuminate\\Auth\\AuthServiceProvider',
1 => 'Illuminate\\Broadcasting\\BroadcastServiceProvider',
2 => 'Illuminate\\Bus\\BusServiceProvider',
3 => 'Illuminate\\Cache\\CacheServiceProvider',
4 => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
5 => 'Illuminate\\Concurrency\\ConcurrencyServiceProvider',
6 => 'Illuminate\\Cookie\\CookieServiceProvider',
7 => 'Illuminate\\Database\\DatabaseServiceProvider',
8 => 'Illuminate\\Encryption\\EncryptionServiceProvider',
9 => 'Illuminate\\Filesystem\\FilesystemServiceProvider',
10 => 'Illuminate\\Foundation\\Providers\\FoundationServiceProvider',
11 => 'Illuminate\\Hashing\\HashServiceProvider',
12 => 'Illuminate\\Mail\\MailServiceProvider',
13 => 'Illuminate\\Notifications\\NotificationServiceProvider',
14 => 'Illuminate\\Pagination\\PaginationServiceProvider',
15 => 'Illuminate\\Auth\\Passwords\\PasswordResetServiceProvider',
16 => 'Illuminate\\Pipeline\\PipelineServiceProvider',
17 => 'Illuminate\\Queue\\QueueServiceProvider',
18 => 'Illuminate\\Redis\\RedisServiceProvider',
19 => 'Illuminate\\Session\\SessionServiceProvider',
20 => 'Illuminate\\Translation\\TranslationServiceProvider',
21 => 'Illuminate\\Validation\\ValidationServiceProvider',
22 => 'Illuminate\\View\\ViewServiceProvider',
23 => 'Laravel\\Pail\\PailServiceProvider',
24 => 'Laravel\\Sail\\SailServiceProvider',
25 => 'Laravel\\Sanctum\\SanctumServiceProvider',
26 => 'Laravel\\Tinker\\TinkerServiceProvider',
27 => 'Carbon\\Laravel\\ServiceProvider',
28 => 'NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider',
29 => 'Termwind\\Laravel\\TermwindServiceProvider',
30 => 'PHPOpenSourceSaver\\JWTAuth\\Providers\\LaravelServiceProvider',
31 => 'App\\Providers\\AppServiceProvider',
32 => 'App\\Providers\\EventServiceProvider',
),
'eager' =>
array (
0 => 'Illuminate\\Auth\\AuthServiceProvider',
1 => 'Illuminate\\Cookie\\CookieServiceProvider',
2 => 'Illuminate\\Database\\DatabaseServiceProvider',
3 => 'Illuminate\\Encryption\\EncryptionServiceProvider',
4 => 'Illuminate\\Filesystem\\FilesystemServiceProvider',
5 => 'Illuminate\\Foundation\\Providers\\FoundationServiceProvider',
6 => 'Illuminate\\Notifications\\NotificationServiceProvider',
7 => 'Illuminate\\Pagination\\PaginationServiceProvider',
8 => 'Illuminate\\Session\\SessionServiceProvider',
9 => 'Illuminate\\View\\ViewServiceProvider',
10 => 'Laravel\\Pail\\PailServiceProvider',
11 => 'Laravel\\Sanctum\\SanctumServiceProvider',
12 => 'Carbon\\Laravel\\ServiceProvider',
13 => 'NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider',
14 => 'Termwind\\Laravel\\TermwindServiceProvider',
15 => 'PHPOpenSourceSaver\\JWTAuth\\Providers\\LaravelServiceProvider',
16 => 'App\\Providers\\AppServiceProvider',
17 => 'App\\Providers\\EventServiceProvider',
),
'deferred' =>
array (
'Illuminate\\Broadcasting\\BroadcastManager' => 'Illuminate\\Broadcasting\\BroadcastServiceProvider',
'Illuminate\\Contracts\\Broadcasting\\Factory' => 'Illuminate\\Broadcasting\\BroadcastServiceProvider',
'Illuminate\\Contracts\\Broadcasting\\Broadcaster' => 'Illuminate\\Broadcasting\\BroadcastServiceProvider',
'Illuminate\\Bus\\Dispatcher' => 'Illuminate\\Bus\\BusServiceProvider',
'Illuminate\\Contracts\\Bus\\Dispatcher' => 'Illuminate\\Bus\\BusServiceProvider',
'Illuminate\\Contracts\\Bus\\QueueingDispatcher' => 'Illuminate\\Bus\\BusServiceProvider',
'Illuminate\\Bus\\BatchRepository' => 'Illuminate\\Bus\\BusServiceProvider',
'Illuminate\\Bus\\DatabaseBatchRepository' => 'Illuminate\\Bus\\BusServiceProvider',
'cache' => 'Illuminate\\Cache\\CacheServiceProvider',
'cache.store' => 'Illuminate\\Cache\\CacheServiceProvider',
'cache.psr6' => 'Illuminate\\Cache\\CacheServiceProvider',
'memcached.connector' => 'Illuminate\\Cache\\CacheServiceProvider',
'Illuminate\\Cache\\RateLimiter' => 'Illuminate\\Cache\\CacheServiceProvider',
'Illuminate\\Foundation\\Console\\AboutCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Cache\\Console\\ClearCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Cache\\Console\\ForgetCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ClearCompiledCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Auth\\Console\\ClearResetsCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ConfigCacheCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ConfigClearCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ConfigShowCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Database\\Console\\DbCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Database\\Console\\MonitorCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Database\\Console\\PruneCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Database\\Console\\ShowCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Database\\Console\\TableCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Database\\Console\\WipeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\DownCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\EnvironmentCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\EnvironmentDecryptCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\EnvironmentEncryptCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\EventCacheCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\EventClearCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\EventListCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Concurrency\\Console\\InvokeSerializedClosureCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\KeyGenerateCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\OptimizeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\OptimizeClearCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\PackageDiscoverCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Cache\\Console\\PruneStaleTagsCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Queue\\Console\\ClearCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Queue\\Console\\ListFailedCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Queue\\Console\\FlushFailedCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Queue\\Console\\ForgetFailedCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Queue\\Console\\ListenCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Queue\\Console\\MonitorCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Queue\\Console\\PauseCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Queue\\Console\\PruneBatchesCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Queue\\Console\\PruneFailedJobsCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Queue\\Console\\RestartCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Queue\\Console\\ResumeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Queue\\Console\\RetryCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Queue\\Console\\RetryBatchCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Queue\\Console\\WorkCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ReloadCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\RouteCacheCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\RouteClearCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\RouteListCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Database\\Console\\DumpCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Database\\Console\\Seeds\\SeedCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Console\\Scheduling\\ScheduleFinishCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Console\\Scheduling\\ScheduleListCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Console\\Scheduling\\ScheduleRunCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Console\\Scheduling\\ScheduleClearCacheCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Console\\Scheduling\\ScheduleTestCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Console\\Scheduling\\ScheduleWorkCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Console\\Scheduling\\ScheduleInterruptCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Database\\Console\\ShowModelCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\StorageLinkCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\StorageUnlinkCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\UpCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ViewCacheCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ViewClearCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ApiInstallCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\BroadcastingInstallCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Cache\\Console\\CacheTableCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\CastMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ChannelListCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ChannelMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ClassMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ComponentMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ConfigMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ConfigPublishCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ConsoleMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Routing\\Console\\ControllerMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\DocsCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\EnumMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\EventGenerateCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\EventMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ExceptionMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Database\\Console\\Factories\\FactoryMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\InterfaceMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\JobMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\JobMiddlewareMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\LangPublishCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ListenerMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\MailMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Routing\\Console\\MiddlewareMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ModelMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\NotificationMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Notifications\\Console\\NotificationTableCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ObserverMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\PolicyMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ProviderMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Queue\\Console\\FailedTableCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Queue\\Console\\TableCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Queue\\Console\\BatchesTableCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\RequestMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ResourceMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\RuleMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ScopeMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Database\\Console\\Seeds\\SeederMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Session\\Console\\SessionTableCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ServeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\StubPublishCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\TestMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\TraitMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\VendorPublishCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ViewMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'migrator' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'migration.repository' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'migration.creator' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Database\\Migrations\\Migrator' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Database\\Console\\Migrations\\MigrateCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Database\\Console\\Migrations\\FreshCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Database\\Console\\Migrations\\InstallCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Database\\Console\\Migrations\\RefreshCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Database\\Console\\Migrations\\ResetCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Database\\Console\\Migrations\\RollbackCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Database\\Console\\Migrations\\StatusCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Database\\Console\\Migrations\\MigrateMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'composer' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Concurrency\\ConcurrencyManager' => 'Illuminate\\Concurrency\\ConcurrencyServiceProvider',
'hash' => 'Illuminate\\Hashing\\HashServiceProvider',
'hash.driver' => 'Illuminate\\Hashing\\HashServiceProvider',
'mail.manager' => 'Illuminate\\Mail\\MailServiceProvider',
'mailer' => 'Illuminate\\Mail\\MailServiceProvider',
'Illuminate\\Mail\\Markdown' => 'Illuminate\\Mail\\MailServiceProvider',
'auth.password' => 'Illuminate\\Auth\\Passwords\\PasswordResetServiceProvider',
'auth.password.broker' => 'Illuminate\\Auth\\Passwords\\PasswordResetServiceProvider',
'Illuminate\\Contracts\\Pipeline\\Hub' => 'Illuminate\\Pipeline\\PipelineServiceProvider',
'pipeline' => 'Illuminate\\Pipeline\\PipelineServiceProvider',
'queue' => 'Illuminate\\Queue\\QueueServiceProvider',
'queue.connection' => 'Illuminate\\Queue\\QueueServiceProvider',
'queue.failer' => 'Illuminate\\Queue\\QueueServiceProvider',
'queue.listener' => 'Illuminate\\Queue\\QueueServiceProvider',
'queue.worker' => 'Illuminate\\Queue\\QueueServiceProvider',
'redis' => 'Illuminate\\Redis\\RedisServiceProvider',
'redis.connection' => 'Illuminate\\Redis\\RedisServiceProvider',
'translator' => 'Illuminate\\Translation\\TranslationServiceProvider',
'translation.loader' => 'Illuminate\\Translation\\TranslationServiceProvider',
'validator' => 'Illuminate\\Validation\\ValidationServiceProvider',
'validation.presence' => 'Illuminate\\Validation\\ValidationServiceProvider',
'Illuminate\\Contracts\\Validation\\UncompromisedVerifier' => 'Illuminate\\Validation\\ValidationServiceProvider',
'Laravel\\Sail\\Console\\InstallCommand' => 'Laravel\\Sail\\SailServiceProvider',
'Laravel\\Sail\\Console\\PublishCommand' => 'Laravel\\Sail\\SailServiceProvider',
'command.tinker' => 'Laravel\\Tinker\\TinkerServiceProvider',
),
'when' =>
array (
'Illuminate\\Broadcasting\\BroadcastServiceProvider' =>
array (
),
'Illuminate\\Bus\\BusServiceProvider' =>
array (
),
'Illuminate\\Cache\\CacheServiceProvider' =>
array (
),
'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider' =>
array (
),
'Illuminate\\Concurrency\\ConcurrencyServiceProvider' =>
array (
),
'Illuminate\\Hashing\\HashServiceProvider' =>
array (
),
'Illuminate\\Mail\\MailServiceProvider' =>
array (
),
'Illuminate\\Auth\\Passwords\\PasswordResetServiceProvider' =>
array (
),
'Illuminate\\Pipeline\\PipelineServiceProvider' =>
array (
),
'Illuminate\\Queue\\QueueServiceProvider' =>
array (
),
'Illuminate\\Redis\\RedisServiceProvider' =>
array (
),
'Illuminate\\Translation\\TranslationServiceProvider' =>
array (
),
'Illuminate\\Validation\\ValidationServiceProvider' =>
array (
),
'Laravel\\Sail\\SailServiceProvider' =>
array (
),
'Laravel\\Tinker\\TinkerServiceProvider' =>
array (
),
),
);
-1
View File
@@ -1 +0,0 @@
*.sqlite*
@@ -1,11 +0,0 @@
# Phase 9 module ownership. Replace usernames/team slugs with real owners before enforcing.
/app/Domain/SchoolCore/Context/ @platform-owner
/app/Domain/SchoolCore/Finance/ @finance-owner @platform-owner
/app/Domain/SchoolCore/Attendance/ @attendance-owner @platform-owner
/app/Domain/SchoolCore/Students/ @students-owner @platform-owner
/app/Domain/SchoolCore/Communication/ @communication-owner @platform-owner
/app/Domain/SchoolCore/Reporting/ @reporting-owner @data-security-owner
/app/Domain/IslamicSundaySchool/ @islamic-sunday-school-owner @platform-owner
/app/Http/Controllers/ @api-owner
/routes/ @api-owner @platform-owner
/docs/ @platform-owner
@@ -1,19 +0,0 @@
## Modular Architecture Checklist
- [ ] SchoolContext is used where required.
- [ ] No SchoolCore dependency on IslamicSundaySchool.
- [ ] Controllers remain thin and delegate to contracts.
- [ ] FormRequest validates mutable endpoint input.
- [ ] Resource or ApiResponse controls output.
- [ ] Authorization is tested.
- [ ] Wrong-school access is tested for scoped data.
- [ ] Sensitive fields are protected or masked.
- [ ] New route appears in route inventory.
- [ ] New report/export is audited.
- [ ] Bulk communication requires preview.
- [ ] Finance changes avoid float math and direct controller mutation.
- [ ] Module owner reviewed.
## Risk Notes
List any boundary exceptions, deprecated paths, new routes, new reports/exports, or sensitive data exposure. If this section is empty for a high-risk change, assume the PR is lying by omission.
@@ -1,29 +0,0 @@
name: Architecture Governance
on:
pull_request:
push:
branches: [ main ]
jobs:
architecture:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
- name: Validate composer
run: composer validate --no-check-publish || true
- name: Install dependencies
run: composer install --no-interaction --prefer-dist || true
- name: Run architecture tests
run: php artisan test tests/Architecture || true
- name: Run architecture scan in warning mode
run: php artisan app:architecture-scan --fail-on=none || true
- name: Generate route inventory
run: php artisan api:route-inventory --markdown || true
- name: Check route inventory
run: php artisan app:route-inventory-check || true
- name: Generate dependency map
run: php artisan app:dependency-map --markdown || true
@@ -1,23 +0,0 @@
name: Release Gate
on:
workflow_dispatch:
push:
tags:
- 'v*'
jobs:
release-gate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
- run: composer install --no-interaction --prefer-dist || true
- run: php artisan test tests/Architecture
- run: php artisan app:architecture-scan --fail-on=error
- run: php artisan api:route-inventory --markdown
- run: php artisan app:route-inventory-check
- run: php artisan app:docs-coverage
- run: php artisan app:dependency-map --markdown
@@ -1,27 +0,0 @@
name: Release Readiness
on:
workflow_dispatch:
pull_request:
paths:
- 'app/**'
- 'config/**'
- 'routes/**'
- 'docs/release/**'
- '.github/workflows/release-readiness.yml'
jobs:
release-readiness:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
- run: composer install --no-interaction --prefer-dist
- run: php artisan app:release-feature-flags
- run: php artisan app:release-migration-validate
- run: php artisan app:release-shadow-compare
- run: php artisan app:legacy-unsafe-route-audit
- run: php artisan app:release-readiness-gate --warning-mode
- run: php artisan test tests/Feature/Release tests/Architecture/Phase10ReleaseReadinessArchitectureTest.php
View File
-4
View File
@@ -1,4 +0,0 @@
*
!private/
!public/
!.gitignore
-2
View File
@@ -1,2 +0,0 @@
*
!.gitignore
-2
View File
@@ -1,2 +0,0 @@
*
!.gitignore
-9
View File
@@ -1,9 +0,0 @@
compiled.php
config.php
down
events.scanned.php
maintenance.php
routes.php
routes.scanned.php
schedule-*
services.json
BIN
View File
Binary file not shown.
+117
View File
@@ -0,0 +1,117 @@
<?php
namespace Tests\Concerns;
use App\Models\Configuration;
use App\Models\Role;
use App\Models\SchoolYear;
use App\Models\User;
trait CreatesApiTestUsers
{
protected function seedApiTestConfig(string $schoolYear = '2025-2026', string $semester = 'Fall'): void
{
if (class_exists(Configuration::class)) {
Configuration::query()->updateOrCreate(['config_key' => 'school_year'], ['config_value' => $schoolYear]);
Configuration::query()->updateOrCreate(['config_key' => 'semester'], ['config_value' => $semester]);
}
if (class_exists(SchoolYear::class)) {
SchoolYear::query()->updateOrCreate(
['name' => $schoolYear],
[
'start_date' => '2025-09-01',
'end_date' => '2026-06-30',
'status' => SchoolYear::STATUS_ACTIVE,
'is_current' => true,
]
);
}
}
/**
* @return array<string, Role>
*/
protected function seedApiRoles(): array
{
$roles = [
'administrator' => ['priority' => 1, 'dashboard_route' => 'administrator/administratordashboard'],
'admin' => ['priority' => 2, 'dashboard_route' => 'administrator/administratordashboard'],
'teacher' => ['priority' => 3, 'dashboard_route' => 'teacher/classes'],
'parent' => ['priority' => 4, 'dashboard_route' => 'parents/profile'],
'student' => ['priority' => 5, 'dashboard_route' => 'student/dashboard'],
];
$created = [];
foreach ($roles as $name => $attributes) {
$created[$name] = Role::query()->updateOrCreate(
['name' => $name],
[
'slug' => $name,
'description' => ucfirst($name),
'priority' => $attributes['priority'],
'dashboard_route' => $attributes['dashboard_route'],
'is_active' => 1,
]
);
}
return $created;
}
protected function createApiUserWithRole(string $roleName = 'administrator', array $overrides = []): User
{
$roles = $this->seedApiRoles();
$this->seedApiTestConfig();
$user = User::factory()->create(array_merge([
'status' => 'Active',
'is_verified' => 1,
'is_suspended' => 0,
'email' => $roleName . '-api-test-' . str_replace('.', '', uniqid('', true)) . '@example.test',
], $overrides));
$role = $roles[$roleName] ?? Role::query()->where('name', $roleName)->firstOrFail();
$user->roles()->syncWithoutDetaching([$role->id]);
return $user->fresh() ?? $user;
}
protected function actingAsApiAdministrator(): User
{
$user = $this->createApiUserWithRole('administrator');
$this->actingAs($user, 'api');
return $user;
}
/**
* @return array<string, mixed>
*/
protected function validUserPayload(int $roleId, array $overrides = []): array
{
$unique = str_replace('.', '', uniqid('', true));
return array_merge([
'firstname' => 'Api',
'lastname' => 'Coverage',
'gender' => 'Male',
'cellphone' => '5551234567',
'email' => "api.coverage.$unique@example.test",
'address_street' => '1 Test Street',
'city' => 'Brooklyn',
'state' => 'NY',
'zip' => '11201',
'accept_school_policy' => true,
'role_id' => $roleId,
'password' => 'password',
'semester' => 'Fall',
'school_year' => '2025-2026',
'school_id' => 1,
'user_type' => 'primary',
'status' => 'Active',
'is_verified' => true,
'is_suspended' => false,
], $overrides);
}
}
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
@@ -0,0 +1,87 @@
<?php
namespace Tests\Feature\Api;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\Concerns\CreatesApiTestUsers;
use Tests\TestCase;
class ApiAttendanceTemplateFeatureTest extends TestCase
{
use CreatesApiTestUsers;
use RefreshDatabase;
public function test_attendance_comment_templates_can_be_managed_through_current_api(): void
{
$this->actingAsApiAdministrator();
$create = $this->postJson('/api/v1/attendance-comment-templates', [
'min_score' => 0,
'max_score' => 59,
'template_text' => 'Needs follow up with parent.',
'is_active' => true,
])
->assertCreated()
->assertJsonPath('status', 'success');
$id = (int) data_get($create->json(), 'data.id');
$this->assertGreaterThan(0, $id);
$this->getJson('/api/v1/attendance-comment-templates')
->assertOk()
->assertJsonPath('status', 'success')
->assertJsonFragment(['template_text' => 'Needs follow up with parent.']);
$this->getJson("/api/v1/attendance-comment-templates/$id")
->assertOk()
->assertJsonPath('data.id', $id);
$this->putJson("/api/v1/attendance-comment-templates/$id", [
'max_score' => 60,
'template_text' => 'Updated follow up text.',
])
->assertOk()
->assertJsonPath('status', 'success');
$this->deleteJson("/api/v1/attendance-comment-templates/$id")
->assertOk()
->assertJsonPath('status', 'success');
}
public function test_attendance_comment_template_validation_blocks_bad_ranges(): void
{
$this->actingAsApiAdministrator();
$this->postJson('/api/v1/attendance-comment-templates', [
'min_score' => 90,
'max_score' => 10,
'template_text' => '',
])
->assertStatus(422)
->assertJsonValidationErrors(['max_score', 'template_text']);
}
public function test_legacy_attendance_template_aliases_still_work(): void
{
$this->actingAsApiAdministrator();
$this->postJson('/api/v1/attendance-templates/save', [
'min_score' => 70,
'max_score' => 80,
'template_text' => 'Legacy alias text.',
'is_active' => 'on',
])
->assertOk()
->assertJsonPath('status', 'success');
$list = $this->getJson('/api/v1/attendance-templates')
->assertOk()
->assertJsonFragment(['template_text' => 'Legacy alias text.']);
$id = (int) collect($list->json('templates'))->firstWhere('template_text', 'Legacy alias text.')['id'];
$this->postJson('/api/v1/attendance-templates/delete', ['id' => $id])
->assertOk()
->assertJsonPath('status', 'success');
}
}
@@ -0,0 +1,126 @@
<?php
namespace Tests\Feature\Api;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Routing\Route as LaravelRoute;
use Illuminate\Support\Facades\Route;
use Tests\Concerns\CreatesApiTestUsers;
use Tests\TestCase;
class ApiAuthenticationAndAuthorizationTest extends TestCase
{
use CreatesApiTestUsers;
use RefreshDatabase;
public function test_auth_me_requires_authentication(): void
{
$this->getJson('/api/v1/auth/me')->assertUnauthorized();
}
public function test_auth_me_returns_the_current_authenticated_user(): void
{
$user = $this->createApiUserWithRole('teacher', [
'firstname' => 'Teacher',
'lastname' => 'Tester',
'email' => 'teacher.me@example.test',
]);
$this->actingAs($user, 'api')
->getJson('/api/v1/auth/me')
->assertOk()
->assertJsonPath('status', true)
->assertJsonPath('data.email', 'teacher.me@example.test');
}
public function test_admin_only_routes_reject_non_admin_users(): void
{
$parent = $this->createApiUserWithRole('parent');
$this->actingAs($parent, 'api');
foreach ($this->adminOnlyEndpoints() as [$method, $uri]) {
$response = $this->json($method, $uri);
$this->assertContains(
$response->getStatusCode(),
[401, 403],
"$method $uri should reject a non-admin user; got {$response->getStatusCode()}"
);
}
}
public function test_protected_api_routes_reject_anonymous_requests_before_business_logic(): void
{
$failures = [];
foreach ($this->sampleProtectedRoutes() as $route) {
$method = $this->primaryMethod($route);
$uri = '/' . $this->uriWithPlaceholders($route->uri());
$response = $this->json($method, $uri);
$status = $response->getStatusCode();
if (! in_array($status, [401, 403, 404, 405, 419, 422], true)) {
$failures[] = "$method $uri returned $status";
}
}
$this->assertSame([], $failures, "Anonymous requests reached unexpected responses:\n" . implode("\n", $failures));
}
/**
* @return list<array{0: string, 1: string}>
*/
private function adminOnlyEndpoints(): array
{
return [
['GET', '/api/v1/users'],
['POST', '/api/v1/users'],
['GET', '/api/v1/administrator/dashboard/metrics'],
['GET', '/api/v1/administrator/staff'],
['GET', '/api/v1/school-years'],
['GET', '/api/v1/administrator/role-permission/roles'],
];
}
/**
* @return list<LaravelRoute>
*/
private function sampleProtectedRoutes(): array
{
return collect(Route::getRoutes()->getRoutes())
->filter(fn (LaravelRoute $route): bool => str_starts_with($route->uri(), 'api/'))
->filter(function (LaravelRoute $route): bool {
return collect($route->gatherMiddleware())->contains(function (string $middleware): bool {
return str_contains($middleware, 'auth:api') || str_contains($middleware, 'jwt.auth');
});
})
->reject(function (LaravelRoute $route): bool {
$uri = $route->uri();
return str_contains($uri, 'files/')
|| str_contains($uri, 'receipts/')
|| str_contains($uri, 'reimbursements/')
|| str_contains($uri, 'attachments/');
})
->take(80)
->values()
->all();
}
private function primaryMethod(LaravelRoute $route): string
{
foreach ($route->methods() as $method) {
if (! in_array($method, ['HEAD', 'OPTIONS'], true)) {
return $method;
}
}
return 'GET';
}
private function uriWithPlaceholders(string $uri): string
{
return preg_replace('/\{[^}]+\}/', '1', $uri) ?? $uri;
}
}
@@ -0,0 +1,116 @@
<?php
namespace Tests\Feature\Api;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\Concerns\CreatesApiTestUsers;
use Tests\TestCase;
class ApiPreferencesFeatureTest extends TestCase
{
use CreatesApiTestUsers;
use RefreshDatabase;
public function test_authenticated_user_can_create_read_update_and_delete_preferences(): void
{
$user = $this->createApiUserWithRole('teacher');
$this->actingAs($user, 'api');
$this->postJson('/api/v1/preferences', [
'theme' => 'dark',
'language' => 'en',
'receive_email_notifications' => true,
'receive_sms_notifications' => false,
])
->assertStatus(201)
->assertJsonPath('status', true);
$this->getJson('/api/v1/preferences')
->assertOk()
->assertJsonPath('status', true)
->assertJsonPath('data.preferences.theme', 'dark');
$this->putJson("/api/v1/preferences/{$user->id}", [
'theme' => 'light',
'language' => 'es',
])
->assertOk()
->assertJsonPath('status', true);
$this->getJson("/api/v1/preferences/{$user->id}")
->assertOk()
->assertJsonPath('data.preferences.theme', 'light')
->assertJsonPath('data.preferences.language', 'es');
$this->deleteJson("/api/v1/preferences/{$user->id}")
->assertOk()
->assertJsonPath('status', true);
}
public function test_non_admin_cannot_list_or_manage_another_users_preferences(): void
{
$owner = $this->createApiUserWithRole('teacher');
$other = $this->createApiUserWithRole('parent');
$this->actingAs($owner, 'api');
$this->postJson('/api/v1/preferences', [
'theme' => 'dark',
'language' => 'en',
])->assertStatus(201);
$this->actingAs($other, 'api');
$this->postJson('/api/v1/preferences', [
'theme' => 'light',
'language' => 'en',
])->assertStatus(201);
$this->getJson('/api/v1/preferences/list')
->assertForbidden();
$this->getJson("/api/v1/preferences/{$owner->id}")
->assertForbidden();
$this->putJson("/api/v1/preferences/{$owner->id}", [
'theme' => 'light',
'language' => 'es',
])->assertForbidden();
$this->deleteJson("/api/v1/preferences/{$owner->id}")
->assertForbidden();
}
public function test_admin_can_list_and_delete_any_users_preferences(): void
{
$owner = $this->createApiUserWithRole('teacher');
$admin = $this->createApiUserWithRole('administrator');
$this->actingAs($owner, 'api');
$this->postJson('/api/v1/preferences', [
'theme' => 'dark',
'language' => 'en',
])->assertStatus(201);
$this->actingAs($admin, 'api');
$this->getJson('/api/v1/preferences/list')
->assertOk()
->assertJsonPath('status', true);
$this->deleteJson("/api/v1/preferences/{$owner->id}")
->assertOk()
->assertJsonPath('status', true);
}
public function test_preferences_validation_rejects_invalid_options(): void
{
$user = $this->createApiUserWithRole('teacher');
$this->actingAs($user, 'api');
$this->postJson('/api/v1/preferences', [
'theme' => 'neon-chaos',
'language' => 'klingon',
])
->assertStatus(422)
->assertJsonValidationErrors(['theme', 'language']);
}
}
@@ -0,0 +1,60 @@
<?php
namespace Tests\Feature\Api;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class ApiPublicEndpointsTest extends TestCase
{
use RefreshDatabase;
public function test_health_endpoint_is_available(): void
{
$this->getJson('/api/v1/health')
->assertOk();
}
public function test_public_policy_endpoints_are_available(): void
{
foreach (['/api/v1/policies/school', '/api/v1/policies/picture'] as $uri) {
$response = $this->getJson($uri);
$this->assertLessThan(500, $response->getStatusCode(), "$uri returned a server error");
}
}
public function test_public_static_pages_are_available(): void
{
foreach (['privacy', 'terms', 'help'] as $page) {
$response = $this->getJson("/api/v1/pages/$page");
$this->assertLessThan(500, $response->getStatusCode(), "/api/v1/pages/$page returned a server error");
}
}
public function test_public_frontend_content_endpoints_do_not_server_error(): void
{
foreach (['/', 'facility', 'team', 'call-to-action', 'testimonial', 'not-found'] as $path) {
$uri = '/api/v1/frontend' . ($path === '/' ? '' : '/' . $path);
$response = $this->getJson($uri);
$this->assertLessThan(500, $response->getStatusCode(), "$uri returned a server error");
}
}
public function test_registration_captcha_endpoint_returns_captcha_payload(): void
{
$this->getJson('/api/v1/auth/register/captcha')
->assertOk()
->assertJsonPath('ok', true)
->assertJsonStructure(['ok', 'captcha']);
}
public function test_invalid_login_payload_is_rejected_cleanly(): void
{
$this->postJson('/api/v1/auth/login', [])
->assertStatus(400)
->assertJsonPath('status', false);
}
}
@@ -0,0 +1,108 @@
<?php
namespace Tests\Feature\Api;
use App\Models\Permission;
use App\Models\Role;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\Concerns\CreatesApiTestUsers;
use Tests\TestCase;
class ApiRolePermissionFeatureTest extends TestCase
{
use CreatesApiTestUsers;
use RefreshDatabase;
public function test_admin_can_manage_roles_permissions_and_assign_roles(): void
{
$this->actingAsApiAdministrator();
$roles = $this->seedApiRoles();
$target = User::factory()->create(['email' => 'assign-role-target@example.test']);
$roleResponse = $this->postJson('/api/v1/administrator/role-permission/roles', [
'name' => 'finance_manager',
'slug' => 'finance-manager',
'description' => 'Finance Manager',
'dashboard_route' => 'finance/dashboard',
'priority' => 20,
'is_active' => 1,
])
->assertCreated()
->assertJsonPath('status', true);
$roleId = (int) data_get($roleResponse->json(), 'data.role.id');
$this->assertGreaterThan(0, $roleId);
$this->getJson('/api/v1/administrator/role-permission/roles')
->assertOk()
->assertJsonFragment(['name' => 'finance_manager']);
$this->patchJson("/api/v1/administrator/role-permission/roles/$roleId", [
'description' => 'Updated Finance Manager',
'priority' => 21,
])
->assertOk()
->assertJsonPath('status', true);
$permissionResponse = $this->postJson('/api/v1/administrator/role-permission/permissions', [
'name' => 'finance.reports.view',
'description' => 'View finance reports',
])
->assertCreated()
->assertJsonPath('status', true);
$permissionId = (int) data_get($permissionResponse->json(), 'data.permission.id');
$this->assertGreaterThan(0, $permissionId);
$this->putJson("/api/v1/administrator/role-permission/roles/$roleId/permissions", [
'permissions' => [
(string) $permissionId => [
'read' => true,
'create' => false,
'update' => false,
'delete' => false,
'manage' => false,
],
],
])
->assertOk()
->assertJsonPath('status', true);
$this->postJson("/api/v1/administrator/role-permission/users/{$target->id}/roles", [
'role_ids' => [$roles['parent']->id, $roleId],
])
->assertOk()
->assertJsonPath('status', true);
$this->assertDatabaseHas('roles', ['id' => $roleId, 'priority' => 21]);
$this->assertDatabaseHas('permissions', ['id' => $permissionId, 'name' => 'finance.reports.view']);
$this->assertContains('finance_manager', $target->fresh()->roleNames());
$this->deleteJson("/api/v1/administrator/role-permission/permissions/$permissionId")
->assertOk()
->assertJsonPath('status', true);
$this->deleteJson("/api/v1/administrator/role-permission/roles/$roleId")
->assertOk()
->assertJsonPath('status', true);
}
public function test_role_permission_validation_rejects_bad_payloads(): void
{
$this->actingAsApiAdministrator();
$this->postJson('/api/v1/administrator/role-permission/roles', [
'name' => 'ab',
'dashboard_route' => 'bad route with spaces',
'priority' => -1,
'is_active' => 2,
])
->assertStatus(422)
->assertJsonValidationErrors(['name', 'dashboard_route', 'priority', 'is_active']);
$this->postJson('/api/v1/administrator/role-permission/permissions', [])
->assertStatus(422)
->assertJsonValidationErrors(['name']);
}
}
+109
View File
@@ -0,0 +1,109 @@
<?php
namespace Tests\Feature\Api;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Routing\Route as LaravelRoute;
use Illuminate\Support\Facades\Route;
use Tests\TestCase;
class ApiRouteContractTest extends TestCase
{
use RefreshDatabase;
public function test_every_api_route_points_to_an_existing_controller_method(): void
{
$missing = [];
foreach ($this->apiRoutes() as $route) {
$action = $route->getActionName();
if ($action === 'Closure' || ! str_contains($action, '@')) {
continue;
}
[$class, $method] = explode('@', $action, 2);
if (! class_exists($class) || ! method_exists($class, $method)) {
$missing[] = implode('|', $route->methods()) . ' ' . $route->uri() . ' -> ' . $action;
}
}
$this->assertSame([], $missing, "These API routes reference missing controllers or methods:\n" . implode("\n", $missing));
}
public function test_api_routes_do_not_duplicate_method_and_uri_pairs(): void
{
$seen = [];
$duplicates = [];
foreach ($this->apiRoutes() as $route) {
foreach ($route->methods() as $method) {
if ($method === 'HEAD') {
continue;
}
$key = $method . ' ' . $route->uri();
if (isset($seen[$key])) {
$duplicates[] = $key;
}
$seen[$key] = true;
}
}
$this->assertSame([], array_values(array_unique($duplicates)), "Duplicate API route method/URI pairs found. Ambiguous routing is not a feature, despite humanity's best efforts.");
}
public function test_mutating_api_routes_are_protected_unless_explicitly_public(): void
{
$publicAllowList = [
'api/login',
'api/register',
'api/v1/login',
'api/v1/register',
'api/v1/auth/login',
'api/v1/auth/register',
'api/v1/pages/contact',
'api/v1/contact',
'api/set_authorized_user_password/{authorizedUserId}',
'api/v1/badge_scan/scan',
'api/v1/scanner/process',
];
$unprotected = [];
foreach ($this->apiRoutes() as $route) {
$methods = array_diff($route->methods(), ['HEAD', 'OPTIONS']);
if (array_intersect($methods, ['POST', 'PUT', 'PATCH', 'DELETE']) === []) {
continue;
}
if (in_array($route->uri(), $publicAllowList, true)) {
continue;
}
$middleware = $route->gatherMiddleware();
$hasProtection = collect($middleware)->contains(function (string $middleware): bool {
return str_contains($middleware, 'auth')
|| str_contains($middleware, 'throttle')
|| str_contains($middleware, 'teacher.portal.auth')
|| str_contains($middleware, 'badge_scan');
});
if (! $hasProtection) {
$unprotected[] = implode('|', $methods) . ' ' . $route->uri();
}
}
$this->assertSame([], $unprotected, "Mutating API routes without an obvious auth/throttle middleware:\n" . implode("\n", $unprotected));
}
/**
* @return list<LaravelRoute>
*/
private function apiRoutes(): array
{
return array_values(array_filter(Route::getRoutes()->getRoutes(), function (LaravelRoute $route): bool {
return str_starts_with($route->uri(), 'api/');
}));
}
}
@@ -0,0 +1,88 @@
<?php
namespace Tests\Feature\Api;
use App\Models\Role;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Hash;
use Tests\Concerns\CreatesApiTestUsers;
use Tests\TestCase;
class ApiUserManagementFeatureTest extends TestCase
{
use CreatesApiTestUsers;
use RefreshDatabase;
public function test_admin_can_create_list_update_and_delete_a_user_through_api(): void
{
$this->actingAsApiAdministrator();
$roles = $this->seedApiRoles();
$createPayload = $this->validUserPayload((int) $roles['parent']->id, [
'email' => 'api-user-crud@example.test',
'firstname' => 'Original',
]);
$create = $this->postJson('/api/v1/users', $createPayload)
->assertCreated()
->assertJsonPath('ok', true);
$userId = (int) $create->json('user_id');
$this->assertGreaterThan(0, $userId);
$created = User::query()->findOrFail($userId);
$this->assertSame('api-user-crud@example.test', $created->email);
$this->assertTrue(Hash::check('password', (string) $created->password));
$this->assertSame(['parent'], $created->roleNames());
$this->getJson('/api/v1/users')
->assertOk()
->assertJsonPath('ok', true)
->assertJsonFragment(['email' => 'api-user-crud@example.test']);
$this->putJson("/api/v1/users/$userId", [
'firstname' => 'Updated',
'status' => 'Inactive',
])
->assertOk()
->assertJsonPath('ok', true);
$this->assertDatabaseHas('users', [
'id' => $userId,
'firstname' => 'Updated',
'status' => 'Inactive',
]);
$this->deleteJson("/api/v1/users/$userId")
->assertOk()
->assertJsonPath('ok', true);
}
public function test_user_creation_validates_required_payload_and_unique_email(): void
{
$this->actingAsApiAdministrator();
$roles = $this->seedApiRoles();
$this->postJson('/api/v1/users', [])
->assertStatus(422)
->assertJsonValidationErrors(['firstname', 'lastname', 'cellphone', 'email', 'address_street', 'city', 'state', 'zip', 'accept_school_policy', 'role_id', 'password', 'semester']);
User::factory()->create(['email' => 'already-used@example.test']);
$this->postJson('/api/v1/users', $this->validUserPayload((int) $roles['teacher']->id, [
'email' => 'already-used@example.test',
]))
->assertStatus(422)
->assertJsonValidationErrors(['email']);
}
public function test_user_creation_rejects_nonexistent_role_id(): void
{
$this->actingAsApiAdministrator();
$this->postJson('/api/v1/users', $this->validUserPayload(999999))
->assertStatus(422)
->assertJsonValidationErrors(['role_id']);
}
}
@@ -0,0 +1,249 @@
<?php
namespace Tests\Feature\Api\V1\PrintRequests;
use App\Models\PrintRequest;
use App\Models\Role;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
class PrintRequestsWorkflowTest extends TestCase
{
use RefreshDatabase;
public function test_teacher_can_create_hand_copy_request_and_admin_can_advance_statuses(): void
{
$teacher = $this->createUserWithRole('teacher');
$admin = $this->createUserWithRole('administrator');
$classSectionId = $this->seedClassSection();
$this->actingAs($teacher, 'api');
$createResponse = $this->postJson('/api/v1/print-requests/hand-copy', [
'class_id' => $classSectionId,
'num_copies' => 3,
'required_by' => '2026-09-13 11:00:00',
'pickup_method' => 'Self Pickup',
]);
$createResponse
->assertCreated()
->assertJsonPath('status', true)
->assertJsonPath('data.print_request.status', 'not_assigned');
$requestId = (int) $createResponse->json('data.print_request.id');
$this->assertDatabaseHas('print_requests', [
'id' => $requestId,
'teacher_id' => $teacher->id,
'class_id' => $classSectionId,
'file_path' => '',
'num_copies' => 3,
'status' => 'not_assigned',
]);
$this->actingAs($admin, 'api');
foreach (['assigned', 'done', 'delivered'] as $status) {
$this->patchJson("/api/v1/print-requests/{$requestId}/status", [
'status' => $status,
])
->assertOk()
->assertJsonPath('status', true)
->assertJsonPath('data.print_request.status', $status);
}
$this->assertDatabaseHas('print_requests', [
'id' => $requestId,
'admin_id' => $admin->id,
'status' => 'delivered',
]);
}
public function test_admin_status_rejects_invalid_transition(): void
{
$teacher = $this->createUserWithRole('teacher');
$admin = $this->createUserWithRole('administrator');
$request = $this->seedPrintRequest($teacher, ['status' => 'delivered']);
$this->actingAs($admin, 'api');
$this->patchJson("/api/v1/print-requests/{$request->id}/status", [
'status' => 'assigned',
])
->assertUnprocessable()
->assertJsonPath('status', false);
$this->assertDatabaseHas('print_requests', [
'id' => $request->id,
'status' => 'delivered',
]);
}
public function test_teacher_routes_reject_non_teacher_roles_and_admin_routes_reject_teacher_roles(): void
{
$parent = $this->createUserWithRole('parent');
$teacher = $this->createUserWithRole('teacher');
$this->actingAs($parent, 'api');
$this->getJson('/api/v1/print-requests/teacher')
->assertForbidden();
$this->actingAs($teacher, 'api');
$this->getJson('/api/v1/print-requests/admin')
->assertForbidden();
}
public function test_teacher_cannot_update_delete_or_download_another_teachers_request(): void
{
$owner = $this->createUserWithRole('teacher');
$intruder = $this->createUserWithRole('teacher');
$request = $this->seedPrintRequest($owner, [
'status' => 'not_assigned',
'file_path' => 'missing-file.pdf',
]);
$this->actingAs($intruder, 'api');
$this->patchJson("/api/v1/print-requests/{$request->id}", [
'num_copies' => 8,
'required_by' => '2026-09-20 11:00:00',
'pickup_method' => 'Self Pickup',
])->assertForbidden();
$this->deleteJson("/api/v1/print-requests/{$request->id}")
->assertForbidden();
$this->getJson("/api/v1/print-requests/{$request->id}/file")
->assertForbidden();
$this->assertDatabaseHas('print_requests', [
'id' => $request->id,
'teacher_id' => $owner->id,
'num_copies' => 1,
]);
}
public function test_teacher_cannot_delete_request_after_processing_has_started(): void
{
$teacher = $this->createUserWithRole('teacher');
$request = $this->seedPrintRequest($teacher, ['status' => 'done']);
$this->actingAs($teacher, 'api');
$this->deleteJson("/api/v1/print-requests/{$request->id}")
->assertBadRequest()
->assertJsonPath('status', false);
$this->assertDatabaseHas('print_requests', [
'id' => $request->id,
'status' => 'done',
]);
}
public function test_upload_requires_valid_file_and_page_selection_format(): void
{
$teacher = $this->createUserWithRole('teacher');
$classSectionId = $this->seedClassSection();
$this->actingAs($teacher, 'api');
$response = $this->post('/api/v1/print-requests', [
'file' => UploadedFile::fake()->create('lesson.pdf', 12, 'application/pdf'),
'class_id' => $classSectionId,
'num_copies' => 1,
'required_by' => '2026-09-13 11:00:00',
'pickup_method' => 'Self Pickup',
'page_selection' => 'one through five',
]);
$response
->assertUnprocessable()
->assertJsonPath('status', false)
->assertJsonStructure(['errors' => ['page_selection']]);
}
public function test_teacher_can_upload_and_download_own_file_and_admin_can_download_any_request_file(): void
{
$teacher = $this->createUserWithRole('teacher');
$admin = $this->createUserWithRole('administrator');
$classSectionId = $this->seedClassSection();
$this->actingAs($teacher, 'api');
$createResponse = $this->post('/api/v1/print-requests', [
'file' => UploadedFile::fake()->create('lesson.pdf', 12, 'application/pdf'),
'class_id' => $classSectionId,
'num_copies' => 2,
'required_by' => '2026-09-13 11:00:00',
'pickup_method' => 'Self Pickup',
'page_selection' => '1-3,5',
]);
$createResponse
->assertCreated()
->assertJsonPath('status', true);
$requestId = (int) $createResponse->json('data.print_request.id');
$this->actingAs($teacher, 'api');
$this->get("/api/v1/print-requests/{$requestId}/file")
->assertOk();
$this->actingAs($admin, 'api');
$this->get("/api/v1/print-requests/{$requestId}/file")
->assertOk();
}
private function createUserWithRole(string $roleName): User
{
$role = Role::query()->firstOrCreate(
['name' => $roleName],
[
'slug' => $roleName,
'description' => ucfirst(str_replace('_', ' ', $roleName)),
'dashboard_route' => $roleName === 'teacher' ? 'teacher/classes' : 'administrator/administratordashboard',
'priority' => 1,
'is_active' => 1,
]
);
$user = User::factory()->create([
'status' => 'Active',
'is_verified' => 1,
'is_suspended' => 0,
'email' => $roleName . '-print-request-' . str_replace('.', '', uniqid('', true)) . '@example.test',
]);
$user->roles()->syncWithoutDetaching([$role->id]);
return $user->fresh() ?? $user;
}
private function seedClassSection(): int
{
DB::table('classSection')->insert([
'class_id' => 1,
'class_section_id' => 9901,
'class_section_name' => 'Print Request Test Class',
'semester' => 'Fall',
'school_year' => '2026-2027',
]);
return 9901;
}
/**
* @param array<string, mixed> $overrides
*/
private function seedPrintRequest(User $teacher, array $overrides = []): PrintRequest
{
return PrintRequest::query()->create(array_merge([
'teacher_id' => $teacher->id,
'admin_id' => null,
'class_id' => $this->seedClassSection(),
'file_path' => '',
'page_selection' => null,
'num_copies' => 1,
'required_by' => '2026-09-13 11:00:00',
'pickup_method' => 'Self Pickup',
'status' => 'not_assigned',
], $overrides));
}
}
@@ -33,7 +33,13 @@ class RoleSwitcherControllerTest extends TestCase
public function test_switch_sets_role(): void
{
$user = $this->seedUser();
$this->seedRole();
$roleId = $this->seedRole();
DB::table('user_roles')->insert([
'user_id' => $user->id,
'role_id' => $roleId,
]);
Sanctum::actingAs($user);
$response = $this->postJson('/api/v1/role-switcher/switch', [
@@ -52,6 +58,55 @@ class RoleSwitcherControllerTest extends TestCase
$response->assertStatus(401);
}
public function test_switch_rejects_role_that_user_does_not_have(): void
{
$user = $this->seedUser();
$this->seedRole();
$teacherRoleId = DB::table('roles')->insertGetId([
'name' => 'teacher',
'slug' => 'teacher',
'description' => 'Teacher',
'dashboard_route' => 'teacher/classes',
'priority' => 2,
'is_active' => 1,
'created_at' => now(),
'updated_at' => now(),
]);
DB::table('user_roles')->insert([
'user_id' => $user->id,
'role_id' => $teacherRoleId,
]);
Sanctum::actingAs($user);
$this->postJson('/api/v1/role-switcher/switch', [
'role' => 'admin',
])
->assertForbidden()
->assertJsonPath('status', false);
}
public function test_switch_validates_empty_role_payload(): void
{
$user = $this->seedUser();
$this->seedRole();
Sanctum::actingAs($user);
$this->postJson('/api/v1/role-switcher/switch', [])
->assertUnprocessable();
}
public function test_index_returns_not_found_when_authenticated_user_has_no_roles(): void
{
$user = $this->seedUser();
Sanctum::actingAs($user);
$this->getJson('/api/v1/role-switcher')
->assertNotFound()
->assertJsonPath('status', false);
}
private function seedUser(): User
{
$userId = DB::table('users')->insertGetId([