Completed
Pull Request — master (#61)
by Vladimir
09:45
created

PHPUnit_Stakx_TestCase::getMockCollectionManager()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 11
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 6
nc 1
nop 0
1
<?php
2
3
/**
4
 * @copyright 2017 Vladimir Jimenez
5
 * @license   https://github.com/allejo/stakx/blob/master/LICENSE.md MIT
6
 */
7
8
namespace allejo\stakx\Test;
9
10
use allejo\stakx\Command\BuildableCommand;
11
use allejo\stakx\Configuration;
12
use allejo\stakx\Core\StakxLogger;
13
use allejo\stakx\Document\FrontMatterDocument;
14
use allejo\stakx\Filesystem\File;
15
use allejo\stakx\Filesystem\FilesystemLoader as fs;
16
use allejo\stakx\Manager\CollectionManager;
17
use allejo\stakx\Manager\DataManager;
18
use allejo\stakx\Manager\MenuManager;
19
use allejo\stakx\Manager\PageManager;
20
use allejo\stakx\MarkupEngine\MarkdownEngine;
21
use allejo\stakx\MarkupEngine\MarkupEngineManager;
22
use allejo\stakx\MarkupEngine\PlainTextEngine;
23
use allejo\stakx\MarkupEngine\RstEngine;
24
use allejo\stakx\Service;
25
use allejo\stakx\System\Filesystem;
26
use allejo\stakx\Filesystem\Folder;
27
use allejo\stakx\Templating\Twig\TwigExtension;
28
use org\bovigo\vfs\vfsStream;
29
use org\bovigo\vfs\vfsStreamDirectory;
30
use org\bovigo\vfs\vfsStreamFile;
31
use Psr\Log\LoggerInterface;
32
use Symfony\Component\Console\Output\ConsoleOutput;
33
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
34
use Symfony\Component\Yaml\Yaml;
35
36
abstract class PHPUnit_Stakx_TestCase extends \PHPUnit_Framework_TestCase
0 ignored issues
show
Coding Style introduced by
This class is not in CamelCase format.

Classes in PHP are usually named in CamelCase.

In camelCase names are written without any punctuation, the start of each new word being marked by a capital letter. The whole name starts with a capital letter as well.

Thus the name database provider becomes DatabaseProvider.

Loading history...
37
{
38
    const FM_OBJ_TEMPLATE = "---\n%s\n---\n\n%s";
39
40
    /** @var string */
41
    protected $assetFolder;
42
    /** @var vfsStreamFile */
43
    protected $dummyFile;
44
    /** @var vfsStreamDirectory */
45
    protected $rootDir;
46
    /** @var Filesystem */
47
    protected $fs;
0 ignored issues
show
Comprehensibility introduced by
Avoid variables with short names like $fs. Configured minimum length is 3.

Short variable names may make your code harder to understand. Variable names should be self-descriptive. This check looks for variable names who are shorter than a configured minimum.

Loading history...
48
49
    public function setUp()
50
    {
51
        $this->dummyFile = vfsStream::newFile('stakx.html.twig');
52
        $this->rootDir = vfsStream::setup();
53
        $this->fs = new Filesystem();
54
55
        Service::setWorkingDirectory(null);
56
        Service::setParameter(BuildableCommand::USE_DRAFTS, false);
57
        Service::setParameter(BuildableCommand::WATCHING, false);
58
        Service::setParameter(BuildableCommand::SAFE_MODE, false);
59
        Service::setParameter(BuildableCommand::BUILD_PROFILE, false);
60
        Service::setParameter(Configuration::HIGHLIGHTER_ENABLED, true);
61
        Service::setParameter('build.preserveCase', false);
62
63
        // Inspect the VFS as an array
64
        // vfsStream::inspect(new vfsStreamStructureVisitor())->getStructure();
0 ignored issues
show
Unused Code Comprehensibility introduced by
63% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
65
    }
66
67
    public function tearDown()
68
    {
69
        if ($this->assetFolder !== null)
70
        {
71
            $this->fs->remove($this->assetFolder);
72
        }
73
    }
74
75
    ///
76
    // Assertion Functions
77
    ///
78
79
    /**
80
     * @param string $needle
81
     * @param string $haystack
82
     * @param string $message
83
     */
84
    protected function assertStringContains($needle, $haystack, $message = '')
85
    {
86
        $this->assertNotFalse(strpos($haystack, $needle), $message);
87
    }
88
89
    /**
90
     * @param string $fileContent
91
     * @param string $filePath
92
     * @param string $message
93
     */
94
    protected function assertFileContains($fileContent, $filePath, $message = '')
95
    {
96
        (substr($filePath, -1, 1) == '/') && $filePath .= 'index.html';
97
98
        $contents = file_get_contents($filePath);
99
100
        $this->assertStringContains($fileContent, $contents, $message);
101
    }
102
103
    ///
104
    // Filesystem Functions
105
    ///
106
107
    /**
108
     * Create a temporary folder where temporary file writes will be made to.
109
     *
110
     * @param string $folderName
111
     */
112
    protected function createAssetFolder($folderName)
113
    {
114
        $this->assetFolder = fs::getRelativePath(fs::appendPath(__DIR__, $folderName));
115
116
        fs::mkdir($this->assetFolder);
117
    }
118
119
    /**
120
     * Write a file to the asset folder.
121
     *
122
     * This file will be written to the actual filesystem and not the virtual filesystem. This file will be deleted at
123
     * each tearDown().
124
     *
125
     * @param string $fileName
126
     * @param string $content
127
     *
128
     * @return string Path to the temporary file; relative to the project's root
129
     */
130
    protected function createPhysicalFile($fileName, $content)
131
    {
132
        $folder = new Folder($this->assetFolder);
133
        $folder->writeFile($fileName, $content);
134
135
        return fs::appendPath($this->assetFolder, $fileName);
136
    }
137
138
    /**
139
     * Write a file to the virtual filesystem.
140
     *
141
     * This file will be deleted at each tearDown().
142
     *
143
     * @param string $filename
144
     * @param string $content
145
     *
146
     * @return string The URL of the file on the virtual filesystem.
147
     */
148
    protected function createVirtualFile($filename, $content)
149
    {
150
        $file = vfsStream::newFile($filename);
151
        $file
152
            ->setContent($content)
153
            ->at($this->rootDir)
154
        ;
155
156
        return $file->url();
157
    }
158
159
    /**
160
     * Create an object of a given type.
161
     *
162
     * This will create a virtual file and then create an object of the specified type for the created file.
163
     *
164
     * @param string $classType
165
     * @param string $filename
166
     * @param string $content
167
     *
168
     * @return object An instance of $classType
169
     */
170
    protected function createDocumentOfType($classType, $filename, $content)
171
    {
172
        $file = $this->createVirtualFile($filename, $content);
173
174
        return new $classType(new File($file));
175
    }
176
177
    /**
178
     * Create an object of a given type following the Front Matter format.
179
     *
180
     * @param string $classType
181
     * @param string $filename
0 ignored issues
show
Documentation introduced by
Should the type for parameter $filename not be string|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
182
     * @param array  $frontMatter
183
     * @param string $content
184
     *
185
     * @return object An instance of $classType
186
     */
187
    protected function createFrontMatterDocumentOfType($classType, $filename = null, $frontMatter = [], $content = 'Body Text')
188
    {
189
        $body = $this->buildFrontMatterTemplate($frontMatter, $content);
190
191
        if (!$filename)
0 ignored issues
show
Bug Best Practice introduced by
The expression $filename of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
192
        {
193
            $filename = hash('sha256', uniqid(mt_rand(), true), false);
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $filename. This often makes code more readable.
Loading history...
194
        }
195
196
        return $this->createDocumentOfType($classType, $filename, $body);
197
    }
198
199
    /**
200
     * Create multiple virtual files from a given array of information.
201
     *
202
     * ```php
203
     * $elements = [
204
     *     [
205
     *         'filename' => '<string>',
206
     *         'frontmatter' => [],
207
     *         'body' => '<string>',
208
     *     ],
209
     * ];
210
     * ```
211
     *
212
     * @param string $classType
213
     * @param array  $elements
214
     *
215
     * @return array
216
     */
217
    protected function createMultipleFrontMatterDocumentsOfType($classType, $elements)
218
    {
219
        $results = [];
220
221
        foreach ($elements as $element)
222
        {
223
            $filename = (isset($element['filename'])) ? $element['filename'] : null;
224
            $frontMatter = (!isset($element['frontmatter']) || empty($element['frontmatter'])) ? [] : $element['frontmatter'];
225
            $body = (isset($element['body'])) ? $element['body'] : 'Body Text';
226
227
            /** @var FrontMatterDocument $item */
228
            $item = $this->createFrontMatterDocumentOfType($classType, $filename, $frontMatter, $body);
229
            $item->evaluateFrontMatter();
230
231
            $results[] = $item;
232
        }
233
234
        return $results;
235
    }
236
237
    /**
238
     * Create a File object from a given path.
239
     *
240
     * @deprecated
241
     *
242
     * @param  string $filePath
243
     *
244
     * @return File
245
     */
246
    protected function createFileObjectFromPath($filePath)
247
    {
248
        return (new File($filePath));
249
    }
250
251
    ///
252
    // Mock Objects
253
    ///
254
255
    /**
256
     * @return Configuration|\PHPUnit_Framework_MockObject_MockObject
257
     */
258
    protected function getMockConfiguration()
259
    {
260
        $stub = $this->getMockBuilder(Configuration::class)
261
            ->disableOriginalConstructor()
262
            ->getMock()
263
        ;
264
265
        $stub->method('getConfiguration')->willReturn([]);
266
        $stub->method('getTwigAutoescape')->willReturn(false);
267
268
        return $stub;
269
    }
270
271
    /**
272
     * @return PageManager|\PHPUnit_Framework_MockObject_MockObject
273
     */
274
    protected function getMockPageManager()
275
    {
276
        $stub = $this->getMockBuilder(PageManager::class)
277
            ->disableOriginalConstructor()
278
            ->getMock()
279
        ;
280
281
        $stub->method('getJailedStaticPageViews')->willReturn([]);
282
283
        return $stub;
284
    }
285
286
    /**
287
     * @return MenuManager|\PHPUnit_Framework_MockObject_MockObject
288
     */
289
    protected function getMockMenuManager()
290
    {
291
        $stub = $this->getMockBuilder(MenuManager::class)
292
            ->disableOriginalConstructor()
293
            ->getMock()
294
        ;
295
296
        $stub->method('getSiteMenu')->willReturn([]);
297
298
        return $stub;
299
    }
300
301
    /**
302
     * @return CollectionManager|\PHPUnit_Framework_MockObject_MockObject
303
     */
304
    protected function getMockCollectionManager()
305
    {
306
        $stub = $this->getMockBuilder(CollectionManager::class)
307
            ->disableOriginalConstructor()
308
            ->getMock()
309
        ;
310
311
        $stub->method('getJailedCollections')->willReturn([]);
312
313
        return $stub;
314
    }
315
316
    /**
317
     * @return DataManager|\PHPUnit_Framework_MockObject_MockObject
318
     */
319
    protected function getMockDataManager()
320
    {
321
        $stub = $this->getMockBuilder(DataManager::class)
322
            ->disableOriginalConstructor()
323
            ->getMock()
324
        ;
325
326
        $stub->method('getJailedDataItems')->willReturn([]);
327
328
        return $stub;
329
    }
330
331
    /**
332
     * @return TwigExtension
333
     */
334
    protected function getMockTwigExtension()
335
    {
336
        // too lazy to actually mock all the methods... just create an actual instance of and dub it a "mock" to match
337
        // all the other mock methods. if this causes problems: sorry, future me!
338
        return new TwigExtension($this->getMockMarkupEngineManager());
339
    }
340
341
    /**
342
     * @return MarkupEngineManager
343
     */
344
    protected function getMockMarkupEngineManager()
345
    {
346
        // Just trying to keep the naming convention the same, but create an actual markup engine manager since it's
347
        // necessary in a lot of the unit tests
348
        $markupEngine = new MarkupEngineManager();
349
350
        $markupEngine->addMarkupEngines([
351
            new MarkdownEngine(),
352
            new RstEngine(),
353
            new PlainTextEngine(),
354
        ]);
355
356
        return $markupEngine;
357
    }
358
359
    /**
360
     * Get a mock EventDispatcher.
361
     *
362
     * @return EventDispatcherInterface
363
     */
364
    protected function getMockEventDistpatcher()
365
    {
366
        return $this->getMock(EventDispatcherInterface::class);
367
    }
368
369
    /**
370
     * Get a mock logger.
371
     *
372
     * @return LoggerInterface
373
     */
374
    protected function getMockLogger()
375
    {
376
        return $this->getMock(LoggerInterface::class);
377
    }
378
379
    /**
380
     * Get a real logger instance that will save output to the console.
381
     *
382
     * @return StakxLogger
383
     */
384
    protected function getReadableLogger()
385
    {
386
        stream_filter_register('intercept', StreamInterceptor::class);
387
        $stakxLogger = new StakxLogger(new ConsoleOutput());
388
        stream_filter_append($stakxLogger->getOutputInterface()->getStream(), 'intercept');
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Symfony\Component\Console\Output\OutputInterface as the method getStream() does only exist in the following implementations of said interface: Symfony\Component\Console\Output\ConsoleOutput, Symfony\Component\Console\Output\StreamOutput.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
389
390
        return $stakxLogger;
391
    }
392
393
    ///
394
    // Utility Functions
395
    ///
396
397
    /**
398
     * Get the directory of the unit tests.
399
     *
400
     * @return string
401
     */
402
    protected static function getTestRoot()
403
    {
404
        return __DIR__;
405
    }
406
407
    /**
408
     * Generate a FrontMatter-ready syntax to be used as a file's content.
409
     *
410
     * @param array  $frontMatter
411
     * @param string $body
412
     *
413
     * @return string
414
     */
415
    protected function buildFrontMatterTemplate(array $frontMatter = array(), $body = 'Body text')
416
    {
417
        $fm = (empty($frontMatter)) ? '' : Yaml::dump($frontMatter, 2);
0 ignored issues
show
Comprehensibility introduced by
Avoid variables with short names like $fm. Configured minimum length is 3.

Short variable names may make your code harder to understand. Variable names should be self-descriptive. This check looks for variable names who are shorter than a configured minimum.

Loading history...
418
419
        return sprintf(self::FM_OBJ_TEMPLATE, $fm, $body);
420
    }
421
422
    ///
423
    // Misc Functions
424
    ///
425
426
    protected function bookCollectionProvider($jailed = false)
427
    {
428
        $cm = new CollectionManager(
0 ignored issues
show
Comprehensibility introduced by
Avoid variables with short names like $cm. Configured minimum length is 3.

Short variable names may make your code harder to understand. Variable names should be self-descriptive. This check looks for variable names who are shorter than a configured minimum.

Loading history...
429
            $this->getMockMarkupEngineManager(),
430
            $this->getMock(Configuration::class),
431
            $this->getMockEventDistpatcher(),
432
            $this->getMockLogger()
433
        );
434
        $cm->parseCollections(array(
435
            array(
436
                'name' => 'books',
437
                'folder' => 'tests/allejo/stakx/Test/assets/MyBookCollection/',
438
            ),
439
        ));
440
441
        return (!$jailed) ? $cm->getCollections() : $cm->getJailedCollections();
442
    }
443
}
444