Compare commits

..

14 Commits

Author SHA1 Message Date
Alexey Eschenko 07f89712d9 Merged in composer_update (pull request #27)
composer update.
2020-04-29 14:22:11 +00:00
Alexey Skobkin fd09f389f7
composer update. 2020-04-29 16:55:02 +03:00
Alexey Skobkin b6f7ac8ec5 Fixing UserRepositoryTest according to new test users. 2019-04-03 20:40:09 +03:00
Alexey Skobkin f598864d4d Crutch-fixing PostController::showAction() exception handling with 404 instead of 403 exception. 2019-04-03 20:34:16 +03:00
Alexey Skobkin 44c4158602 Fixing automatic replacing of AccessDeniedExceptions with InsufficientAuthenticationException in Symfony\Component\Security\Http\Firewall\ExceptionListener::handleAccessDeniedException(). 2019-04-03 20:16:34 +03:00
Alexey Skobkin aa751bbbc1 Fixing MainControllerTest::testAjaxUserAutoCompleteHasValidUserObjectsForUnnamedUser() in case of user's name in null instead of empty string. 2019-04-03 19:43:32 +03:00
Alexey Skobkin 9e5f59a2b2 Fixina small MainControllerTest problems. 2019-04-03 19:38:57 +03:00
Alexey Skobkin 7fcdcbf728 Fixing AJAX data deserialization in the MainControllerTest. 2019-04-03 19:34:01 +03:00
Alexey Skobkin 60dcc5e955 Fixing MainControllerTest dependencies. 2019-04-03 19:28:18 +03:00
Alexey Skobkin c3605b2db1 Fixing MainControllerTest::{testAjaxUserAutoCompleteHasOptions, testFindUsersLikeLogin}() and adding new tests for user without name. 2019-04-03 19:19:03 +03:00
Alexey Skobkin b455a6c8e7 Adding new tests in PostControllerTest to check for potential private post leakage. 2019-04-03 18:55:29 +03:00
Alexey Skobkin 5e8935ce66 Fixing PostController::showAction() exception on private author's post. 2019-04-03 18:52:36 +03:00
Alexey Skobkin d9c0673445 Fixing user and post data fixtures to fix PostControllerTest::testShortPostPageIsOk() according to previous privacy fix. New post for more advanced privacy test-cases added. Test must be written though. 2019-04-03 18:38:53 +03:00
Alexey Skobkin 0c004085fd Fixing privacy in PostController::showAction(). 2019-04-03 18:07:47 +03:00
38 changed files with 1525 additions and 1821 deletions

View File

@ -19,7 +19,6 @@ class AppKernel extends Kernel
new Symfony\Bundle\WebServerBundle\WebServerBundle(),
new JMS\SerializerBundle\JMSSerializerBundle(),
new Csa\Bundle\GuzzleBundle\CsaGuzzleBundle(),
new Leezy\PheanstalkBundle\LeezyPheanstalkBundle(),
new Ob\HighchartsBundle\ObHighchartsBundle(),
new Knp\Bundle\MarkdownBundle\KnpMarkdownBundle(),
new Knp\Bundle\PaginatorBundle\KnpPaginatorBundle(),

View File

@ -1,51 +0,0 @@
<?php
namespace Application\Migrations;
use Doctrine\DBAL\Migrations\AbstractMigration;
use Doctrine\DBAL\Schema\Schema;
/**
* Issue #44 - Post and Comment schema refactoring.
* - Post subscription status added.
* - Comments parent-child relations removed.
* - User login index
* - Other adjustments
*/
class Version20180427143940 extends AbstractMigration
{
public function up(Schema $schema)
{
// this up() migration is auto-generated, please modify it to your needs
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'postgresql', 'Migration can only be executed safely on \'postgresql\'.');
$this->addSql('ALTER TABLE posts.posts ADD is_subscribed BOOLEAN DEFAULT FALSE NOT NULL');
// Removing parent_id constraint and index
$this->addSql('ALTER TABLE posts.comments DROP CONSTRAINT fk_62899975727aca70');
$this->addSql('DROP INDEX posts.idx_62899975727aca70');
$this->addSql('ALTER TABLE posts.comments DROP parent_id');
$this->addSql('ALTER TABLE posts.comments ADD to_number INT');
$this->addSql('ALTER TABLE posts.comments ALTER number TYPE INT');
$this->addSql('ALTER TABLE posts.comments ALTER number DROP DEFAULT');
$this->addSql('CREATE UNIQUE INDEX UNIQ_6289997596901F54 ON posts.comments (number)');
$this->addSql('CREATE INDEX idx_user_login ON users.users (login)');
}
public function down(Schema $schema)
{
// this down() migration is auto-generated, please modify it to your needs
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'postgresql', 'Migration can only be executed safely on \'postgresql\'.');
$this->addSql('ALTER TABLE posts.posts DROP is_subscribed');
$this->addSql('DROP INDEX posts.UNIQ_6289997596901F54');
$this->addSql('ALTER TABLE posts.comments ADD parent_id INT DEFAULT NULL');
$this->addSql('ALTER TABLE posts.comments DROP to_number');
$this->addSql('ALTER TABLE posts.comments ALTER number TYPE SMALLINT');
$this->addSql('ALTER TABLE posts.comments ALTER number DROP DEFAULT');
$this->addSql('ALTER TABLE posts.comments ADD CONSTRAINT fk_62899975727aca70 FOREIGN KEY (parent_id) REFERENCES posts.comments (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('CREATE INDEX idx_62899975727aca70 ON posts.comments (parent_id)');
$this->addSql('DROP INDEX users.idx_user_login');
}
}

View File

@ -1,28 +0,0 @@
<?php declare(strict_types=1);
namespace Application\Migrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20190227233244 extends AbstractMigration
{
public function up(Schema $schema) : void
{
// this up() migration is auto-generated, please modify it to your needs
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'postgresql', 'Migration can only be executed safely on \'postgresql\'.');
$this->addSql('CREATE UNIQUE INDEX unique_post_id_comment_number ON posts.comments (post_id, number)');
}
public function down(Schema $schema) : void
{
// this down() migration is auto-generated, please modify it to your needs
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'postgresql', 'Migration can only be executed safely on \'postgresql\'.');
$this->addSql('DROP INDEX posts.unique_post_id_comment_number');
}
}

View File

@ -71,13 +71,6 @@ doctrine_migrations:
table_name: migration_versions
name: Application Migrations
leezy_pheanstalk:
pheanstalks:
primary:
server: "%beanstalkd_host%"
port: "%beanstalkd_port%"
default: true
# Swiftmailer Configuration
swiftmailer:
transport: "%mailer_transport%"

View File

@ -6,11 +6,6 @@ parameters:
database_user: point
database_password: ~
# Message Queue settings
beanstalkd_host: 'localhost'
beanstalkd_port: 11300
beanstalkd_ws_updates_tube: 'point-websocket-updates'
mailer_transport: smtp
mailer_host: 127.0.0.1
mailer_user: ~

View File

@ -9,4 +9,4 @@ security:
security: false
default:
anonymous: ~
anonymous: true

View File

