Completed
Push — master ( b1b8bf...c9de9a )
by Joschi
03:57
created

ObjectTest::createRepositoryAndArticleObject()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 15
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 8
c 1
b 0
f 0
nc 1
nop 2
dl 0
loc 15
rs 9.4285
1
<?php
2
3
/**
4
 * apparat-object
5
 *
6
 * @category    Apparat
7
 * @package     Apparat\Object
8
 * @subpackage  Apparat\Object\Infrastructure
9
 * @author      Joschi Kuphal <[email protected]> / @jkphl
10
 * @copyright   Copyright © 2016 Joschi Kuphal <[email protected]> / @jkphl
11
 * @license     http://opensource.org/licenses/MIT The MIT License (MIT)
12
 */
13
14
/***********************************************************************************
15
 *  The MIT License (MIT)
16
 *
17
 *  Copyright © 2016 Joschi Kuphal <[email protected]> / @jkphl
18
 *
19
 *  Permission is hereby granted, free of charge, to any person obtaining a copy of
20
 *  this software and associated documentation files (the "Software"), to deal in
21
 *  the Software without restriction, including without limitation the rights to
22
 *  use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
23
 *  the Software, and to permit persons to whom the Software is furnished to do so,
24
 *  subject to the following conditions:
25
 *
26
 *  The above copyright notice and this permission notice shall be included in all
27
 *  copies or substantial portions of the Software.
28
 *
29
 *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
30
 *  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
31
 *  FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
32
 *  COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
33
 *  IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
34
 *  CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
35
 ***********************************************************************************/
