Nette\Tester: testování metod po přihlášení

Upozornění: Tohle vlákno je hodně staré a informace nemusí být platné pro současné Nette.
zimmi
Člen | 94
+
0
-

Zdravím,
pročetl jsem si vlákna týkající se testování přihlašování uživatelů a používání metod po přihlášení, ale nejsem z toho moc moudrý, proto mám několik otázek.

class UserPresenterTest extends BaseTestCase
{
	public function testLogIn()
    {
        $presenter = $this->getPresenter('Admin:Login');
        $request = new Nette\Application\Request('Admin:Login', 'POST', array(
            'action' => 'default',
            'do' => 'loginForm-submit',
        ), array(
            'username' => 'user',
            'password' => 'pass',
        ));
        $this->getContainer()->user->logout();
        $response = $presenter->run($request);

        Assert::true( $this->getContainer()->user->isLoggedIn() );
    }
}

id(new UserPresenterTest($container))->run();

Snažím se přihlásit pomocí tohoto kódu, výsledkem assertu je ale False. Nic víc na výpisu konzole nedostávám, lze Nette\Tester donutit k nějakému obsáhlejšímu výstupu?
Authenticator sahá při přihlašování do db, kde ověřuje existenci uživatele pomocí modelu userRepo, který dostane v konstruktoru. V testu mu tuto závislost musím nějak předat, nebo se předá tak jako v aplikaci? Poprvé píšu testy, budu tedy rád za jakékoliv popostrčení.

besanek
Člen | 128
+
0
-

Já osobně řeším login tak, že si vytvořím identitu. Zhruba takto.

$admin = new Nette\Security\Identity(1, 'admin', array('nick' => 'admin'));

$user = $this->getContainer()->getByType('Nette\Security\User');
$user->login($admin);
zimmi
Člen | 94
+
0
-

besanek napsal(a):

Já osobně řeším login tak, že si vytvořím identitu. Zhruba takto.

$admin = new Nette\Security\Identity(1, 'admin', array('nick' => 'admin'));

$user = $this->getContainer()->getByType('Nette\Security\User');
$user->login($admin);

Můžeš pak nějak otestovat, jestli ti přihlašovací formulář funguje tak, jak by měl fungovat?

besanek
Člen | 128
+
0
-

Ah sorry. Špatně jsem to pochopil. Myslel jsem, že chceš testovat něco, k čemu musíš být přihlášený.

Takže znovu a lépe.

Všechno se předá stejně jako v aplikaci. Dumpni si $response, jestli presenter vrací to co by měl. Tj. RedirectResponse. Pokud ti vrací TextResponse, tak si vypiš getSource() a zkontroluj, jestli v něm není náhodou vypsána nějaká chybová hláška.

zimmi
Člen | 94
+
0
-

No, asi jsem se vyjádřil špatně. Chtěl bych testovat jednak funkčnost přihlašovacího formuláře, dvak funkčnost řízení přístupu k jednotlivým metodám. Příklad: metodu renderXy() budu mít označenou jako Secured a přístup k ní bude mít uživatel v roli admin. V aplikaci se o přidělení přístupu stará Authorizator na základě databáze. V testu budu chtít otestovat:

  1. přístup k metodě renderXy() jako guest → chci obdržet RedirectResponse, protože k metodě nemám oprávnění. S tímhle testem nemám problém a vím, jak na něj.
  2. přístup k metodě renderXy() jako přihlášený uživatel → tzn. chtěl bych ten presenter zavolat tak, aby mě přijal jako přihlášeného uživatele.

besanek napsal(a):

Ah sorry. Špatně jsem to pochopil. Myslel jsem, že chceš testovat něco, k čemu musíš být přihlášený.

Takže znovu a lépe.

Všechno se předá stejně jako v aplikaci. Dumpni si $response, jestli presenter vrací to co by měl. Tj. RedirectResponse. Pokud ti vrací TextResponse, tak si vypiš getSource() a zkontroluj, jestli v něm není náhodou vypsána nějaká chybová hláška.

enumag
Člen | 2118
+
0
-

@zimmi: V zásadě znám dvě možnosti:

  1. Funkcionální (integrační) test:
// create $container
// přihlaš uživatele
$presenter = $container->getByType('Nette\Application\IPresenterFactory')->createPresenter($presenterName);
$request = new \Nette\Application\Request($presenterName, ...); //parametry podle toho co testuješ
$response = $presenter->run($request);
//to celé zabalit do funkce která by měla vyhodit určitou výjimku - tu zachytíš a otestuješ že je taková jakou jsi čekal (např. nějaká ForbiddenRequestException)
//anebo otestuješ $response pokud výjimka neměla nastat
  1. Akceptační test: Selenium.

