add api tests
This commit is contained in:
@@ -1,2 +0,0 @@
|
||||
*
|
||||
!.gitignore
|
||||
+63
@@ -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',
|
||||
),
|
||||
),
|
||||
);
|
||||
+267
@@ -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 +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
|
||||
-19
@@ -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.
|
||||
-29
@@ -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
|
||||
-23
@@ -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
|
||||
-27
@@ -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
|
||||
@@ -1,4 +0,0 @@
|
||||
*
|
||||
!private/
|
||||
!public/
|
||||
!.gitignore
|
||||
@@ -1,2 +0,0 @@
|
||||
*
|
||||
!.gitignore
|
||||
@@ -1,2 +0,0 @@
|
||||
*
|
||||
!.gitignore
|
||||
@@ -1,9 +0,0 @@
|
||||
compiled.php
|
||||
config.php
|
||||
down
|
||||
events.scanned.php
|
||||
maintenance.php
|
||||
routes.php
|
||||
routes.scanned.php
|
||||
schedule-*
|
||||
services.json
|
||||
Vendored
BIN
Binary file not shown.
@@ -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);
|
||||
}
|
||||
}
|
||||
Vendored
BIN
Binary file not shown.
Vendored
BIN
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']);
|
||||
}
|
||||
}
|
||||
@@ -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([
|
||||
|
||||
Reference in New Issue
Block a user