36
37
namespace Apparat\Object\Tests {
38
39
    use Apparat\Object\Application\Factory\ObjectFactory;
40
    use Apparat\Object\Application\Model\Object\Article;
41
    use Apparat\Object\Domain\Model\Object\AbstractObject;
42
    use Apparat\Object\Domain\Model\Object\Id;
43
    use Apparat\Object\Domain\Model\Object\ResourceInterface;
44
    use Apparat\Object\Domain\Model\Object\Revision;
45
    use Apparat\Object\Domain\Model\Object\Type;
46
    use Apparat\Object\Domain\Model\Path\RepositoryPath;
47
    use Apparat\Object\Domain\Model\Properties\SystemProperties;
48
    use Apparat\Object\Domain\Repository\Repository;
49
    use Apparat\Object\Infrastructure\Repository\FileAdapterStrategy;
50
    use Apparat\Object\Ports\Object;
51
    use Apparat\Object\Ports\Repository as RepositoryFactory;
52
53
    /**
54
     * Object tests
55
     *
56
     * @package Apparat\Object
57
     * @subpackage Apparat\Object\Test
58
     */
59
    class ObjectTest extends AbstractRepositoryEnabledTest
60
    {
61
        /**
62
         * Example object path
63
         *
64
         * @var string
65
         */
66
        const OBJECT_PATH = '/2015/12/21/1-article/1';
67
        /**
68
         * Example hidden object path
69
         *
70
         * @var string
71
         */
72
        const HIDDEN_OBJECT_PATH = '/2016/05/26/6-article/6';
73
74
        /**
75
         * Tears down the fixture
76
         */
77
        public function tearDown()
78
        {
79
            putenv('MOCK_FLOCK');
80
            putenv('MOCK_RENAME');
81
            TestType::removeInvalidType();
82
            parent::tearDown();
83
        }
84
85
        /**
86
         * Test undefined object type
87
         *
88
         * @expectedException \Apparat\Object\Application\Factory\InvalidArgumentException
89
         * @expectedExceptionCode 1450905868
90
         */
91
        public function testUndefinedObjectType()
92
        {
93
            $resource = $this->getMock(ResourceInterface::class);
94
            $resource->method('getPropertyData')->willReturn([]);
95
            $repositoryPath = $this->getMockBuilder(RepositoryPath::class)->disableOriginalConstructor()->getMock();
96
97
            /** @var ResourceInterface $resource */
98
            /** @var RepositoryPath $repositoryPath */
99
            ObjectFactory::createFromResource($repositoryPath, $resource);
100
        }
101
102
        /**
103
         * Test invalid object type
104
         *
105
         * @expectedException \Apparat\Object\Domain\Model\Object\InvalidArgumentException
106
         * @expectedExceptionCode 1449871242
107
         */
108
        public function testInvalidObjectType()
109
        {
110
            $resource = $this->getMock(ResourceInterface::class);
111
            $resource->method('getPropertyData')->willReturn([SystemProperties::COLLECTION => ['type' => 'invalid']]);
112
            $articleObjectPath = new RepositoryPath(self::$repository, self::OBJECT_PATH);
113
114
            /** @var ResourceInterface $resource */
115
            ObjectFactory::createFromResource($articleObjectPath, $resource);
116
        }
117
118
        /**
119
         * Load an article object with an invalid visibility requirement
120
         *
121
         * @expectedException \Apparat\Object\Domain\Repository\InvalidArgumentException
122
         * @expectedExceptionCode 1449999646
123
         */
124
        public function testLoadArticleObjectInvalidVisibility()
125
        {
126
            $articleObjectPath = new RepositoryPath(self::$repository, self::OBJECT_PATH);
127
            self::$repository->loadObject($articleObjectPath, 0);
128
        }
129
130
        /**
131
         * Load an article object
132
         */
133
        public function testLoadArticleObject()
134
        {
135
            $articleObjectPath = new RepositoryPath(self::$repository, self::OBJECT_PATH);
136
            $articleObject = self::$repository->loadObject($articleObjectPath);
137
            $this->assertEquals(
138
                getenv('APPARAT_BASE_URL').getenv('REPOSITORY_URL').self::OBJECT_PATH,
139
                $articleObject->getAbsoluteUrl()
140
            );
141
            $this->assertFalse($articleObject->isDeleted());
142
            $this->assertFalse($articleObject->getRepositoryPath()->isHidden());
143
        }
144
145
        /**
146
         * Load a hidden article object
147
         */
148
        public function testLoadHiddenArticleObject()
149
        {
150
            $articleObjectPath = new RepositoryPath(self::$repository, self::HIDDEN_OBJECT_PATH);
151
            $articleObject = self::$repository->loadObject($articleObjectPath);
152
            $this->assertTrue($articleObject->isDeleted());
153
            $this->assertTrue($articleObject->getRepositoryPath()->isHidden());
154
        }
155
156
        /**
157
         * Load an article object and test its system properties
158
         */
159
        public function testLoadArticleObjectSystemProperties()
160
        {
161
            $articleObjectPath = new RepositoryPath(self::$repository, self::OBJECT_PATH);
162
            $articleObject = self::$repository->loadObject($articleObjectPath);
163
            $this->assertInstanceOf(Article::class, $articleObject);
164
            $this->assertEquals(new Id(1), $articleObject->getId());
165
            $this->assertEquals(new Type(Type::ARTICLE), $articleObject->getType());
166
            $this->assertEquals(new Revision(1), $articleObject->getRevision());
167
            $this->assertFalse($articleObject->isDraft());
168
            $this->assertEquals(new \DateTimeImmutable('2015-12-21T22:30:00'), $articleObject->getCreated());
169
            $this->assertEquals(new \DateTimeImmutable('2015-12-21T22:45:00'), $articleObject->getPublished());
170
            $this->assertNull($articleObject->getDeleted());
171
            $this->assertEquals('en', $articleObject->getLanguage());
172
            $this->assertEquals(
173
                "# Example article object\n\nThis file is an example for an object of type `\"article\"`. It has a link to [Joschi Kuphal's website](https://jkphl.is) and features his avatar:\n![Joschi Kuphal](https://jkphl.is/avatar.jpg)\n",
174
                $articleObject->getPayload()
175
            );
176
        }
177
178
        /**
179
         * Load an article object and test its meta properties
180
         */
181
        public function testLoadArticleObjectMetaProperties()
182
        {
183
            $articleObjectPath = new RepositoryPath(self::$repository, self::OBJECT_PATH);
184
            $articleObject = self::$repository->loadObject($articleObjectPath);
185
            $this->assertInstanceOf(Article::class, $articleObject);
186
            $this->assertEquals('Example article object', $articleObject->getDescription());
187
            $this->assertEquals(
188
                'Article objects feature a Markdown payload along with some custom properties',
189
                $articleObject->getAbstract()
190
            );
191
            $this->assertArrayEquals(['apparat', 'object', 'example', 'article'], $articleObject->getKeywords());
192
            $this->assertArrayEquals(['example', 'text'], $articleObject->getCategories());
193
194
            // TODO Replace with contributed-by relations
195
//            $authorCount = count($articleObject->getAuthors());
196
//            $articleObject->addAuthor(AuthorFactory::createFromString(AuthorTest::GENERIC_AUTHOR));
197
//            $this->assertEquals($authorCount + 1, count($articleObject->getAuthors()));
198
        }
199
200
        /**
201
         * Load an article object and test its domain properties
202
         *
203
         * @expectedException \Apparat\Object\Domain\Model\Properties\InvalidArgumentException
204
         * @expectedExceptionCode 1450818168
205
         */
206
        public function testLoadArticleObjectDomainProperties()
207
        {
208
            $articleObjectPath = new RepositoryPath(self::$repository, self::OBJECT_PATH);
209
            $articleObject = self::$repository->loadObject($articleObjectPath);
210
            $this->assertEquals('/system/url', $articleObject->getDomainProperty('uid'));
211
            $this->assertEquals('value', $articleObject->getDomainProperty('group:single'));
212
            $articleObject->getDomainProperty('group:invalid');
213
        }
214
215
        /**
216
         * Load an article object and test an empty domain property name
217
         *
218
         * @expectedException \Apparat\Object\Domain\Model\Properties\InvalidArgumentException
219
         * @expectedExceptionCode 1450817720
220
         */
221
        public function testLoadArticleObjectDomainEmptyProperty()
222
        {
223
            $articleObjectPath = new RepositoryPath(self::$repository, self::OBJECT_PATH);
224
            $articleObject = self::$repository->loadObject($articleObjectPath);
225
            $articleObject->getDomainProperty('');
226
        }
227
228
        /**
229
         * Test the object facade with an absolute object URL
230
         */
231
        public function testObjectFacadeAbsolute()
232
        {
233
            $object = Object::instance(getenv('APPARAT_BASE_URL').getenv('REPOSITORY_URL').self::OBJECT_PATH);
234
            $this->assertInstanceOf(Article::class, $object);
235
        }
236
237
        /**
238
         * Test the object facade with a relative object URL
239
         */
240
        public function testObjectFacadeRelative()
241
        {
242
            $object = Object::instance(getenv('REPOSITORY_URL').self::OBJECT_PATH);
243
            $this->assertInstanceOf(Article::class, $object);
244
        }
245
246
        /**
247
         * Test the object facade with an invalid relative object URL
248
         *
249
         * @expectedException \Apparat\Resource\Ports\InvalidReaderArgumentException
250
         * @expectedExceptionCode 1447616824
251
         */
252
        public function testObjectFacadeRelativeInvalid()
253
        {
254
            $object = Object::instance(getenv('REPOSITORY_URL').'/2015/12/21/2-article/2');
255
            $this->assertInstanceOf(Article::class, $object);
256
        }
257
258
        /**
259
         * Test with a missing object type class
260
         *
261
         * @expectedException \Apparat\Object\Application\Factory\InvalidArgumentException
262
         * @expectedExceptionCode 1450824842
263
         */
264
        public function testInvalidObjectTypeClass()
265
        {
266
            TestType::addInvalidType();
267
268
            $resource = $this->getMock(ResourceInterface::class);
269
            $resource->method('getPropertyData')->willReturn([SystemProperties::COLLECTION => ['type' => 'invalid']]);
270
            $articleObjectPath = new RepositoryPath(self::$repository, '/2016/02/16/5-invalid/5');
271
272
            /** @var ResourceInterface $resource */
273
            ObjectFactory::createFromResource($articleObjectPath, $resource);
274
        }
275
276
        /**
277
         * Test instantiation of object with invalid domain properties collection
278
         *
279
         * @expectedException \Apparat\Object\Domain\Model\Properties\InvalidArgumentException
280
         * @expectedExceptionCode 1452288429
281
         */
282
        public function testInvalidDomainPropertyCollectionClass()
283
        {
284
            $this->getMockBuilder(AbstractObject::class)
285
                ->setConstructorArgs([new RepositoryPath(self::$repository, self::OBJECT_PATH)])
286
                ->getMock();
287
        }
288
289
        /**
290
         * Test the property data
291
         */
292
        public function testObjectPropertyData()
293
        {
294
//  $frontMarkResource = Resource::frontMark('file://'.__DIR__.DIRECTORY_SEPARATOR.'Fixture'.self::OBJECT_PATH.'.md');
295
            $object = Object::instance(getenv('REPOSITORY_URL').self::OBJECT_PATH);
296
            $this->assertTrue(is_array($object->getPropertyData()));
297
//        print_r($frontMarkResource->getData());
298
//        print_r($object->getPropertyData());
299
        }
300
301
        /**
302
         * Test mutation by altering metadata
303
         *
304
         * @expectedException \Apparat\Object\Domain\Model\Properties\OutOfBoundsException
305
         * @expectedExceptionCode 1462632083
306
         */
307
        public function testMetaDataMutation()
308
        {
309
            $object = Object::instance(getenv('REPOSITORY_URL').self::OBJECT_PATH);
310
            $this->assertTrue(is_array($object->getPropertyData()));
311
            $objectUrl = $object->getAbsoluteUrl();
312
            $objectRevision = $object->getRevision();
313
            $object->setTitle($object->getTitle().' (mutated)');
314
            $object->setSlug($object->getSlug().'-mutated');
315
            $object->setDescription($object->getDescription().' (mutated)');
316
            $object->setAbstract($object->getAbstract());
317
            $object->setLicense(ltrim($object->getLicense().', ', ', ').'MIT');
318
            $object->setKeywords(array_merge($object->getKeywords(), ['mutated']));
319
            $object->setCategories($object->getCategories());
320
            $this->assertEquals(preg_replace('%\/(.?+)$%', '/.$1-2', $objectUrl), $object->getAbsoluteUrl());
321
            $this->assertEquals($objectRevision->getRevision() + 1, $object->getRevision()->getRevision());
322
            $this->assertTrue($object->hasBeenModified());
323
            $this->assertTrue($object->hasBeenMutated());
324
            $this->assertEquals('MIT', $object->getLicense());
325
            $this->assertEquals(Object::PRIVACY_PRIVATE, $object->getPrivacy());
326
            $this->assertEquals(Object::PRIVACY_PUBLIC, $object->setPrivacy(Object::PRIVACY_PUBLIC)->getPrivacy());
327
            $object->setPrivacy('invalid');
328
        }
329
330
        /**
331
         * Test mutation by altering domain properties
332
         */
333
        public function testDomainPropertyMutation()
334
        {
335
            $object = Object::instance(getenv('REPOSITORY_URL').self::OBJECT_PATH);
336
            $this->assertTrue(is_array($object->getPropertyData()));
337
            $objectUrl = $object->getAbsoluteUrl();
338
            $objectRevision = $object->getRevision();
339
            $object->setDomainProperty('a:b:c', 'mutated');
340
            $this->assertEquals(preg_replace('%\/(.?+)$%', '/.$1-2', $objectUrl), $object->getAbsoluteUrl());
341
            $this->assertEquals($objectRevision->getRevision() + 1, $object->getRevision()->getRevision());
342
            $this->assertTrue($object->hasBeenModified());
343
            $this->assertTrue($object->hasBeenMutated());
344
        }
345
346
        /**
347
         * Test change by altering processing instructions
348
         */
349
        public function testProcessingInstructionChange()
350
        {
351
            $object = Object::instance(getenv('REPOSITORY_URL').self::OBJECT_PATH);
352
            $this->assertTrue(is_array($object->getPropertyData()));
353
            $objectUrl = $object->getAbsoluteUrl();
354
            $objectRevision = $object->getRevision();
355
            $object->setProcessingInstruction('css', 'other-style.css');
356
            $this->assertEquals($objectUrl, $object->getAbsoluteUrl());
357
            $this->assertEquals($objectRevision->getRevision(), $object->getRevision()->getRevision());
358
            $this->assertTrue($object->hasBeenModified());
359
            $this->assertFalse($object->hasBeenMutated());
360
        }
361
362
        /**
363
         * Test change by altering relations
364
         */
365
        public function testRelationChange()
366
        {
367
            // TODO: Implement
368
        }
369
370
        /**
371
         * Test to persist an earlier revision
372
         */
373
        public function testPersistEarlierRevision()
374
        {
375
            // TODO
376
        }
377
378
        /**
379
         * Test the creation and persisting of an article object with failing file lock
380
         *
381
         * @expectedException \Apparat\Object\Domain\Repository\RuntimeException
382
         * @expectedExceptionCode 1461406873
383
         */
384
        public function testCreateArticleObjectLockingImpossible()
385
        {
386
            putenv('MOCK_FLOCK=1');
387
            $this->testCreateAndPublishArticleObject();
388
        }
389
390
        /**
391
         * Test the creation and persisting of an article object
392
         */
393
        public function testCreateAndPublishArticleObject()
394
        {
395
            // Create a temporary repository & article
396
            $tempRepoDirectory = sys_get_temp_dir().DIRECTORY_SEPARATOR.'temp-repo';
397
            $payload = 'Revision 1 draft';
398
            $article = $this->createRepositoryAndArticleObject($tempRepoDirectory, $payload);
399
            $this->assertInstanceOf(Article::class, $article);
400
            $this->assertEquals($payload, $article->getPayload());
401
            $this->assertFileExists($tempRepoDirectory.
402
                str_replace('/', DIRECTORY_SEPARATOR, $article->getRepositoryPath()
403
                    ->withExtension(getenv('OBJECT_RESOURCE_EXTENSION'))));
404
405
            // Alter and persist the object
406
            $article->setPayload('Revision 1 draft (updated)');
407
            $article->persist();
408
409
            // Publish and persist the first object revision
410
            $article->setPayload('Revision 1');
411
            $article->publish();
412
            $article->persist();
413
414
            // Draft a second object revision
415
            $article->setPayload('Revision 2 draft');
416
            $article->persist();
417
418
            // Publish and persist the second object revision
419
            $article->publish();
420
            $article->setPayload('Revision 2');
421
            $article->persist();
422
423
            // Modify and persist a third object draft revision
424
            $article->setPayload('Revision 3 draft');
425
            $article->persist();
426
427
            // Wait for 2 seconds, modify and re-persist the object
428
            $now = time();
429
            sleep(2);
430
            $article->setPayload('Revision 3 draft (delayed modification)');
431
            $article->persist();
432
            $this->assertGreaterThanOrEqual($now + 2, $article->getModified()->format('U'));
433
434
            // Publish and persist a third object draft revision
435
            $article->publish()->persist();
436
437
            // Delete the object (and all it's revisions)
438
            $article->delete()->persist();
439
440
            // Undelete the object (and all it's revisions)
441
            $article->undelete()->persist();
442
443
            // Delete temporary repository
444
            $this->deleteRecursive($tempRepoDirectory);
445
        }
446
447
        /**
448
         * Test the creation and persisting of an article object with failing file lock
449
         *
450
         * @expectedException \Apparat\Object\Infrastructure\Repository\RuntimeException
451
         * @expectedExceptionCode 1464269155
452
         */
453
        public function testDeleteArticleObjectImpossible()
454
        {
455
            putenv('MOCK_RENAME=1');
456
            $this->tmpFiles[] = $tempRepoDirectory = sys_get_temp_dir().DIRECTORY_SEPARATOR.'temp-repo';
457
            $article = $this->createRepositoryAndArticleObject($tempRepoDirectory, 'Revision 1 draft');
458
            $this->deleteRecursive($tempRepoDirectory);
459
            $article->delete()->persist();
460
        }
461
462
        /**
463
         * Test the creation and persisting of an article object with failing file lock
464
         *
465
         * @expectedException \Apparat\Object\Infrastructure\Repository\RuntimeException
466
         * @expectedExceptionCode 1464269179
467
         */
468
        public function testUndeleteArticleObjectImpossible()
469
        {
470
            $this->tmpFiles[] = $tempRepoDirectory = sys_get_temp_dir().DIRECTORY_SEPARATOR.'temp-repo';
471
            $article = $this->createRepositoryAndArticleObject($tempRepoDirectory, 'Revision 1 draft');
472
            $article->delete()->persist();
473
            $this->deleteRecursive($tempRepoDirectory);
474
            putenv('MOCK_RENAME=1');
475
            $article->undelete()->persist();
476
        }
477
478
        /**
479
         * Create a temporary repository and article object
480
         *
481
         * @param string $tempRepoDirectory Repository directory
482
         * @param string $payload Article payload
483
         * @return Article Article object
484
         */
485
        protected function createRepositoryAndArticleObject($tempRepoDirectory, $payload)
486
        {
487
            $fileRepository = RepositoryFactory::create(
488
                getenv('REPOSITORY_URL'),
489
                [
490
                    'type' => FileAdapterStrategy::TYPE,
491
                    'root' => $tempRepoDirectory,
492
                ]
493
            );
494
            $this->assertInstanceOf(Repository::class, $fileRepository);
495
            $this->assertEquals($fileRepository->getAdapterStrategy()->getRepositorySize(), 0);
496
497
            // Create a new article in the temporary repository
498
            return $fileRepository->createObject(Type::ARTICLE, $payload);
499
        }
500
501
        /**
502
         * Recursively register a directory and all nested files and directories for deletion on teardown
503
         *
504
         * @param string $directory Directory
505
         */
506
        protected function deleteRecursive($directory)
507
        {
508
            $this->tmpFiles[] = $directory;
509
            foreach (scandir($directory) as $item) {
510
                if (!preg_match('%^\.+$%', $item)) {
511
                    $path = $directory.DIRECTORY_SEPARATOR.$item;
512
                    if (is_dir($path)) {
513
                        $this->deleteRecursive($path);
514
                        continue;
515
                    }
516
517
                    $this->tmpFiles[] = $path;
518
                }
519
            }
520
        }
521
    }
522
}
523
524
namespace Apparat\Object\Infrastructure\Repository {
525
526
    /**
527
     * Mocked version of the native flock() function
528
     *
529
     * @param resource $handle An open file pointer.
530
     * @param int $operation Operation is one of the following: LOCK_SH to acquire a shared lock (reader).
531
     * @param int $wouldblock The optional third argument is set to true if the lock would block (EWOULDBLOCK errno
532
     *     condition).
533
     * @return bool True on success or False on failure.
534
     */
535
    function flock($handle, $operation, &$wouldblock = null)
536
    {
537
        return (getenv('MOCK_FLOCK') != 1) ? \flock($handle, $operation, $wouldblock) : false;
538
    }
539
540
    /**
541
     * Mocked version of the native rename() function
542
     *
543
     * @param string $oldname The old name. The wrapper used in oldname must match the wrapper used in newname.
544
     * @param @param string $newname The new name.
545
     * @return bool true on success or false on failure.
546
     */
547
    function rename($oldname, $newname)
548
    {
549
        return (getenv('MOCK_RENAME') != 1) ? \rename($oldname, $newname) : false;
550
    }
551
}
552