Completed
Push — master ( bd6d37...98df61 )
by Joschi
03:30
created

ObjectTest::testProcessingInstructionChange()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 12
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 4
Bugs 0 Features 2
Metric Value
cc 1
eloc 10
c 4
b 0
f 2
nc 1
nop 0
dl 0
loc 12
rs 9.4285

1 Method

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