Editoval enumag (31. 7. 2013 20:32)

zimmi
Člen | 94
+
0
-

@enumag Mohl bys prosím rozvést to //přihlaš uživatele? Respektive existuje i jiný způsob než od besanek?

enumag napsal(a):

@zimmi: V zásadě znám dvě možnosti:

  1. Funkcionální (integrační) test:
// create $container
// přihlaš uživatele
$presenter = $container->getByType('Nette\Application\IPresenterFactory')->createPresenter($presenterName);
$request = new \Nette\Application\Request($presenterName, ...); //parametry podle toho co testuješ
$response = $presenter->run($request);
//to celé zabalit do funkce která by měla vyhodit určitou výjimku - tu zachytíš a otestuješ že je taková jakou jsi čekal (např. nějaká ForbiddenRequestException)
//anebo otestuješ $response pokud výjimka neměla nastat
  1. Akceptační test: Selenium.

Editoval zimmi (31. 7. 2013 20:53)

enumag
Člen | 2118
+
0
-

@zimni:

Pro funkcionální test akce přihlášení (přesněji signálu submit na login formuláři) vytvoříš request s danou akcí a signálem, pošleš ho presenteru jak jsem napsal a na konci ověříš že uživatel je přihlášen takhle:

assertTrue($container->getByType('Nette\Security\User')->isLoggedIn());
//+ případně ověřit identitu uživatele

EDIT: Nečetl jsem téma od začátku, teď koukám že přesně to jsi udělal. V tom případě bude asi nějaký problém s vytvářením DI containeru. Respektive zkus si nejdříve samostatně otestovat authenticator.

Pro funkcionální test nějaké akce po přihlášení je způsob od @besanek naprosto v pořádku.

Můžeš samozřejmě dělat obojí v jednom testu, ale připadá mi lepší mít ty testy oddělené.

Editoval enumag (31. 7. 2013 21:07)

zimmi
Člen | 94
+
0
-

@enumag což se dostáváme zpět k příspěvku #1, kde jsem psal, že mi přesně tenhle kód vrací False, ačkoliv by (si myslím) měl vrátit True. Potřeboval bych ho nějak debuggovat, respektive zjistit z toho testu něco víc.

enumag
Člen | 2118
+
0
-

@zimni: Co vrátí tohle?

$container->getByType('Nette\Security\Authenticator')->authenticate(array($jmeno, $heslo));

Mohl bys ukázat jak vytváříš $container?

Editoval enumag (31. 7. 2013 21:17)

zimmi
Člen | 94
+
0
-

@enumag:
Vrací

Nette\DI\MissingServiceException: Service of type Nette\Security\Authenticator not found. in Nette/DI/Container.php(208) in gismob/tests/UserPresenterTest.php(97) Nette\DI\Container->getByType()

a container:

<?php

require __DIR__ . '/../libs/autoload.php';

if (!class_exists('Tester\Assert')) {
	echo "Install Nette Tester using `composer update --dev`\n";
	exit(1);
}


Tester\Helpers::setup();

function id($val) {
	return $val;
}

$configurator = new Nette\Config\Configurator;
$configurator->setDebugMode(True);
$configurator->setTempDirectory(__DIR__ . '/../temp');
$configurator->createRobotLoader()
	->addDirectory(__DIR__ . '/../app')
	->register();

$configurator->addConfig(__DIR__ . '/../app/config/config.neon');
$configurator->addConfig(__DIR__ . '/../app/config/config.local.neon', $configurator::NONE); // none section
$container = $configurator->createContainer();
return $container;

Preventivně přikládám i část dumpu $containeru (relevatní):

"nette\database\connection" => "nette.database.default" (22)
      "adminmodule\registrator" => "registrator" (11)
      routerfactory => "routerFactory" (13)
      "adminmodule\repository" => "groupRepository" (15)
      "adminmodule\grouprepository" => "groupRepository" (15)
      "adminmodule\rolerepository" => "roleRepository" (14)
      "nette\security\iauthenticator" => "authenticator" (13)
      "adminmodule\authenticator" => "authenticator" (13)
      "nette\security\iauthorizator" => "authorizator" (12)
      "adminmodule\authorizator" => "authorizator" (12)
      "adminmodule\userrepository" => "userRepository" (14)
enumag
Člen | 2118
+
0
-

Sorry, má to být Nette\Security\IAuthenticator.

Na té konfiguraci DIC na první pohled nic špatného nevidím.

zimmi
Člen | 94
+
0
-

Teď to vrátí správnou hodnotu.

