1<?php
2
3namespace FINDOLOGIC\Export\Tests;
4
5use DateTime;
6use FINDOLOGIC\Export\Data\Attribute;
7use FINDOLOGIC\Export\Data\Image;
8use FINDOLOGIC\Export\Data\Keyword;
9use FINDOLOGIC\Export\Data\Ordernumber;
10use FINDOLOGIC\Export\Data\Price;
11use FINDOLOGIC\Export\Data\Property;
12use FINDOLOGIC\Export\Data\Usergroup;
13use FINDOLOGIC\Export\Exporter;
14use FINDOLOGIC\Export\Helpers\XMLHelper;
15use FINDOLOGIC\Export\XML\XMLExporter;
16use FINDOLOGIC\Export\XML\XMLItem;
17use PHPUnit\Framework\TestCase;
18
19/**
20 * Class XmlSerializationTest
21 * @package FINDOLOGIC\Tests
22 *
23 * Tests that the serialized output adheres to the defined schema and to things that cannot be covered by it.
24 *
25 * The schema is fetched from GitHub every time the test case is run, to ensure that tests still pass in case the schema
26 * changed in the meantime.
27 */
28class XmlSerializationTest extends TestCase
29{
30    const SCHEMA_URL = 'https://raw.githubusercontent.com/findologic/xml-export/master/src/main/resources/findologic.xsd';
31
32    private static $schema;
33
34    public static function setUpBeforeClass()
35    {
36        parent::setUpBeforeClass();
37
38        // Download the schema once for the whole test case for speed as compared to downloading it for each test.
39        self::$schema = file_get_contents(self::SCHEMA_URL);
40    }
41
42    public function tearDown()
43    {
44        try {
45            unlink('/tmp/findologic_0_1.xml');
46        } catch (\Exception $e) {
47            // No need to delete a written file if the test didn't write it.
48        }
49    }
50
51    /** @var XMLExporter */
52    private $exporter;
53
54    /**
55     * @SuppressWarnings(PHPMD.StaticAccess)
56     */
57    public function setUp()
58    {
59        $this->exporter = Exporter::create(Exporter::TYPE_XML);
60    }
61
62    private function getMinimalItem()
63    {
64        $item = $this->exporter->createItem('123');
65
66        $price = new Price();
67        $price->setValue('13.37');
68        $item->setPrice($price);
69
70        return $item;
71    }
72
73    private function assertPageIsValid($xmlString)
74    {
75        $xmlDocument = new \DOMDocument('1.0', 'utf-8');
76        $xmlDocument->loadXML($xmlString);
77
78        $this->assertTrue($xmlDocument->schemaValidateSource(self::$schema));
79    }
80
81    public function testEmptyPageIsValid()
82    {
83        $page = $this->exporter->serializeItems([], 0, 0, 0);
84
85        $this->assertPageIsValid($page);
86    }
87
88    public function testMinimalItemIsValid()
89    {
90        $item = $this->getMinimalItem();
91        $page = $this->exporter->serializeItems([$item], 0, 1, 1);
92
93        $this->assertPageIsValid($page);
94    }
95
96    /**
97     * @expectedException \FINDOLOGIC\Export\XML\ItemsExceedCountValueException
98     */
99    public function testMoreItemsSuppliedThanCountValueCausesException()
100    {
101        $items = [];
102
103        for ($i = 0; $i <= 2; $i++) {
104            $item = $this->exporter->createItem((string)$i);
105
106            $price = new Price();
107            //Generate a random price
108            $price->setValue(rand(1, 2000)*1.24);
109            $item->setPrice($price);
110
111            $items[] = $item;
112        }
113
114        $this->exporter->serializeItems($items, 0, 1, 1);
115    }
116
117    public function testPropertyKeysAndValuesAreCdataWrapped()
118    {
119        $item = $this->getMinimalItem();
120
121        $property = new Property('&quot;</>', [null => '&quot;</>']);
122        $item->addProperty($property);
123
124        $page = $this->exporter->serializeItems([$item], 0, 1, 1);
125
126        $this->assertPageIsValid($page);
127    }
128
129    public function testAttributesAreCdataWrapped()
130    {
131        $item = $this->getMinimalItem();
132
133        $attribute = new Attribute('&quot;</>', ['&quot;</>', 'regular']);
134        $item->addAttribute($attribute);
135
136        $page = $this->exporter->serializeItems([$item], 0, 1, 1);
137
138        $this->assertPageIsValid($page);
139    }
140
141    public function testImagesCanBeDefaultAndThumbnail()
142    {
143        $item = $this->getMinimalItem();
144
145        $item->setAllImages([
146            new Image('http://example.org/default.png'),
147            new Image('http://example.org/thumbnail.png', Image::TYPE_THUMBNAIL),
148            new Image('http://example.org/ug_default.png', Image::TYPE_DEFAULT, 'usergroup'),
149        ]);
150
151        $page = $this->exporter->serializeItems([$item], 0, 1, 1);
152
153        $this->assertPageIsValid($page);
154    }
155
156    public function testBaseImageCanBeExported()
157    {
158        $item = $this->getMinimalItem();
159
160        $imageUrl = 'http://example.org/thumbnail.png';
161        $item->setAllImages([
162            new Image($imageUrl),
163        ]);
164
165        $document = new \DOMDocument('1.0', 'utf-8');
166        $root = XMLHelper::createElement($document, 'findologic', ['version' => '1.0']);
167        $document->appendChild($root);
168
169        $xmlItems = XMLHelper::createElement($document, 'items', [
170            'start' => 0,
171            'count' => 1,
172            'total' => 1
173        ]);
174        $root->appendChild($xmlItems);
175
176        $itemDom = $item->getDomSubtree($document);
177
178        foreach ($itemDom->childNodes as $node) {
179            if ($node->nodeName === 'allImages') {
180                $this->assertEquals($imageUrl, $node->nodeValue);
181            }
182        }
183    }
184
185    /**
186     * @expectedException \FINDOLOGIC\Export\Data\BaseImageMissingException
187     */
188    public function testMissingBaseImageCausesException()
189    {
190        $item = $this->getMinimalItem();
191
192        $item->setAllImages([
193            new Image('http://example.org/thumbnail.png', Image::TYPE_THUMBNAIL),
194            new Image('http://example.org/ug_default.png', Image::TYPE_THUMBNAIL, 'usergroup'),
195        ]);
196
197        $this->exporter->serializeItems([$item], 0, 1, 1);
198    }
199
200    /**
201     * @expectedException \FINDOLOGIC\Export\Data\ImagesWithoutUsergroupMissingException
202     */
203    public function testImagesWithoutUsergroupMissingCausesException()
204    {
205        $item = $this->getMinimalItem();
206
207        $item->setAllImages([
208            new Image('http://example.org/ug_default.png', Image::TYPE_DEFAULT, 'usergroup'),
209        ]);
210
211        $this->exporter->serializeItems([$item], 0, 1, 1);
212    }
213
214    public function testOrdernumbersSupportUsergroups()
215    {
216        $item = $this->getMinimalItem();
217
218        $item->setAllOrdernumbers([
219            new Ordernumber('137-42-23.7'),
220            new Ordernumber('137-42-23.7-A', 'usergroup'),
221        ]);
222
223        $page = $this->exporter->serializeItems([$item], 0, 1, 1);
224
225        $this->assertPageIsValid($page);
226    }
227
228    public function testKeywordsSupportUsergroups()
229    {
230        $item = $this->getMinimalItem();
231
232        $item->setAllKeywords([
233            new Keyword('awesome &quot;</>]]>7'),
234            new Keyword('restricted', 'usergroup'),
235        ]);
236
237        $page = $this->exporter->serializeItems([$item], 0, 1, 1);
238
239        $this->assertPageIsValid($page);
240    }
241
242    public function testUsergroupVisibilitiesAreExported()
243    {
244        $item = $this->getMinimalItem();
245
246        $item->setAllUsergroups([
247            new Usergroup('one group'),
248            new Usergroup('another group')
249        ]);
250
251        $page = $this->exporter->serializeItems([$item], 0, 1, 1);
252
253        $this->assertPageIsValid($page);
254    }
255
256    public function testXmlCanBeWrittenDirectlyToFile()
257    {
258        $item = $this->getMinimalItem();
259
260        $expectedXml = $this->exporter->serializeItems([$item], 0, 1, 1);
261        $this->exporter->serializeItemsToFile('/tmp', [$item], 0, 1, 1);
262
263        self::assertEquals($expectedXml, file_get_contents('/tmp/findologic_0_1.xml'));
264    }
265
266    public function testAttemptingToGetCsvFromAnXmlItemResultsInAnException()
267    {
268        $item = new XMLItem(123);
269
270        try {
271            $item->getCsvFragment();
272        } catch (\BadMethodCallException $e) {
273            $this->assertEquals('XMLItem does not implement CSV export.', $e->getMessage());
274        }
275    }
276
277    /**
278     * @return array Name of add method to call in a test to add a certain value, and an array of values with
279     *      usergroup names as key.
280     */
281    public function simpleValueAddingShortcutProvider()
282    {
283        $stringValuesWithUsergroupKeys = [
284            '' => 'No usergroup',
285            'foo' => 'One usergroup',
286            'bar' => 'Another usergroup'
287        ];
288
289        $integerValuesWithUsergroupKeys = [
290            '' => 0,
291            'foo' => 3,
292            'bar' => 10
293        ];
294
295        return [
296            'name' => ['Name', $stringValuesWithUsergroupKeys],
297            'summary' => ['Summary', $stringValuesWithUsergroupKeys],
298            'description' => ['Description', $stringValuesWithUsergroupKeys],
299            'price' => ['Price', [
300                '' => 13.37,
301                'foo' => 42,
302                'bar' => 12.00
303            ]],
304            'url' => ['Url', [
305                '' => 'https://example.org/product.html',
306                'foo' => 'https://example.org/product.html?group=foo',
307                'bar' => 'https://example.org/product.html?group=bar'
308            ]],
309            'bonus' => ['Bonus', $integerValuesWithUsergroupKeys],
310            'sales frequency' => ['SalesFrequency', $integerValuesWithUsergroupKeys],
311            'sort' => ['Sort', $integerValuesWithUsergroupKeys],
312        ];
313    }
314
315    /**
316     * @dataProvider simpleValueAddingShortcutProvider
317     *
318     * @param string $valueType
319     * @param array $values
320     */
321    public function testSimpleValuesAddedToItemViaShortcutAccumulate($valueType, array $values)
322    {
323        $item = new XMLItem(123);
324
325        foreach ($values as $usergroup => $value) {
326            $item->{'add' . $valueType}($value, $usergroup);
327        }
328
329        $this->assertEquals($values, $item->{'get' . $valueType}()->getValues());
330    }
331
332    public function testDateValuesAddedToItemViaShortcutAccumulate()
333    {
334        $values = [
335            '' => new DateTime('today midnight'),
336            'foo' => new DateTime('yesterday midnight'),
337            'bar' => new DateTime('tomorrow midnight')
338        ];
339
340        // On assignment, dates are converted to strings according to the format set in the schema.
341        $expectedValues = array_map(function (DateTime $date) {
342            return $date->format(DATE_ATOM);
343        }, $values);
344
345        $item = new XMLItem(123);
346
347        foreach ($values as $usergroup => $value) {
348            $item->addDateAdded($value, $usergroup);
349        }
350
351        $this->assertEquals($expectedValues, $item->getDateAdded()->getValues());
352    }
353}
354