@ -101,13 +101,6 @@ services:
# Send message
Skobkin\Bundle\PointToolsBundle\Command\TelegramSendMessageCommand:
tags: [{ name: console.command }]
# WebSocket MQ processing
Skobkin\Bundle\PointToolsBundle\Command\ProcessWebsocketUpdatesCommand:
arguments:
$bsClient: '@leezy.pheanstalk.primary'
$bsTubeName: '%beanstalkd_ws_updates_tube%'
tags:
- { name: console.command }
# Entity repositories as services

View File

@ -13,6 +13,7 @@
},
"require": {
"php": ">=7.1.0",
"ext-json": "*",
"symfony/symfony": "^3.4",
"doctrine/orm": "^2.5",
"doctrine/annotations": "^1.3.0",
@ -33,8 +34,7 @@
"unreal4u/telegram-api": "^2.2",
"csa/guzzle-bundle": "^3",
"symfony/web-server-bundle": "^3.3",
"sentry/sentry-symfony": "^2.2",
"leezy/pheanstalk-bundle": "^3.3"
"sentry/sentry-symfony": "^2.2"
},
"require-dev": {
"symfony/phpunit-bridge": "^3.0",

1912
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,114 +0,0 @@
<?php
namespace Skobkin\Bundle\PointToolsBundle\Command;
use Doctrine\ORM\EntityManagerInterface;
use JMS\Serializer\Serializer;
use Leezy\PheanstalkBundle\Proxy\PheanstalkProxy;
use Pheanstalk\Job;
use Skobkin\Bundle\PointToolsBundle\DTO\Api\WebSocket\Message;
use Skobkin\Bundle\PointToolsBundle\Exception\WebSocket\UnsupportedTypeException;
use Skobkin\Bundle\PointToolsBundle\Service\WebSocket\WebSocketMessageProcessor;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\{InputInterface, InputOption};
use Symfony\Component\Console\Output\OutputInterface;
/**
* This command processes WebSocket updates MQ and stores new content in the DB
*/
class ProcessWebsocketUpdatesCommand extends Command
{
/** @var EntityManagerInterface */
private $em;
/** @var PheanstalkProxy */
private $bsClient;
/** @var string */
private $bsTubeName;
/** @var Serializer */
private $serializer;
/** @var WebSocketMessageProcessor */
private $messageProcessor;
/** @var \Raven_Client */
private $sentryClient;
public function __construct(
EntityManagerInterface $em,
\Raven_Client $raven,
PheanstalkProxy $bsClient,
string $bsTubeName,
Serializer $serializer,
WebSocketMessageProcessor $processor
) {
$this->em = $em;
$this->sentryClient = $raven;
$this->serializer = $serializer;
$this->messageProcessor = $processor;
$this->bsClient = $bsClient;
$this->bsTubeName = $bsTubeName;
parent::__construct();
}
/** {@inheritdoc} */
protected function configure()
{
$this
->setName('point:update:websocket-messages')
->setDescription('Reads and processes updates from Beanstalkd queue pipe')
->addOption('keep-jobs', 'k', InputOption::VALUE_NONE, 'Don\'t delete jobs from queue after processing')
;
}
/** {@inheritdoc} */
protected function execute(InputInterface $input, OutputInterface $output)
{
$keepJobs = (bool) $input->getOption('keep-jobs');
/** @var Job $job */
while ($job = $this->bsClient->reserveFromTube($this->bsTubeName, 0)) {
try {
/** @var Message $message */
$message = $this->serializer->deserialize($job->getData(), Message::class, 'json');
} catch (\Exception $e) {
$output->writeln(sprintf(
'Error while deserializing #%d data: \'%s\'',
$job->getId(),
$job->getData()
));
$this->sentryClient->captureException($e);
continue;
}
$output->writeln('Processing job #'.$job->getId().' ('.$message->getA().')');
try {
if ($this->messageProcessor->processMessage($message)) {
$this->em->flush();
if (!$keepJobs) {
$this->bsClient->delete($job);
}
$this->em->clear();
}
} catch (UnsupportedTypeException $e) {
$output->writeln(' Unsupported message type: '.$message->getA());
$this->sentryClient->captureException($e);
continue;
} catch (\Exception $e) {
$output->writeln(' Message processing error: '.$e->getMessage());
$this->sentryClient->captureException($e);
continue;
}
}
}
}

View File

@ -12,11 +12,19 @@ class PostController extends AbstractController
{
/**
* @ParamConverter("post", class="SkobkinPointToolsBundle:Blogs\Post")
*
* @return Response
*/
public function showAction(Post $post, PostRepository $postRepository): Response
{
if ((!$post->getAuthor()->isPublic()) || $post->getAuthor()->isWhitelistOnly()) {
/**
* Throwing 404 instead of 403 because of
* @see \Symfony\Component\Security\Http\Firewall\ExceptionListener::handleAccessDeniedException()
* starts to replace 403 by 401 exceptions for anonymous users and tries to authenticate them.
*/
throw $this->createNotFoundException('Author\'s blog is private.');
//throw $this->createAccessDeniedException('Author\'s blog is private.');
}
return $this->render('SkobkinPointToolsBundle:Post:show.html.twig', [
'post' => $postRepository->getPostWithComments($post->getId()),
]);

View File

@ -4,22 +4,39 @@ namespace Skobkin\Bundle\PointToolsBundle\DTO\Api;
class Comment implements ValidableInterface
{
/** @var int|null */
/**
* @var string|null
*/
private $postId;
/**
* @var int|null
*/
private $number;
/** @var int|null */
/**
* @var int|null
*/
private $toCommentId;
/** @var string|null */
/**
* @var string|null
*/
private $created;
/** @var string|null */
/**
* @var string|null
*/
private $text;
/** @var User|null */
/**
* @var User|null
*/
private $author;
/** @var bool|null */
/**
* @var bool|null
*/
private $isRec;

View File

@ -4,17 +4,27 @@ namespace Skobkin\Bundle\PointToolsBundle\DTO\Api;
class MetaPost implements ValidableInterface
{
/** @var Post|null */
/**
* @var Post|null
*/
private $post;
/** @var Comment[]|null */
/**
* @var Comment[]|null
*/
private $comments;
public function getPost(): ?Post
{
return $this->post;
}
public function setPost(?Post $post): void
{
$this->post = $post;
}
/**
* @return Comment[]|null
*/
@ -23,8 +33,20 @@ class MetaPost implements ValidableInterface
return $this->comments;
}
/**
* @param Comment[]|null $comments
*/
public function setComments(?array $comments): void
{
$this->comments = $comments;
}
public function isValid(): bool
{
return (null !== $this->post && $this->post->isValid());
if (null !== $this->post && $this->post->isValid()) {
return true;
}
return false;
}
}

View File

@ -4,28 +4,44 @@ namespace Skobkin\Bundle\PointToolsBundle\DTO\Api;
class Post implements ValidableInterface
{
/** @var string|null */
/**
* @var string|null
*/
private $id;
/** @var string[]|null */
/**
* @var string[]|null
*/
private $tags;
/** @var string[]|null */
/**
* @var string[]|null
*/
private $files;
/** @var User|null */
/**
* @var User|null
*/
private $author;
/** @var string|null */
/**
* @var string|null
*/
private $text;
/** @var string|null */
/**
* @var string|null
*/
private $created;
/** @var string|null */
/**
* @var string|null
*/
private $type;
/** @var bool|null */
/**
* @var bool|null
*/
private $private;

View File

@ -1,330 +0,0 @@
<?php
namespace Skobkin\Bundle\PointToolsBundle\DTO\Api\WebSocket;
use Skobkin\Bundle\PointToolsBundle\DTO\Api\ValidableInterface;
use Skobkin\Bundle\PointToolsBundle\Exception\WebSocket\UnsupportedTypeException;
/**
* WebSocket update message
*/
class Message implements ValidableInterface
{
public const TYPE_COMMENT = 'comment';
public const TYPE_COMMENT_EDITED = 'comment_edited';
public const TYPE_POST = 'post';
public const TYPE_POST_EDITED = 'post_edited';
public const TYPE_POST_COMMENT_RECOMMENDATION = 'rec';
public const TYPE_RECOMMENDATION_WITH_COMMENT = 'ok';
/**
* Event type. @see Message::TYPE_* constants
*
* @var string
*/
private $a;
/**
* Login of the user
*
* @var string
*/
private $author;
/**
* @var int
*/
private $authorId;
/** @var string|null */
private $authorName;
/**
* Number of the comment in the thread
*
* @var int|null
*/
private $commentId;
/**
* ???
*
* @var bool|null
*/
private $cut;
/**
* Array of file paths
*
* @var string[]|null
*/
private $files;
/** @var string|null */
private $html;
/**
* @deprecated Link in the Post::type=feed posts
*
* @var string|null
*/
private $link;
/**
* @var int|null
*/
private $postAuthorId;
/** @var string */
private $postId;
/** @var bool|null */
private $private;
/**
* Number of the comment in the thread for recommendation with text
*
* @var int|null
*/
private $rcid;
/**
* Array of tags
*
* @var string[]|null
*/
private $tags;
/** @var string */
private $text;
/**
* @deprecated ???
*
* @var string|null
*/
private $title;
/**
* Number of the comment to which this comment is answering
*
* @var string|null
*/
private $toCommentId;
/**
* Text quotation of the comment to which this comment is answering
*
* @var string|null
*/
private $toText;
/**
* Array of logins of users to which post is addressed
*
* @var string[]|null
*/
private $toUsers;
public function isPost(): bool
{
return self::TYPE_POST === $this->a;
}
public function isComment(): bool
{
return self::TYPE_COMMENT === $this->a;
}
public function isCommentRecommendation(): bool
{
return self::TYPE_RECOMMENDATION_WITH_COMMENT === $this->a;
}
public function isPostRecommendation(): bool
{
return self::TYPE_POST_COMMENT_RECOMMENDATION === $this->a;
}
/**
* @throws \RuntimeException
* @throws UnsupportedTypeException
*/
public function isValid(): bool
{
switch ($this->a) {
case self::TYPE_POST:
return $this->isValidPost();
break;
case self::TYPE_POST_EDITED:
return $this->isValidPostEdited();
break;
case self::TYPE_COMMENT;
return $this->isValidComment();
break;
case self::TYPE_COMMENT_EDITED;
return $this->isValidCommentEdited();
break;
case self::TYPE_RECOMMENDATION_WITH_COMMENT;
return $this->isValidRecommendationWithComment();
break;
case self::TYPE_POST_COMMENT_RECOMMENDATION;
return $this->isValidPostCommentRecommendation();
break;
case null:
throw new \RuntimeException('Message has NULL type.');
break;
default:
throw new UnsupportedTypeException(sprintf('Type \'%s\' is not supported.', $this->a));
}
}
public function getA(): string
{
return $this->a;
}
public function getAuthor(): string
{
return $this->author;
}
public function getAuthorId(): int
{
return $this->authorId;
}
public function getAuthorName(): ?string
{
return $this->authorName;
}
public function getCommentId(): ?int
{
return $this->commentId;
}
public function getCut(): ?bool
{
return $this->cut;
}
public function getFiles(): ?array
{
return $this->files;
}
public function getHtml(): ?string
{
return $this->html;
}
public function getLink(): ?string
{
return $this->link;
}
public function getPostId(): string
{
return $this->postId;
}
public function getPostAuthorId(): ?int
{
return $this->postAuthorId;
}
public function getPrivate(): ?bool
{
return $this->private;
}
public function getRcid(): ?int
{
return $this->rcid;
}
public function getTags(): ?array
{
return $this->tags;
}
public function getText(): string
{
return $this->text;
}
public function getTitle(): ?string
{
return $this->title;
}
public function getToCommentId(): ?string
{
return $this->toCommentId;
}
public function getToText(): ?string
{
return $this->toText;
}
public function getToUsers(): ?array
{
return $this->toUsers;
}
private function hasCommonMandatoryData(): bool
{
return (
null !== $this->author &&
null !== $this->authorId &&
null !== $this->postId
);
}
private function isValidPost(): bool
{
return $this->hasCommonMandatoryData() && (
// Text can be empty ("") though
null !== $this->text &&
null !== $this->tags
);
}
private function isValidPostEdited(): bool
{
return $this->isValidPost();
}
private function isValidComment(): bool
{
return $this->hasCommonMandatoryData() && (
null !== $this->commentId &&
null !== $this->html &&
null !== $this->text
);
}
private function isValidCommentEdited(): bool
{
return $this->hasCommonMandatoryData() && (null !== $this->commentId);
}
private function isValidRecommendationWithComment(): bool
{
return $this->hasCommonMandatoryData();
}
private function isValidPostCommentRecommendation(): bool
{
return $this->hasCommonMandatoryData() && (null !== $this->postAuthorId);
}
}

View File

@ -11,7 +11,7 @@ use Skobkin\Bundle\PointToolsBundle\Entity\User;
class LoadCommentsData extends AbstractFixture implements OrderedFixtureInterface
{
public function load(ObjectManager $om): void
public function load(ObjectManager $om)
{
/** @var Post $post */
$post = $this->getReference('test_post_longpost');
@ -25,28 +25,29 @@ class LoadCommentsData extends AbstractFixture implements OrderedFixtureInterfac
$this->getReference('test_user_99995'),
];
$text = 'Some text with [link to @skobkin-ru site](https://skobk.in/) and `code block`'.PHP_EOL.
'and some quotation:'.PHP_EOL.
'> test test quote'.PHP_EOL.
'and some text after';
$comments = [];
foreach (range(1, 10000) as $num) {
$comment = new Comment(
$text,
new \DateTime(),
false,
$post,
$num,
($num > 1 && !random_int(0, 4)) ? random_int(1, $num - 1) : null,
$users[array_rand($users)],
[]
);
$comment = (new Comment())
->setNumber($num)
->setDeleted(mt_rand(0, 15) ? false : true)
->setCreatedAt(new \DateTime())
->setAuthor($users[array_rand($users)])
->setRec(false)
->setText(
'Some text with [link to @skobkin-ru site](https://skobk.in/) and `code block`'.PHP_EOL.
'and some quotation:'.PHP_EOL.
'> test test quote'.PHP_EOL.
'and some text after'
)
;
if (!random_int(0, 15)) {
$comment->delete();
if (count($comments) > 0 && mt_rand(0, 1)) {
$comment->setParent($comments[mt_rand(0, count($comments) - 1)]);
}
$post->addComment($comment);
$comments[] = $comment;
$om->persist($comment);
}

View File

@ -2,33 +2,64 @@
namespace Skobkin\Bundle\PointToolsBundle\DataFixtures\ORM;
use Doctrine\Common\DataFixtures\AbstractFixture;
use Doctrine\Common\DataFixtures\OrderedFixtureInterface;
use Doctrine\Common\DataFixtures\{AbstractFixture, OrderedFixtureInterface};
use Doctrine\Common\Persistence\ObjectManager;
use Skobkin\Bundle\PointToolsBundle\Entity\Blogs\Post;
use Skobkin\Bundle\PointToolsBundle\Entity\User;
use Skobkin\Bundle\PointToolsBundle\Entity\{Blogs\Post, User};
class LoadPostData extends AbstractFixture implements OrderedFixtureInterface
{
public const POST_ID_LONG = 'longpost';
public const POST_ID_SHORT = 'shortpost';
public const POST_ID_PR_USER = 'prusrpst';
public const POST_ID_WL_USER = 'wlusrpst';
public const POST_ID_PR_WL_USER = 'prwlusrpst';
public function load(ObjectManager $om)
{
/** @var User $testUser */
$testUser = $this->getReference('test_user_99999');
/** @var User $mainUser */
$mainUser = $this->getReference('test_user_'.LoadUserData::USER_MAIN_ID);
/** @var User $privateUser */
$privateUser = $this->getReference('test_user_'.LoadUserData::USER_PRIV_ID);
/** @var User $wlUser */
$wlUser = $this->getReference('test_user_'.LoadUserData::USER_WLON_ID);
/** @var User $prWlUser */
$prWlUser = $this->getReference('test_user_'.LoadUserData::USER_PRWL_ID);
$longPost = (new Post('longpost', $testUser, new \DateTime(), Post::TYPE_POST))
$longPost = (new Post(self::POST_ID_LONG, $mainUser, new \DateTime(), Post::TYPE_POST))
->setText('Test post with many comments')
->setPrivate(false)
->setDeleted(false)
;
$shortPost = (new Post('shortpost', $testUser, new \DateTime(), Post::TYPE_POST))
$shortPost = (new Post(self::POST_ID_SHORT, $mainUser, new \DateTime(), Post::TYPE_POST))
->setText('Test short post')
->setPrivate(false)
->setDeleted(false)
;
$privateUserPost = (new Post(self::POST_ID_PR_USER, $privateUser, new \DateTime(), Post::TYPE_POST))
->setText('Post from private user. Should not be visible in the public feed.')
->setPrivate(false)
->setDeleted(false)
;
$wlUserPost = (new Post(self::POST_ID_WL_USER, $wlUser, new \DateTime(), Post::TYPE_POST))
->setText('Post from whitelist-only user. Should only be visible for whitelisted users.')
->setPrivate(false)
->setDeleted(false)
;
$privateWlUserPost = (new Post(self::POST_ID_PR_WL_USER, $prWlUser, new \DateTime(), Post::TYPE_POST))
->setText('Post from private AND whitelist-only user. Should not be visible in the public feed.')
->setPrivate(false)
->setDeleted(false)
;
$om->persist($longPost);
$om->persist($shortPost);
$om->persist($privateUserPost);
$om->persist($wlUserPost);
$om->persist($privateWlUserPost);
$om->flush();
$this->addReference('test_post_longpost', $longPost);

View File

@ -9,25 +9,27 @@ use Skobkin\Bundle\PointToolsBundle\Entity\User;
class LoadUserData extends AbstractFixture implements OrderedFixtureInterface
{
public const USER_MAIN_ID = 99999;
public const USER_SCND_ID = 99998;
public const USER_PRIV_ID = 99997;
public const USER_WLON_ID = 99996;
public const USER_PRWL_ID = 99995;
public const USER_UNNM_ID = 99994;
private $users = [
// 99999
['login' => 'testuser', 'name' => 'Test User 1'],
// 99998
['login' => 'testuser2', 'name' => 'Test User 2'],
// 99997
['login' => 'testuser3', 'name' => 'Test User 3'],
// 99996
['login' => 'testuser4', 'name' => 'Test User 4'],
//99995
['login' => 'testuser5', 'name' => null],
['id' => self::USER_MAIN_ID, 'login' => 'testuser', 'name' => 'Test User 1', 'private' => false, 'whitelist-only' => false],
['id' => self::USER_SCND_ID, 'login' => 'testuser2', 'name' => 'Test User 2 for autocomplete test', 'private' => false, 'whitelist-only' => false],
['id' => self::USER_PRIV_ID, 'login' => 'private_user', 'name' => 'Test User 3', 'private' => true, 'whitelist-only' => false],
['id' => self::USER_WLON_ID, 'login' => 'whitelist_only_user', 'name' => 'Test User 4', 'private' => false, 'whitelist-only' => true],
['id' => self::USER_PRWL_ID, 'login' => 'private_whitelist_only_user', 'name' => 'Test User 4', 'private' => false, 'whitelist-only' => true],
['id' => self::USER_UNNM_ID, 'login' => 'unnamed_user', 'name' => null, 'private' => false, 'whitelist-only' => false],
];
public function load(ObjectManager $om)
{
$userId = 99999;
foreach ($this->users as $userData) {
$user = new User($userId--, new \DateTime(), $userData['login'], $userData['name']);
$user = new User($userData['id'], new \DateTime(), $userData['login'], $userData['name']);
$user->updatePrivacy(!$userData['private'], $userData['whitelist-only']);
$om->persist($user);

View File

@ -9,8 +9,6 @@ use Skobkin\Bundle\PointToolsBundle\Entity\User;
/**
* @ORM\Table(name="comments", schema="posts", indexes={
* @ORM\Index(name="idx_comment_created_at", columns={"created_at"})
* }, uniqueConstraints={
* @ORM\UniqueConstraint(name="unique_post_id_comment_number", columns={"post_id", "number"})
* })
* @ORM\Entity(repositoryClass="Skobkin\Bundle\PointToolsBundle\Repository\Blogs\CommentRepository")
*/
@ -64,17 +62,10 @@ class Comment
/**
* @var int
*
* @ORM\Column(name="number", type="integer", unique=true)
* @ORM\Column(name="number", type="smallint")
*/
private $number;
/**
* @var int|null
*
* @ORM\Column(name="to_number", type="integer", nullable=true)
*/
private $toNumber;
/**
* @var User
*
@ -94,36 +85,26 @@ class Comment
*/
private $files;
/**
* @var Comment|null
*
* @ORM\ManyToOne(targetEntity="Skobkin\Bundle\PointToolsBundle\Entity\Blogs\Comment", inversedBy="children")
* @ORM\JoinColumn(name="parent_id", nullable=true)
*/
private $parent;
public function __construct(
string $text,
\DateTime $createdAt,
bool $rec,
Post $post,
int $number,
?int $toNumber,
User $author,
array $files
) {
$this->text = $text;
$this->createdAt = $createdAt;
$this->rec = $rec;
$this->post = $post;
$this->number = $number;
$this->toNumber = $toNumber;
$this->author = $author;
/**
* @var Comment[]|ArrayCollection
*
* @ORM\OneToMany(targetEntity="Skobkin\Bundle\PointToolsBundle\Entity\Blogs\Comment", fetch="EXTRA_LAZY", mappedBy="parent")
*/
private $children;
public function __construct()
{
$this->files = new ArrayCollection();
foreach ($files as $file) {
if (!($file instanceof File)) {
throw new \RuntimeException(sprintf(
'$files array must contain only \'%s\' objects. %s given.',
\is_object($file) ? \get_class($file) : \gettype($file)
));
}
$this->files->add($file);
}
$this->children = new ArrayCollection();
}
public function getId(): int
@ -131,41 +112,95 @@ class Comment
return $this->id;
}
public function setCreatedAt(\DateTime $createdAt): self
{
$this->createdAt = $createdAt;
return $this;
}
public function getCreatedAt(): \DateTime
{
return $this->createdAt;
}
public function setText(string $text): self
{
$this->text = $text;
return $this;
}
public function getText(): string
{
return $this->text;
}
public function setRec(bool $rec): self
{
$this->rec = $rec;
return $this;
}
public function isRec(): bool
{
return $this->rec;
}
public function getRec(): bool
{
return $this->rec;
}
public function getPost(): Post
{
return $this->post;
}
public function setPost(Post $post): self
{
$this->post = $post;
return $this;
}
public function setNumber(int $number): self
{
$this->number = $number;
return $this;
}
public function getNumber(): int
{
return $this->number;
}
public function getToNumber(): ?int
{
return $this->toNumber;
}
public function getAuthor(): User
{
return $this->author;
}
public function setAuthor(User $author): self
{
$this->author = $author;
return $this;
}
public function addFile(File $files): self
{
$this->files[] = $files;
return $this;
}
public function removeFile(File $files): void
{
$this->files->removeElement($files);
}
/**
* @return File[]|ArrayCollection
*/
@ -174,22 +209,52 @@ class Comment
return $this->files;
}
public function delete(): self
public function getParent(): ?Comment
{
$this->deleted = true;
return $this->parent;
}
public function setParent(Comment $parent): self
{
$this->parent = $parent;
return $this;
}
public function restore(): self
public function setDeleted(bool $deleted): self
{
$this->deleted = false;
$this->deleted = $deleted;
return $this;
}
public function getDeleted(): bool
{
return $this->deleted;
}
public function isDeleted(): bool
{
return $this->deleted;
}
public function addChild(Comment $children): self
{
$this->children[] = $children;
return $this;
}
public function removeChild(Comment $children): void
{
$this->children->removeElement($children);
}
/**
* @return Comment[]|ArrayCollection
*/
public function getChildren(): iterable
{
return $this->children;
}
}

View File

@ -69,13 +69,6 @@ class Post
*/
private $deleted = false;
/**
* @var bool Status of point-tools subscription to the post (to receive WS updates)
*
* @ORM\Column(name="is_subscribed", type="boolean", options={"default": false})
*/
private $subscribed = false;
/**
* @var User
*
@ -110,7 +103,7 @@ class Post
private $comments;
public function __construct(string $id, User $author, \DateTime $createdAt, string $type = self::TYPE_POST)
public function __construct(string $id, User $author, \DateTime $createdAt, string $type)
{
$this->id = $id;
$this->author = $author;
@ -223,25 +216,6 @@ class Post
{
return $this->deleted;
}
public function isSubscribed(): bool
{
return $this->subscribed;
}
public function subscribe(): self
{
$this->subscribed = true;
return $this;
}
public function unsubscribe(): self
{
$this->subscribed = false;
return $this;
}
public function setPrivate(bool $private): self
{
@ -263,6 +237,7 @@ class Post
public function addComment(Comment $comment): self
{
$this->comments[] = $comment;
$comment->setPost($this);
return $this;
}

View File

@ -7,8 +7,8 @@ use Skobkin\Bundle\PointToolsBundle\Entity\User;
/**
* @ORM\Table(name="telegram_accounts", schema="users", indexes={
* @ORM\Index(name="subscriber_notification_idx", columns={"subscriber_notification"}, options={"where": "(subscriber_notification = true)"}),
* @ORM\Index(name="rename_notification_idx", columns={"rename_notification"}, options={"where": "(rename_notification = true)"}),
* @ORM\Index(name="subscriber_notification_idx", columns={"subscriber_notification"}, options={"where": "subscriber_notification = TRUE"}),
* @ORM\Index(name="rename_notification_idx", columns={"rename_notification"}, options={"where": "rename_notification = TRUE"}),
* })
* @ORM\Entity(repositoryClass="Skobkin\Bundle\PointToolsBundle\Repository\Telegram\AccountRepository")
* @ORM\HasLifecycleCallbacks()

View File

@ -7,7 +7,6 @@ use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Table(name="users", schema="users", indexes={
* @ORM\Index(name="idx_user_login", columns={"login"}),
* @ORM\Index(name="idx_user_public", columns={"public"}),
* @ORM\Index(name="idx_user_removed", columns={"is_removed"})
* })
@ -191,24 +190,15 @@ class User
return $this->createdAt;
}
public function updateCreatedAt(\DateTime $date): self
{
$this->createdAt = $date;
return $this;
}
public function getUpdatedAt(): ?\DateTime
{
return $this->updatedAt;
}
public function updatePrivacy(bool $public, bool $whitelistOnly): self
public function updatePrivacy(?bool $public, ?bool $whitelistOnly): void
{
$this->public = $public;
$this->whitelistOnly = $whitelistOnly;
return $this;
}
public function isPublic(): ?bool

View File

@ -1,20 +0,0 @@
<?php
namespace Skobkin\Bundle\PointToolsBundle\Exception\Api;
class PostNotFoundException extends NotFoundException
{
/** @var string */
private $id;
public function __construct(string $id, \Exception $previous)
{
parent::__construct('Post not found', 0, $previous);
$this->id = $id;
}
public function getId(): string
{
return $this->id;
}
}

View File

@ -4,11 +4,16 @@ namespace Skobkin\Bundle\PointToolsBundle\Exception\Api;
class UserNotFoundException extends NotFoundException
{
/** @var int */
private $userId;
/**
* @var int
*/
protected $userId;
/**
* @var string
*/
protected $login;
/** @var string */
private $login;
/**
* {@inheritdoc}

View File

@ -1,9 +0,0 @@
<?php
namespace Skobkin\Bundle\PointToolsBundle\Exception\WebSocket;
use RuntimeException;
class UnsupportedTypeException extends RuntimeException
{
}

View File

@ -2,6 +2,10 @@ Skobkin\Bundle\PointToolsBundle\DTO\Api\Comment:
exclusion_policy: none
access_type: public_method
properties:
postId:
serialized_name: 'post_id'
type: 'Skobkin\Bundle\PointToolsBundle\DTO\Api\Post'
max_depth: 2
number:
serialized_name: 'id'
type: integer

View File

@ -1,9 +1,10 @@
Skobkin\Bundle\PointToolsBundle\DTO\Api\MetaPost:
exclusion_policy: none
access_type: public_method
properties:
post:
serialized_name: 'post'
type: 'Skobkin\Bundle\PointToolsBundle\DTO\Api\Post'
max_depth: 2
comments:
serialized_name: 'comments'
type: 'array<Skobkin\Bundle\PointToolsBundle\DTO\Api\Comment>'
max_depth: 2

View File

@ -1,60 +0,0 @@
Skobkin\Bundle\PointToolsBundle\DTO\Api\WebSocket\Message:
exclusion_policy: none
properties:
a:
type: string
serialized_name: 'a'
author:
type: string
serialized_name: 'author'
authorId:
type: int
serialized_name: 'author_id'
authorName:
type: string
serialized_name: 'author_name'
commentId:
type: int
serialized_name: 'comment_id'
cut:
type: bool
serialized_name: 'cut'
files:
type: array<string>
serialized_name: 'files'
html:
type: string
serialized_name: 'html'
link:
type: string
serialized_name: 'link'
postId:
type: string
serialized_name: 'post_id'
postAuthorId:
type: int
serialized_name: 'post_author_id'
private:
type: bool
serialized_name: 'private'
rcid:
type: int
serialized_name: 'rcid'
tags:
type: array<string>
serialized_name: 'tags'
text:
type: string
serialized_name: 'text'
title:
type: string
serialized_name: 'title'
toCommentId:
type: string
serialized_name: 'to_comment_id'
toText:
type: string
serialized_name: 'to_text'
toUsers:
type: array<string>
serialized_name: 'to_users'

View File

@ -5,9 +5,6 @@ namespace Skobkin\Bundle\PointToolsBundle\Service\Api;
use GuzzleHttp\ClientInterface;
use JMS\Serializer\SerializerInterface;
use Psr\Log\LoggerInterface;
use Skobkin\Bundle\PointToolsBundle\DTO\Api\MetaPost;
use Skobkin\Bundle\PointToolsBundle\Entity\Blogs\Post;
use Skobkin\Bundle\PointToolsBundle\Exception\Api\{NotFoundException, PostNotFoundException};
use Skobkin\Bundle\PointToolsBundle\Service\Factory\Blogs\PostFactory;
/**
@ -15,8 +12,6 @@ use Skobkin\Bundle\PointToolsBundle\Service\Factory\Blogs\PostFactory;
*/
class PostApi extends AbstractApi
{
private const PREFIX = '/api/post/';
/**
* @var PostFactory
*/
@ -29,24 +24,4 @@ class PostApi extends AbstractApi
$this->postFactory = $postFactory;
}
/**
* @throws PostNotFoundException
*/
public function getById(string $id): Post
{
try {
$postData = $this->getGetJsonData(
self::PREFIX.$id,
[],
MetaPost::class
);
} catch (NotFoundException $e) {
throw new PostNotFoundException($id, $e);
}
// Not catching ForbiddenException right now
return $this->postFactory->findOrCreateFromDtoWithContent($postData);
}
}

View File

@ -3,7 +3,9 @@
namespace Skobkin\Bundle\PointToolsBundle\Service\Api;
use GuzzleHttp\ClientInterface;
use JMS\Serializer\{DeserializationContext, SerializerInterface};
use JMS\Serializer\{
DeserializationContext, SerializerInterface
};
use Psr\Log\LoggerInterface;
use Skobkin\Bundle\PointToolsBundle\DTO\Api\{Auth, User as UserDTO};
use Skobkin\Bundle\PointToolsBundle\Entity\User;

View File

@ -6,8 +6,6 @@ use Psr\Log\LoggerInterface;
abstract class AbstractFactory
{
public const DATE_FORMAT = 'Y-m-d_H:i:s';
/** @var LoggerInterface */
protected $logger;

View File

@ -3,10 +3,7 @@
namespace Skobkin\Bundle\PointToolsBundle\Service\Factory\Blogs;
use Psr\Log\LoggerInterface;
use Skobkin\Bundle\PointToolsBundle\DTO\Api\WebSocket\Message;
use Skobkin\Bundle\PointToolsBundle\Entity\Blogs\Comment;
use Skobkin\Bundle\PointToolsBundle\Repository\Blogs\{CommentRepository, PostRepository};
use Skobkin\Bundle\PointToolsBundle\Service\Api\PostApi;
use Skobkin\Bundle\PointToolsBundle\Service\Factory\{AbstractFactory, UserFactory};
class CommentFactory extends AbstractFactory
@ -20,45 +17,12 @@ class CommentFactory extends AbstractFactory
/** @var UserFactory */
private $userFactory;
/** @var PostApi */
private $postApi;
public function __construct(LoggerInterface $logger, CommentRepository $commentRepository, PostRepository $postRepository, UserFactory $userFactory, PostApi $postApi)
public function __construct(LoggerInterface $logger, CommentRepository $commentRepository, PostRepository $postRepository, UserFactory $userFactory)
{
parent::__construct($logger);
$this->userFactory = $userFactory;
$this->commentRepository = $commentRepository;
$this->postRepository = $postRepository;
$this->postApi = $postApi;
}
public function findOrCreateFromWebsocketMessage(Message $message): Comment
{
if ($message->isValid()) {
throw new \InvalidArgumentException('Comment is invalid');
}
if ($message->isComment()) {
throw new \InvalidArgumentException(sprintf(
'Invalid Message object provided. %s expected, %s given',
Message::TYPE_COMMENT,
$message->getA()
));
}
if (null === $comment = $this->commentRepository->findOneBy(['post' => $post, 'number' => $message->getCommentId()])) {
$author = $this->userFactory->findOrCreateFromIdLoginAndName(
$message->getAuthorId(),
$message->getAuthor(),
$message->getAuthorName()
);
if (null === $post = $this->postRepository->find($message->getPostId())) {
$post = $this->postApi->getById($message->getPostId());
}
// TODO
//$comment = new Comment()
}
}
}

View File

@ -4,11 +4,10 @@ namespace Skobkin\Bundle\PointToolsBundle\Service\Factory\Blogs;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
use Skobkin\Bundle\PointToolsBundle\DTO\Api\{MetaPost, Post as ApiPost, PostsPage};
use Skobkin\Bundle\PointToolsBundle\DTO\Api\WebSocket\Message as WebsocketMessage;
use Skobkin\Bundle\PointToolsBundle\DTO\Api\{MetaPost, Post as PostDTO, PostsPage};
use Skobkin\Bundle\PointToolsBundle\Entity\{Blogs\Post, Blogs\PostTag, User};
use Skobkin\Bundle\PointToolsBundle\Exception\{Api\InvalidResponseException, Factory\Blog\InvalidDataException};
use Skobkin\Bundle\PointToolsBundle\Repository\{Blogs\PostRepository, UserRepository};
use Skobkin\Bundle\PointToolsBundle\Repository\Blogs\PostRepository;
use Skobkin\Bundle\PointToolsBundle\Service\Factory\{AbstractFactory, UserFactory};
class PostFactory extends AbstractFactory
@ -19,9 +18,6 @@ class PostFactory extends AbstractFactory
/** @var PostRepository */
private $postRepository;
/** @var UserRepository */
private $userRepository;
/** @var UserFactory */
private $userFactory;
@ -39,7 +35,6 @@ class PostFactory extends AbstractFactory
LoggerInterface $logger,
EntityManagerInterface $em,
PostRepository $postRepository,
UserRepository $userRepository,
UserFactory $userFactory,
FileFactory $fileFactory,
CommentFactory $commentFactory,
@ -48,7 +43,6 @@ class PostFactory extends AbstractFactory
parent::__construct($logger);
$this->em = $em;
$this->postRepository = $postRepository;
$this->userRepository = $userRepository;
$this->userFactory = $userFactory;
$this->fileFactory = $fileFactory;
$this->commentFactory = $commentFactory;
@ -98,6 +92,8 @@ class PostFactory extends AbstractFactory
/**
* Create full post with tags, files and comments
*
* @todo Implement comments
*
* @throws InvalidDataException
*/
public function findOrCreateFromDtoWithContent(MetaPost $metaPost): Post
@ -115,7 +111,7 @@ class PostFactory extends AbstractFactory
throw $e;
}
$post = $this->findOrCreateFromApiDto($postData, $author);
$post = $this->findOrCreateFromDto($postData, $author);
try {
$this->updatePostTags($post, $postData->getTags() ?: []);
@ -131,62 +127,10 @@ class PostFactory extends AbstractFactory
throw $e;
}
// @TODO implement comments
return $post;
}
public function findOrCreateFromWebsocketDto(WebsocketMessage $message): Post
{
if (!$message->isValid()) {
throw new InvalidDataException('Invalid post data');
}
if (!$message->isPost()) {
throw new \LogicException(sprintf(
'Incorrect message type received. \'post\' expected \'%s\' given',
$message->getA()
));
}
if (null === $post = $this->postRepository->find($message->getPostId())) {
$author = $this->userFactory->findOrCreateFromIdLoginAndName(
$message->getAuthorId(),
$message->getAuthor(),
$message->getAuthorName()
);
$post = new Post(
$message->getPostId(),
$author,
new \DateTime(),
Post::TYPE_POST
);
$this->postRepository->add($post);
}
$post
->setText($message->getText())
->setPrivate((bool) $message->getPrivate())
;
try {
$this->updatePostTags($post, $message->getTags() ?: []);
} catch (\Exception $e) {
$this->logger->error('Error while updating post tags');
throw $e;
}
try {
$this->updatePostFiles($post, $message->getFiles() ?: []);
} catch (\Exception $e) {
$this->logger->error('Error while updating post files');
throw $e;
}
return $post;
}
private function findOrCreateFromApiDto(ApiPost $postData, User $author): Post
private function findOrCreateFromDto(PostDTO $postData, User $author): Post
{
if (null === ($post = $this->postRepository->find($postData->getId()))) {
// Creating new post
@ -201,7 +145,7 @@ class PostFactory extends AbstractFactory
$post
->setText($postData->getText())
->setPrivate((bool) $postData->getPrivate())
->setPrivate($postData->getPrivate())
;
return $post;

View File

@ -10,9 +10,12 @@ use Skobkin\Bundle\PointToolsBundle\Exception\Factory\InvalidUserDataException;
class UserFactory extends AbstractFactory
{
public const DATE_FORMAT = 'Y-m-d_H:i:s';
/** @var UserRepository */
private $userRepository;
public function __construct(LoggerInterface $logger, UserRepository $userRepository)
{
parent::__construct($logger);
@ -20,25 +23,27 @@ class UserFactory extends AbstractFactory
}
/**
* @param UserDTO $userData
*
* @return User
*
* @throws InvalidUserDataException
*/
public function findOrCreateFromDTO(UserDTO $userData): User
{
// @todo LOG
if (!$userData->isValid()) {
throw new InvalidUserDataException('Invalid user data', $userData);
}
$createdAt = \DateTime::createFromFormat(self::DATE_FORMAT, $userData->getCreated()) ?: new \DateTime();
/** @var User $user */
if (null === ($user = $this->userRepository->find($userData->getId()))) {
$user = new User(
$userData->getId(),
$createdAt
\DateTime::createFromFormat('Y-m-d_H:i:s', $userData->getCreated()) ?: new \DateTime()
);
$this->userRepository->add($user);
} else {
$user->updateCreatedAt($createdAt);
}
$user->updateLoginAndName($userData->getLogin(), $userData->getName());
@ -50,21 +55,6 @@ class UserFactory extends AbstractFactory
return $user;
}
public function findOrCreateFromIdLoginAndName(int $id, string $login, ?string $name): User
{
/** @var User $user */
if (null === $user = $this->userRepository->find($id)) {
// We're using current date now but next time when we'll be updating user from API it'll be fixed
$user = new User($id, new \DateTime(), $login, $name);
$this->userRepository->add($user);
} else {
// @todo update login?
// Probably don't because no name in the WS message (or maybe after PR to point?)
}
return $user;
}
/**
* @return User[]
*/

View File

@ -1,69 +0,0 @@
<?php
namespace Skobkin\Bundle\PointToolsBundle\Service\WebSocket;
use Skobkin\Bundle\PointToolsBundle\DTO\Api\WebSocket\Message;
use Skobkin\Bundle\PointToolsBundle\Service\Factory\Blogs\{CommentFactory, PostFactory};
class WebSocketMessageProcessor
{
/** @var PostFactory */
private $postFactory;
/** @var CommentFactory */
private $commentFactory;
public function __construct(PostFactory $postFactory, CommentFactory $commentFactory)
{
$this->postFactory = $postFactory;
$this->commentFactory = $commentFactory;
}
/**
* Returns true on success (all data saved)
*/
public function processMessage(Message $message): bool
{
if (!$message->isValid()) {
return false;
}
switch (true) {
case $message->isPost():
return $this->processPost($message);
break;
case $message->isComment():
return $this->processComment($message);
break;
case $message->isCommentRecommendation():
return $this->processRecommendation($message);
break;
}
return false;
}
private function processPost(Message $postData): bool
{
$this->postFactory->findOrCreateFromWebsocketDto($postData);
return true;
}
private function processComment(Message $commentData): bool
{
// Not done yet
return false;
$this->commentFactory->findOrCreateFromWebsocketMessage($commentData);
return true;
}
private function processRecommendation(Message $recommendData): bool
{
return false;
}
}

View File

@ -2,11 +2,12 @@
namespace Tests\Skobkin\PointToolsBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Client;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
class MainControllerTest extends WebTestCase
{
public function testUserSearch()
public function testUserSearch(): void
{
$client = static::createClient();
$crawler = $client->request('GET', '/');
@ -19,7 +20,7 @@ class MainControllerTest extends WebTestCase
$this->assertTrue($client->getResponse()->isRedirect('/user/testuser'), 'Redirect to testuser\'s page didn\'t happen');
}
public function testNonExistingUserSearch()
public function testNonExistingUserSearch(): void
{
$client = static::createClient();
$crawler = $client->request('GET', '/');
@ -49,7 +50,7 @@ class MainControllerTest extends WebTestCase
$this->assertEquals(' Login not found', $firstError->text(), 'Incorrect error text');
}
public function testUserStats()
public function testUserStats(): void
{
$client = static::createClient();
$crawler = $client->request('GET', '/');
@ -75,14 +76,10 @@ class MainControllerTest extends WebTestCase
/**
* Tests AJAX user search autocomplete and returns JSON response string
*
* @return string
*/
public function testAjaxUserAutoComplete()
public function testAjaxUserAutoComplete(): string
{
$client = static::createClient();
// We need to search all test user with 'testuser5' included which will test the code against null-string problem in User#getName()
$client->request('GET', '/ajax/users/search/testuser');
$client = $this->createClientForAjaxUserSearchByLogin('testuser');
$this->assertTrue($client->getResponse()->headers->contains('Content-Type', 'application/json'), 'Response has "Content-Type" = "application/json"');
@ -91,26 +88,22 @@ class MainControllerTest extends WebTestCase
/**
* @depends testAjaxUserAutoComplete
*
* @param $json
*/
public function testAjaxUserAutoCompleteHasOptions($json)
public function testAjaxUserAutoCompleteHasOptions(string $json): array
{
$data = json_decode($json);
$data = json_decode($json, true);
$this->assertNotNull($data, 'JSON data successfully decoded and not empty');
$this->assertTrue(is_array($data), 'JSON data is array');
$this->assertCount(5, $data, 'Array has 5 elements');
$this->assertCount(2, $data, 'Array has 2 elements');
return $data;
}
/**
* @depends testAjaxUserAutoCompleteHasOptions
*
* @param array $users
*/
public function testAjaxUserAutoCompleteHasValidUserObjects(array $users)
public function testAjaxUserAutoCompleteHasValidUserObjects(array $users): void
{
foreach ($users as $key => $user) {
$this->assertTrue(array_key_exists('login', $user), sprintf('%d row of result has \'login\' field', $key));
@ -118,7 +111,43 @@ class MainControllerTest extends WebTestCase
}
}
public function testAjaxUserAutoCompleteForNonExistingUser()
/**
* Tests AJAX user search autocomplete for unnamed user and returns JSON response string
*/
public function testAjaxUserAutoCompleteForUnnamedUser(): string
{
$client = $this->createClientForAjaxUserSearchByLogin('unnamed_user');
$this->assertTrue($client->getResponse()->headers->contains('Content-Type', 'application/json'), 'Response has "Content-Type" = "application/json"');
return $client->getResponse()->getContent();
}
/**
* @depends testAjaxUserAutoCompleteForUnnamedUser
*/
public function testAjaxUserAutoCompleteHasOptionsForUnnamedUser(string $json): array
{
$data = json_decode($json, true);
$this->assertNotNull($data, 'JSON data successfully decoded and not empty');
$this->assertInternalType('array', $data, 'JSON data is array');
$this->assertCount(1, $data, 'Array has 1 elements');
return reset($data);
}
/**
* @depends testAjaxUserAutoCompleteHasOptionsForUnnamedUser
*/
public function testAjaxUserAutoCompleteHasValidUserObjectsForUnnamedUser(array $user): void
{
$this->assertTrue(array_key_exists('login', $user), 'Result has \'login\' field');
$this->assertTrue(array_key_exists('name', $user), 'Result has \'name\' field');
$this->assertEquals(true, ('' === $user['name'] || null === $user['name']), 'User name is empty string or null');
}
public function testAjaxUserAutoCompleteIsEmptyForNonExistingUser(): void
{
$client = static::createClient();
$client->request('GET', '/ajax/users/search/aksdjhaskdjhqwhdgqkjwhdgkjah');
@ -128,7 +157,15 @@ class MainControllerTest extends WebTestCase
$data = json_decode($client->getResponse()->getContent());
$this->assertNotNull($data, 'JSON data successfully decoded and not empty');
$this->assertTrue(is_array($data), 'JSON data is array');
$this->assertEquals(0, count($data), 'Array has no elements');
$this->assertInternalType('array', $data, 'JSON data is array');
$this->assertCount(0, $data, 'Array has no elements');
}
private function createClientForAjaxUserSearchByLogin(string $login): Client
{
$client = static::createClient();
$client->request('GET', '/ajax/users/search/'.$login);
return $client;
}
}

View File

@ -2,15 +2,15 @@
namespace Tests\Skobkin\PointToolsBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Skobkin\Bundle\PointToolsBundle\DataFixtures\ORM\LoadPostData;
use Symfony\Bundle\FrameworkBundle\{Client, Test\WebTestCase};
use Symfony\Component\DomCrawler\Crawler;
class PostControllerTest extends WebTestCase
{
public function testNonExistingPostPage()
{
$client = static::createClient();
$client->request('GET', '/nonexistingpost');
$client = $this->createClientForPostId('nonexistingpost');
$this->assertTrue($client->getResponse()->isNotFound(), '404 response code for non-existing post');
}
@ -20,12 +20,11 @@ class PostControllerTest extends WebTestCase
*/
public function testShortPostPageIsOk()
{
$client = static::createClient();
$crawler = $client->request('GET', '/shortpost');
$client = $this->createClientForPostId(LoadPostData::POST_ID_SHORT);
$this->assertTrue($client->getResponse()->isOk(), '200 response code for existing post');
return $crawler;
return $client->getCrawler();
}
/**
@ -58,4 +57,33 @@ class PostControllerTest extends WebTestCase
$this->assertEquals(1, $p->count(), '.post-text has zero or more than one paragraphs');
$this->assertEquals('Test short post', $p->text(), '.post-text has no correct post text');
}
public function testPrivateUserPostForbidden()
{
$client = $this->createClientForPostId(LoadPostData::POST_ID_PR_USER);
$this->assertTrue($client->getResponse()->isNotFound(), '404 response code for private user\'s post');
}
public function testWhitelistOnlyUserPostForbidden()
{
$client = $this->createClientForPostId(LoadPostData::POST_ID_WL_USER);
$this->assertTrue($client->getResponse()->isNotFound(), '404 response code for whitelist-only user\'s post');
}
public function testPrivateWhitelistOnlyUserPostForbidden()
{
$client = $this->createClientForPostId(LoadPostData::POST_ID_PR_WL_USER);
$this->assertTrue($client->getResponse()->isNotFound(), '404 response code for private whitelist-only user\'s post');
}
private function createClientForPostId(string $id): Client
{
$client = static::createClient();
$client->request('GET', '/'.$id);
return $client;
}
}

View File

@ -32,7 +32,7 @@ class UserRepositoryTest extends KernelTestCase
{
$users = $this->userRepo->findAll();
$this->assertCount(5, $users, 'Not exactly 5 users in the databas');
$this->assertCount(6, $users, 'Not exactly 6 users in the databas');
}
public function testFindOneByLogin()
@ -58,14 +58,14 @@ class UserRepositoryTest extends KernelTestCase
// Searching LIKE %stus% (testuserX)
$users = $this->userRepo->findUsersLikeLogin('stus');
$this->assertCount(5, $users, 'Repository found not exactly 5 users');
$this->assertCount(2, $users, 'Repository found not exactly 5 users');
}
public function testGetUsersCount()
{
$count = $this->userRepo->getUsersCount();
$this->assertEquals(5, $count, 'Counted not exactly 5 users');
$this->assertEquals(6, $count, 'Counted not exactly 5 users');
}
public function testFindUserSubscribersById()