diff --git a/.github/workflows/check-coding-standards.yml b/.github/workflows/check-coding-standards.yml
deleted file mode 100644
index 222aae0..0000000
--- a/.github/workflows/check-coding-standards.yml
+++ /dev/null
@@ -1,32 +0,0 @@
-name: Check Coding Standards
-
-on:
- push:
- paths:
- - '**.php'
-
-jobs:
- ci:
- runs-on: ubuntu-latest
-
- steps:
- - name: Checkout code
- uses: actions/checkout@v3
-
- - name: Setup PHP
- uses: shivammathur/setup-php@v2 #https://github.com/shivammathur/setup-php
- with:
- php-version: '8.1'
- extensions: mbstring, dom, fileinfo
-
- - name: Install Composer dependencies
- run: composer update --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist
-
- - name: Coding Style Checks
- run: composer test:lint
-
- - name: Type Checks
- run: composer test:types
-
- - name: Type Coverage Checks
- run: composer test:type-coverage
diff --git a/.github/workflows/laravel.yml b/.github/workflows/laravel.yml
new file mode 100644
index 0000000..45cff04
--- /dev/null
+++ b/.github/workflows/laravel.yml
@@ -0,0 +1,74 @@
+name: CI
+
+on:
+ push:
+ branches: [ 3.x, 4.x ]
+ pull_request:
+ branches: [ 3.x, 4.x ]
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ laravel:
+ runs-on: ubuntu-latest
+
+ services:
+ postgres:
+ image: postgres:17
+ env:
+ POSTGRES_USER: seatplus
+ POSTGRES_PASSWORD: secret
+ POSTGRES_DB: laravel
+ ports:
+ - 5432:5432
+ options: >-
+ --health-cmd pg_isready
+ --health-interval 10s
+ --health-timeout 5s
+ --health-retries 5
+
+ redis:
+ image: redis:7
+ ports:
+ - 6379:6379
+ options: >-
+ --health-cmd "redis-cli ping"
+ --health-interval 10s
+ --health-timeout 5s
+ --health-retries 5
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Setup PHP
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: '8.3'
+ extensions: mbstring, dom, fileinfo, pgsql, pdo_pgsql, redis
+ coverage: xdebug
+
+ - name: Cache Composer dependencies
+ uses: actions/cache@v4
+ with:
+ path: vendor
+ key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
+ restore-keys: ${{ runner.os }}-composer-
+
+ - name: Install Dependencies
+ run: composer install --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist
+
+ - name: Check Coding Standards
+ run: composer run test:lint
+
+ - name: Static Analysis
+ run: composer run test:types
+
+ - name: Type Coverage
+ run: composer run test:type-coverage
+
+ - name: Run Tests
+ env:
+ XDEBUG_MODE: coverage
+ run: vendor/bin/pest --coverage --min=100 --colors=always
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
deleted file mode 100644
index bc5e396..0000000
--- a/.github/workflows/tests.yml
+++ /dev/null
@@ -1,42 +0,0 @@
-name: Tests
-
-on:
- push:
- branches: [ 2.x, 3.x ]
- pull_request:
- branches: [ 2.x, 3.x ]
-
-jobs:
- laravel:
-
- runs-on: ubuntu-latest
-
- steps:
- - uses: actions/checkout@v2
- - name: Setup PHP, with composer and extensions
- uses: shivammathur/setup-php@v2 #https://github.com/shivammathur/setup-php
- with:
- php-version: '8.1'
- extensions: mbstring, dom, fileinfo
- coverage: xdebug #optional
- - uses: getong/mariadb-action@v1.1
- with:
- host port: 3308 # Optional, default value is 3306. The port of host
- mariadb version: '10.7' # Optional, default value is "latest". The version of the MariaDB
- mysql database: 'testbench' # Optional, default value is "test". The specified database which will be create
- mysql user: 'default' # Required if "mysql root password" is empty, default is empty. The superuser for the specified database. Can use secrets, too
- mysql password: 'secret' # Required if "mysql user" exists. The password for the "mysql user"
- - name: Redis Server in GitHub Actions
- uses: supercharge/redis-github-action@1.1.0
- with:
- # Redis version to use
- redis-version: 5 # optional, default is latest
- - name: Install Dependencies
- run: composer install --no-ansi --no-interaction --no-scripts --prefer-dist
- - name: Test & publish code coverage
- uses: paambaati/codeclimate-action@v2.6.0
- env:
- CC_TEST_REPORTER_ID: dfdd27f143f73ad7911f5393e3378b9b989cfed884bf522fa3b0d9fbc2b8d4c1
- with:
- coverageCommand: vendor/bin/pest --coverage --ci
- debug: false
diff --git a/.gitignore b/.gitignore
index 1c18527..3a1a1c8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,5 +8,6 @@ build/
.php_cs
.php_cs.cache
.phpunit.result.cache
+.phpunit.cache
.php-cs-fixer.cache
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..268121d
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,28 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
+and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+## [Unreleased]
+
+Upgrading to this version will require you to update your `auth` package to `^4.0.0`.
+You are required to implement the `auth.login` and `auth.login` routes in your application. The `LoginController` and `LogoutController` have been removed from the package.
+
+
+### Added
+- Introduced LoginAssetAction to serve login assets
+- Introduced LogoutAction to serve logout assets
+
+### Changed
+- Switching main character has changed to use `PUT: auth/main-character/switch/{new_character_id}`. The correct route parameters are now required.
+
+### Fixed
+
+### Removed
+- Removed login controller and route
+- Removed logout controller and route
+
+## [4.0.0] - 2024-09-01
+### Added
diff --git a/README.md b/README.md
index 95f13e6..4505257 100644
--- a/README.md
+++ b/README.md
@@ -1,11 +1,87 @@
-# auth
-handels authentication for web and eveapi
+# seatplus/auth
-# Usage
+[](https://github.com/seatplus/auth/actions/workflows/laravel.yml)
-## Add more scopes
-By default the minimal scopes are requested for users. However one might add scopes to an existing user by adding
-a query parameters stating comma separated which scopes should be add:
+Handles authentication, authorisation, and SSO scope compliance for the seatplus EVE Online management platform. This is the core package — `seatplus/eveapi` and `seatplus/web` both depend on it.
+
+## Overview
+
+### Role system
+
+Four role types with distinct membership and permission semantics:
+
+| Type | Membership | Use case |
+|------|-----------|---------|
+| `automatic` | Auto-assigned when a character belongs to a configured corporation or alliance | Fleet / alliance access |
+| `on-request` | User applies, moderator approves or denies | Corp-specific elevated access |
+| `manual` | Admin explicitly adds / removes individual users | One-off grants |
+| `opt-in` | User self-joins if they meet the criteria | Opt-in programmes |
+
+### Affiliation system
+
+Every role has `Affiliation` records that define **permission scope** (which EVE entities the role holder can access data for), not membership. Three types:
+
+- `allowed` — these corporations / alliances / characters are in scope
+- `inverse` — everyone *except* these is in scope
+- `forbidden` — always excluded, overrides `allowed` / `inverse`
+
+### SSO scope compliance
+
+`IsUserCompliantService` checks whether every character owned by a user has all required OAuth scopes. Required scopes are aggregated from global settings, corporation-level `SsoScopes` records, and alliance-level records. Non-compliant users have their role memberships set to `inactive` automatically on the next `handleMembers()` call.
+
+### Permission checking
+
+`CanUserService::check()` runs a Laravel Pipeline to validate a set of EVE entity IDs against a user's permissions. The pipeline strips IDs the user owns, IDs covered by in-game corporation roles (e.g. Director), and IDs covered by Spatie permissions. Any remaining IDs are denied. The `superuser` permission bypasses all checks.
+
+## Installation
+
+```bash
+composer require seatplus/auth
+```
+
+Publish and run migrations:
+
+```bash
+php artisan vendor:publish --provider="Seatplus\Auth\AuthServiceProvider"
+php artisan migrate
```
-/eve/sso/{character_id?}/step_up?add_scopes=scope1,scope2
+
+## Usage
+
+### Add OAuth scopes to a character
+
+By default the minimal scopes are requested. To step up a character to additional scopes, redirect to:
+
+```
+/eve/sso/{character_id}/step_up?add_scopes=esi-skills.read_skills.v1,esi-wallet.read_character_wallet.v1
+```
+
+### Check permissions
+
+```php
+use Seatplus\Auth\Services\Dtos\ValidateIdsDTO;
+use Seatplus\Auth\Services\CanUserService;
+
+$dto = ValidateIdsDTO::make(entity_ids: [12345678], user: $user);
+CanUserService::check($user, $dto, permissions: ['view member tracking']);
+```
+
+## Development
+
+### Requirements
+
+- PHP 8.3+
+- PostgreSQL (user `seatplus`, password `secret`, database `laravel` @ `127.0.0.1:5432`)
+- Redis @ `127.0.0.1:6379`
+
+### Running the test suite
+
+```bash
+composer run test # lint + PHPStan + type-coverage + unit tests
+composer run test:unit # unit tests only
+composer run test:lint # Pint formatting check
+composer run lint # auto-fix formatting with Pint
+composer run test:types # PHPStan static analysis
+composer run test:type-coverage # 100% type coverage check
```
+
diff --git a/composer.json b/composer.json
index 34b5b08..005f1fe 100644
--- a/composer.json
+++ b/composer.json
@@ -15,7 +15,6 @@
"Seatplus\\Auth\\Database\\Factories\\": "database/factories/"
},
"files": [
- "src/Helpers/helpers.php"
]
},
"autoload-dev": {
@@ -23,25 +22,42 @@
"Seatplus\\Auth\\Tests\\": "tests/"
}
},
- "minimum-stability": "stable",
+ "minimum-stability": "dev",
"prefer-stable": true,
+ "repositories": [
+ {
+ "type": "vcs",
+ "url": "https://github.com/seatplus/eveapi.git"
+ },
+ {
+ "type": "vcs",
+ "url": "https://github.com/seatplus/esi-client.git"
+ },
+ {
+ "type": "vcs",
+ "url": "https://github.com/seatplus/esi-schema.git"
+ }
+ ],
"require": {
- "php": "^8.1",
- "laravel/framework": "^10.0",
+ "php": "^8.5",
+ "laravel/framework": "^13.0",
"laravel/socialite": "^5.0",
- "seatplus/eveapi": "^3.0",
- "spatie/laravel-permission": "^5.4",
+ "seatplus/eveapi": "dev-chore/laravel-13-upgrade as 4.1.0",
+ "seatplus/esi-client": "dev-chore/php-8.5-upgrade as 4.1.0",
+ "spatie/laravel-permission": "^6.10",
"socialiteproviders/eveonline": "^4.0"
},
"require-dev": {
- "orchestra/testbench": "^8.0",
- "nunomaduro/collision": "^7.0",
- "pestphp/pest": "^2.0",
- "pestphp/pest-plugin-laravel": "^2.0",
- "rector/rector": "^0.15.21",
- "driftingly/rector-laravel": "^0.17.0",
- "larastan/larastan": "^2.9",
- "pestphp/pest-plugin-type-coverage": "^2.8"
+ "orchestra/testbench": "^11.0",
+ "nunomaduro/collision": "^8.1",
+ "pestphp/pest": "^4.0",
+ "pestphp/pest-plugin-laravel": "^4.1",
+ "pestphp/pest-plugin-type-coverage": "^4.0",
+ "phpstan/phpstan": "^2.0",
+ "rector/rector": "^2.0",
+ "driftingly/rector-laravel": "^2.0",
+ "larastan/larastan": "^3.0",
+ "laravel/pint": "^1.9"
},
"extra": {
"laravel": {
@@ -67,6 +83,12 @@
"config": {
"allow-plugins": {
"pestphp/pest-plugin": true
+ },
+ "audit": {
+ "ignore": [
+ "PKSA-y2cr-5h3j-g3ys",
+ "PKSA-2kqm-ps5x-s4f5"
+ ]
}
}
}
diff --git a/config/auth.permissions.php b/config/auth.permissions.php
new file mode 100644
index 0000000..eaddcac
--- /dev/null
+++ b/config/auth.permissions.php
@@ -0,0 +1,6 @@
+ Seatplus\Auth\Models\Permissions\Permission::class,
+ 'permission' => Permission::class,
/*
* When using the "HasRoles" trait from this package, we need to know which
@@ -48,7 +27,7 @@
* `Spatie\Permission\Contracts\Role` contract.
*/
- 'role' => Seatplus\Auth\Models\Permissions\Role::class,
+ 'role' => Role::class,
],
@@ -96,6 +75,11 @@
],
'column_names' => [
+ /*
+ * Change this if you want to name the related pivots other than defaults
+ */
+ 'role_pivot_key' => null, // default 'role_id',
+ 'permission_pivot_key' => null, // default 'permission_id',
/*
* Change this if you want to name the related model primary key other than
@@ -106,16 +90,79 @@
*/
'model_morph_key' => 'model_id',
+
+ /*
+ * Change this if you want to use the teams feature and your related model's
+ * foreign key is other than `team_id`.
+ */
+
+ 'team_foreign_key' => 'team_id',
],
/*
- * When set to true, the required permission/role names are added to the exception
- * message. This could be considered an information leak in some contexts, so
- * the default setting is false here for optimum safety.
+ * When set to true, the method for checking permissions will be registered on the gate.
+ * Set this to false if you want to implement custom logic for checking permissions.
+ */
+
+ 'register_permission_check_method' => true,
+
+ /*
+ * When set to true, Laravel\Octane\Events\OperationTerminated event listener will be registered
+ * this will refresh permissions on every TickTerminated, TaskTerminated and RequestTerminated
+ * NOTE: This should not be needed in most cases, but an Octane/Vapor combination benefited from it.
+ */
+ 'register_octane_reset_listener' => false,
+
+ /*
+ * Teams Feature.
+ * When set to true the package implements teams using the 'team_foreign_key'.
+ * If you want the migrations to register the 'team_foreign_key', you must
+ * set this to true before doing the migration.
+ * If you already did the migration then you must make a new migration to also
+ * add 'team_foreign_key' to 'roles', 'model_has_roles', and 'model_has_permissions'
+ * (view the latest version of this package's migration file)
+ */
+
+ 'teams' => false,
+
+ /*
+ * Passport Client Credentials Grant
+ * When set to true the package will use Passports Client to check permissions
+ */
+
+ 'use_passport_client_credentials' => false,
+
+ /*
+ * When set to true, the required permission names are added to exception messages.
+ * This could be considered an information leak in some contexts, so the default
+ * setting is false here for optimum safety.
*/
'display_permission_in_exception' => false,
+ /*
+ * When set to true, the required role names are added to exception messages.
+ * This could be considered an information leak in some contexts, so the default
+ * setting is false here for optimum safety.
+ */
+
+ 'display_role_in_exception' => false,
+
+ /*
+ * By default wildcard permission lookups are disabled.
+ * See documentation to understand supported syntax.
+ */
+
+ 'enable_wildcard_permission' => false,
+
+ /*
+ * The class to use for interpreting wildcard permissions.
+ * If you need to modify delimiters, override the class and specify its name here.
+ */
+ // 'permission.wildcard_permission' => Spatie\Permission\WildcardPermission::class,
+
+ /* Cache-specific settings */
+
'cache' => [
/*
@@ -123,7 +170,7 @@
* When permissions or roles are updated the cache is flushed automatically.
*/
- 'expiration_time' => \DateInterval::createFromDateString('24 hours'),
+ 'expiration_time' => DateInterval::createFromDateString('24 hours'),
/*
* The cache key used to store all permissions.
@@ -131,17 +178,6 @@
'key' => 'spatie.permission.cache',
- /*
- * When checking for a permission against a model by passing a Permission
- * instance to the check, this key determines what attribute on the
- * Permissions model is used to cache against.
- *
- * Ideally, this should match your preferred way of checking permissions, eg:
- * `$user->can('view-posts')` would be 'name'.
- */
-
- 'model_key' => 'name',
-
/*
* You may optionally indicate a specific cache driver to use for permission and
* role caching using any of the `store` drivers listed in the cache.php config
diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php
index 04c6e05..515c694 100644
--- a/database/factories/UserFactory.php
+++ b/database/factories/UserFactory.php
@@ -27,6 +27,7 @@
namespace Seatplus\Auth\Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
+use Illuminate\Support\Str;
use Seatplus\Auth\Models\CharacterUser;
use Seatplus\Auth\Models\User;
use Seatplus\Eveapi\Models\Character\CharacterInfo;
@@ -47,6 +48,7 @@ public function definition(): array
return [
'main_character_id' => CharacterInfo::factory(),
'active' => true,
+ 'remember_token' => Str::random(10),
];
}
}
diff --git a/database/migrations/2024_07_15_183306_migrate_acl.php b/database/migrations/2024_07_15_183306_migrate_acl.php
new file mode 100644
index 0000000..0b93486
--- /dev/null
+++ b/database/migrations/2024_07_15_183306_migrate_acl.php
@@ -0,0 +1,75 @@
+createTables();
+
+ $this->migrateData();
+
+ $this->dropTables();
+ }
+
+ private function createTables(): void
+ {
+ Schema::create('role_memberships', function (Blueprint $table) {
+ $table->id();
+ $table->unsignedInteger('role_id');
+ $table->foreign('role_id')->references('id')->on('roles')->onDelete('cascade');
+ $table->morphs('entity');
+ $table->boolean('can_moderate')->default(false);
+ $table->string('status')->nullable()->default(null);
+ $table->timestamps();
+ });
+
+ Schema::table('affiliations', function (Blueprint $table) {
+ $table->unsignedInteger('role_id')->change();
+ $table->foreign('role_id')->references('id')->on('roles')->onDelete('cascade');
+ });
+ }
+
+ private function migrateData(): void
+ {
+ DB::table('acl_affiliations')->get()->each(function ($acl_affiliation) {
+ DB::table('role_memberships')->insert([
+ 'role_id' => $acl_affiliation->role_id,
+ 'entity_type' => $acl_affiliation->affiliatable_type,
+ 'entity_id' => $acl_affiliation->affiliatable_id,
+ 'can_moderate' => $acl_affiliation->can_moderate,
+ 'status' => $acl_affiliation->affiliatable_type === User::class ? 'member' : null,
+ 'created_at' => $acl_affiliation->created_at,
+ 'updated_at' => $acl_affiliation->updated_at,
+ ]);
+ });
+
+ DB::table('acl_members')->get()->each(function ($acl_member) {
+ DB::table('role_memberships')->insert([
+ 'role_id' => $acl_member->role_id,
+ 'entity_type' => User::class,
+ 'entity_id' => $acl_member->user_id,
+ 'can_moderate' => false,
+ 'status' => $acl_member->status,
+ 'created_at' => $acl_member->created_at,
+ 'updated_at' => $acl_member->updated_at,
+ ]);
+ });
+ }
+
+ private function dropTables(): void
+ {
+ Schema::dropIfExists('acl_affiliations');
+ Schema::dropIfExists('acl_members');
+ }
+};
diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon
index 666f5fd..535cc33 100644
--- a/phpstan-baseline.neon
+++ b/phpstan-baseline.neon
@@ -1,21 +1,67 @@
parameters:
ignoreErrors:
-
- message: "#^Consider using bind method instead or pass a closure\\.$#"
+ message: '#^Called ''env'' outside of the config directory which returns null when the config is cached, use ''config''\.$#'
+ identifier: larastan.noEnvCallsOutsideOfConfig
+ count: 3
+ path: config/auth.services.php
+
+ -
+ message: '#^Method Seatplus\\Auth\\Database\\Factories\\UserFactory\:\:configure\(\) should return \$this\(Seatplus\\Auth\\Database\\Factories\\UserFactory\) but returns static\(Seatplus\\Auth\\Database\\Factories\\UserFactory\)\.$#'
+ identifier: return.type
+ count: 1
+ path: database/factories/UserFactory.php
+
+ -
+ message: '#^Consider using bind method instead or pass a closure\.$#'
+ identifier: larastan.octaneCompatibility
count: 1
path: src/AuthenticationServiceProvider.php
-
- message: "#^Call to static method render\\(\\) on an unknown class Inertia\\\\Inertia\\.$#"
+ message: '#^Access to an undefined property Illuminate\\Database\\Eloquent\\Model\:\:\$characters\.$#'
+ identifier: property.notFound
count: 1
- path: src/Http/Controllers/Auth/LoginController.php
+ path: src/Http/Actions/Sso/FindOrCreateUserAction.php
+
+ -
+ message: '#^Access to an undefined property Illuminate\\Database\\Eloquent\\Model\:\:\$main_character_id\.$#'
+ identifier: property.notFound
+ count: 1
+ path: src/Http/Actions/Sso/FindOrCreateUserAction.php
+
+ -
+ message: '#^Property Seatplus\\Auth\\Http\\Actions\\Sso\\FindOrCreateUserAction\:\:\$user \(Seatplus\\Auth\\Models\\User\) does not accept Illuminate\\Database\\Eloquent\\Model\|null\.$#'
+ identifier: assign.propertyType
+ count: 1
+ path: src/Http/Actions/Sso/FindOrCreateUserAction.php
+
+ -
+ message: '#^Using nullsafe property access "\?\-\>scopes" on left side of \?\? is unnecessary\. Use \-\> instead\.$#'
+ identifier: nullsafe.neverNull
+ count: 1
+ path: src/Http/Controllers/Auth/StepUpController.php
+
+ -
+ message: '#^Access to an undefined property Illuminate\\Database\\Eloquent\\Model\:\:\$active\.$#'
+ identifier: property.notFound
+ count: 1
+ path: src/Observers/CharacterAffiliationObserver.php
+
+ -
+ message: '#^Access to an undefined property Illuminate\\Database\\Eloquent\\Model\:\:\$characters\.$#'
+ identifier: property.notFound
+ count: 2
+ path: src/Observers/CharacterAffiliationObserver.php
-
- message: "#^Method Seatplus\\\\Auth\\\\Http\\\\Controllers\\\\Auth\\\\LoginController\\:\\:showLoginForm\\(\\) has invalid return type Inertia\\\\Response\\.$#"
+ message: '#^Access to an undefined property Illuminate\\Database\\Eloquent\\Model\:\:\$main_character_id\.$#'
+ identifier: property.notFound
count: 1
- path: src/Http/Controllers/Auth/LoginController.php
+ path: src/Observers/CharacterAffiliationObserver.php
-
- message: "#^Method Seatplus\\\\Auth\\\\Http\\\\Middleware\\\\CheckPermissionAndAffiliation\\:\\:checkPermission\\(\\) is unused\\.$#"
+ message: '#^Property Seatplus\\Auth\\Services\\Roles\\BaseRoleService\:\:\$role \(Seatplus\\Auth\\Models\\Permissions\\Role\|null\) does not accept Spatie\\Permission\\Contracts\\Role\.$#'
+ identifier: assign.propertyType
count: 1
- path: src/Http/Middleware/CheckPermissionAndAffiliation.php
+ path: src/Services/Roles/BaseRoleService.php
diff --git a/phpstan.neon.dist b/phpstan.neon.dist
index 9aee2c3..83ece64 100644
--- a/phpstan.neon.dist
+++ b/phpstan.neon.dist
@@ -3,6 +3,7 @@ includes:
- vendor/larastan/larastan/extension.neon
parameters:
+ editorUrl: 'phpstorm://open?file=%%file%%&line=%%line%%'
databaseMigrationsPath:
- database/migrations
- vendor/seatplus/eveapi/database/migrations
diff --git a/phpunit.xml b/phpunit.xml
index 3180dc8..051873c 100644
--- a/phpunit.xml
+++ b/phpunit.xml
@@ -16,10 +16,11 @@
+
-
-
-
+
+
+
diff --git a/rector.php b/rector.php
index e1fb92a..1e70eef 100644
--- a/rector.php
+++ b/rector.php
@@ -10,7 +10,7 @@
// here we can define, what sets of rules will be applied
// tip: use "SetList" class to autocomplete sets
$rectorConfig->sets([
- //SetList::CODE_QUALITY,
+ // SetList::CODE_QUALITY,
LaravelSetList::LARAVEL_100,
LaravelSetList::LARAVEL_CODE_QUALITY,
]);
@@ -21,5 +21,5 @@
$rectorConfig->phpVersion(PhpVersion::PHP_81);
// register single rule
- //$rectorConfig->rule(TypedPropertyRector::class);
+ // $rectorConfig->rule(TypedPropertyRector::class);
};
diff --git a/src/Models/AccessControl/AclMember.php b/resources/lang/en/auth.php
similarity index 68%
rename from src/Models/AccessControl/AclMember.php
rename to resources/lang/en/auth.php
index 6c1db79..7f33393 100644
--- a/src/Models/AccessControl/AclMember.php
+++ b/resources/lang/en/auth.php
@@ -24,26 +24,7 @@
* SOFTWARE.
*/
-namespace Seatplus\Auth\Models\AccessControl;
-
-use Illuminate\Database\Eloquent\Model;
-use Illuminate\Database\Eloquent\Relations\BelongsTo;
-use Seatplus\Auth\Models\Permissions\Role;
-use Seatplus\Auth\Models\User;
-
-class AclMember extends Model
-{
- public $incrementing = false;
-
- protected $guarded = [];
-
- public function user(): BelongsTo
- {
- return $this->belongsTo(User::class, 'user_id');
- }
-
- public function role(): BelongsTo
- {
- return $this->belongsTo(Role::class, 'role_id');
- }
-}
+return [
+ 'sso_config_warning' => 'SSO does not appear to have been configured yet. Please check your .env file.',
+ 'login_welcome' => 'Welcome, please login using EVE Online SSO',
+];
diff --git a/routes/routes.php b/routes/routes.php
index 6ba1e89..00e3ac0 100644
--- a/routes/routes.php
+++ b/routes/routes.php
@@ -25,26 +25,34 @@
*/
use Illuminate\Support\Facades\Route;
-use Seatplus\Auth\Http\Controllers\Auth\LoginController;
-use Seatplus\Auth\Http\Controllers\Auth\SsoController;
+use Seatplus\Auth\Http\Controllers\Auth\CallbackController;
+use Seatplus\Auth\Http\Controllers\Auth\RedirectSSOController;
use Seatplus\Auth\Http\Controllers\Auth\StepUpController;
-use Seatplus\Auth\Http\Controllers\MainCharacterController;
+use Seatplus\Auth\Http\Controllers\SwitchMainCharacterController;
-Route::prefix('auth')
- ->middleware('web')
+Route::middleware('web')
->group(function () {
- // Auth
- Route::get('login', [LoginController::class, 'showLoginForm'])->name('auth.login');
- Route::get('logout', [LoginController::class, 'logout'])->name('auth.logout');
+ // auth/eve/callback
+ // auth/eve/redirect
+ // auth/eve/step-up/{character_id}
+ // auth/main-character/switch/{new_character_id}
+
+ // Auth
+ Route::prefix('auth')
+ ->group(function () {
- // SSO
- Route::get('/eve/sso/', [SsoController::class, 'redirectToProvider'])->name('auth.eve');
- Route::get('/eve/sso/{character_id}/step_up', StepUpController::class)->name('auth.eve.step_up');
+ // SSO
+ Route::prefix('eve')
+ ->group(function () {
+ Route::get('sso', RedirectSSOController::class)->name('auth.eve');
+ Route::get('sso/{character_id}/step_up', StepUpController::class)->name('auth.eve.step_up');
+ Route::get('callback', CallbackController::class)->name('auth.eve.callback'); // do not change this route /auth/eve/callback - this is registered in eve application
+ });
- Route::get('/eve/callback', [SsoController::class, 'handleProviderCallback'])->name('auth.eve.callback');
+ // MainCharacter
+ Route::put('main-character/switch/{new_character_id}', SwitchMainCharacterController::class)
+ ->name('change.main_character');
+ });
- // MainCharacter
- Route::post('main_character/change', [MainCharacterController::class, 'change'])
- ->name('change.main_character');
});
diff --git a/src/Actions/GetAffiliatedIdsByPermissionArray.php b/src/Actions/GetAffiliatedIdsByPermissionArray.php
deleted file mode 100644
index b8d8f8e..0000000
--- a/src/Actions/GetAffiliatedIdsByPermissionArray.php
+++ /dev/null
@@ -1,153 +0,0 @@
-cache_key;
- }
-
- public function __construct(private string $permission, private string $corporation_role = '')
- {
- $this->user = User::find(auth()->id());
- $this->cache_key = "affiliated character ids by permission {$this->user->id} for user wit user_id: {$this->permission}";
- }
-
- public function execute(): array
- {
- try {
- return cache($this->cache_key) ?? $this->getResult();
- } catch (\Exception $e) {
- throw $e;
- }
- }
-
- /**
- * @throws \Exception
- */
- private function getResult(): array
- {
- $affiliated_ids = $this->getAffiliatedIds()->toArray();
-
- cache([$this->cache_key => $affiliated_ids], now()->addMinutes(5));
-
- return $affiliated_ids;
- }
-
- private function getAffiliatedIds(): Collection
- {
- if ($this->user->can('superuser')) {
- return $this->getAllCharacterAndCorporationIds();
- }
-
- $user = User::with(
- [
- 'roles.permissions',
- //'roles.affiliations.affiliatable.characters' => fn ($query) => $query->has('characters')->select('character_infos.character_id'),
- 'roles.affiliations.affiliatable' => fn (MorphTo $morph_to) => $morph_to->morphWith([CorporationInfo::class => 'characters', AllianceInfo::class => ['characters', 'corporations']]),
- ]
- )->whereHas('roles.permissions', function (Builder $query) {
- $query->where('name', $this->permission);
- })
- ->where('id', $this->user->id)
- ->first();
-
- // if authenticated user has no roles, make sure to skip the roles access
- $affiliated_ids = ! $user ? collect() : $user->roles->map(fn (Role $role) => $role->affiliated_ids);
-
- // before returning add the owned character ids
- return $affiliated_ids->merge($this->buildOwnedIds())
- ->flatten()
- ->unique();
- }
-
- private function getAllCharacterAndCorporationIds(): Collection
- {
- $all_ids = collect();
-
- CharacterInfo::query()->cursor()->each(fn (CharacterInfo $character) => $all_ids->push($character->character_id));
- CorporationInfo::query()->cursor()->each(fn (CorporationInfo $corporation) => $all_ids->push($corporation->corporation_id));
-
- return $all_ids;
- }
-
- private function buildOwnedIds(): Collection
- {
- //$this->user->characters->pluck('character_id');
- // TODO: optimize and load user initially with necessairy relations
-
- return User::whereId($this->user->getAuthIdentifier())
- ->with('characters.roles', 'characters.corporation')
- ->get()
- ->whenNotEmpty(
- fn (Collection $collection) => $collection
- ->first()
- ->characters
- // for owned corporation tokens, we need to add the affiliation as long as the character has the required role
- ->map(fn (CharacterInfo $character) => [$this->getCorporationId($character), $character->character_id])
- ->flatten()
- ->filter()
- )
- ->flatten()->unique();
- }
-
- private function getCorporationId(CharacterInfo $character): ?int
- {
- if (! $this->corporation_role || ! $character->roles) {
- return null;
- }
-
- $roles = explode('|', $this->corporation_role);
-
- foreach ($roles as $role) {
- if ($character->roles->hasRole('roles', Str::ucfirst($role))) {
- return $character->corporation->corporation_id;
- }
- }
-
- return null;
- }
-}
diff --git a/src/AuthenticationServiceProvider.php b/src/AuthenticationServiceProvider.php
index 89ed714..40d9d88 100644
--- a/src/AuthenticationServiceProvider.php
+++ b/src/AuthenticationServiceProvider.php
@@ -32,6 +32,8 @@
use Laravel\Socialite\SocialiteManager;
use Seatplus\Auth\Listeners\ReactOnFreshRefreshToken;
use Seatplus\Auth\Listeners\UpdatingRefreshTokenListener;
+use Seatplus\Auth\Models\Permissions\Permission;
+use Seatplus\Auth\Models\Permissions\Role;
use Seatplus\Auth\Models\User;
use Seatplus\Auth\Observers\ApplicationObserver;
use Seatplus\Auth\Observers\CharacterAffiliationObserver;
@@ -50,7 +52,7 @@ class AuthenticationServiceProvider extends ServiceProvider
{
public function boot(): void
{
- //Add Migrations
+ // Add Migrations
$this->loadMigrationsFrom(__DIR__.'/../database/migrations/');
// Add routes
@@ -59,6 +61,9 @@ public function boot(): void
// Add event listeners
$this->addEventListeners();
+ // Add translations
+ $this->loadTranslationsFrom(__DIR__.'/../resources/lang', 'auth');
+
// Add GateLogic
Gate::before(function (User $user, string $ability): ?bool {
try {
@@ -89,16 +94,20 @@ public function register(): void
$socialite->extend(
'eveonline',
function (Container $app) use ($socialite) {
- $config = $app['config']['services.eveonline'];
+ $config = config('services.eveonline');
return $socialite->buildProvider(Provider::class, $config);
}
);
- $this->mergeConfigFrom(__DIR__.'/../config/permission.php', 'permission');
$this->mergeConfigFrom(__DIR__.'/../config/auth.updateJobs.php', 'seatplus.updateJobs');
$this->mergeConfigFrom(__DIR__.'/../config/auth.services.php', 'services');
+ config()->set('permission.models', [
+ 'permission' => Permission::class,
+ 'role' => Role::class,
+ ]);
+
$this->setUserModel();
}
diff --git a/src/DataTransferObjects/CheckPermissionAffiliationDto.php b/src/DataTransferObjects/CheckPermissionAffiliationDto.php
deleted file mode 100644
index f93e1b3..0000000
--- a/src/DataTransferObjects/CheckPermissionAffiliationDto.php
+++ /dev/null
@@ -1,31 +0,0 @@
-validated_ids = collect();
- }
-
- public function allIdsValidated(): bool
- {
- $different_ids = $this->requested_ids->diff($this->validated_ids);
-
- return $different_ids->isEmpty();
- }
-
- public function mergeValidatedIds(array|Collection $validatedIds): void
- {
- $this->validated_ids = $this->validated_ids
- ->merge($validatedIds)
- ->unique();
- }
-}
diff --git a/src/Enums/AffiliationType.php b/src/Enums/AffiliationType.php
index 8da9700..c37f8e2 100644
--- a/src/Enums/AffiliationType.php
+++ b/src/Enums/AffiliationType.php
@@ -2,26 +2,9 @@
namespace Seatplus\Auth\Enums;
-enum AffiliationType
+enum AffiliationType: string
{
- case ALLOWED;
- case INVERSE;
- case FORBIDDEN;
-
- public function operator(): string
- {
- return match ($this) {
- self::ALLOWED, self::FORBIDDEN => '=',
- self::INVERSE => '='
- };
- }
-
- public function value(): string
- {
- return match ($this) {
- self::ALLOWED => 'allowed',
- self::FORBIDDEN => 'forbidden',
- self::INVERSE => 'inverse'
- };
- }
+ case ALLOWED = 'allowed';
+ case INVERSE = 'inverse';
+ case FORBIDDEN = 'forbidden';
}
diff --git a/src/Enums/RoleMembershipStatus.php b/src/Enums/RoleMembershipStatus.php
new file mode 100644
index 0000000..11d2af3
--- /dev/null
+++ b/src/Enums/RoleMembershipStatus.php
@@ -0,0 +1,10 @@
+execute();
- } catch (Exception $exception) {
- report($exception);
- $ids = [];
- }
-
- return $ids;
- }
-}
diff --git a/src/Http/Actions/LoginAssetsAction.php b/src/Http/Actions/LoginAssetsAction.php
new file mode 100644
index 0000000..87bd337
--- /dev/null
+++ b/src/Http/Actions/LoginAssetsAction.php
@@ -0,0 +1,30 @@
+flash('warning', trans('auth::auth.sso_config_warning'));
+ }
+
+ return [
+ 'login_welcome' => trans('auth::auth.login_welcome'),
+ 'evesso_img_src' => asset('img/evesso.png'),
+ ];
+ }
+}
diff --git a/src/Http/Actions/LogoutAction.php b/src/Http/Actions/LogoutAction.php
new file mode 100644
index 0000000..3e1db98
--- /dev/null
+++ b/src/Http/Actions/LogoutAction.php
@@ -0,0 +1,19 @@
+logout();
+
+ $session = session();
+ $session->invalidate();
+ $session->regenerateToken();
+
+ return redirect('/');
+ }
+}
diff --git a/src/Http/Actions/Roles/AddModeratorRoleAction.php b/src/Http/Actions/Roles/AddModeratorRoleAction.php
new file mode 100644
index 0000000..970fd49
--- /dev/null
+++ b/src/Http/Actions/Roles/AddModeratorRoleAction.php
@@ -0,0 +1,18 @@
+setModerator->execute($role_id, $user_id, true);
+ }
+}
diff --git a/src/Http/Actions/Roles/ManageAutomaticRoleAction.php b/src/Http/Actions/Roles/ManageAutomaticRoleAction.php
new file mode 100644
index 0000000..4d21560
--- /dev/null
+++ b/src/Http/Actions/Roles/ManageAutomaticRoleAction.php
@@ -0,0 +1,63 @@
+checkPermission();
+
+ $validated = $request->validated();
+ $roleService = $this->baseRoleService->for($validated['role_id'])->automatic();
+
+ // setRoleType first: if the type changes it calls resetRoleMemberships(),
+ // which would wipe any criteria written below.
+ $roleService->setRoleType(RoleType::AUTOMATIC);
+
+ if ($name = Arr::get($validated, 'name')) {
+ $roleService->updateRoleName($name);
+ }
+
+ if (is_array($affiliated = Arr::get($validated, 'affiliated'))) {
+ $roleService->syncAffiliateManyEntities(
+ ...array_map(fn (array $affiliationData) => AffiliationData::fromArray($affiliationData), $affiliated)
+ );
+ }
+
+ if (is_array($assigned = Arr::get($validated, 'assigned'))) {
+ $roleService->automaticallyAssignRoleTo(
+ ...array_map(fn (array $criteriaData) => CriteriaData::fromArray($criteriaData), $assigned)
+ );
+ }
+
+ $roleService->handleMembers();
+ }
+
+ private function checkPermission(): void
+ {
+ $auth = auth()->user();
+
+ throw_unless($auth, \Exception::class, 'User not authenticated');
+
+ if (! auth()->user()->can('administrate access control groups')) {
+ abort(403, 'You are not allowed to administrate access control groups');
+ }
+ }
+}
diff --git a/src/Http/Actions/Roles/Manual/AddMemberAction.php b/src/Http/Actions/Roles/Manual/AddMemberAction.php
new file mode 100644
index 0000000..5485662
--- /dev/null
+++ b/src/Http/Actions/Roles/Manual/AddMemberAction.php
@@ -0,0 +1,18 @@
+setMember->execute($role_id, $user_id, true);
+ }
+}
diff --git a/src/Http/Actions/Roles/Manual/ManageManualRoleAction.php b/src/Http/Actions/Roles/Manual/ManageManualRoleAction.php
new file mode 100644
index 0000000..70952d7
--- /dev/null
+++ b/src/Http/Actions/Roles/Manual/ManageManualRoleAction.php
@@ -0,0 +1,41 @@
+validated();
+ $roleService = $this->baseRoleService->for($validated['role_id'])->manual();
+
+ $roleService->setRoleType(RoleType::MANUAL);
+
+ if ($name = Arr::get($validated, 'name')) {
+ $roleService->updateRoleName($name);
+ }
+
+ if (is_array($affiliated = Arr::get($validated, 'affiliated'))) {
+ $roleService->syncAffiliateManyEntities(
+ ...array_map(fn (array $affiliationData) => AffiliationData::fromArray($affiliationData), $affiliated)
+ );
+ }
+
+ $roleService->handleMembers();
+ }
+}
diff --git a/src/Http/Actions/Roles/Manual/RemoveMemberAction.php b/src/Http/Actions/Roles/Manual/RemoveMemberAction.php
new file mode 100644
index 0000000..311b736
--- /dev/null
+++ b/src/Http/Actions/Roles/Manual/RemoveMemberAction.php
@@ -0,0 +1,18 @@
+setMember->execute($role_id, $user_id, false);
+ }
+}
diff --git a/src/Http/Actions/Roles/Manual/SetMemberAction.php b/src/Http/Actions/Roles/Manual/SetMemberAction.php
new file mode 100644
index 0000000..05fcef2
--- /dev/null
+++ b/src/Http/Actions/Roles/Manual/SetMemberAction.php
@@ -0,0 +1,45 @@
+baseRoleService->for($role_id);
+ $this->checkPermission();
+
+ $roleService = $this->baseRoleService->manual();
+
+ /** @var User $user */
+ $user = User::query()->findOrFail($user_id);
+
+ match ($is_member) {
+ true => $roleService->addMember($user),
+ false => $roleService->removeMember($user)
+ };
+ }
+
+ private function checkPermission(): void
+ {
+ /* @var User $user */
+ $user = auth()->user();
+
+ $can_moderate = $this->baseRoleService->canModerate($user);
+
+ if (! $can_moderate) {
+ abort(403, 'You are not allowed to do this action');
+ }
+
+ }
+}
diff --git a/src/Http/Actions/Roles/OnRequest/ApplyAction.php b/src/Http/Actions/Roles/OnRequest/ApplyAction.php
new file mode 100644
index 0000000..b33d599
--- /dev/null
+++ b/src/Http/Actions/Roles/OnRequest/ApplyAction.php
@@ -0,0 +1,26 @@
+baseRoleService->for($role_id)->onRequest();
+
+ /** @var User $user */
+ $user = User::query()->findOrFail($user_id);
+
+ $roleService->submitApplicationForRole($user);
+ }
+}
diff --git a/src/Http/Actions/Roles/OnRequest/ApproveAction.php b/src/Http/Actions/Roles/OnRequest/ApproveAction.php
new file mode 100644
index 0000000..14e34a5
--- /dev/null
+++ b/src/Http/Actions/Roles/OnRequest/ApproveAction.php
@@ -0,0 +1,28 @@
+baseRoleService = $baseRoleService ?? new BaseRoleService;
+ }
+
+ /**
+ * @throws \Throwable
+ */
+ public function execute(int $role_id, int $user_id): void
+ {
+ $roleService = $this->baseRoleService->for($role_id)->onRequest();
+
+ /** @var User $user */
+ $user = User::query()->findOrFail($user_id);
+
+ $roleService->approveApplicationForRole($user);
+ }
+}
diff --git a/src/Http/Actions/Roles/OnRequest/DenyAction.php b/src/Http/Actions/Roles/OnRequest/DenyAction.php
new file mode 100644
index 0000000..0275c10
--- /dev/null
+++ b/src/Http/Actions/Roles/OnRequest/DenyAction.php
@@ -0,0 +1,28 @@
+baseRoleService = $baseRoleService ?? new BaseRoleService;
+ }
+
+ /**
+ * @throws \Throwable
+ */
+ public function execute(int $role_id, int $user_id): void
+ {
+ $roleService = $this->baseRoleService->for($role_id)->onRequest();
+
+ /** @var User $user */
+ $user = User::query()->findOrFail($user_id);
+
+ $roleService->denyApplication($user);
+ }
+}
diff --git a/src/Http/Actions/Roles/OnRequest/ManageOnRequestRoleAction.php b/src/Http/Actions/Roles/OnRequest/ManageOnRequestRoleAction.php
new file mode 100644
index 0000000..efe1df3
--- /dev/null
+++ b/src/Http/Actions/Roles/OnRequest/ManageOnRequestRoleAction.php
@@ -0,0 +1,57 @@
+checkPermission();
+
+ $validated = $request->validated();
+ $roleService = $this->baseRoleService->for($validated['role_id'])->onRequest();
+
+ $roleService->setRoleType(RoleType::ON_REQUEST);
+
+ if ($name = Arr::get($validated, 'name')) {
+ $roleService->updateRoleName($name);
+ }
+
+ if (is_array($affiliated = Arr::get($validated, 'affiliated'))) {
+ $roleService->syncAffiliateManyEntities(
+ ...array_map(fn (array $affiliationData) => AffiliationData::fromArray($affiliationData), $affiliated)
+ );
+ }
+
+ if (is_array($assigned = Arr::get($validated, 'assigned'))) {
+ $roleService->addCriteriaForRoleApplication(
+ ...array_map(fn (array $criteriaData) => CriteriaData::fromArray($criteriaData), $assigned)
+ );
+ }
+
+ $roleService->handleMembers();
+ }
+
+ private function checkPermission(): void
+ {
+ if (! auth()->user()->can('administrate access control groups')) {
+ abort(403, 'You are not allowed to administrate access control groups');
+ }
+ }
+}
diff --git a/src/Http/Actions/Roles/OnRequest/OptOutAction.php b/src/Http/Actions/Roles/OnRequest/OptOutAction.php
new file mode 100644
index 0000000..aaa1e1c
--- /dev/null
+++ b/src/Http/Actions/Roles/OnRequest/OptOutAction.php
@@ -0,0 +1,26 @@
+baseRoleService->for($role_id)->onRequest();
+
+ /** @var User $user */
+ $user = User::query()->findOrFail($user_id);
+
+ $roleService->removeApplication($user);
+ }
+}
diff --git a/src/Http/Actions/Roles/OptIn/JoinAction.php b/src/Http/Actions/Roles/OptIn/JoinAction.php
new file mode 100644
index 0000000..6d88892
--- /dev/null
+++ b/src/Http/Actions/Roles/OptIn/JoinAction.php
@@ -0,0 +1,28 @@
+baseRoleService = $baseRoleService ?? new BaseRoleService;
+ }
+
+ /**
+ * @throws \Throwable
+ */
+ public function execute(int $role_id, int $user_id): void
+ {
+ $roleService = $this->baseRoleService->for($role_id)->optIn();
+
+ /** @var User $user */
+ $user = User::query()->findOrFail($user_id);
+
+ $roleService->joinRole($user);
+ }
+}
diff --git a/src/Http/Actions/Roles/OptIn/LeaveAction.php b/src/Http/Actions/Roles/OptIn/LeaveAction.php
new file mode 100644
index 0000000..36ea2e2
--- /dev/null
+++ b/src/Http/Actions/Roles/OptIn/LeaveAction.php
@@ -0,0 +1,28 @@
+baseRoleService = $baseRoleService ?? new BaseRoleService;
+ }
+
+ /**
+ * @throws \Throwable
+ */
+ public function execute(int $role_id, int $user_id): void
+ {
+ $roleService = $this->baseRoleService->for($role_id)->optIn();
+
+ /** @var User $user */
+ $user = User::query()->findOrFail($user_id);
+
+ $roleService->leaveRole($user);
+ }
+}
diff --git a/src/Http/Actions/Roles/OptIn/ManageOptInRoleAction.php b/src/Http/Actions/Roles/OptIn/ManageOptInRoleAction.php
new file mode 100644
index 0000000..325b350
--- /dev/null
+++ b/src/Http/Actions/Roles/OptIn/ManageOptInRoleAction.php
@@ -0,0 +1,57 @@
+checkPermission();
+
+ $validated = $request->validated();
+ $roleService = $this->baseRoleService->for($validated['role_id'])->optIn();
+
+ $roleService->setRoleType(RoleType::OPT_IN);
+
+ if ($name = Arr::get($validated, 'name')) {
+ $roleService->updateRoleName($name);
+ }
+
+ if (is_array($affiliated = Arr::get($validated, 'affiliated'))) {
+ $roleService->syncAffiliateManyEntities(
+ ...array_map(fn (array $affiliationData) => AffiliationData::fromArray($affiliationData), $affiliated)
+ );
+ }
+
+ if (is_array($assigned = Arr::get($validated, 'assigned'))) {
+ $roleService->addCriteriaForRole(
+ ...array_map(fn (array $criteriaData) => CriteriaData::fromArray($criteriaData), $assigned)
+ );
+ }
+
+ $roleService->handleMembers();
+ }
+
+ private function checkPermission(): void
+ {
+ if (! auth()->user()->can('administrate access control groups')) {
+ abort(403, 'You are not allowed to administrate access control groups');
+ }
+ }
+}
diff --git a/src/Http/Actions/Roles/RemoveModeratorRoleAction.php b/src/Http/Actions/Roles/RemoveModeratorRoleAction.php
new file mode 100644
index 0000000..856ab24
--- /dev/null
+++ b/src/Http/Actions/Roles/RemoveModeratorRoleAction.php
@@ -0,0 +1,18 @@
+action->execute($role_id, $user_id, false);
+ }
+}
diff --git a/src/Http/Actions/Roles/SetModeratorAction.php b/src/Http/Actions/Roles/SetModeratorAction.php
new file mode 100644
index 0000000..1d25b02
--- /dev/null
+++ b/src/Http/Actions/Roles/SetModeratorAction.php
@@ -0,0 +1,53 @@
+baseRoleService->for($role_id);
+ $this->checkPermission();
+
+ /** @var OnRequestRoleService|ManualRoleService|OptInRoleService $roleService */
+ $roleService = $this->baseRoleService->getTypeService();
+
+ $this->validateRoleType($roleService);
+
+ /** @var User $user */
+ $user = User::query()->findOrFail($user_id);
+
+ $roleService->setModerator($user, $can_moderate);
+ }
+
+ private function checkPermission(): void
+ {
+ /* @var User $user */
+ $user = auth()->user();
+
+ $can_moderate = $this->baseRoleService->canModerate($user);
+
+ if (! $can_moderate) {
+ abort(403, 'You are not allowed to add moderators');
+ }
+
+ }
+
+ private function validateRoleType(AbstractRoleService $roleService): void
+ {
+ if (! $roleService instanceof ManualRoleService && ! $roleService instanceof OnRequestRoleService && ! $roleService instanceof OptInRoleService) {
+ abort(403, 'This action is not allowed');
+ }
+ }
+}
diff --git a/src/Http/Actions/Sso/UpdateRefreshTokenAction.php b/src/Http/Actions/Sso/UpdateRefreshTokenAction.php
index efeae9b..6fe7ef5 100644
--- a/src/Http/Actions/Sso/UpdateRefreshTokenAction.php
+++ b/src/Http/Actions/Sso/UpdateRefreshTokenAction.php
@@ -53,6 +53,6 @@ public function __invoke(EveUser $eve_data)
$refresh_token->restore();
}
- //TODO: if user was deactivated reactivate him https://github.com/eveseat/web/blob/a0c1dd6a73c10e91813276cd57b5b51460bdfc43/src/Http/Controllers/Auth/SsoController.php#L264
+ // TODO: if user was deactivated reactivate him https://github.com/eveseat/web/blob/a0c1dd6a73c10e91813276cd57b5b51460bdfc43/src/Http/Controllers/Auth/SsoController.php#L264
}
}
diff --git a/src/Http/Controllers/Auth/CallbackController.php b/src/Http/Controllers/Auth/CallbackController.php
new file mode 100644
index 0000000..6ca31c6
--- /dev/null
+++ b/src/Http/Controllers/Auth/CallbackController.php
@@ -0,0 +1,100 @@
+driver('eveonline')->user();
+
+ $eve_data = new EveUser(
+ character_id: data_get($socialite_user, 'attributes.character_id'),
+ character_owner_hash: data_get($socialite_user, 'attributes.character_owner_hash'),
+ token: data_get($socialite_user, 'token'),
+ refreshToken: data_get($socialite_user, 'refreshToken'),
+ expiresIn: data_get($socialite_user, 'expiresIn'),
+ user: data_get($socialite_user, 'user'),
+ );
+
+ // if return url was set, set the intended URL
+ $return_url = session()->pull('rurl');
+ if ($return_url) {
+ $this->authenticationService->setIntendedUrl($return_url);
+ }
+
+ // check if the requested scopes matches the provided scopes
+ if ($this->authenticationService->isUserAuthenticated()) {
+ $hasNotMatchingSsoScopes = $this->hasNotMatchingSsoScopes($eve_data);
+ $isDifferentCharacterIdProvided = $this->isDifferentCharacterIdProvided($eve_data);
+
+ if ($isDifferentCharacterIdProvided || $hasNotMatchingSsoScopes) {
+ return redirect()->intended();
+ }
+ }
+
+ // Get or create the User bound to this login.
+ $user = $find_or_create_user_action($eve_data);
+
+ /*
+ * Update the refresh token for this character.
+ */
+ $update_refresh_token_action($eve_data);
+
+ if (! $this->authenticationService->loginUser($user)) {
+ return redirect()->back()
+ ->with('error', 'Login failed. Please contact your administrator.');
+ }
+
+ $this->authenticationService->flashMessage('success', 'Character added/updated successfully');
+
+ RoleMemberSync::dispatch()->onQueue('high');
+
+ return redirect()->intended();
+ }
+
+ private function hasNotMatchingSsoScopes(EveUser $user): bool
+ {
+ $sso_scopes = $this->authenticationService->getSessionValue('sso_scopes');
+ $missing_scopes = array_diff($sso_scopes, $user->getScopes());
+
+ if (! empty($missing_scopes)) {
+ $this->authenticationService->flashMessage('error', 'Something might have gone wrong. You might have changed the requested scopes on esi, please refer from doing so.');
+
+ return true;
+ }
+
+ return false;
+ }
+
+ private function isDifferentCharacterIdProvided(EveUser $user): bool
+ {
+ $step_up_character_id = $this->authenticationService->getSessionValue('step_up');
+
+ if (! $step_up_character_id || $step_up_character_id === $user->character_id) {
+ return false;
+ }
+
+ $this->authenticationService->flashMessage('error', 'Please make sure to select the same character to step up on CCP as on seatplus.');
+
+ return true;
+ }
+}
diff --git a/src/Http/Controllers/Auth/LoginController.php b/src/Http/Controllers/Auth/RedirectSSOController.php
similarity index 51%
rename from src/Http/Controllers/Auth/LoginController.php
rename to src/Http/Controllers/Auth/RedirectSSOController.php
index 0687a62..d483c75 100644
--- a/src/Http/Controllers/Auth/LoginController.php
+++ b/src/Http/Controllers/Auth/RedirectSSOController.php
@@ -26,43 +26,52 @@
namespace Seatplus\Auth\Http\Controllers\Auth;
-use Inertia\Inertia;
+use Laravel\Socialite\Contracts\Factory as Socialite;
use Seatplus\Auth\Http\Controllers\Controller;
+use Seatplus\Auth\Services\AuthenticationService;
+use Seatplus\Auth\Services\SsoScopes\GlobalSsoScopesService;
+use SocialiteProviders\Eveonline\Provider;
+use Symfony\Component\HttpFoundation\RedirectResponse;
-class LoginController extends Controller
+class RedirectSSOController extends Controller
{
- /**
- * Where to redirect users after login.
- */
- protected string $redirectTo = '/home';
+ public function __construct(
+ private GlobalSsoScopesService $service,
+ private AuthenticationService $authenticationService
+ ) {}
/**
- * Create a new controller instance.
+ * Redirect the user to the Eve Online authentication page.
*
- * @return void
+ * @throws \Throwable
*/
- public function __construct()
- {
- $this->middleware('guest')->except('logout');
- }
-
- public function showLoginForm(): \Inertia\Response
+ public function __invoke(Socialite $socialite): RedirectResponse
{
- // Warn if SSO has not been configured yet.
- if (strlen(config('web.config.EVE_CLIENT_ID')) < 5 || strlen(config('web.config.EVE_CLIENT_SECRET')) < 5) {
- session()->flash('warning', trans('web::auth.sso_config_warning'));
+ if ($this->authenticationService->isUserAuthenticated()) {
+ return redirect('/');
}
- return Inertia::render('Auth/Login', [
- 'login_welcome' => trans('web::auth.login_welcome'),
- 'evesso_img_src' => asset('img/evesso.png'),
+ $scopes = $this->getScopes();
+
+ session([
+ 'rurl' => $this->authenticationService->getPreviousUrl(),
+ 'sso_scopes' => $scopes,
]);
+
+ $driver = $socialite->driver('eveonline');
+
+ /** @var Provider $driver */
+ return $driver->scopes($scopes)->redirect();
}
- public function logout(): \Illuminate\Http\RedirectResponse
+ private function getScopes(): array
{
- auth()->logout();
+ $global_scopes = $this->service->get();
- return redirect('/');
+ return collect(config('eveapi.scopes.minimum'))
+ ->merge($global_scopes)
+ ->unique()
+ ->filter()
+ ->all();
}
}
diff --git a/src/Http/Controllers/Auth/SsoController.php b/src/Http/Controllers/Auth/SsoController.php
deleted file mode 100644
index 3ffa1a7..0000000
--- a/src/Http/Controllers/Auth/SsoController.php
+++ /dev/null
@@ -1,159 +0,0 @@
-execute()->toArray();
-
- session([
- 'rurl' => session()->previousUrl(),
- 'sso_scopes' => $scopes,
- ]);
-
- $driver = $socialite->driver('eveonline');
-
- /** @var Provider $driver */
- return $driver->scopes($scopes)->redirect();
- }
-
- /**
- * Obtain the user information from Eve Online.
- *
- * @return \Illuminate\Http\RedirectResponse
- */
- public function handleProviderCallback(
- Socialite $social,
- FindOrCreateUserAction $find_or_create_user_action,
- UpdateRefreshTokenAction $update_refresh_token_action
- ): RedirectResponse {
-
- /* @var \SocialiteProviders\Manager\OAuth2\User $socialite_user */
- $socialite_user = $social->driver('eveonline')->user();
- $rurl = session()->pull('rurl');
-
- $eve_data = new EveUser(
- character_id: data_get($socialite_user, 'attributes.character_id'),
- character_owner_hash: data_get($socialite_user, 'attributes.character_owner_hash'),
- token: data_get($socialite_user, 'token'),
- refreshToken: data_get($socialite_user, 'refreshToken'),
- expiresIn: data_get($socialite_user, 'expiresIn'),
- user: data_get($socialite_user, 'user'),
- );
-
- // if return url was set, set the intended URL
- if ($rurl) {
- redirect()->setIntendedUrl($rurl);
- }
-
- // check if the requested scopes matches the provided scopes
- if (auth()->user()) {
- $this->checkForInvalidProviderCallback($eve_data);
- $this->checkIfDifferentCharacterIdHasBeenProvided($eve_data);
-
- if ($this->should_redirect) {
- return redirect()->intended();
- }
- }
-
- // Get or create the User bound to this login.
- $user = $find_or_create_user_action($eve_data);
-
- /*
- * Update the refresh token for this character.
- */
- $update_refresh_token_action($eve_data);
-
- if (! $this->loginUser($user)) {
- return redirect()->route('auth.login')
- ->with('error', 'Login failed. Please contact your administrator.');
- }
-
- session()->flash('success', 'Character added/updated successfully');
-
- UserRolesSync::dispatch($user)->onQueue('high');
-
- return redirect()->intended();
- }
-
- /**
- * Login the user.
- *
- * This method returns a boolean as a status flag for the
- * login routine. If a false is returned, it might mean
- * that that account is not allowed to sign in.
- */
- public function loginUser(User $user): bool
- {
- // Login and "remember" the given user...
- auth()->login($user, true);
-
- return true;
- }
-
- private function checkForInvalidProviderCallback(EveUser $user): void
- {
- $missing_scopes = array_diff(session()->pull('sso_scopes'), $user->getScopes());
-
- if (empty($missing_scopes)) {
- return;
- }
-
- session()->flash('error', 'Something might have gone wrong. You might have changed the requested scopes on esi, please refer from doing so.');
- $this->should_redirect = true;
- }
-
- private function checkIfDifferentCharacterIdHasBeenProvided(EveUser $user): void
- {
- $step_up_character_id = session()->pull('step_up');
-
- if (! $step_up_character_id || $step_up_character_id === $user->character_id) {
- return;
- }
-
- session()->flash('error', 'Please make sure to select the same character to step up on CCP as on seatplus.');
- $this->should_redirect = true;
- }
-}
diff --git a/src/Http/Controllers/Auth/StepUpController.php b/src/Http/Controllers/Auth/StepUpController.php
index 7f971e4..04dae8b 100644
--- a/src/Http/Controllers/Auth/StepUpController.php
+++ b/src/Http/Controllers/Auth/StepUpController.php
@@ -31,15 +31,14 @@
use Seatplus\Auth\Models\User;
use Seatplus\Eveapi\Models\RefreshToken;
use SocialiteProviders\Eveonline\Provider;
+use Symfony\Component\HttpFoundation\RedirectResponse;
class StepUpController extends Controller
{
/**
* Redirect the user to the Eve Online authentication page.
- *
- * @return \Symfony\Component\HttpFoundation\RedirectResponse
*/
- public function __invoke(Socialite $socialite, int $character_id)
+ public function __invoke(Socialite $socialite, int $character_id): RedirectResponse
{
if (! $this->isCharacterAssociatedToCurrentUser($character_id)) {
return redirect()->back()->with('error', 'character must belong to your account');
diff --git a/src/Http/Controllers/MainCharacterController.php b/src/Http/Controllers/SwitchMainCharacterController.php
similarity index 72%
rename from src/Http/Controllers/MainCharacterController.php
rename to src/Http/Controllers/SwitchMainCharacterController.php
index 71baef1..6896724 100644
--- a/src/Http/Controllers/MainCharacterController.php
+++ b/src/Http/Controllers/SwitchMainCharacterController.php
@@ -27,23 +27,19 @@
namespace Seatplus\Auth\Http\Controllers;
use Illuminate\Database\Eloquent\Builder;
-use Illuminate\Http\Request;
+use Illuminate\Http\RedirectResponse;
use Seatplus\Auth\Models\User;
-class MainCharacterController extends Controller
+class SwitchMainCharacterController extends Controller
{
- public function change(Request $request): \Illuminate\Http\RedirectResponse
+ public function __invoke(int $new_character_id): RedirectResponse
{
- $request->validate(['character_id' => ['required', 'exists:character_infos,character_id']]);
-
- $character_id = $request->get('character_id');
-
- $user = User::whereHas('character_users', fn (Builder $query) => $query->where('character_id', $character_id))
+ $user = User::whereHas('character_users', fn (Builder $query) => $query->where('character_id', $new_character_id))
->firstWhere('id', auth()->user()->getAuthIdentifier());
- abort_if(is_null($user), 403, 'Unauthorized: supplied character_id does not belong to the current user');
+ abort_if(is_null($user), 403);
- $user->changeMainCharacter($character_id);
+ $user->changeMainCharacter($new_character_id);
return back();
}
diff --git a/src/Models/AccessControl/AclAffiliation.php b/src/Http/Middleware/CheckAuthorization.php
similarity index 53%
rename from src/Models/AccessControl/AclAffiliation.php
rename to src/Http/Middleware/CheckAuthorization.php
index cbd2dd0..8679019 100644
--- a/src/Models/AccessControl/AclAffiliation.php
+++ b/src/Http/Middleware/CheckAuthorization.php
@@ -24,41 +24,37 @@
* SOFTWARE.
*/
-namespace Seatplus\Auth\Models\AccessControl;
+namespace Seatplus\Auth\Http\Middleware;
-use Illuminate\Database\Eloquent\Model;
-use Illuminate\Database\Eloquent\Relations\BelongsTo;
-use Illuminate\Database\Eloquent\Relations\MorphTo;
-use Illuminate\Support\Collection;
-use Seatplus\Auth\Models\Permissions\Role;
-use Seatplus\Eveapi\Models\Character\CharacterInfo;
+use Closure;
+use Illuminate\Http\Request;
+use Seatplus\Auth\Models\User;
+use Seatplus\Auth\Services\Permissions\CanUserService;
+use Seatplus\Auth\Services\Permissions\DTO\ValidateIdsDTO;
-class AclAffiliation extends Model
+class CheckAuthorization
{
- public $incrementing = false;
-
- protected $guarded = [];
-
- protected $casts = [
- 'can_moderate' => 'boolean',
- ];
-
- public function affiliatable(): MorphTo
- {
- return $this->morphTo();
+ public function __construct(
+ private ?CanUserService $canUserService = null
+ ) {
+ $this->canUserService = $this->canUserService ?? new CanUserService;
}
- public function role(): BelongsTo
+ public function handle(Request $request, Closure $next, string $permissions, ?string $corporation_role = null): mixed
{
- return $this->belongsTo(Role::class, 'id', 'role_id');
- }
-
- public function getCharacterIdsAttribute(): Collection
- {
- if (! $this->affiliatable) {
- return collect();
- }
-
- return $this->affiliatable instanceof CharacterInfo ? collect($this->affiliatable->character_id) : $this->affiliatable->characters->pluck('character_id');
+ /** @var User $user */
+ $user = auth()->user();
+ $ids_dto = ValidateIdsDTO::fromRequest($request);
+ $permissions = explode('|', $permissions);
+ $corporation_role = explode('|', $corporation_role);
+
+ abort_unless($this->canUserService->check(
+ user: $user,
+ idsDTO: $ids_dto,
+ permissions: $permissions,
+ corporation_roles: $corporation_role
+ ), 403);
+
+ return $next($request);
}
}
diff --git a/src/Http/Middleware/CheckPermissionAndAffiliation.php b/src/Http/Middleware/CheckPermissionAndAffiliation.php
deleted file mode 100644
index 72fc1af..0000000
--- a/src/Http/Middleware/CheckPermissionAndAffiliation.php
+++ /dev/null
@@ -1,177 +0,0 @@
-checkPermissionAffiliation($request, $permissions, $corporation_role);
-
- return $next($request);
- }
-
- private function checkPermissionAffiliation(Request $request, string $permissions, ?string $corporation_role): void
- {
- $this->validateAndSetRequestedIds($request);
-
- if ($this->getUser()->can('superuser')) {
- return;
- }
-
- $checkPermissionAffiliationDto = new CheckPermissionAffiliationDto(
- requested_ids: $this->getRequestedIds(),
- affiliationsDto: $this->getAffiliationsDto($permissions, $corporation_role),
- );
-
- $all_requested_ids_validated = app(Pipeline::class)
- ->send($checkPermissionAffiliationDto)
- ->through($this->getPipelines())
- ->thenReturn()
- ->allIdsValidated();
-
- abort_unless($all_requested_ids_validated, 401, 'You are not allowed to access the requested entity');
- }
-
- private function checkPermission(string $permissions, ?string $corporation_role): void
- {
- if ($this->getUser()->can('superuser')) {
- return;
- }
-
- $permissions = explode('|', $permissions);
-
- if ($this->getUser()->hasAnyPermission($permissions)) {
- return;
- }
-
- if ($this->hasCorporationRole($corporation_role)) {
- return;
- }
-
- abort('401', 'You are not authorized to perform this action');
- }
-
- private function hasCorporationRole(?string $corporation_role): bool
- {
- if (is_null($corporation_role)) {
- return false;
- }
-
- return CharacterUser::query()
- ->whereHas(
- 'character.roles',
- fn (HasOne $query) => $query
- ->whereJsonContains('roles', 'Director')
- ->orWhereJsonContains('roles', $corporation_role)
- )
- ->where('user_id', $this->getUser()->getAuthIdentifier())
- ->exists();
- }
-
- private function validateAndSetRequestedIds(Request $request): void
- {
- // validate request and set requsted ids
- // ignore non-validated payload
- $current_payload = Arr::where($request->input(), fn (mixed $value, string $key) => in_array($key, [
- 'character_id', 'character_ids',
- 'corporation_id', 'corporation_ids',
- 'alliance_id', 'alliance_ids',
- ]));
-
- $route_parameters = $request->route()->parameters();
-
- $constructed_payload = collect($current_payload)
- ->merge($route_parameters)
- ->unique()
- ->toArray();
-
- $validator = Validator::make($constructed_payload, [
- 'character_id' => ['required_without_all:corporation_id,alliance_id,character_ids,corporation_ids,alliance_ids', 'integer'],
- 'corporation_id' => ['required_without_all:character_id,alliance_id,character_ids,corporation_ids,alliance_ids', 'integer'],
- 'alliance_id' => ['required_without_all:character_id,corporation_id,character_ids,corporation_ids,alliance_ids', 'integer'],
- 'character_ids' => ['required_without_all:character_id,corporation_id,alliance_id,corporation_ids,alliance_ids', 'array'],
- 'corporation_ids' => ['required_without_all:character_id,corporation_id,alliance_id,character_ids,alliance_ids', 'array'],
- 'alliance_ids' => ['required_without_all:character_id,corporation_id,alliance_id,character_ids,corporation_ids', 'array'],
- ]);
-
- abort_if($validator->fails(), 403, implode(', ', $validator->errors()->all()));
-
- $this->requested_ids = collect($validator->validated())->flatten()->unique();
- }
-
- /**
- * @return array|string[]
- */
- public function getPipelines(): array
- {
- return $this->pipelines;
- }
-
- public function getRequestedIds(): Collection
- {
- return $this->requested_ids;
- }
-
- public function getUser(): User
- {
- return User::find(auth()->user()->getAuthIdentifier());
- }
-
- public function getAffiliationsDto(string $permissions, ?string $character_role = null): AffiliationsDto
- {
- return new AffiliationsDto(
- permissions: explode('|', $permissions),
- user: $this->getUser(),
- corporation_roles: is_string($character_role) ? explode('|', $character_role) : null
- );
- }
-}
diff --git a/src/Http/Middleware/CheckPermissionOrCorporationRole.php b/src/Http/Middleware/CheckPermissionOrCorporationRole.php
deleted file mode 100644
index a0bbcc2..0000000
--- a/src/Http/Middleware/CheckPermissionOrCorporationRole.php
+++ /dev/null
@@ -1,90 +0,0 @@
-user()) {
- abort(401);
- }
-
- // validate request and set requsted ids
- // we do this before fast tracking superuser to ensure superuser requests are valid too.
-
- $this->checkPermission($permissions, $corporation_role);
-
- return $next($request);
- }
-
- private function checkPermission(string $permissions, ?string $corporation_role): void
- {
- if ($this->getUser()->can('superuser')) {
- return;
- }
-
- $permissions = explode('|', $permissions);
-
- if ($this->getUser()->hasAnyPermission($permissions)) {
- return;
- }
-
- if ($this->hasCorporationRole($corporation_role)) {
- return;
- }
-
- abort('401', 'You are not authorized to perform this action');
- }
-
- private function hasCorporationRole(?string $corporation_role): bool
- {
- if (is_null($corporation_role)) {
- return false;
- }
-
- return CharacterUser::query()
- ->whereHas(
- 'character.roles',
- fn (\Illuminate\Database\Eloquent\Builder $query) => $query
- ->whereJsonContains('roles', 'Director')
- ->orWhereJsonContains('roles', $corporation_role)
- )
- ->where('user_id', $this->getUser()->getAuthIdentifier())
- ->exists();
- }
-
- public function getUser(): User
- {
- return User::find(auth()->user()->getAuthIdentifier());
- }
-}
diff --git a/src/Http/Middleware/CheckRequiredScopes.php b/src/Http/Middleware/CheckRequiredScopes.php
index 996a05c..07f7850 100644
--- a/src/Http/Middleware/CheckRequiredScopes.php
+++ b/src/Http/Middleware/CheckRequiredScopes.php
@@ -28,82 +28,31 @@
use Closure;
use Illuminate\Http\Request;
-use Illuminate\Support\Arr;
-use Illuminate\Support\Collection;
-use Illuminate\Support\Facades\Cache;
use Seatplus\Auth\Models\User;
-use Seatplus\Auth\Services\BuildCharacterScopesArray;
-use Seatplus\Auth\Services\BuildUserLevelRequiredScopes;
-use Seatplus\Eveapi\Models\Character\CharacterInfo;
-use Seatplus\Eveapi\Models\SsoScopes;
+use Seatplus\Auth\Services\SsoScopes\IsUserCompliantService;
+use Symfony\Component\HttpFoundation\Response;
class CheckRequiredScopes
{
- private User $user;
+ public function __construct(
+ private ?IsUserCompliantService $isUserCompliantService = null,
+ ) {
+ $this->isUserCompliantService ??= new IsUserCompliantService;
+ }
public function handle(Request $request, Closure $next) // @pest-ignore-type
{
- $characters_with_missing_scopes = Cache::tags(['characters_with_missing_scopes', $this->getUserId()])->get($this->getCacheKey());
-
- if (is_null($characters_with_missing_scopes)) {
- $this->buildUser();
-
- $characters_with_missing_scopes = $this->getCharactersWithMissingScopes();
- }
- return $characters_with_missing_scopes->isEmpty()
+ return $this->isUserCompliantService->check($request->user())
? $next($request)
- : $this->redirectTo($characters_with_missing_scopes);
- }
-
- public function buildUser(): void
- {
- /** @noinspection PhpFieldAssignmentTypeMismatchInspection */
- $this->user = User::with(
- 'characters.alliance.ssoScopes',
- 'characters.corporation.ssoScopes',
- 'characters.alliance.ssoScopes',
- 'characters.application.corporation.ssoScopes',
- 'characters.application.corporation.alliance.ssoScopes',
- 'characters.refresh_token',
- 'application.corporation.ssoScopes',
- 'application.corporation.alliance.ssoScopes'
- )->addSelect(['global_scope' => SsoScopes::global()->select('selected_scopes')])
- ->find(auth()->user()->getAuthIdentifier());
- }
-
- private function getCharactersWithMissingScopes(): Collection
- {
- // Get user level required scopes
- $user_scopes = BuildUserLevelRequiredScopes::get($this->user);
-
- $missing_scopes = $this->user
- ->characters
- ->map(fn (CharacterInfo $character) => BuildCharacterScopesArray::make()->setUserScopes($user_scopes)->setCharacter($character)->get())
- ->filter(fn (array $character_scopes) => Arr::get($character_scopes, 'missing_scopes'));
-
- Cache::tags(['characters_with_missing_scopes', $this->getUserId()])->put($this->getCacheKey(), $missing_scopes, now()->addMinutes(15));
-
- return $missing_scopes;
- }
-
- private function getCacheKey(): string
- {
- $user_id = $this->getUserId();
-
- return "UserScopes:${user_id}";
- }
-
- private function getUserId(): string
- {
- return (string) isset($this->user) ? $this->user->id : auth()->user()->getAuthIdentifier();
+ : $this->redirectTo($this->isUserCompliantService->getMissingScopes($request->user()));
}
/*
* This method should return the user to a view where he needs to handle the addition of required scopes
*/
- protected function redirectTo(Collection $missing_character_scopes) // @pest-ignore-type
+ protected function redirectTo(array $missing_character_scopes): Response
{
- //TODO: extend this with default view.
+ return redirect('/');
}
}
diff --git a/src/Http/Requests/RoleRequest.php b/src/Http/Requests/RoleRequest.php
new file mode 100644
index 0000000..7e89cb6
--- /dev/null
+++ b/src/Http/Requests/RoleRequest.php
@@ -0,0 +1,30 @@
+ 'required|integer',
+ 'name' => 'nullable|string',
+ 'affiliated' => 'nullable|array',
+ 'affiliated.*.entity_id' => 'required|integer',
+ 'affiliated.*.entity_type' => ['required', 'string', Rule::in(['character', 'corporation', 'alliance'])],
+ 'affiliated.*.affiliation_type' => [
+ 'required',
+ 'string',
+ Rule::in(array_map(fn (AffiliationType $affiliationType) => $affiliationType->value, AffiliationType::cases())),
+ ],
+ 'assigned' => 'nullable|array',
+ 'assigned.*.entity_id' => 'required|integer',
+ 'assigned.*.entity_type' => ['required', 'string', Rule::in(['character', 'corporation', 'alliance'])],
+ 'assigned.*.can_moderate' => 'nullable|boolean',
+ ];
+ }
+}
diff --git a/src/Jobs/DispatchUserRoleSync.php b/src/Jobs/RoleMemberSync.php
similarity index 81%
rename from src/Jobs/DispatchUserRoleSync.php
rename to src/Jobs/RoleMemberSync.php
index 042607d..fbae40e 100644
--- a/src/Jobs/DispatchUserRoleSync.php
+++ b/src/Jobs/RoleMemberSync.php
@@ -32,9 +32,10 @@
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
-use Seatplus\Auth\Models\User;
+use Seatplus\Auth\Models\Permissions\Role;
+use Seatplus\Auth\Services\Roles\BaseRoleService;
-class DispatchUserRoleSync implements ShouldBeUnique, ShouldQueue
+class RoleMemberSync implements ShouldBeUnique, ShouldQueue
{
use Dispatchable;
use InteractsWithQueue;
@@ -43,6 +44,12 @@ class DispatchUserRoleSync implements ShouldBeUnique, ShouldQueue
public int $tries = 1;
+ public function __construct(
+ private ?BaseRoleService $service = null
+ ) {
+ $this->service = $service ?? new BaseRoleService;
+ }
+
/**
* Assign this job a tag so that Horizon can categorize and allow
* for specific tags to be monitored.
@@ -58,8 +65,8 @@ public function tags(): array
public function handle(): void
{
- foreach (User::cursor() as $user) {
- UserRolesSync::dispatch($user)->onQueue('high');
- }
+ Role::query()->each(function (Role $role) {
+ $this->service->for($role)->handleMembers();
+ });
}
}
diff --git a/src/Jobs/UserRolesSync.php b/src/Jobs/UserRolesSync.php
deleted file mode 100644
index 3f08346..0000000
--- a/src/Jobs/UserRolesSync.php
+++ /dev/null
@@ -1,133 +0,0 @@
-tags());
- }
-
- /**
- * The number of seconds after which the job's unique lock will be released.
- */
- public int $uniqueFor = 3600;
-
- public function __construct(
- private User $user
- ) {
- $this->character_ids = User::query()
- ->has('characters.refresh_token')
- ->with(['characters.refresh_token' => fn (HasOne $query) => $query->select('character_id')])
- ->whereId($this->user->id)
- ->get()
- ->whenNotEmpty(function (Collection $collection) {
- return $collection->first()->characters->map(fn (CharacterInfo $character) => $character->character_id);
- })
- ->toArray();
- }
-
- /**
- * Assign this job a tag so that Horizon can categorize and allow
- * for specific tags to be monitored.
- *
- * If a job specifies the tags property, that is added.
- */
- public function tags(): array
- {
- return [
- 'Roles sync',
- "user_id: {$this->user->id}",
- 'main_character: '.($this->user->main_character->name ?? ''),
- ];
- }
-
- public function handle(): void
- {
- $this->handleAutomaticRoles();
- $this->handleOtherRoles();
- }
-
- private function handleAutomaticRoles(): void
- {
- $automatic_roles = Role::has('acl_affiliations')
- ->whereType('automatic')
- ->with('acl_affiliations.affiliatable.characters')
- ->cursor();
-
- $this->handleMemberships($automatic_roles);
- }
-
- private function handleOtherRoles(): void
- {
- $roles = Role::query()
- ->has('acl_affiliations')
- ->with('acl_affiliations.affiliatable.characters')
- ->whereNotIn('type', ['manual', 'automatic'])
- ->whereHas(
- 'acl_members',
- fn (Builder $query) => $query->where('user_id', $this->user->getAuthIdentifier())
- ->whereIn('status', ['member', 'paused'])
- )
- ->cursor();
-
- $this->handleMemberships($roles);
- }
-
- private function handleMemberships(LazyCollection $roles): void
- {
- foreach ($roles as $role) {
- collect($this->character_ids)->intersect($role->acl_affiliated_ids)->isNotEmpty()
- ? $role->activateMember($this->user)
- : $role->pauseMember($this->user);
- }
- }
-}
diff --git a/src/Listeners/ReactOnFreshRefreshToken.php b/src/Listeners/ReactOnFreshRefreshToken.php
index c5eb56a..51de674 100644
--- a/src/Listeners/ReactOnFreshRefreshToken.php
+++ b/src/Listeners/ReactOnFreshRefreshToken.php
@@ -38,7 +38,6 @@ public function handle(RefreshTokenCreated $refresh_token_event): void
->where('character_id', $refresh_token_event->refresh_token->character_id)
->firstOrFail();
- $user_id = $character_user->user_id;
- Cache::tags(['characters_with_missing_scopes', $user_id])->flush();
+ Cache::forget("user_permissions_{$character_user->user_id}");
}
}
diff --git a/src/Listeners/UpdatingRefreshTokenListener.php b/src/Listeners/UpdatingRefreshTokenListener.php
index d9baeb9..9ae5282 100644
--- a/src/Listeners/UpdatingRefreshTokenListener.php
+++ b/src/Listeners/UpdatingRefreshTokenListener.php
@@ -44,8 +44,7 @@ public function handle(UpdatingRefreshTokenEvent $refresh_token_event): void
->where('character_id', $refresh_token->character_id)
->firstOrFail();
- $user_id = $character_user->user_id;
- Cache::tags(['characters_with_missing_scopes', $user_id])->flush();
+ Cache::forget("user_permissions_{$character_user->user_id}");
}
}
diff --git a/src/Models/AccessControl/RoleMembership.php b/src/Models/AccessControl/RoleMembership.php
new file mode 100644
index 0000000..a05d3fd
--- /dev/null
+++ b/src/Models/AccessControl/RoleMembership.php
@@ -0,0 +1,39 @@
+ 'integer',
+ 'entity_id' => 'integer',
+ 'can_moderate' => 'boolean',
+ ];
+
+ protected $fillable = [
+ 'role_id',
+ 'entity_type',
+ 'entity_id',
+ 'can_moderate',
+ 'status',
+ ];
+
+ public function role(): BelongsTo
+ {
+ return $this->belongsTo(Role::class, 'role_id');
+ }
+
+ public function entity(): MorphTo
+ {
+ return $this->morphTo();
+ }
+}
diff --git a/src/Models/Permissions/Affiliation.php b/src/Models/Permissions/Affiliation.php
index b53d073..a2bead8 100644
--- a/src/Models/Permissions/Affiliation.php
+++ b/src/Models/Permissions/Affiliation.php
@@ -26,18 +26,18 @@
namespace Seatplus\Auth\Models\Permissions;
+use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\MorphTo;
-use Illuminate\Support\Collection;
use Seatplus\Eveapi\Models\Alliance\AllianceInfo;
use Seatplus\Eveapi\Models\Character\CharacterInfo;
use Seatplus\Eveapi\Models\Corporation\CorporationInfo;
/*
+ * Seatplus\Auth\Models\Permissions\Affiliation
+ *
* @property string $type
- * @property Collection $affiliated_ids
- * @property Collection $inverse_affiliated_ids
*/
class Affiliation extends Model
{
@@ -61,46 +61,28 @@ public function role(): BelongsTo
return $this->belongsTo(Role::class, 'role_id', 'id');
}
- public function getAffiliatedIdsAttribute(): Collection
+ /**
+ * @return Attribute
+ */
+ protected function affiliatedIds(): Attribute
{
- return $this->getCharacterIds()->merge($this->getCorporationIds());
- }
-
- public function getInverseAffiliatedIdsAttribute(): Collection
- {
- return $this->getInverseCharacterIds()->merge($this->getInverseCorporationIds());
- }
-
- private function getCharacterIds(): Collection
- {
- if (! $this->affiliatable) {
- return collect();
- }
-
- return $this->affiliatable instanceof CharacterInfo ? collect($this->affiliatable->character_id) : $this->affiliatable->characters->pluck('character_id');
- }
+ return new Attribute(
+ get: function () {
+ return match (true) {
+ $this->affiliatable instanceof CharacterInfo => collect($this->affiliatable->character_id),
+ $this->affiliatable instanceof CorporationInfo => collect([
+ $this->affiliatable->corporation_id,
+ $this->affiliatable->characters->pluck('character_id'),
+ ])->flatten(),
+ $this->affiliatable instanceof AllianceInfo => collect([
+ $this->affiliatable->alliance_id,
+ $this->affiliatable->corporations->pluck('corporation_id'),
+ $this->affiliatable->characters->pluck('character_id'),
+ ])->flatten(),
+ default => collect(),
+ };
+ }
+ );
- private function getInverseCharacterIds(): Collection
- {
- return CharacterInfo::query()
- ->whereNotIn('character_id', $this->getCharacterIds()->toArray())
- ->pluck('character_id');
- }
-
- private function getCorporationIds(): Collection
- {
- if (! $this->affiliatable) {
- return collect();
- }
-
- return $this->affiliatable instanceof CorporationInfo ? collect($this->affiliatable->corporation_id)
- : ($this->affiliatable instanceof AllianceInfo ? $this->affiliatable->corporations->pluck('corporation_id') : collect());
- }
-
- private function getInverseCorporationIds(): Collection
- {
- return CorporationInfo::query()
- ->whereNotIn('corporation_id', $this->getCorporationIds()->toArray())
- ->pluck('corporation_id');
}
}
diff --git a/src/Models/Permissions/Role.php b/src/Models/Permissions/Role.php
index 3321f78..752f525 100644
--- a/src/Models/Permissions/Role.php
+++ b/src/Models/Permissions/Role.php
@@ -26,178 +26,27 @@
namespace Seatplus\Auth\Models\Permissions;
-use Exception;
use Illuminate\Database\Eloquent\Relations\HasMany;
-use Illuminate\Database\Eloquent\Relations\MorphTo;
-use Illuminate\Support\Collection;
-use Seatplus\Auth\Models\AccessControl\AclAffiliation;
-use Seatplus\Auth\Models\AccessControl\AclMember;
-use Seatplus\Auth\Models\User;
-use Seatplus\Eveapi\Models\Alliance\AllianceInfo;
-use Seatplus\Eveapi\Models\Corporation\CorporationInfo;
+use Seatplus\Auth\Enums\RoleType;
+use Seatplus\Auth\Models\AccessControl\RoleMembership;
use Spatie\Permission\Models\Role as SpatieRole;
/**
- * @property string $type
+ * @property RoleType $type
*/
class Role extends SpatieRole
{
+ protected $casts = [
+ 'type' => RoleType::class,
+ ];
+
public function affiliations(): HasMany
{
return $this->hasMany(Affiliation::class, 'role_id');
}
- public function acl_affiliations(): HasMany
- {
- return $this->hasMany(AclAffiliation::class, 'role_id')
- ->where('can_moderate', false);
- }
-
- public function moderators(): HasMany
- {
- return $this->hasMany(AclAffiliation::class, 'role_id')
- ->where('can_moderate', true);
- }
-
- public function acl_members(): HasMany
- {
- return $this->hasMany(AclMember::class, 'role_id');
- }
-
- public function members(): HasMany
- {
- return $this->hasMany(AclMember::class, 'role_id')
- ->where('status', 'member');
- }
-
- public function activateMember(User $user): void
- {
- if (in_array($this->type, ['automatic', 'opt-in', 'on-request'])) {
- if ($user->characters->pluck('character_id')->intersect($this->getAclAffiliatedIdsAttribute())->isEmpty()) {
- throw new Exception('User is not allowed for this access control group');
- }
- }
-
- AclMember::query()->updateOrInsert(
- ['role_id' => $this->id, 'user_id' => $user->getAuthIdentifier()],
- ['status' => 'member']
- );
-
- $user->assignRole($this);
- }
-
- public function joinWaitlist(User $user): void
- {
- if ($this->type !== 'on-request') {
- throw new Exception('Only on-request control groups do have a waitlist');
- }
-
- if ($user->characters->pluck('character_id')->intersect($this->getAclAffiliatedIdsAttribute())->isEmpty()) {
- throw new Exception('User is not allowed for this access control group');
- }
-
- AclMember::query()->updateOrInsert(
- ['role_id' => $this->id, 'user_id' => $user->getAuthIdentifier()],
- ['status' => 'waitlist']
- );
- }
-
- public function pauseMember(User $user): void
- {
- AclMember::where('user_id', $user->getAuthIdentifier())
- ->where('role_id', $this->id)
- ->where('status', 'member')
- ->update(['status' => 'paused']);
-
- $user->removeRole($this);
- }
-
- public function removeMember(User $user): void
+ public function role_memberships(): HasMany
{
- AclMember::where('user_id', $user->getAuthIdentifier())
- ->where('role_id', $this->id)
- ->where('status', 'member')
- ->delete();
-
- $user->removeRole($this);
- }
-
- public function isModerator(User $user): bool
- {
- return $user->characters
- ->pluck('character_id')
- ->intersect($this->getModeratorIdsAttribute())
- ->isNotEmpty();
- }
-
- public function getAffiliatedIdsAttribute(): array
- {
- //eager load relations for preventing n+1 queries
- $role_with_relationships = $this->loadMissing([
- 'affiliations.affiliatable' => fn (MorphTo $morph_to) => $morph_to->morphWith([CorporationInfo::class => 'characters', AllianceInfo::class => ['characters', 'corporations']]),
- ]);
-
- return $role_with_relationships->getAffiliatedIds()
- ->diff($role_with_relationships->getForbiddenAndInverseIds()->toArray())
- ->all();
- }
-
- public function getAclAffiliatedIdsAttribute(): array
- {
- $acl_affiliations = $this->acl_affiliations()
- ->with(
- ['affiliatable' => function (MorphTo $morph_to) {
- $morph_to->morphWith([CorporationInfo::class => 'characters', AllianceInfo::class => 'characters']);
- }]
- )
- ->cursor();
-
- return $acl_affiliations
- ->map(fn (AclAffiliation $affiliation) => $affiliation->character_ids)
- ->flatten()
- ->unique()
- ->toArray();
- }
-
- public function getModeratorIdsAttribute(): array
- {
- //eager load relations for preventing n+1 queries
- $role_with_relationships = $this->loadMissing([
- 'moderators.affiliatable' => fn (MorphTo $morph_to) => $morph_to->morphWith([CorporationInfo::class => 'characters', AllianceInfo::class => 'characters']),
- ]);
-
- return $role_with_relationships->moderators
- ->map(fn (AclAffiliation $affiliation) => $affiliation->character_ids)
- ->flatten()
- ->unique()
- ->toArray();
- }
-
- private function getAffiliatedIds(): Collection
- {
- return $this->affiliations
- ->reject(fn (Affiliation $affiliation) => $affiliation->type === 'forbidden')
- // TODO get IDs instead of character_ids
- ->map(fn (Affiliation $affiliation) => $affiliation->type === 'allowed' ? $affiliation->affiliated_ids : $affiliation->inverse_affiliated_ids)
- ->flatten()
- ->unique();
- }
-
- private function getForbiddenAndInverseIds(): Collection
- {
- return $this->affiliations
- // we are only concerned about forbidden and inverse ids
- ->reject(fn (Affiliation $affiliation) => $affiliation->type === 'allowed')
- ->map(fn (Affiliation $affiliation) => $affiliation->affiliated_ids)
- ->flatten()
- ->unique();
- }
-
- public function delete(): bool
- {
- $this->affiliations()->delete();
- $this->acl_affiliations()->delete();
-
- return parent::delete();
+ return $this->hasMany(RoleMembership::class, 'role_id');
}
}
diff --git a/src/Models/User.php b/src/Models/User.php
index f378f6b..c11846c 100644
--- a/src/Models/User.php
+++ b/src/Models/User.php
@@ -57,7 +57,7 @@ class User extends Authenticatable
public $incrementing = true;
protected $fillable = [
- 'main_character_id', 'character_owner_hash',
+ 'main_character_id', 'character_owner_hash', 'active',
];
protected $hidden = [
@@ -102,6 +102,11 @@ public function application(): MorphOne
return $this->morphOne(Application::class, 'applicationable')->whereStatus('open');
}
+ public function getAuthPassword(): string
+ {
+ return '';
+ }
+
public function changeMainCharacter(int $character_id): bool
{
$this->main_character_id = $character_id;
diff --git a/src/Observers/ApplicationObserver.php b/src/Observers/ApplicationObserver.php
index e74f1d3..16728e9 100644
--- a/src/Observers/ApplicationObserver.php
+++ b/src/Observers/ApplicationObserver.php
@@ -34,14 +34,21 @@
class ApplicationObserver
{
+ /**
+ * @throws \Throwable
+ */
public function created(Application $application): void
{
- $user_id = match ($application->applicationable_type) {
+
+ $application_type = $application->applicationable_type;
+
+ throw_unless(in_array($application_type, [User::class, CharacterInfo::class]), new \Exception('Applicationable type not supported'));
+
+ $user_id = match ($application_type) {
User::class => $application->applicationable_id,
CharacterInfo::class => CharacterUser::query()->firstWhere('character_id', $application->applicationable_id)->user_id,
- default => null,
};
- Cache::tags(['characters_with_missing_scopes', $user_id])->flush();
+ Cache::forget("user_permissions_{$user_id}");
}
}
diff --git a/src/Observers/SsoScopeObserver.php b/src/Observers/SsoScopeObserver.php
index 5e30268..e384127 100644
--- a/src/Observers/SsoScopeObserver.php
+++ b/src/Observers/SsoScopeObserver.php
@@ -27,6 +27,7 @@
namespace Seatplus\Auth\Observers;
use Illuminate\Support\Facades\Cache;
+use Seatplus\Auth\Models\User;
use Seatplus\Eveapi\Models\SsoScopes;
class SsoScopeObserver
@@ -48,6 +49,10 @@ public function deleted(SsoScopes $ssoScopes): void
private function flushCache(): void
{
- Cache::tags(['characters_with_missing_scopes'])->flush();
+ $user_ids = User::query()->pluck('id');
+
+ foreach ($user_ids as $user_id) {
+ Cache::forget("user_permissions_{$user_id}");
+ }
}
}
diff --git a/src/Pipelines/Middleware/CheckAffiliatedIdsPipe.php b/src/Pipelines/Middleware/CheckAffiliatedIdsPipe.php
deleted file mode 100644
index bf8e15c..0000000
--- a/src/Pipelines/Middleware/CheckAffiliatedIdsPipe.php
+++ /dev/null
@@ -1,26 +0,0 @@
-affiliationsDto)
- ->getQuery()
- ->pluck('affiliated_id')
- ->intersect($checkPermissionAffiliationDto->requested_ids);
-
- $checkPermissionAffiliationDto->mergeValidatedIds($validated_ids);
-
- return $checkPermissionAffiliationDto;
- }
-
- protected function shouldBeChecked(CheckPermissionAffiliationDto $checkPermissionAffiliationDto): bool
- {
- return ! $checkPermissionAffiliationDto->allIdsValidated();
- }
-}
diff --git a/src/Pipelines/Middleware/CheckOwnedAffiliatedIdsPipe.php b/src/Pipelines/Middleware/CheckOwnedAffiliatedIdsPipe.php
deleted file mode 100644
index 79a5d17..0000000
--- a/src/Pipelines/Middleware/CheckOwnedAffiliatedIdsPipe.php
+++ /dev/null
@@ -1,30 +0,0 @@
-affiliationsDto)
- ->getQuery()
- ->pluck('affiliated_id')
- ->intersect($checkPermissionAffiliationDto->requested_ids);
-
- $checkPermissionAffiliationDto->mergeValidatedIds($validated_ids);
-
- return $checkPermissionAffiliationDto;
- }
-
- protected function shouldBeChecked(CheckPermissionAffiliationDto $checkPermissionAffiliationDto): bool
- {
- if ($checkPermissionAffiliationDto->allIdsValidated()) {
- return false;
- }
-
- return true;
- }
-}
diff --git a/src/Pipelines/Middleware/CheckPermissionAffiliationPipeline.php b/src/Pipelines/Middleware/CheckPermissionAffiliationPipeline.php
deleted file mode 100644
index 9a8e34e..0000000
--- a/src/Pipelines/Middleware/CheckPermissionAffiliationPipeline.php
+++ /dev/null
@@ -1,22 +0,0 @@
-shouldBeChecked($checkPermissionAffiliationDto)) {
- return $next($checkPermissionAffiliationDto);
- }
-
- return $next($this->check($checkPermissionAffiliationDto));
- }
-
- abstract protected function check(CheckPermissionAffiliationDto $checkPermissionAffiliationDto): CheckPermissionAffiliationDto;
-
- abstract protected function shouldBeChecked(CheckPermissionAffiliationDto $checkPermissionAffiliationDto): bool;
-}
diff --git a/src/Pipelines/Middleware/CheckPermissionAffiliationPipelineInterface.php b/src/Pipelines/Middleware/CheckPermissionAffiliationPipelineInterface.php
deleted file mode 100644
index 1eec777..0000000
--- a/src/Pipelines/Middleware/CheckPermissionAffiliationPipelineInterface.php
+++ /dev/null
@@ -1,11 +0,0 @@
-getAllowedAffiliatedCharacterAffiliations();
- $inverted = $this->getInvertedAffiliatedCharacterAffiliations();
-
- return $allowed
- ->union($inverted)
- ->distinct();
- }
-
- private function getAllowedAffiliatedCharacterAffiliations(): QueryBuilder
- {
- $allowed_affiliations = GetAllowedAffiliatedIdsService::make($this->affiliationsDto)
- ->getQuery();
-
- return $this->removeForbiddenAffiliations($allowed_affiliations);
- }
-
- private function getInvertedAffiliatedCharacterAffiliations(): QueryBuilder
- {
- $inverse_affiliations = GetInvertedAffiliatedIdsService::make($this->affiliationsDto)
- ->getQuery();
-
- return $this->removeForbiddenAffiliations($inverse_affiliations);
- }
-}
diff --git a/src/Services/Affiliations/GetAffiliatedIdsServiceBase.php b/src/Services/Affiliations/GetAffiliatedIdsServiceBase.php
deleted file mode 100644
index 5a3367e..0000000
--- a/src/Services/Affiliations/GetAffiliatedIdsServiceBase.php
+++ /dev/null
@@ -1,81 +0,0 @@
-on('character_affiliations.character_id', '=', "$alias.affiliatable_id")->where("$alias.affiliatable_type", CharacterInfo::class)
- ->orOn('character_affiliations.corporation_id', '=', "$alias.affiliatable_id")->where("$alias.affiliatable_type", CorporationInfo::class)
- ->orOn('character_affiliations.alliance_id', '=', "$alias.affiliatable_id")->where("$alias.affiliatable_type", AllianceInfo::class);
- }
-
- protected function joinAffiliatedCorporationAffiliations(JoinClause $join, string $alias): JoinClause
- {
- return $join
- ->on('character_affiliations.corporation_id', '=', "$alias.affiliatable_id")->where("$alias.affiliatable_type", CorporationInfo::class)
- ->orOn('character_affiliations.alliance_id', '=', "$alias.affiliatable_id")->where("$alias.affiliatable_type", AllianceInfo::class);
- }
-
- protected function getAffiliations(): Builder
- {
- if (! isset($this->affiliations)) {
- $this->createAffiliations();
- }
-
- return clone $this->affiliations;
- }
-
- protected function createAffiliations(): void
- {
- $permissions = $this->affiliationsDto->permissions;
-
- $affiliations = Affiliation::query()
- ->whereRelation('role.permissions', 'name', array_shift($permissions))
- ->whereRelation('role.members', 'user_id', $this->affiliationsDto->user->getAuthIdentifier());
-
- foreach ($permissions as $permission) {
- $affiliations->whereRelation('role.permissions', 'name', $permission);
- }
-
- $this->affiliations = $affiliations;
- }
-
- protected function removeForbiddenAffiliations(Builder $query): QueryBuilder
- {
- $forbidden = GetForbiddenAffiliatedIdService::make($this->affiliationsDto)->getQuery();
-
- return \DB::query()
- ->fromSub($query, 'affiliations')
- ->when(
- $forbidden->count(),
- fn (QueryBuilder $query) => $query
- ->leftJoinSub(
- $forbidden,
- 'remove_forbidden',
- 'remove_forbidden.forbidden_id',
- '=',
- 'affiliations.affiliated_id'
- )
- ->whereNull('forbidden_id')
- )
- ->select('affiliated_id');
- }
-}
diff --git a/src/Services/Affiliations/GetAllowedAffiliatedIdsService.php b/src/Services/Affiliations/GetAllowedAffiliatedIdsService.php
deleted file mode 100644
index 402d4f5..0000000
--- a/src/Services/Affiliations/GetAllowedAffiliatedIdsService.php
+++ /dev/null
@@ -1,50 +0,0 @@
-value());
-
- $affiliation = $this->getAffiliations()->where('type', $type->value());
-
- $character_affiliations = CharacterAffiliation::query()
- ->joinSub(
- $affiliation,
- $alias,
- fn (JoinClause $join) => $this->joinAffiliatedCharacterAffiliations($join, $alias)
- )
- ->select('character_affiliations.character_id as affiliated_id');
-
- $corporation_affiliations = CharacterAffiliation::query()
- ->joinSub(
- $affiliation,
- $alias,
- fn (JoinClause $join) => $this->joinAffiliatedCorporationAffiliations($join, $alias)
- )
- ->select('character_affiliations.corporation_id as affiliated_id');
-
- $alliance_affiliations = $affiliation
- ->where('affiliatable_type', AllianceInfo::class)
- ->select('affiliatable_id as affiliated_id');
-
- return $character_affiliations
- ->union($corporation_affiliations)
- ->union($alliance_affiliations);
- }
-}
diff --git a/src/Services/Affiliations/GetForbiddenAffiliatedIdService.php b/src/Services/Affiliations/GetForbiddenAffiliatedIdService.php
deleted file mode 100644
index 2913f0f..0000000
--- a/src/Services/Affiliations/GetForbiddenAffiliatedIdService.php
+++ /dev/null
@@ -1,83 +0,0 @@
-value());
-
- $owned_character_affiliations = GetOwnedAffiliatedIdsService::make($this->affiliationsDto)
- ->getQuery();
-
- $affiliation = $this->getAffiliations()->where('type', $type->value());
- /*->whereNotExists(
- fn (QueryBuilder $query) => $query
- ->select(DB::raw(1))
- ->fromSub($owned_character_affiliations, 'owned')
- ->whereColumn('affiliations.affiliatable_id', 'owned.affiliated_id')
- )*/
-
- $character_affiliations = CharacterAffiliation::query()
- ->when(
- $affiliation->count(),
- fn (Builder $query) => $query
- ->joinSub(
- $affiliation,
- $alias,
- fn (JoinClause $join) => $this->joinAffiliatedCharacterAffiliations($join, $alias)
- ),
- fn (Builder $query) => $query->whereNull('character_affiliations.character_id')
- )
- ->whereNotExists(
- fn (QueryBuilder $query) => $query
- ->select(DB::raw(1))
- ->fromSub($owned_character_affiliations, 'owned')
- ->whereColumn('character_affiliations.character_id', 'owned.affiliated_id')
- )
- ->select('character_affiliations.character_id as forbidden_id');
-
- $corporation_affiliations = CharacterAffiliation::query()
- ->when(
- $affiliation->count(),
- fn (Builder $query) => $query
- ->joinSub(
- $affiliation,
- $alias,
- fn (JoinClause $join) => $this->joinAffiliatedCorporationAffiliations($join, $alias)
- ),
- fn (Builder $query) => $query->whereNull('character_affiliations.corporation_id')
- )
- ->whereNotExists(
- fn (QueryBuilder $query) => $query
- ->select(DB::raw(1))
- ->fromSub($owned_character_affiliations, 'owned')
- ->whereColumn('character_affiliations.corporation_id', 'owned.affiliated_id')
- )
- ->select('character_affiliations.corporation_id as forbidden_id');
-
- $alliance_affiliations = $affiliation
- ->where('affiliatable_type', AllianceInfo::class)
- ->select('affiliatable_id as forbidden_id');
-
- return $character_affiliations
- ->union($corporation_affiliations)
- ->union($alliance_affiliations);
- }
-}
diff --git a/src/Services/Affiliations/GetInvertedAffiliatedIdsService.php b/src/Services/Affiliations/GetInvertedAffiliatedIdsService.php
deleted file mode 100644
index 443e197..0000000
--- a/src/Services/Affiliations/GetInvertedAffiliatedIdsService.php
+++ /dev/null
@@ -1,75 +0,0 @@
-value());
-
- $affiliation = $this->getAffiliations()->where('type', $type->value());
-
- $character_affiliations = CharacterAffiliation::query()
- ->when(
- $affiliation->count(),
- fn (Builder $query) => $query
- ->leftJoinSub(
- $affiliation,
- $alias,
- fn (JoinClause $join) => $this->joinAffiliatedCharacterAffiliations($join, $alias)
- )
- ->whereNull("$alias.affiliatable_id"),
- fn (Builder $query) => $query->whereNull('character_id')
- )
- ->select('character_affiliations.character_id as affiliated_id');
-
- $corporation_affiliations = CharacterAffiliation::query()
- ->when(
- $affiliation->whereIn('affiliatable_type', [CorporationInfo::class, AllianceInfo::class])->count(),
- fn (Builder $query) => $query
- ->leftJoinSub(
- $affiliation,
- $alias,
- fn (JoinClause $join) => $this->joinAffiliatedCorporationAffiliations($join, $alias)
- )
- ->whereNull("$alias.affiliatable_id"),
- fn (Builder $query) => $query->whereNull('corporation_id')
- )
- ->select('character_affiliations.corporation_id as affiliated_id');
-
- $alliance_affiliations = CharacterAffiliation::query()
- ->when(
- $affiliation->where('affiliatable_type', AllianceInfo::class)->count(),
- fn (Builder $query) => $query
- ->leftJoinSub(
- $affiliation,
- $alias,
- 'character_affiliations.alliance_id',
- '=',
- "$alias.affiliatable_id"
- )
- ->whereNull("$alias.affiliatable_id"),
- fn (Builder $query) => $query->whereNull('alliance_id')
- )
- ->select('character_affiliations.alliance_id as affiliated_id');
-
- return $character_affiliations
- ->union($corporation_affiliations)
- ->union($alliance_affiliations);
- }
-}
diff --git a/src/Services/Affiliations/GetOwnedAffiliatedIdsService.php b/src/Services/Affiliations/GetOwnedAffiliatedIdsService.php
deleted file mode 100644
index 95a1a76..0000000
--- a/src/Services/Affiliations/GetOwnedAffiliatedIdsService.php
+++ /dev/null
@@ -1,73 +0,0 @@
-getCharacterQuery();
-
- if (! $this->affiliationsDto->corporation_roles) {
- return $character_query;
- }
-
- $corporation_query = $this->getCorporationQuery();
-
- return $character_query
- ->union($corporation_query);
- }
-
- private function getCharacterQuery(): Builder
- {
- return CharacterAffiliation::query()
- ->join(
- 'character_users',
- fn (JoinClause $join) => $join
- ->on('character_affiliations.character_id', '=', 'character_users.character_id')
- ->where('user_id', $this->affiliationsDto->user->getAuthIdentifier())
- )
- ->select('character_affiliations.character_id as affiliated_id');
- }
-
- private function getCorporationQuery(): Builder
- {
- $character_users = CharacterUser::query()
- ->whereHas(
- 'character.roles',
- function (Builder $query) {
- $query->whereJsonContains('roles', 'Director');
-
- foreach ($this->affiliationsDto->corporation_roles as $role) {
- $query->orWhereJsonContains('roles', $role);
- }
- }
- )
- ->where('user_id', $this->affiliationsDto->user->getAuthIdentifier());
-
- return CharacterAffiliation::query()
- ->joinSub(
- $character_users,
- 'character_users_sub',
- 'character_affiliations.character_id',
- '=',
- 'character_users_sub.character_id'
- )
- ->select('character_affiliations.corporation_id as affiliated_id');
- }
-}
diff --git a/src/Services/AuthenticationService.php b/src/Services/AuthenticationService.php
new file mode 100644
index 0000000..2a696d9
--- /dev/null
+++ b/src/Services/AuthenticationService.php
@@ -0,0 +1,66 @@
+auth = $auth;
+ $this->session = $session;
+ }
+
+ /**
+ * Login the user.
+ *
+ * This method returns a boolean as a status flag for the
+ * login routine. If a false is returned, it might mean
+ * that that account is not allowed to sign in.
+ */
+ public function loginUser(User $user): bool
+ {
+ try {
+ $this->auth->login($user, true);
+ } catch (\Exception $e) {
+ report($e);
+
+ return false;
+ }
+
+ return true;
+ }
+
+ public function setIntendedUrl(string $url): void
+ {
+ Redirect::setIntendedUrl($url);
+ }
+
+ public function getPreviousUrl(): string
+ {
+ return $this->session->previousUrl();
+ }
+
+ public function flashMessage(string $type, string $message): void
+ {
+ $this->session->flash($type, $message);
+ }
+
+ public function getSessionValue(string $key): mixed
+ {
+ return $this->session->pull($key);
+ }
+
+ public function isUserAuthenticated(): bool
+ {
+ return $this->auth->check();
+ }
+}
diff --git a/src/Services/BuildCharacterScopesArray.php b/src/Services/BuildCharacterScopesArray.php
deleted file mode 100644
index 6713873..0000000
--- a/src/Services/BuildCharacterScopesArray.php
+++ /dev/null
@@ -1,100 +0,0 @@
-withUserScope) {
- return [];
- }
-
- return $this->user_scopes;
- }
-
- public function getCharacter(): CharacterInfo
- {
- return $this->character;
- }
-
- public static function make(): self
- {
- return new self();
- }
-
- public function setUserScopes(array $user_scopes): self
- {
- $this->withUserScope = true;
- $this->user_scopes = $user_scopes;
-
- return $this;
- }
-
- public function setCharacter(CharacterInfo $character): self
- {
- $this->character = $character;
-
- return $this;
- }
-
- public function get(): array
- {
- $character_array = [
- 'character' => $this->getCharacter(),
- 'required_scopes' => collect([
- 'corporation_scopes' => $this->getCharacter()->corporation->ssoScopes->selected_scopes ?? [],
- 'alliance_scopes' => $this->getCharacter()->alliance->ssoScopes->selected_scopes ?? [],
- 'character_application_corporation_scopes' => $this->getCharacter()->application->corporation->ssoScopes->selected_scopes ?? [],
- 'character_application_alliance_scopes' => $this->getCharacter()->application->corporation->alliance->ssoScopes->selected_scopes ?? [],
- 'user_scope' => $this->getUserScopes(),
- ])->flatten(1)
- ->filter()
- ->unique()
- ->flatten(1)
- ->toArray(),
- 'token_scopes' => $this->getCharacter()->refresh_token->scopes ?? [],
- ];
-
- $required_scopes = Arr::get($character_array, 'required_scopes');
- $token_scopes = Arr::get($character_array, 'token_scopes');
- $missing_scopes = collect($required_scopes)
- ->reject(fn (string $required_scope) => in_array($required_scope, $token_scopes))
- ->toArray();
-
- return Arr::add($character_array, 'missing_scopes', array_values($missing_scopes));
- }
-}
diff --git a/src/Services/BuildUserLevelRequiredScopes.php b/src/Services/BuildUserLevelRequiredScopes.php
deleted file mode 100644
index dff3813..0000000
--- a/src/Services/BuildUserLevelRequiredScopes.php
+++ /dev/null
@@ -1,76 +0,0 @@
-replicate();
-
- return $user
- ->characters
- ->map(fn (CharacterInfo $character) => collect([
- $character->corporation->ssoScopes ?? [],
- $character->alliance->ssoScopes ?? [],
- ])->where('type', 'user'))
- ->filter(fn (Collection $collection) => $collection->isNotEmpty())
- ->map(fn (Collection $collection) => $collection->map(fn (SsoScopes $scope) => [
- $scope->selected_scopes,
- ]))
- ->concat([
- 'user_application_corporation_scopes' => $user->getRelation('application') ? $user->application->corporation->ssoScopes?->selected_scopes : [],
- 'user_application_alliance_scopes' => $user->getRelation('application') ? $user->application->corporation->alliance?->ssoScopes?->selected_scopes : [],
- 'global_scopes' => self::getGlobalScopes($user),
- ])
- ->flatten()
- ->unique()
- ->toArray();
- }
-
- private static function getSelectedScopes(): array
- {
- $query_result = SsoScopes::global()->select('selected_scopes')->first();
-
- return $query_result ? $query_result->selected_scopes : [];
- }
-
- private static function getGlobalScopes(User $user): array
- {
-
- $global_scopes = Arr::has($user->getAttributes(), 'global_scope') ? $user->getAttribute('global_scope') : self::getSelectedScopes();
-
- // return is_array($global_scopes) ? $global_scopes : json_decode($global_scopes, true) ?? [];
- return is_array($global_scopes) ? $global_scopes : (is_string($global_scopes) ? json_decode($global_scopes) : []);
- }
-}
diff --git a/src/Services/ConvertClassToPermissionStringService.php b/src/Services/ConvertClassToPermissionStringService.php
deleted file mode 100644
index 29452e7..0000000
--- a/src/Services/ConvertClassToPermissionStringService.php
+++ /dev/null
@@ -1,13 +0,0 @@
-scopes = collect(config('eveapi.scopes.minimum'));
- }
-
- public function execute(): Collection
- {
- if (auth()->guest()) {
- return $this->scopes->merge(setting('global_sso_scopes'));
- }
-
- /** @noinspection PhpFieldAssignmentTypeMismatchInspection */
- $this->user = User::with(
- 'application.corporation.ssoScopes',
- 'application.corporation.alliance.ssoScopes'
- )->find(auth()->user()->getAuthIdentifier());
-
- return $this->scopes
- ->merge(
- collect([
- setting('global_sso_scopes'),
- $this->user->application->corporation->ssoScopes->selected_scopes ?? [],
- $this->user->application->corporation->alliance->ssoScopes->selected_scopes ?? [],
- ])
- )
- ->flatten(1)
- ->unique()
- ->filter();
- }
-}
diff --git a/src/Services/Permissions/CanUserService.php b/src/Services/Permissions/CanUserService.php
new file mode 100644
index 0000000..903cb0a
--- /dev/null
+++ b/src/Services/Permissions/CanUserService.php
@@ -0,0 +1,159 @@
+user_permission_service = $this->user_permission_service ?? new UserPermissionService;
+ }
+
+ /**
+ * @throws ValidationException
+ */
+ public function check(User $user, ValidateIdsDTO $idsDTO, array $permissions, array $corporation_roles = []): bool
+ {
+ $ids_to_validate = $idsDTO->get();
+
+ // match whether ids are provided or not
+ $is_validated = match (empty($ids_to_validate)) {
+ true => $this->validateSimplePermissions($user, $permissions, $corporation_roles),
+ false => $this->validateIds($user, $ids_to_validate, $permissions, $corporation_roles)
+ };
+
+ return $is_validated || $user->can('superuser');
+ }
+
+ private function validateOwnedCharacterIds(array $data, Closure $next): array
+ {
+ $ids_to_validate = $data['ids_to_validate'];
+
+ $owned_character_ids = $data['user_permissions']['owned_character_ids'];
+
+ // remove owned character ids from ids
+ $ids_to_validate = array_diff($ids_to_validate, $owned_character_ids);
+
+ $data['ids_to_validate'] = $ids_to_validate;
+
+ return $next($data);
+ }
+
+ private function validateCorporationRoles(array $data, Closure $next): array
+ {
+ $ids_to_validate = $data['ids_to_validate'];
+
+ // if no ids are left, we return early
+ if (empty($ids_to_validate)) {
+ return $next($data);
+ }
+
+ $corporation_roles = $data['corporation_roles'];
+ $user_permissions = $data['user_permissions'];
+
+ // if a corporation role is provided, we check if the user has the required role
+ if ($corporation_roles) {
+
+ // add Director to corporation roles
+ $corporation_roles[] = 'Director';
+
+ foreach ($corporation_roles as $corporation_role) {
+ $corporation_role_ids = $user_permissions['corporation_roles'][$corporation_role] ?? [];
+
+ // remove ids that are within the corp_ids from ids_to_validate
+ $ids_to_validate = array_diff($ids_to_validate, $corporation_role_ids);
+ $data['ids_to_validate'] = $ids_to_validate;
+
+ // if ids are empty, end the loop
+ if (empty($ids_to_validate)) {
+ break;
+ }
+ }
+ }
+
+ return $next($data);
+ }
+
+ private function validatePermissions(array $data, Closure $next): array
+ {
+ $ids_to_validate = $data['ids_to_validate'];
+
+ // if no ids are left, we return early
+ if (empty($ids_to_validate)) {
+ return $next($data);
+ }
+
+ $permissions = $data['permissions'];
+ $user_permissions = $data['user_permissions'];
+
+ // check if user has the required permissions
+ foreach ($permissions as $permission) {
+ $ids_with_permission = $user_permissions['permissions'][$permission] ?? [];
+
+ // remove ids that are within the ids_with_permission from ids_to_validate
+ $ids_to_validate = array_diff($ids_to_validate, $ids_with_permission);
+ $data['ids_to_validate'] = $ids_to_validate;
+
+ // if ids are empty, end the loop
+ if (empty($ids_to_validate)) {
+ break;
+ }
+ }
+
+ return $next($data);
+ }
+
+ private function validateIds(User $user, array $ids_to_validate, array $permissions, array $corporation_roles): bool
+ {
+
+ $data = app(Pipeline::class)
+ ->send([
+ 'ids_to_validate' => $ids_to_validate,
+ 'user_permissions' => $this->getUserPermissionObject($user),
+ 'permissions' => $permissions,
+ 'corporation_roles' => $corporation_roles,
+ ])
+ ->through([
+ fn (array $data, Closure $next) => $this->validateOwnedCharacterIds($data, $next),
+ fn (array $data, Closure $next) => $this->validateCorporationRoles($data, $next),
+ fn (array $data, Closure $next) => $this->validatePermissions($data, $next),
+ ])->thenReturn();
+
+ $ids_not_validated = $data['ids_to_validate'];
+
+ // return true if all ids are validated
+ return empty($ids_not_validated);
+ }
+
+ private function validateSimplePermissions(User $user, array $permissions, array $corporation_role): bool
+ {
+ if ($user->hasAnyPermission($permissions)) {
+ return true;
+ }
+
+ $user_permission_object = $this->getUserPermissionObject($user);
+
+ $users_corporation_roles = array_keys([...$user_permission_object['corporation_roles']]);
+
+ // if user corporation roles contain the role 'Director' we return true
+ if (in_array('Director', $users_corporation_roles)) {
+ return true;
+ }
+
+ // if any of the corporation roles is in the users corporation roles, we return true
+ return (bool) array_intersect($corporation_role, $users_corporation_roles);
+ }
+
+ public function getUserPermissionObject(User $user): mixed
+ {
+ return Cache::remember("user_permissions_{$user->id}", now()->addMinutes(5), fn () => $this->user_permission_service->get($user));
+ }
+}
diff --git a/src/Services/Permissions/DTO/ValidateIdsDTO.php b/src/Services/Permissions/DTO/ValidateIdsDTO.php
new file mode 100644
index 0000000..fdc4b45
--- /dev/null
+++ b/src/Services/Permissions/DTO/ValidateIdsDTO.php
@@ -0,0 +1,100 @@
+all(), ...$request->route()->parameters()];
+
+ return new self(
+ character_id: Arr::get($all_data, 'character_id'),
+ corporation_id: Arr::get($all_data, 'corporation_id'),
+ alliance_id: Arr::get($all_data, 'alliance_id'),
+ character_ids: Arr::get($all_data, 'character_ids'),
+ corporation_ids: Arr::get($all_data, 'corporation_ids'),
+ alliance_ids: Arr::get($all_data, 'alliance_ids')
+ );
+ }
+
+ /*public static function make(...$args): ValidateIdsDTO
+ {
+ return new self(...$args);
+ }*/
+
+ /**
+ * @throws ValidationException
+ */
+ public function get(): array
+ {
+
+ // if any of the constructor parameters is not null, we return the validated array
+ if (! array_filter(get_object_vars($this), fn (null|int|array $value) => ! is_null($value))) {
+ return [];
+ }
+
+ return collect($this->validate())
+ ->flatten()
+ ->map(fn (string|int $value) => (int) $value)
+ ->all();
+ }
+
+ /**
+ * @throws ValidationException
+ */
+ private function validate(): array
+ {
+ $ids = collect([
+ 'character_id' => $this->character_id,
+ 'corporation_id' => $this->corporation_id,
+ 'alliance_id' => $this->alliance_id,
+ 'character_ids' => $this->character_ids,
+ 'corporation_ids' => $this->corporation_ids,
+ 'alliance_ids' => $this->alliance_ids,
+ ])->filter()->all();
+
+ $keys = [
+ 'character_id', 'character_ids',
+ 'corporation_id', 'corporation_ids',
+ 'alliance_id', 'alliance_ids',
+ ];
+
+ $presentKeys = array_filter($keys, function (string $key) use ($ids) {
+ return ! is_null($ids[$key] ?? null);
+ });
+
+ abort_unless(count($presentKeys) === 1, 403, 'Exactly one of the parameters ['.implode(', ', $keys).'] must be present.');
+
+ $validator = Validator::make($ids, [
+ 'character_id' => 'nullable|integer',
+ 'character_ids' => 'nullable|array',
+ 'character_ids.*' => 'integer',
+ 'corporation_id' => 'nullable|integer',
+ 'corporation_ids' => 'nullable|array',
+ 'corporation_ids.*' => 'integer',
+ 'alliance_id' => 'nullable|integer',
+ 'alliance_ids' => 'nullable|array',
+ 'alliance_ids.*' => 'integer',
+ ]);
+
+ abort_if($validator->fails(), 403, implode(', ', $validator->errors()->all()));
+
+ return $validator->validated();
+ }
+}
diff --git a/src/Services/Permissions/RolePermissionObjectService.php b/src/Services/Permissions/RolePermissionObjectService.php
new file mode 100644
index 0000000..9c58c3c
--- /dev/null
+++ b/src/Services/Permissions/RolePermissionObjectService.php
@@ -0,0 +1,27 @@
+role_affiliated_ids_service = $role_affiliated_ids_service ?? new RoleAffiliatedIdsService;
+ }
+
+ public function get(Role $role): Collection
+ {
+ $role = $role->loadMissing('permissions');
+
+ $affiliated_ids = $this->role_affiliated_ids_service->get($role);
+
+ return $role->permissions
+ ->mapWithKeys(fn (Permission $permission) => [$permission->name => $affiliated_ids]);
+ }
+}
diff --git a/src/Services/Permissions/UserPermissionService.php b/src/Services/Permissions/UserPermissionService.php
new file mode 100644
index 0000000..4a3c666
--- /dev/null
+++ b/src/Services/Permissions/UserPermissionService.php
@@ -0,0 +1,77 @@
+role_permission_object_service = $role_permission_object_service ?? new RolePermissionObjectService;
+ }
+
+ public function get(User $user): array
+ {
+
+ $user = $user->loadMissing(['characters.roles', 'roles.permissions']);
+
+ $this->buildCorporationRoles($user);
+ $this->buildPermissions($user);
+ $this->buildCharacterIds($user);
+
+ return [
+ 'corporation_roles' => $this->corporation_roles,
+ 'permissions' => $this->permissions,
+ 'character_ids' => $this->character_ids,
+ 'owned_character_ids' => $user->characters->pluck('character_id')->toArray(),
+ ];
+
+ }
+
+ private function buildCorporationRoles(User $user): void
+ {
+ $user
+ ->characters
+ ->each(function (CharacterInfo $character) {
+
+ /** @var array $roles */
+ $roles = $character->roles->roles ?? [];
+
+ if (empty($roles)) {
+ return;
+ }
+
+ foreach ($roles as $role) {
+ $this->corporation_roles[$role] = array_merge($this->corporation_roles[$role] ?? [], [$character->corporation_id]);
+ }
+ });
+ }
+
+ private function buildPermissions(User $user): void
+ {
+ $user->roles->each(function (Role $role) {
+ $role_permissions = $this->role_permission_object_service->get($role);
+
+ // merge on permissions. The key might exist, so we extend the array
+ $this->permissions = $role_permissions
+ ->mergeRecursive($this->permissions)
+ ->toArray();
+
+ });
+ }
+
+ private function buildCharacterIds(User $user): void
+ {
+ $this->character_ids = $user->characters->pluck('character_id')->toArray();
+ }
+}
diff --git a/src/Services/Roles/AbstractRoleService.php b/src/Services/Roles/AbstractRoleService.php
new file mode 100644
index 0000000..adb814a
--- /dev/null
+++ b/src/Services/Roles/AbstractRoleService.php
@@ -0,0 +1,271 @@
+create([
+ 'role_id' => $this->role->id,
+ 'affiliatable_id' => $entity_id,
+ 'affiliatable_type' => $entity_type,
+ 'type' => $affiliationType->value,
+ ]);
+ }
+
+ private function resetAffiliation(): void
+ {
+ Affiliation::query()
+ ->where('role_id', $this->role->id)
+ ->delete();
+ }
+
+ /**
+ * @throws \Throwable
+ */
+ protected function addCriteria(CriteriaData ...$entities): void
+ {
+ $this->resetCriteria();
+
+ foreach ($entities as $entity) {
+ $this->setRoleMembership(
+ entity_id: $entity->entity_id,
+ entity_type: $entity->entityClass()
+ );
+ }
+ }
+
+ private function resetCriteria(): void
+ {
+ RoleMembership::query()
+ ->where('role_id', $this->role->id)
+ ->whereIn('entity_type', [CorporationInfo::class, AllianceInfo::class])
+ ->delete();
+ }
+
+ private function revokeTheRolesFromUsersThatAreNotInMembers(\Illuminate\Support\Collection $member_ids): void
+ {
+ User::query()
+ ->whereHas('roles', fn (Builder $query) => $query->where('id', $this->role->id))
+ ->whereNotIn('id', $member_ids)
+ ->each(fn (User $user) => $user->removeRole($this->role));
+ }
+
+ private function getActiveMembers(): \Illuminate\Support\Collection
+ {
+ return $this->role->role_memberships()
+ ->where('entity_type', User::class)
+ ->where('status', RoleMembershipStatus::ACTIVE)
+ ->pluck('entity_id');
+ }
+
+ private function assignTheRolesToUsersThatAreInMembers(\Illuminate\Support\Collection $member_ids): void
+ {
+ User::query()
+ ->whereDoesntHave('roles', fn (Builder $query) => $query->where('id', $this->role->id))
+ ->whereIn('id', $member_ids)
+ ->each(fn (User $user) => $user->assignRole($this->role));
+ }
+
+ protected function removeRoleMembership(User $user): void
+ {
+ RoleMembership::query()
+ ->where('role_id', $this->role->id)
+ ->where('entity_id', $user->id)
+ ->where('entity_type', User::class)
+ ->delete();
+ }
+
+ public function setRoleType(RoleType $roleType): void
+ {
+ $originalRoleType = $this->role->type;
+
+ // if the role type has not changed, we return early
+ if ($originalRoleType === $roleType) {
+ return;
+ }
+
+ $this->role->update([
+ 'type' => $roleType,
+ ]);
+
+ $this->resetRoleMemberships();
+ }
+
+ protected function setRoleMembership(
+ int|string $entity_id,
+ string $entity_type,
+ bool $can_moderate = false,
+ ?RoleMembershipStatus $status = null
+ ): void {
+
+ $values_to_update = ['can_moderate' => $can_moderate];
+
+ // if $status is set, we add it to the values to update
+ if ($status) {
+ $values_to_update['status'] = $status->value;
+ }
+
+ RoleMembership::query()->updateOrInsert([
+ 'role_id' => $this->role->id,
+ 'entity_id' => $entity_id,
+ 'entity_type' => $entity_type,
+ ], $values_to_update);
+ }
+
+ protected function getAssignedCharacterIds(): array
+ {
+ $role = $this->role->refresh()->loadMissing(['role_memberships.entity' => function (MorphTo $morph_to) {
+ $morph_to->morphWith([CorporationInfo::class => 'characters', AllianceInfo::class => 'characters']);
+ }]);
+
+ return $role
+ ->role_memberships
+ ->filter(fn (RoleMembership $role_membership) => $role_membership->entity_type === CorporationInfo::class || $role_membership->entity_type === AllianceInfo::class)
+ ->pluck('entity.characters')
+ ->flatten()
+ ->pluck('character_id')
+ ->toArray();
+ }
+
+ protected function getUnassignedMembers(): Collection
+ {
+
+ $members = RoleMembership::query()
+ ->where('role_id', $this->role->id)
+ ->where('entity_type', User::class);
+
+ $character_ids = $this->getAssignedCharacterIds();
+
+ if (! array_filter($character_ids)) {
+ return $members->get();
+ }
+
+ return $members->whereDoesntHaveMorph(
+ 'entity',
+ [User::class],
+ fn (Builder $query) => $query
+ ->whereHas('characters', fn (Builder $query) => $query
+ ->whereIn('character_infos.character_id', $character_ids)
+ )
+ )
+ ->get();
+ }
+
+ protected function removeUnassignedMembers(): void
+ {
+ $unassigned_members = $this->getUnassignedMembers();
+ $unassigned_members->each(fn (RoleMembership $role_membership) => $role_membership->delete());
+ }
+
+ public function handleMembers(): void
+ {
+ // sync the members, so that only the members that are compliant are considered as active
+ $this->syncMembers();
+
+ // get the active members
+ $member_ids = $this->getActiveMembers();
+
+ // revoke the roles from users that are not in members
+ $this->revokeTheRolesFromUsersThatAreNotInMembers($member_ids);
+
+ // assign the roles to users that are in members
+ $this->assignTheRolesToUsersThatAreInMembers($member_ids);
+ }
+
+ protected function isUserCompliant(User $user): bool
+ {
+ // build a service to check if user is compliant. We do not consider applications here
+ $is_user_compliant_service = new IsUserCompliantService(false);
+
+ return $is_user_compliant_service->check($user);
+ }
+
+ /**
+ * @throws \Throwable
+ */
+ public function syncAffiliateManyEntities(AffiliationData ...$entity_sets): void
+ {
+ $this->resetAffiliation();
+
+ foreach ($entity_sets as $entity_set) {
+ $this->affiliateEntity($entity_set->entity_id, $entity_set->entityClass(), $entity_set->affiliation_type);
+ }
+ }
+
+ public function updateMemberStatusBasedOnUserCompliance(): void
+ {
+ RoleMembership::query()
+ ->where('role_id', $this->role->id)
+ ->where('entity_type', User::class)
+ ->whereIn('status', [RoleMembershipStatus::ACTIVE->value, RoleMembershipStatus::INACTIVE->value])
+ ->with('entity')
+ ->get()
+ ->each(fn (RoleMembership $role_membership) => $role_membership->updateOrFail([
+ 'status' => $this->isUserCompliant($role_membership->entity) ? RoleMembershipStatus::ACTIVE : RoleMembershipStatus::INACTIVE,
+ ]));
+ }
+
+ abstract public function syncMembers(): void;
+
+ protected function resetRoleMemberships(): void
+ {
+ RoleMembership::query()
+ ->where('role_id', $this->role->id)
+ ->delete();
+ }
+
+ protected function isModerator(User $user): bool
+ {
+ return RoleMembership::query()
+ ->where('role_id', $this->role->id)
+ ->where('entity_id', $user->id)
+ ->where('entity_type', User::class)
+ ->where('can_moderate', true)
+ ->exists();
+ }
+
+ protected function meetsCriteria(User $user): bool
+ {
+
+ $assigned_character_ids = $this->getAssignedCharacterIds();
+
+ // return early if no character is assigned
+ if (empty($assigned_character_ids)) {
+ return false;
+ }
+
+ return User::query()
+ ->where('id', $user->id)
+ ->whereHas('characters', fn (Builder $query) => $query->whereIn('character_infos.character_id', $assigned_character_ids))
+ ->exists();
+ }
+
+ public function updateRoleName(string $name): void
+ {
+ $this->role->update([
+ 'name' => $name,
+ ]);
+ }
+}
diff --git a/src/Services/Roles/AutomaticRoleService.php b/src/Services/Roles/AutomaticRoleService.php
new file mode 100644
index 0000000..6331b37
--- /dev/null
+++ b/src/Services/Roles/AutomaticRoleService.php
@@ -0,0 +1,62 @@
+addCriteria(...$entities);
+
+ $this->handleMembers();
+ }
+
+ public function syncMembers(): void
+ {
+ // remove members that are not within users
+ $this->removeUnassignedMembers();
+
+ $this->addAssignedMembers();
+
+ $this->updateMemberStatusBasedOnUserCompliance();
+ }
+
+ private function addAssignedMembers(): void
+ {
+ $assigned_character_ids = $this->getAssignedCharacterIds();
+ $users = User::query()
+ ->whereHas('characters', fn (Builder $query) => $query->whereIn('character_infos.character_id', $assigned_character_ids))
+ ->get();
+
+ $users->each(fn (User $user) => $this->setRoleMembership(
+ entity_id: $user->id,
+ entity_type: User::class,
+ status: RoleMembershipStatus::ACTIVE
+ ));
+ }
+
+ public function canJoin(User $user): bool
+ {
+ return false;
+ }
+
+ public function canModerate(User $user): bool
+ {
+ return false;
+ }
+
+ public function canView(User $user): bool
+ {
+ return $this->meetsCriteria($user);
+ }
+}
diff --git a/src/Services/Roles/BaseRoleService.php b/src/Services/Roles/BaseRoleService.php
new file mode 100644
index 0000000..3e68990
--- /dev/null
+++ b/src/Services/Roles/BaseRoleService.php
@@ -0,0 +1,92 @@
+for($role);
+ }
+
+ public function for(Role|string|int $role): self
+ {
+
+ /* @var Role $resolved_role */
+ $resolved_role = match (true) {
+ $role instanceof Role => $role,
+ is_string($role) => Role::findByName($role),
+ is_int($role) => Role::findById($role),
+ };
+
+ $this->role = $resolved_role;
+
+ return $this;
+ }
+
+ public function automatic(): AutomaticRoleService
+ {
+ return new AutomaticRoleService($this->role);
+ }
+
+ public function onRequest(): OnRequestRoleService
+ {
+ return new OnRequestRoleService($this->role);
+ }
+
+ public function manual(): ManualRoleService
+ {
+ return new ManualRoleService($this->role);
+ }
+
+ public function optIn(): OptInRoleService
+ {
+ return new OptInRoleService($this->role);
+ }
+
+ /**
+ * @throws \Exception
+ */
+ public function getTypeService(): RoleServiceInterface
+ {
+ return match ($this->getType()) {
+ RoleType::AUTOMATIC => $this->automatic(),
+ RoleType::ON_REQUEST => $this->onRequest(),
+ RoleType::MANUAL => $this->manual(),
+ RoleType::OPT_IN => $this->optIn(),
+ };
+ }
+
+ public function getType(): RoleType
+ {
+ return $this->role->type;
+ }
+
+ public function handleMembers(): void
+ {
+ $this->getTypeService()->handleMembers();
+ }
+
+ public function canView(User $user): bool
+ {
+ return $this->getTypeService()->canView($user);
+ }
+
+ public function canJoin(User $user): bool
+ {
+ return $this->getTypeService()->canJoin($user);
+ }
+
+ public function canModerate(User $user): bool
+ {
+ return $this->getTypeService()->canModerate($user);
+ }
+}
diff --git a/src/Services/Roles/DTO/AffiliationData.php b/src/Services/Roles/DTO/AffiliationData.php
new file mode 100644
index 0000000..57e4fa9
--- /dev/null
+++ b/src/Services/Roles/DTO/AffiliationData.php
@@ -0,0 +1,38 @@
+entity_type) {
+ 'character' => CharacterInfo::class,
+ 'corporation' => CorporationInfo::class,
+ 'alliance' => AllianceInfo::class,
+ default => throw new \ValueError("Unknown entity type: {$this->entity_type}"),
+ };
+ }
+}
diff --git a/src/Services/Roles/DTO/CriteriaData.php b/src/Services/Roles/DTO/CriteriaData.php
new file mode 100644
index 0000000..3353d5b
--- /dev/null
+++ b/src/Services/Roles/DTO/CriteriaData.php
@@ -0,0 +1,33 @@
+entity_type) {
+ 'corporation' => CorporationInfo::class,
+ 'alliance' => AllianceInfo::class,
+ default => throw new \ValueError("Unknown entity type: {$this->entity_type}"),
+ };
+ }
+}
diff --git a/src/Services/Roles/ManualRoleService.php b/src/Services/Roles/ManualRoleService.php
new file mode 100644
index 0000000..758895c
--- /dev/null
+++ b/src/Services/Roles/ManualRoleService.php
@@ -0,0 +1,53 @@
+setRoleMembership(
+ entity_id: $user->id,
+ entity_type: User::class,
+ can_moderate: $can_moderate
+ );
+ }
+
+ public function addMember(User $user): void
+ {
+ $this->setRoleMembership(
+ entity_id: $user->id,
+ entity_type: User::class,
+ status: RoleMembershipStatus::ACTIVE
+ );
+ }
+
+ public function removeMember(User $user): void
+ {
+ $this->removeRoleMembership($user);
+ }
+
+ public function syncMembers(): void
+ {
+ // update the status of the members based on the user compliance
+ $this->updateMemberStatusBasedOnUserCompliance();
+ }
+
+ public function canJoin(User $user): bool
+ {
+ return false;
+ }
+
+ public function canModerate(User $user): bool
+ {
+ return $this->isModerator($user);
+ }
+
+ public function canView(User $user): bool
+ {
+ return false;
+ }
+}
diff --git a/src/Services/Roles/OnRequestRoleService.php b/src/Services/Roles/OnRequestRoleService.php
new file mode 100644
index 0000000..978557d
--- /dev/null
+++ b/src/Services/Roles/OnRequestRoleService.php
@@ -0,0 +1,101 @@
+addCriteria(...$entities);
+
+ $this->syncMembers();
+ }
+
+ public function submitApplicationForRole(User $user): void
+ {
+
+ $meets_criteria = $this->meetsCriteria($user);
+
+ if (! $meets_criteria) {
+ throw new \Exception('User does not meet criteria to join role');
+ }
+
+ $this->setRoleMembership(
+ entity_id: $user->id,
+ entity_type: User::class,
+ status: RoleMembershipStatus::PENDING
+ );
+ }
+
+ /**
+ * @throws \Exception
+ */
+ public function approveApplicationForRole(User $user): void
+ {
+
+ $meets_criteria = $this->meetsCriteria($user);
+
+ if (! $meets_criteria) {
+ $this->removeRoleMembership($user);
+ throw new \Exception('User does not meet criteria to join role');
+ }
+
+ $this->setRoleMembership(
+ entity_id: $user->id,
+ entity_type: User::class,
+ status: RoleMembershipStatus::ACTIVE
+ );
+ }
+
+ public function denyApplication(User $user): void
+ {
+ $this->removeRoleMembership($user);
+ }
+
+ public function removeApplication(User $user): void
+ {
+ $this->removeRoleMembership($user);
+ }
+
+ public function setModerator(User $user, bool $can_moderate = true): void
+ {
+ $this->setRoleMembership(
+ entity_id: $user->id,
+ entity_type: User::class,
+ can_moderate: $can_moderate
+ );
+ }
+
+ public function syncMembers(): void
+ {
+ // remove all members that are not within the criteria
+ $this->removeUnassignedMembers();
+
+ // update the status of the members based on the user compliance
+ $this->updateMemberStatusBasedOnUserCompliance();
+ }
+
+ public function canView(User $user): bool
+ {
+ return $this->meetsCriteria($user);
+ }
+
+ public function canJoin(User $user): bool
+ {
+ return $this->meetsCriteria($user);
+ }
+
+ public function canModerate(User $user): bool
+ {
+ return $this->isModerator($user);
+ }
+}
diff --git a/src/Services/Roles/OptInRoleService.php b/src/Services/Roles/OptInRoleService.php
new file mode 100644
index 0000000..b6d9bda
--- /dev/null
+++ b/src/Services/Roles/OptInRoleService.php
@@ -0,0 +1,75 @@
+addCriteria(...$entities);
+
+ $this->syncMembers();
+ }
+
+ /**
+ * @throws \Throwable
+ */
+ public function joinRole(User $user): void
+ {
+
+ throw_unless($this->meetsCriteria($user), \Exception::class, 'User does not meet criteria to join role');
+
+ $this->setRoleMembership(
+ entity_id: $user->id,
+ entity_type: User::class,
+ status: RoleMembershipStatus::ACTIVE
+ );
+ }
+
+ public function leaveRole(User $user): void
+ {
+ $this->removeRoleMembership($user);
+ }
+
+ public function setModerator(User $user, bool $can_moderate = true): void
+ {
+ $this->setRoleMembership(
+ entity_id: $user->id,
+ entity_type: User::class,
+ can_moderate: $can_moderate
+ );
+ }
+
+ public function syncMembers(): void
+ {
+ // remove all members that are not within the criteria
+ $this->removeUnassignedMembers();
+
+ // update the status of the members based on the user compliance
+ $this->updateMemberStatusBasedOnUserCompliance();
+ }
+
+ public function canView(User $user): bool
+ {
+ return $this->meetsCriteria($user);
+ }
+
+ public function canJoin(User $user): bool
+ {
+ return $this->meetsCriteria($user);
+ }
+
+ public function canModerate(User $user): bool
+ {
+ return $this->isModerator($user);
+ }
+}
diff --git a/src/Services/Roles/RoleAffiliatedIdsService.php b/src/Services/Roles/RoleAffiliatedIdsService.php
new file mode 100644
index 0000000..5cff9b1
--- /dev/null
+++ b/src/Services/Roles/RoleAffiliatedIdsService.php
@@ -0,0 +1,71 @@
+buildAffiliatedIds($role);
+ }
+
+ private function buildInverse(Collection $inverted): Collection
+ {
+
+ return CharacterInfo::query()->whereNotIn('character_id', $inverted)->pluck('character_id')
+ ->merge(CorporationInfo::query()->whereNotIn('corporation_id', $inverted)->pluck('corporation_id'))
+ ->merge(AllianceInfo::query()->whereNotIn('alliance_id', $inverted)->pluck('alliance_id'));
+ }
+
+ private function buildAffiliatedIds(Role $role): array
+ {
+ $role = $this->loadMissingRelationships($role);
+
+ $allowed = collect();
+ $inverted = collect();
+ $forbidden = collect();
+
+ $role->affiliations->each(function (Affiliation $affiliation) use (&$allowed, &$inverted, &$forbidden) {
+
+ $affiliated_ids = $affiliation->affiliated_ids;
+
+ match ($affiliation->type) {
+ AffiliationType::ALLOWED->value => $allowed = $allowed->merge($affiliated_ids),
+ AffiliationType::INVERSE->value => $inverted = $inverted->merge($affiliated_ids),
+ AffiliationType::FORBIDDEN->value => $forbidden = $forbidden->merge($affiliated_ids),
+ };
+ });
+
+ // if ids are present,
+ // build inverse of inverted and merge with allowed
+ if ($inverted->isNotEmpty()) {
+ $allowed = $allowed->merge($this->buildInverse($inverted));
+ }
+
+ // remove forbidden
+ $allowed = $allowed->diff($forbidden);
+
+ return $allowed->all();
+ }
+
+ public function loadMissingRelationships(Role $role): Role
+ {
+ return $role->loadMissing([
+ 'affiliations.affiliatable' => fn (MorphTo $morph_to) => $morph_to
+ ->morphWith([
+ CorporationInfo::class => 'characters',
+ AllianceInfo::class => ['characters', 'corporations'],
+ ]),
+ ]);
+ }
+}
diff --git a/src/Services/Roles/RoleServiceInterface.php b/src/Services/Roles/RoleServiceInterface.php
new file mode 100644
index 0000000..b999a0c
--- /dev/null
+++ b/src/Services/Roles/RoleServiceInterface.php
@@ -0,0 +1,23 @@
+ */
+ const array USER_RELATIONS = [
+ 'characters' => self::CHARACTER_RELATIONS,
+ 'application.corporation' => ['ssoScopes', 'alliance.ssoScopes'],
+ ];
+
+ /** @var array */
+ const array CHARACTER_RELATIONS = [
+ 'alliance.ssoScopes',
+ 'corporation.ssoScopes',
+ 'application.corporation' => ['ssoScopes', 'alliance.ssoScopes'],
+ 'refresh_token',
+ ];
+
+ public function __construct(
+ private readonly bool $with_application_scopes = true,
+ private ?GlobalSsoScopesService $globalSsoScopesService = null
+ ) {
+ $this->globalSsoScopesService = $globalSsoScopesService ?? new GlobalSsoScopesService;
+ }
+
+ private function getUserRequiredScopes(User $user): array
+ {
+ $user = $user->loadMissing(self::USER_RELATIONS);
+
+ $required_scopes = $this->getUserScopes($user);
+
+ if ($this->isWithApplicationScopes()) {
+ $required_scopes['user_application_corporation_scopes'] = $user->application->corporation->ssoScopes->selected_scopes ?? [];
+ $required_scopes['user_application_alliance_scopes'] = $user->application->corporation->alliance->ssoScopes->selected_scopes ?? [];
+ }
+
+ return collect($required_scopes)
+ ->flatten(1)
+ ->filter()
+ ->unique()
+ ->flatten(1)
+ ->toArray();
+ }
+
+ private function getGlobalScopes(): array
+ {
+ return $this->globalSsoScopesService->get();
+ }
+
+ private function getUserScopes(User $user): array
+ {
+ // get all corporation and alliance ids
+ $corporation_ids = $user->characters->pluck('corporation.corporation_id')->unique()->all();
+ $alliance_ids = $user->characters->pluck('alliance.alliance_id')->filter()->unique()->all();
+
+ // get all scopes for the corporations and alliances
+ return SsoScopes::query()
+ ->whereIn('morphable_id', [...$corporation_ids, ...$alliance_ids])
+ ->where('type', 'user')
+ ->pluck('selected_scopes')
+ ->flatten()
+ ->unique()
+ ->toArray();
+ }
+
+ private function build(User $user): array
+ {
+ $user_required_scopes = $this->getUserRequiredScopes($user);
+
+ return $user->characters
+ ->map(function (CharacterInfo $character) use ($user_required_scopes) {
+
+ $required_scopes = [...$user_required_scopes, ...$this->getCharacterRequiredScopes($character)];
+ $token_scopes = $character->refresh_token->scopes ?? [];
+ $missing_scopes = array_diff($required_scopes, $token_scopes);
+
+ return [
+ 'character' => $character,
+ 'required_scopes' => $required_scopes,
+ 'missing_scopes' => $missing_scopes,
+ ];
+ })
+ ->toArray();
+ }
+
+ private function getCharacterRequiredScopes(CharacterInfo $character): array
+ {
+ $character = $character->loadMissing(self::CHARACTER_RELATIONS);
+
+ $required_scopes = [
+ 'corporation_scopes' => $character->corporation->ssoScopes->selected_scopes ?? [],
+ 'alliance_scopes' => $character->alliance->ssoScopes->selected_scopes ?? [],
+ 'global_scope' => $this->getGlobalScopes(),
+ ];
+
+ if ($this->isWithApplicationScopes()) {
+ $required_scopes['character_application_corporation_scopes'] = $character->application->corporation->ssoScopes->selected_scopes ?? [];
+ $required_scopes['character_application_alliance_scopes'] = $character->application->corporation->alliance->ssoScopes->selected_scopes ?? [];
+ }
+
+ return collect($required_scopes)
+ ->flatten(1)
+ ->filter()
+ ->unique()
+ ->flatten(1)
+ ->toArray();
+ }
+
+ private function isWithApplicationScopes(): bool
+ {
+ return $this->with_application_scopes;
+ }
+
+ public function get(User|CharacterInfo $entity): array
+ {
+
+ $user = User::query()
+ ->when($entity instanceof CharacterInfo, fn (Builder $query) => $query
+ ->whereHas('characters', fn (Builder $query) => $query
+ ->where('character_id', $entity->character_id)
+ )
+ )
+ ->with(self::USER_RELATIONS)
+ ->first();
+
+ return $this->build($user);
+ }
+}
diff --git a/src/Services/SsoScopes/GlobalSsoScopesService.php b/src/Services/SsoScopes/GlobalSsoScopesService.php
new file mode 100644
index 0000000..a957d5e
--- /dev/null
+++ b/src/Services/SsoScopes/GlobalSsoScopesService.php
@@ -0,0 +1,24 @@
+create([
+ 'selected_scopes' => $scopes,
+ 'type' => 'global',
+ ]);
+ }
+
+ public function get(): array
+ {
+ return SsoScopes::query()
+ ->where('type', 'global')
+ ->pluck('selected_scopes')
+ ->toArray();
+ }
+}
diff --git a/src/Services/SsoScopes/IsUserCompliantService.php b/src/Services/SsoScopes/IsUserCompliantService.php
new file mode 100644
index 0000000..1194a01
--- /dev/null
+++ b/src/Services/SsoScopes/IsUserCompliantService.php
@@ -0,0 +1,42 @@
+build_scopes_array_service = new BuildScopesArrayService($this->consider_applications);
+ }
+
+ public function check(User $user): bool
+ {
+ $missing_scopes = $this->getMissingScopes($user);
+
+ return $this->isUserCompliant($missing_scopes);
+ }
+
+ public function getMissingScopes(User $user): array
+ {
+ $scopes = $this->build_scopes_array_service
+ ->get($user);
+
+ return collect($scopes)
+ ->pluck('missing_scopes')
+ ->toArray();
+ }
+
+ private function isUserCompliant(array $missing_scopes): bool
+ {
+ $flat_missing_scopes = collect($missing_scopes)
+ ->flatten()
+ ->unique();
+
+ return $flat_missing_scopes->isEmpty();
+ }
+}
diff --git a/tests/Architecture/ArchitectureTest.php b/tests/Architecture/ArchitectureTest.php
new file mode 100644
index 0000000..29a26ff
--- /dev/null
+++ b/tests/Architecture/ArchitectureTest.php
@@ -0,0 +1,5 @@
+expect(['dd', 'dump'])
+ ->not->toBeUsed();
diff --git a/tests/Feature/Jobs/UserRolesSyncTest.php b/tests/Feature/Jobs/UserRolesSyncTest.php
deleted file mode 100644
index 919e208..0000000
--- a/tests/Feature/Jobs/UserRolesSyncTest.php
+++ /dev/null
@@ -1,153 +0,0 @@
-role = Role::create(['name' => 'derp']);
-
- test()->test_user = test()->test_user->refresh();
-
- test()->job = new UserRolesSync(test()->test_user);
-});
-
-it('gives automatic role', function () {
- // Update role to be automatic
- test()->role->update(['type' => 'automatic']);
-
- //assure that role is of type auto
- expect(test()->role->type)->toEqual('automatic');
-
- // First create acl affiliation with user
- test()->role->acl_affiliations()->create([
- 'affiliatable_id' => test()->test_character->character_id,
- 'affiliatable_type' => CharacterInfo::class,
- ]);
-
- expect(test()->role->members->isEmpty())->toBeTrue();
-
- test()->job->handle();
-
- expect(test()->role->refresh()->members->isEmpty())->toBeFalse();
-
- expect(test()->test_user->hasRole('derp'))->toBeTrue();
-});
-
-it('removes automatic role', function () {
- // Update role to be automatic
- test()->role->update(['type' => 'automatic']);
-
- //assure that role is of type auto
- expect(test()->role->type)->toEqual('automatic');
-
- // First create acl affiliation with user
- test()->role->acl_affiliations()->create([
- 'affiliatable_id' => test()->test_character->character_id,
- 'affiliatable_type' => CharacterInfo::class,
- ]);
-
- expect(test()->role->members->isEmpty())->toBeTrue();
-
- test()->job->handle();
-
- expect(test()->role->refresh()->members->isEmpty())->toBeFalse();
-
- expect(test()->test_user->hasRole('derp'))->toBeTrue();
-
- RefreshToken::find(test()->test_character->character_id)->delete();
-
- // we need a new job instance, as the valid character_ids are build in the constructor of the job
- $job = new UserRolesSync(test()->test_user->refresh());
- $job->handle();
-
- expect(test()->test_user->hasRole('derp'))->toBeFalse();
-});
-
-it('adds membership for paused user', function () {
- // Update role to be on-request
- test()->role->update(['type' => 'on-request']);
-
- //assure that role is of type auto
- expect(test()->role->type)->toEqual('on-request');
-
- // First create acl affiliation with user
- test()->role->acl_affiliations()->create([
- 'affiliatable_id' => test()->test_character->character_id,
- 'affiliatable_type' => CharacterInfo::class,
- ]);
-
- // Second add character as paused to role
- test()->role->acl_members()->create([
- 'user_id' => test()->test_user->getAuthIdentifier(),
- 'status' => 'paused',
- ]);
-
- expect(test()->role->members->isEmpty())->toBeTrue();
-
- test()->job->handle();
-
- expect(test()->role->refresh()->members->isEmpty())->toBeFalse();
-});
-
-it('removes membership if refresh token is removed', function () {
- // Update role to be on-request
- test()->role->update(['type' => 'on-request']);
-
- //assure that role is of type auto
- expect(test()->role->type)->toEqual('on-request');
-
- // First create acl affiliation with user
- test()->role->acl_affiliations()->create([
- 'affiliatable_id' => test()->test_character->character_id,
- 'affiliatable_type' => CharacterInfo::class,
- ]);
-
- // Second add character as paused to role
- test()->role->acl_members()->create([
- 'user_id' => test()->test_user->getAuthIdentifier(),
- 'status' => 'member',
- ]);
-
- expect(test()->role->refresh()->members->isEmpty())->toBeFalse();
-
- // Remove refresh_token
- RefreshToken::find(test()->test_character->character_id)->delete();
-
- // we need a new job instance, as the valid character_ids are build in the constructor of the job
- $job = new UserRolesSync(test()->test_user->refresh());
- $job->handle();
-
- expect(test()->role->refresh()->members->isEmpty())->toBeTrue();
-});
-
-test('roles without acl affiliations are not impacted by job', function () {
- // Update role to be on-request
- test()->role->update(['type' => 'automatic']);
-
- expect(test()->role->acl_affiliations->isEmpty())->toBeTrue();
-
- //assure that role is of type auto
- expect(test()->role->type)->toEqual('automatic');
-
- expect(test()->test_user->hasRole(test()->role))->toBeFalse();
-
- test()->job->handle();
-
- expect(test()->test_user->hasRole(test()->role))->toBeFalse();
-});
-
-test('dispatching roles sync', function () {
- Queue::fake();
-
- $dispatch_job = new DispatchUserRoleSync;
-
- $dispatch_job->handle();
-
- Queue::assertPushedOn('high', UserRolesSync::class);
-});
diff --git a/tests/Feature/Middleware/CheckAuthorizationTest.php b/tests/Feature/Middleware/CheckAuthorizationTest.php
new file mode 100644
index 0000000..6071019
--- /dev/null
+++ b/tests/Feature/Middleware/CheckAuthorizationTest.php
@@ -0,0 +1,326 @@
+role = Role::create(['name' => faker()->name]);
+ $this->permission_name = faker()->name;
+ test()->permission = Permission::create(['name' => $this->permission_name]);
+ test()->role->givePermissionTo(test()->permission);
+
+ test()->test_user->assignRole(test()->role);
+
+ app()[PermissionRegistrar::class]->forgetCachedPermissions();
+
+ Route::middleware([CheckAuthorization::class.":$this->permission_name"])
+ ->prefix('character')
+ ->name('character.')
+ ->group(function () {
+ Route::post('/test/', fn () => response('Hello World'))->name('post');
+ Route::get('/character/{character_id}/', fn (int $character_id) => response('Hello World'))->name('character');
+ Route::get('/corporation/{corporation_id}/', fn (int $corporation_id) => response('Hello World'))->name('corporation');
+ Route::get('/alliance/{alliance_id}/', fn (int $alliance_id) => response('Hello World'))->name('alliance');
+ Route::get('/character_ids', fn () => response('Hello World'))->name('character_ids');
+ Route::get('/corporation_ids', fn () => response('Hello World'))->name('corporation_ids');
+ Route::get('/alliance_ids', fn () => response('Hello World'))->name('alliance_ids');
+ });
+
+ Route::middleware([CheckAuthorization::class.":$this->permission_name,Director"])
+ ->prefix('corporation')
+ ->name('corporation.')
+ ->group(function () {
+ Route::post('/test/', fn () => response('Hello World'))->name('post');
+ Route::get('/character/{character_id}/', fn (int $character_id) => response('Hello World'))->name('character');
+ Route::get('/corporation/{corporation_id}/', fn (int $corporation_id) => response('Hello World'))->name('corporation');
+ Route::get('/alliance/{alliance_id}/', fn (int $alliance_id) => response('Hello World'))->name('alliance');
+ Route::get('/character_ids', fn () => response('Hello World'))->name('character_ids');
+ Route::get('/corporation_ids', fn () => response('Hello World'))->name('corporation_ids');
+ Route::get('/alliance_ids', fn () => response('Hello World'))->name('alliance_ids');
+ });
+
+ test()->secondary_character = CharacterInfo::factory()->create();
+ });
+
+ it('it validates parameters for superuser', function (string $method, string $route, int|array $route_param, string $status = 'ok') {
+ assignPermissionToTestUser(['superuser']);
+
+ test()->actingAs(test()->test_user);
+
+ $response = match ($method) {
+ 'post' => post(route($route, $route_param)),
+ 'get' => get(route($route, $route_param))
+ };
+
+ match ($status) {
+ 'forbidden' => $response->assertForbidden(), // 403
+ 'ok' => $response->assertOk()
+ };
+ })
+ ->with([
+ // Character
+ ['post', 'character.post', fn () => ['character_id' => test()->test_character->character_id]],
+ ['post', 'character.post', fn () => ['corporation_id' => test()->test_character->corporation->corporation_id]],
+ ['post', 'character.post', fn () => ['alliance_id' => test()->test_character->alliance->alliance_id]],
+ ['get', 'character.character', fn () => test()->test_character->character_id],
+ ['get', 'character.corporation', fn () => test()->test_character->corporation->corporation_id],
+ ['get', 'character.alliance', fn () => test()->test_character->alliance->alliance_id],
+ // ['get', 'character.character_ids', fn () => ['character_ids' => []], 'forbidden'],
+ // ['get', 'character.corporation_ids', fn () => ['corporation_ids' => []], 'forbidden'],
+ // ['get', 'character.alliance_ids', fn () => ['alliance_ids' => []], 'forbidden'],
+ ['get', 'character.character_ids', fn () => ['character_ids' => [test()->test_character->character_id]]],
+ ['get', 'character.corporation_ids', fn () => ['corporation_ids' => [test()->test_character->corporation->corporation_id]]],
+ ['get', 'character.corporation_ids', fn () => ['alliance_ids' => [test()->test_character->alliance->alliance_id]]],
+ // Corporation Role
+ ['post', 'corporation.post', fn () => ['character_id' => test()->test_character->character_id]],
+ ['post', 'corporation.post', fn () => ['corporation_id' => test()->test_character->corporation->corporation_id]],
+ ['post', 'corporation.post', fn () => ['alliance_id' => test()->test_character->alliance->alliance_id]],
+ ['get', 'corporation.character', fn () => test()->test_character->character_id],
+ ['get', 'corporation.corporation', fn () => test()->test_character->corporation->corporation_id],
+ ['get', 'corporation.alliance', fn () => test()->test_character->alliance->alliance_id],
+ // ['get', 'corporation.character_ids', fn () => ['corporation_ids' => []], 'forbidden'],
+ // ['get', 'corporation.corporation_ids', fn () => ['corporation_ids' => []], 'forbidden'],
+ // ['get', 'corporation.alliance_ids', fn () => ['alliance_ids' => []], 'forbidden'],
+ ['get', 'corporation.character_ids', fn () => ['corporation_ids' => [test()->test_character->character_id]]],
+ ['get', 'corporation.corporation_ids', fn () => ['corporation_ids' => [test()->test_character->corporation->corporation_id]]],
+ ['get', 'corporation.alliance_ids', fn () => ['alliance_ids' => [test()->test_character->alliance->alliance_id]]],
+ ]);
+
+ it('checks owned character ids', function (string $method, string $route, array|int $route_param, string $status = 'ok') {
+ expect(test()->test_user->can('superuser'))->toBeFalse();
+
+ // Ensure no stale character roles from other test runs pollute the permission check.
+ // This test verifies that owning a character does NOT grant corporation-level access.
+ CharacterRole::query()->delete();
+
+ test()->actingAs(test()->test_user);
+
+ // Act
+ $response = match ($method) {
+ 'post' => post(route($route, $route_param)),
+ 'get' => get(route($route, $route_param))
+ };
+
+ // Assert
+
+ match ($status) {
+ 'forbidden' => $response->assertForbidden(), // 403
+ 'ok' => $response->assertOk()
+ };
+ })
+ ->with([
+ ['post', 'character.post', fn () => ['character_id' => test()->test_character->character_id]],
+ ['post', 'character.post', fn () => ['corporation_id' => test()->test_character->corporation->corporation_id], 'forbidden'],
+ ['post', 'character.post', fn () => ['alliance_id' => test()->test_character->alliance->alliance_id], 'forbidden'],
+ ['get', 'character.character', fn () => test()->test_character->character_id],
+ ['get', 'character.corporation', fn () => test()->test_character->corporation->corporation_id, 'forbidden'],
+ ['get', 'character.alliance', fn () => test()->test_character->alliance->alliance_id, 'forbidden'],
+ ['get', 'character.character_ids', fn () => ['character_ids' => [test()->test_character->character_id]]],
+ ['get', 'character.corporation_ids', fn () => ['corporation_ids' => [test()->test_character->corporation->corporation_id]], 'forbidden'],
+ ['get', 'character.corporation_ids', fn () => ['alliance_ids' => [test()->test_character->alliance->alliance_id]], 'forbidden'],
+ // Corporation Role
+ ['post', 'corporation.post', fn () => ['character_id' => test()->test_character->character_id]],
+ ['post', 'corporation.post', fn () => ['corporation_id' => test()->test_character->corporation->corporation_id], 'forbidden'],
+ ['post', 'corporation.post', fn () => ['alliance_id' => test()->test_character->alliance->alliance_id], 'forbidden'],
+ ['get', 'corporation.character', fn () => test()->test_character->character_id],
+ ['get', 'corporation.corporation', fn () => test()->test_character->corporation->corporation_id, 'forbidden'],
+ ['get', 'corporation.alliance', fn () => test()->test_character->alliance->alliance_id, 'forbidden'],
+ ['get', 'corporation.character_ids', fn () => ['corporation_ids' => [test()->test_character->character_id]]],
+ ['get', 'corporation.corporation_ids', fn () => ['corporation_ids' => [test()->test_character->corporation->corporation_id]], 'forbidden'],
+ ['get', 'corporation.alliance_ids', fn () => ['alliance_ids' => [test()->test_character->alliance->alliance_id]], 'forbidden'],
+ ]);
+
+ it('checks owned corporation id', function (string $method, string $route, array|int $route_param) {
+ expect(test()->test_user->can('superuser'))->toBeFalse();
+
+ CharacterRole::query()->delete();
+
+ CharacterRole::factory()->create([
+ 'character_id' => test()->test_character->character_id,
+ 'roles' => ['Director'],
+ ]);
+
+ test()->actingAs(test()->test_user);
+
+ match ($method) {
+ 'post' => post(route($route, $route_param))->assertOk(),
+ 'get' => get(route($route, $route_param))->assertOk()
+ };
+ })
+ ->with([
+ ['post', 'corporation.post', fn () => ['corporation_id' => test()->test_character->corporation->corporation_id]],
+ ['get', 'corporation.corporation_ids', fn () => ['character_ids' => [test()->test_character->corporation->corporation_id]]],
+ ['get', 'corporation.corporation', fn () => test()->test_character->corporation->corporation_id],
+ ]);
+
+ it('checks affiliated ids', function (string $method, string $route, array|int $route_param, string $status = 'ok') {
+ expect(test()->test_user->can('superuser'))->toBeFalse();
+
+ createAffiliation(
+ test()->role,
+ test()->secondary_character->alliance->alliance_id,
+ AllianceInfo::class,
+ AffiliationType::ALLOWED
+ );
+
+ test()->actingAs(test()->test_user);
+
+ // Act
+ $response = match ($method) {
+ 'post' => post(route($route), $route_param),
+ 'get' => get(route($route, $route_param))
+ };
+
+ // Assert
+ expect(test()->test_user->roles)->toHaveCount(1)
+ ->and(test()->test_user->roles->first()->permissions->first()->name)->toBe($this->permission_name);
+
+ match ($status) {
+ 'forbidden' => $response->assertForbidden(), // 403
+ 'ok' => $response->assertOk()
+ };
+ })
+ ->with([
+ ['post', 'character.post', fn () => ['character_id' => test()->secondary_character->character_id]],
+ ['post', 'character.post', fn () => ['corporation_id' => test()->secondary_character->corporation->corporation_id]],
+ ['post', 'character.post', fn () => ['alliance_id' => test()->secondary_character->alliance->alliance_id]],
+ ['get', 'character.character', fn () => test()->secondary_character->character_id],
+ ['get', 'character.corporation', fn () => test()->secondary_character->corporation->corporation_id],
+ ['get', 'character.alliance', fn () => test()->secondary_character->alliance->alliance_id],
+ ['get', 'character.character_ids', fn () => ['character_ids' => [test()->secondary_character->character_id]]],
+ ['get', 'character.corporation_ids', fn () => ['corporation_ids' => [test()->secondary_character->corporation->corporation_id]]],
+ ['get', 'character.corporation_ids', fn () => ['alliance_ids' => [test()->secondary_character->alliance->alliance_id]]],
+ // Corporation Role
+ ['post', 'corporation.post', fn () => ['character_id' => test()->secondary_character->character_id]],
+ ['post', 'corporation.post', fn () => ['corporation_id' => test()->secondary_character->corporation->corporation_id]],
+ ['post', 'corporation.post', fn () => ['alliance_id' => test()->secondary_character->alliance->alliance_id]],
+ ['get', 'corporation.character', fn () => test()->secondary_character->character_id],
+ ['get', 'corporation.corporation', fn () => test()->secondary_character->corporation->corporation_id],
+ ['get', 'corporation.alliance', fn () => test()->secondary_character->alliance->alliance_id],
+ ['get', 'corporation.character_ids', fn () => ['corporation_ids' => [test()->secondary_character->character_id]]],
+ ['get', 'corporation.corporation_ids', fn () => ['corporation_ids' => [test()->secondary_character->corporation->corporation_id]]],
+ ['get', 'corporation.alliance_ids', fn () => ['alliance_ids' => [test()->secondary_character->alliance->alliance_id]]],
+ ]);
+
+ it('returns forbidden for non affiliated ids', function (string $method, string $route, array|int $route_param, string $status = 'ok') {
+ expect(test()->test_user->can('superuser'))->toBeFalse();
+
+ createAffiliation(
+ test()->role,
+ test()->secondary_character->character_id,
+ CharacterInfo::class,
+ AffiliationType::FORBIDDEN
+ );
+
+ test()->actingAs(test()->test_user);
+
+ $response = match ($method) {
+ 'post' => post(route($route), $route_param),
+ 'get' => get(route($route, $route_param))
+ };
+
+ match ($status) {
+ 'forbidden' => $response->assertForbidden(), // 403
+ 'ok' => $response->assertOk()
+ };
+ })
+ ->with([
+ // POST
+ ['post', 'character.post', fn () => ['character_id' => test()->secondary_character->character_id], 'forbidden'],
+ ['post', 'character.post', fn () => ['character_id' => test()->test_character->character_id], 'ok'],
+ ['post', 'character.post', fn () => ['character_ids' => [test()->secondary_character->character_id]], 'forbidden'],
+ ['post', 'character.post', fn () => ['character_ids' => [test()->test_character->character_id]], 'ok'],
+ ['post', 'character.post', fn () => ['character_ids' => [test()->test_character->character_id, test()->secondary_character->character_id]], 'forbidden'],
+ // GET
+ ['get', 'character.character', fn () => test()->secondary_character->character_id, 'forbidden'],
+ ['get', 'character.character', fn () => test()->test_character->character_id, 'ok'],
+ ['get', 'character.character_ids', fn () => ['character_ids' => [test()->secondary_character->character_id]], 'forbidden'],
+ ['get', 'character.character_ids', fn () => ['character_ids' => [test()->test_character->character_id]], 'ok'],
+ ['get', 'character.character_ids', fn () => ['character_ids' => [test()->test_character->character_id, test()->secondary_character->character_id]], 'forbidden'],
+ ]);
+
+ it('works with duplication of params', function () {
+ expect(test()->test_user->can('superuser'))->toBeFalse();
+
+ test()->actingAs(test()->test_user);
+
+ get(route('character.character', [
+ 'character_id' => test()->test_character->character_id,
+ '0' => test()->test_character->character_id,
+ ]))->assertOk();
+ });
+});
+
+describe('middleware checks permission or corporation role test', function () {
+ beforeEach(function () {
+ test()->role = Role::create(['name' => faker()->name]);
+ $this->permission_name = faker()->streetName();
+ test()->permission = Permission::create(['name' => $this->permission_name]);
+
+ Route::middleware([CheckAuthorization::class.":$this->permission_name,Accountant"])
+ ->prefix('test')
+ ->get('/', function () {
+ return 'test';
+ })->name('test');
+ });
+
+ it('user has permission', function (string $permission) {
+ test()->actingAs(test()->test_user);
+ test()->assignPermissionToTestUser($permission);
+
+ $response = $this->get(route('test'));
+ $response->assertStatus(200);
+ })->with([
+ 'superuser' => 'superuser',
+ 'accountant' => fn () => $this->permission_name,
+ ]);
+
+ it('has corporation_role', function (string $corporation_role) {
+ test()->actingAs(test()->test_user);
+ CharacterRole::query()->delete();
+
+ CharacterRole::factory()->create([
+ 'character_id' => test()->test_character->character_id,
+ 'roles' => [$corporation_role],
+ ]);
+
+ $response = $this->get(route('test'));
+ $response->assertStatus(200);
+ })->with([
+ 'Accountant' => 'Accountant',
+ 'Director' => 'Director',
+ ]);
+
+ it('is missing corporation_role', function () {
+ test()->actingAs(test()->test_user);
+ CharacterRole::query()->delete();
+
+ $response = $this->get(route('test'));
+ $response->assertStatus(403);
+ });
+});
+
+function createAffiliation(Role $role, int|string $affiliatable_id, string $affiliatable_type, AffiliationType $type): Affiliation
+{
+ /** @var Affiliation $affiliation */
+ $affiliation = Affiliation::query()->create([
+ 'role_id' => $role->id,
+ 'affiliatable_id' => $affiliatable_id,
+ 'affiliatable_type' => $affiliatable_type,
+ 'type' => $type->value,
+ ]);
+
+ return $affiliation;
+}
diff --git a/tests/Feature/Middleware/CheckPermissionAffiliationTest.php b/tests/Feature/Middleware/CheckPermissionAffiliationTest.php
deleted file mode 100644
index 8065ecd..0000000
--- a/tests/Feature/Middleware/CheckPermissionAffiliationTest.php
+++ /dev/null
@@ -1,259 +0,0 @@
-role = Role::create(['name' => faker()->name]);
- test()->permission = Permission::create(['name' => faker()->streetName()]);
-
- test()->role->givePermissionTo(test()->permission);
- test()->role->activateMember(test()->test_user);
-
- $permission = test()->permission->name;
-
- Route::middleware([CheckPermissionAndAffiliation::class.":$permission"])
- ->prefix('character')
- ->name('character.')
- ->group(function () {
- Route::post('/test/', fn () => response('Hello World'))->name('post');
- Route::get('/character/{character_id}/', fn (int $character_id) => response('Hello World'))->name('character');
- Route::get('/corporation/{corporation_id}/', fn (int $corporation_id) => response('Hello World'))->name('corporation');
- Route::get('/alliance/{alliance_id}/', fn (int $alliance_id) => response('Hello World'))->name('alliance');
- Route::get('/character_ids', fn () => response('Hello World'))->name('character_ids');
- Route::get('/corporation_ids', fn () => response('Hello World'))->name('corporation_ids');
- Route::get('/alliance_ids', fn () => response('Hello World'))->name('alliance_ids');
- });
-
- Route::middleware([CheckPermissionAndAffiliation::class.":$permission,Director"])
- ->prefix('corporation')
- ->name('corporation.')
- ->group(function () {
- Route::post('/test/', fn () => response('Hello World'))->name('post');
- Route::get('/character/{character_id}/', fn (int $character_id) => response('Hello World'))->name('character');
- Route::get('/corporation/{corporation_id}/', fn (int $corporation_id) => response('Hello World'))->name('corporation');
- Route::get('/alliance/{alliance_id}/', fn (int $alliance_id) => response('Hello World'))->name('alliance');
- Route::get('/character_ids', fn () => response('Hello World'))->name('character_ids');
- Route::get('/corporation_ids', fn () => response('Hello World'))->name('corporation_ids');
- Route::get('/alliance_ids', fn () => response('Hello World'))->name('alliance_ids');
- });
-
- test()->secondary_character = CharacterInfo::factory()->create();
-});
-
-it('it validates parameters for superuser', function (string $method, string $route, int|array $route_param, string $status = 'ok') {
- assignPermissionToTestUser(['superuser']);
-
- test()->actingAs(test()->test_user);
-
- $response = match ($method) {
- 'post' => post(route($route, $route_param)),
- 'get' => get(route($route, $route_param))
- };
-
- match ($status) {
- 'forbidden' => $response->assertForbidden(), //403
- 'unauthorized' => $response->assertUnauthorized(), //401
- 'ok' => $response->assertOk()
- };
-})
- ->with([
- // Character
- ['post', 'character.post', fn () => ['character_id' => test()->test_character->character_id]],
- ['post', 'character.post', fn () => ['corporation_id' => test()->test_character->corporation->corporation_id]],
- ['post', 'character.post', fn () => ['alliance_id' => test()->test_character->alliance->alliance_id]],
- ['get', 'character.character', fn () => test()->test_character->character_id],
- ['get', 'character.corporation', fn () => test()->test_character->corporation->corporation_id],
- ['get', 'character.alliance', fn () => test()->test_character->alliance->alliance_id],
- ['get', 'character.character_ids', fn () => ['character_ids' => []], 'forbidden'],
- ['get', 'character.corporation_ids', fn () => ['corporation_ids' => []], 'forbidden'],
- ['get', 'character.alliance_ids', fn () => ['alliance_ids' => []], 'forbidden'],
- ['get', 'character.character_ids', fn () => ['character_ids' => [test()->test_character->character_id]]],
- ['get', 'character.corporation_ids', fn () => ['corporation_ids' => [test()->test_character->corporation->corporation_id]]],
- ['get', 'character.corporation_ids', fn () => ['alliance_ids' => [test()->test_character->alliance->alliance_id]]],
- // Corporation Role
- ['post', 'corporation.post', fn () => ['character_id' => test()->test_character->character_id]],
- ['post', 'corporation.post', fn () => ['corporation_id' => test()->test_character->corporation->corporation_id]],
- ['post', 'corporation.post', fn () => ['alliance_id' => test()->test_character->alliance->alliance_id]],
- ['get', 'corporation.character', fn () => test()->test_character->character_id],
- ['get', 'corporation.corporation', fn () => test()->test_character->corporation->corporation_id],
- ['get', 'corporation.alliance', fn () => test()->test_character->alliance->alliance_id],
- ['get', 'corporation.character_ids', fn () => ['corporation_ids' => []], 'forbidden'],
- ['get', 'corporation.corporation_ids', fn () => ['corporation_ids' => []], 'forbidden'],
- ['get', 'corporation.alliance_ids', fn () => ['alliance_ids' => []], 'forbidden'],
- ['get', 'corporation.character_ids', fn () => ['corporation_ids' => [test()->test_character->character_id]]],
- ['get', 'corporation.corporation_ids', fn () => ['corporation_ids' => [test()->test_character->corporation->corporation_id]]],
- ['get', 'corporation.alliance_ids', fn () => ['alliance_ids' => [test()->test_character->alliance->alliance_id]]],
- ]);
-
-it('checks owned character ids', function (string $method, string $route, array|int $route_param, string $status = 'ok') {
- expect(test()->test_user->can('superuser'))->toBeFalse();
-
- test()->actingAs(test()->test_user);
-
- $response = match ($method) {
- 'post' => post(route($route, $route_param)),
- 'get' => get(route($route, $route_param))
- };
-
- match ($status) {
- 'forbidden' => $response->assertForbidden(), //403
- 'unauthorized' => $response->assertUnauthorized(), //401
- 'ok' => $response->assertOk()
- };
-})
- ->with([
- ['post', 'character.post', fn () => ['character_id' => test()->test_character->character_id]],
- ['post', 'character.post', fn () => ['corporation_id' => test()->test_character->corporation->corporation_id], 'unauthorized'],
- ['post', 'character.post', fn () => ['alliance_id' => test()->test_character->alliance->alliance_id], 'unauthorized'],
- ['get', 'character.character', fn () => test()->test_character->character_id],
- ['get', 'character.corporation', fn () => test()->test_character->corporation->corporation_id, 'unauthorized'],
- ['get', 'character.alliance', fn () => test()->test_character->alliance->alliance_id, 'unauthorized'],
- ['get', 'character.character_ids', fn () => ['character_ids' => []], 'forbidden'],
- ['get', 'character.corporation_ids', fn () => ['corporation_ids' => []], 'forbidden'],
- ['get', 'character.alliance_ids', fn () => ['alliance_ids' => []], 'forbidden'],
- ['get', 'character.character_ids', fn () => ['character_ids' => [test()->test_character->character_id]]],
- ['get', 'character.corporation_ids', fn () => ['corporation_ids' => [test()->test_character->corporation->corporation_id]], 'unauthorized'],
- ['get', 'character.corporation_ids', fn () => ['alliance_ids' => [test()->test_character->alliance->alliance_id]], 'unauthorized'],
- // Corporation Role
- ['post', 'corporation.post', fn () => ['character_id' => test()->test_character->character_id]],
- ['post', 'corporation.post', fn () => ['corporation_id' => test()->test_character->corporation->corporation_id], 'unauthorized'],
- ['post', 'corporation.post', fn () => ['alliance_id' => test()->test_character->alliance->alliance_id], 'unauthorized'],
- ['get', 'corporation.character', fn () => test()->test_character->character_id],
- ['get', 'corporation.corporation', fn () => test()->test_character->corporation->corporation_id, 'unauthorized'],
- ['get', 'corporation.alliance', fn () => test()->test_character->alliance->alliance_id, 'unauthorized'],
- ['get', 'corporation.character_ids', fn () => ['corporation_ids' => []], 'forbidden'],
- ['get', 'corporation.corporation_ids', fn () => ['corporation_ids' => []], 'forbidden'],
- ['get', 'corporation.alliance_ids', fn () => ['alliance_ids' => []], 'forbidden'],
- ['get', 'corporation.character_ids', fn () => ['corporation_ids' => [test()->test_character->character_id]]],
- ['get', 'corporation.corporation_ids', fn () => ['corporation_ids' => [test()->test_character->corporation->corporation_id]], 'unauthorized'],
- ['get', 'corporation.alliance_ids', fn () => ['alliance_ids' => [test()->test_character->alliance->alliance_id]], 'unauthorized'],
- ]);
-
-it('checks owned corporation id', function (string $method, string $route, array|int $route_param) {
- expect(test()->test_user->can('superuser'))->toBeFalse();
-
- CharacterRole::factory()->create([
- 'character_id' => test()->test_character->character_id,
- 'roles' => ['Director'],
- ]);
-
- test()->actingAs(test()->test_user);
-
- match ($method) {
- 'post' => post(route($route, $route_param))->assertOk(),
- 'get' => get(route($route, $route_param))->assertOk()
- };
-})
- ->with([
- ['post', 'corporation.post', fn () => ['corporation_id' => test()->test_character->corporation->corporation_id]],
- ['get', 'corporation.corporation_ids', fn () => ['character_ids' => [test()->test_character->corporation->corporation_id]]],
- ['get', 'corporation.corporation', fn () => test()->test_character->corporation->corporation_id],
- ]);
-
-it('checks affiliated ids', function (string $method, string $route, array|int $route_param, string $status = 'ok') {
- expect(test()->test_user->can('superuser'))->toBeFalse();
-
- test()->createAffiliation(
- test()->role,
- test()->secondary_character->alliance->alliance_id,
- \Seatplus\Eveapi\Models\Alliance\AllianceInfo::class,
- 'allowed'
- );
-
- test()->actingAs(test()->test_user);
-
- $response = match ($method) {
- 'post' => post(route($route), $route_param),
- 'get' => get(route($route, $route_param))
- };
-
- match ($status) {
- 'forbidden' => $response->assertForbidden(), //403
- 'unauthorized' => $response->assertUnauthorized(), //401
- 'ok' => $response->assertOk()
- };
-})
- ->with([
- ['post', 'character.post', fn () => ['character_id' => test()->secondary_character->character_id]],
- ['post', 'character.post', fn () => ['corporation_id' => test()->secondary_character->corporation->corporation_id]],
- ['post', 'character.post', fn () => ['alliance_id' => test()->secondary_character->alliance->alliance_id]],
- ['get', 'character.character', fn () => test()->secondary_character->character_id],
- ['get', 'character.corporation', fn () => test()->secondary_character->corporation->corporation_id],
- ['get', 'character.alliance', fn () => test()->secondary_character->alliance->alliance_id],
- ['get', 'character.character_ids', fn () => ['character_ids' => []], 'forbidden'],
- ['get', 'character.corporation_ids', fn () => ['corporation_ids' => []], 'forbidden'],
- ['get', 'character.alliance_ids', fn () => ['alliance_ids' => []], 'forbidden'],
- ['get', 'character.character_ids', fn () => ['character_ids' => [test()->secondary_character->character_id]]],
- ['get', 'character.corporation_ids', fn () => ['corporation_ids' => [test()->secondary_character->corporation->corporation_id]]],
- ['get', 'character.corporation_ids', fn () => ['alliance_ids' => [test()->secondary_character->alliance->alliance_id]]],
- // Corporation Role
- ['post', 'corporation.post', fn () => ['character_id' => test()->secondary_character->character_id]],
- ['post', 'corporation.post', fn () => ['corporation_id' => test()->secondary_character->corporation->corporation_id]],
- ['post', 'corporation.post', fn () => ['alliance_id' => test()->secondary_character->alliance->alliance_id]],
- ['get', 'corporation.character', fn () => test()->secondary_character->character_id],
- ['get', 'corporation.corporation', fn () => test()->secondary_character->corporation->corporation_id],
- ['get', 'corporation.alliance', fn () => test()->secondary_character->alliance->alliance_id],
- ['get', 'corporation.character_ids', fn () => ['corporation_ids' => []], 'forbidden'],
- ['get', 'corporation.corporation_ids', fn () => ['corporation_ids' => []], 'forbidden'],
- ['get', 'corporation.alliance_ids', fn () => ['alliance_ids' => []], 'forbidden'],
- ['get', 'corporation.character_ids', fn () => ['corporation_ids' => [test()->secondary_character->character_id]]],
- ['get', 'corporation.corporation_ids', fn () => ['corporation_ids' => [test()->secondary_character->corporation->corporation_id]]],
- ['get', 'corporation.alliance_ids', fn () => ['alliance_ids' => [test()->secondary_character->alliance->alliance_id]]],
- ]);
-
-it('returns unauthorized for non affiliated ids', function (string $method, string $route, array|int $route_param, string $status = 'ok') {
- expect(test()->test_user->can('superuser'))->toBeFalse();
-
- test()->createAffiliation(
- test()->role,
- test()->secondary_character->character_id,
- CharacterInfo::class,
- 'forbidden'
- );
-
- test()->actingAs(test()->test_user);
-
- $response = match ($method) {
- 'post' => post(route($route), $route_param),
- 'get' => get(route($route, $route_param))
- };
-
- match ($status) {
- 'forbidden' => $response->assertForbidden(), //403
- 'unauthorized' => $response->assertUnauthorized(), //401
- 'ok' => $response->assertOk()
- };
-})
- ->with([
- // POST
- ['post', 'character.post', fn () => ['character_id' => test()->secondary_character->character_id], 'unauthorized'],
- ['post', 'character.post', fn () => ['character_id' => test()->test_character->character_id], 'ok'],
- ['post', 'character.post', fn () => ['character_ids' => [test()->secondary_character->character_id]], 'unauthorized'],
- ['post', 'character.post', fn () => ['character_ids' => [test()->test_character->character_id]], 'ok'],
- ['post', 'character.post', fn () => ['character_ids' => [test()->test_character->character_id, test()->secondary_character->character_id]], 'unauthorized'],
- // GET
- ['get', 'character.character', fn () => test()->secondary_character->character_id, 'unauthorized'],
- ['get', 'character.character', fn () => test()->test_character->character_id, 'ok'],
- ['get', 'character.character_ids', fn () => ['character_ids' => [test()->secondary_character->character_id]], 'unauthorized'],
- ['get', 'character.character_ids', fn () => ['character_ids' => [test()->test_character->character_id]], 'ok'],
- ['get', 'character.character_ids', fn () => ['character_ids' => [test()->test_character->character_id, test()->secondary_character->character_id]], 'unauthorized'],
- ]);
-
-it('works with duplication of params', function () {
- expect(test()->test_user->can('superuser'))->toBeFalse();
-
- test()->actingAs(test()->test_user);
-
- get(route('character.character', [
- 'character_id' => test()->test_character->character_id,
- '0' => test()->test_character->character_id,
- ]))->assertOk();
-});
diff --git a/tests/Feature/Middleware/CheckPermissionOrCorporationRoleTest.php b/tests/Feature/Middleware/CheckPermissionOrCorporationRoleTest.php
deleted file mode 100644
index a3e96cc..0000000
--- a/tests/Feature/Middleware/CheckPermissionOrCorporationRoleTest.php
+++ /dev/null
@@ -1,59 +0,0 @@
-role = Role::create(['name' => faker()->name]);
- test()->permission = Permission::create(['name' => faker()->streetName()]);
-
- $permission = test()->permission->name;
-
- Route::middleware([CheckPermissionOrCorporationRole::class.":$permission,Accountant"])
- ->prefix('test')
- ->get('/', function () {
- return 'test';
- })->name('test');
-});
-
-it('returns a 401 if user is not authenticated', function () {
- $response = $this->get(route('test'));
- $response->assertStatus(401);
-});
-
-it('returns a 200 if user has permission', function (string $permission) {
- test()->actingAs(test()->test_user);
- test()->assignPermissionToTestUser($permission);
-
- $response = $this->get(route('test'));
- $response->assertStatus(200);
-})->with([
- 'superuser' => 'superuser',
- 'accountant' => fn () => test()->permission->name,
-]);
-
-it('return a 200 if user has corporation_role', function (string $corporation_role) {
- test()->actingAs(test()->test_user);
- CharacterRole::query()->delete();
-
- CharacterRole::factory()->create([
- 'character_id' => test()->test_character->character_id,
- 'roles' => [$corporation_role],
- ]);
-
- $response = $this->get(route('test'));
- $response->assertStatus(200);
-})->with([
- 'Accountant' => 'Accountant',
- 'Director' => 'Director',
-]);
-
-it('return a 401 if user is missing corporation_role', function () {
- test()->actingAs(test()->test_user);
- CharacterRole::query()->delete();
-
- $response = $this->get(route('test'));
- $response->assertStatus(401);
-});
diff --git a/tests/Feature/MainCharacter/MainCharacterTest.php b/tests/Feature/Routes/MainCharacterTest.php
similarity index 71%
rename from tests/Feature/MainCharacter/MainCharacterTest.php
rename to tests/Feature/Routes/MainCharacterTest.php
index e8bb61c..d772883 100644
--- a/tests/Feature/MainCharacter/MainCharacterTest.php
+++ b/tests/Feature/Routes/MainCharacterTest.php
@@ -13,9 +13,9 @@
test()->assertNotEquals($secondary->character_id, test()->test_user->main_character_id);
- test()->actingAs(test()->test_user)->post(route('change.main_character'), [
- 'character_id' => $secondary->character_id,
- ])->assertRedirect();
+ test()->actingAs(test()->test_user)->put(route('change.main_character', [
+ 'new_character_id' => $secondary->character_id,
+ ]))->assertRedirect();
expect(test()->test_user->refresh()->main_character_id)->toEqual($secondary->character_id);
});
@@ -27,7 +27,7 @@
test()->assertNotEquals($secondary->character_id, test()->test_user->main_character_id);
- test()->actingAs(test()->test_user)->post(route('change.main_character'), [
- 'character_id' => $secondary->character_id,
- ])->assertForbidden();
+ test()->actingAs(test()->test_user)->put(route('change.main_character', [
+ 'new_character_id' => $secondary->character_id,
+ ]))->assertForbidden();
});
diff --git a/tests/Feature/Auth/SsoControllerTest.php b/tests/Feature/Routes/SsoControllerTest.php
similarity index 97%
rename from tests/Feature/Auth/SsoControllerTest.php
rename to tests/Feature/Routes/SsoControllerTest.php
index 9599518..1301d7b 100644
--- a/tests/Feature/Auth/SsoControllerTest.php
+++ b/tests/Feature/Routes/SsoControllerTest.php
@@ -28,7 +28,7 @@
use Illuminate\Support\Facades\Queue;
use Laravel\Socialite\Contracts\Provider;
use Laravel\Socialite\Facades\Socialite;
-use Seatplus\Auth\Jobs\UserRolesSync;
+use Seatplus\Auth\Jobs\RoleMemberSync;
it('works for non authed users', function () {
$abstractUser = createSocialiteUser();
@@ -102,7 +102,7 @@
$result = test()->get(route('auth.eve.callback'));
// assert no UserRolesSync job has been dispatched
- Queue::assertPushedOn('high', UserRolesSync::class);
+ Queue::assertPushedOn('high', RoleMemberSync::class);
// assert that no error is present
expect(session('error'))->toBeNull();
diff --git a/tests/Feature/Auth/StepUpTest.php b/tests/Feature/Routes/StepUpTest.php
similarity index 84%
rename from tests/Feature/Auth/StepUpTest.php
rename to tests/Feature/Routes/StepUpTest.php
index 9c4f92a..2626f20 100644
--- a/tests/Feature/Auth/StepUpTest.php
+++ b/tests/Feature/Routes/StepUpTest.php
@@ -48,8 +48,8 @@
'add_scopes' => $add_scopes,
]));
- expect(session('step_up'))->toEqual(test()->test_character->character_id);
- expect(session('sso_scopes'))->toEqual(['a', 'b', '1', '2']);
+ expect(session('step_up'))->toEqual(test()->test_character->character_id)
+ ->and(session('sso_scopes'))->toEqual(['a', 'b', '1', '2']);
});
test('one can request another scope for a deleted token', function () {
@@ -67,6 +67,14 @@
'add_scopes' => $add_scopes,
]));
- expect(session('step_up'))->toEqual(test()->test_character->character_id);
- expect(session('sso_scopes'))->toEqual(['1', '2']);
+ expect(session('step_up'))->toEqual(test()->test_character->character_id)
+ ->and(session('sso_scopes'))->toEqual(['1', '2']);
+});
+
+test('one can not request another scope for a character not associated to the user', function () {
+ $response = test()->actingAs(test()->test_user)->get(route('auth.eve.step_up', [
+ 'character_id' => 123,
+ ]));
+
+ $response->assertSessionHas('error', 'character must belong to your account');
});
diff --git a/tests/Feature/Services/RoleAffiliatedIdsServiceTest.php b/tests/Feature/Services/RoleAffiliatedIdsServiceTest.php
new file mode 100644
index 0000000..64453b5
--- /dev/null
+++ b/tests/Feature/Services/RoleAffiliatedIdsServiceTest.php
@@ -0,0 +1,201 @@
+secondary_character = CharacterInfo::factory()->create();
+
+ test()->tertiary_character = CharacterInfo::factory()->create();
+
+ test()->role = Role::create(['name' => 'derp']);
+
+ $this->service = new AutomaticRoleService($this->role);
+});
+
+dataset('entity_types', [
+ 'character',
+ 'corporation',
+ 'alliance',
+]);
+
+function getId(string $entity_type, int $character_level)
+{
+ $character = match ($character_level) {
+ 1 => test()->test_character,
+ 2 => test()->secondary_character,
+ 3 => test()->tertiary_character,
+ };
+
+ return match ($entity_type) {
+ 'character' => $character->character_id,
+ 'corporation' => $character->corporation_id,
+ 'alliance' => $character->alliance_id,
+ };
+}
+
+describe('allowed only', function () {
+ test('primary and secondary are affiliated ', function ($entity_type, $affiliation_type) {
+
+ $primaray_id = getId($entity_type, 1);
+
+ $secondary_id = getId($entity_type, 2);
+
+ $this->service->syncAffiliateManyEntities(
+ new AffiliationData($primaray_id, $entity_type, AffiliationType::from($affiliation_type)),
+ new AffiliationData($secondary_id, $entity_type, AffiliationType::from($affiliation_type)),
+ );
+
+ $affiliated_ids = (new RoleAffiliatedIdsService)->get(test()->role);
+
+ expect($affiliated_ids)->toContain($primaray_id)
+ ->toContain($secondary_id)
+ ->not()->toContain(test()->tertiary_character->character_id);
+
+ })->with('entity_types')->with([AffiliationType::ALLOWED->value]);
+});
+
+describe('inverse only', function () {
+ test('primary and secondary are affiliated, but not tertiary ', function ($entity_type, $affiliation_type) {
+
+ $primary_id = getId($entity_type, 1);
+ $secondary_id = getId($entity_type, 2);
+ $tertiary_id = getId($entity_type, 3);
+
+ $this->service->syncAffiliateManyEntities(
+ new AffiliationData($tertiary_id, $entity_type, AffiliationType::from($affiliation_type)),
+ );
+
+ $affiliated_ids = (new RoleAffiliatedIdsService)->get(test()->role);
+
+ expect($affiliated_ids)
+ ->toContain($primary_id)
+ ->toContain($secondary_id)
+ ->not()->toContain(test()->tertiary_character->character_id)
+ ->not()->toContain($tertiary_id);
+
+ })->with('entity_types')->with([AffiliationType::INVERSE->value]);
+});
+
+describe('forbidden only', function () {
+ test('primary and secondary are affiliated, but not tertiary ', function ($entity_type, $affiliation_type) {
+
+ $primary_id = getId($entity_type, 1);
+ $secondary_id = getId($entity_type, 2);
+ $tertiary_id = getId($entity_type, 3);
+
+ $this->service->syncAffiliateManyEntities(
+ new AffiliationData($tertiary_id, $entity_type, AffiliationType::from($affiliation_type)),
+ );
+
+ $affiliated_ids = (new RoleAffiliatedIdsService)->get(test()->role);
+
+ expect($affiliated_ids)
+ ->not()->toContain($primary_id)
+ ->not()->toContain($secondary_id)
+ ->not()->toContain(test()->tertiary_character->character_id)
+ ->not()->toContain($tertiary_id);
+
+ })->with('entity_types')->with([AffiliationType::FORBIDDEN->value]);
+});
+
+describe('allowed and inverse', function () {
+ test('testcharacter, secondary and tertiary are affiliated, but not tertiary ', function ($entity_type) {
+
+ $primary_id = getId($entity_type, 1);
+ $secondary_id = getId($entity_type, 2);
+ $tertiary_id = getId($entity_type, 3);
+
+ $this->service->syncAffiliateManyEntities(
+ new AffiliationData(test()->test_character->character_id, 'character', AffiliationType::ALLOWED),
+ new AffiliationData($primary_id, $entity_type, AffiliationType::INVERSE),
+ );
+
+ $affiliated_ids = (new RoleAffiliatedIdsService)->get(test()->role);
+
+ expect($affiliated_ids)
+ ->toContain(test()->test_character->character_id)
+ ->toContain($secondary_id)
+ ->toContain($tertiary_id)
+ ->toContain(test()->secondary_character->character_id)
+ ->toContain(test()->tertiary_character->character_id);
+
+ })->with('entity_types');
+});
+
+describe('allowed and forbidden', function () {
+ test('primary affiliated but test_character forbidden ', function ($entity_type) {
+
+ $primary_id = getId($entity_type, 1);
+
+ $this->service->syncAffiliateManyEntities(
+ new AffiliationData(test()->test_character->character_id, 'character', AffiliationType::FORBIDDEN),
+ new AffiliationData($primary_id, $entity_type, AffiliationType::ALLOWED),
+ );
+
+ $affiliated_ids = (new RoleAffiliatedIdsService)->get(test()->role);
+
+ expect($affiliated_ids)
+ ->not()->toContain(test()->test_character->character_id)
+ ->when($entity_type === 'character', function ($collection) {
+ $collection->toHaveCount(0);
+ })
+ ->when($entity_type !== 'character', function ($collection) use ($primary_id) {
+ $collection->toContain($primary_id);
+ });
+
+ })->with('entity_types');
+});
+
+describe('inverse and forbidden', function () {
+ test('test_character forbidden but primary affiliated through inverse', function ($entity_type) {
+
+ $primary_id = getId($entity_type, 1);
+ $secondary_id = getId($entity_type, 2);
+
+ $this->service->syncAffiliateManyEntities(
+ new AffiliationData(test()->test_character->character_id, 'character', AffiliationType::FORBIDDEN),
+ new AffiliationData($secondary_id, $entity_type, AffiliationType::INVERSE),
+ );
+
+ $affiliated_ids = (new RoleAffiliatedIdsService)->get(test()->role);
+
+ expect($affiliated_ids)
+ ->not()->toContain(test()->test_character->character_id)
+ ->when($entity_type !== 'character', function ($collection) use ($primary_id) {
+ $collection->toContain($primary_id);
+ });
+
+ })->with('entity_types');
+});
+
+describe('allowed, inverse and forbidden', function () {
+ test('test_character forbidden, primary allowed, secondary inverse', function ($entity_type) {
+
+ $primary_id = getId($entity_type, 1);
+ $secondary_id = getId($entity_type, 2);
+
+ $this->service->syncAffiliateManyEntities(
+ new AffiliationData(test()->test_character->character_id, 'character', AffiliationType::FORBIDDEN),
+ new AffiliationData($primary_id, $entity_type, AffiliationType::ALLOWED),
+ new AffiliationData($secondary_id, $entity_type, AffiliationType::INVERSE),
+ );
+
+ $affiliated_ids = (new RoleAffiliatedIdsService)->get(test()->role);
+
+ expect($affiliated_ids)
+ ->toContain(test()->tertiary_character->character_id)
+ ->not()->toContain(test()->test_character->character_id)
+ ->not()->toContain($secondary_id);
+
+ })->with('entity_types');
+
+});
diff --git a/tests/Pest.php b/tests/Pest.php
index b27e48d..3d107f5 100644
--- a/tests/Pest.php
+++ b/tests/Pest.php
@@ -1,12 +1,12 @@
in('Unit', 'Feature');
-uses(\Illuminate\Foundation\Testing\LazilyRefreshDatabase::class)->in('Unit', 'Feature');
+uses(TestCase::class)->in('Unit', 'Feature');
+uses(LazilyRefreshDatabase::class)->in('Unit', 'Feature');
/*
|--------------------------------------------------------------------------
@@ -52,7 +52,7 @@
*/
/** @link https://pestphp.com/docs/helpers */
-function createRefreshTokenWithScopes(array $scopes)
+function createRefreshTokenWithScopes(array $scopes): void
{
Event::fakeFor(function () use ($scopes) {
if (test()->test_character->refresh_token) {
@@ -87,7 +87,7 @@ function createSocialiteUser($character_id = null, array $scopes = ['esi-skills.
{
$refresh_token = RefreshToken::factory()->scopes($scopes)->make();
- $socialiteUser = test()->createMock(SocialiteUser::class);
+ $socialiteUser = Mockery::mock(SocialiteUser::class)->makePartial();
$attributes = (object) [
'character_id' => $character_id ?? $refresh_token->character_id,
@@ -97,7 +97,7 @@ function createSocialiteUser($character_id = null, array $scopes = ['esi-skills.
$socialiteUser->attributes = $attributes;
$socialiteUser->token = $refresh_token->token;
$socialiteUser->refreshToken = $refresh_token->refresh_token;
- $socialiteUser->expiresIn = 12 * 60; //let's just say 12 minutes
+ $socialiteUser->expiresIn = 12 * 60; // let's just say 12 minutes
$socialiteUser->user = [
'scp' => $scopes,
];
@@ -107,11 +107,7 @@ function createSocialiteUser($character_id = null, array $scopes = ['esi-skills.
function faker()
{
- if (! isset(test()->faker)) {
- test()->faker = Factory::create();
- }
-
- return test()->faker;
+ return Factory::create();
}
function createEveUser(?int $character_id = null, ?string $character_owner_hash = null): EveUser
@@ -139,14 +135,5 @@ function assignPermissionToTestUser(array|string $permission_strings)
}
// now re-register all the roles and permissions
- app()->make(PermissionRegistrar::class)->registerPermissions();
-}
-
-function createAffiliation(Role $role, $affiliatable_id, $affiliatable_type, $type = 'allowed'): Affiliation
-{
- return $role->affiliations()->create([ //test()->role
- 'affiliatable_id' => $affiliatable_id,
- 'affiliatable_type' => $affiliatable_type,
- 'type' => $type,
- ]);
+ app()[PermissionRegistrar::class]->forgetCachedPermissions();
}
diff --git a/tests/TestCase.php b/tests/TestCase.php
index a05367c..fe18cd0 100644
--- a/tests/TestCase.php
+++ b/tests/TestCase.php
@@ -27,6 +27,8 @@
namespace Seatplus\Auth\Tests;
use Illuminate\Database\Eloquent\Factories\Factory;
+use Illuminate\Database\Eloquent\Model;
+use Illuminate\Foundation\Application;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Queue;
use Illuminate\Support\Str;
@@ -34,6 +36,7 @@
use Seatplus\Auth\AuthenticationServiceProvider;
use Seatplus\Auth\Models\User;
use Seatplus\Eveapi\EveapiServiceProvider;
+use Spatie\Permission\PermissionServiceProvider;
abstract class TestCase extends OrchestraTestCase
{
@@ -45,6 +48,8 @@ protected function setUp(): void
{
parent::setUp();
+ Model::shouldBeStrict();
+
Factory::guessFactoryNamesUsing(
fn (string $modelName) => match (true) {
Str::startsWith($modelName, 'Seatplus\Auth') => 'Seatplus\\Auth\\Database\\Factories\\'.class_basename($modelName).'Factory',
@@ -68,7 +73,7 @@ protected function setUp(): void
/**
* Get application providers.
*
- * @param \Illuminate\Foundation\Application $app
+ * @param Application $app
* @return array
*/
protected function getPackageProviders($app)
@@ -76,35 +81,34 @@ protected function getPackageProviders($app)
return [
EveapiServiceProvider::class,
AuthenticationServiceProvider::class,
+ PermissionServiceProvider::class,
];
}
/**
- * @param \Illuminate\Foundation\Application $app
+ * @param Application $app
*/
private function setupDatabase($app)
{
// Path to our migrations to load
- //$this->loadMigrationsFrom(__DIR__ . '/database/migrations');
+ // $this->loadMigrationsFrom(__DIR__ . '/database/migrations');
$this->artisan('migrate');
}
/**
* Define environment setup.
*
- * @param \Illuminate\Foundation\Application $app
+ * @param Application $app
* @return void
*/
protected function getEnvironmentSetUp($app)
{
- config(['database.default' => 'mysql']);
-
config(['app.debug' => true]);
config(['activitylog.table_name' => 'activity_log']);
// Use test User model for users provider
$app['config']->set('auth.providers.users.model', User::class);
- //$app['config']->set('cache.prefix', 'seatplus_tests---');
+ // $app['config']->set('cache.prefix', 'seatplus_tests---');
}
}
diff --git a/tests/Unit/Actions/AddMemberActionTest.php b/tests/Unit/Actions/AddMemberActionTest.php
new file mode 100644
index 0000000..71e5e6f
--- /dev/null
+++ b/tests/Unit/Actions/AddMemberActionTest.php
@@ -0,0 +1,14 @@
+mock(SetMemberAction::class, function ($mock) {
+ $mock->shouldReceive('execute')->with(1, 2, true)->once();
+ });
+
+ $action = app(AddMemberAction::class);
+
+ $action->execute(1, 2);
+});
diff --git a/tests/Unit/Actions/AddModeratorRoleActionTest.php b/tests/Unit/Actions/AddModeratorRoleActionTest.php
new file mode 100644
index 0000000..c1585f9
--- /dev/null
+++ b/tests/Unit/Actions/AddModeratorRoleActionTest.php
@@ -0,0 +1,16 @@
+mock(SetModeratorAction::class, function ($mock) {
+ $mock->shouldReceive('execute')->with(1, 2, true)->once();
+ });
+
+ $action = app(AddModeratorRoleAction::class);
+
+ $action->execute(1, 2);
+});
diff --git a/tests/Unit/Actions/ApplyActionTest.php b/tests/Unit/Actions/ApplyActionTest.php
new file mode 100644
index 0000000..7df7322
--- /dev/null
+++ b/tests/Unit/Actions/ApplyActionTest.php
@@ -0,0 +1,47 @@
+mock(BaseRoleService::class, function (MockInterface $mock) {
+ $mock->shouldReceive('for')->with(1)
+ ->andReturnSelf();
+
+ $mock->shouldReceive('onRequest')
+ ->once()
+ ->andReturn(mock(OnRequestRoleService::class, function (MockInterface $mock) {
+ $mock->shouldReceive('onRequest')->andReturnSelf();
+ $mock->shouldReceive('submitApplicationForRole')->once();
+ }));
+ });
+
+ $user = $this->test_user;
+
+ $action = app(ApplyAction::class);
+
+ $action->execute(1, $user->id);
+
+ // expect(true)->toBeTrue(); // Just to ensure the test runs without exceptions
+});
+
+it('throws exception if user not found', function () {
+ $this->mock(BaseRoleService::class, function (MockInterface $mock) {
+ $mock->shouldReceive('for')->with(1)
+ ->andReturnSelf();
+
+ $mock->shouldReceive('onRequest')
+ ->once()
+ ->andReturn(mock(OnRequestRoleService::class, function (MockInterface $mock) {
+ $mock->shouldReceive('onRequest')->andReturnSelf();
+ $mock->shouldReceive('submitApplicationForRole')->never();
+ }));
+ });
+
+ $action = app(ApplyAction::class);
+
+ expect(fn () => $action->execute(1, 999))->toThrow(ModelNotFoundException::class);
+});
diff --git a/tests/Unit/Actions/ApproveActionTest.php b/tests/Unit/Actions/ApproveActionTest.php
new file mode 100644
index 0000000..39d81d0
--- /dev/null
+++ b/tests/Unit/Actions/ApproveActionTest.php
@@ -0,0 +1,36 @@
+mock(BaseRoleService::class, function (MockInterface $mock) {
+ $mock->shouldReceive('for')->with(1)->andReturnSelf();
+ $mock->shouldReceive('onRequest')->andReturn(mock(OnRequestRoleService::class, function (MockInterface $mock) {
+ $mock->shouldReceive('approveApplicationForRole')->once();
+ }));
+ });
+
+ $user = User::factory()->create();
+
+ $action = app(ApproveAction::class);
+ $action->execute(1, $user->id);
+});
+
+it('throws exception if user not found during approval', function () {
+ $roleServiceMock = $this->mock(BaseRoleService::class, function ($mock) {
+ $mock->shouldReceive('for')->with(1)->andReturnSelf();
+ $mock->shouldReceive('onRequest')->andReturn(mock(OnRequestRoleService::class, function ($mock) {
+ $mock->shouldReceive('approveApplicationForRole')->never();
+ }));
+ });
+
+ $action = app(ApproveAction::class, ['baseRoleService' => $roleServiceMock]);
+
+ expect(fn () => $action->execute(1, 999))->toThrow(ModelNotFoundException::class);
+});
diff --git a/tests/Unit/Actions/DenyActionTest.php b/tests/Unit/Actions/DenyActionTest.php
new file mode 100644
index 0000000..52a8cf0
--- /dev/null
+++ b/tests/Unit/Actions/DenyActionTest.php
@@ -0,0 +1,34 @@
+mock(BaseRoleService::class, function ($mock) {
+ $mock->shouldReceive('for')->with(1)->andReturnSelf();
+ $mock->shouldReceive('onRequest')->andReturn(mock(OnRequestRoleService::class, function ($mock) {
+ $mock->shouldReceive('denyApplication')->once();
+ }));
+ });
+
+ $user = User::factory()->create();
+
+ $action = app(DenyAction::class);
+ $action->execute(1, $user->id);
+});
+
+it('throws exception if user not found during application', function () {
+ $this->mock(BaseRoleService::class, function ($mock) {
+ $mock->shouldReceive('for')->with(1)->andReturnSelf();
+ $mock->shouldReceive('onRequest')->andReturn(mock(OnRequestRoleService::class, function ($mock) {
+ $mock->shouldReceive('submitApplicationForRole')->never();
+ }));
+ });
+
+ $action = app(DenyAction::class);
+
+ expect(fn () => $action->execute(1, 999))->toThrow(ModelNotFoundException::class);
+});
diff --git a/tests/Unit/Actions/GetAffiliatedIdsByPermissionArrayTest.php b/tests/Unit/Actions/GetAffiliatedIdsByPermissionArrayTest.php
deleted file mode 100644
index e8142cc..0000000
--- a/tests/Unit/Actions/GetAffiliatedIdsByPermissionArrayTest.php
+++ /dev/null
@@ -1,379 +0,0 @@
-role = Role::create(['name' => 'writer']);
- test()->permission = Permission::create(['name' => 'edit articles']);
-
- test()->role->givePermissionTo(test()->permission);
- test()->test_user->assignRole(test()->role);
-
- test()->test_character_user = test()->test_user->character_users->first();
-
- test()->actingAs(test()->test_user);
-
- Event::fakeFor(function () {
- test()->secondary_character = CharacterInfo::factory()->create();
-
- test()->tertiary_character = CharacterInfo::factory()->create();
- });
-});
-
-/**
- * @throws \Exception
- */
-it('returns own character id', function () {
- test()->role->affiliations()->create([
- 'affiliatable_id' => test()->test_character->character_id,
- 'affiliatable_type' => CharacterInfo::class,
- 'type' => 'allowed',
- ]);
-
- $ids = (new GetAffiliatedIdsByPermissionArray(test()->permission->name))->execute();
-
- expect(in_array(test()->test_character_user->character_id, $ids))->toBeTrue();
-});
-
-it('returns other and own character id for inverted', function () {
- test()->role->affiliations()->create([
- 'affiliatable_id' => test()->test_character->character_id,
- 'affiliatable_type' => CharacterInfo::class,
- 'type' => 'inverse',
- ]);
-
- $ids = (new GetAffiliatedIdsByPermissionArray(test()->permission->name))->execute();
-
- expect(in_array(test()->test_character_user->character_id, $ids))->toBeTrue();
- expect(in_array(test()->secondary_character->character_id, $ids))->toBeTrue();
-});
-
-it('does not return secondary character id if secondary character is inverted', function () {
- test()->role->affiliations()->create([
- 'affiliatable_id' => test()->secondary_character->character_id,
- 'affiliatable_type' => CharacterInfo::class,
- 'type' => 'inverse',
- ]);
-
- $ids = (new GetAffiliatedIdsByPermissionArray(test()->permission->name))->execute();
-
- // Assert that the owned character_ids are present
- expect(in_array(test()->test_character_user->character_id, $ids))->toBeTrue();
-
- // Assert that ids from the inverted corporation is missing
- expect(in_array(test()->secondary_character->character_id, $ids))->toBeFalse();
-
- // Assert that ids from any other third party is present
- expect(in_array(test()->tertiary_character->character_id, $ids))->toBeTrue();
-});
-
-it('does not return secondary character id if secondary corporation is inverted', function () {
- test()->role->affiliations()->create([
- 'affiliatable_id' => test()->secondary_character->corporation->corporation_id,
- 'affiliatable_type' => CorporationInfo::class,
- 'type' => 'inverse',
- ]);
-
- $ids = (new GetAffiliatedIdsByPermissionArray(test()->permission->name))->execute();
-
- // Assert that second character does not share same corporation as the first character
- test()->assertNotEquals(test()->secondary_character->corporation->corporation_id, test()->test_character->corporation->corporation_id);
-
- // Assert that the owned character_ids are present
- expect(in_array(test()->test_character_user->character_id, $ids))->toBeTrue();
-
- // Assert that ids from the inverted corporation is missing
- expect(in_array(test()->secondary_character->character_id, $ids))->toBeFalse();
-
- // Assert that ids from any other third party is present
- expect(in_array(test()->tertiary_character->character_id, $ids))->toBeTrue();
-});
-
-it('does not return secondary character id if secondary alliance is inverted', function () {
- test()->role->affiliations()->create([
- 'affiliatable_id' => test()->secondary_character->alliance->alliance_id,
- 'affiliatable_type' => AllianceInfo::class,
- 'type' => 'inverse',
- ]);
-
- $ids = (new GetAffiliatedIdsByPermissionArray(test()->permission->name))->execute();
-
- // Assert that second character does not share same corporation as the first character
- test()->assertNotEquals(test()->secondary_character->alliance->alliance_id, test()->test_character->alliance->alliance_id);
-
- // Assert that the owned character_ids are present
- expect(in_array(test()->test_character_user->character_id, $ids))->toBeTrue();
-
- // Assert that ids from the inverted corporation is missing
- expect(in_array(test()->secondary_character->character_id, $ids))->toBeFalse();
-
- // Assert that ids from any other third party is present
- expect(in_array(test()->tertiary_character->character_id, $ids))->toBeTrue();
-});
-
-it('does return secondary character id if secondary character is allowed', function () {
- test()->role->affiliations()->create([
- 'affiliatable_id' => test()->secondary_character->character_id,
- 'affiliatable_type' => CharacterInfo::class,
- 'type' => 'allowed',
- ]);
-
- $ids = (new GetAffiliatedIdsByPermissionArray(test()->permission->name))->execute();
-
- // Assert that second character does not share same corporation as the first character
- test()->assertNotEquals(test()->secondary_character->character_id, test()->test_character->character_id);
-
- // Assert that the owned character_ids are present
- expect(in_array(test()->test_character_user->character_id, $ids))->toBeTrue();
-
- // Assert that ids from the allowed character is present
- expect(in_array(test()->secondary_character->character_id, $ids))->toBeTrue();
-
- // Assert that ids from any other third party is not present
- expect(in_array(test()->tertiary_character->character_id, $ids))->toBeFalse();
-});
-
-it('does return secondary character id if secondary corporation is allowed', function () {
- test()->role->affiliations()->create([
- 'affiliatable_id' => test()->secondary_character->corporation->corporation_id,
- 'affiliatable_type' => CorporationInfo::class,
- 'type' => 'allowed',
- ]);
-
- $ids = (new GetAffiliatedIdsByPermissionArray(test()->permission->name))->execute();
-
- // Assert that second character does not share same corporation as the first character
- test()->assertNotEquals(test()->secondary_character->corporation->corporation_id, test()->test_character->corporation->corporation_id);
-
- // Assert that the owned character_ids are present
- expect(in_array(test()->test_character_user->character_id, $ids))->toBeTrue();
-
- // Assert that ids from the allowed character is present
- expect(in_array(test()->secondary_character->character_id, $ids))->toBeTrue();
-
- // Assert that ids from any other third party is not present
- expect(in_array(test()->tertiary_character->character_id, $ids))->toBeFalse();
-});
-
-it('does return secondary character id if secondary alliance is allowed', function () {
- test()->role->affiliations()->create([
- 'affiliatable_id' => test()->secondary_character->alliance->alliance_id,
- 'affiliatable_type' => AllianceInfo::class,
- 'type' => 'allowed',
- ]);
-
- $ids = (new GetAffiliatedIdsByPermissionArray(test()->permission->name))->execute();
-
- // Assert that second character does not share same corporation as the first character
- test()->assertNotEquals(test()->secondary_character->alliance->alliance_id, test()->test_character->alliance->alliance_id);
-
- // Assert that the owned character_ids are present
- expect(in_array(test()->test_character_user->character_id, $ids))->toBeTrue();
-
- // Assert that ids from the allowed character is present
- expect(in_array(test()->secondary_character->character_id, $ids))->toBeTrue();
-
- // Assert that ids from any other third party is not present
- expect(in_array(test()->tertiary_character->character_id, $ids))->toBeFalse();
-});
-
-it('does return own character even if listed as forbidden', function () {
- test()->role->affiliations()->create([
- 'affiliatable_id' => test()->secondary_character->character_id,
- 'affiliatable_type' => CharacterInfo::class,
- 'type' => 'forbidden',
- ]);
-
- $ids = (new GetAffiliatedIdsByPermissionArray(test()->permission->name))->execute();
-
- // Assert that the owned character_ids are present
- expect(in_array(test()->test_character_user->character_id, $ids))->toBeTrue();
-});
-
-it('does not return secondary character id if secondary character is forbidden', function () {
- test()->role->affiliations()->createMany([
- [
- 'affiliatable_id' => test()->secondary_character->character_id,
- 'affiliatable_type' => CharacterInfo::class,
- 'type' => 'allowed',
- ],
- [
- 'affiliatable_id' => test()->secondary_character->character_id,
- 'affiliatable_type' => CharacterInfo::class,
- 'type' => 'forbidden',
- ],
- ]);
-
- $ids = (new GetAffiliatedIdsByPermissionArray(test()->permission->name))->execute();
-
- // Assert that second character does not share same corporation as the first character
- test()->assertNotEquals(test()->secondary_character->character_id, test()->test_character->character_id);
-
- // Assert that the owned character_ids are present
- expect(in_array(test()->test_character_user->character_id, $ids))->toBeTrue();
-
- // Assert that ids from the allowed character is not present
- expect(in_array(test()->secondary_character->character_id, $ids))->toBeFalse();
-
- // Assert that ids from any other third party is not present
- expect(in_array(test()->tertiary_character->character_id, $ids))->toBeFalse();
-});
-
-it('does not return secondary character id if secondary corporation is forbidden', function () {
- test()->role->affiliations()->createMany([
- [
- 'affiliatable_id' => test()->secondary_character->corporation->corporation_id,
- 'affiliatable_type' => CorporationInfo::class,
- 'type' => 'allowed',
- ],
- [
- 'affiliatable_id' => test()->secondary_character->corporation->corporation_id,
- 'affiliatable_type' => CorporationInfo::class,
- 'type' => 'forbidden',
- ],
- ]);
-
- $ids = (new GetAffiliatedIdsByPermissionArray(test()->permission->name))->execute();
-
- // Assert that second character does not share same corporation as the first character
- test()->assertNotEquals(test()->secondary_character->corporation->corporation_id, test()->test_character->corporation->corporation_id);
-
- // Assert that the owned character_ids are present
- expect(in_array(test()->test_character_user->character_id, $ids))->toBeTrue();
-
- // Assert that ids from the allowed character is not present
- expect(in_array(test()->secondary_character->character_id, $ids))->toBeFalse();
-
- // Assert that ids from any other third party is not present
- expect(in_array(test()->tertiary_character->character_id, $ids))->toBeFalse();
-});
-
-it('does not return secondary character id if secondary alliance is forbidden', function () {
- test()->role->affiliations()->createMany([
- [
- 'affiliatable_id' => test()->secondary_character->alliance->alliance_id,
- 'affiliatable_type' => AllianceInfo::class,
- 'type' => 'allowed',
- ],
- [
- 'affiliatable_id' => test()->secondary_character->alliance->alliance_id,
- 'affiliatable_type' => AllianceInfo::class,
- 'type' => 'forbidden',
- ],
- ]);
-
- $ids = (new GetAffiliatedIdsByPermissionArray(test()->permission->name))->execute();
-
- // Assert that second character does not share same corporation as the first character
- test()->assertNotEquals(test()->secondary_character->alliance->alliance_id, test()->test_character->alliance->alliance_id);
-
- // Assert that the owned character_ids are present
- expect(in_array(test()->test_character_user->character_id, $ids))->toBeTrue();
-
- // Assert that ids from the allowed character is not present
- expect(in_array(test()->secondary_character->character_id, $ids))->toBeFalse();
-
- // Assert that ids from any other third party is not present
- expect(in_array(test()->tertiary_character->character_id, $ids))->toBeFalse();
-});
-
-it('caches results', function () {
- test()->role->affiliations()->createMany([
- [
- 'affiliatable_id' => test()->secondary_character->alliance->alliance_id,
- 'affiliatable_type' => AllianceInfo::class,
- 'type' => 'allowed',
- ],
- [
- 'affiliatable_id' => test()->secondary_character->alliance->alliance_id,
- 'affiliatable_type' => AllianceInfo::class,
- 'type' => 'forbidden',
- ],
- ]);
-
- $action = new GetAffiliatedIdsByPermissionArray(test()->permission->name);
-
- expect(cache()->has($action->getCacheKey()))->toBeFalse();
-
- $ids = $action->execute();
-
- expect(cache()->has($action->getCacheKey()))->toBeTrue();
- expect(cache($action->getCacheKey()))->toEqual($ids);
-});
-
-it('returns corporation id', function () {
- // first make sure test_character corporation is in the alliance
- $corporation = test()->test_character->corporation;
- $corporation->alliance_id = test()->test_character->alliance->alliance_id;
- $corporation->save();
-
- // create role affiliation on alliance level
- test()->role->affiliations()->create([
- 'affiliatable_id' => test()->test_character->alliance->alliance_id,
- 'affiliatable_type' => AllianceInfo::class,
- 'type' => 'allowed',
- ]);
-
- // Create director role for corporation
- $character_role = CharacterRole::factory()->make([
- 'character_id' => test()->test_character->character_id,
- 'roles' => ['Contract_Manager', 'Director'],
- ]);
-
- $ids = (new GetAffiliatedIdsByPermissionArray(test()->permission->name, 'Director'))->execute();
-
- // Assert that the owned character_ids are present
- expect(in_array(test()->test_character->character_id, $ids))->toBeTrue();
-
- // Assert that the corporation_id of test_character with director role is present
- expect(in_array(test()->test_character->corporation->corporation_id, $ids))->toBeTrue();
-});
-
-it('returns all character and corporation ids for superuser', function () {
- // give test user superuser
- Permission::create(['name' => 'superuser']);
- test()->test_user->givePermissionTo('superuser');
-
- // collect all corporation_ids
- $corporation_ids = CorporationInfo::all()->pluck('corporation_id')->values();
-
- // collect all character_ids
- $character_ids = CharacterInfo::all()->pluck('character_id')->values();
-
- // get ids
- $ids = (new GetAffiliatedIdsByPermissionArray(test()->permission->name))->execute();
-
- // check if ids are present
- expect(collect([...$character_ids, ...$corporation_ids])->diff($ids)->isEmpty())->toBeTrue();
-});
diff --git a/tests/Unit/Actions/JoinActionTest.php b/tests/Unit/Actions/JoinActionTest.php
new file mode 100644
index 0000000..c94d202
--- /dev/null
+++ b/tests/Unit/Actions/JoinActionTest.php
@@ -0,0 +1,48 @@
+mock(BaseRoleService::class, function ($mock) {
+ $mock->shouldReceive('for')->with(1)->andReturnSelf();
+ $mock->shouldReceive('optIn')->andReturn(mock(OptInRoleService::class, function ($mock) {
+ $mock->shouldReceive('joinRole')->once();
+ }));
+ });
+
+ $user = User::factory()->create();
+
+ $action = app(JoinAction::class);
+ $action->execute(1, $user->id);
+
+ expect(true)->toBeTrue(); // Just to ensure the test runs without exceptions
+});
+
+it('throws exception if user not found during join', function () {
+ $this->mock(BaseRoleService::class, function ($mock) {
+ $mock->shouldReceive('for')->with(1)->andReturnSelf();
+ $mock->shouldReceive('optIn')->andReturn(mock(OptInRoleService::class, function ($mock) {
+ $mock->shouldReceive('joinRole')->never();
+ }));
+ });
+
+ $action = app(JoinAction::class);
+
+ expect(fn () => $action->execute(1, 999))->toThrow(ModelNotFoundException::class);
+});
+
+it('throws exception if role service not found during join', function () {
+ $this->mock(BaseRoleService::class, function ($mock) {
+ $mock->shouldReceive('for')->with(1)->andThrow(new Exception('Role service not found'));
+ });
+
+ $user = User::factory()->create();
+
+ $action = app(JoinAction::class);
+
+ expect(fn () => $action->execute(1, $user->id))->toThrow(Exception::class);
+});
diff --git a/tests/Unit/Actions/LeaveActionTest.php b/tests/Unit/Actions/LeaveActionTest.php
new file mode 100644
index 0000000..a9d84b6
--- /dev/null
+++ b/tests/Unit/Actions/LeaveActionTest.php
@@ -0,0 +1,36 @@
+mock(BaseRoleService::class, function ($mock) {
+ $mock->shouldReceive('for')->with(1)->andReturnSelf();
+ $mock->shouldReceive('optIn')->andReturn(mock(OptInRoleService::class, function ($mock) {
+ $mock->shouldReceive('leaveRole')->once();
+ }));
+ });
+
+ $user = User::factory()->create();
+
+ $action = app(LeaveAction::class);
+ $action->execute(1, $user->id);
+
+ expect(true)->toBeTrue(); // Just to ensure the test runs without exceptions
+});
+
+it('throws exception if user not found during leave', function () {
+ $this->mock(BaseRoleService::class, function ($mock) {
+ $mock->shouldReceive('for')->with(1)->andReturnSelf();
+ $mock->shouldReceive('optIn')->andReturn(mock(OptInRoleService::class, function ($mock) {
+ $mock->shouldReceive('leaveRole')->never();
+ }));
+ });
+
+ $action = app(LeaveAction::class);
+
+ expect(fn () => $action->execute(1, 999))->toThrow(ModelNotFoundException::class);
+});
diff --git a/tests/Unit/Actions/LoginAssetActionTest.php b/tests/Unit/Actions/LoginAssetActionTest.php
new file mode 100644
index 0000000..89c8043
--- /dev/null
+++ b/tests/Unit/Actions/LoginAssetActionTest.php
@@ -0,0 +1,38 @@
+toBe([
+ 'login_welcome' => trans('auth::auth.login_welcome'),
+ 'evesso_img_src' => asset('img/evesso.png'),
+ ]);
+});
+
+it('adds a warning if SSO is not configured', function () {
+ Config::set('services.eveonline.client_id', '1234');
+ Config::set('services.eveonline.client_secret', '1234');
+
+ $action = new LoginAssetsAction;
+ $action();
+
+ expect(Session::get('warning'))->toBe(trans('auth::auth.sso_config_warning'));
+});
+
+it('does not add a warning if SSO is configured correctly', function () {
+ Config::set('services.eveonline.client_id', 'valid_client_id');
+ Config::set('services.eveonline.client_secret', 'valid_client_secret');
+
+ $action = new LoginAssetsAction;
+ $action();
+
+ expect(Session::get('warning'))->toBeNull();
+});
diff --git a/tests/Unit/Actions/LogoutActionTest.php b/tests/Unit/Actions/LogoutActionTest.php
new file mode 100644
index 0000000..a10dad5
--- /dev/null
+++ b/tests/Unit/Actions/LogoutActionTest.php
@@ -0,0 +1,17 @@
+test_user->id;
+ $test_user = User::find($test_user_id)->makeVisible(['remember_token']);
+
+ $this->actingAs($test_user);
+
+ $action = new LogoutAction;
+ $response = $action();
+
+ expect($response->getStatusCode())->toBe(302);
+});
diff --git a/tests/Unit/Actions/ManageAutomaticRoleActionTest.php b/tests/Unit/Actions/ManageAutomaticRoleActionTest.php
new file mode 100644
index 0000000..668fe15
--- /dev/null
+++ b/tests/Unit/Actions/ManageAutomaticRoleActionTest.php
@@ -0,0 +1,120 @@
+actingAs(test()->test_user);
+
+ $action = app(ManageAutomaticRoleAction::class);
+ $action->execute($request);
+})->throws(Exception::class, 'You are not allowed to administrate access control groups');
+
+it('invokes role service with valid role id', function () {
+ $role = Role::create(['name' => 'test']);
+
+ $request = mock(RoleRequest::class, function (MockInterface $mock) use ($role) {
+ $mock->shouldReceive('validated')->once()->andReturn(['role_id' => $role->refresh()->id, 'affiliated' => [], 'assigned' => []]);
+ });
+
+ $admin_permission = 'administrate access control groups';
+
+ // give the user the permission to administrate access control groups
+ assignPermissionToTestUser($admin_permission);
+
+ $this->actingAs(test()->test_user);
+
+ expect(test()->test_user->hasPermissionTo($admin_permission))->toBeTrue() // ok
+ ->and(auth()->user()->hasPermissionTo($admin_permission))->toBeTrue() // ok
+ ->and(auth()->user()->can($admin_permission))->toBeTrue(); // fails
+
+ $action = app(ManageAutomaticRoleAction::class);
+ $action->execute($request);
+});
+
+it('invokes role service with affiliated entities', function () {
+ $request = mock(RoleRequest::class, function (MockInterface $mock) {
+ $mock->shouldReceive('validated')->andReturn(['role_id' => 1, 'affiliated' => [['entity_id' => 1, 'entity_type' => 'corporation', 'affiliation_type' => 'allowed']], 'assigned' => []]);
+ });
+
+ $this->mock(BaseRoleService::class, function (MockInterface $mock) {
+ $mock->shouldReceive('for')->with(1)->andReturn($mock);
+ $mock->shouldReceive('automatic')->andReturn(mock(AutomaticRoleService::class, function (MockInterface $mock) {
+ $mock->shouldReceive('setRoleType')->once()->with(RoleType::AUTOMATIC);
+ $mock->shouldReceive('syncAffiliateManyEntities')->once()->withArgs(function (AffiliationData $entity) {
+ return $entity->entity_id === 1 && $entity->entity_type === 'corporation' && $entity->affiliation_type === AffiliationType::ALLOWED;
+ });
+ // assigned: [] → empty array → automaticallyAssignRoleTo called with 0 args (clears criteria)
+ $mock->shouldReceive('automaticallyAssignRoleTo')->once()->withNoArgs();
+ $mock->shouldReceive('handleMembers')->once();
+ }));
+ });
+
+ $this->actingAs(test()->test_user);
+ // give the user the permission to administrate access control groups
+ assignPermissionToTestUser('administrate access control groups');
+
+ $action = app(ManageAutomaticRoleAction::class);
+ $action->execute($request);
+});
+
+it('invokes role service with assigned entities', function () {
+ $request = mock(RoleRequest::class, function (MockInterface $mock) {
+ $mock->shouldReceive('validated')->andReturn(['role_id' => 1, 'affiliated' => [], 'assigned' => [['entity_id' => 1, 'entity_type' => 'corporation']]]);
+ });
+
+ $this->mock(BaseRoleService::class, function (MockInterface $mock) {
+ $mock->shouldReceive('for')->once()->with(1)->andReturn($mock);
+ $mock->shouldReceive('automatic')->andReturn(mock(AutomaticRoleService::class, function (MockInterface $mock) {
+ $mock->shouldReceive('setRoleType')->once()->with(RoleType::AUTOMATIC);
+ // affiliated: [] → empty array → syncAffiliateManyEntities called with 0 args (clears scope)
+ $mock->shouldReceive('syncAffiliateManyEntities')->once()->withNoArgs();
+ $mock->shouldReceive('automaticallyAssignRoleTo')->once()->withArgs(function (CriteriaData $entity) {
+ return $entity->entity_id === 1 && $entity->entity_type === 'corporation';
+ });
+ $mock->shouldReceive('handleMembers')->once();
+ }));
+ });
+
+ $this->actingAs(test()->test_user);
+ // give the user the permission to administrate access control groups
+ assignPermissionToTestUser('administrate access control groups');
+
+ $action = app(ManageAutomaticRoleAction::class);
+ $action->execute($request);
+});
+
+it('updates name of role', function () {
+ $request = mock(RoleRequest::class, function (MockInterface $mock) {
+ $mock->shouldReceive('validated')->andReturn(['role_id' => 1, 'name' => 'new name', 'affiliated' => [], 'assigned' => []]);
+ });
+
+ $this->mock(BaseRoleService::class, function (MockInterface $mock) {
+ $mock->shouldReceive('for')->once()->with(1)->andReturn($mock);
+ $mock->shouldReceive('automatic')->andReturn(mock(AutomaticRoleService::class, function (MockInterface $mock) {
+ $mock->shouldReceive('setRoleType')->once()->with(RoleType::AUTOMATIC);
+ $mock->shouldReceive('updateRoleName')->once()->with('new name');
+ // affiliated: [] and assigned: [] → both called with 0 args
+ $mock->shouldReceive('syncAffiliateManyEntities')->once()->withNoArgs();
+ $mock->shouldReceive('automaticallyAssignRoleTo')->once()->withNoArgs();
+ $mock->shouldReceive('handleMembers')->once();
+ }));
+ });
+
+ $this->actingAs(test()->test_user);
+ // give the user the permission to administrate access control groups
+ assignPermissionToTestUser('administrate access control groups');
+
+ $action = app(ManageAutomaticRoleAction::class);
+ $action->execute($request);
+});
diff --git a/tests/Unit/Actions/ManageManualRoleActionTest.php b/tests/Unit/Actions/ManageManualRoleActionTest.php
new file mode 100644
index 0000000..a4db0bf
--- /dev/null
+++ b/tests/Unit/Actions/ManageManualRoleActionTest.php
@@ -0,0 +1,96 @@
+ 'test_role']);
+
+ $role_request = mock(RoleRequest::class, function (MockInterface $mock) use ($role) {
+ $mock->shouldReceive('validated')
+ ->andReturn(['role_id' => $role->id]);
+ });
+
+ $this->mock(BaseRoleService::class, function (MockInterface $mock) use ($role) {
+ $mock->shouldReceive('for')
+ ->with($role->id)
+ ->andReturn($mock);
+
+ $mock->shouldReceive('manual')
+ ->andReturn(mock(ManualRoleService::class, function (MockInterface $mock) {
+ $mock->shouldReceive('setRoleType')->once();
+ $mock->shouldReceive('handleMembers')->once();
+ }));
+ });
+
+ $action = app(ManageManualRoleAction::class);
+
+ $action->execute($role_request);
+
+ expect($role->refresh()->type)->toBe(RoleType::MANUAL);
+});
+
+it('updates the role name', function () {
+ $role = Role::create(['name' => 'test_role']);
+
+ $role_request = mock(RoleRequest::class, function (MockInterface $mock) use ($role) {
+ $mock->shouldReceive('validated')
+ ->andReturn(['role_id' => $role->id, 'name' => 'new_name']);
+ });
+
+ $this->mock(BaseRoleService::class, function (MockInterface $mock) use ($role) {
+ $mock->shouldReceive('for')
+ ->with($role->id)
+ ->andReturn($mock);
+
+ $mock->shouldReceive('manual')
+ ->andReturn(mock(ManualRoleService::class, function (MockInterface $mock) {
+ $mock->shouldReceive('setRoleType')->once();
+ $mock->shouldReceive('updateRoleName')->once()->with('new_name');
+ $mock->shouldReceive('handleMembers')->once();
+ }));
+ });
+
+ $action = app(ManageManualRoleAction::class);
+
+ $action->execute($role_request);
+});
+
+it('affiliates many entities', function () {
+ $role = Role::create(['name' => 'test_role']);
+
+ $role_request = mock(RoleRequest::class, function (MockInterface $mock) use ($role) {
+ $mock->shouldReceive('validated')
+ ->andReturn(['role_id' => $role->id, 'affiliated' => [['entity_id' => 1, 'entity_type' => 'corporation', 'affiliation_type' => 'allowed']]]);
+ });
+
+ $this->mock(BaseRoleService::class, function (MockInterface $mock) use ($role) {
+ $mock->shouldReceive('for')
+ ->with($role->id)
+ ->andReturn($mock);
+
+ $mock->shouldReceive('manual')
+ ->andReturn(mock(ManualRoleService::class, function (MockInterface $mock) {
+ $mock->shouldReceive('setRoleType')->once();
+ $mock->shouldReceive('syncAffiliateManyEntities')->once()->withArgs(function (AffiliationData $affiliationData) {
+ return $affiliationData->entity_id === 1
+ && $affiliationData->entity_type === 'corporation'
+ && $affiliationData->affiliation_type === AffiliationType::ALLOWED;
+ });
+ $mock->shouldReceive('handleMembers')->once();
+ }));
+ });
+
+ $action = app(ManageManualRoleAction::class);
+
+ $action->execute($role_request);
+});
diff --git a/tests/Unit/Actions/ManageOnRequestRoleActionTest.php b/tests/Unit/Actions/ManageOnRequestRoleActionTest.php
new file mode 100644
index 0000000..889fdac
--- /dev/null
+++ b/tests/Unit/Actions/ManageOnRequestRoleActionTest.php
@@ -0,0 +1,76 @@
+ 'Role Name']);
+
+ $request = mock(RoleRequest::class, function (MockInterface $mock) use ($role) {
+
+ $mock->shouldReceive('validated')->once()->andReturn([
+ 'role_id' => $role->refresh()->id,
+ 'affiliated' => [['entity_id' => 1, 'entity_type' => 'corporation', 'affiliation_type' => 'allowed']],
+ 'assigned' => [['entity_id' => 5, 'entity_type' => 'character']],
+ 'name' => 'New Role Name',
+ ]);
+ });
+
+ $this->mock(BaseRoleService::class, function ($mock) use ($role) {
+ $mock->shouldReceive('for')
+ ->with($role->id)
+ ->andReturn($mock);
+
+ $mock->shouldReceive('onRequest')->andReturn(mock(OnRequestRoleService::class, function ($mock) {
+ $mock->shouldReceive('setRoleType')->with(RoleType::ON_REQUEST)->once();
+ $mock->shouldReceive('updateRoleName')->once();
+ $mock->shouldReceive('syncAffiliateManyEntities')->once()->withArgs(function (AffiliationData $entity) {
+ return $entity->entity_id === 1 && $entity->entity_type === 'corporation' && $entity->affiliation_type === AffiliationType::ALLOWED;
+ });
+ $mock->shouldReceive('addCriteriaForRoleApplication')->once()->withArgs(function (CriteriaData $entity) {
+ return $entity->entity_id === 5 && $entity->entity_type === 'character';
+ });
+ $mock->shouldReceive('handleMembers')->once();
+ }));
+ });
+
+ // give the user the permission to manage roles
+ assignPermissionToTestUser('administrate access control groups');
+ $this->actingAs($this->test_user->refresh());
+
+ $action = app(ManageOnRequestRoleAction::class);
+ $action->execute($request);
+
+ expect(true)->toBeTrue(); // Just to ensure the test runs without exceptions
+});
+
+it('throws exception if user does not have permission', function () {
+ $this->mock(BaseRoleService::class, function ($mock) {
+ $mock->shouldReceive('for')->never();
+ $mock->shouldReceive('onRequest')->never();
+ });
+
+ $this->actingAs($this->test_user);
+
+ $request = Mockery::mock(RoleRequest::class);
+ $request->shouldReceive('validated')->andReturn([
+ 'role_id' => 1,
+ 'affiliated' => ['entity1', 'entity2'],
+ 'assigned' => ['criteria1', 'criteria2'],
+ 'name' => 'New Role Name',
+ ]);
+
+ $action = app(ManageOnRequestRoleAction::class);
+
+ expect(fn () => $action->execute($request))->toThrow(HttpException::class);
+});
diff --git a/tests/Unit/Actions/ManageOptInRoleActionTest.php b/tests/Unit/Actions/ManageOptInRoleActionTest.php
new file mode 100644
index 0000000..0307164
--- /dev/null
+++ b/tests/Unit/Actions/ManageOptInRoleActionTest.php
@@ -0,0 +1,76 @@
+ 'Role Name']);
+
+ $request = mock(RoleRequest::class, function (MockInterface $mock) use ($role) {
+
+ $mock->shouldReceive('validated')->once()->andReturn([
+ 'role_id' => $role->refresh()->id,
+ 'affiliated' => [['entity_id' => 1, 'entity_type' => 'corporation', 'affiliation_type' => 'allowed']],
+ 'assigned' => [['entity_id' => 5, 'entity_type' => 'character']],
+ 'name' => 'New Role Name',
+ ]);
+ });
+
+ $this->mock(BaseRoleService::class, function ($mock) use ($role) {
+ $mock->shouldReceive('for')
+ ->with($role->id)
+ ->andReturn($mock);
+
+ $mock->shouldReceive('optIn')->andReturn(mock(OptInRoleService::class, function ($mock) {
+ $mock->shouldReceive('setRoleType')->with(RoleType::OPT_IN)->once();
+ $mock->shouldReceive('updateRoleName')->once();
+ $mock->shouldReceive('syncAffiliateManyEntities')->once()->withArgs(function (AffiliationData $entity) {
+ return $entity->entity_id === 1 && $entity->entity_type === 'corporation' && $entity->affiliation_type === AffiliationType::ALLOWED;
+ });
+ $mock->shouldReceive('addCriteriaForRole')->once()->withArgs(function (CriteriaData $entity) {
+ return $entity->entity_id === 5 && $entity->entity_type === 'character';
+ });
+ $mock->shouldReceive('handleMembers')->once();
+ }));
+ });
+
+ // give the user the permission to manage roles
+ assignPermissionToTestUser('administrate access control groups');
+ $this->actingAs($this->test_user->refresh());
+
+ $action = app(ManageOptInRoleAction::class);
+ $action->execute($request);
+
+ expect(true)->toBeTrue(); // Just to ensure the test runs without exceptions
+});
+
+it('throws exception if user does not have permission', function () {
+ $this->mock(BaseRoleService::class, function ($mock) {
+ $mock->shouldReceive('for')->never();
+ $mock->shouldReceive('optIn')->never();
+ });
+
+ $this->actingAs($this->test_user);
+
+ $request = Mockery::mock(RoleRequest::class);
+ $request->shouldReceive('validated')->andReturn([
+ 'role_id' => 1,
+ 'affiliated' => ['entity1', 'entity2'],
+ 'assigned' => ['criteria1', 'criteria2'],
+ 'name' => 'New Role Name',
+ ]);
+
+ $action = app(ManageOptInRoleAction::class);
+
+ expect(fn () => $action->execute($request))->toThrow(HttpException::class);
+});
diff --git a/tests/Unit/Actions/OptOutActionTest.php b/tests/Unit/Actions/OptOutActionTest.php
new file mode 100644
index 0000000..3ca3cf7
--- /dev/null
+++ b/tests/Unit/Actions/OptOutActionTest.php
@@ -0,0 +1,36 @@
+mock(BaseRoleService::class, function ($mock) {
+ $mock->shouldReceive('for')->with(1)->andReturnSelf();
+ $mock->shouldReceive('onRequest')->andReturn(mock(OnRequestRoleService::class, function ($mock) {
+ $mock->shouldReceive('removeApplication')->once();
+ }));
+ });
+
+ $user = User::factory()->create();
+
+ $action = app(OptOutAction::class);
+ $action->execute(1, $user->id);
+
+ expect(true)->toBeTrue(); // Just to ensure the test runs without exceptions
+});
+
+it('throws exception if user not found during opt out', function () {
+ $this->mock(BaseRoleService::class, function ($mock) {
+ $mock->shouldReceive('for')->with(1)->andReturnSelf();
+ $mock->shouldReceive('onRequest')->andReturn(mock(OnRequestRoleService::class, function ($mock) {
+ $mock->shouldReceive('removeApplication')->never();
+ }));
+ });
+
+ $action = app(OptOutAction::class);
+
+ expect(fn () => $action->execute(1, 999))->toThrow(ModelNotFoundException::class);
+});
diff --git a/tests/Unit/Actions/RemoveMemberActionTest.php b/tests/Unit/Actions/RemoveMemberActionTest.php
new file mode 100644
index 0000000..2eaae4c
--- /dev/null
+++ b/tests/Unit/Actions/RemoveMemberActionTest.php
@@ -0,0 +1,16 @@
+ 'test']);
+
+ $this->mock(SetMemberAction::class, function ($mock) use ($role) {
+ $mock->shouldReceive('execute')->with($role->id, 1, false)->once();
+ });
+
+ $action = app(RemoveMemberAction::class);
+ $action->execute($role->id, 1);
+});
diff --git a/tests/Unit/Actions/RemoveModeratorRoleActionTest.php b/tests/Unit/Actions/RemoveModeratorRoleActionTest.php
new file mode 100644
index 0000000..6cdc147
--- /dev/null
+++ b/tests/Unit/Actions/RemoveModeratorRoleActionTest.php
@@ -0,0 +1,16 @@
+mock(SetModeratorAction::class, function ($mock) {
+ $mock->shouldReceive('execute')->with(1, 2, false)->once();
+ });
+
+ $action = app(RemoveModeratorRoleAction::class);
+
+ $action->execute(1, 2);
+});
diff --git a/tests/Unit/Actions/SetMemberActionTest.php b/tests/Unit/Actions/SetMemberActionTest.php
new file mode 100644
index 0000000..76c7b1e
--- /dev/null
+++ b/tests/Unit/Actions/SetMemberActionTest.php
@@ -0,0 +1,58 @@
+mock(BaseRoleService::class, function (MockInterface $mock) {
+
+ $mock->shouldReceive('for')
+ ->once()
+ ->with(1);
+
+ $mock->shouldReceive('canModerate')
+ ->once()
+ ->andReturn(false);
+ });
+
+ $action = app(SetMemberAction::class);
+
+ $this->actingAs($this->test_user);
+ $action->execute(1, 1, true);
+})->throws(Exception::class, 'You are not allowed to do this action');
+
+it('sets member', function (bool $is_member) {
+
+ $this->mock(BaseRoleService::class, function (MockInterface $mock) use ($is_member) {
+
+ $mock->shouldReceive('for')
+ ->once()
+ ->with(1);
+
+ $mock->shouldReceive('canModerate')
+ ->once()
+ ->andReturn(true);
+
+ $mock->shouldReceive('manual')
+ ->once()
+ ->andReturn(mock(ManualRoleService::class, function (MockInterface $mock) use ($is_member) {
+
+ if ($is_member) {
+ $mock->shouldReceive('addMember')
+ ->once();
+ } else {
+ $mock->shouldReceive('removeMember')
+ ->once();
+ }
+
+ }));
+ });
+
+ $action = app(SetMemberAction::class);
+
+ $this->actingAs($this->test_user);
+ $action->execute(1, $this->test_user->id, $is_member);
+})->with([true, false]);
diff --git a/tests/Unit/Actions/SetModeratorActionTest.php b/tests/Unit/Actions/SetModeratorActionTest.php
new file mode 100644
index 0000000..68694f3
--- /dev/null
+++ b/tests/Unit/Actions/SetModeratorActionTest.php
@@ -0,0 +1,55 @@
+actingAs(test()->test_user);
+
+ $this->mock(BaseRoleService::class, function (MockInterface $mock) {
+ $mock->shouldReceive('for')->with(1);
+ $mock->shouldReceive('canModerate')->andReturn(false);
+ });
+
+ $action = app(SetModeratorAction::class);
+
+ $action->execute(1, 1, true);
+
+})->throws(Exception::class, 'You are not allowed to add moderators');
+
+it('sets moderator role', function () {
+ $this->actingAs(test()->test_user);
+
+ $this->mock(BaseRoleService::class, function (MockInterface $mock) {
+ $mock->shouldReceive('for')->with(1);
+ $mock->shouldReceive('canModerate')->andReturn(true);
+ $mock->shouldReceive('getTypeService')->andReturn(
+ mock(OnRequestRoleService::class, function (MockInterface $mock) {
+ $mock->shouldReceive('setModerator')->once()->andReturn();
+ })
+ );
+ });
+
+ $action = app(SetModeratorAction::class);
+
+ $action->execute(1, test()->test_user->id, true);
+});
+
+it('throws exception if role type is not manual or on request', function () {
+ $this->actingAs(test()->test_user);
+
+ $this->mock(BaseRoleService::class, function (MockInterface $mock) {
+ $mock->shouldReceive('for')->with(1);
+ $mock->shouldReceive('canModerate')->andReturn(true);
+ $mock->shouldReceive('getTypeService')->andReturn(
+ mock(AutomaticRoleService::class)
+ );
+ });
+
+ $action = app(SetModeratorAction::class);
+
+ $action->execute(1, 1, true);
+})->throws(Exception::class, 'This action is not allowed');
diff --git a/tests/Unit/Affiliations/SeatPlusRolesTest.php b/tests/Unit/Affiliations/SeatPlusRolesTest.php
deleted file mode 100644
index a7fa70f..0000000
--- a/tests/Unit/Affiliations/SeatPlusRolesTest.php
+++ /dev/null
@@ -1,261 +0,0 @@
-secondary_character = CharacterInfo::factory()->create();
-
- test()->tertiary_character = CharacterInfo::factory()->create();
-
- test()->role = Role::create(['name' => 'derp']);
-});
-
-test('user has no roles test', function () {
- expect(test()->test_user->roles->isEmpty())->toBeTrue();
-});
-
-test('user has role test', function () {
- test()->test_user->assignRole(test()->role);
-
- expect(test()->test_user->roles->isNotEmpty())->toBeTrue();
-});
-
-test('role has no affiliation test', function () {
- expect(test()->role->affiliations->isEmpty())->toBeTrue();
-});
-
-test('role has an affiliation test', function () {
- test()->role->affiliations()->create([
- 'affiliatable_id' => test()->test_character->character_id,
- 'affiliatable_type' => CharacterInfo::class,
- 'type' => 'allowed',
- ]);
-
- test()->assertNotNUll(test()->role->affiliations);
-});
-
-test('user is in affiliation test', function () {
- test()->role->affiliations()->create([
- 'affiliatable_id' => test()->test_character->character_id,
- 'affiliatable_type' => CharacterInfo::class,
- 'type' => 'allowed',
- ]);
-
- expect(in_array(test()->test_character->character_id, test()->role->affiliated_ids))->toBeTrue();
-});
-
-test('character is in character allowed affiliation test', function () {
- $secondary_character = CharacterInfo::factory()->create();
-
- test()->role->affiliations()->createMany([
- [
- 'affiliatable_id' => test()->test_character->character_id,
- 'affiliatable_type' => CharacterInfo::class,
- 'type' => 'allowed',
- ],
- [
- 'affiliatable_id' => $secondary_character->character_id,
- 'affiliatable_type' => CharacterInfo::class,
- 'type' => 'allowed',
- ],
-
- ]);
-
- expect(in_array(test()->test_character->character_id, test()->role->affiliated_ids))->toBeTrue();
- expect(in_array($secondary_character->character_id, test()->role->affiliated_ids))->toBeTrue();
-});
-
-test('character is in character inversed affiliation test', function () {
- test()->role->affiliations()->createMany([
- [
- 'affiliatable_id' => test()->test_character->character_id,
- 'affiliatable_type' => CharacterInfo::class,
- 'type' => 'inverse',
- ],
- [
- 'affiliatable_id' => 1234,
- 'affiliatable_type' => CharacterInfo::class,
- 'type' => 'inverse',
- ],
- ]);
-
- expect(in_array(test()->test_character->character_id, test()->role->affiliated_ids))->toBeFalse();
- expect(in_array(test()->secondary_character->character_id, test()->role->affiliated_ids))->toBeTrue();
-});
-
-test('character is not in character inverse affiliation test', function () {
- test()->role->affiliations()->createMany([
- [
- 'affiliatable_id' => test()->secondary_character->character_id,
- 'affiliatable_type' => CharacterInfo::class,
- 'type' => 'inverse',
- ],
- [
- 'affiliatable_id' => test()->tertiary_character->character_id,
- 'affiliatable_type' => CharacterInfo::class,
- 'type' => 'inverse',
- ],
- ]);
-
- expect(in_array(test()->test_character->character_id, test()->role->affiliated_ids))->toBeTrue();
- expect(in_array(test()->secondary_character->character_id, test()->role->affiliated_ids))->toBeFalse();
- expect(in_array(test()->tertiary_character->character_id, test()->role->affiliated_ids))->toBeFalse();
-});
-
-test('character is in character forbidden affiliation test', function () {
- test()->role->affiliations()->createMany([
- [
- 'affiliatable_id' => test()->test_character->character_id,
- 'affiliatable_type' => CharacterInfo::class,
- 'type' => 'forbidden',
- ],
- [
- 'affiliatable_id' => test()->secondary_character->character_id,
- 'affiliatable_type' => CharacterInfo::class,
- 'type' => 'forbidden',
- ],
- ]);
-
- expect(in_array(test()->test_character->character_id, test()->role->affiliated_ids))->toBeFalse();
- expect(in_array(test()->secondary_character->character_id, test()->role->affiliated_ids))->toBeFalse();
- expect(in_array(test()->tertiary_character->character_id, test()->role->affiliated_ids))->toBeFalse();
-});
-
-test('character is in corporation allowed affiliation test', function () {
- test()->role->affiliations()->create([
- 'affiliatable_id' => test()->test_character->corporation_id,
- 'affiliatable_type' => CorporationInfo::class,
- 'type' => 'allowed',
- ]);
-
- expect(in_array(test()->test_character->character_id, test()->role->affiliated_ids))->toBeTrue();
- expect(in_array(test()->secondary_character->character_id, test()->role->affiliated_ids))->toBeFalse();
- expect(in_array(test()->tertiary_character->character_id, test()->role->affiliated_ids))->toBeFalse();
-});
-
-test('character is in corporation inversed affiliation test', function () {
- test()->role->affiliations()->createMany([
- [
- 'affiliatable_id' => test()->test_character->corporation_id,
- 'affiliatable_type' => CorporationInfo::class,
- 'type' => 'inverse',
- ],
- [
- 'affiliatable_id' => test()->secondary_character->corporation_id,
- 'affiliatable_type' => CorporationInfo::class,
- 'type' => 'inverse',
- ],
- ]);
-
- expect(in_array(test()->test_character->character_id, test()->role->affiliated_ids))->toBeFalse();
- expect(in_array(test()->secondary_character->character_id, test()->role->affiliated_ids))->toBeFalse();
- expect(in_array(test()->tertiary_character->character_id, test()->role->affiliated_ids))->toBeTrue();
-});
-
-test('character is in corporation forbidden affiliation test', function () {
- test()->role->affiliations()->createMany([
- [
- 'affiliatable_id' => test()->test_character->corporation_id,
- 'affiliatable_type' => CorporationInfo::class,
- 'type' => 'forbidden',
- ],
- [
- 'affiliatable_id' => test()->secondary_character->corporation_id,
- 'affiliatable_type' => CorporationInfo::class,
- 'type' => 'forbidden',
- ],
- ]);
-
- expect(in_array(test()->test_character->character_id, test()->role->affiliated_ids))->toBeFalse();
- expect(in_array(test()->secondary_character->character_id, test()->role->affiliated_ids))->toBeFalse();
- expect(in_array(test()->tertiary_character->character_id, test()->role->affiliated_ids))->toBeFalse();
-});
-
-test('character is in alliance allowed affiliation test', function () {
- test()->role->affiliations()->createMany([
- [
- 'affiliatable_id' => test()->test_character->alliance_id,
- 'affiliatable_type' => AllianceInfo::class,
- 'type' => 'allowed',
- ],
- [
- 'affiliatable_id' => test()->secondary_character->alliance_id,
- 'affiliatable_type' => AllianceInfo::class,
- 'type' => 'allowed',
- ],
- ]);
-
- expect(in_array(test()->test_character->character_id, test()->role->affiliated_ids))->toBeTrue();
- expect(in_array(test()->secondary_character->character_id, test()->role->affiliated_ids))->toBeTrue();
- expect(in_array(test()->tertiary_character->character_id, test()->role->affiliated_ids))->toBeFalse();
-});
-
-test('character is in alliance inversed affiliation test', function () {
- test()->role->affiliations()->createMany([
- [
- 'affiliatable_id' => test()->test_character->alliance_id,
- 'affiliatable_type' => AllianceInfo::class,
- 'type' => 'inverse',
- ],
- [
- 'affiliatable_id' => test()->secondary_character->alliance_id,
- 'affiliatable_type' => AllianceInfo::class,
- 'type' => 'inverse',
- ],
- ]);
-
- expect(in_array(test()->test_character->character_id, test()->role->affiliated_ids))->toBeFalse();
- expect(in_array(test()->secondary_character->character_id, test()->role->affiliated_ids))->toBeFalse();
- expect(in_array(test()->tertiary_character->character_id, test()->role->affiliated_ids))->toBeTrue();
-});
-
-test('character is in alliance forbidden affiliation test', function () {
- test()->role->affiliations()->createMany([
- [
- 'affiliatable_id' => test()->test_character->alliance_id,
- 'affiliatable_type' => AllianceInfo::class,
- 'type' => 'forbidden',
- ],
- [
- 'affiliatable_id' => test()->secondary_character->alliance_id,
- 'affiliatable_type' => AllianceInfo::class,
- 'type' => 'forbidden',
- ],
- ]);
-
- expect(in_array(test()->test_character->character_id, test()->role->affiliated_ids))->toBeFalse();
- expect(in_array(test()->secondary_character->character_id, test()->role->affiliated_ids))->toBeFalse();
- expect(in_array(test()->tertiary_character->character_id, test()->role->affiliated_ids))->toBeFalse();
-});
diff --git a/tests/Unit/AuthenticationServiceProviderTest.php b/tests/Unit/AuthenticationServiceProviderTest.php
new file mode 100644
index 0000000..9e821dc
--- /dev/null
+++ b/tests/Unit/AuthenticationServiceProviderTest.php
@@ -0,0 +1,11 @@
+driver('eveonline');
+
+ expect($driver)->toBeInstanceOf(Provider::class);
+});
diff --git a/tests/Unit/Controllers/CallbackControllerTest.php b/tests/Unit/Controllers/CallbackControllerTest.php
new file mode 100644
index 0000000..b0126fd
--- /dev/null
+++ b/tests/Unit/Controllers/CallbackControllerTest.php
@@ -0,0 +1,98 @@
+makePartial();
+ });
+
+ $socialite_user->attributes = (object) [
+ 'character_id' => 1,
+ 'character_owner_hash' => faker()->sha256,
+ ];
+ $socialite_user->token = 'token';
+ $socialite_user->refreshToken = 'refreshToken';
+ $socialite_user->expiresIn = 12 * 60; // let's just say 12 minutes
+ $socialite_user->user = [
+ 'scp' => ['esi-skills.read_skills.v1', 'esi-skills.read_skillqueue.v1'],
+ ];
+
+ $social = mock(Socialite::class, function (MockInterface $social) use ($socialite_user) {
+ $social->shouldReceive('driver->user')->andReturn($socialite_user);
+ });
+
+ $find_or_create_user_action = mock(FindOrCreateUserAction::class, function (MockInterface $mock) {
+ $mock->shouldReceive('__invoke')->andReturn(mock(User::class));
+ });
+
+ $update_refresh_token_action = mock(UpdateRefreshTokenAction::class, function (MockInterface $mock) {
+ $mock->shouldReceive('__invoke')->andReturnNull();
+ });
+
+ $authenticationService = mock(AuthenticationService::class, function (MockInterface $mock) {
+ $mock->shouldReceive('isUserAuthenticated')->andReturnFalse();
+ $mock->shouldReceive('loginUser')->andReturnFalse();
+ });
+
+ $controller = new CallbackController($authenticationService);
+
+ $response = $controller($social, $find_or_create_user_action, $update_refresh_token_action);
+
+ expect($response)->toBeInstanceOf(RedirectResponse::class)
+ ->and(session('error'))->toBe('Login failed. Please contact your administrator.');
+});
+
+it('redirects back if different character id is provided', function () {
+ $socialite_user = mock(SocialiteUser::class, function (MockInterface $mock) {
+ $mock->makePartial();
+ });
+
+ $socialite_user->attributes = (object) [
+ 'character_id' => 1,
+ 'character_owner_hash' => faker()->sha256,
+ ];
+ $socialite_user->token = 'token';
+ $socialite_user->refreshToken = 'refreshToken';
+ $socialite_user->expiresIn = 12 * 60; // let's just say 12 minutes
+ $socialite_user->user = [
+ 'scp' => ['esi-skills.read_skills.v1', 'esi-skills.read_skillqueue.v1'],
+ ];
+
+ $social = mock(Socialite::class, function (MockInterface $social) use ($socialite_user) {
+ $social->shouldReceive('driver->user')->andReturn($socialite_user);
+ });
+
+ $find_or_create_user_action = mock(FindOrCreateUserAction::class, function (MockInterface $mock) {
+ $mock->shouldReceive('__invoke')->andReturn(mock(User::class));
+ });
+
+ $update_refresh_token_action = mock(UpdateRefreshTokenAction::class, function (MockInterface $mock) {
+ $mock->shouldReceive('__invoke')->andReturnNull();
+ });
+
+ $authenticationService = mock(AuthenticationService::class, function (MockInterface $mock) {
+ $mock->shouldReceive('isUserAuthenticated')->andReturnTrue();
+ $mock->shouldReceive('getSessionValue')->with('sso_scopes')->andReturn(['esi-skills.read_skills.v1', 'esi-skills.read_skillqueue.v1']);
+ $mock->shouldReceive('getSessionValue')->with('step_up')->andReturn(2);
+ $mock->shouldReceive('flashMessage')
+ ->once()
+ ->with('error', 'Please make sure to select the same character to step up on CCP as on seatplus.')
+ ->andReturnNull();
+ });
+
+ $controller = new CallbackController($authenticationService);
+
+ $response = $controller($social, $find_or_create_user_action, $update_refresh_token_action);
+
+ expect($response)->toBeInstanceOf(RedirectResponse::class);
+});
diff --git a/tests/Unit/Controllers/RedirectSSOControllerTest.php b/tests/Unit/Controllers/RedirectSSOControllerTest.php
new file mode 100644
index 0000000..b1569a6
--- /dev/null
+++ b/tests/Unit/Controllers/RedirectSSOControllerTest.php
@@ -0,0 +1,44 @@
+serviceMock = mock(GlobalSsoScopesService::class);
+ $this->authenticationServiceMock = mock(AuthenticationService::class);
+ $this->socialiteMock = mock(Socialite::class);
+ $this->controller = new RedirectSSOController($this->serviceMock, $this->authenticationServiceMock);
+});
+
+afterEach(function () {
+ Mockery::close();
+});
+
+it('redirects to Eve Online authentication page when user is not authenticated', function () {
+ $this->authenticationServiceMock->shouldReceive('isUserAuthenticated')->andReturn(false);
+ $this->authenticationServiceMock->shouldReceive('getPreviousUrl')->andReturn('http://example.com/previous');
+ $this->serviceMock->shouldReceive('get')->andReturn(['scope1', 'scope2']);
+ $this->socialiteMock->shouldReceive('driver')
+ ->with('eveonline')
+ ->andReturn(mock(Provider::class, function (MockInterface $mock) {
+ $mock->shouldReceive('scopes')->andReturnSelf();
+ $mock->shouldReceive('redirect')->andReturn(new RedirectResponse('http://example.com/redirect'));
+ }));
+
+ $response = $this->controller->__invoke($this->socialiteMock);
+
+ expect($response->getTargetUrl())->toBe('http://example.com/redirect');
+});
+
+it('redirects home when user is already authenticated', function () {
+ $this->authenticationServiceMock->shouldReceive('isUserAuthenticated')->andReturn(true);
+
+ $response = $this->controller->__invoke($this->socialiteMock);
+
+ expect($response)->toBeInstanceOf(RedirectResponse::class);
+});
diff --git a/tests/Unit/Events/RefreshTokenTest.php b/tests/Unit/Events/RefreshTokenTest.php
index c648304..6dc9c30 100644
--- a/tests/Unit/Events/RefreshTokenTest.php
+++ b/tests/Unit/Events/RefreshTokenTest.php
@@ -4,11 +4,12 @@
use Seatplus\Auth\Models\CharacterUser;
use Seatplus\Eveapi\Models\RefreshToken;
-it('flush characters_with_missing_scopes cache for user when a new character is added', function () {
+it('forgets user permission object when a new character is added', function () {
$user_id = test()->test_user->id;
- Cache::shouldReceive('tags')->with(['characters_with_missing_scopes', $user_id])->andReturnSelf();
- Cache::shouldReceive('flush')->once();
+ Cache::shouldReceive('forget')
+ ->once()
+ ->with("user_permissions_{$user_id}");
$character_user = CharacterUser::factory()->create([
'user_id' => $user_id,
@@ -18,11 +19,12 @@
RefreshToken::factory()->create(['character_id' => $character_user->character_id]);
});
-it('flush characters_with_missing_scopes cache for user when refresh_token scopes are updated', function () {
+it('forgets user permission object when refresh_token scopes are updated', function () {
$user_id = test()->test_user->id;
- Cache::shouldReceive('tags')->with(['characters_with_missing_scopes', $user_id])->andReturnSelf();
- Cache::shouldReceive('flush')->once();
+ Cache::shouldReceive('forget')
+ ->once()
+ ->with("user_permissions_{$user_id}");
$refresh_token = test()->test_character->refresh_token;
$refresh_token->token = createSocialiteUser($refresh_token->character_id, ['foo', 'bar'])->token;
diff --git a/tests/Unit/FindOrCreateUserActionTest.php b/tests/Unit/FindOrCreateUserActionTest.php
index 9a1eaaa..ba8af7f 100644
--- a/tests/Unit/FindOrCreateUserActionTest.php
+++ b/tests/Unit/FindOrCreateUserActionTest.php
@@ -34,7 +34,7 @@
'main_character_id' => $eve_user->character_id,
]);
- $action = new FindOrCreateUserAction();
+ $action = new FindOrCreateUserAction;
$user = $action($eve_user);
test()->assertDatabaseHas('users', [
@@ -63,7 +63,7 @@
$secondary_character->character_owner_hash
);
- $action = new FindOrCreateUserAction();
+ $action = new FindOrCreateUserAction;
$user = $action($eve_user);
expect($user->id)->toEqual(test()->test_user->id);
@@ -82,7 +82,7 @@
'anotherHashValue'
);
- $action = new FindOrCreateUserAction();
+ $action = new FindOrCreateUserAction;
$user = $action($eve_user);
test()->assertDatabaseHas('users', [
@@ -117,7 +117,7 @@
'anotherHashValue'
);
- $action = new FindOrCreateUserAction();
+ $action = new FindOrCreateUserAction;
$user = $action($eve_user);
expect($user->character_users->count())->toEqual(1);
@@ -134,7 +134,7 @@
expect(test()->test_user->id)->not()->toBe($user->id);
- //5. assert that secondary character is not affiliated to first user
+ // 5. assert that secondary character is not affiliated to first user
test()->assertDatabaseMissing('character_users', [
'user_id' => test()->test_user->id,
@@ -156,7 +156,7 @@
$secondary_user->character_owner_hash
);
- $action = new FindOrCreateUserAction();
+ $action = new FindOrCreateUserAction;
// act as test user
test()->actingAs(test()->test_user);
diff --git a/tests/Unit/Jobs/RoleMemberSyncTest.php b/tests/Unit/Jobs/RoleMemberSyncTest.php
new file mode 100644
index 0000000..546887d
--- /dev/null
+++ b/tests/Unit/Jobs/RoleMemberSyncTest.php
@@ -0,0 +1,30 @@
+serviceMock = mock(BaseRoleService::class);
+ $this->job = new RoleMemberSync($this->serviceMock);
+});
+
+afterEach(function () {
+ Mockery::close();
+});
+
+it('handles role member synchronization successfully', function () {
+
+ $role = Role::create(['name' => 'derp']);
+
+ $this->serviceMock->shouldReceive('for')->once()->andReturnSelf();
+ $this->serviceMock->shouldReceive('handleMembers')->once();
+
+ $this->job->handle();
+});
+
+it('returns correct tags for the job', function () {
+ $tags = $this->job->tags();
+
+ expect($tags)->toBe(['Dispatch Role Updates']);
+});
diff --git a/tests/Unit/Middleware/CheckRequiredScopesTest.php b/tests/Unit/Middleware/CheckRequiredScopesTest.php
index 6cdc195..910cd97 100644
--- a/tests/Unit/Middleware/CheckRequiredScopesTest.php
+++ b/tests/Unit/Middleware/CheckRequiredScopesTest.php
@@ -24,374 +24,350 @@
* SOFTWARE.
*/
+use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
-use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Event;
+use Illuminate\Support\Str;
use Seatplus\Auth\Http\Middleware\CheckRequiredScopes;
use Seatplus\Auth\Models\CharacterUser;
+use Seatplus\Auth\Models\User;
+use Seatplus\Auth\Services\SsoScopes\IsUserCompliantService;
use Seatplus\Eveapi\Models\Character\CharacterInfo;
use Seatplus\Eveapi\Models\RefreshToken;
use Seatplus\Eveapi\Models\SsoScopes;
beforeEach(function () {
- //test()->actingAs(test()->test_user);
+ // test()->actingAs(test()->test_user);
mockRequest();
Event::fake();
});
-it('lets request through if no scopes are required', function () {
- createRefreshTokenWithScopes(['a', 'b']);
+describe('redirect request', function () {
+ it('if required scopes are missing', function () {
+ // 1. Create RefreshToken for Character
+ createRefreshTokenWithScopes(['a', 'b']);
- test()->actingAs(test()->test_user);
-
- mockMiddleware();
-
- //test()->middleware->shouldReceive('redirectTo')->once();
- test()->request->shouldReceive('forward')->times(1);
-
- test()->middleware->handle(test()->request, test()->next);
-});
-
-it('lets request through if required scopes are present', function () {
- // 1. Create RefreshToken for Character
- createRefreshTokenWithScopes(['a', 'b']);
+ // 2. Create SsoScope (Corporation)
+ createCorporationSsoScope([
+ 'character' => ['c'],
+ 'corporation' => [],
+ ]);
- // 2. Create SsoScope (Corporation)
- createCorporationSsoScope([
- 'character' => ['a'],
- 'corporation' => [],
- ]);
+ // TestingTime
- // TestingTime
+ test()->actingAs(test()->test_user);
- test()->actingAs(test()->test_user);
+ mockMiddleware();
- mockMiddleware();
+ // Expect redirect
+ test()->middleware->shouldReceive('redirectTo')->times(1);
- //Expect 1 forward
- test()->request->shouldReceive('forward')->times(1);
+ test()->middleware->handle(test()->request, test()->next);
+ });
- test()->middleware->handle(test()->request, test()->next);
-});
+ it('if required corporation role scopes is missing', function () {
+ // 1. Create RefreshToken for Character
+ createRefreshTokenWithScopes(['a', 'b']);
-it('stops request if required scopes are missing', function () {
- // 1. Create RefreshToken for Character
- createRefreshTokenWithScopes(['a', 'b']);
+ // 2. Create SsoScope (Corporation)
+ createCorporationSsoScope(['c']);
- // 2. Create SsoScope (Corporation)
- createCorporationSsoScope([
- 'character' => ['c'],
- 'corporation' => [],
- ]);
+ // TestingTime
- // TestingTime
+ test()->actingAs(test()->test_user);
- test()->actingAs(test()->test_user);
+ mockMiddleware();
- mockMiddleware();
+ // Expect redirect
+ test()->middleware->shouldReceive('redirectTo')->times(1);
- //Expect redirect
- test()->middleware->shouldReceive('redirectTo')->times(1);
+ test()->middleware->handle(test()->request, test()->next);
+ });
- test()->middleware->handle(test()->request, test()->next);
-});
+ it('if user scopes is missing', function () {
+ // Create RefreshToken for Character
+ createRefreshTokenWithScopes(['a', 'b']);
-it('stops request if required corporation role scopes is missing', function () {
- // 1. Create RefreshToken for Character
- createRefreshTokenWithScopes(['a', 'b']);
+ // create user corporation scope
+ createCorporationSsoScope(['a'], 'user');
- // 2. Create SsoScope (Corporation)
- createCorporationSsoScope(['c']);
+ // to this point the middleware should pass no question asked
- // TestingTime
+ // Create secondary character
+ $secondary_character = Event::fakeFor(function () {
+ $character_user = CharacterUser::factory()->make();
+ test()->test_user->character_users()->save($character_user);
- test()->actingAs(test()->test_user);
+ return CharacterInfo::find($character_user->character_id);
+ });
- mockMiddleware();
+ // test that the test user owns both characters
+ expect(test()->test_user->refresh()->characters)->toHaveCount(2);
- //Expect redirect
- test()->middleware->shouldReceive('redirectTo')->times(1);
+ // test that primary and secondary character has different corporations
+ test()->assertNotEquals(test()->test_character->corporation->corporation_id, $secondary_character->corporation->corporation_id);
- test()->middleware->handle(test()->request, test()->next);
-});
+ // create refresh_token for secondary character
+ Event::fakeFor(function () use ($secondary_character) {
+ $helper_token = RefreshToken::factory()->scopes(['c'])->make([
+ 'character_id' => $secondary_character->character_id,
+ ]);
-it('lets request through if required corporation role scopes is present', function () {
- // 1. Create RefreshToken for Character
- createRefreshTokenWithScopes(['a', 'b', 'esi-characters.read_corporation_roles.v1']);
+ $refresh_token = $secondary_character->refresh_token;
+ $refresh_token->token = $helper_token->token;
+ $refresh_token->save();
+ });
+ // at this point secondary character has scope c and misses scope a thus should result in an error
- // 2. Create SsoScope (Corporation)
- createCorporationSsoScope([
- 'character' => [],
- 'corporation' => ['b'],
- ]);
+ // TestingTime
- // TestingTime
+ test()->actingAs(test()->test_user);
- test()->actingAs(test()->test_user);
+ mockMiddleware();
- mockMiddleware();
+ // Expect redirect
+ test()->middleware->shouldReceive('redirectTo')->times(1);
- //Expect redirect
- test()->request->shouldReceive('forward')->times(1);
+ test()->middleware->handle(test()->request, test()->next);
+ });
- test()->middleware->handle(test()->request, test()->next);
-});
+ it('if user misses global scopes', function () {
+ // 1. Create RefreshToken for Character
+ createRefreshTokenWithScopes(['a', 'b']);
-it('forwards request if user misses global scopes', function () {
- // 1. Create RefreshToken for Character
- createRefreshTokenWithScopes(['a', 'b']);
+ // 2. create global required scope
+ SsoScopes::updateOrCreate(['type' => 'global'], ['selected_scopes' => ['c']]);
- // 2. create global required scope
- SsoScopes::updateOrCreate(['type' => 'global'], ['selected_scopes' => ['c']]);
+ // TestingTime
- // TestingTime
+ test()->actingAs(test()->test_user);
- test()->actingAs(test()->test_user);
+ mockMiddleware();
- mockMiddleware();
+ // Expect redirect
+ test()->middleware->shouldReceive('redirectTo')->times(1);
- //Expect redirect
- test()->middleware->shouldReceive('redirectTo')->times(1);
+ test()->middleware->handle(test()->request, test()->next);
+ });
- test()->middleware->handle(test()->request, test()->next);
-});
+ it('if user application has not required scopes', function () {
+ // 1. Create RefreshToken for Character
+ createRefreshTokenWithScopes(['a', 'b']);
-it('lets request through if required global scopes are present', function () {
- // 1. Create RefreshToken for Character
- createRefreshTokenWithScopes(['a', 'b']);
+ // 2. create user application
+ test()->test_user->application()->create(['id' => Str::uuid(), 'corporation_id' => test()->test_character->corporation->corporation_id]);
- // 2. create global required sso scope
- SsoScopes::updateOrCreate(['type' => 'global'], ['selected_scopes' => ['a']]);
+ // 3. create required corp scopes
+ createCorporationSsoScope(['c']);
- // TestingTime
+ // TestingTime
- test()->actingAs(test()->test_user);
+ test()->actingAs(test()->test_user);
- mockMiddleware();
+ mockMiddleware();
- //Expect 1 forward
- test()->request->shouldReceive('forward')->times(1);
+ // Expect redirect
+ test()->middleware->shouldReceive('redirectTo')->times(1);
- test()->middleware->handle(test()->request, test()->next);
+ test()->middleware->handle(test()->request, test()->next);
+ });
});
-it('stops request if user scopes is missing', function () {
- // Create RefreshToken for Character
- createRefreshTokenWithScopes(['a', 'b']);
+describe('passes middleware', function () {
+ it('lets request through if no scopes are required', function () {
+ createRefreshTokenWithScopes(['a', 'b']);
- // create user corporation scope
- createCorporationSsoScope(['a'], 'user');
+ test()->actingAs(test()->test_user);
- // to this point the middleware should pass no question asked
+ mockMiddleware();
- // Create secondary character
- $secondary_character = Event::fakeFor(function () {
- $character_user = CharacterUser::factory()->make();
- test()->test_user->character_users()->save($character_user);
+ // test()->middleware->shouldReceive('redirectTo')->once();
+ test()->request->shouldReceive('forward')->times(1);
- return CharacterInfo::find($character_user->character_id);
+ test()->middleware->handle(test()->request, test()->next);
});
- // test that the test user owns both characters
- expect(test()->test_user->refresh()->characters)->toHaveCount(2);
+ it('if required scopes are present', function () {
+ // 1. Create RefreshToken for Character
+ createRefreshTokenWithScopes(['a', 'b']);
- // test that primary and secondary character has different corporations
- test()->assertNotEquals(test()->test_character->corporation->corporation_id, $secondary_character->corporation->corporation_id);
-
- // create refresh_token for secondary character
- Event::fakeFor(function () use ($secondary_character) {
- $helper_token = RefreshToken::factory()->scopes(['c'])->make([
- 'character_id' => $secondary_character->character_id,
+ // 2. Create SsoScope (Corporation)
+ createCorporationSsoScope([
+ 'character' => ['a'],
+ 'corporation' => [],
]);
- $refresh_token = $secondary_character->refresh_token;
- $refresh_token->token = $helper_token->token;
- $refresh_token->save();
- });
-
- // at this point secondary character has scope c and misses scope a thus should result in an error
+ // TestingTime
- // TestingTime
+ test()->actingAs(test()->test_user);
- test()->actingAs(test()->test_user);
+ mockMiddleware();
- mockMiddleware();
+ // Expect 1 forward
+ test()->request->shouldReceive('forward')->times(1);
- //Expect redirect
- test()->middleware->shouldReceive('redirectTo')->times(1);
-
- test()->middleware->handle(test()->request, test()->next);
-});
-
-it('lets request through if user scopes is present', function () {
- // Create RefreshToken for Character
- createRefreshTokenWithScopes(['a', 'b']);
-
- // create user corporation scope
- createCorporationSsoScope(['a'], 'user');
+ test()->middleware->handle(test()->request, test()->next);
+ });
- // to this point the middleware should pass no question asked
+ it('if required corporation role scopes is present', function () {
+ // 1. Create RefreshToken for Character
+ createRefreshTokenWithScopes(['a', 'b', 'esi-characters.read_corporation_roles.v1']);
- // Create secondary character
- $secondary_character = Event::fakeFor(function () {
- $character_user = CharacterUser::factory()->make();
- test()->test_user->character_users()->save($character_user);
+ // 2. Create SsoScope (Corporation)
+ createCorporationSsoScope([
+ 'character' => [],
+ 'corporation' => ['b'],
+ ]);
- return CharacterInfo::find($character_user->character_id);
- });
+ // TestingTime
- // test that the test user owns both characters
- expect(test()->test_user->refresh()->characters)->toHaveCount(2);
+ test()->actingAs(test()->test_user);
- // test that primary and secondary character has different corporations
- test()->assertNotEquals(test()->test_character->corporation->corporation_id, $secondary_character->corporation->corporation_id);
+ mockMiddleware();
- // update refresh_token for secondary character
- Event::fakeFor(function () use ($secondary_character) {
- $helper_token = RefreshToken::factory()->scopes(['a'])->make([
- 'character_id' => $secondary_character->character_id,
- ]);
+ // Expect redirect
+ test()->request->shouldReceive('forward')->times(1);
- $refresh_token = $secondary_character->refresh_token;
- $refresh_token->token = $helper_token->token;
- $refresh_token->save();
+ test()->middleware->handle(test()->request, test()->next);
});
- // at this point secondary character has scope a and scope a is required, thus should result in an forward
-
- // TestingTime
+ it('if required global scopes are present', function () {
+ // 1. Create RefreshToken for Character
+ createRefreshTokenWithScopes(['a', 'b']);
- test()->actingAs(test()->test_user);
+ // 2. create global required sso scope
+ SsoScopes::updateOrCreate(['type' => 'global'], ['selected_scopes' => ['a']]);
- mockMiddleware();
+ // TestingTime
- //Expect redirect
- test()->request->shouldReceive('forward')->times(1);
+ test()->actingAs(test()->test_user);
- test()->middleware->handle(test()->request, test()->next);
-});
+ mockMiddleware();
-it('lets request through if user application has no required scopes', function () {
- // 1. Create RefreshToken for Character
- createRefreshTokenWithScopes(['a', 'b']);
+ // Expect 1 forward
+ test()->request->shouldReceive('forward')->times(1);
- // 2. create user application
- test()->test_user->application()->create(['id' => \Illuminate\Support\Str::uuid(), 'corporation_id' => test()->test_character->corporation->corporation_id]);
+ test()->middleware->handle(test()->request, test()->next);
+ });
- // TestingTime
+ it('if user scopes is present', function () {
+ // Create RefreshToken for Character
+ createRefreshTokenWithScopes(['a', 'b']);
- test()->actingAs(test()->test_user);
+ // create user corporation scope
+ createCorporationSsoScope(['a'], 'user');
- mockMiddleware();
+ // to this point the middleware should pass no question asked
- //Expect 1 forward
- test()->request->shouldReceive('forward')->times(1);
+ // Create secondary character
+ $secondary_character = Event::fakeFor(function () {
+ $character_user = CharacterUser::factory()->make();
+ test()->test_user->character_users()->save($character_user);
- test()->middleware->handle(test()->request, test()->next);
-});
+ return CharacterInfo::find($character_user->character_id);
+ });
-it('lets request through if user application has required scopes', function () {
- // 1. Create RefreshToken for Character
- createRefreshTokenWithScopes(['a', 'b']);
+ // test that the test user owns both characters
+ expect(test()->test_user->refresh()->characters)->toHaveCount(2);
- // 2. create user application
- test()->test_user->application()->create(['id' => \Illuminate\Support\Str::uuid(), 'corporation_id' => test()->test_character->corporation->corporation_id]);
+ // test that primary and secondary character has different corporations
+ test()->assertNotEquals(test()->test_character->corporation->corporation_id, $secondary_character->corporation->corporation_id);
- // 3. create required corp scopes
- createCorporationSsoScope(['a']);
+ // update refresh_token for secondary character
+ Event::fakeFor(function () use ($secondary_character) {
+ $helper_token = RefreshToken::factory()->scopes(['a'])->make([
+ 'character_id' => $secondary_character->character_id,
+ ]);
- // TestingTime
+ $refresh_token = $secondary_character->refresh_token;
+ $refresh_token->token = $helper_token->token;
+ $refresh_token->save();
+ });
- test()->actingAs(test()->test_user);
+ // at this point secondary character has scope a and scope a is required, thus should result in an forward
- mockMiddleware();
+ // TestingTime
- //Expect 1 forward
- test()->request->shouldReceive('forward')->times(1);
+ test()->actingAs(test()->test_user);
- test()->middleware->handle(test()->request, test()->next);
-});
+ mockMiddleware();
-it('forwards request if user application has not required scopes', function () {
- // 1. Create RefreshToken for Character
- createRefreshTokenWithScopes(['a', 'b']);
+ // Expect redirect
+ test()->request->shouldReceive('forward')->times(1);
- // 2. create user application
- test()->test_user->application()->create(['id' => \Illuminate\Support\Str::uuid(), 'corporation_id' => test()->test_character->corporation->corporation_id]);
+ test()->middleware->handle(test()->request, test()->next);
+ });
- // 3. create required corp scopes
- createCorporationSsoScope(['c']);
+ it('if user application has no required scopes', function () {
+ // 1. Create RefreshToken for Character
+ createRefreshTokenWithScopes(['a', 'b']);
- // TestingTime
+ // 2. create user application
+ test()->test_user->application()->create(['id' => Str::uuid(), 'corporation_id' => test()->test_character->corporation->corporation_id]);
- test()->actingAs(test()->test_user);
+ // TestingTime
- mockMiddleware();
+ test()->actingAs(test()->test_user);
- //Expect redirect
- test()->middleware->shouldReceive('redirectTo')->times(1);
+ mockMiddleware();
- test()->middleware->handle(test()->request, test()->next);
-});
+ // Expect 1 forward
+ test()->request->shouldReceive('forward')->times(1);
-it('caches characters_with_missing_scopes', function () {
- // 1. Create RefreshToken for Character
- createRefreshTokenWithScopes(['a', 'b']);
+ test()->middleware->handle(test()->request, test()->next);
+ });
- // 2. create user application
- test()->test_user->application()->create(['id' => \Illuminate\Support\Str::uuid(), 'corporation_id' => test()->test_character->corporation->corporation_id]);
+ it('lets request through if user application has required scopes', function () {
+ // 1. Create RefreshToken for Character
+ createRefreshTokenWithScopes(['a', 'b']);
- // 3. create required corp scopes
- createCorporationSsoScope(['c']);
+ // 2. create user application
+ test()->test_user->application()->create(['id' => Str::uuid(), 'corporation_id' => test()->test_character->corporation->corporation_id]);
- // TestingTime
+ // 3. create required corp scopes
+ createCorporationSsoScope(['a']);
- test()->actingAs(test()->test_user);
+ // TestingTime
- mockMiddleware();
+ test()->actingAs(test()->test_user);
- //Expect redirect
- test()->middleware->shouldReceive('redirectTo')->times(1);
+ mockMiddleware();
- Cache::shouldReceive('tags')
- ->with(['characters_with_missing_scopes', test()->test_user->id])
- ->andReturnSelf();
- Cache::shouldReceive('get')->andReturnNull();
- Cache::shouldReceive('put')
- ->once();
+ // Expect 1 forward
+ test()->request->shouldReceive('forward')->times(1);
- test()->middleware->handle(test()->request, test()->next);
+ test()->middleware->handle(test()->request, test()->next);
+ });
});
-it('it get caches characters_with_missing_scopes', function () {
- // prepare
- test()->actingAs(test()->test_user);
- $user_id = test()->test_user->id;
- $cache_key = "UserScopes:{$user_id}";
-
- mockMiddleware();
-
- Cache::shouldReceive('tags')
- ->with(['characters_with_missing_scopes', $user_id])
- ->andReturnSelf();
+it('redirects when user is not compliant', function () {
+ $this->mock(IsUserCompliantService::class, function ($mock) {
+ $mock->shouldReceive('check')->with(Mockery::type(User::class))->andReturn(false);
+ $mock->shouldReceive('getMissingScopes')->with(Mockery::type(User::class))->andReturn(['scope1', 'scope2']);
+ });
- Cache::shouldReceive('get')->with($cache_key)->andReturn(collect(['foo' => 'bar']));
+ $middleware = new CheckRequiredScopes(app(IsUserCompliantService::class));
+ $request = Mockery::mock(Request::class);
+ $request->shouldReceive('user')->andReturn(new User);
- Cache::shouldReceive('put')->never();
+ $next = function ($req) {
+ return 'next';
+ };
- //Expect redirect
- test()->middleware->shouldReceive('redirectTo')->times(1);
+ $response = $middleware->handle($request, $next);
- // test
- test()->middleware->handle(test()->request, test()->next);
+ expect($response)->toBeInstanceOf(RedirectResponse::class)
+ ->and($response->getTargetUrl())->toBe('http://localhost');
});
// Helpers
function mockRequest(): void
{
- test()->request = Mockery::mock(Request::class);
+ test()->request = mock(Request::class, function ($mock) {
+ $mock->shouldReceive('user')->andReturn(test()->test_user);
+ });
test()->next = function ($request) {
$request->forward();
diff --git a/tests/Unit/Models/AclMemberTest.php b/tests/Unit/Models/AclMemberTest.php
deleted file mode 100644
index 07d216d..0000000
--- a/tests/Unit/Models/AclMemberTest.php
+++ /dev/null
@@ -1,21 +0,0 @@
-role = Role::create(['name' => 'derp']);
-});
-
-it('has user relationship', function () {
- test()->role->members()->create([
- 'user_id' => test()->test_user->id,
- 'status' => 'member',
- ]);
-
- $member = AclMember::where('user_id', test()->test_user->id)
- ->get()->first();
-
- expect($member->user::class)->toEqual(User::class);
-});
diff --git a/tests/Unit/Models/AffiliationModelTest.php b/tests/Unit/Models/AffiliationModelTest.php
new file mode 100644
index 0000000..f579c55
--- /dev/null
+++ b/tests/Unit/Models/AffiliationModelTest.php
@@ -0,0 +1,76 @@
+role = Role::create(['name' => 'derp']);
+});
+
+dataset('primary entities', [
+ 'character' => fn () => [test()->test_character->character_id, CharacterInfo::class],
+ 'corporation' => fn () => [test()->test_character->corporation_id, CorporationInfo::class],
+ 'alliance' => fn () => [test()->test_character->alliance_id, AllianceInfo::class],
+]);
+
+dataset('affiliation types', [
+ 'allowed' => AffiliationType::ALLOWED->value,
+ 'inverted' => AffiliationType::INVERSE->value,
+ 'forbidden' => AffiliationType::FORBIDDEN->value,
+]);
+
+it('has affiliated_ids attribute', function ($primary_entities, $affiliation_type) {
+
+ [$entity_id, $entity_type] = $primary_entities();
+ // Arrange
+ $affiliation = Affiliation::query()->create([
+ 'role_id' => test()->role->id,
+ 'affiliatable_id' => $entity_id,
+ 'affiliatable_type' => $entity_type,
+ 'type' => $affiliation_type,
+ ]);
+
+ // Assert
+ expect($affiliation->affiliated_ids)->toContain(test()->test_character->character_id)
+ ->when($entity_type === CorporationInfo::class, function ($collection) {
+ $collection->toContain(test()->test_character->corporation_id);
+ })
+ ->when($entity_type === AllianceInfo::class, function ($collection) {
+ $collection->toContain(test()->test_character->alliance_id);
+ });
+
+})->with('primary entities')->with('affiliation types');
+
+describe('relationship tests', function () {
+
+ beforeEach(function () {
+ Affiliation::query()->create([
+ 'role_id' => test()->role->id,
+ 'affiliatable_id' => test()->test_character->character_id,
+ 'affiliatable_type' => CharacterInfo::class,
+ 'type' => AffiliationType::ALLOWED->value,
+ ]);
+ });
+
+ it('has role relationship', function () {
+ // Arrange
+ $affiliation = Affiliation::first();
+
+ // Assert
+ expect($affiliation->role)->toBeInstanceOf(Role::class);
+ });
+
+ it('has affiliatable relationship', function () {
+ // Arrange
+ $affiliation = Affiliation::first();
+
+ // Assert
+ expect($affiliation->affiliatable)->toBeInstanceOf(CharacterInfo::class);
+ });
+});
diff --git a/tests/Unit/Models/CharacterUserTest.php b/tests/Unit/Models/CharacterUserTest.php
new file mode 100644
index 0000000..25bd5e0
--- /dev/null
+++ b/tests/Unit/Models/CharacterUserTest.php
@@ -0,0 +1,9 @@
+test_user;
+
+ $character_user = $test_user->character_users->first();
+
+ expect($character_user->character->character_id)->toBe($this->test_character->character_id);
+});
diff --git a/tests/Unit/Models/RoleMembershipTest.php b/tests/Unit/Models/RoleMembershipTest.php
new file mode 100644
index 0000000..4c2250e
--- /dev/null
+++ b/tests/Unit/Models/RoleMembershipTest.php
@@ -0,0 +1,24 @@
+ 'test role']);
+
+ RoleMembership::query()->create([
+ 'role_id' => $role->id,
+ 'entity_id' => test()->test_character->corporation_id,
+ 'entity_type' => CorporationInfo::class,
+ ]);
+
+ // Act
+ $role_membership = RoleMembership::first();
+
+ // Assert
+ expect($role_membership->role->name)->toEqual('test role');
+
+});
diff --git a/tests/Unit/Models/RoleModelTest.php b/tests/Unit/Models/RoleModelTest.php
index ad6af78..c1232d4 100644
--- a/tests/Unit/Models/RoleModelTest.php
+++ b/tests/Unit/Models/RoleModelTest.php
@@ -24,10 +24,11 @@
* SOFTWARE.
*/
+use Seatplus\Auth\Enums\RoleType;
+use Seatplus\Auth\Models\AccessControl\RoleMembership;
use Seatplus\Auth\Models\Permissions\Affiliation;
use Seatplus\Auth\Models\Permissions\Permission;
use Seatplus\Auth\Models\Permissions\Role;
-use Seatplus\Eveapi\Models\Character\CharacterInfo;
use Seatplus\Eveapi\Models\Corporation\CorporationInfo;
beforeEach(function () {
@@ -85,108 +86,16 @@
});
it('has default type attribute', function () {
- expect(test()->role->fresh()->type)->toEqual('manual');
+ expect(test()->role->fresh()->type)->toEqual(RoleType::MANUAL);
});
-it('has acl affiliations', function () {
- test()->role->acl_affiliations()->create([
- 'affiliatable_id' => test()->test_character->character_id,
- 'affiliatable_type' => CharacterInfo::class,
- ]);
-
- expect(test()->role->acl_affiliations->first()->affiliatable::class)->toEqual(CharacterInfo::class);
-});
-
-it('has acl moderators', function () {
- test()->role->acl_affiliations()->create([
- 'affiliatable_id' => test()->test_character->character_id,
- 'affiliatable_type' => CharacterInfo::class,
- 'can_moderate' => true,
- ]);
-
- expect(test()->role->acl_affiliations->isEmpty())->toBeTrue();
-
- expect(test()->role->moderators->first()->affiliatable::class)->toEqual(CharacterInfo::class);
-});
-
-it('has acl members', function () {
- test()->role->members()->create([
- 'user_id' => test()->test_user->id,
- 'status' => 'member',
- ]);
-
- expect(test()->role->members->isNotEmpty())->toBeTrue();
-});
-
-test('one can add member', function () {
- test()->role->activateMember(test()->test_user);
-
- expect(test()->role->members->isNotEmpty())->toBeTrue();
-});
-
-test('one can pause member', function () {
- test()->role->activateMember(test()->test_user);
-
- expect(test()->role->members->isNotEmpty())->toBeTrue();
-
- test()->role->pauseMember(test()->test_user);
-
- expect(test()->role->refresh()->members->isEmpty())->toBeTrue();
-});
-
-test('one can remove member', function () {
- test()->role->activateMember(test()->test_user);
-
- expect(test()->role->members->isNotEmpty())->toBeTrue();
+it('has role memberships', function () {
- test()->role->removeMember(test()->test_user);
-
- expect(test()->role->refresh()->members->isEmpty())->toBeTrue();
-});
-
-it('throws error if unaffiliated user wants to join', function () {
- $role = Role::create(['name' => 'test', 'type' => 'on-request']);
-
- test()->expectExceptionMessage('User is not allowed for this access control group');
-
- $role->activateMember(test()->test_user);
-});
-
-it('throws error if one tries to join waitlist on invalid role', function () {
- test()->expectExceptionMessage('Only on-request control groups do have a waitlist');
-
- test()->role->joinWaitlist(test()->test_user);
-});
-
-it('throws error if unaffiliated user tries to join waitlist', function () {
- $role = Role::create(['name' => 'test', 'type' => 'on-request']);
-
- test()->expectExceptionMessage('User is not allowed for this access control group');
-
- $role->joinWaitlist(test()->test_user);
-});
-
-test('user can join waitlist', function () {
- $role = Role::create(['name' => 'test', 'type' => 'on-request']);
-
- $role->acl_affiliations()->create([
- 'affiliatable_id' => test()->test_character->character_id,
- 'affiliatable_type' => CharacterInfo::class,
- ]);
-
- $role->joinWaitlist(test()->test_user);
-
- expect($role->refresh()->acl_members()->whereStatus('waitlist')->first()->user_id)->toEqual(test()->test_user->id);
-});
-
-test('one can get moderator ids', function () {
- $role = Role::create(['name' => 'test', 'type' => 'on-request']);
-
- $role->acl_affiliations()->create([
- 'affiliatable_id' => test()->test_character->character_id,
- 'affiliatable_type' => CharacterInfo::class,
- 'can_moderate' => true,
+ RoleMembership::query()->create([
+ 'role_id' => test()->role->id,
+ 'entity_id' => test()->test_character->corporation_id,
+ 'entity_type' => CorporationInfo::class,
]);
- expect($role->refresh()->isModerator(test()->test_user))->toBeTrue();
+ expect(test()->role->role_memberships->first()->entity)->toBeInstanceOf(CorporationInfo::class);
});
diff --git a/tests/Unit/Observers/ApplicationObserverTest.php b/tests/Unit/Observers/ApplicationObserverTest.php
index a0cfbd9..6526821 100644
--- a/tests/Unit/Observers/ApplicationObserverTest.php
+++ b/tests/Unit/Observers/ApplicationObserverTest.php
@@ -5,8 +5,11 @@
use Seatplus\Eveapi\Models\Character\CharacterInfo;
it('flushes cache after creation', function (User|CharacterInfo $entity) {
- Cache::shouldReceive('tags')->with(['characters_with_missing_scopes', test()->test_user->id])->andReturnSelf();
- Cache::shouldReceive('flush')->once();
+ $user_id = test()->test_user->id;
+
+ Cache::shouldReceive('forget')
+ ->once()
+ ->with("user_permissions_{$user_id}");
$entity->application()->create([
'corporation_id' => test()->test_character->corporation->corporation_id,
diff --git a/tests/Unit/Observers/CharacterAffiliationObserverTest.php b/tests/Unit/Observers/CharacterAffiliationObserverTest.php
index 2eebaba..364e1de 100644
--- a/tests/Unit/Observers/CharacterAffiliationObserverTest.php
+++ b/tests/Unit/Observers/CharacterAffiliationObserverTest.php
@@ -1,11 +1,15 @@
test_user)
->active->toBeTrue()
->characters->toHaveCount(1);
- $character_affiliation = \Seatplus\Eveapi\Models\Character\CharacterAffiliation::firstWhere('character_id', $this->test_user->characters->first()->character_id);
+ $character_affiliation = CharacterAffiliation::firstWhere('character_id', $this->test_user->characters->first()->character_id);
// doomheim the character
$character_affiliation->corporation_id = 1000001;
@@ -20,7 +24,7 @@
$user->main_character_id = test()->test_character->character_id;
$user->save();
- $character_user = \Seatplus\Auth\Models\CharacterUser::factory()
+ $character_user = CharacterUser::factory()
->create(['user_id' => test()->test_user->id]);
expect(test()->test_user->refresh())
@@ -30,7 +34,7 @@
->main_character_id->not()->toBe($character_user->character_id);
// doomheim the character
- $character_affiliation = \Seatplus\Eveapi\Models\Character\CharacterAffiliation::firstWhere('character_id', $character_user->character_id);
+ $character_affiliation = CharacterAffiliation::firstWhere('character_id', $character_user->character_id);
$character_affiliation->corporation_id = 1000001;
$character_affiliation->save();
@@ -46,7 +50,7 @@
$user->main_character_id = test()->test_character->character_id;
$user->save();
- $character_user = \Seatplus\Auth\Models\CharacterUser::factory()
+ $character_user = CharacterUser::factory()
->create(['user_id' => test()->test_user->id]);
expect(test()->test_user->refresh())
@@ -55,14 +59,14 @@
->main_character_id->toBeInt()->toBe(test()->test_character->character_id)
->main_character_id->not()->toBe($character_user->character_id);
- expect(\Seatplus\Auth\Models\User::all())->toHaveCount(1);
+ expect(User::all())->toHaveCount(1);
// doomheim the character
- $character_affiliation = \Seatplus\Eveapi\Models\Character\CharacterAffiliation::firstWhere('character_id', test()->test_character->character_id);
+ $character_affiliation = CharacterAffiliation::firstWhere('character_id', test()->test_character->character_id);
$character_affiliation->corporation_id = 1000001;
$character_affiliation->save();
- expect(\Seatplus\Auth\Models\User::all())->toHaveCount(2);
+ expect(User::all())->toHaveCount(2);
// original user should still be active
expect($user->refresh())
@@ -71,7 +75,7 @@
->main_character_id->toBeInt()->toBe($character_user->character_id)
->characters->toHaveCount(1);
- expect(\Seatplus\Auth\Models\User::firstWhere('main_character_id', '<>', $character_user->character_id))
+ expect(User::firstWhere('main_character_id', '<>', $character_user->character_id))
->active->toBeFalsy()
->main_character_id->toBeInt()->toBe(test()->test_character->character_id)
->main_character_id->toBeInt()->not()->toBe($character_user->character_id)
diff --git a/tests/Unit/Observers/SsoScopeObserverTest.php b/tests/Unit/Observers/SsoScopeObserverTest.php
index 97f3b5e..09db1ae 100644
--- a/tests/Unit/Observers/SsoScopeObserverTest.php
+++ b/tests/Unit/Observers/SsoScopeObserverTest.php
@@ -5,8 +5,12 @@
use Seatplus\Eveapi\Models\SsoScopes;
it('flushes cache after creation', function () {
- Cache::shouldReceive('tags')->with(['characters_with_missing_scopes'])->andReturnSelf();
- Cache::shouldReceive('flush')->once();
+
+ $user_id = test()->test_user->id;
+
+ Cache::shouldReceive('forget')
+ ->once()
+ ->with("user_permissions_{$user_id}");
SsoScopes::factory()->create();
});
@@ -14,8 +18,11 @@
it('flushes cache after updated', function () {
Event::fakeFor(fn () => SsoScopes::factory()->create());
- Cache::shouldReceive('tags')->with(['characters_with_missing_scopes'])->andReturnSelf();
- Cache::shouldReceive('flush')->once();
+ $user_id = test()->test_user->id;
+
+ Cache::shouldReceive('forget')
+ ->once()
+ ->with("user_permissions_{$user_id}");
$ssoScopes = SsoScopes::first();
$ssoScopes->morphable_id = faker()->randomNumber();
@@ -25,8 +32,11 @@
it('flushes cache after deleted', function () {
Event::fakeFor(fn () => SsoScopes::factory()->create());
- Cache::shouldReceive('tags')->with(['characters_with_missing_scopes'])->andReturnSelf();
- Cache::shouldReceive('flush')->once();
+ $user_id = test()->test_user->id;
+
+ Cache::shouldReceive('forget')
+ ->once()
+ ->with("user_permissions_{$user_id}");
$ssoScopes = SsoScopes::first();
$ssoScopes->delete();
diff --git a/tests/Unit/Requests/RoleRequestTest.php b/tests/Unit/Requests/RoleRequestTest.php
new file mode 100644
index 0000000..b4d8742
--- /dev/null
+++ b/tests/Unit/Requests/RoleRequestTest.php
@@ -0,0 +1,75 @@
+rules());
+
+ return $validator->passes();
+}
+
+dataset('role request', [
+ fn () => [
+ 'role_id' => 1,
+ 'affiliated' => [
+ [
+ 'entity_id' => 1,
+ 'entity_type' => 'corporation',
+ 'affiliation_type' => AffiliationType::cases()[fake()->randomElement([0, 1, 2])]->value,
+ ],
+ ],
+ 'assigned' => [
+ [
+ 'entity_id' => 1,
+ 'entity_type' => fake()->randomElement(['corporation', 'alliance']),
+ 'can_moderate' => fake()->boolean(),
+ ],
+ ],
+ ],
+]);
+
+it('can validate role request', function ($data) {
+
+ expect(validate($data))->toBeTrue();
+})->with('role request');
+
+it('fails when role_id is missing', function ($data) {
+ unset($data['role_id']);
+
+ expect(validate($data))->toBeFalse();
+})->with('role request');
+
+it('does not fail when affiliated is missing', function ($data) {
+ unset($data['affiliated']);
+
+ expect(validate($data))->toBeTrue();
+})->with('role request');
+
+it('fails when affiliated.*.entity_id is missing', function ($data) {
+ unset($data['affiliated'][0]['entity_id']);
+
+ expect(validate($data))->toBeFalse();
+})->with('role request');
+
+it('fails when affiliation_type is not in ENUM', function ($data) {
+
+ $data['affiliated'][0]['affiliation_type'] = 'not in enum';
+
+ expect(validate($data))->toBeFalse();
+})->with('role request');
+
+it('fails when assigned.*.entity_type is not corporation or alliance', function ($data) {
+
+ $data['assigned'][0]['entity_type'] = 'not corporation or alliance';
+
+ expect(validate($data))->toBeFalse();
+})->with('role request');
+
+it('validates when assigned is missing', function ($data) {
+ unset($data['assigned']);
+
+ expect(validate($data))->toBeTrue();
+})->with('role request');
diff --git a/tests/Unit/Services/AuthenticationServiceTest.php b/tests/Unit/Services/AuthenticationServiceTest.php
new file mode 100644
index 0000000..9abb303
--- /dev/null
+++ b/tests/Unit/Services/AuthenticationServiceTest.php
@@ -0,0 +1,76 @@
+authMock = mock(Guard::class);
+ $this->sessionMock = mock(Session::class);
+ $this->authenticationService = new AuthenticationService($this->authMock, $this->sessionMock);
+});
+
+afterEach(function () {
+ Mockery::close();
+});
+
+it('logs in user successfully', function () {
+ $user = mock(User::class);
+ $this->authMock->shouldReceive('login')->with($user, true)->andReturnNull();
+
+ $result = $this->authenticationService->loginUser($user);
+
+ expect($result)->toBeTrue();
+});
+
+it('fails to log in user and reports exception', function () {
+ $user = mock(User::class);
+ $this->authMock->shouldReceive('login')->with($user, true)->andThrow(new Exception);
+
+ $result = $this->authenticationService->loginUser($user);
+
+ expect($result)->toBeFalse();
+});
+
+it('sets intended URL', function () {
+ $url = 'http://example.com';
+ Redirect::shouldReceive('setIntendedUrl')->with($url)->once();
+
+ $this->authenticationService->setIntendedUrl($url);
+});
+
+it('flashes a message to the session', function () {
+ $type = 'error';
+ $message = 'An error occurred';
+ $this->sessionMock->shouldReceive('flash')->with($type, $message)->once();
+
+ $this->authenticationService->flashMessage($type, $message);
+});
+
+it('retrieves and removes a session value', function () {
+ $key = 'step_up';
+ $value = 'some_value';
+ $this->sessionMock->shouldReceive('pull')->with($key)->andReturn($value);
+
+ $result = $this->authenticationService->getSessionValue($key);
+
+ expect($result)->toBe($value);
+});
+
+it('checks if user is authenticated', function () {
+ $this->authMock->shouldReceive('check')->andReturn(true);
+
+ $result = $this->authenticationService->isUserAuthenticated();
+
+ expect($result)->toBeTrue();
+});
+
+it('retrieves the previous URL from the session', function () {
+ $previousUrl = 'http://example.com/previous';
+ $this->sessionMock->shouldReceive('previousUrl')->andReturn($previousUrl);
+
+ $result = $this->authenticationService->getPreviousUrl();
+
+ expect($result)->toBe($previousUrl);
+});
diff --git a/tests/Unit/Services/ConvertClassToPermissionStringServiceTest.php b/tests/Unit/Services/ConvertClassToPermissionStringServiceTest.php
deleted file mode 100644
index fdef543..0000000
--- a/tests/Unit/Services/ConvertClassToPermissionStringServiceTest.php
+++ /dev/null
@@ -1,8 +0,0 @@
-toBe('assets');
-});
diff --git a/tests/Unit/Services/GetAffiliatedIdsServiceTest.php b/tests/Unit/Services/GetAffiliatedIdsServiceTest.php
deleted file mode 100644
index 73efb46..0000000
--- a/tests/Unit/Services/GetAffiliatedIdsServiceTest.php
+++ /dev/null
@@ -1,122 +0,0 @@
-role = Role::create(['name' => faker()->name]);
- test()->permission = Permission::create(['name' => faker()->company]);
-
- test()->role->givePermissionTo(test()->permission);
- test()->role->activateMember(test()->test_user);
-
- test()->affiliationsDto = new \Seatplus\Auth\Services\Dtos\AffiliationsDto(
- user: test()->test_user,
- permissions: [test()->permission->name]
- );
-
- \Illuminate\Support\Facades\Queue::fake();
-
- expect(test()->test_character->corporation->alliance_id)->not()->toBeNull();
- // {character_id: 1, corporation_id: A, alliance_id: B}
-
- test()->secondary_character = CharacterInfo::factory()->create();
-
- CharacterAffiliation::query()
- ->updateOrCreate([
- 'character_id' => test()->secondary_character->character_id,
- ], [
- 'corporation_id' => test()->test_character->corporation->corporation_id,
- 'alliance_id' => test()->test_character->corporation->alliance_id,
- ]);
-
- // {character_id: 2, corporation_id: A, alliance_id: B}
-
- test()->tertiary_character = CharacterInfo::factory()->create();
-
- CharacterAffiliation::query()
- ->updateOrCreate([
- 'character_id' => test()->tertiary_character->character_id,
- ], [
- 'alliance_id' => test()->test_character->corporation->alliance_id,
- ]);
-
- // {character_id: 3, corporation_id: C, alliance_id: B}
-
- // delete all other character_affiliations
- CharacterAffiliation::query()
- ->whereNotIn('character_id', [
- test()->test_character->character_id,
- test()->secondary_character->character_id,
- test()->tertiary_character->character_id,
- ])->delete();
-
- // {character_id: 1, corporation_id: A, alliance_id: B}
- // {character_id: 2, corporation_id: A, alliance_id: B}
- // {character_id: 3, corporation_id: C, alliance_id: B}
- expect(test()->tertiary_character->corporation->corporation_id)
- ->not()->toBe(test()->secondary_character->corporation->corporation_id)
- ->and(test()->tertiary_character->alliance->alliance_id)
- ->toBe(test()->test_character->alliance->alliance_id);
-});
-
-it('returns inverse affiliated_ids via GetAffiliatedIdsService', function () {
- test()->createAffiliation(
- test()->role,
- test()->secondary_character->character_id,
- CharacterInfo::class,
- 'inverse'
- );
-
- $affiliated_ids = GetAffiliatedIdsService::make(test()->affiliationsDto)
- ->getQuery()
- ->pluck('affiliated_id');
-
- // {character_id: 1, corporation_id: A, alliance_id: B}
- // {character_id: 2, corporation_id: A, alliance_id: B}
- // {character_id: 3, corporation_id: C, alliance_id: B}
- // result: [1,3]
- expect($affiliated_ids)
- ->toHaveCount(2)
- ->toBeCollection()
- ->contains(test()->test_character->character_id)->toBeTrue()
- ->contains(test()->secondary_character->character_id)->toBeFalse()
- ->contains(test()->tertiary_character->character_id)->toBeTrue();
-});
-
-it('returns allowed ids from affiliated corporation but not the forbidden character_id', function () {
- test()->createAffiliation(
- test()->role,
- test()->secondary_character->corporation->corporation_id,
- CorporationInfo::class,
- 'allowed'
- );
-
- test()->createAffiliation(
- test()->role,
- test()->secondary_character->character_id,
- CharacterInfo::class,
- 'forbidden'
- );
-
- $allowed_ids = GetAffiliatedIdsService::make(test()->affiliationsDto)
- ->getQuery()
- ->pluck('affiliated_id');
-
- // {character_id: 1, corporation_id: A, alliance_id: B}
- // {character_id: 2, corporation_id: A, alliance_id: B}
- // {character_id: 3, corporation_id: C, alliance_id: B}
- // result: [1,A]
- expect($allowed_ids)
- ->toHaveCount(2)
- ->toBeCollection()
- ->contains(test()->test_character->character_id)->toBeTrue()
- ->contains(test()->secondary_character->character_id)->toBeFalse()
- ->contains(test()->secondary_character->corporation->corporation_id)->toBeTrue()
- ->contains(test()->tertiary_character->character_id)->toBeFalse()
- ->contains(test()->tertiary_character->corporation->corporation_id)->toBeFalse();
-});
diff --git a/tests/Unit/Services/GetAllowedAffiliatedIdsServiceTest.php b/tests/Unit/Services/GetAllowedAffiliatedIdsServiceTest.php
deleted file mode 100644
index 83e228c..0000000
--- a/tests/Unit/Services/GetAllowedAffiliatedIdsServiceTest.php
+++ /dev/null
@@ -1,131 +0,0 @@
-role = Role::create(['name' => faker()->name]);
- test()->permission = Permission::create(['name' => faker()->company]);
-
- test()->role->givePermissionTo(test()->permission);
- test()->role->activateMember(test()->test_user);
-
- test()->affiliationsDto = new \Seatplus\Auth\Services\Dtos\AffiliationsDto(
- user: test()->test_user,
- permissions: [test()->permission->name]
- );
-
- \Illuminate\Support\Facades\Queue::fake();
-
- expect(test()->test_character->corporation->alliance_id)->not()->toBeNull();
- // {character_id: 1, corporation_id: A, alliance_id: B}
-
- test()->secondary_character = CharacterInfo::factory()->create();
-
- CharacterAffiliation::query()
- ->updateOrCreate([
- 'character_id' => test()->secondary_character->character_id,
- ], [
- 'corporation_id' => test()->test_character->corporation->corporation_id,
- 'alliance_id' => test()->test_character->corporation->alliance_id,
- ]);
-
- // {character_id: 2, corporation_id: A, alliance_id: B}
-
- test()->tertiary_character = CharacterInfo::factory()->create();
-
- CharacterAffiliation::query()
- ->updateOrCreate([
- 'character_id' => test()->tertiary_character->character_id,
- ], [
- 'alliance_id' => test()->test_character->corporation->alliance_id,
- ]);
-
- // {character_id: 3, corporation_id: C, alliance_id: B}
-
- // {character_id: 1, corporation_id: A, alliance_id: B}
- // {character_id: 2, corporation_id: A, alliance_id: B}
- // {character_id: 3, corporation_id: C, alliance_id: B}
- expect(test()->tertiary_character->corporation->corporation_id)
- ->not()->toBe(test()->secondary_character->corporation->corporation_id);
-
- expect(test()->tertiary_character->alliance->alliance_id)
- ->toBe(test()->test_character->alliance->alliance_id);
-});
-
-it('returns allowed ids from affiliated character', function () {
- test()->createAffiliation(
- test()->role,
- test()->secondary_character->character_id,
- CharacterInfo::class,
- 'allowed'
- );
-
- $allowed_ids = GetAllowedAffiliatedIdsService::make(test()->affiliationsDto)
- ->getQuery();
-
- // {character_id: 1, corporation_id: A, alliance_id: B}
- // {character_id: 2, corporation_id: A, alliance_id: B}
- // {character_id: 3, corporation_id: C, alliance_id: B}
- // result: [2]
- expect($allowed_ids->pluck('affiliated_id'))
- ->toHaveCount(1)
- ->toBeCollection()
- ->contains(test()->secondary_character->character_id)->toBeTrue();
-});
-
-it('returns allowed ids from affiliated corporation', function () {
- test()->createAffiliation(
- test()->role,
- test()->secondary_character->corporation->corporation_id,
- CorporationInfo::class,
- 'allowed'
- );
-
- $allowed_ids = GetAllowedAffiliatedIdsService::make(test()->affiliationsDto)
- ->getQuery()
- ->pluck('affiliated_id');
-
- // {character_id: 1, corporation_id: A, alliance_id: B}
- // {character_id: 2, corporation_id: A, alliance_id: B}
- // {character_id: 3, corporation_id: C, alliance_id: B}
- // result: [1, 2, A]
- expect($allowed_ids)
- ->toHaveCount(3)
- ->toBeCollection()
- ->contains(test()->test_character->character_id)->toBeTrue()
- ->contains(test()->secondary_character->character_id)->toBeTrue()
- ->contains(test()->secondary_character->corporation->corporation_id)->toBeTrue()
- ->contains(test()->tertiary_character->character_id)->toBeFalse();
-});
-
-it('returns allowed ids from affiliated alliance', function () {
- test()->createAffiliation(
- test()->role,
- test()->secondary_character->corporation->alliance_id,
- AllianceInfo::class,
- 'allowed'
- );
-
- $allowed_ids = GetAllowedAffiliatedIdsService::make(test()->affiliationsDto)
- ->getQuery()
- ->pluck('affiliated_id');
-
- // {character_id: 1, corporation_id: A, alliance_id: B}
- // {character_id: 2, corporation_id: A, alliance_id: B}
- // {character_id: 3, corporation_id: C, alliance_id: B}
- // result: [1, 2, 3, A, C, B]
- expect($allowed_ids)
- ->toBeCollection()
- ->contains(test()->test_character->character_id)->toBeTrue()
- ->contains(test()->secondary_character->character_id)->toBeTrue()
- ->contains(test()->tertiary_character->character_id)->toBeTrue()
- ->contains(test()->test_character->corporation->corporation_id)->toBeTrue()
- ->contains(test()->secondary_character->corporation->corporation_id)->toBeTrue()
- ->contains(test()->tertiary_character->corporation->corporation_id)->toBeTrue();
-});
diff --git a/tests/Unit/Services/GetForbiddenAffiliatedIdsServiceTest.php b/tests/Unit/Services/GetForbiddenAffiliatedIdsServiceTest.php
deleted file mode 100644
index ea4f744..0000000
--- a/tests/Unit/Services/GetForbiddenAffiliatedIdsServiceTest.php
+++ /dev/null
@@ -1,169 +0,0 @@
-role = Role::create(['name' => faker()->name]);
- test()->permission = Permission::create(['name' => faker()->company]);
-
- test()->role->givePermissionTo(test()->permission);
- test()->role->activateMember(test()->test_user);
-
- test()->affiliationsDto = new \Seatplus\Auth\Services\Dtos\AffiliationsDto(
- user: test()->test_user,
- permissions: [test()->permission->name]
- );
-
- \Illuminate\Support\Facades\Queue::fake();
-
- expect(test()->test_character->corporation->alliance_id)->not()->toBeNull();
- // {character_id: 1, corporation_id: A, alliance_id: B}
-
- test()->secondary_character = CharacterInfo::factory()->create();
-
- CharacterAffiliation::query()
- ->updateOrCreate([
- 'character_id' => test()->secondary_character->character_id,
- ], [
- 'corporation_id' => test()->test_character->corporation->corporation_id,
- 'alliance_id' => test()->test_character->corporation->alliance_id,
- ]);
-
- // {character_id: 2, corporation_id: A, alliance_id: B}
-
- test()->tertiary_character = CharacterInfo::factory()->create();
-
- CharacterAffiliation::query()
- ->updateOrCreate([
- 'character_id' => test()->tertiary_character->character_id,
- ], [
- 'alliance_id' => test()->test_character->corporation->alliance_id,
- ]);
-
- // {character_id: 3, corporation_id: C, alliance_id: B}
-
- // {character_id: 1, corporation_id: A, alliance_id: B}
- // {character_id: 2, corporation_id: A, alliance_id: B}
- // {character_id: 3, corporation_id: C, alliance_id: B}
- expect(test()->tertiary_character->corporation->corporation_id)
- ->not()->toBe(test()->secondary_character->corporation->corporation_id)
- ->and(test()->tertiary_character->alliance->alliance_id)
- ->toBe(test()->test_character->alliance->alliance_id);
-});
-
-it('returns forbidden ids from forbidden character', function () {
- test()->createAffiliation(
- test()->role,
- test()->secondary_character->character_id,
- CharacterInfo::class,
- 'forbidden'
- );
-
- $forbidden_ids = GetForbiddenAffiliatedIdService::make(test()->affiliationsDto)
- ->getQuery()
- ->pluck('forbidden_id');
-
- // {character_id: 1, corporation_id: A, alliance_id: B}
- // {character_id: 2, corporation_id: A, alliance_id: B}
- // {character_id: 3, corporation_id: C, alliance_id: B}
- // result: [2]
- expect($forbidden_ids)
- ->toHaveCount(1)
- ->toBeCollection()
- ->contains(test()->secondary_character->character_id)->toBeTrue();
-});
-
-it('returns forbidden ids from forbidden corporation but not owned character_id', function () {
- test()->createAffiliation(
- test()->role,
- test()->secondary_character->corporation->corporation_id,
- CorporationInfo::class,
- 'forbidden'
- );
-
- $forbidden_ids = GetForbiddenAffiliatedIdService::make(test()->affiliationsDto)
- ->getQuery()
- ->pluck('forbidden_id');
-
- // {character_id: 1, corporation_id: A, alliance_id: B}
- // {character_id: 2, corporation_id: A, alliance_id: B}
- // {character_id: 3, corporation_id: C, alliance_id: B}
- // result: [2, A]
- expect($forbidden_ids)
- ->toHaveCount(2)
- ->toBeCollection()
- ->contains(test()->test_character->character_id)->toBeFalse()
- ->contains(test()->secondary_character->character_id)->toBeTrue()
- ->contains(test()->secondary_character->corporation->corporation_id)->toBeTrue()
- ->contains(test()->tertiary_character->character_id)->toBeFalse();
-});
-
-// TODO own corporation
-it('returns forbidden ids from forbidden alliance', function () {
- test()->createAffiliation(
- test()->role,
- test()->secondary_character->corporation->alliance_id,
- AllianceInfo::class,
- 'forbidden'
- );
-
- $forbidden_ids = GetForbiddenAffiliatedIdService::make(test()->affiliationsDto)
- ->getQuery()
- ->pluck('forbidden_id');
-
- // {character_id: 1, corporation_id: A, alliance_id: B}
- // {character_id: 2, corporation_id: A, alliance_id: B}
- // {character_id: 3, corporation_id: C, alliance_id: B}
- // result: [A,B,2,3,C]
- expect($forbidden_ids)
- ->toBeCollection()
- ->toHaveCount(5)
- ->contains(test()->test_character->character_id)->toBeFalse()
- ->contains(test()->secondary_character->character_id)->toBeTrue()
- ->contains(test()->tertiary_character->character_id)->toBeTrue()
- ->contains(test()->test_character->corporation->corporation_id)->toBeTrue()
- ->contains(test()->secondary_character->corporation->corporation_id)->toBeTrue()
- ->contains(test()->tertiary_character->corporation->corporation_id)->toBeTrue()
- ->contains(test()->secondary_character->corporation->alliance_id)->toBeTrue();
-});
-
-it('returns forbidden ids from forbidden alliance but not owned corporation via role', function () {
- test()->createAffiliation(
- test()->role,
- test()->secondary_character->corporation->alliance_id,
- AllianceInfo::class,
- 'forbidden'
- );
-
- test()->affiliationsDto->corporation_roles = ['Director'];
-
- \Seatplus\Eveapi\Models\Character\CharacterRole::factory()->create([
- 'character_id' => test()->test_character->character_id,
- 'roles' => test()->affiliationsDto->corporation_roles,
- ]);
-
- $forbidden_ids = GetForbiddenAffiliatedIdService::make(test()->affiliationsDto)
- ->getQuery()
- ->pluck('forbidden_id');
-
- // {character_id: 1, corporation_id: A, alliance_id: B}
- // {character_id: 2, corporation_id: A, alliance_id: B}
- // {character_id: 3, corporation_id: C, alliance_id: B}
- // result: [B,2,3,C]
- expect($forbidden_ids)
- ->toBeCollection()
- ->toHaveCount(4)
- ->contains(test()->test_character->character_id)->toBeFalse()
- ->contains(test()->secondary_character->character_id)->toBeTrue()
- ->contains(test()->tertiary_character->character_id)->toBeTrue()
- ->contains(test()->test_character->corporation->corporation_id)->toBeFalse()
- ->contains(test()->secondary_character->corporation->corporation_id)->toBeFalse()
- ->contains(test()->tertiary_character->corporation->corporation_id)->toBeTrue()
- ->contains(test()->secondary_character->corporation->alliance_id)->toBeTrue();
-});
diff --git a/tests/Unit/Services/GetInvertedAffiliatedIdsServiceTest.php b/tests/Unit/Services/GetInvertedAffiliatedIdsServiceTest.php
deleted file mode 100644
index a2224c1..0000000
--- a/tests/Unit/Services/GetInvertedAffiliatedIdsServiceTest.php
+++ /dev/null
@@ -1,152 +0,0 @@
-role = Role::create(['name' => faker()->name]);
- test()->permission = Permission::create(['name' => faker()->company]);
-
- test()->role->givePermissionTo(test()->permission);
- test()->role->activateMember(test()->test_user);
-
- test()->affiliationsDto = new \Seatplus\Auth\Services\Dtos\AffiliationsDto(
- user: test()->test_user,
- permissions: [test()->permission->name]
- );
-
- \Illuminate\Support\Facades\Queue::fake();
-
- expect(test()->test_character->corporation->alliance_id)->not()->toBeNull();
- // {character_id: 1, corporation_id: A, alliance_id: B}
-
- test()->secondary_character = CharacterInfo::factory()->create();
-
- CharacterAffiliation::query()
- ->updateOrCreate([
- 'character_id' => test()->secondary_character->character_id,
- ], [
- 'corporation_id' => test()->test_character->corporation->corporation_id,
- 'alliance_id' => test()->test_character->corporation->alliance_id,
- ]);
-
- // {character_id: 2, corporation_id: A, alliance_id: B}
-
- test()->tertiary_character = CharacterInfo::factory()->create();
-
- CharacterAffiliation::query()
- ->updateOrCreate([
- 'character_id' => test()->tertiary_character->character_id,
- ], [
- 'alliance_id' => test()->test_character->corporation->alliance_id,
- ]);
-
- // {character_id: 3, corporation_id: C, alliance_id: B}
-
- // delete all other character_affiliations
- CharacterAffiliation::query()
- ->whereNotIn('character_id', [
- test()->test_character->character_id,
- test()->secondary_character->character_id,
- test()->tertiary_character->character_id,
- ])->delete();
-
- // {character_id: 1, corporation_id: A, alliance_id: B}
- // {character_id: 2, corporation_id: A, alliance_id: B}
- // {character_id: 3, corporation_id: C, alliance_id: B}
- expect(test()->tertiary_character->corporation->corporation_id)
- ->not()->toBe(test()->secondary_character->corporation->corporation_id)
- ->and(test()->tertiary_character->alliance->alliance_id)
- ->toBe(test()->test_character->alliance->alliance_id);
-});
-
-it('returns inversed ids from affiliated character', function () {
- test()->createAffiliation(
- test()->role,
- test()->secondary_character->character_id,
- CharacterInfo::class,
- 'inverse'
- );
-
- $allowed_ids = GetInvertedAffiliatedIdsService::make(test()->affiliationsDto)
- ->getQuery()
- ->pluck('affiliated_id');
-
- // {character_id: 1, corporation_id: A, alliance_id: B}
- // {character_id: 2, corporation_id: A, alliance_id: B}
- // {character_id: 3, corporation_id: C, alliance_id: B}
- // result: [1,3]
- expect($allowed_ids)
- ->toHaveCount(2)
- ->toBeCollection()
- ->contains(test()->test_character->character_id)->toBeTrue()
- ->contains(test()->secondary_character->character_id)->toBeFalse()
- ->contains(test()->tertiary_character->character_id)->toBeTrue();
-});
-
-it('returns inverted ids from affiliated corporation', function () {
- test()->createAffiliation(
- test()->role,
- test()->secondary_character->corporation->corporation_id,
- CorporationInfo::class,
- 'inverse'
- );
-
- $allowed_ids = GetInvertedAffiliatedIdsService::make(test()->affiliationsDto)
- ->getQuery()
- ->pluck('affiliated_id');
-
- // {character_id: 1, corporation_id: A, alliance_id: B}
- // {character_id: 2, corporation_id: A, alliance_id: B}
- // {character_id: 3, corporation_id: C, alliance_id: B}
- // result: [3,C]
- expect($allowed_ids)
- ->toHaveCount(2)
- ->toBeCollection()
- ->contains(test()->test_character->character_id)->toBeFalse()
- ->contains(test()->secondary_character->character_id)->toBeFalse()
- ->contains(test()->secondary_character->corporation->corporation_id)->toBeFalse()
- ->contains(test()->tertiary_character->character_id)->toBeTrue()
- ->contains(test()->tertiary_character->corporation->corporation_id)->toBeTrue();
-});
-
-it('returns inverted ids from affiliated alliance', function () {
- test()->createAffiliation(
- test()->role,
- test()->secondary_character->corporation->alliance_id,
- AllianceInfo::class,
- 'inverse'
- );
-
- $random_affiliation = CharacterAffiliation::factory()->withAlliance()->create();
-
- // {character_id: 4, corporation_id: X, alliance_id: Y}
- expect($random_affiliation->alliance_id)->not()->toBe(test()->secondary_character->corporation->alliance_id);
-
- $allowed_ids = GetInvertedAffiliatedIdsService::make(test()->affiliationsDto)
- ->getQuery()
-
- ->pluck('affiliated_id');
-
- // {character_id: 1, corporation_id: A, alliance_id: B}
- // {character_id: 2, corporation_id: A, alliance_id: B}
- // {character_id: 3, corporation_id: C, alliance_id: B}
- // {character_id: 4, corporation_id: X, alliance_id: Y}
- // result: [4, X, Y]
- expect($allowed_ids)
- ->toBeCollection()
- ->toHaveCount(3)
- ->contains(test()->test_character->character_id)->toBeFalse()
- ->contains(test()->secondary_character->character_id)->toBeFalse()
- ->contains(test()->tertiary_character->character_id)->toBeFalse()
- ->contains(test()->test_character->corporation->corporation_id)->toBeFalse()
- ->contains(test()->secondary_character->corporation->corporation_id)->toBeFalse()
- ->contains(test()->tertiary_character->corporation->corporation_id)->toBeFalse()
- ->contains(test()->tertiary_character->alliance->alliance_id)->toBeFalse()
- ->contains($random_affiliation->alliance_id)->toBeTrue();
-});
diff --git a/tests/Unit/Services/GetOwnedAffiliatedIdsServiceTest.php b/tests/Unit/Services/GetOwnedAffiliatedIdsServiceTest.php
deleted file mode 100644
index 5cc89b2..0000000
--- a/tests/Unit/Services/GetOwnedAffiliatedIdsServiceTest.php
+++ /dev/null
@@ -1,63 +0,0 @@
-role = Role::create(['name' => faker()->name]);
- test()->permission = Permission::create(['name' => faker()->company]);
-
- test()->role->givePermissionTo(test()->permission);
-
- test()->affiliationsDto = new \Seatplus\Auth\Services\Dtos\AffiliationsDto(
- user: test()->test_user,
- permissions: [test()->permission->name],
- corporation_roles: ['Director']
- );
-
- \Illuminate\Support\Facades\Queue::fake();
-
- expect(test()->test_character->corporation->alliance_id)->not()->toBeNull();
- // {character_id: 1, corporation_id: A, alliance_id: B}
-});
-
-it('returns own character ids', function () {
- test()->createAffiliation(
- test()->role,
- test()->test_character->character_id,
- CharacterInfo::class,
- 'inverse'
- );
-
- $allowed_ids = GetOwnedAffiliatedIdsService::make(test()->affiliationsDto)
- ->getQuery()
- ->pluck('affiliated_id');
-
- // {character_id: 1, corporation_id: A, alliance_id: B}
- // result: [1]
- expect($allowed_ids)
- ->toHaveCount(1)
- ->toBeCollection()
- ->contains(test()->test_character->character_id)->toBeTrue();
-});
-
-it('returns owned character_id and corporation_id if corp role exists', function () {
- \Seatplus\Eveapi\Models\Character\CharacterRole::factory()->create([
- 'character_id' => test()->test_character->character_id,
- 'roles' => test()->affiliationsDto->corporation_roles,
- ]);
-
- $allowed_ids = GetOwnedAffiliatedIdsService::make(test()->affiliationsDto)
- ->getQuery()
- ->pluck('affiliated_id');
-
- // {character_id: 1, corporation_id: A, alliance_id: B}
- // result: [3, A]
- expect($allowed_ids)
- ->toHaveCount(2)
- ->toBeCollection()
- ->contains(test()->test_character->character_id)->toBeTrue()
- ->contains(test()->test_character->corporation->corporation_id)->toBeTrue();
-});
diff --git a/tests/Unit/Services/Permissions/UserPermissionServiceTest.php b/tests/Unit/Services/Permissions/UserPermissionServiceTest.php
new file mode 100644
index 0000000..864c8d2
--- /dev/null
+++ b/tests/Unit/Services/Permissions/UserPermissionServiceTest.php
@@ -0,0 +1,104 @@
+test_user;
+
+ // Act
+ $user_permission_service = new UserPermissionService;
+
+ $result = $user_permission_service->get($user);
+
+ // Assert
+ expect($result['owned_character_ids'])->toBe($user->characters->pluck('character_id')->toArray());
+});
+
+it('builds corporation_roles from user', function () {
+
+ // Arrange
+ $user = test()->test_user;
+
+ CharacterRole::query()->delete();
+
+ CharacterRole::factory()->create([
+ 'character_id' => test()->test_character->character_id,
+ 'roles' => ['Director', 'Personnel Manager'],
+ ]);
+
+ // Act
+ $user_permission_service = new UserPermissionService;
+
+ $result = $user_permission_service->get($user);
+
+ // Assert
+ expect($result['corporation_roles'])
+ ->toHaveCount(2)
+ ->toHaveKey('Director')
+ ->toHaveKey('Personnel Manager')
+ ->and($result['corporation_roles']['Director'])->toContain(test()->test_character->corporation_id)
+ ->and($result['corporation_roles']['Personnel Manager'])->toContain(test()->test_character->corporation_id);
+});
+
+it('builds permissions from user', function () {
+
+ // Arrange
+ $user = test()->test_user;
+
+ $role1 = Role::create(['name' => Str::random()]);
+ $role2 = Role::create(['name' => Str::random()]);
+
+ // create 3 permissions
+ $permissions = collect([
+ Permission::create(['name' => Str::random()]),
+ Permission::create(['name' => Str::random()]),
+ Permission::create(['name' => Str::random()]),
+ ]);
+
+ // sync first two permissions to role1
+ $role1->syncPermissions($permissions->take(2));
+
+ // sync last 2 permission to role2
+ $role2->syncPermissions($permissions->slice(1));
+
+ $user->assignRole([$role1, $role2]);
+
+ $role_permission_object_service = mock(RolePermissionObjectService::class, function (MockInterface $mock) use ($permissions) {
+
+ $result1 = collect([
+ $permissions[0]->name => [1, 2, 3],
+ $permissions[1]->name => [4, 5, 6],
+ ]);
+
+ $result2 = collect([
+ $permissions[1]->name => [7, 8, 9],
+ $permissions[2]->name => [10, 11, 12],
+ ]);
+
+ $mock->shouldReceive('get')
+ // ->with($role1)
+ ->andReturn($result1, $result2);
+ });
+
+ // Act
+ $user_permission_service = new UserPermissionService($role_permission_object_service);
+
+ $result = $user_permission_service->get($user);
+
+ // Assert
+ expect($result['permissions'])
+ ->toHaveCount(3)
+ ->toHaveKeys($permissions->pluck('name')->toArray())
+ ->and($result['permissions'][$permissions[0]->name])->toBe([1, 2, 3])
+ ->and($result['permissions'][$permissions[1]->name])->toContain(4, 5, 6, 7, 8, 9)
+ ->and($result['permissions'][$permissions[2]->name])->toBe([10, 11, 12]);
+});
+
+describe('cache user permissions', function () {})->only();
diff --git a/tests/Unit/Services/Roles/AbstractRoleServiceTest.php b/tests/Unit/Services/Roles/AbstractRoleServiceTest.php
new file mode 100644
index 0000000..0651c1f
--- /dev/null
+++ b/tests/Unit/Services/Roles/AbstractRoleServiceTest.php
@@ -0,0 +1,98 @@
+role = Role::create(['name' => 'test']);
+ $this->role = $this->role->refresh();
+ $this->service = new class($this->role) extends AbstractRoleService
+ {
+ public function syncMembers(): void {}
+
+ public function canView(User $user): bool
+ {
+ return false;
+ }
+
+ public function canJoin(User $user): bool
+ {
+ return false;
+ }
+
+ public function canModerate(User $user): bool
+ {
+ return false;
+ }
+ };
+});
+
+it('affiliates role to corporation and getting role on test user', function () {
+
+ // Arrange
+ $test_character = test()->test_character;
+ $corporation_id = $test_character->corporation_id;
+ $alliance_id = $test_character->alliance_id;
+
+ expect(Affiliation::count())->toEqual(0);
+
+ // act
+ $this->service->syncAffiliateManyEntities(
+ new AffiliationData($corporation_id, 'corporation', AffiliationType::ALLOWED),
+ new AffiliationData($test_character->character_id, 'character', AffiliationType::ALLOWED),
+ new AffiliationData($alliance_id, 'alliance', AffiliationType::ALLOWED),
+ );
+
+ // Test
+ expect(Affiliation::count())->toEqual(3)
+ ->and(Affiliation::first()->affiliatable_id)->toEqual($corporation_id)
+ ->and(Affiliation::first()->affiliatable_type)->toEqual(CorporationInfo::class)
+ ->and(Affiliation::first()->type)->toEqual(AffiliationType::ALLOWED->value);
+});
+
+it('returns early when setting same role type', function () {
+
+ // Arrange
+ $this->role->type = RoleType::AUTOMATIC;
+ $this->role->save();
+
+ // Act
+ $automated_role_service = new AutomaticRoleService($this->role);
+ $automated_role_service->automaticallyAssignRoleTo(
+ new CriteriaData(1, 'corporation'),
+ );
+
+ // Assert
+ expect($this->role->refresh()->type)->toEqual(RoleType::AUTOMATIC);
+});
+
+it('sets role type to', function (RoleType $role_type) {
+
+ // Act
+ $this->service->setRoleType($role_type);
+
+ // Assert
+ expect($this->role->refresh()->type)->toEqual($role_type);
+})->with([
+ RoleType::AUTOMATIC,
+ RoleType::ON_REQUEST,
+ RoleType::OPT_IN,
+ RoleType::MANUAL,
+]);
+
+it('rename role', function () {
+
+ // Act
+ $this->service->updateRoleName('new name');
+
+ // Assert
+ expect($this->role->refresh()->name)->toEqual('new name');
+});
diff --git a/tests/Unit/Services/Roles/AutomaticRoleServiceTest.php b/tests/Unit/Services/Roles/AutomaticRoleServiceTest.php
new file mode 100644
index 0000000..8769a5a
--- /dev/null
+++ b/tests/Unit/Services/Roles/AutomaticRoleServiceTest.php
@@ -0,0 +1,124 @@
+role = Role::create(['name' => 'test']);
+ $this->role = $this->role->refresh();
+ $this->service = new AutomaticRoleService($this->role);
+});
+
+describe('assigning', function () {
+ it('role to corporation and getting role on test user', function () {
+
+ $test_character = test()->test_character;
+ $corporation_id = $test_character->corporation_id;
+
+ expect(test()->test_user->refresh()->hasRole($this->role->name))->toBeFalse();
+
+ $this->service->automaticallyAssignRoleTo(
+ new CriteriaData($corporation_id, 'corporation'),
+ );
+
+ expect(RoleMembership::get())->toHaveCount(2) // User and Corporation
+ ->and(test()->test_user->refresh()->hasRole($this->role->name))->toBeTrue();
+
+ });
+
+ it('role to alliance', function () {
+
+ $test_character = test()->test_character;
+ $alliance_id = $test_character->alliance_id;
+
+ $this->service->automaticallyAssignRoleTo(
+ new CriteriaData($alliance_id, 'alliance'),
+ );
+
+ expect(test()->test_user->refresh()->hasRole($this->role->name))->toBeTrue();
+ });
+
+ it('role to corporation and alliance', function () {
+
+ $test_character = test()->test_character;
+ $corporation_id = $test_character->corporation_id;
+ $alliance_id = $test_character->alliance_id;
+
+ $this->service->automaticallyAssignRoleTo(
+ new CriteriaData($corporation_id, 'corporation'),
+ new CriteriaData($alliance_id, 'alliance'),
+ );
+
+ expect(RoleMembership::get())->toHaveCount(3) // User, Corporation and Alliance
+ ->and(test()->test_user->refresh()->hasRole($this->role->name))->toBeTrue();
+ });
+});
+
+describe('handling Members', function () {
+ it('removes role from user if nothing is assigned', function () {
+
+ $test_user = test()->test_user;
+
+ $test_user->assignRole($this->role);
+
+ expect(test()->test_user->refresh()->hasRole($this->role->name))->toBeTrue();
+
+ $this->service->automaticallyAssignRoleTo();
+
+ expect(test()->test_user->refresh()->hasRole($this->role->name))->toBeFalse()
+ ->and(RoleMembership::query()->count())->toBe(0);
+ });
+
+ it('works also with role in constructor', function () {
+ /** @var Role $role */
+ $role = Role::create(['name' => 'constructor test']);
+
+ $service = new AutomaticRoleService($role);
+
+ $test_character = test()->test_character;
+ $corporation_id = $test_character->corporation_id;
+
+ $service->automaticallyAssignRoleTo(
+ new CriteriaData($corporation_id, 'corporation'),
+ );
+
+ expect(RoleMembership::get())->toHaveCount(2) // User and Corporation
+ ->and(test()->test_user->refresh()->hasRole($role->name))->toBeTrue();
+ });
+});
+
+it('sets role type to automatic', function () {
+
+ expect($this->role->type)->toBe(RoleType::MANUAL);
+
+ $this->service->setRoleType(RoleType::AUTOMATIC);
+
+ expect($this->role->type)->toBe(RoleType::AUTOMATIC);
+});
+
+it('cannot view', function () {
+ expect($this->service->canView(test()->test_user))->toBeFalse();
+});
+
+it('can view when meets criteria', function () {
+ $test_character = test()->test_character;
+ $corporation_id = $test_character->corporation_id;
+
+ $this->service->automaticallyAssignRoleTo(
+ new CriteriaData($corporation_id, 'corporation'),
+ );
+
+ expect($this->service->canView(test()->test_user))->toBeTrue();
+});
+
+it('cannot join', function () {
+ expect($this->service->canJoin(test()->test_user))->toBeFalse();
+});
+
+it('cannot moderate', function () {
+ expect($this->service->canModerate(test()->test_user))->toBeFalse();
+});
diff --git a/tests/Unit/Services/Roles/BaseRoleServiceTest.php b/tests/Unit/Services/Roles/BaseRoleServiceTest.php
new file mode 100644
index 0000000..7641d08
--- /dev/null
+++ b/tests/Unit/Services/Roles/BaseRoleServiceTest.php
@@ -0,0 +1,63 @@
+role = Role::create(['name' => faker()->name()]);
+ $this->role = $this->role->refresh();
+ $this->service = new BaseRoleService;
+});
+
+describe('make', function () {
+ test('service can be made role', function () {
+
+ $service = BaseRoleService::make($this->role);
+
+ expect($service)->toBeInstanceOf(BaseRoleService::class);
+ });
+
+ test('service can be made role by id', function () {
+
+ $service = BaseRoleService::make($this->role->id);
+
+ expect($service)->toBeInstanceOf(BaseRoleService::class);
+ });
+
+ it('throws exception if role not found', function () {
+
+ BaseRoleService::make('abc');
+ })->expectException(RoleDoesNotExist::class);
+});
+
+it('can get automatic role service', function () {
+
+ $service = BaseRoleService::make($this->role)->automatic();
+
+ expect($service)->toBeInstanceOf(AutomaticRoleService::class);
+});
+
+it('work with the various role types', function (RoleType $role_type) {
+ // Arrange
+ $this->role->update(['type' => $role_type->value]);
+
+ $service = BaseRoleService::make($this->role->refresh());
+
+ $service->for($this->role);
+
+ // act
+ $service->handleMembers();
+
+ $can_view = $service->canView(test()->test_user);
+ $can_join = $service->canJoin(test()->test_user);
+ $can_moderate = $service->canModerate(test()->test_user);
+
+ // assert
+ expect($can_view)->toBeFalse()
+ ->and($can_join)->toBeFalse()
+ ->and($can_moderate)->toBeFalse();
+
+})->with(RoleType::cases());
diff --git a/tests/Unit/Services/Roles/ManualRoleServiceTest.php b/tests/Unit/Services/Roles/ManualRoleServiceTest.php
new file mode 100644
index 0000000..1c7dba0
--- /dev/null
+++ b/tests/Unit/Services/Roles/ManualRoleServiceTest.php
@@ -0,0 +1,80 @@
+role = Role::create(['name' => 'test']);
+ $this->role = $this->role->refresh();
+
+ $this->service = new ManualRoleService($this->role);
+});
+
+it('can add a member', function () {
+ $test_user = test()->test_user;
+
+ $this->service->addMember($test_user);
+
+ expect(RoleMembership::query()->count())->toBe(1)
+ ->and(RoleMembership::first())->entity_type->toBe(User::class);
+});
+
+it('can remove a member', function () {
+ $test_user = test()->test_user;
+
+ $this->service->addMember($test_user);
+
+ expect(RoleMembership::query()->count())->toBe(1);
+
+ $this->service->removeMember($test_user);
+
+ expect(RoleMembership::query()->count())->toBe(0);
+});
+
+it('can add user as moderator and does not change status', function () {
+ $test_user = test()->test_user;
+
+ $this->service->addMember($test_user);
+
+ expect(RoleMembership::first())->status->toBe(RoleMembershipStatus::ACTIVE->value);
+
+ $this->service->setModerator($test_user);
+
+ expect(RoleMembership::first())->status->toBe(RoleMembershipStatus::ACTIVE->value)
+ ->can_moderate->toBeTrue();
+
+ $this->service->setModerator($test_user, false);
+
+ expect(RoleMembership::first())->status->toBe(RoleMembershipStatus::ACTIVE->value)
+ ->can_moderate->toBeFalse();
+});
+
+it('syncs members', function () {
+ $test_user = test()->test_user;
+
+ $this->service->addMember($test_user);
+
+ expect(RoleMembership::first())->status->toBe(RoleMembershipStatus::ACTIVE->value);
+
+ $this->service->syncMembers();
+
+ expect(RoleMembership::first())->status->toBe(RoleMembershipStatus::ACTIVE->value);
+});
+
+it('can view', function () {
+ expect($this->service->canView(test()->test_user))->toBeFalse();
+});
+
+it('can join', function () {
+ expect($this->service->canJoin(test()->test_user))->toBeFalse();
+});
+
+it('can moderate', function () {
+
+ $this->service->setModerator(test()->test_user);
+
+ expect($this->service->canModerate(test()->test_user))->toBeTrue();
+});
diff --git a/tests/Unit/Services/Roles/OnRequestRoleServiceTest.php b/tests/Unit/Services/Roles/OnRequestRoleServiceTest.php
new file mode 100644
index 0000000..ebee34d
--- /dev/null
+++ b/tests/Unit/Services/Roles/OnRequestRoleServiceTest.php
@@ -0,0 +1,275 @@
+role = Role::create(['name' => 'test', 'type' => RoleType::ON_REQUEST->value]);
+ $this->role = $this->role->refresh();
+
+ $this->service = new OnRequestRoleService($this->role);
+});
+
+describe('adding criteria for role application', function () {
+ it('adds criteria for role application with valid entities', function () {
+ // Arrange
+ $corporation_id = test()->test_character->corporation_id;
+ $alliance_id = test()->test_character->alliance_id;
+
+ // Act
+ $this->service->addCriteriaForRoleApplication(
+ new CriteriaData($corporation_id, 'corporation'),
+ new CriteriaData($alliance_id, 'alliance'),
+ );
+
+ // Assert
+ expect(RoleMembership::query()->count())->toBe(2);
+ });
+
+ it('throws validation exception for invalid entities', function () {
+ // Act
+ $this->service->addCriteriaForRoleApplication(
+ new CriteriaData(test()->test_character->corporation_id, 'corporation'),
+ new CriteriaData(test()->test_character->alliance_id, 'invalid'),
+ );
+ })->throws(ValueError::class);
+
+ it('resets criterias', function () {
+ // Arrange
+
+ // create random role membership that acts as criteria
+ RoleMembership::query()->create([
+ 'role_id' => $this->role->id,
+ 'entity_id' => 12345,
+ 'entity_type' => CorporationInfo::class,
+ ]);
+
+ // create user role membership that acts as member and should not be deleted
+ RoleMembership::query()->create([
+ 'role_id' => $this->role->id,
+ 'entity_id' => test()->test_user->id,
+ 'entity_type' => User::class,
+ ]);
+
+ // Act
+ $this->service->addCriteriaForRoleApplication(
+ new CriteriaData(test()->test_character->corporation_id, 'corporation'),
+ new CriteriaData(test()->test_character->alliance_id, 'alliance'),
+ );
+
+ // Assert
+ expect(RoleMembership::query()->count())->toBe(3)
+ ->and(RoleMembership::query()->where('entity_type', User::class)->count())->toBe(1)
+ ->and(RoleMembership::query()->where('entity_id', 12345)->count())->toBe(0);
+ });
+});
+
+it('cannot submit application if no criteria is set', function () {
+ // arrange
+ $user = test()->test_user;
+
+ // act
+ $this->service->submitApplicationForRole($user);
+
+ // assert
+
+})->throws(Exception::class, 'User does not meet criteria to join role');
+
+it('submits application for role', function () {
+ // arrange
+ $user = test()->test_user;
+
+ $this->service->addCriteriaForRoleApplication(
+ new CriteriaData(test()->test_character->corporation_id, 'corporation'),
+ );
+
+ // act
+ $this->service->submitApplicationForRole($user);
+
+ // assert
+ expect(RoleMembership::query()->where('role_id', $this->role->id)->where('status', 'pending')->count())->toBe(1)
+ ->and(RoleMembership::query()->where('role_id', $this->role->id)->where('entity_id', $user->id)->first())
+ ->status->toBe(RoleMembershipStatus::PENDING->value)
+ ->entity_id->toBe($user->id);
+});
+
+it('approving application for role', function () {
+ // arrange
+ $user = test()->test_user;
+ $this->service->addCriteriaForRoleApplication(
+ new CriteriaData(test()->test_character->corporation_id, 'corporation'),
+ );
+
+ // act
+ $this->service->approveApplicationForRole($user);
+
+ // assert
+ expect(RoleMembership::query()->where('role_id', $this->role->id)->count())->toBe(2)
+ ->and(RoleMembership::query()->where('role_id', $this->role->id)->where('entity_id', $user->id)->first())
+ ->status->toBe(RoleMembershipStatus::ACTIVE->value)
+ ->entity_id->toBe($user->id);
+});
+
+it('throws exception when approving application for role with no criteria', function () {
+ // arrange
+ $user = test()->test_user;
+
+ // act
+ $this->service->approveApplicationForRole($user);
+
+ // assert
+})->throws(Exception::class, 'User does not meet criteria to join role');
+
+it('denies application for role', function () {
+ // arrange
+ $user = test()->test_user;
+ RoleMembership::query()->create([
+ 'role_id' => $this->role->id,
+ 'entity_id' => $user->id,
+ 'entity_type' => User::class,
+ 'status' => RoleMembershipStatus::PENDING->value,
+ ]);
+
+ // act
+ $this->service->denyApplication($user);
+
+ // assert
+ expect(RoleMembership::query()->where('role_id', $this->role->id)->where('entity_id', $user->id)->count())->toBe(0);
+});
+
+it('removes application for role', function () {
+ // arrange
+ $user = test()->test_user;
+ RoleMembership::query()->create([
+ 'role_id' => $this->role->id,
+ 'entity_id' => $user->id,
+ 'entity_type' => User::class,
+ 'status' => RoleMembershipStatus::PENDING->value,
+ ]);
+
+ // act
+ $this->service->removeApplication($user);
+
+ // assert
+ expect(RoleMembership::query()->where('role_id', $this->role->id)->where('entity_id', $user->id)->count())->toBe(0);
+});
+
+it('sets moderator status for user', function (bool $can_moderate) {
+ // arrange
+ $user = test()->test_user;
+
+ // act
+ $this->service->setModerator($user, $can_moderate);
+
+ // assert
+ expect(RoleMembership::query()->where('role_id', $this->role->id)->where('entity_id', $user->id)->first())
+ ->can_moderate->toBe($can_moderate);
+})->with([
+ true,
+ false,
+]);
+
+describe('sync', function () {
+ it('removes members outside criteria', function () {
+ // Arrange
+ $user = test()->test_user;
+
+ RoleMembership::query()->create([
+ 'role_id' => $this->role->id,
+ 'entity_id' => $user->id,
+ 'entity_type' => User::class,
+ 'status' => RoleMembershipStatus::ACTIVE->value,
+ ]);
+
+ // Act
+ $this->service->syncMembers();
+
+ // Assert
+ expect(RoleMembership::count())->toBe(0);
+
+ });
+
+ it('does not removes members within criteria', function () {
+ // Arrange
+
+ // set criteria
+ $test_character = test()->test_character;
+ $test_character = test()->test_character;
+ RoleMembership::query()->create([
+ 'role_id' => $this->role->id,
+ 'entity_id' => $test_character->corporation_id,
+ 'entity_type' => CorporationInfo::class,
+ ]);
+
+ // Add Member
+ $user = test()->test_user;
+ RoleMembership::query()->create([
+ 'role_id' => $this->role->id,
+ 'entity_id' => $user->id,
+ 'entity_type' => User::class,
+ 'status' => RoleMembershipStatus::ACTIVE->value,
+ ]);
+
+ expect(RoleMembership::count())->toBe(2);
+
+ //
+ // Act
+ $this->service->syncMembers();
+
+ // Assert
+ expect(RoleMembership::count())->toBe(2);
+ });
+});
+
+describe('can', function () {
+
+ beforeEach(function () {
+ $this->service->addCriteriaForRoleApplication(
+ new CriteriaData(test()->test_character->corporation_id, 'corporation'),
+ new CriteriaData(test()->test_character->alliance_id, 'alliance'),
+ );
+ });
+
+ it('can view', function () {
+ // Act
+ $result = $this->service->canView(test()->test_user);
+
+ // Assert
+ expect($result)->toBeTrue();
+ });
+
+ it('can join', function () {
+ // Act
+ $result = $this->service->canJoin(test()->test_user);
+
+ // Assert
+ expect($result)->toBeTrue();
+ });
+
+});
+
+it('cannot moderate', function () {
+ // Act
+ $result = $this->service->canModerate(test()->test_user);
+
+ // Assert
+ expect($result)->toBeFalse();
+});
+
+it('can moderate', function () {
+ // Arrange
+ $user = test()->test_user;
+ $this->service->setModerator($user);
+
+ // Act
+ $result = $this->service->canModerate($user);
+
+ // Assert
+ expect($result)->toBeTrue();
+});
diff --git a/tests/Unit/Services/Roles/OptInRoleServiceTest.php b/tests/Unit/Services/Roles/OptInRoleServiceTest.php
new file mode 100644
index 0000000..19a18b6
--- /dev/null
+++ b/tests/Unit/Services/Roles/OptInRoleServiceTest.php
@@ -0,0 +1,103 @@
+role = Role::create(['name' => 'test']);
+ $this->role = $this->role->refresh();
+
+ $this->service = new OptInRoleService($this->role);
+});
+
+it('can add criteria', function () {
+
+ $this->service->addCriteriaForRole(
+ new CriteriaData(1, 'corporation'),
+ );
+
+ expect(RoleMembership::query()->count())->toBe(1)
+ ->and(RoleMembership::first())->entity_type->toBe(CorporationInfo::class);
+});
+
+it('can join role', function () {
+ $test_user = test()->test_user;
+
+ $this->service->addCriteriaForRole(
+ new CriteriaData(test()->test_character->corporation_id, 'corporation'),
+ );
+
+ $this->service->joinRole($test_user);
+
+ expect(RoleMembership::query()->count())->toBe(2);
+});
+
+it('can leave role', function () {
+ $test_user = test()->test_user;
+ $this->service->addCriteriaForRole(
+ new CriteriaData(test()->test_character->corporation_id, 'corporation'),
+ );
+
+ $this->service->joinRole($test_user);
+
+ expect(RoleMembership::query()->count())->toBe(2);
+
+ $this->service->leaveRole($test_user);
+
+ expect(RoleMembership::query()->count())->toBe(1);
+});
+
+it('syncs members', function () {
+ $test_user = test()->test_user;
+ $this->service->addCriteriaForRole(
+ new CriteriaData(test()->test_character->corporation_id, 'corporation'),
+ );
+
+ $this->service->joinRole($test_user);
+
+ // RoleMember
+ $role_member = RoleMembership::query()->where('entity_type', User::class)->get();
+
+ expect($role_member->count())->toBe(1)
+ ->and($role_member->first())->status->toBe(RoleMembershipStatus::ACTIVE->value);
+
+ // remove criteria makes the user not meet the criteria anymore
+ $this->service->addCriteriaForRole(
+ new CriteriaData(1234, 'corporation'),
+ );
+
+ $this->service->syncMembers();
+
+ expect(RoleMembership::count())->toBe(1);
+});
+
+describe('it can', function () {
+ beforeEach(function () {
+ $this->service->addCriteriaForRole(
+ new CriteriaData(test()->test_character->corporation_id, 'corporation'),
+ );
+ });
+
+ it('can view', function () {
+ $test_user = test()->test_user;
+
+ expect($this->service->canView($test_user))->toBeTrue();
+ });
+
+ it('can join', function () {
+ $test_user = test()->test_user;
+
+ expect($this->service->canJoin($test_user))->toBeTrue();
+ });
+});
+
+it('cannot moderate', function () {
+ $test_user = test()->test_user;
+
+ expect($this->service->canModerate($test_user))->toBeFalse();
+});
diff --git a/tests/Unit/Services/Roles/RolePermissionObjectServiceTest.php b/tests/Unit/Services/Roles/RolePermissionObjectServiceTest.php
new file mode 100644
index 0000000..4f9c29d
--- /dev/null
+++ b/tests/Unit/Services/Roles/RolePermissionObjectServiceTest.php
@@ -0,0 +1,38 @@
+ Str::random()]);
+
+ // create 3 permissions
+ $permissions = [
+ Permission::create(['name' => Str::random()]),
+ Permission::create(['name' => Str::random()]),
+ Permission::create(['name' => Str::random()]),
+ ];
+
+ $role->syncPermissions($permissions);
+
+ $mock = mock(RoleAffiliatedIdsService::class, function ($mock) {
+ $mock->shouldReceive('get')->andReturn([1, 2, 3]);
+ });
+
+ // Act
+ $role_permission_object_service = new RolePermissionObjectService($mock);
+
+ $result = $role_permission_object_service->get($role);
+
+ // Assert
+ expect($result)->toHaveCount(3)
+ ->toHaveKeys([$permissions[0]->name, $permissions[1]->name, $permissions[2]->name])
+ ->and($result[$permissions[0]->name])->toBe([1, 2, 3])
+ ->and($result[$permissions[1]->name])->toBe([1, 2, 3])
+ ->and($result[$permissions[2]->name])->toBe([1, 2, 3]);
+});
diff --git a/tests/Unit/Services/SsoScopes/GlobalSsoScopesServiceTest.php b/tests/Unit/Services/SsoScopes/GlobalSsoScopesServiceTest.php
new file mode 100644
index 0000000..43803f2
--- /dev/null
+++ b/tests/Unit/Services/SsoScopes/GlobalSsoScopesServiceTest.php
@@ -0,0 +1,17 @@
+set($scopes);
+
+ $sso_scopes = SsoScopes::query()
+ ->where('type', 'global')
+ ->first();
+
+ expect($sso_scopes->selected_scopes)->toBe($scopes);
+});
diff --git a/tests/Unit/UpdateRefreshTokenActionTest.php b/tests/Unit/UpdateRefreshTokenActionTest.php
index 7e81b5b..9595be7 100644
--- a/tests/Unit/UpdateRefreshTokenActionTest.php
+++ b/tests/Unit/UpdateRefreshTokenActionTest.php
@@ -31,7 +31,7 @@
test('create refresh token', function () {
$eve_data = createEveUser(test()->test_user->id);
- $action = new UpdateRefreshTokenAction();
+ $action = new UpdateRefreshTokenAction;
Event::fakeFor(fn () => $action($eve_data));
test()->assertDatabaseHas('refresh_tokens', [
@@ -45,7 +45,7 @@
// create RefreshToken
$eveUser = createEveUser();
- $action = new UpdateRefreshTokenAction();
+ $action = new UpdateRefreshTokenAction;
Event::fakeFor(fn () => $action($eveUser));
test()->assertDatabaseHas('refresh_tokens', [
@@ -71,7 +71,7 @@
// create RefreshToken
$eveUser = createEveUser();
- $action = new UpdateRefreshTokenAction();
+ $action = new UpdateRefreshTokenAction;
Event::fakeFor(fn () => $action($eveUser));
test()->assertDatabaseHas('refresh_tokens', [
@@ -98,7 +98,7 @@
// create RefreshToken
$eveUser = createEveUser();
- $action = new UpdateRefreshTokenAction();
+ $action = new UpdateRefreshTokenAction;
Event::fakeFor(fn () => $action($eveUser));
test()->assertDatabaseHas('refresh_tokens', [