diff --git a/app/AppKernel.php b/app/AppKernel.php index 43b0758..4a71f27 100644 --- a/app/AppKernel.php +++ b/app/AppKernel.php @@ -10,10 +10,10 @@ public function registerBundles() : array $bundles = [ /** SYMFONY */ new Doctrine\Bundle\DoctrineBundle\DoctrineBundle(), - /* new Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle(), */ - /* new Symfony\Bundle\SecurityBundle\SecurityBundle(), */ + new Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle(), new Symfony\Bundle\FrameworkBundle\FrameworkBundle(), new Symfony\Bundle\MonologBundle\MonologBundle(), + new Symfony\Bundle\SecurityBundle\SecurityBundle(), new Symfony\Bundle\TwigBundle\TwigBundle(), /** EXTRA */ new JMS\SerializerBundle\JMSSerializerBundle(), diff --git a/app/DoctrineMigrations/Version20160724000000.php b/app/DoctrineMigrations/Version20160724000000.php new file mode 100644 index 0000000..e1e8e13 --- /dev/null +++ b/app/DoctrineMigrations/Version20160724000000.php @@ -0,0 +1,50 @@ +skipIf($this->connection->getDatabasePlatform()->getName() != 'postgresql', 'Migration can only be executed safely on \'postgresql\'.'); + + $this->addSql('CREATE TABLE player_sessions (id SERIAL NOT NULL, player INT NOT NULL, hash VARCHAR(40) NOT NULL, timestamp TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_9CB7DCF398197A65 ON player_sessions (player)'); + $this->addSql('CREATE INDEX INDEX_SESSION_HASH ON player_sessions (hash)'); + $this->addSql('ALTER TABLE player_sessions ADD CONSTRAINT FK_9CB7DCF398197A65 FOREIGN KEY (player) REFERENCES players (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('DROP INDEX index_player_name'); + $this->addSql('ALTER TABLE players ADD passwordHash VARCHAR(40) NOT NULL'); + $this->addSql('ALTER TABLE players RENAME COLUMN name TO email'); + $this->addSql('ALTER TABLE players ALTER email TYPE VARCHAR(255)'); + $this->addSql('CREATE INDEX INDEX_PLAYER_EMAIL ON players (email)'); + $this->addSql('CREATE INDEX INDEX_PLAYER_EMAIL_AND_PASSWORD ON players (email, passwordHash)'); + } + + /** + * @param Schema $schema + */ + public function down(Schema $schema) + { + // this down() migration is auto-generated, please modify it to your needs + $this->skipIf($this->connection->getDatabasePlatform()->getName() != 'postgresql', 'Migration can only be executed safely on \'postgresql\'.'); + + $this->addSql('CREATE SCHEMA public'); + $this->addSql('DROP TABLE player_sessions'); + $this->addSql('DROP INDEX INDEX_PLAYER_EMAIL'); + $this->addSql('DROP INDEX INDEX_PLAYER_EMAIL_AND_PASSWORD'); + $this->addSql('ALTER TABLE players DROP passwordHash'); + $this->addSql('ALTER TABLE players RENAME COLUMN email TO name'); + $this->addSql('ALTER TABLE players ALTER name TYPE VARCHAR(25)'); + $this->addSql('CREATE INDEX index_player_name ON players (name)'); + } +} diff --git a/app/DoctrineMigrations/Version20160724000001.php b/app/DoctrineMigrations/Version20160724000001.php new file mode 100644 index 0000000..8d68025 --- /dev/null +++ b/app/DoctrineMigrations/Version20160724000001.php @@ -0,0 +1,45 @@ +skipIf($this->connection->getDatabasePlatform()->getName() != 'mysql', 'Migration can only be executed safely on \'mysql\'.'); + + $this->addSql('RENAME TABLE players TO users'); + $this->addSql('CREATE TABLE user_sessions (id INT AUTO_INCREMENT NOT NULL, player INT NOT NULL, hash VARCHAR(40) NOT NULL, timestamp DATETIME NOT NULL, INDEX IDX_7AED791398197A65 (player), INDEX INDEX_SESSION_HASH (hash), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB'); + $this->addSql('ALTER TABLE user_sessions ADD CONSTRAINT FK_7AED791398197A65 FOREIGN KEY (player) REFERENCES users (id)'); + $this->addSql('DROP INDEX INDEX_PLAYER_NAME ON users'); + $this->addSql('ALTER TABLE users CHANGE name email VARCHAR(255) NOT NULL, ADD passwordHash VARCHAR(40) NOT NULL'); + $this->addSql('CREATE INDEX INDEX_USER_EMAIL ON users (email)'); + $this->addSql('CREATE INDEX INDEX_USER_EMAIL_AND_PASSWORD ON users (email, passwordHash)'); + } + + /** + * @param Schema $schema + */ + public function down(Schema $schema) + { + // this down() migration is auto-generated, please modify it to your needs + $this->skipIf($this->connection->getDatabasePlatform()->getName() != 'mysql', 'Migration can only be executed safely on \'mysql\'.'); + + $this->addSql('RENAME TABLE users TO players'); + $this->addSql('DROP TABLE user_sessions'); + $this->addSql('DROP INDEX INDEX_USER_EMAIL ON users'); + $this->addSql('DROP INDEX INDEX_USER_EMAIL_AND_PASSWORD ON users'); + $this->addSql('ALTER TABLE users CHANGE email name VARCHAR(25) NOT NULL COLLATE utf8_unicode_ci, DROP passwordHash'); + $this->addSql('CREATE INDEX INDEX_PLAYER_NAME ON users (name)'); + } +} diff --git a/app/DoctrineMigrations/Version20160724000002.php b/app/DoctrineMigrations/Version20160724000002.php new file mode 100644 index 0000000..2163e4d --- /dev/null +++ b/app/DoctrineMigrations/Version20160724000002.php @@ -0,0 +1,110 @@ +skipIf($this->connection->getDatabasePlatform()->getName() != 'sqlite', 'Migration can only be executed safely on \'sqlite\'.'); + + $this->addSql('CREATE TABLE player_sessions (id INTEGER NOT NULL, player INTEGER NOT NULL, hash VARCHAR(40) NOT NULL, timestamp DATETIME NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_9CB7DCF398197A65 ON player_sessions (player)'); + $this->addSql('CREATE INDEX INDEX_SESSION_HASH ON player_sessions (hash)'); + $this->addSql('DROP INDEX INDEX_BATTLEFIELDS_PLAYER'); + $this->addSql('DROP INDEX INDEX_BATTLEFIELDS_GAME'); + $this->addSql('CREATE TEMPORARY TABLE __temp__battlefields AS SELECT id, game, player FROM battlefields'); + $this->addSql('DROP TABLE battlefields'); + $this->addSql('CREATE TABLE battlefields (id INTEGER NOT NULL, game INTEGER NOT NULL, player INTEGER NOT NULL, PRIMARY KEY(id), CONSTRAINT FK_EDE65EA6232B318C FOREIGN KEY (game) REFERENCES games (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_EDE65EA698197A65 FOREIGN KEY (player) REFERENCES players (id) NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO battlefields (id, game, player) SELECT id, game, player FROM __temp__battlefields'); + $this->addSql('DROP TABLE __temp__battlefields'); + $this->addSql('CREATE INDEX INDEX_BATTLEFIELDS_PLAYER ON battlefields (player)'); + $this->addSql('CREATE INDEX INDEX_BATTLEFIELDS_GAME ON battlefields (game)'); + $this->addSql('DROP INDEX UNIQUE_CELL_PER_BATTLEFIELD'); + $this->addSql('DROP INDEX INDEX_CELLS_BATTLEFIELD'); + $this->addSql('CREATE TEMPORARY TABLE __temp__cells AS SELECT id, battlefield, coordinate, flags FROM cells'); + $this->addSql('DROP TABLE cells'); + $this->addSql('CREATE TABLE cells (id INTEGER NOT NULL, battlefield INTEGER NOT NULL, coordinate VARCHAR(3) NOT NULL COLLATE BINARY, flags INTEGER NOT NULL, PRIMARY KEY(id), CONSTRAINT FK_55C1CBD851B7F6D5 FOREIGN KEY (battlefield) REFERENCES battlefields (id) NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO cells (id, battlefield, coordinate, flags) SELECT id, battlefield, coordinate, flags FROM __temp__cells'); + $this->addSql('DROP TABLE __temp__cells'); + $this->addSql('CREATE UNIQUE INDEX UNIQUE_CELL_PER_BATTLEFIELD ON cells (battlefield, coordinate)'); + $this->addSql('CREATE INDEX INDEX_CELLS_BATTLEFIELD ON cells (battlefield)'); + $this->addSql('DROP INDEX INDEX_GAME_RESULT_WINNER'); + $this->addSql('DROP INDEX INDEX_GAME_RESULT_GAME'); + $this->addSql('DROP INDEX UNIQ_A619B3B232B318C'); + $this->addSql('CREATE TEMPORARY TABLE __temp__game_results AS SELECT id, game, player, timestamp FROM game_results'); + $this->addSql('DROP TABLE game_results'); + $this->addSql('CREATE TABLE game_results (id INTEGER NOT NULL, game INTEGER NOT NULL, player INTEGER NOT NULL, timestamp DATETIME NOT NULL, PRIMARY KEY(id), CONSTRAINT FK_A619B3B232B318C FOREIGN KEY (game) REFERENCES games (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_A619B3B98197A65 FOREIGN KEY (player) REFERENCES players (id) NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO game_results (id, game, player, timestamp) SELECT id, game, player, timestamp FROM __temp__game_results'); + $this->addSql('DROP TABLE __temp__game_results'); + $this->addSql('CREATE INDEX INDEX_GAME_RESULT_WINNER ON game_results (player)'); + $this->addSql('CREATE INDEX INDEX_GAME_RESULT_GAME ON game_results (game)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_A619B3B232B318C ON game_results (game)'); + $this->addSql('DROP INDEX INDEX_PLAYER_NAME'); + $this->addSql('CREATE TEMPORARY TABLE __temp__players AS SELECT id, name, flags FROM players'); + $this->addSql('DROP TABLE players'); + $this->addSql('CREATE TABLE players (id INTEGER NOT NULL, flags INTEGER NOT NULL, email VARCHAR(25) NOT NULL, passwordHash VARCHAR(40) NOT NULL, PRIMARY KEY(id))'); + $this->addSql('INSERT INTO players (id, email, flags) SELECT id, name, flags FROM __temp__players'); + $this->addSql('DROP TABLE __temp__players'); + $this->addSql('CREATE INDEX INDEX_PLAYER_EMAIL ON players (email)'); + $this->addSql('CREATE INDEX INDEX_PLAYER_EMAIL_AND_PASSWORD ON players (email, passwordHash)'); + } + + /** + * @param Schema $schema + */ + public function down(Schema $schema) + { + // this down() migration is auto-generated, please modify it to your needs + $this->skipIf($this->connection->getDatabasePlatform()->getName() != 'sqlite', 'Migration can only be executed safely on \'sqlite\'.'); + + $this->addSql('DROP TABLE player_sessions'); + $this->addSql('DROP INDEX INDEX_BATTLEFIELDS_GAME'); + $this->addSql('DROP INDEX INDEX_BATTLEFIELDS_PLAYER'); + $this->addSql('CREATE TEMPORARY TABLE __temp__battlefields AS SELECT id, game, player FROM battlefields'); + $this->addSql('DROP TABLE battlefields'); + $this->addSql('CREATE TABLE battlefields (id INTEGER NOT NULL, game INTEGER NOT NULL, player INTEGER NOT NULL, PRIMARY KEY(id))'); + $this->addSql('INSERT INTO battlefields (id, game, player) SELECT id, game, player FROM __temp__battlefields'); + $this->addSql('DROP TABLE __temp__battlefields'); + $this->addSql('CREATE INDEX INDEX_BATTLEFIELDS_GAME ON battlefields (game)'); + $this->addSql('CREATE INDEX INDEX_BATTLEFIELDS_PLAYER ON battlefields (player)'); + $this->addSql('DROP INDEX INDEX_CELLS_BATTLEFIELD'); + $this->addSql('DROP INDEX UNIQUE_CELL_PER_BATTLEFIELD'); + $this->addSql('CREATE TEMPORARY TABLE __temp__cells AS SELECT id, battlefield, coordinate, flags FROM cells'); + $this->addSql('DROP TABLE cells'); + $this->addSql('CREATE TABLE cells (id INTEGER NOT NULL, battlefield INTEGER NOT NULL, coordinate VARCHAR(3) NOT NULL, flags INTEGER NOT NULL, PRIMARY KEY(id))'); + $this->addSql('INSERT INTO cells (id, battlefield, coordinate, flags) SELECT id, battlefield, coordinate, flags FROM __temp__cells'); + $this->addSql('DROP TABLE __temp__cells'); + $this->addSql('CREATE INDEX INDEX_CELLS_BATTLEFIELD ON cells (battlefield)'); + $this->addSql('CREATE UNIQUE INDEX UNIQUE_CELL_PER_BATTLEFIELD ON cells (battlefield, coordinate)'); + $this->addSql('DROP INDEX UNIQ_A619B3B232B318C'); + $this->addSql('DROP INDEX INDEX_GAME_RESULT_GAME'); + $this->addSql('DROP INDEX INDEX_GAME_RESULT_WINNER'); + $this->addSql('CREATE TEMPORARY TABLE __temp__game_results AS SELECT id, game, player, timestamp FROM game_results'); + $this->addSql('DROP TABLE game_results'); + $this->addSql('CREATE TABLE game_results (id INTEGER NOT NULL, game INTEGER NOT NULL, player INTEGER NOT NULL, timestamp DATETIME NOT NULL, PRIMARY KEY(id))'); + $this->addSql('INSERT INTO game_results (id, game, player, timestamp) SELECT id, game, player, timestamp FROM __temp__game_results'); + $this->addSql('DROP TABLE __temp__game_results'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_A619B3B232B318C ON game_results (game)'); + $this->addSql('CREATE INDEX INDEX_GAME_RESULT_GAME ON game_results (game)'); + $this->addSql('CREATE INDEX INDEX_GAME_RESULT_WINNER ON game_results (player)'); + $this->addSql('DROP INDEX INDEX_PLAYER_EMAIL'); + $this->addSql('DROP INDEX INDEX_PLAYER_EMAIL_AND_PASSWORD'); + $this->addSql('CREATE TEMPORARY TABLE __temp__players AS SELECT id, email, flags FROM players'); + $this->addSql('DROP TABLE players'); + $this->addSql('CREATE TABLE players (id INTEGER NOT NULL, flags INTEGER NOT NULL, name VARCHAR(25) NOT NULL COLLATE BINARY, PRIMARY KEY(id))'); + $this->addSql('INSERT INTO players (id, name, flags) SELECT id, email, flags FROM __temp__players'); + $this->addSql('DROP TABLE __temp__players'); + $this->addSql('CREATE INDEX INDEX_PLAYER_NAME ON players (name)'); + } +} diff --git a/app/config/config.yml b/app/config/config.yml index e1c6a0d..e7fd151 100644 --- a/app/config/config.yml +++ b/app/config/config.yml @@ -1,5 +1,6 @@ imports: - { resource: parameters.yml } + - { resource: security.yml } - { resource: services.yml } # Put parameters here that don't need to change on each machine where the app is deployed diff --git a/app/config/security.yml b/app/config/security.yml new file mode 100644 index 0000000..1dbf483 --- /dev/null +++ b/app/config/security.yml @@ -0,0 +1,25 @@ +# To get started with security, check out the documentation: +# http://symfony.com/doc/current/book/security.html +security: + # http://symfony.com/doc/current/book/security.html#where-do-users-come-from-user-providers + providers: + in_memory: + memory: ~ + + database_users: + entity: { class: GameBundle:PlayerSession, property: hash } + + firewalls: + # disables authentication for assets and the profiler, adapt it according to your needs + dev: + pattern: ^/(_(profiler|wdt)|css|images|js)/ + security: false + main: + anonymous: ~ + # activate different ways to authenticate + + # http_basic: ~ + # http://symfony.com/doc/current/book/security.html#a-configuring-how-your-users-will-authenticate + + # form_login: ~ + # http://symfony.com/doc/current/cookbook/security/form_login_setup.html diff --git a/app/config/services.yml b/app/config/services.yml index 4b5e65f..e4967cc 100644 --- a/app/config/services.yml +++ b/app/config/services.yml @@ -1,2 +1,3 @@ imports: + - { resource: "@FoundationBundle/Resources/config/services.yml" } - { resource: "@GameBundle/Resources/config/services.yml" } diff --git a/behat.yml b/behat.yml index a5fa6f8..efa52fe 100644 --- a/behat.yml +++ b/behat.yml @@ -15,6 +15,9 @@ default: - EM\Tests\Behat\GameBundle\Controller\GameResultControllerContext: filters: tags: "@api" + api.player.management: + paths: + - "%paths.base%/tests/behat/suites/api/api.player_management.feature" api.response.validation.against.schema: paths: [ "%paths.base%/tests/behat/suites/api/api.json.validation.against.schema.feature" ] contexts: diff --git a/src/FoundationBundle/Controller/UserController.php b/src/FoundationBundle/Controller/UserController.php new file mode 100644 index 0000000..cfbae68 --- /dev/null +++ b/src/FoundationBundle/Controller/UserController.php @@ -0,0 +1,150 @@ +getContent()); + if (!isset($json->email, $json->password)) { + return new Response(null, Response::HTTP_BAD_REQUEST); + } + + $player = $this->getDoctrine()->getRepository(User::class)->findOneBy(['email' => $json->email]); + if (null !== $player) { + throw new UserException(Response::HTTP_UNPROCESSABLE_ENTITY, "player with {$json->email} already exists"); + } + + $player = $this + ->get('em.foundation_bundle.model.user') + ->createPlayer($json->email, $json->password); + + $om = $this->getDoctrine()->getManager(); + $om->persist($player); + $om->flush(); + + return $this->processLogin($json->email, $json->password); + } + + /** + * @since 23.0 + * + * @ApiDoc( + * section = "API: Foundation", + * description = "authenticate and returns player details", + * input = "", + * responseMap = { + * 201 = "EM\FoundationBundle\Entity\UserSession", + * }, + * statusCodes = { + * 201 = "successfull login", + * 400 = "bad request", + * 401 = "unsucessfull authorization" + * } + * ) + * + * @param Request $request + * + * @return Response + * @throws UserException + */ + public function loginAction(Request $request) : Response + { + $json = json_decode($request->getContent()); + if (!isset($json->email, $json->password)) { + return new Response(null, Response::HTTP_BAD_REQUEST); + } + + return $this->processLogin($json->email, $json->password); + } + + /** + * @since 23.0 + * + * @param string $email + * @param string $password + * + * @return Response + */ + private function processLogin(string $email, string $password) : Response + { + try { + $session = $this->get('em.foundation_bundle.model.user_session')->authenticate($email, $password); + + $om = $this->getDoctrine()->getManager(); + $om->persist($session); + $om->flush(); + + return $this->prepareSerializedResponse($session, Response::HTTP_CREATED); + } catch (BadCredentialsException $e) { + return new Response(null, Response::HTTP_UNAUTHORIZED); + } + } + + /** + * @since 23.0 + * + * @Security("has_role('PLAYER')") + * @ApiDoc( + * section = "API: Foundation", + * description = "deletes session from database", + * input = "", + * statusCodes = { + * 202 = "successful logout", + * 403 = "not authorized" + * } + * ) + * + * @return Response + * @throws UserException + */ + public function logoutAction() : Response + { + /** @var WsseToken $token */ + $token = $this->get('security.token_storage')->getToken(); + $session = $token->getSession(); + + $om = $this->getDoctrine()->getManager(); + $om->remove($session); + $om->flush(); + + return $this->prepareSerializedResponse([], Response::HTTP_ACCEPTED); + } +} diff --git a/src/FoundationBundle/DataFixtures/ORM/UserSessionsFixture.php b/src/FoundationBundle/DataFixtures/ORM/UserSessionsFixture.php new file mode 100644 index 0000000..32065d3 --- /dev/null +++ b/src/FoundationBundle/DataFixtures/ORM/UserSessionsFixture.php @@ -0,0 +1,40 @@ +container->get('em.foundation_bundle.model.user_session')->authenticate( + UsersFixture::TEST_PLAYER_EMAIL, + UsersFixture::TEST_PLAYER_PASSWORD + ); + + $om->persist($session); + + $om->flush(); + } + + /** + * {@inheritDoc} + */ + public function getOrder() + { + return 2; + } +} diff --git a/src/FoundationBundle/DataFixtures/ORM/UsersFixture.php b/src/FoundationBundle/DataFixtures/ORM/UsersFixture.php new file mode 100644 index 0000000..6509e38 --- /dev/null +++ b/src/FoundationBundle/DataFixtures/ORM/UsersFixture.php @@ -0,0 +1,43 @@ +container->get('em.foundation_bundle.model.user'); + + $humanPlayer = $model->createPlayer(static::TEST_PLAYER_EMAIL, static::TEST_PLAYER_PASSWORD); + $om->persist($humanPlayer); + $aiPlayer = $model->createOnRequestAIControlled(static::TEST_AI_PLAYER_EMAIL); + $om->persist($aiPlayer); + + $om->flush(); + } + + /** + * {@inheritDoc} + */ + public function getOrder() + { + return 1; + } +} diff --git a/src/FoundationBundle/DependencyInjection/Factory/WsseFactory.php b/src/FoundationBundle/DependencyInjection/Factory/WsseFactory.php new file mode 100644 index 0000000..76c248c --- /dev/null +++ b/src/FoundationBundle/DependencyInjection/Factory/WsseFactory.php @@ -0,0 +1,39 @@ +setDefinition($providerId, new DefinitionDecorator('wsse.security.authentication.provider')) +// ->replaceArgument(0, new Reference($userProvider)); +// +// $listenerId = 'security.authentication.listener.wsse.' . $id; +// $listener = $container->setDefinition($listenerId, new DefinitionDecorator('wsse.security.authentication.listener')); +// +// return [$providerId, $listenerId, $defaultEntryPoint]; +// } +// +// public function getPosition() +// { +// return 'pre_auth'; +// } +// +// public function getKey() +// { +// return 'wsse'; +// } +// +// public function addConfiguration(NodeDefinition $node) +// { +// } +//} diff --git a/src/FoundationBundle/Entity/User.php b/src/FoundationBundle/Entity/User.php new file mode 100644 index 0000000..d686aca --- /dev/null +++ b/src/FoundationBundle/Entity/User.php @@ -0,0 +1,66 @@ +email; + } + + public function setEmail(string $email) : self + { + $this->email = $email; + + return $this; + } + + public function getPasswordHash() : string + { + return $this->passwordHash; + } + + public function setPasswordHash(string $hash) : self + { + $this->passwordHash = $hash; + + return $this; + } +} diff --git a/src/FoundationBundle/Entity/UserSession.php b/src/FoundationBundle/Entity/UserSession.php new file mode 100644 index 0000000..f09b70a --- /dev/null +++ b/src/FoundationBundle/Entity/UserSession.php @@ -0,0 +1,51 @@ +hash; + } + + public function setHash(string $hash) : self + { + $this->hash = $hash; + + return $this; + } +} diff --git a/src/FoundationBundle/Model/UserModel.php b/src/FoundationBundle/Model/UserModel.php new file mode 100644 index 0000000..d59fc43 --- /dev/null +++ b/src/FoundationBundle/Model/UserModel.php @@ -0,0 +1,55 @@ +repository = $repository; + $this->salt = $salt; + } + + public static function isAIControlled(User $player) : bool + { + return $player->hasFlag(static::FLAG_AI_CONTROLLED); + } + + public function generatePasswordHash(string $username, string $password) : string + { + return sha1("{$username}:{$password}:{$this->salt}"); + } + + public function createPlayer(string $email, string $password, int $flag = self::FLAG_NONE) : User + { + return (new User()) + ->setEmail($email) + ->setPasswordHash($this->generatePasswordHash($email, $password)) + ->setFlags($flag); + } + + public function createOnRequestAIControlled(string $email) : User + { + /** @var User $user */ + $user = $this->repository->findOneBy(['email' => $email]); + + return $user ?? $this->createPlayer($email, '', static::FLAG_AI_CONTROLLED); + } +} diff --git a/src/FoundationBundle/Model/UserSessionModel.php b/src/FoundationBundle/Model/UserSessionModel.php new file mode 100644 index 0000000..b3d68e8 --- /dev/null +++ b/src/FoundationBundle/Model/UserSessionModel.php @@ -0,0 +1,99 @@ +sessionRepository = $sessionRepository; + $this->playerRepository = $playerRepository; + $this->model = $model; + $this->salt = $salt; + } + + /** + * @param string $email + * @param string $password + * + * @return UserSession + * @throws BadCredentialsException + */ + public function authenticate(string $email, string $password) : UserSession + { + $passwordHash = $this->model->generatePasswordHash($email, $password); + + /** @var User $player */ + $player = $this->playerRepository->findOneBy(['email' => $email, 'passwordHash' => $passwordHash]); + + if (!$player) { + throw new BadCredentialsException(); + } + + return $this->create($player); + } + + /** + * @param string $hash + * + * @return UserSession + * @throws CredentialsExpiredException + * @throws BadCredentialsException + */ + public function find(string $hash) : UserSession + { + /** @var UserSession $session */ + if (null !== $session = $this->sessionRepository->findOneBy(['hash' => $hash])) { + if ($session->getTimestamp()->getTimestamp() + static::TTL >= time()) { + return $session; + } + + throw new CredentialsExpiredException(); + } + + throw new BadCredentialsException(); + } + + private function create(User $player) : UserSession + { + $session = new UserSession(); + $session + ->setUser($player) + ->setHash($this->generateSessionHash($player)); + + return $session; + } + + private function generateSessionHash(User $player) : string + { + return sha1("{$player->getEmail()}:{$player->getPasswordHash()}:{$this->salt}:" . microtime(true)); + } +} diff --git a/src/FoundationBundle/ORM/AbstractFlaggedEntity.php b/src/FoundationBundle/ORM/AbstractFlaggedEntity.php index 2e10611..4bf5d0b 100644 --- a/src/FoundationBundle/ORM/AbstractFlaggedEntity.php +++ b/src/FoundationBundle/ORM/AbstractFlaggedEntity.php @@ -23,6 +23,11 @@ abstract class AbstractFlaggedEntity extends AbstractEntity implements FlaggedIn */ protected $flags; + /** + * @param int $flag + * + * @return static + */ public function addFlag(int $flag) : self { $this->flags |= $flag; @@ -30,6 +35,11 @@ public function addFlag(int $flag) : self return $this; } + /** + * @param int $flag + * + * @return static + */ public function removeFlag(int $flag) : self { $this->flags &= ~$flag; @@ -42,6 +52,11 @@ public function getFlags() : int return $this->flags; } + /** + * @param int $flag + * + * @return static + */ public function setFlags(int $flag) : self { $this->flags = $flag; diff --git a/src/FoundationBundle/ORM/TimestampedTrait.php b/src/FoundationBundle/ORM/TimestampedTrait.php index 252656e..8d46bfe 100644 --- a/src/FoundationBundle/ORM/TimestampedTrait.php +++ b/src/FoundationBundle/ORM/TimestampedTrait.php @@ -28,6 +28,8 @@ public function getTimestamp() : \DateTime /** * @ORM\PrePersist + * + * @return static */ public function setTimestamp() : self { diff --git a/src/FoundationBundle/ORM/UserAwareInterface.php b/src/FoundationBundle/ORM/UserAwareInterface.php new file mode 100644 index 0000000..8deb3ed --- /dev/null +++ b/src/FoundationBundle/ORM/UserAwareInterface.php @@ -0,0 +1,16 @@ +user; + } + + /** + * @param User $user + * + * @return static + */ + public function setUser(User $user) : self + { + $this->user = $user; + + return $this; + } +} diff --git a/src/FoundationBundle/Resources/config/routing/api.yml b/src/FoundationBundle/Resources/config/routing/api.yml index e69de29..0887883 100644 --- a/src/FoundationBundle/Resources/config/routing/api.yml +++ b/src/FoundationBundle/Resources/config/routing/api.yml @@ -0,0 +1,12 @@ +foundation_bundle.api.player_create: + path: player/register + defaults: { _controller: "FoundationBundle:User:register" } + methods: [POST] +foundation_bundle.api.player_login: + path: player/login + defaults: { _controller: "FoundationBundle:User:login" } + methods: [POST] +foundation_bundle.api.player_logout: + path: player/logout + defaults: { _controller: "FoundationBundle:User:logout" } + methods: [DELETE] diff --git a/src/FoundationBundle/Resources/config/services.yml b/src/FoundationBundle/Resources/config/services.yml new file mode 100644 index 0000000..1d2a16e --- /dev/null +++ b/src/FoundationBundle/Resources/config/services.yml @@ -0,0 +1,49 @@ +services: + # Models + em.foundation_bundle.model.user: + class: EM\FoundationBundle\Model\UserModel + arguments: + - "@em.foundation_bundle.repository.user" + - "%secret%" + em.foundation_bundle.model.user_session: + class: EM\FoundationBundle\Model\UserSessionModel + arguments: + - "@em.foundation_bundle.repository.user_session" + - "@em.foundation_bundle.repository.user" + - "@em.foundation_bundle.model.user" + - "%%secret%" + # Repositories + em.foundation_bundle.repository.user: + class: Doctrine\ORM\EntityRepository + factory: ["@doctrine.orm.entity_manager", getRepository] + arguments: + - EM\FoundationBundle\Entity\User + em.foundation_bundle.repository.user_session: + class: Doctrine\ORM\EntityRepository + factory: ["@doctrine.orm.entity_manager", getRepository] + arguments: + - EM\FoundationBundle\Entity\UserSession +# +# # Firewall +# wsse.security.authentication.provider: +# class: EM\FoundationBundle\Security\Authorization\Provider\WsseProvider +# arguments: +# - '' +# - '@cache.app' +# public: false +# +# wsse.security.authentication.listener: +# class: EM\FoundationBundle\Security\Authorization\Listener\WsseListener +# arguments: +# - "@security.token_storage" +# - "@em.foundation_bundle.model.user_session" +# public: false + + # Event Listeners + em.foundation_bundle.security.authorization.listener: + class: EM\FoundationBundle\Security\Authorization\Listener\WsseListener + arguments: + - "@security.token_storage" + - "@em.foundation_bundle.model.user_session" + tags: + - { name: kernel.event_listener, event: kernel.request, method: handle } diff --git a/src/FoundationBundle/Security/Authorization/Listener/WsseListener.php b/src/FoundationBundle/Security/Authorization/Listener/WsseListener.php new file mode 100644 index 0000000..cda67d3 --- /dev/null +++ b/src/FoundationBundle/Security/Authorization/Listener/WsseListener.php @@ -0,0 +1,46 @@ +storage = $storage; + $this->model = $model; + } + + public function handle(GetResponseEvent $event) : bool + { + if ($event->isMasterRequest() && $event->getRequest()->headers->get(UserSessionModel::SESSION_HEADER)) { + $sessionHash = $event->getRequest()->headers->get(UserSessionModel::SESSION_HEADER); + + $session = $this->model->find($sessionHash); + + $token = (new WsseToken(['PLAYER'])) + ->setSession($session); + + $this->storage->setToken($token); + } + + return false; + } +} diff --git a/src/FoundationBundle/Security/Authorization/Provider/WsseProvider.php b/src/FoundationBundle/Security/Authorization/Provider/WsseProvider.php new file mode 100644 index 0000000..548c0fa --- /dev/null +++ b/src/FoundationBundle/Security/Authorization/Provider/WsseProvider.php @@ -0,0 +1,34 @@ +userProvider = $userProvider; + $this->cachePool = $cachePool; + } + + public function authenticate(TokenInterface $token) + { + throw new AuthenticationException('The WSSE authentication failed.'); + } + + + public function supports(TokenInterface $token) + { + return $token instanceof WsseToken; + } +} diff --git a/src/FoundationBundle/Security/Authorization/Token/WsseToken.php b/src/FoundationBundle/Security/Authorization/Token/WsseToken.php new file mode 100644 index 0000000..dc4b4f0 --- /dev/null +++ b/src/FoundationBundle/Security/Authorization/Token/WsseToken.php @@ -0,0 +1,57 @@ +getUser(); + } + + public function getSession() : UserSession + { + return $this->session; + } + + public function setSession(UserSession $session) : self + { + $this->session = $session; + $this->setUser($session->getUser()); + + return $this; + } + + /** + * @param User $user + */ + public function setUser($user) + { + $this->user = $user; + $this->setAuthenticated(true); + } + + /** + * @return User + */ + public function getUser() + { + return $this->user; + } +} diff --git a/src/GameBundle/Controller/GameController.php b/src/GameBundle/Controller/GameController.php index 64d8e28..0c7d578 100644 --- a/src/GameBundle/Controller/GameController.php +++ b/src/GameBundle/Controller/GameController.php @@ -11,6 +11,7 @@ use EM\GameBundle\Response\GameTurnResponse; use EM\Tests\PHPUnit\GameBundle\Controller\GameControllerTest; use Nelmio\ApiDocBundle\Annotation\ApiDoc; +use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Exception\HttpException; @@ -23,9 +24,7 @@ class GameController extends AbstractAPIController { /** - * @see GameControllerTest::unsuccessfulInitAction - * @see GameControllerTest::successfulInitAction_JSON - * @see GameControllerTest::successfulInitAction_XML + * @Security("has_role('PLAYER')") * * @ApiDoc( * section = "Game:: Mechanics", @@ -50,7 +49,7 @@ public function initAction(Request $request) : Response throw new HttpException(Response::HTTP_BAD_REQUEST, 'request validation failed, please check documentation'); } - $game = $this->get('em.game_bundle.service.game_builder')->buildGame(new GameInitiationRequest($request->getContent())); + $game = $this->get('em.game_bundle.service.game_builder')->buildGame(new GameInitiationRequest($request->getContent()), $this->getUser()); $om = $this->getDoctrine()->getManager(); $om->persist($game); @@ -60,8 +59,7 @@ public function initAction(Request $request) : Response } /** - * @see GameControllerTest::successfulTurnAction - * @see GameControllerTest::unsuccessfulTurnActionOnDeadCell + * @Security("has_role('PLAYER')") * * @ApiDoc( * section = "Game:: Mechanics", diff --git a/src/GameBundle/Controller/GameResultController.php b/src/GameBundle/Controller/GameResultController.php index f2ed10b..8bff80b 100644 --- a/src/GameBundle/Controller/GameResultController.php +++ b/src/GameBundle/Controller/GameResultController.php @@ -5,6 +5,7 @@ use EM\FoundationBundle\Controller\AbstractAPIController; use EM\Tests\PHPUnit\GameBundle\Controller\GameResultControllerTest; use Nelmio\ApiDocBundle\Annotation\ApiDoc; +use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security; use Symfony\Component\HttpFoundation\Response; /** @@ -23,6 +24,8 @@ class GameResultController extends AbstractAPIController * output = "EM\GameBundle\Response\GameResultsResponse" * ) * + * @Security("has_role('PLAYER')") + * * @param int $page * * @return Response diff --git a/src/GameBundle/DataFixtures/ORM/LoadGameData.php b/src/GameBundle/DataFixtures/ORM/LoadGameData.php new file mode 100644 index 0000000..de8989e --- /dev/null +++ b/src/GameBundle/DataFixtures/ORM/LoadGameData.php @@ -0,0 +1,54 @@ +files()->in("{$directory}/game-initiation-requests/valid"); + + $builder = $this->container->get('em.game_bundle.service.game_builder'); + $player = $om->getRepository(User::class)->findOneBy(['email' => UsersFixture::TEST_PLAYER_EMAIL]); + + foreach ($finder as $file) { + for ($i = 0; $i < 3; $i++) { + $game = $builder->buildGame(new GameInitiationRequest($file->getContents()), $player); + + $om->persist($game); + } + } + + $om->flush(); + } + + /** + * {@inheritDoc} + */ + public function getOrder() + { + return 3; + } +} diff --git a/src/GameBundle/DataFixtures/ORM/LoadPlayerData.php b/src/GameBundle/DataFixtures/ORM/LoadPlayerData.php deleted file mode 100644 index ddfb1b7..0000000 --- a/src/GameBundle/DataFixtures/ORM/LoadPlayerData.php +++ /dev/null @@ -1,42 +0,0 @@ -container->get('em.game_bundle.service.player_model'); - - $om->persist($model->createOnRequestHumanControlled(static::TEST_HUMAN_PLAYER_EMAIL)); - $om->persist($model->createOnRequestAIControlled(static::TEST_AI_CONTROLLED_PLAYER_EMAIL)); - - $om->flush(); - } - - /** - * {@inheritDoc} - */ - public function getOrder() - { - return 1; - } -} diff --git a/src/GameBundle/Entity/Battlefield.php b/src/GameBundle/Entity/Battlefield.php index ed856d7..94c463d 100644 --- a/src/GameBundle/Entity/Battlefield.php +++ b/src/GameBundle/Entity/Battlefield.php @@ -6,8 +6,8 @@ use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; use EM\FoundationBundle\ORM\AbstractEntity; -use EM\FoundationBundle\ORM\PlayerInterface; -use EM\FoundationBundle\ORM\PlayerTrait; +use EM\FoundationBundle\ORM\UserAwareInterface; +use EM\FoundationBundle\ORM\UserAwareTrait; use JMS\Serializer\Annotation as JMS; /** @@ -25,9 +25,9 @@ * @JMS\AccessorOrder(order="custom", custom={"id", "player", "cells"}) * @JMS\XmlRoot("battlefield") */ -class Battlefield extends AbstractEntity implements PlayerInterface +class Battlefield extends AbstractEntity implements UserAwareInterface { - use PlayerTrait; + use UserAwareTrait; /** * @ORM\ManyToOne(targetEntity="EM\GameBundle\Entity\Game", inversedBy="battlefields", fetch="EAGER") * @ORM\JoinColumn(name="game", referencedColumnName="id", nullable=false) diff --git a/src/GameBundle/Entity/GameResult.php b/src/GameBundle/Entity/GameResult.php index fa6a590..f9fe7d7 100644 --- a/src/GameBundle/Entity/GameResult.php +++ b/src/GameBundle/Entity/GameResult.php @@ -4,8 +4,8 @@ use Doctrine\ORM\Mapping as ORM; use EM\FoundationBundle\ORM\AbstractEntity; -use EM\FoundationBundle\ORM\PlayerInterface; -use EM\FoundationBundle\ORM\PlayerTrait; +use EM\FoundationBundle\ORM\UserAwareInterface; +use EM\FoundationBundle\ORM\UserAwareTrait; use EM\FoundationBundle\ORM\TimestampedInterface; use EM\FoundationBundle\ORM\TimestampedTrait; use JMS\Serializer\Annotation as JMS; @@ -26,9 +26,9 @@ * @JMS\AccessorOrder(order="custom", custom={"id", "timestamp", "player"}) * @JMS\XmlRoot("game-result") */ -class GameResult extends AbstractEntity implements PlayerInterface, TimestampedInterface +class GameResult extends AbstractEntity implements UserAwareInterface, TimestampedInterface { - use PlayerTrait, TimestampedTrait; + use UserAwareTrait, TimestampedTrait; /** * @ORM\OneToOne(targetEntity="EM\GameBundle\Entity\Game", inversedBy="result") * @ORM\JoinColumn(name="game", referencedColumnName="id", nullable=false) diff --git a/src/GameBundle/Exception/PlayerException.php b/src/GameBundle/Exception/UserException.php similarity index 74% rename from src/GameBundle/Exception/PlayerException.php rename to src/GameBundle/Exception/UserException.php index 02a9355..7b111f8 100644 --- a/src/GameBundle/Exception/PlayerException.php +++ b/src/GameBundle/Exception/UserException.php @@ -7,6 +7,6 @@ /** * @since 9.2 */ -class PlayerException extends HttpException +class UserException extends HttpException { } diff --git a/src/GameBundle/Model/PlayerModel.php b/src/GameBundle/Model/PlayerModel.php deleted file mode 100644 index f6c0554..0000000 --- a/src/GameBundle/Model/PlayerModel.php +++ /dev/null @@ -1,73 +0,0 @@ -repository = $repository; - } - - /** - * @param string $name - * - * @return Player - * @throws PlayerException - */ - public function createOnRequestAIControlled(string $name) : Player - { - return $this->createOnRequest($name, true); - } - - /** - * @param string $name - * - * @return Player - * @throws PlayerException - */ - public function createOnRequestHumanControlled(string $name) : Player - { - return $this->createOnRequest($name, false); - } - - /** - * @param string $name - * @param bool $controlledByAI - * - * @return Player - * @throws PlayerException - */ - protected function createOnRequest(string $name, bool $controlledByAI) : Player - { - /** @var Player $player */ - $player = $this->repository->findOneBy(['name' => $name]); - - if (null !== $player && $controlledByAI !== static::isAIControlled($player)) { - throw new PlayerException("player with '$name' already exists and controlledByAI do not match"); - } - - return $player ?? (new Player()) - ->setName($name) - ->setFlags($controlledByAI ? static::FLAG_AI_CONTROLLED : static::FLAG_NONE); - } - - public static function isAIControlled(Player $player) : bool - { - return $player->hasFlag(self::FLAG_AI_CONTROLLED); - } -} diff --git a/src/GameBundle/Request/GameInitiationRequest.php b/src/GameBundle/Request/GameInitiationRequest.php index 9ee8ef3..6fd2894 100644 --- a/src/GameBundle/Request/GameInitiationRequest.php +++ b/src/GameBundle/Request/GameInitiationRequest.php @@ -21,13 +21,6 @@ class GameInitiationRequest * @var int */ private $size; - /** - * @JMS\Type("string") - * @JMS\SerializedName("playerName") - * - * @var string - */ - private $playerName; /** * @JMS\Type("array") * @@ -46,7 +39,6 @@ public function parse(string $json) : self $this->size = $data->size; $this->opponents = $data->opponents; - $this->playerName = $data->playerName; $this->coordinates = $data->coordinates; return $this; @@ -62,11 +54,6 @@ public function getSize() : int return $this->size; } - public function getPlayerName() : string - { - return $this->playerName; - } - /** * @return string[] */ diff --git a/src/GameBundle/Resources/config/routing/gui.yml b/src/GameBundle/Resources/config/routing/gui.yml new file mode 100644 index 0000000..e69de29 diff --git a/src/GameBundle/Resources/config/services.yml b/src/GameBundle/Resources/config/services.yml index 9736f4d..d0c02c4 100644 --- a/src/GameBundle/Resources/config/services.yml +++ b/src/GameBundle/Resources/config/services.yml @@ -1,9 +1,5 @@ services: # Models - em.game_bundle.service.player_model: - class: EM\GameBundle\Model\PlayerModel - arguments: - - "@em.game_bundle.repository.player" em.game_bundle.service.game_result_model: class: EM\GameBundle\Model\GameResultModel arguments: @@ -13,7 +9,7 @@ services: em.game_bundle.service.game_builder: class: EM\GameBundle\Service\GameSystem\GameBuilder arguments: - - "@em.game_bundle.service.player_model" + - "@em.foundation_bundle.model.user" # Game Processors em.game_bundle.service.game_processor: class: EM\GameBundle\Service\GameSystem\GameProcessor @@ -31,11 +27,6 @@ services: em.game_bundle.service.ai_strategy_processor: class: EM\GameBundle\Service\AI\AIStrategyProcessor # Repositories - em.game_bundle.repository.player: - class: Doctrine\ORM\EntityRepository - factory: ["@doctrine.orm.entity_manager", getRepository] - arguments: - - EM\FoundationBundle\Entity\Player em.game_bundle.repository.game_result: class: EM\GameBundle\Repository\GameResultRepository factory: ["@doctrine.orm.entity_manager", getRepository] diff --git a/src/GameBundle/Response/GameInitiationResponse.php b/src/GameBundle/Response/GameInitiationResponse.php index 9cb8c9d..489df04 100644 --- a/src/GameBundle/Response/GameInitiationResponse.php +++ b/src/GameBundle/Response/GameInitiationResponse.php @@ -5,7 +5,7 @@ use Doctrine\Common\Collections\Collection; use EM\GameBundle\Entity\Battlefield; use EM\GameBundle\Model\CellModel; -use EM\GameBundle\Model\PlayerModel; +use EM\FoundationBundle\Model\UserModel; use JMS\Serializer\Annotation as JMS; /** @@ -41,7 +41,7 @@ public function addBattlefield(Battlefield $battlefield) : self { $this->battlefields[] = $battlefield; - if (PlayerModel::isAIControlled($battlefield->getPlayer())) { + if (UserModel::isAIControlled($battlefield->getUser())) { foreach ($battlefield->getCells() as $cell) { $cell->setFlags(CellModel::FLAG_NONE); } diff --git a/src/GameBundle/Service/GameSystem/GameBuilder.php b/src/GameBundle/Service/GameSystem/GameBuilder.php index 7ca2576..e0efb70 100644 --- a/src/GameBundle/Service/GameSystem/GameBuilder.php +++ b/src/GameBundle/Service/GameSystem/GameBuilder.php @@ -3,9 +3,10 @@ namespace EM\GameBundle\Service\GameSystem; use EM\GameBundle\Entity\Game; +use EM\FoundationBundle\Entity\User; use EM\GameBundle\Model\BattlefieldModel; use EM\GameBundle\Model\CellModel; -use EM\GameBundle\Model\PlayerModel; +use EM\FoundationBundle\Model\UserModel; use EM\GameBundle\Request\GameInitiationRequest; /** @@ -16,36 +17,34 @@ class GameBuilder { /** - * @var PlayerModel + * @var UserModel */ - private $playerModel; + private $userModel; - public function __construct(PlayerModel $playerModel) + public function __construct(UserModel $UserModel) { - $this->playerModel = $playerModel; + $this->userModel = $UserModel; } protected function attachAIBattlefields(Game $game, int $amount, int $size) { for ($i = 0; $i < $amount; $i++) { - $player = $this->playerModel->createOnRequestAIControlled("CPU {$i}"); + $user = $this->userModel->createOnRequestAIControlled("CPU {$i}"); /** hard-code ship into B2 for testing purposes */ $battlefield = BattlefieldModel::generate($size, ['B2']) - ->setPlayer($player); + ->setUser($user); $game->addBattlefield($battlefield); } } - public function buildGame(GameInitiationRequest $request) : Game + public function buildGame(GameInitiationRequest $request, User $user) : Game { $game = new Game(); $this->attachAIBattlefields($game, $request->getOpponents(), $request->getSize()); - $player = $this->playerModel->createOnRequestHumanControlled($request->getPlayerName()); - $battlefield = BattlefieldModel::generate($request->getSize(), $request->getCoordinates()); - $battlefield->setPlayer($player); + $battlefield->setUser($user); $game->addBattlefield($battlefield); /** for test purposes only - mark player cells as damaged */ diff --git a/src/GameBundle/Service/GameSystem/GameProcessor.php b/src/GameBundle/Service/GameSystem/GameProcessor.php index 0b49a66..7a9a8bc 100644 --- a/src/GameBundle/Service/GameSystem/GameProcessor.php +++ b/src/GameBundle/Service/GameSystem/GameProcessor.php @@ -6,11 +6,11 @@ use EM\GameBundle\Entity\Cell; use EM\GameBundle\Entity\Game; use EM\GameBundle\Entity\GameResult; -use EM\FoundationBundle\Entity\Player; +use EM\FoundationBundle\Entity\User; use EM\GameBundle\Exception\GameProcessorException; use EM\GameBundle\Model\BattlefieldModel; use EM\GameBundle\Model\CellModel; -use EM\GameBundle\Model\PlayerModel; +use EM\FoundationBundle\Model\UserModel; use EM\GameBundle\Service\AI\AIService; /** @@ -45,10 +45,10 @@ public function processTurn(Cell $cell) : Game } foreach ($game->getBattlefields() as $battlefield) { - $attacker = $battlefield->getPlayer(); + $attacker = $battlefield->getUser(); if ($this->processPlayerTurnOnBattlefields($game, $attacker, $cell)) { $result = (new GameResult()) - ->setPlayer($attacker); + ->setUser($attacker); $game->setResult($result); break; @@ -117,7 +117,7 @@ protected function processPlayerTurnOnBattlefield(Battlefield $battlefield, Play */ protected function processPlayerTurn(Battlefield $battlefield, Cell $cell) : Cell { - return PlayerModel::isAIControlled($battlefield->getPlayer()) + return UserModel::isAIControlled($battlefield->getPlayer()) ? CellModel::switchPhase($cell) : $this->ai->processCPUTurn($battlefield); } diff --git a/src/GameBundle/Validator/GameInitiationRequestValidator.php b/src/GameBundle/Validator/GameInitiationRequestValidator.php index 7f5d818..6572393 100644 --- a/src/GameBundle/Validator/GameInitiationRequestValidator.php +++ b/src/GameBundle/Validator/GameInitiationRequestValidator.php @@ -40,7 +40,6 @@ public function validate(string $json) : bool return $this->validateStructure($data) - && $this->validatePlayerName($data->playerName) // will be replaced by Authorization header which will reflect Player.id content && $this->validateOpponentsAmount($data->opponents) && $this->validateBattlefieldSize($data->size) && $this->validateCoordinates($data->coordinates); @@ -50,15 +49,10 @@ protected function validateStructure($data) : bool { return $data instanceof \stdClass && - isset($data->opponents, $data->playerName, $data->size, $data->coordinates) + isset($data->opponents, $data->size, $data->coordinates) && is_array($data->coordinates); } - protected function validatePlayerName(string $value) : bool - { - return !empty($value); - } - protected function validateBattlefieldSize(int $value) : bool { return $this->isBetween($value, $this->minBattlefieldSize, $this->maxBattlefieldSize); diff --git a/tests/behat/contexts/CommonControllerContext.php b/tests/behat/contexts/CommonControllerContext.php index 9dfbcf3..abce52b 100644 --- a/tests/behat/contexts/CommonControllerContext.php +++ b/tests/behat/contexts/CommonControllerContext.php @@ -3,15 +3,25 @@ namespace EM\Tests\Behat; use Behat\Behat\Context\Context; +use EM\FoundationBundle\DataFixtures\ORM\UsersFixture; use EM\Tests\Environment\AbstractControllerTestCase; +use Symfony\Bundle\FrameworkBundle\Client; class CommonControllerContext extends AbstractControllerTestCase implements Context { + /** + * @var Client + */ + protected static $client; + /** * @BeforeScenario */ public static function beforeEachScenario() { + static::$client = null; + static::$initiated = null; + static::setUpBeforeClass(); } @@ -34,6 +44,7 @@ public function requestAPIRoute(string $route, string $method, string $content = /** * @Given request :route route via :method + * @Given request API :route route via :method * * @param string $route * @param string $method @@ -42,6 +53,9 @@ public function requestAPIRoute(string $route, string $method, string $content = */ public function requestRoute(string $route, string $method, array $server = [], string $content = null) { + $server['CONTENT_TYPE'] = 'application/json'; + $server['HTTP_accept'] = 'application/json'; + static::$client->request( $method, $route, @@ -52,6 +66,17 @@ public function requestRoute(string $route, string $method, array $server = [], ); } + /** + * @Given I am authorized + * @Given I am :notAuthorized authorized + * + * @param bool $authorized + */ + public function prepareClient(bool $authorized = false) + { + static::$client = $this->getAuthorizedClient($authorized ? '' : UsersFixture::TEST_PLAYER_EMAIL); + } + /** * @Then observe response status code :statusCode * @@ -59,7 +84,9 @@ public function requestRoute(string $route, string $method, array $server = [], */ public function observeResponseStatusCode(int $statusCode) { - $this->assertEquals($statusCode, static::$client->getResponse()->getStatusCode()); + $response = static::$client->getResponse(); + + $this->assertEquals($statusCode, $response->getStatusCode()); } /** @@ -67,7 +94,9 @@ public function observeResponseStatusCode(int $statusCode) */ public function observeValidJsonResponse() { - $this->assertJson(static::$client->getResponse()->getContent()); + $response = static::$client->getResponse(); + + $this->assertJson($response->getContent()); } /** @@ -77,6 +106,8 @@ public function observeValidJsonResponse() */ public function observeRedirectionTo(string $route) { - $this->assertEquals($route, static::$client->getResponse()->headers->get('location')); + $response = static::$client->getResponse(); + + $this->assertEquals($route, $response->headers->get('location')); } } diff --git a/tests/behat/suites/api/api.game.feature b/tests/behat/suites/api/api.game.feature index 485a14e..1cb9a61 100644 --- a/tests/behat/suites/api/api.game.feature +++ b/tests/behat/suites/api/api.game.feature @@ -1,12 +1,13 @@ Feature: Battleship Game: API: Game Mechanics - @api - @mechanics - Scenario Outline: routes should return unsuccessful response on wrong data - Given request API "" route via "" - Then observe response status code "" + @api + @mechanics + Scenario Outline: routes should return unsuccessful response on wrong data + Given I am authorized + When request API "" route via "" + Then observe response status code "" - Examples: - | method | route | statusCode | - | POST | /api/game-init | 400 | - | PATCH | /api/game-turn/cell-id/0 | 404 | + Examples: + | method | route | code | + | POST | /api/game-init | 400 | + | PATCH | /api/game-turn/cell-id/0 | 404 | diff --git a/tests/behat/suites/api/api.game_results.feature b/tests/behat/suites/api/api.game_results.feature index fe48e67..1005a84 100644 --- a/tests/behat/suites/api/api.game_results.feature +++ b/tests/behat/suites/api/api.game_results.feature @@ -1,13 +1,14 @@ Feature: Battleship Game: API: Game Results - @api - Scenario Outline: routes should return successful response - Given request API "" route via "" - Then observe response status code "" - And observe valid JSON response + @api + Scenario Outline: routes should return successful response + Given I am authorized + When request API "" route via "" + Then observe response status code "" + And observe valid JSON response - Examples: - | method | route | code | - | GET | /api/game-results/page/1 | 200 | - | GET | /api/game-results/page/2 | 200 | - | GET | /api/game-results/page/999 | 200 | + Examples: + | method | route | code | + | GET | /api/game-results/page/1 | 200 | + | GET | /api/game-results/page/2 | 200 | + | GET | /api/game-results/page/999 | 200 | diff --git a/tests/behat/suites/api/api.player_management.feature b/tests/behat/suites/api/api.player_management.feature new file mode 100644 index 0000000..ab4a2aa --- /dev/null +++ b/tests/behat/suites/api/api.player_management.feature @@ -0,0 +1,24 @@ +Feature: Battleship Game: API: Game Mechanics + + @api + @mechanics + Scenario Outline: routes should return unsuccessful response on wrong data + Given I am not authorized + When request API "" route via "" + Then observe response status code "" + + Examples: + | method | route | code | + | POST | /api/player/register | 400 | + | POST | /api/player/login | 400 | + + @api + @mechanics + Scenario Outline: routes should return unsuccessful response on wrong data + Given I am authorized + When request API "" route via "" + Then observe response status code "" + + Examples: + | method | route | code | + | DELETE | /api/player/logout | 202 | \ No newline at end of file diff --git a/tests/phpunit/FoundationBundle/Authorization/AuthorizationListenerTest.php b/tests/phpunit/FoundationBundle/Authorization/AuthorizationListenerTest.php new file mode 100644 index 0000000..81431f4 --- /dev/null +++ b/tests/phpunit/FoundationBundle/Authorization/AuthorizationListenerTest.php @@ -0,0 +1,12 @@ +getUnauthorizedClient(); + $client->request( + Request::METHOD_POST, + '/api/player/register', + [], + [], + ['CONTENT_TYPE' => 'application/json', 'HTTP_accept' => 'application/json'], + $json + ); + + $response = $client->getResponse(); + + $this->assertSame($expectedStatusCode, $response->getStatusCode()); + } + + /** + * + */ + public function loginActionDataProvider() : array + { + $email = UsersFixture::TEST_PLAYER_EMAIL; + $password = UsersFixture::TEST_PLAYER_PASSWORD; + + return [ + [Response::HTTP_CREATED, "{\"email\": \"{$email}\", \"password\": \"{$password}\"}"], + [Response::HTTP_BAD_REQUEST, '{"email": "example@example.com"}'], + [Response::HTTP_UNAUTHORIZED, '{"email": "not-exists@example.com", "password": "password"}'] + ]; + } + + /** + * @see PlayerController::loginAction + * @test + * + * @dataProvider loginActionDataProvider + * + * @param int $expectedStatusCode + * @param string $json + */ + public function loginAction(int $expectedStatusCode, string $json) + { + $client = $this->getUnauthorizedClient(); + $client->request( + Request::METHOD_POST, + '/api/player/login', + [], + [], + ['CONTENT_TYPE' => 'application/json', 'HTTP_accept' => 'application/json'], + $json + ); + + $response = $client->getResponse(); + + $this->assertSame($expectedStatusCode, $response->getStatusCode()); + } + + /** + * @see PlayerController::logoutAction + * @test + */ + public function logoutAction() + { + $client = $this->getAuthorizedClient(UsersFixture::TEST_PLAYER_EMAIL); + $client->request( + Request::METHOD_DELETE, + '/api/player/logout' + ); + + $sessionHash = $client->getServerParameter(static::AUTH_HEADER); + $response = $client->getResponse(); + + $this->assertEquals(Response::HTTP_ACCEPTED, $response->getStatusCode()); + + $session = static::$om->getRepository(UserSession::class)->findOneBy(['hash' => $sessionHash]); + static::assertNull($session); + } +} diff --git a/tests/phpunit/FoundationBundle/ORM/TimestampedTraitTest.php b/tests/phpunit/FoundationBundle/ORM/TimestampedTraitTest.php index 0e622ee..aba4733 100644 --- a/tests/phpunit/FoundationBundle/ORM/TimestampedTraitTest.php +++ b/tests/phpunit/FoundationBundle/ORM/TimestampedTraitTest.php @@ -17,8 +17,8 @@ class TimestampedTraitTest extends AbstractKernelTestSuite public function setTimestampSetOnPersist() { $result = MockFactory::getGameResultMock(2, 0); - $player = $result->getGame()->getBattlefields()[0]->getPlayer(); - $result->setPlayer($player); + $user = $result->getGame()->getBattlefields()[0]->getUser(); + $result->setUser($user); static::$om->persist($result->getGame()); $this->assertInstanceOf(\DateTime::class, $result->getTimestamp()); diff --git a/tests/phpunit/GameBundle/Controller/GameControllerTest.php b/tests/phpunit/GameBundle/Controller/GameControllerTest.php index fec1199..3ce778f 100644 --- a/tests/phpunit/GameBundle/Controller/GameControllerTest.php +++ b/tests/phpunit/GameBundle/Controller/GameControllerTest.php @@ -2,226 +2,133 @@ namespace EM\Tests\PHPUnit\GameBundle\Controller; -use EM\GameBundle\Model\CellModel; -use EM\GameBundle\Model\PlayerModel; +use EM\GameBundle\Entity\Battlefield; +use EM\GameBundle\Entity\Game; +use EM\FoundationBundle\Entity\User; +use EM\FoundationBundle\DataFixtures\ORM\UsersFixture; use EM\Tests\Environment\AbstractControllerTestCase; use EM\Tests\Environment\Cleaner\CellModelCleaner; +use Symfony\Component\Finder\Finder; +use Symfony\Component\Finder\SplFileInfo; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; /** * @see GameController */ class GameControllerTest extends AbstractControllerTestCase { - /** - * @see GameController::initAction - * @test - */ - public function unsuccessfulInitAction() + public function initActionProvider() : array { - foreach (['application/xml', 'application/json'] as $acceptHeader) { - $client = static::$client; - $client->request( - Request::METHOD_POST, - '/api/game-init', - [], - [], - ['CONTENT_TYPE' => 'application/json', 'HTTP_accept' => $acceptHeader] - ); - $this->assertUnsuccessfulResponse($client->getResponse()); + $suites = []; + $finder = new Finder(); + $finder->files()->in("{$this->getSharedFixturesDirectory()}/game-initiation-requests"); + + /** @var SplFileInfo $file */ + foreach ($finder as $file) { + $suites[$file->getFilename()] = [ + $file->getRelativePath() === 'invalid' ? Response::HTTP_BAD_REQUEST : Response::HTTP_CREATED, + $file->getContents() + ]; } + + return $suites; } /** - * @see GameController::initAction + * @see GameController::initAction * @test * - * @depends unsuccessfulInitAction + * @dataProvider initActionProvider + * + * @param int $expectedStatusCode + * @param string $content */ - public function successfulInitAction_JSON() + public function initAction(int $expectedStatusCode, string $content) { - $client = static::$client; + $client = $this->getAuthorizedClient(UsersFixture::TEST_PLAYER_EMAIL); $client->request( Request::METHOD_POST, '/api/game-init', [], [], ['CONTENT_TYPE' => 'application/json', 'HTTP_accept' => 'application/json'], - static::getSharedFixtureContent('game-initiation-requests/valid/valid-1-opponent-7x7.json') + $content ); - $this->assertSuccessfulJSONResponse($client->getResponse()); - - $response = json_decode($client->getResponse()->getContent()); - $this->assertInternalType('array', $response); - - foreach ($response as $battlefield) { - $this->assertInternalType('int', $battlefield->id); - $this->assertInstanceOf(\stdClass::class, $battlefield->player); - $this->assertInternalType('int', $battlefield->player->id); - $this->assertInternalType('int', $battlefield->player->flags); - $this->assertInternalType('string', $battlefield->player->name); - - $this->assertCount(49, (array)$battlefield->cells); - foreach ($battlefield->cells as $cell) { - $this->assertInternalType('int', $cell->id); - $this->assertInternalType('int', $cell->flags); - $this->assertInternalType('string', $cell->coordinate); - - /** as CPU fields should have CellModel::FLAG_NONE on initiation */ - $expected = $battlefield->player->flags == PlayerModel::FLAG_AI_CONTROLLED ? CellModel::FLAG_NONE : $cell->flags; - $this->assertEquals($expected, $cell->flags); - } - } + $this->assertEquals($expectedStatusCode, $client->getResponse()->getStatusCode()); + } - /** pass the response to the dependant class */ - return $response; + public function turnActionCoordinatesProvider() : array + { + return [ +// 'not existed cell' => [Response::HTTP_NOT_FOUND, 'A0'], + 'first request on A1 cell' => [Response::HTTP_OK, 'A1'], + 'second request on A1 cell' => [Response::HTTP_UNPROCESSABLE_ENTITY, 'A1'], + //'first request on A2 cell' => [Response::HTTP_OK, 'A2'], + //'second request on A2 cell' => [Response::HTTP_UNPROCESSABLE_ENTITY, 'A2'], + //'first request on A3 cell' => [Response::HTTP_OK, 'A3'], + //'second request on A3 cell' => [Response::HTTP_UNPROCESSABLE_ENTITY, 'A3'], + //'first request on A4 cell' => [Response::HTTP_OK, 'A4'], + //'second request on A4 cell' => [Response::HTTP_UNPROCESSABLE_ENTITY, 'A4'], + //'first request on A5 cell' => [Response::HTTP_OK, 'A5'], + //'second request on A5 cell' => [Response::HTTP_UNPROCESSABLE_ENTITY, 'A5'], + //'first request on A6 cell' => [Response::HTTP_OK, 'A6'], + //'second request on A6 cell' => [Response::HTTP_UNPROCESSABLE_ENTITY, 'A6'], + //'first request on A7 cell' => [Response::HTTP_OK, 'A7'], + //'second request on A7 cell' => [Response::HTTP_UNPROCESSABLE_ENTITY, 'A7'] + ]; } /** - * @see GameController::initAction + * @see GameController::turnAction * @test * - * @depends unsuccessfulInitAction + * @dataProvider turnActionCoordinatesProvider */ - public function successfulInitAction_XML() + public function unsuccessfulTurnActionOnNotExistingCell() { - $client = static::$client; + $client = $this->getAuthorizedClient(UsersFixture::TEST_PLAYER_EMAIL); $client->request( - Request::METHOD_POST, - '/api/game-init', + Request::METHOD_PATCH, + '/api/game-turn/cell-id/0', [], [], - ['CONTENT_TYPE' => 'application/json', 'HTTP_accept' => 'application/xml'], - static::getSharedFixtureContent('game-initiation-requests/valid/valid-1-opponent-7x7.json') + ['CONTENT_TYPE' => 'application/json', 'HTTP_accept' => 'application/json'] ); - - $this->assertSuccessfulXMLResponse($client->getResponse()); - - $response = simplexml_load_string($client->getResponse()->getContent(), 'SimpleXMLElement', LIBXML_NOCDATA); - $this->assertInstanceOf(\SimpleXMLElement::class, $response); - - foreach ($response as $battlefield) { - /** @var \SimpleXMLElement $battlefield */ - $this->assertInstanceOf(\SimpleXMLElement::class, $battlefield); - $this->assertEquals('battlefield', $battlefield->getName()); - - $player = $battlefield->player; - /** @var \SimpleXMLElement $player */ - $this->assertInstanceOf(\SimpleXMLElement::class, $player); - - $this->assertInternalType('string', (string)$player->id); - $this->assertInternalType('string', (string)$player->flags); - $this->assertInternalType('string', (string)$player->name); - - $cells = $battlefield->cells->children(); - $this->assertEquals(49, $cells->count()); - foreach ($cells as $cell) { - $this->assertInstanceOf(\SimpleXMLElement::class, $cell); - - $this->assertInternalType('string', (string)$cell->id); - $this->assertInternalType('string', (string)$cell->flags); - $this->assertInternalType('string', (string)$cell->coordinate); - - /** as CPU fields should have CellModel::FLAG_NONE on initiation */ - if ((string)$player->flags == PlayerModel::FLAG_AI_CONTROLLED) { - $this->assertEquals(CellModel::FLAG_NONE, (string)$cell->flags); - } else { - $this->assertContains((string)$cell->flags, [CellModel::FLAG_NONE, CellModel::FLAG_SHIP, CellModel::FLAG_DEAD_SHIP]); - } - } - } - } - - /** - * @see GameController::turnAction - * @test - * - * @depends successfulInitAction_JSON - * @depends successfulInitAction_XML - */ - public function unsuccessfulTurnActionOnNotExistingCell() - { - $client = static::$client; - foreach (['application/xml', 'application/json'] as $acceptHeader) { - $client->request( - Request::METHOD_PATCH, - '/api/game-turn/cell-id/0', - [], - [], - ['CONTENT_TYPE' => 'application/json', 'HTTP_accept' => $acceptHeader] - ); - $this->assertUnsuccessfulResponse($client->getResponse()); - } + $this->assertUnsuccessfulResponse($client->getResponse()); } /** * simulate human interaction until game has been finished * - * @see GameController::turnAction + * @see GameController::turnAction * @test * - * @depends successfulInitAction_JSON + * @dataProvider turnActionCoordinatesProvider * - * @param \stdClass[] $response + * @param int $expectedStatusCode + * @param string $coordinate */ - public function successfulTurnAction(array $response) + public function turnAction(int $expectedStatusCode, string $coordinate) { - foreach ($response as $battlefield) { - if ($battlefield->player->flags !== PlayerModel::FLAG_AI_CONTROLLED) { - continue; - } - - foreach ($battlefield->cells as $cell) { - CellModelCleaner::resetChangedCells(); + CellModelCleaner::resetChangedCells(); - $client = static::$client; - $client->request( - Request::METHOD_PATCH, - "/api/game-turn/cell-id/{$cell->id}", - [], - [], - ['CONTENT_TYPE' => 'application/json', 'HTTP_accept' => 'application/json'] - ); - $this->assertSuccessfulJSONResponse($client->getResponse()); + $game = static::$om->getRepository(Game::class)->findBy([], ['id' => 'ASC'])[0]; + $player = static::$om->getRepository(Player::class)->findOneBy(['email' => 'CPU 0']); + $battlefield = static::$om->getRepository(Battlefield::class)->findOneBy(['player' => $player, 'game' => $game]); - $parsed = json_decode($client->getResponse()->getContent()); - if (isset($parsed->result)) { - return; - } - } - } - } + $cell = $battlefield->getCellByCoordinate($coordinate); - /** - * simulate human interaction until game has been finished - * - * @see GameController::turnAction - * @test - * - * @depends successfulInitAction_JSON - * - * @param \stdClass[] $response - */ - public function unsuccessfulTurnActionOnDeadCell(array $response) - { - foreach ($response as $battlefield) { - if ($battlefield->player->flags === PlayerModel::FLAG_AI_CONTROLLED) { - foreach ($battlefield->cells as $cell) { - CellModelCleaner::resetChangedCells(); + $client = $this->getAuthorizedClient(UsersFixture::TEST_PLAYER_EMAIL); + $client->request( + Request::METHOD_PATCH, + "/api/game-turn/cell-id/{$cell->getId()}", + [], + [], + ['CONTENT_TYPE' => 'application/json', 'HTTP_accept' => 'application/json'] + ); - $client = static::$client; - $client->request( - Request::METHOD_PATCH, - "/api/game-turn/cell-id/{$cell->id}", - [], - [], - ['CONTENT_TYPE' => 'application/json', 'HTTP_accept' => 'application/json'] - ); - $this->assertUnsuccessfulResponse($client->getResponse()); - break 2; - } - } - } + $this->assertEquals($expectedStatusCode, $client->getResponse()->getStatusCode()); } } diff --git a/tests/phpunit/GameBundle/Controller/GameResultControllerTest.php b/tests/phpunit/GameBundle/Controller/GameResultControllerTest.php index f14f962..0c8c5aa 100644 --- a/tests/phpunit/GameBundle/Controller/GameResultControllerTest.php +++ b/tests/phpunit/GameBundle/Controller/GameResultControllerTest.php @@ -3,6 +3,7 @@ namespace EM\Tests\PHPUnit\GameBundle\Controller; use EM\GameBundle\Controller\GameResultController; +use EM\GameBundle\DataFixtures\ORM\UsersFixture; use EM\Tests\Environment\AbstractControllerTestCase; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -34,7 +35,7 @@ public function orderedByDateActionDataProvider() : array */ public function orderedByDateAction($pageId, int $expectedResponseCode) { - $client = static::$client; + $client = $this->getAuthorizedClient(UsersFixture::TEST_PLAYER_EMAIL); $client->request( Request::METHOD_GET, "/api/game-results/page/{$pageId}", diff --git a/tests/phpunit/GameBundle/Model/GameResultModelTest.php b/tests/phpunit/GameBundle/Model/GameResultModelTest.php index ac6432f..aeeecb5 100644 --- a/tests/phpunit/GameBundle/Model/GameResultModelTest.php +++ b/tests/phpunit/GameBundle/Model/GameResultModelTest.php @@ -36,8 +36,8 @@ public function prepareResponse() /** populated 2 full pages of Game Results + 1 result */ for ($i = 0; $i < $perPage * 2 + 1; $i++) { $result = MockFactory::getGameResultMock(2, 0); - $player = $result->getGame()->getBattlefields()[0]->getPlayer(); - $result->setPlayer($player); + $user = $result->getGame()->getBattlefields()[0]->getUser(); + $result->setUser($user); static::$om->persist($result->getGame()); } diff --git a/tests/phpunit/GameBundle/Model/PlayerModelTest.php b/tests/phpunit/GameBundle/Model/PlayerModelTest.php deleted file mode 100644 index 99478f9..0000000 --- a/tests/phpunit/GameBundle/Model/PlayerModelTest.php +++ /dev/null @@ -1,162 +0,0 @@ -get('em.game_bundle.service.player_model'); - } - - /*********************************** STATIC HELPERS ***********************************/ - /** - * should return false if player is not marked by @see PlayerModel::FLAG_AI_CONTROLLED flag - * - * @see PlayerModel::isAIControlled - * @test - */ - public function isAIControlledOnFlagNone() - { - $this->assertFalse(PlayerModel::isAIControlled(MockFactory::getPlayerMock(''))); - } - - /** - * should return true if player marked by @see PlayerModel::FLAG_AI_CONTROLLED flag - * - * @see PlayerModel::isAIControlled - * @test - */ - public function isAIControlledOnFlagAIControlled() - { - $this->assertTrue(PlayerModel::isAIControlled(MockFactory::getAIPlayerMock(''))); - } - - /*********************************** AI CONTROLLED PLAYER ***********************************/ - /** - * should return existing player controlled by AI, as it existed before - * - * @see PlayerModel::createOnRequestAIControlled - * @test - * - * @depends isAIControlledOnFlagNone - * @requires isAIControlledOnFlagAIControlled - */ - public function createOnRequestAIControlledOnExistingPlayer() - { - $player = static::$playerModel->createOnRequestAIControlled(LoadPlayerData::TEST_AI_CONTROLLED_PLAYER_EMAIL); - - $this->assertEquals(LoadPlayerData::TEST_AI_CONTROLLED_PLAYER_EMAIL, $player->getName()); - $this->assertTrue(PlayerModel::isAIControlled($player)); - - /** because player is already persisted */ - $this->assertNotNull($player->getId()); - } - - /** - * should return new player controlled by AI, as it didn't exist before - * - * @see PlayerModel::createOnRequestAIControlled - * @test - * - * @depends isAIControlledOnFlagNone - * @requires isAIControlledOnFlagAIControlled - */ - public function createOnRequestAIControlledOnNonExistingPlayer() - { - $player = static::$playerModel->createOnRequestAIControlled('NON-EXISTING-CPU-PLAYER'); - - $this->assertEquals('NON-EXISTING-CPU-PLAYER', $player->getName()); - $this->assertTrue(PlayerModel::isAIControlled($player)); - - /** because player is not persisted yet */ - $this->assertNull($player->getId()); - } - - /** - * should throw exception, because existed Player is not controlled By AI - * - * @see PlayerModel::createOnRequestAIControlled - * @test - * - * @expectedException \EM\GameBundle\Exception\PlayerException - * - * @depends isAIControlledOnFlagNone - * @requires isAIControlledOnFlagAIControlled - */ - public function createOnRequestAIControlledOnNonExistingHumanPlayer() - { - static::$playerModel->createOnRequestAIControlled(LoadPlayerData::TEST_HUMAN_PLAYER_EMAIL); - } - /*********************************** HUMAN PLAYER ***********************************/ - /** - * should return existing player controlled by Human, as it existed before - * - * @see PlayerModel::createOnRequestHumanControlled - * @test - * - * @depends isAIControlledOnFlagNone - * @requires isAIControlledOnFlagAIControlled - */ - public function createOnRequestHumanControlledOnExistingPlayer() - { - $player = static::$playerModel->createOnRequestHumanControlled(LoadPlayerData::TEST_HUMAN_PLAYER_EMAIL); - - $this->assertEquals(LoadPlayerData::TEST_HUMAN_PLAYER_EMAIL, $player->getName()); - $this->assertFalse(PlayerModel::isAIControlled($player)); - - /** because player is already persisted */ - $this->assertNotNull($player->getId()); - } - - /** - * should return new player controlled by Human, as it didn't exist before - * - * @see PlayerModel::createOnRequestHumanControlled - * @test - * - * @depends isAIControlledOnFlagNone - * @requires isAIControlledOnFlagAIControlled - */ - public function createOnRequestHumanControlledOnNonExistingPlayer() - { - $player = static::$playerModel->createOnRequestHumanControlled('NON-EXISTING-HUMAN-PLAYER'); - - $this->assertEquals('NON-EXISTING-HUMAN-PLAYER', $player->getName()); - $this->assertFalse(PlayerModel::isAIControlled($player)); - - /** because player is not persisted yet */ - $this->assertNull($player->getId()); - } - - /** - * should throw exception, because existed Player is not controlled By AI - * - * @see PlayerModel::createOnRequestHumanControlled - * @test - * - * @expectedException \EM\GameBundle\Exception\PlayerException - * - * @depends isAIControlledOnFlagNone - * @requires isAIControlledOnFlagAIControlled - */ - public function createOnRequestHumanControlledOnNonExistingAIPlayer() - { - static::$playerModel->createOnRequestHumanControlled(LoadPlayerData::TEST_AI_CONTROLLED_PLAYER_EMAIL); - } -} diff --git a/tests/phpunit/GameBundle/Model/UserModelTest.php b/tests/phpunit/GameBundle/Model/UserModelTest.php new file mode 100644 index 0000000..84e1c54 --- /dev/null +++ b/tests/phpunit/GameBundle/Model/UserModelTest.php @@ -0,0 +1,107 @@ +get('em.foundation_bundle.model.user'); + } + + public function isAIControlledDataProvider() : array + { + return [ + [false, MockFactory::getUserMock('')], + [true, MockFactory::getAIUserMock('')] + ]; + } + + /** + * should return true if player marked by @see UserModel::FLAG_AI_CONTROLLED flag otherwise false + * + * @see UserModel::isAIControlled + * @test + * + * @dataProvider isAIControlledDataProvider + * + * @param bool $result + * @param User $user + */ + public function isAIControlled(bool $result, User $user) + { + $this->assertSame($result, UserModel::isAIControlled($user)); + } + + public function createOnRequestAIControlledDataProvider() : array + { + return [ + [UsersFixture::TEST_AI_PLAYER_EMAIL, 'int'], + [UsersFixture::TEST_AI_PLAYER_EMAIL . 'NON-EXISTS', 'null'] + ]; + } + + /** + * should return new player controlled by AI, as it didn't exist before + * + * @see UserModel::createOnRequestAIControlled + * @test + * + * @dataProvider createOnRequestAIControlledDataProvider + * + * @param string $username + * @param string $idFieldType + */ + public function createOnRequestAIControlled(string $username, string $idFieldType) + { + $player = static::$UserModel->createOnRequestAIControlled($username); + + $this->assertInternalType($idFieldType, $player->getId()); + $this->assertTrue(UserModel::isAIControlled($player)); + $this->assertSame($username, $player->getEmail()); + } + + public function createPlayerDataProvider() : array + { + return [ + ['AI controlled', '', UserModel::FLAG_AI_CONTROLLED], + ['human controlled', '', UserModel::FLAG_NONE] + ]; + } + + /** + * @see UserModel::createOnRequestHumanControlled + * @test + * + * @dataProvider createPlayerDataProvider + * + * @param string $username + * @param string $password + * @param int $flag + */ + public function createPlayer(string $username, string $password, int $flag) + { + $player = static::$UserModel->createPlayer($username, $password, $flag); + + /** because player is not persisted yet */ + $this->assertNull($player->getId()); + $this->assertSame($username, $player->getEmail()); + $this->assertSame($flag, $player->getFlags()); + } +} diff --git a/tests/phpunit/GameBundle/Response/GameInitiationResponseTest.php b/tests/phpunit/GameBundle/Response/GameInitiationResponseTest.php index 8e4a7cb..033aa31 100644 --- a/tests/phpunit/GameBundle/Response/GameInitiationResponseTest.php +++ b/tests/phpunit/GameBundle/Response/GameInitiationResponseTest.php @@ -2,7 +2,7 @@ namespace EM\Tests\PHPUnit\GameBundle\Response; use EM\GameBundle\Model\CellModel; -use EM\GameBundle\Model\PlayerModel; +use EM\FoundationBundle\Model\UserModel; use EM\GameBundle\Response\GameInitiationResponse; use EM\Tests\Environment\Factory\MockFactory; @@ -19,13 +19,13 @@ public function addBattlefield() { $game = MockFactory::getGameMock(); $game->getBattlefields()[0] - ->setPlayer(MockFactory::getAIPlayerMock('')) + ->setUser(MockFactory::getAIUserMock('')) ->getCellByCoordinate('A1')->setFlags(CellModel::FLAG_SHIP); $request = new GameInitiationResponse($game->getBattlefields()); foreach ($request->getBattlefields() as $battlefield) { foreach ($battlefield->getCells() as $cell) { - if (PlayerModel::isAIControlled($battlefield->getPlayer())) { + if (UserModel::isAIControlled($battlefield->getUser())) { $this->assertEquals(CellModel::FLAG_NONE, $cell->getFlags()); } } diff --git a/tests/phpunit/GameBundle/Service/GameSystem/GameBuilderTest.php b/tests/phpunit/GameBundle/Service/GameSystem/GameBuilderTest.php index 3312758..1de902f 100644 --- a/tests/phpunit/GameBundle/Service/GameSystem/GameBuilderTest.php +++ b/tests/phpunit/GameBundle/Service/GameSystem/GameBuilderTest.php @@ -4,7 +4,7 @@ use EM\GameBundle\Model\BattlefieldModel; use EM\GameBundle\Model\CellModel; -use EM\GameBundle\Model\PlayerModel; +use EM\FoundationBundle\Model\UserModel; use EM\GameBundle\Request\GameInitiationRequest; use EM\GameBundle\Service\GameSystem\GameBuilder; use EM\Tests\Environment\AbstractKernelTestSuite; @@ -47,7 +47,7 @@ public function attachAIBattlefields() foreach ($game->getBattlefields() as $battlefield) { $this->assertCount(49, $battlefield->getCells()); - $this->assertTrue(PlayerModel::isAIControlled($battlefield->getPlayer())); + $this->assertTrue(UserModel::isAIControlled($battlefield->getPlayer())); $this->assertTrue(BattlefieldModel::hasUnfinishedShips($battlefield)); foreach ($battlefield->getCells() as $coordinate => $cell) { @@ -71,7 +71,7 @@ public function buildGame() { $request = new GameInitiationRequest($this->getSharedFixtureContent('game-initiation-requests/valid/valid-1-opponent-7x7.json')); - $game = static::$gameBuilder->buildGame($request); + $game = static::$gameBuilder->buildGame($request, MockFactory::getUserMock('')); $this->assertCount(2, $game->getBattlefields()); foreach ($game->getBattlefields() as $battlefield) { diff --git a/tests/phpunit/GameBundle/Service/GameSystem/GameProcessorTest.php b/tests/phpunit/GameBundle/Service/GameSystem/GameProcessorTest.php index 22f2626..41754d0 100644 --- a/tests/phpunit/GameBundle/Service/GameSystem/GameProcessorTest.php +++ b/tests/phpunit/GameBundle/Service/GameSystem/GameProcessorTest.php @@ -4,7 +4,7 @@ use EM\GameBundle\Entity\Battlefield; use EM\GameBundle\Entity\GameResult; -use EM\FoundationBundle\Entity\Player; +use EM\FoundationBundle\Entity\User; use EM\GameBundle\Exception\GameProcessorException; use EM\GameBundle\Model\BattlefieldModel; use EM\GameBundle\Model\CellModel; @@ -49,11 +49,11 @@ public function processPlayerTurnOnBattlefieldExpectedExceptionOnOwnPlayerBattle public function processPlayerTurnOnBattlefieldOnNotWin() { $battlefield = MockFactory::getBattlefieldMock() - ->setPlayer(MockFactory::getAIPlayerMock('')); + ->setUser(MockFactory::getAIUserMock('')); $battlefield->getCellByCoordinate('A1')->setFlags(CellModel::FLAG_SHIP); $battlefield->getCellByCoordinate('A2')->setFlags(CellModel::FLAG_SHIP); - $this->assertFalse($this->processPlayerTurnOnBattlefield($battlefield, MockFactory::getPlayerMock(''))); + $this->assertFalse($this->processPlayerTurnOnBattlefield($battlefield, MockFactory::getUserMock(''))); } /** @@ -63,22 +63,22 @@ public function processPlayerTurnOnBattlefieldOnNotWin() public function processPlayerTurnOnBattlefieldToWin() { $battlefield = MockFactory::getBattlefieldMock() - ->setPlayer(MockFactory::getAIPlayerMock('')); + ->setUser(MockFactory::getAIUserMock('')); $battlefield->getCellByCoordinate('A1')->setFlags(CellModel::FLAG_SHIP); - $this->assertTrue($this->processPlayerTurnOnBattlefield($battlefield, MockFactory::getPlayerMock(''))); + $this->assertTrue($this->processPlayerTurnOnBattlefield($battlefield, MockFactory::getUserMock(''))); } /** * @see GameProcessor::processPlayerTurnOnBattlefield * * @param Battlefield $battlefield - * @param Player $attacker + * @param User $attacker * * @return bool * @throws GameProcessorException */ - private function processPlayerTurnOnBattlefield(Battlefield $battlefield, Player $attacker) + private function processPlayerTurnOnBattlefield(Battlefield $battlefield, User $attacker) { return $this->invokeMethod( static::$gameProcessor, @@ -112,7 +112,7 @@ public function processTurnToNotWin() { $game = MockFactory::getGameMock(); $aiBattlefield = $game->getBattlefields()[0]; - $aiBattlefield->setPlayer(MockFactory::getAIPlayerMock('')); + $aiBattlefield->setUser(MockFactory::getAIUserMock('')); foreach ($game->getBattlefields() as $battlefield) { $battlefield->getCellByCoordinate('A1')->addFlag(CellModel::FLAG_SHIP); @@ -147,7 +147,7 @@ public function processTurnToWin() $game->getBattlefields()[0]->getCellByCoordinate('A1')->addFlag(CellModel::FLAG_SHIP); $game->getBattlefields()[0]->getCellByCoordinate('A2')->addFlag(CellModel::FLAG_SHIP); - $game->getBattlefields()[1]->setPlayer(MockFactory::getAIPlayerMock('')); + $game->getBattlefields()[1]->setUser(MockFactory::getAIUserMock('')); $game->getBattlefields()[1]->getCellByCoordinate('A1')->addFlag(CellModel::FLAG_SHIP); $game = static::$gameProcessor->processTurn($game->getBattlefields()[1]->getCellByCoordinate('A1')); diff --git a/tests/shared-environment/AbstractControllerTestCase.php b/tests/shared-environment/AbstractControllerTestCase.php index e959fb0..7b324de 100644 --- a/tests/shared-environment/AbstractControllerTestCase.php +++ b/tests/shared-environment/AbstractControllerTestCase.php @@ -2,6 +2,8 @@ namespace EM\Tests\Environment; +use EM\FoundationBundle\Entity\User; +use EM\FoundationBundle\Entity\UserSession; use EM\Tests\Environment\AssertionSuite\ResponseAssertionSuites; use Symfony\Bundle\FrameworkBundle\Client; @@ -12,14 +14,57 @@ abstract class AbstractControllerTestCase extends AbstractKernelTestSuite { use ResponseAssertionSuites; /** - * @var Client + * @var string */ - protected static $client; + const AUTH_HEADER = 'HTTP_x-wsse'; - public static function setUpBeforeClass() + protected function getAuthorizedClient(string $username) : Client { - parent::setUpBeforeClass(); + $username = trim($username); + if (empty($username)) { + return $this->getUnauthorizedClient(); + } - static::$client = static::$container->get('test.client'); + $player = static::$om->getRepository(User::class)->findOneBy(['email' => $username]); + if (null === $player) { + throw new \Exception("user with username: {$username} not found"); + } + + $session = $this->mockAuthorizedSession($player); + + return $this->createClientWithAuthHeader($session->getHash()); + } + + protected function getUnauthorizedClient() : Client + { + return $this->createClientWithAuthHeader(''); + } + + private function createClientWithAuthHeader(string $token) : Client + { + $client = static::$container->get('test.client'); + $client->restart(); + + $client->setServerParameters([ + /** API auth */ + static::AUTH_HEADER => $token + ]); + + return $client; + } + + private function mockAuthorizedSession(User $user) : UserSession + { + $hash = sha1(microtime(true)); + $session = new UserSession(); + $session->setHash($hash); + $session->setUser($user); + $user->setPasswordHash($hash); + + static::$om->persist($session); + static::$om->persist($user); + static::$om->flush(); + + return $session; } } diff --git a/tests/shared-environment/AbstractKernelTestSuite.php b/tests/shared-environment/AbstractKernelTestSuite.php index d91333d..346ddf7 100644 --- a/tests/shared-environment/AbstractKernelTestSuite.php +++ b/tests/shared-environment/AbstractKernelTestSuite.php @@ -68,7 +68,7 @@ protected function getSharedFixtureContent(string $filename) : string return file_get_contents(static::getSharedFixturesDirectory() . "/{$filename}"); } - protected function getSharedFixturesDirectory() : string + static public function getSharedFixturesDirectory() : string { return dirname(__DIR__) . '/shared-fixtures'; } diff --git a/tests/shared-environment/Factory/MockFactory.php b/tests/shared-environment/Factory/MockFactory.php index c2bac5b..bda6e90 100644 --- a/tests/shared-environment/Factory/MockFactory.php +++ b/tests/shared-environment/Factory/MockFactory.php @@ -2,14 +2,14 @@ namespace EM\Tests\Environment\Factory; +use EM\FoundationBundle\Entity\User; +use EM\FoundationBundle\Model\UserModel; use EM\GameBundle\Entity\Battlefield; use EM\GameBundle\Entity\Cell; use EM\GameBundle\Entity\Game; use EM\GameBundle\Entity\GameResult; -use EM\FoundationBundle\Entity\Player; use EM\GameBundle\Model\BattlefieldModel; use EM\GameBundle\Model\CellModel; -use EM\GameBundle\Model\PlayerModel; /** * @since 17.3 @@ -19,7 +19,7 @@ class MockFactory public static function getBattlefieldMock(int $size = 7) : Battlefield { $battlefield = BattlefieldModel::generate($size) - ->setPlayer(static::getPlayerMock('')); + ->setUser(static::getUserMock('')); return $battlefield; } @@ -43,22 +43,23 @@ public static function getGameMock(int $players = 2, int $size = 7) : Game public static function getGameResultMock(int $players = 2, int $battlefieldSize = 7) : GameResult { - $game = static::getGameMock($players, $battlefieldSize); - $gameResult = (new GameResult()); - $game->setResult($gameResult); + $game = static::getGameMock($players, $battlefieldSize); + $result = (new GameResult()); + $game->setResult($result); - return $gameResult; + return $result; } - public static function getPlayerMock(string $name, int $flags = PlayerModel::FLAG_NONE) : Player + public static function getUserMock(string $email, int $flags = UserModel::FLAG_NONE) : User { - return (new Player()) - ->setName($name) + return (new User()) + ->setEmail($email) + ->setPasswordHash(sha1('mockedPassword')) ->setFlags($flags); } - public static function getAIPlayerMock(string $name) : Player + public static function getAIUserMock(string $email) : User { - return static::getPlayerMock($name, PlayerModel::FLAG_AI_CONTROLLED); + return static::getUserMock($email, UserModel::FLAG_AI_CONTROLLED); } } diff --git a/tests/shared-fixtures/game-initiation-requests/invalid/invalid-coordinates-empty.json b/tests/shared-fixtures/game-initiation-requests/invalid/invalid-coordinates-empty.json index e4fb2b2..a2f18cf 100644 --- a/tests/shared-fixtures/game-initiation-requests/invalid/invalid-coordinates-empty.json +++ b/tests/shared-fixtures/game-initiation-requests/invalid/invalid-coordinates-empty.json @@ -1,5 +1,4 @@ { - "playerName": "Human", "opponents": 1, "size": 10, "coordinates": [ diff --git a/tests/shared-fixtures/game-initiation-requests/invalid/invalid-coordinates.json b/tests/shared-fixtures/game-initiation-requests/invalid/invalid-coordinates.json index 48b906e..a5c2a73 100644 --- a/tests/shared-fixtures/game-initiation-requests/invalid/invalid-coordinates.json +++ b/tests/shared-fixtures/game-initiation-requests/invalid/invalid-coordinates.json @@ -1,5 +1,4 @@ { - "playerName": "Human", "opponents": 1, "size": 10, "coordinates": [ diff --git a/tests/shared-fixtures/game-initiation-requests/invalid/invalid-empty-request.json b/tests/shared-fixtures/game-initiation-requests/invalid/invalid-empty-request.json new file mode 100644 index 0000000..e69de29 diff --git a/tests/shared-fixtures/game-initiation-requests/invalid/invalid-game-size.json b/tests/shared-fixtures/game-initiation-requests/invalid/invalid-game-size.json index 1fdc2f0..2dbf215 100644 --- a/tests/shared-fixtures/game-initiation-requests/invalid/invalid-game-size.json +++ b/tests/shared-fixtures/game-initiation-requests/invalid/invalid-game-size.json @@ -1,5 +1,4 @@ { - "playerName": "Human", "opponents": 1, "size": 100500, "coordinates": [ diff --git a/tests/shared-fixtures/game-initiation-requests/invalid/invalid-opponents-amount.json b/tests/shared-fixtures/game-initiation-requests/invalid/invalid-opponents-amount.json index 61aaeed..aa52ada 100644 --- a/tests/shared-fixtures/game-initiation-requests/invalid/invalid-opponents-amount.json +++ b/tests/shared-fixtures/game-initiation-requests/invalid/invalid-opponents-amount.json @@ -1,5 +1,4 @@ { - "playerName": "Human", "opponents": 0, "size": 7, "coordinates": [ diff --git a/tests/shared-fixtures/game-initiation-requests/invalid/invalid-player-name-empty.json b/tests/shared-fixtures/game-initiation-requests/invalid/invalid-player-name-empty.json deleted file mode 100644 index 63bbdca..0000000 --- a/tests/shared-fixtures/game-initiation-requests/invalid/invalid-player-name-empty.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "playerName": "", - "opponents": 1, - "size": 7, - "coordinates": [ - "A1", - "C1", - "D1", - "E1", - "F1", - "A2", - "A3", - "C3", - "F3", - "C4", - "C5", - "G5", - "G6" - ] -} diff --git a/tests/shared-fixtures/game-initiation-requests/invalid/invalid-structure-missing-coordinates.json b/tests/shared-fixtures/game-initiation-requests/invalid/invalid-structure-missing-coordinates.json index 822a99e..f15ae74 100644 --- a/tests/shared-fixtures/game-initiation-requests/invalid/invalid-structure-missing-coordinates.json +++ b/tests/shared-fixtures/game-initiation-requests/invalid/invalid-structure-missing-coordinates.json @@ -1,5 +1,4 @@ { - "playerName": "Human", "opponents": 1, "size": 7 } diff --git a/tests/shared-fixtures/game-initiation-requests/invalid/invalid-structure-missing-game-size.json b/tests/shared-fixtures/game-initiation-requests/invalid/invalid-structure-missing-game-size.json index 5a70f5b..e2378a3 100644 --- a/tests/shared-fixtures/game-initiation-requests/invalid/invalid-structure-missing-game-size.json +++ b/tests/shared-fixtures/game-initiation-requests/invalid/invalid-structure-missing-game-size.json @@ -1,5 +1,4 @@ { - "playerName": "Human", "opponents": 1, "coordinates": [ "A1", diff --git a/tests/shared-fixtures/game-initiation-requests/invalid/invalid-structure-missing-opponents.json b/tests/shared-fixtures/game-initiation-requests/invalid/invalid-structure-missing-opponents.json index 6b9f9b1..aef5675 100644 --- a/tests/shared-fixtures/game-initiation-requests/invalid/invalid-structure-missing-opponents.json +++ b/tests/shared-fixtures/game-initiation-requests/invalid/invalid-structure-missing-opponents.json @@ -1,5 +1,4 @@ { - "playerName": "Human", "size": 7, "coordinates": [ "A1", diff --git a/tests/shared-fixtures/game-initiation-requests/invalid/invalid-structure-missing-player-name.json b/tests/shared-fixtures/game-initiation-requests/invalid/invalid-structure-missing-player-name.json deleted file mode 100644 index 4081ad1..0000000 --- a/tests/shared-fixtures/game-initiation-requests/invalid/invalid-structure-missing-player-name.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "opponents": 1, - "size": 7, - "coordinates": [ - "A1", - "C1", - "D1", - "E1", - "F1", - "A2", - "A3", - "C3", - "F3", - "C4", - "C5", - "G5", - "G6" - ] -} diff --git a/tests/shared-fixtures/game-initiation-requests/valid/valid-1-opponent-7x7.json b/tests/shared-fixtures/game-initiation-requests/valid/valid-1-opponent-7x7.json index 215b507..4081ad1 100644 --- a/tests/shared-fixtures/game-initiation-requests/valid/valid-1-opponent-7x7.json +++ b/tests/shared-fixtures/game-initiation-requests/valid/valid-1-opponent-7x7.json @@ -1,5 +1,4 @@ { - "playerName": "Human", "opponents": 1, "size": 7, "coordinates": [