Nette\Security\Identity(4) {
   id private => "51cbea41178ff375847627" (22)
   roles private => array(1) [
      0 => "51cbe8e59134c891901827" (22)
   ]
   data private => array(0)
   frozen private => FALSE
}

Kdybych chtěl nastavit pro $container->user authenticator na AdminModule\Authenticator, který má v konstruktoru AdminModule\UserRepo, který má v konstruktoru Nette\Database\Connection, jde to udělat nějak elegantně?

Editoval zimmi (31. 7. 2013 21:51)

enumag
Člen | 2118
+
0
-

Tak to bude nějaká haluz, došly mi špatný nápady. :-D

Jak elegantně? Ten DIC by to tak měl udělat automaticky, ne?

Editoval enumag (31. 7. 2013 21:54)

zimmi
Člen | 94
+
0
-

Jo, samozřejmě máš pravdu. Test spouštím pomocí id(new UserPresenterTest($GLOBALS['container']))->run();, což je snad už jediné místo, kde by mohlo být něco blbě.

Aha, on mi ten $response vrací hlavičku 302.

Editoval zimmi (31. 7. 2013 22:12)

enumag
Člen | 2118
+
0
-

Malá technická, pokud je správně test a neprochází tak je špatně to co testuje. :-D

Možná je to jenom typo, ale proč je to UserPresenterTest když tam máš $this->getPresenter('Admin:Login');?

Taky může být něco blbě v BaseTestCase.

Editoval enumag (31. 7. 2013 22:15)

zimmi
Člen | 94
+
0
-

@enumag :D
Tak se dívám, že $response je:

Nette\Application\Responses\RedirectResponse(2) {
   url private => "http://localhost/admin/admin.login/default" (38)
   code private => 302
}

Zdá se, že mi nefungují routy, mám nahozený virtualhost na www.domena.cz, takže url by mělo být http//domena.cz/admin/admin.login/default.

zimmi
Člen | 94
+
0
-

Tak já jsem z toho jelen.

public function testDefault()
    {
        $presenter = $this->getPresenter('Admin:Login');
        $request = new \Nette\Application\Request('Admin:Login', 'GET', array(
            'action' => 'default',
        ));
        $response = $presenter->run($request);
        // zkontroluji response
        Assert::true( $response instanceof Nette\Application\Responses\TextResponse );
    }

Nezabezpečený presenter funguje, jak má. Ale redirect těch zabezpečených mě přesměruje s kódem 302 na neexistující stránku (viz předchozí post). To bohužel nevím, jak vyřešit. Zkoušel jsem do bootstrapu přidat definici rout z z bootstrapu aplikace, což ovšem nepomohlo.

Editoval zimmi (1. 8. 2013 8:23)

besanek
Člen | 128
+
0
-

Testy spouštíš přes příkazovou řádku mimo http server. Takže doména localhost je v pořádku.

Zkus vypnout kanonizaci presenterů.

$presenter->autoCanonicalize = FALSE;
enumag
Člen | 2118
+
0
-

@zimni: Imho bys měl použít Nette\Http\Url a testovat jen $url->getRelativeUrl().

zimmi
Člen | 94
+
0
-

No kluci, děkuju za pomoc, ale asi to momentálně vzdám. Do těch zabezpečených metod se prostě ne a ne přihlásit.

Milo
Nette Core | 1283
+
0
-

@zimni: Četl jsi Davidovo velestručné testování prezenterů?

K Testeru… Tester vypisuje hezký stack trace, kde k chybné aserci došlo. Jen nesmíš zapomenout v každém testu zavolat Tester\Environment::setup() (resp. Tester\Helpers::setup() ve staší verzi). Testy můžeš spouštět i samostatně jako obyčejný PHP skript. To se dobře hodí pro ladění.

php tests/my-test.phpt
zimmi
Člen | 94
+
0
-

Četl. Zkouším to podle něj:

$presenterFactory = $container->getByType('Nette\Application\IPresenterFactory');
$presenter = $presenterFactory->createPresenter('Admin:Login');
$presenter->autoCanonicalize = FALSE;
$request = new Nette\Application\Request('Admin:Login', 'POST', array(
    'action' => 'default',
    'do' => 'loginForm-submit',
    ), array(
        'username' => 'admin',
        'password' => 'admin',
    ));
$response = $presenter->run($request);
Assert::true( $presenter->user->isLoggedIn() ); //vrátí False

Request je stejný jako v aplikaci, ale uživatel se přesto nepřihlásí. Asi už mi docházejí nápady.

Milo napsal(a):

@zimni: Četl jsi Davidovo velestručné testování prezenterů?

