Add login fake

This commit is contained in:
philipp lang 2022-02-19 12:20:11 +01:00
parent 54826f0f73
commit 99e34e75d5
14 changed files with 228 additions and 319 deletions

View File

@ -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

View File

@ -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;
}

View File

@ -0,0 +1,28 @@
<?php
namespace Zoomyboy\LaravelNami\Authentication;
use Illuminate\Support\Facades\Facade;
/**
* @method static void assertNotLoggedIn()
* @method static void success(int $mglnr, string $password)
* @method static void failed(int $mglnr, string $password)
* @method static void assertLoggedInWith(int $mglnr, string $password)
* @method static void assertNotLoggedInWith(int $mglnr, string $password)
* @method static void assertLoggedIn()
*/
class Auth extends Facade {
public static function getFacadeAccessor() {
return Authenticator::class;
}
public static function fake(): Authenticator
{
static::swap($fake = app(FakeCookie::class));
return $fake;
}
}

View File

@ -0,0 +1,12 @@
<?php
namespace Zoomyboy\LaravelNami\Authentication;
use Illuminate\Http\Client\PendingRequest;
abstract class Authenticator {
abstract public function login(int $mglnr, string $password): self;
abstract public function http(): PendingRequest;
}

View File

@ -0,0 +1,96 @@
<?php
namespace Zoomyboy\LaravelNami\Authentication;
use Carbon\Carbon;
use GuzzleHttp\Cookie\FileCookieJar;
use Illuminate\Http\Client\PendingRequest;
use Illuminate\Support\Facades\Http;
use PHPUnit\Framework\Assert;
use Zoomyboy\LaravelNami\LoginException;
class FakeCookie extends Authenticator {
private array $validAccounts = [];
public ?array $invalidAccounts = null;
public ?array $authenticated = null;
public function login(int $mglnr, string $password): self
{
$authenticated = collect($this->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
{
}
}

View File

@ -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);
}
}

View File

@ -1,215 +0,0 @@
<?php
namespace Zoomyboy\LaravelNami\Authentication;
use Illuminate\Auth\GuardHelpers;
use Illuminate\Auth\SessionGuard;
use Illuminate\Cache\Repository as CacheRepository;
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Contracts\Auth\Guard;
use Illuminate\Foundation\Auth\User as AuthenticatableUser;
use Illuminate\Session\Store as SessionStore;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Str;
use Zoomyboy\LaravelNami\LoginException;
use Zoomyboy\LaravelNami\Nami;
use Zoomyboy\LaravelNami\NamiUser;
class NamiGuard {
use GuardHelpers;
protected CacheRepository $cache;
/** @var array<int, callable> $loginCallbacks */
public static array $loginCallbacks = [];
/** @var array<int, string> */
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<int, string> $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<string, string> $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<string, string> $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<string, int|string> $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<string, string> $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<string, string>
*/
private function resolveCache(): ?array
{
return $this->cache->get('namiauth-'.$this->session->get($this->getName()));
}
private function newCacheKey(): string
{
return Str::random(16);
}
}

View File

@ -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);

View File

@ -1,13 +0,0 @@
<?php
namespace Zoomyboy\LaravelNami\Cookies;
use Illuminate\Support\Facades\Facade;
class Cookie extends Facade {
public static function getFacadeAccessor() {
return 'nami.cookie';
}
}

View File

@ -1,26 +0,0 @@
<?php
namespace Zoomyboy\LaravelNami\Cookies;
class FakeCookie {
private $loggedIn = false;
public function forBackend() {
return \GuzzleHttp\Cookie\CookieJar::fromArray([], 'nami.dpsg.de');
}
public function store($cookie) {
$this->loggedIn = true;
}
public function resolve($mglnr) {
return $this->loggedIn;
}
public function isExpired(): bool
{
return false;
}
}

View File

@ -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);
}
}

View File

@ -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);
});

View File

@ -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();

View File

@ -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 [