diff --git a/phpstan.neon b/phpstan.neon index 9adc415..ad36d98 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -10,9 +10,4 @@ parameters: # The level 8 is the highest level level: 6 - ignoreErrors: - - '#Call to an undefined method Illuminate\\Contracts\\Auth\\Authenticatable::api\(\)#' - - '#Call to an undefined method Illuminate\\Contracts\\Auth\\Authenticatable::getNamiGroupId\(\)#' - - '#Illuminate\\Contracts\\Auth\\Authenticatable\|null given#' - checkMissingIterableValueType: false diff --git a/src/Api.php b/src/Api.php index beea8f6..711b070 100644 --- a/src/Api.php +++ b/src/Api.php @@ -12,7 +12,7 @@ use Illuminate\Support\Facades\Http; use Illuminate\Support\LazyCollection; use Illuminate\Support\Str; use Log; -use Zoomyboy\LaravelNami\Authentication\Cookie; +use Zoomyboy\LaravelNami\Authentication\Authenticator; use Zoomyboy\LaravelNami\Backend\Backend; use Zoomyboy\LaravelNami\Concerns\IsNamiMember; use Zoomyboy\LaravelNami\Exceptions\RightException; @@ -21,15 +21,15 @@ use Zoomyboy\LaravelNami\NamiException; class Api { public string $url = 'https://nami.dpsg.de'; - private Cookie $cookie; + private Authenticator $authenticator; - public function __construct(Cookie $cookie) + public function __construct(Authenticator $authenticator) { - $this->cookie = $cookie; + $this->authenticator = $authenticator; } public function http(): PendingRequest { - return Http::withOptions(['cookies' => $this->cookie->load()]); + return $this->authenticator->http(); } public function findNr(int $nr): Member @@ -87,27 +87,7 @@ class Api { public function login(int $mglnr, string $password): self { - if ($this->cookie->isLoggedIn()) { - return $this; - } - - $this->cookie->beforeLogin(); - - $this->http()->get($this->url.'/ica/pages/login.jsp'); - $response = $this->http()->asForm()->post($this->url.'/ica/rest/nami/auth/manual/sessionStartup', [ - 'Login' => 'API', - 'redirectTo' => './app.jsp', - 'username' => $mglnr, - 'password' => $password - ]); - - if ($response->json()['statusCode'] !== 0) { - $e = new LoginException(); - $e->setResponse($response->json()); - throw $e; - } - - $this->cookie->afterLogin(); + $this->authenticator->login($mglnr, $password); return $this; } diff --git a/src/Authentication/Auth.php b/src/Authentication/Auth.php new file mode 100644 index 0000000..b602356 --- /dev/null +++ b/src/Authentication/Auth.php @@ -0,0 +1,28 @@ +validAccounts)->search( + fn ($account) => $account['mglnr'] === $mglnr && $account['password'] === $password + ); + + if ($authenticated !== false) { + $this->authenticated = ['mglnr' => $mglnr, 'password' => $password]; + } else { + $e = new LoginException(); + $e->setResponse(['statusMessage' => "Benutzer nicht gefunden oder Passwort falsch"]); + + throw $e; + } + + return $this; + } + + public function http(): PendingRequest + { + return Http::withOptions([]); + } + + /** + * Reisters an account that can successfully login with + * the given password + * + * @param int $mglnr + * @param string $password + * + * @return void + */ + public function success(int $mglnr, string $password): void + { + $this->validAccounts[] = ['mglnr' => $mglnr, 'password' => $password]; + } + + /** + * Reisters an account that cannot login with the given password + * + * @param int $mglnr + * @param string $password + * + * @return void + */ + public function failed(int $mglnr, string $password): void + { + $this->invalidAccounts[] = ['mglnr' => $mglnr, 'password' => $password]; + } + + public function assertLoggedInWith(int $mglnr, string $password): void + { + Assert::assertSame($mglnr, data_get($this->authenticated, 'mglnr')); + Assert::assertSame($password, data_get($this->authenticated, 'password')); + } + + public function assertNotLoggedInWith(int $mglnr, string $password): void + { + Assert::assertTrue( + $mglnr !== data_get($this->authenticated, 'mglnr') + || $password !== data_get($this->authenticated, 'password'), + "Failed asserting that user {$mglnr} is not loggedd in with {$password}" + ); + } + + public function assertNotLoggedIn(): void + { + Assert::assertNull( + $this->authenticated, + 'Failed asserting that noone is logged in. Found login with '.data_get($this->authenticated, 'mglnr') + ); + } + + public function assertLoggedIn(): void + { + + } + +} diff --git a/src/Authentication/Cookie.php b/src/Authentication/MainCookie.php similarity index 52% rename from src/Authentication/Cookie.php rename to src/Authentication/MainCookie.php index d49c7dc..fca6136 100644 --- a/src/Authentication/Cookie.php +++ b/src/Authentication/MainCookie.php @@ -4,47 +4,51 @@ namespace Zoomyboy\LaravelNami\Authentication; use Carbon\Carbon; use GuzzleHttp\Cookie\FileCookieJar; +use Illuminate\Http\Client\PendingRequest; +use Illuminate\Support\Facades\Http; +use Zoomyboy\LaravelNami\LoginException; -class Cookie { +class MainCookie extends Authenticator { - public string $path = __DIR__.'/../../.cookies'; + private string $path = __DIR__.'/../../.cookies'; private FileCookieJar $cookie; + private string $url = 'https://nami.dpsg.de'; - /** - * Loads the cookie for a new request - * - * @return FileCookieJar - */ - public function load(): FileCookieJar + public function login(int $mglnr, string $password): self { - $cookieFile = $this->file() ?: $this->newFileName(); + if ($this->isLoggedIn()) { + return $this; + } - return $this->cookie = new FileCookieJar($cookieFile); - } - - /** - * Clears all cookies before logging in - * - * @return void - */ - public function beforeLogin(): void - { while ($file = $this->file()) { unlink($file); } - } - /** - * Set last login to now after login - * - * @return void - */ - public function afterLogin(): void - { + $this->http()->get($this->url.'/ica/pages/login.jsp'); + $response = $this->http()->asForm()->post($this->url.'/ica/rest/nami/auth/manual/sessionStartup', [ + 'Login' => 'API', + 'redirectTo' => './app.jsp', + 'username' => $mglnr, + 'password' => $password + ]); + + if ($response->json()['statusCode'] !== 0) { + $e = new LoginException(); + $e->setResponse($response->json()); + throw $e; + } + $this->cookie->save($this->newFileName()); + + return $this; } - public function isLoggedIn(): bool + public function http(): PendingRequest + { + return Http::withOptions(['cookies' => $this->load()]); + } + + private function isLoggedIn(): bool { if ($this->file() === null) { return false; @@ -81,6 +85,16 @@ class Cookie { return $files[0]; } + /** + * Loads the cookie for a new request + * + * @return FileCookieJar + */ + private function load(): FileCookieJar + { + $cookieFile = $this->file() ?: $this->newFileName(); + return $this->cookie = new FileCookieJar($cookieFile); + } } diff --git a/src/Authentication/NamiGuard.php b/src/Authentication/NamiGuard.php deleted file mode 100644 index 4f99b43..0000000 --- a/src/Authentication/NamiGuard.php +++ /dev/null @@ -1,215 +0,0 @@ - $loginCallbacks */ - public static array $loginCallbacks = []; - - /** @var array */ - public array $fallbacks; - - /** - * The currently authenticated user. - * - * @var ?NamiUser - */ - protected $user; - - protected SessionStore $session; - - public function __construct(SessionStore $session, CacheRepository $cache) { - $this->session = $session; - $this->cache = $cache; - } - - /** - * @param array $fallbacks - */ - public function setFallbacks(array $fallbacks): self - { - $this->fallbacks = $fallbacks; - - return $this; - } - - /** - * Set the current user. - * - * @param NamiUser|AuthenticatableUser|null $user - * @return void - */ - public function setUser($user): void - { - $this->user = $user; - } - - public function user() - { - if (! is_null($this->user)) { - return $this->user; - } - - $cache = $this->resolveCache(); - - if (!$cache) { - return null; - } - - return NamiUser::fromPayload($cache); - } - - /** - * @param array $credentials - * @param bool $remember - */ - public function attempt(array $credentials = [], bool $remember = false): bool - { - if (in_array($credentials['provider'], $this->fallbacks) && $this->loginFallback($credentials['provider'], $credentials)) { - return true; - } - - if (data_get($credentials, 'provider', '') !== 'nami') { - return false; - } - - $beforeResult = static::runBeforeLogin($credentials, $remember); - - if (!is_null($beforeResult)) { - return $beforeResult; - } - - try { - return $this->loginNami($credentials); - } catch (LoginException $e) { - return false; - } - } - - /** - * @param array $credentials - */ - public function loginFallback(string $provider, array $credentials): bool - { - $provider = auth()->createUserProvider($provider); - $user = $user = $provider->retrieveByCredentials(Arr::except($credentials, ['provider'])); - - if (!$user) { - return false; - } - - if (!$provider->validateCredentials($user, $credentials)) { - return false; - } - - $payload = [ - 'id' => $user->id, - ]; - - $this->setUser($user); - $key = $this->newCacheKey(); - Cache::forever("namiauth-{$key}", $payload); - $this->updateSession($key); - - return true; - } - - /** - * @param array $credentials - */ - public function loginNami(array $credentials): bool - { - $api = Nami::login($credentials['mglnr'], $credentials['password']); - $user = $api->findNr((int) $credentials['mglnr']); - - $payload = [ - 'credentials' => $credentials, - 'firstname' => $user->firstname, - 'lastname' => $user->lastname, - 'group_id' => $user->group_id, - ]; - - $this->setUser(NamiUser::fromPayload($payload)); - $key = $this->newCacheKey(); - Cache::forever("namiauth-{$key}", $payload); - $this->updateSession($key); - - return true; - } - - /** - * @param array $credentials - * @param bool $remember - */ - protected function runBeforeLogin(array $credentials, bool $remember): ?bool - { - foreach (static::$loginCallbacks as $callback) { - $result = $callback($credentials, $remember); - if ($result !== null) { - return $result; - } - } - - return null; - } - - protected function updateSession(string $data): void - { - $this->session->put($this->getName(), $data); - $this->session->migrate(true); - } - - /** - * @param callable $callback - */ - public static function beforeLogin(callable $callback): void - { - static::$loginCallbacks[] = $callback; - } - - public function getName(): string - { - return 'auth_key'; - } - - public function logout(): void - { - $this->session->forget($this->getName()); - $this->setUser(null); - } - - /** - * @return array - */ - private function resolveCache(): ?array - { - return $this->cache->get('namiauth-'.$this->session->get($this->getName())); - } - - private function newCacheKey(): string - { - return Str::random(16); - } - -} diff --git a/src/Backend/FakeBackend.php b/src/Backend/FakeBackend.php index 901d494..43f7516 100644 --- a/src/Backend/FakeBackend.php +++ b/src/Backend/FakeBackend.php @@ -14,9 +14,6 @@ use Zoomyboy\LaravelNami\Fakes\LoginFake; class FakeBackend { - /** - * @param string $mglnr - */ public function fakeLogin(string $mglnr): self { app(LoginFake::class)->succeeds($mglnr); diff --git a/src/Cookies/Cookie.php b/src/Cookies/Cookie.php deleted file mode 100644 index a139233..0000000 --- a/src/Cookies/Cookie.php +++ /dev/null @@ -1,13 +0,0 @@ -loggedIn = true; - } - - public function resolve($mglnr) { - return $this->loggedIn; - } - - public function isExpired(): bool - { - return false; - } - -} diff --git a/src/Nami.php b/src/Nami.php index 78e9c10..60ee4fb 100644 --- a/src/Nami.php +++ b/src/Nami.php @@ -6,7 +6,15 @@ use Illuminate\Support\Facades\Facade; /** * @method static \Zoomyboy\LaravelNami\Api login(int $mglnr, string $password) + * @method static \Zoomyboy\LaravelNami\Api fake() */ class Nami extends Facade { + protected static function getFacadeAccessor() { return 'nami.api'; } + + protected static function fake(): void + { + static::swap(ApiFake::class); + } + } diff --git a/src/Providers/NamiServiceProvider.php b/src/Providers/NamiServiceProvider.php index c8ca27e..d1cb5d1 100644 --- a/src/Providers/NamiServiceProvider.php +++ b/src/Providers/NamiServiceProvider.php @@ -7,6 +7,8 @@ use GuzzleHttp\Cookie\CookieJarInterface; use Illuminate\Support\Facades\Auth; use Illuminate\Support\ServiceProvider; use Zoomyboy\LaravelNami\Api; +use Zoomyboy\LaravelNami\Authentication\Authenticator; +use Zoomyboy\LaravelNami\Authentication\MainCookie; use Zoomyboy\LaravelNami\Authentication\NamiGuard; use Zoomyboy\LaravelNami\Backend\LiveBackend; use Zoomyboy\LaravelNami\Cookies\CacheCookie; @@ -22,6 +24,9 @@ class NamiServiceProvider extends ServiceProvider } public function register() { + $this->app->bind(Authenticator::class, function() { + return app(MainCookie::class); + }); $this->app->bind('nami.api', function() { return app(Api::class); }); diff --git a/tests/TestCase.php b/tests/TestCase.php index 8c1f3f7..15c0cc7 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -13,9 +13,6 @@ use Zoomyboy\LaravelNami\Tests\Stub\Member; class TestCase extends \Orchestra\Testbench\TestCase { - public $successJson = '{"servicePrefix":null,"methodCall":null,"response":null,"statusCode":0,"statusMessage":"","apiSessionName":"JSESSIONID","apiSessionToken":"ILBY--L4pZEjSKa39tCemens","minorNumber":2,"majorNumber":1}'; - public $bruteJson = '{"servicePrefix":null,"methodCall":null,"response":null,"statusCode":3000,"statusMessage":"Die höchste Anzahl von Login-Versuchen wurde erreicht. Ihr Konto ist für 15 Minuten gesperrt worden. Nach Ablauf dieser Zeitspanne wird ihr Zugang wieder freigegeben.","apiSessionName":"JSESSIONID","apiSessionToken":"tGlSpMMij9ruHfeiUYjO7SD2","minorNumber":0,"majorNumber":0}'; - public $wrongCredentialsJson = '{"servicePrefix":null,"methodCall":null,"response":null,"statusCode":3000,"statusMessage":"Benutzer nicht gefunden oder Passwort falsch.","apiSessionName":"JSESSIONID","apiSessionToken":"v7lrjgPBbXInJR57qJzVIJ05","minorNumber":0,"majorNumber":0}'; public function setUp(): void { parent::setUp(); diff --git a/tests/Unit/LoginTest.php b/tests/Unit/LoginTest.php index fc404fd..9c55bb9 100644 --- a/tests/Unit/LoginTest.php +++ b/tests/Unit/LoginTest.php @@ -3,6 +3,7 @@ namespace Zoomyboy\LaravelNami\Tests\Unit; use Illuminate\Support\Facades\Http; +use Zoomyboy\LaravelNami\Authentication\Auth; use Zoomyboy\LaravelNami\LoginException; use Zoomyboy\LaravelNami\Nami; use Zoomyboy\LaravelNami\Tests\TestCase; @@ -10,6 +11,10 @@ use Zoomyboy\LaravelNami\Tests\TestCase; class LoginTest extends TestCase { + public string $successJson = '{"servicePrefix":null,"methodCall":null,"response":null,"statusCode":0,"statusMessage":"","apiSessionName":"JSESSIONID","apiSessionToken":"ILBY--L4pZEjSKa39tCemens","minorNumber":2,"majorNumber":1}'; + public string $bruteJson = '{"servicePrefix":null,"methodCall":null,"response":null,"statusCode":3000,"statusMessage":"Die höchste Anzahl von Login-Versuchen wurde erreicht. Ihr Konto ist für 15 Minuten gesperrt worden. Nach Ablauf dieser Zeitspanne wird ihr Zugang wieder freigegeben.","apiSessionName":"JSESSIONID","apiSessionToken":"tGlSpMMij9ruHfeiUYjO7SD2","minorNumber":0,"majorNumber":0}'; + public string $wrongCredentialsJson = '{"servicePrefix":null,"methodCall":null,"response":null,"statusCode":3000,"statusMessage":"Benutzer nicht gefunden oder Passwort falsch.","apiSessionName":"JSESSIONID","apiSessionToken":"v7lrjgPBbXInJR57qJzVIJ05","minorNumber":0,"majorNumber":0}'; + public function test_first_successful_login(): void { Http::fake($this->fakeSuccessfulLogin()); @@ -88,6 +93,32 @@ class LoginTest extends TestCase Http::assertSentCount(2); } + public function test_it_fakes_login(): void + { + Auth::fake(); + Auth::success(12345, 'secret'); + Auth::assertNotLoggedIn(); + + Nami::login(12345, 'secret'); + + Auth::assertLoggedInWith(12345, 'secret'); + Auth::assertLoggedIn(); + Auth::assertNotLoggedInWith(12345, 'wrong'); + Http::assertSentCount(0); + } + + public function test_it_fakes_failed_login(): void + { + Auth::fake(); + Auth::failed(12345, 'wrong'); + $this->expectException(LoginException::class); + + Nami::login(12345, 'wrong'); + + Http::assertSentCount(0); + Auth::assertNotLoggedIn(); + } + private function fakeSuccessfulLogin(): array { return [