K Testeru… Tester vypisuje hezký stack trace, kde k chybné aserci došlo. Jen nesmíš zapomenout v každém testu zavolat Tester\Environment::setup() (resp. Tester\Helpers::setup() ve staší verzi). Testy můžeš spouštět i samostatně jako obyčejný PHP skript. To se dobře hodí pro ladění.

php tests/my-test.phpt
Milo
Nette Core | 1283
+
0
-

S jakou verzí Nette pracuješ?

zimmi
Člen | 94
+
0
-

2.0.11

enumag
Člen | 2118
+
0
-

@zimmi: Teď mne napadlo, nechybí v tom post poli odesílací tlačítko? Nette kvůli tomu možná nevyhodnotí ten formulář jako odeslaný. Jinak bych zkusil na různých místech v presenteru / formuláři / authenticatoru vyhazovat výjimky jestli se tam ten test vůbec dostane.

zimmi
Člen | 94
+
0
-

@enumag vyzkouším a dám vědět.

zimmi
Člen | 94
+
0
-

Ahoj,
tak po víkendu jsem se k tomu dostal. To vyhazování výjimek pomohlo, zjistil jsem, že kvůli $form->addProtection() se ten formulář nikdy nedostal ke callbacku onSuccess.

Metoda loginFormSent vypadá takhle:

function loginFormSent(\LoginForm $form)
	{
		//throw new \Exception('FormSent');
		$user   = $this->user;
		$values = $form->values;

		try{
			$user->login($values->username, $values->password);

			if ($values->remember) {
				$user->setExpiration('+30 days', False);
			}
			Debugger:dump($this->user->isLoggedIn());
			$this->redirect('User:list');
		} catch (NS\AuthenticationException $e) {
			$this->flashMessage($e->getMessage(), 'error');
			$this->redirect('this');
		}
	}

Dump vrací v testu True, ale assert pořád False.

Edit: Pokud zakomentuju přesměrování v try bloku, assert vrací True. Co to znamená, respektive jak to mám chápat? To přesměrování v testu neudrží přihlášeného uživatele?

Editoval zimmi (5. 8. 2013 10:15)

Milo
Nette Core | 1283
+
0
-

Ačkoliv s tím nemám osobní zkušenost, odhaduji že problém bude v session. Když se user přihlásí, info se uloží do session a ta se udržuje pomocí cookiesky. Při redirectu se Ti ale už session neobnoví.

Myslím že na to, že se user úspěšně přihlásil, by Ti mělo stačit otestovat správný redirect na User:list + funkci autorizátoru. Takhle testuješ formulář a autorizátor najednou.

zimmi
Člen | 94
+
0
-

@Milo: Teď jsem teda v situaci, kdy mám otestované přihlašování, které je funkční. Takže teď mi v podstatě stačí k těm @Secured metodám vždy vytvořit fake uživatele, na kterém už pak jen otestuju authorizator, jo? O těch session jsem vlastně někde četl, že se dají nahradit vlastní implementací IUserStorage. Když tak na to ještě mrknu, ale jinak to asi zkusím tak, jak jsem napsal.

Každopádně děkuju za rady, už by to snad mělo šlapat.

Milo
Nette Core | 1283
+
0
-

Asi jo, já bych to tak udělal. Jeden test zvlášť na přihlašování. V ostatních už podstrčeného přihlášeného uživatele.

zimmi
Člen | 94
+
0
-

Tak ještě poslední dotaz:
Adresářová struktura aplikace

-app
-libs
   -Grido
   -nette
       -Nette
       -tester
           -Tester/tester.php
-tests

Když testuju šablonu, ve které je vykreslené Grido, dostanu chybu Exited with error code 255 (expected 0) Class Grido\Grid not found on line.... Test spouštím příkazem

php -c /etc/php5/apache2/php.ini  libs/nette/tester/Tester/tester.php -c /etc/php5/apache2/php.ini tests/UserPresenterTest.phpt

Lze tohle nějak vyřešit? Zkoušel jsem do bootstrapu require Grid.php, což začne vyžadovat require dalších částí Grida, až nakonec skončím u toho, že nejde načíst interface. Tohle asi nebude ta správná cesta.

Milo
Nette Core | 1283
+
0
-

Pro Grido a Nette používáš composer? Jestli jo, mělo by stačit v testech requirnout jeho autoload. A případně composer dumpautoload pro aktualiaci loaderu.

Editoval Milo (5. 8. 2013 15:11)

zimmi
Člen | 94
+
0
-

Nette jsem tuším stahoval ručně a rozbaloval ze zipu, Grido jsem stahoval přes Composer.

Milo
Nette Core | 1283
+
0
-

Musíš zkrátka nějak ty soubory s třídami loadnout. Ať už přes autoloader.php vygenerovaný composerem, nebo ručně, nebo použít RobotLoader z Nette.