Completed
Pull Request — master (#69)
by Vladimir
02:48
created

PHPUnit_Stakx_TestCase::getMockTwigExtension()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 6
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
1
<?php
2
3
/**
4
 * @copyright 2018 Vladimir Jimenez
5
 * @license   https://github.com/stakx-io/stakx/blob/master/LICENSE.md MIT
6
 */
7
8
namespace allejo\stakx\Test;
9
10
use allejo\stakx\AssetEngine\AssetEngineInterface;
11
use allejo\stakx\AssetEngine\AssetEngineManager;
12
use allejo\stakx\Configuration;
13
use allejo\stakx\Document\FrontMatterDocument;
14
use allejo\stakx\Filesystem\File;
15
use allejo\stakx\Filesystem\Filesystem;
16
use allejo\stakx\Filesystem\FilesystemLoader as fs;
17
use allejo\stakx\Filesystem\Folder;
18
use allejo\stakx\Logger;
19
use allejo\stakx\Manager\CollectionManager;
20
use allejo\stakx\Manager\DataManager;
21
use allejo\stakx\Manager\MenuManager;
22
use allejo\stakx\Manager\PageManager;
23
use allejo\stakx\MarkupEngine\MarkdownEngine;
24
use allejo\stakx\MarkupEngine\MarkupEngineManager;
25
use allejo\stakx\MarkupEngine\PlainTextEngine;
26
use allejo\stakx\MarkupEngine\RstEngine;
27
use allejo\stakx\RuntimeStatus;
28
use allejo\stakx\Service;
29
use allejo\stakx\Templating\Twig\TwigExtension;
30
use allejo\stakx\Templating\Twig\TwigStakxBridge;
31
use allejo\stakx\Templating\Twig\TwigStakxBridgeFactory;
32
use org\bovigo\vfs\vfsStream;
33
use org\bovigo\vfs\vfsStreamDirectory;
34
use org\bovigo\vfs\vfsStreamFile;
35
use Psr\Log\LoggerInterface;
36
use Symfony\Component\Console\Output\ConsoleOutput;
37
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
38
use Symfony\Component\Yaml\Yaml;
39
40
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...
41
{
42
    const FM_OBJ_TEMPLATE = "---\n%s\n---\n\n%s";
43
44
    /** @var string */
45
    protected $assetFolder;
46
    /** @var vfsStreamFile */
47
    protected $dummyFile;
48
    /** @var vfsStreamDirectory */
49
    protected $rootDir;
50
51
    public function setUp()
52
    {
53
        $this->dummyFile = vfsStream::newFile('stakx.html.twig');
54
        $this->rootDir = vfsStream::setup();
55
56
        Service::resetRuntimeFlags();
57
58
        Service::setWorkingDirectory(null);
59
        Service::setRuntimeFlag(RuntimeStatus::USING_HIGHLIGHTER);
60
61
        // Inspect the VFS as an array
62
        // 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...
63
    }
64
65
    public function tearDown()
66
    {
67
        if ($this->assetFolder !== null)
68
        {
69
            fs::remove($this->assetFolder);
0 ignored issues
show
Bug introduced by
The method remove() does not exist on allejo\stakx\Filesystem\FilesystemLoader. Did you maybe mean removeExtension()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
70
        }
71
    }
72
73
    ///
74
    // Assertion Functions
75
    ///
76
77
    /**
78
     * @param string $needle
79
     * @param string $haystack
80
     * @param string $message
81
     */
82
    protected function assertStringContains($needle, $haystack, $message = '')
83
    {
84
        $this->assertNotFalse(strpos($haystack, $needle), $message);
85
    }
86
87
    /**
88
     * @param string $fileContent
89
     * @param string $filePath
90
     * @param string $message
91
     */
92
    protected function assertFileContains($fileContent, $filePath, $message = '')
93
    {
94
        (substr($filePath, -1, 1) == '/') && $filePath .= 'index.html';
95
96
        $contents = file_get_contents($filePath);
97
98
        $this->assertStringContains($fileContent, $contents, $message);
99
    }
100
101
    ///
102
    // Filesystem Functions
103
    ///
104
105
    /**
106
     * Create a temporary folder where temporary file writes will be made to.
107
     *
108
     * @param string $folderName
109
     */
110
    protected function createAssetFolder($folderName)
111
    {
112
        $this->assetFolder = fs::getRelativePath(fs::appendPath(__DIR__, $folderName));
0 ignored issues
show
Documentation Bug introduced by
It seems like \allejo\stakx\Filesystem...(__DIR__, $folderName)) of type object<string> is incompatible with the declared type string of property $assetFolder.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
113
114
        fs::mkdir($this->assetFolder);
115
    }
116
117
    /**
118
     * Write a file to the asset folder.
119
     *
120
     * This file will be written to the actual filesystem and not the virtual filesystem. This file will be deleted at
121
     * each tearDown().
122
     *
123
     * @param string $fileName
124
     * @param string $content
125
     *
126
     * @return string Path to the temporary file; relative to the project's root
127
     */
128
    protected function createPhysicalFile($fileName, $content)
129
    {
130
        $folder = new Folder($this->assetFolder);
131
        $folder->writeFile($fileName, $content);
132
133
        return fs::appendPath($this->assetFolder, $fileName);
134
    }
135
136
    /**
137
     * Write a file to the virtual filesystem.
138
     *
139
     * This file will be deleted at each tearDown().
140
     *
141
     * @param string $filename
142
     * @param string $content
143
     *
144
     * @return string the URL of the file on the virtual filesystem
145
     */
146
    protected function createVirtualFile($filename, $content)
147
    {
148
        $file = vfsStream::newFile($filename);
149
        $file
150
            ->setContent($content)
151
            ->at($this->rootDir)
152
        ;
153
154
        return $file->url();
155
    }
156
157
    /**
158
     * Create an object of a given type.
159
     *
160
     * This will create a virtual file and then create an object of the specified type for the created file.
161
     *
162
     * @param string $classType
163
     * @param string $filename
164
     * @param string $content
165
     *
166
     * @return object An instance of $classType
167
     */
168
    protected function createDocumentOfType($classType, $filename, $content)
169
    {
170
        $file = $this->createVirtualFile($filename, $content);
171
172
        return new $classType(new File($file));
173
    }
174
175
    /**
176
     * Create an object of a given type following the Front Matter format.
177
     *
178
     * @param string $classType
179
     * @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...
180
     * @param array  $frontMatter
181
     * @param string $content
182
     *
183
     * @return object An instance of $classType
184
     */
185
    protected function createFrontMatterDocumentOfType($classType, $filename = null, $frontMatter = [], $content = 'Body Text')
186
    {
187
        $body = $this->buildFrontMatterTemplate($frontMatter, $content);
188
189
        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...
190
        {
191
            $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...
192
        }
193
194
        return $this->createDocumentOfType($classType, $filename, $body);
195
    }
196
197
    /**
198
     * Create multiple virtual files from a given array of information.
199
     *
200
     * ```php
201
     * $elements = [
202
     *     [
203
     *         'filename' => '<string>',
204
     *         'frontmatter' => [],
205
     *         'body' => '<string>',
206
     *     ],
207
     * ];
208
     * ```
209
     *
210
     * @param string $classType
211
     * @param array  $elements
212
     *
213
     * @return array
214
     */
215
    protected function createMultipleFrontMatterDocumentsOfType($classType, $elements)
216
    {
217
        $results = [];
218
219
        foreach ($elements as $element)
220
        {
221
            $filename = (isset($element['filename'])) ? $element['filename'] : null;
222
            $frontMatter = (!isset($element['frontmatter']) || empty($element['frontmatter'])) ? [] : $element['frontmatter'];
223
            $body = (isset($element['body'])) ? $element['body'] : 'Body Text';
224
225
            /** @var FrontMatterDocument $item */
226
            $item = $this->createFrontMatterDocumentOfType($classType, $filename, $frontMatter, $body);
227
            $item->evaluateFrontMatter();
228
229
            $results[] = $item;
230
        }
231
232
        return $results;
233
    }
234
235
    /**
236
     * Create a File object from a given path.
237
     *
238
     * @deprecated
239
     *
240
     * @param string $filePath
241
     *
242
     * @return File
243
     */
244
    protected function createFileObjectFromPath($filePath)
245
    {
246
        return new File($filePath);
247
    }
248
249
    ///
250
    // Mock Objects
251
    ///
252
253
    /**
254
     * @return AssetEngineManager|\PHPUnit_Framework_MockObject_MockBuilder
255
     */
256
    protected function getMockAssetEngineManager()
257
    {
258
        return new AssetEngineManager();
259
    }
260
261
    /**
262
     * @return Configuration|\PHPUnit_Framework_MockObject_MockObject
263
     */
264
    protected function getMockConfiguration()
265
    {
266
        $stub = $this->getMockBuilder(Configuration::class)
267
            ->disableOriginalConstructor()
268
            ->getMock()
269
        ;
270
271
        $stub->method('getConfiguration')->willReturn([]);
272
        $stub->method('getTwigAutoescape')->willReturn(false);
273
274
        return $stub;
275
    }
276
277
    /**
278
     * @return PageManager|\PHPUnit_Framework_MockObject_MockObject
279
     */
280
    protected function getMockPageManager()
281
    {
282
        $stub = $this->getMockBuilder(PageManager::class)
283
            ->disableOriginalConstructor()
284
            ->getMock()
285
        ;
286
287
        $stub->method('getJailedStaticPageViews')->willReturn([]);
288
289
        return $stub;
290
    }
291
292
    /**
293
     * @return MenuManager|\PHPUnit_Framework_MockObject_MockObject
294
     */
295
    protected function getMockMenuManager()
296
    {
297
        $stub = $this->getMockBuilder(MenuManager::class)
298
            ->disableOriginalConstructor()
299
            ->getMock()
300
        ;
301
302
        $stub->method('getSiteMenu')->willReturn([]);
303
304
        return $stub;
305
    }
306
307
    /**
308
     * @return CollectionManager|\PHPUnit_Framework_MockObject_MockObject
309
     */
310
    protected function getMockCollectionManager()
311
    {
312
        $stub = $this->getMockBuilder(CollectionManager::class)
313
            ->disableOriginalConstructor()
314
            ->getMock()
315
        ;
316
317
        $stub->method('getJailedCollections')->willReturn([]);
318
319
        return $stub;
320
    }
321
322
    /**
323
     * @return DataManager|\PHPUnit_Framework_MockObject_MockObject
324
     */
325
    protected function getMockDataManager()
326
    {
327
        $stub = $this->getMockBuilder(DataManager::class)
328
            ->disableOriginalConstructor()
329
            ->getMock()
330
        ;
331
332
        $stub->method('getJailedDataItems')->willReturn([]);
333
334
        return $stub;
335
    }
336
337
    /**
338
     * @return TwigExtension
339
     */
340
    protected function getMockTwigExtension()
341
    {
342
        // too lazy to actually mock all the methods... just create an actual instance of and dub it a "mock" to match
343
        // all the other mock methods. if this causes problems: sorry, future me!
344
        return new TwigExtension($this->getMockMarkupEngineManager());
345
    }
346
347
    /**
348
     * @return TwigStakxBridge
349
     */
350
    protected function getMockTemplateBridge()
351
    {
352
        return TwigStakxBridgeFactory::createTwigEnvironment(
353
            $this->getMockConfiguration(),
0 ignored issues
show
Bug introduced by
It seems like $this->getMockConfiguration() targeting allejo\stakx\Test\PHPUni...:getMockConfiguration() can also be of type object<PHPUnit_Framework_MockObject_MockObject>; however, allejo\stakx\Templating\...createTwigEnvironment() does only seem to accept object<allejo\stakx\Configuration>, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
354
            $this->getMockTwigExtension(),
355
            $this->getMockLogger()
0 ignored issues
show
Bug introduced by
It seems like $this->getMockLogger() targeting allejo\stakx\Test\PHPUni...stCase::getMockLogger() can also be of type object<PHPUnit_Framework_MockObject_MockObject>; however, allejo\stakx\Templating\...createTwigEnvironment() does only seem to accept object<Psr\Log\LoggerInterface>, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
356
        );
357
    }
358
359
    /**
360
     * @return MarkupEngineManager
361
     */
362
    protected function getMockMarkupEngineManager()
363
    {
364
        // Just trying to keep the naming convention the same, but create an actual markup engine manager since it's
365
        // necessary in a lot of the unit tests
366
        $markupEngine = new MarkupEngineManager();
367
368
        $markupEngine->addMarkupEngines([
369
            new MarkdownEngine(),
370
            new RstEngine(),
371
            new PlainTextEngine(),
372
        ]);
373
374
        return $markupEngine;
375
    }
376
377
    /**
378
     * Get a mock EventDispatcher.
379
     *
380
     * @return EventDispatcherInterface|\PHPUnit_Framework_MockObject_MockObject
381
     */
382
    protected function getMockEventDistpatcher()
383
    {
384
        return $this->getMock(EventDispatcherInterface::class);
385
    }
386
387
    /**
388
     * Get a mock logger.
389
     *
390
     * @return LoggerInterface|\PHPUnit_Framework_MockObject_MockObject
391
     */
392
    protected function getMockLogger()
393
    {
394
        return $this->getMock(LoggerInterface::class);
395
    }
396
397
    /**
398
     * Get a real logger instance that will save output to the console.
399
     *
400
     * @return Logger
401
     */
402
    protected function getReadableLogger()
403
    {
404
        stream_filter_register('intercept', StreamInterceptor::class);
405
        $stakxLogger = new Logger(new ConsoleOutput());
406
        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...
407
408
        return $stakxLogger;
409
    }
410
411
    ///
412
    // Utility Functions
413
    ///
414
415
    /**
416
     * Get the directory of the unit tests.
417
     *
418
     * @return string
419
     */
420
    protected static function getTestRoot()
421
    {
422
        return __DIR__;
423
    }
424
425
    /**
426
     * Generate a FrontMatter-ready syntax to be used as a file's content.
427
     *
428
     * @param array  $frontMatter
429
     * @param string $body
430
     *
431
     * @return string
432
     */
433
    protected function buildFrontMatterTemplate(array $frontMatter = [], $body = 'Body text')
434
    {
435
        $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...
436
437
        return sprintf(self::FM_OBJ_TEMPLATE, $fm, $body);
438
    }
439
440
    ///
441
    // Misc Functions
442
    ///
443
444
    protected function bookCollectionProvider($jailed = false)
445
    {
446
        $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...
447
            $this->getMockMarkupEngineManager(),
448
            $this->getMockConfiguration(),
0 ignored issues
show
Bug introduced by
It seems like $this->getMockConfiguration() targeting allejo\stakx\Test\PHPUni...:getMockConfiguration() can also be of type object<PHPUnit_Framework_MockObject_MockObject>; however, allejo\stakx\Manager\Col...nManager::__construct() does only seem to accept object<allejo\stakx\Configuration>, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
449
            $this->getMockTemplateBridge(),
450
            $this->getMockEventDistpatcher(),
0 ignored issues
show
Bug introduced by
It seems like $this->getMockEventDistpatcher() targeting allejo\stakx\Test\PHPUni...tMockEventDistpatcher() can also be of type object<PHPUnit_Framework_MockObject_MockObject>; however, allejo\stakx\Manager\Col...nManager::__construct() does only seem to accept object<Symfony\Component...entDispatcherInterface>, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
451
            $this->getMockLogger()
0 ignored issues
show
Bug introduced by
It seems like $this->getMockLogger() targeting allejo\stakx\Test\PHPUni...stCase::getMockLogger() can also be of type object<PHPUnit_Framework_MockObject_MockObject>; however, allejo\stakx\Manager\Col...nManager::__construct() does only seem to accept object<Psr\Log\LoggerInterface>, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
452
        );
453
        $cm->parseCollections([
454
            [
455
                'name' => 'books',
456
                'folder' => 'tests/allejo/stakx/Test/assets/MyBookCollection/',
457
            ],
458
        ]);
459
460
        return (!$jailed) ? $cm->getCollections() : $cm->getJailedCollections();
461
    }
462
}
463