theMultilanguageSitemapShouldPassGoogleValidation()   A
last analyzed

Complexity

Conditions 4
Paths 4

Size

Total Lines 33
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 19
nc 4
nop 0
dl 0
loc 33
rs 9.6333
c 0
b 0
f 0
1
<?php declare(strict_types=1);
2
3
namespace MOrtola\BehatSEOContexts\Context;
4
5
use Behat\Mink\Exception\DriverException;
6
use DOMDocument;
7
use DOMElement;
8
use DOMNode;
9
use DOMNodeList;
10
use DOMXPath;
11
use InvalidArgumentException;
12
use MOrtola\BehatSEOContexts\Exception\InvalidOrderException;
13
use Symfony\Component\Routing\Exception\RouteNotFoundException;
14
use Webmozart\Assert\Assert;
15
16
class SitemapContext extends BaseContext
17
{
18
    const SITEMAP_SCHEMA_FILE = __DIR__ . '/../Resources/schemas/sitemap.xsd';
19
    const SITEMAP_XHTML_SCHEMA_FILE = __DIR__ . '/../Resources/schemas/sitemap_xhtml.xsd';
20
    const SITEMAP_INDEX_SCHEMA_FILE = __DIR__ . '/../Resources/schemas/sitemap_index.xsd';
21
22
    /**
23
     * @var DOMDocument
24
     */
25
    private $sitemapXml;
26
27
    /**
28
     * @Given the sitemap :sitemapUrl
29
     */
30
    public function theSitemap(string $sitemapUrl): void
31
    {
32
        $this->sitemapXml = $this->getSitemapXml($sitemapUrl);
33
    }
34
35
    private function getSitemapXml(string $sitemapUrl): DOMDocument
36
    {
37
        $xml = new DOMDocument();
38
        @$xmlLoaded = $xml->load($this->toAbsoluteUrl($sitemapUrl));
39
40
        Assert::true($xmlLoaded, 'Error loading %s Sitemap using DOMDocument');
41
42
        return $xml;
43
    }
44
45
    /**
46
     * @throws InvalidOrderException
47
     *
48
     * @Then the index sitemap should have a child with URL :childSitemapUrl
49
     */
50
    public function theIndexSitemapShouldHaveAChildWithUrl(string $childSitemapUrl): void
51
    {
52
        $this->assertSitemapHasBeenRead();
53
54
        $xpathExpression = sprintf(
55
            '//sm:sitemapindex/sm:sitemap/sm:loc[contains(text(),"%s")]',
56
            $childSitemapUrl
57
        );
58
59
        Assert::greaterThanEq(
60
            $this->getXpathInspector()->query($xpathExpression)->length,
61
            1,
62
            sprintf(
63
                'Sitemap index %s has not child sitemap %s',
64
                $this->sitemapXml->documentURI,
65
                $childSitemapUrl
66
            )
67
        );
68
    }
69
70
    /**
71
     * @throws InvalidOrderException
72
     */
73
    private function assertSitemapHasBeenRead(): void
74
    {
75
        if (!isset($this->sitemapXml)) {
76
            throw new InvalidOrderException(
77
                'You should execute "Given the sitemap :sitemapUrl" step before executing this step.'
78
            );
79
        }
80
    }
81
82
    private function getXpathInspector(): DOMXPath
83
    {
84
        $xpath = new DOMXPath($this->sitemapXml);
85
        $xpath->registerNamespace('sm', 'http://www.sitemaps.org/schemas/sitemap/0.9');
86
        $xpath->registerNamespace('xhtml', 'http://www.w3.org/1999/xhtml');
87
88
        return $xpath;
89
    }
90
91
    /**
92
     * @throws InvalidOrderException
93
     *
94
     * @Then /^the sitemap should have ([0-9]+) children$/
95
     */
96
    public function theSitemapShouldHaveChildren(int $expectedChildrenCount): void
97
    {
98
        $this->assertSitemapHasBeenRead();
99
100
        $sitemapChildrenCount = $this
101
            ->getXpathInspector()
102
            ->query('/*[self::sm:sitemapindex or self::sm:urlset]/*[self::sm:sitemap or self::sm:url]/sm:loc')
103
            ->length;
104
105
        Assert::eq(
106
            $expectedChildrenCount,
107
            $sitemapChildrenCount,
108
            sprintf(
109
                'Sitemap %s has %d children, expected value was: %d',
110
                $this->sitemapXml->documentURI,
111
                $sitemapChildrenCount,
112
                $expectedChildrenCount
113
            )
114
        );
115
    }
116
117
    /**
118
     * @throws InvalidOrderException
119
     *
120
     * @Then the multilanguage sitemap should pass Google validation
121
     */
122
    public function theMultilanguageSitemapShouldPassGoogleValidation(): void
123
    {
124
        $this->assertSitemapHasBeenRead();
125
126
        $this->assertValidSitemap(self::SITEMAP_XHTML_SCHEMA_FILE);
127
128
        $urlsNodes = $this->getXpathInspector()->query('//sm:urlset/sm:url');
129
130
        /** @var DOMElement $urlNode */
131
        foreach ($urlsNodes as $urlNode) {
132
            $urlElement = $urlNode->getElementsByTagName('loc')->item(0);
133
134
            Assert::notNull($urlElement);
135
136
            $urlLoc = $urlElement->nodeValue;
137
138
            /** @var DOMElement $alternateLink */
139
            foreach ($urlNode->getElementsByTagName('link') as $alternateLink) {
140
                $alternateLinkHref = $alternateLink->getAttribute('href');
141
142
                if ($alternateLinkHref !== $urlLoc) {
143
                    $alternateLinkNodes = $this->getXpathInspector()->query(
144
                        sprintf('//sm:urlset/sm:url/sm:loc[text()="%s"]', $alternateLinkHref)
145
                    );
146
147
                    Assert::greaterThanEq(
148
                        $alternateLinkNodes->length,
149
                        1,
150
                        sprintf(
151
                            'Url %s has not reciprocous URL for alternative link %s in Sitemap %s',
152
                            $urlLoc,
153
                            $alternateLinkHref,
154
                            $this->sitemapXml->documentURI
155
                        )
156
                    );
157
                }
158
            }
159
        }
160
    }
161
162
    private function assertValidSitemap(string $sitemapSchemaFile): void
163
    {
164
        Assert::fileExists(
165
            $sitemapSchemaFile,
166
            sprintf('Sitemap schema file %s does not exist', $sitemapSchemaFile)
167
        );
168
169
        Assert::true(
170
            @$this->sitemapXml->schemaValidate($sitemapSchemaFile),
171
            sprintf(
172
                'Sitemap %s does not pass validation using %s schema',
173
                $this->sitemapXml->documentURI,
174
                $sitemapSchemaFile
175
            )
176
        );
177
    }
178
179
    /**
180
     * @throws InvalidOrderException
181
     * @throws DriverException
182
     *
183
     * @Then the sitemap URLs should be alive
184
     */
185
    public function theSitemapUrlsShouldBeAlive(): void
186
    {
187
        $this->assertSitemapHasBeenRead();
188
189
        $locNodes = $this->getXpathInspector()->query('//sm:urlset/sm:url/sm:loc');
190
191
        Assert::isInstanceOf($locNodes, DOMNodeList::class);
192
193
        foreach ($locNodes as $locNode) {
194
            $this->urlIsValid($locNode);
195
            $this->urlIsAlive($locNode);
196
        }
197
    }
198
199
    /**
200
     * @throws DriverException
201
     */
202
    private function urlIsValid(DOMNode $locNode): void
203
    {
204
        try {
205
            $this->visit($locNode->nodeValue);
206
        } catch (RouteNotFoundException $e) {
207
            throw new InvalidArgumentException(
208
                sprintf(
209
                    'Sitemap Url %s is not valid in Sitemap: %s. Exception: %s',
210
                    $locNode->nodeValue,
211
                    $this->sitemapXml->documentURI,
212
                    $e->getMessage()
213
                ),
214
                0,
215
                $e
216
            );
217
        }
218
    }
219
220
    private function urlIsAlive(DOMNode $locNode): void
221
    {
222
        Assert::eq(
223
            200,
224
            $this->getStatusCode(),
225
            sprintf(
226
                'Sitemap Url %s is not valid in Sitemap: %s. Response status code: %s',
227
                $locNode->nodeValue,
228
                $this->sitemapXml->documentURI,
229
                $this->getStatusCode()
230
            )
231
        );
232
    }
233
234
    /**
235
     * @throws DriverException
236
     * @throws InvalidOrderException
237
     *
238
     * @Then /^(\d+) random sitemap URLs? should be alive$/
239
     */
240
    public function randomSitemapUrlsShouldBeAlive(int $randomUrlsCount): void
241
    {
242
        $this->assertSitemapHasBeenRead();
243
244
        $locNodes      = $this->getXpathInspector()->query('//sm:urlset/sm:url/sm:loc');
245
        $locNodesArray = iterator_to_array($locNodes ?? []);
246
247
        Assert::isInstanceOf($locNodes, DOMNodeList::class);
248
249
        $locNodesCount = count($locNodesArray);
250
251
        Assert::greaterThan(
252
            $locNodesCount,
253
            $randomUrlsCount,
254
            sprintf(
255
                'Sitemap %s only has %d children, minimum expected value was: %d',
256
                $this->sitemapXml->documentURI,
257
                $locNodesCount,
258
                $randomUrlsCount
259
            )
260
        );
261
262
        shuffle($locNodesArray);
263
264
        for ($i = 0; $i <= $randomUrlsCount - 1; $i++) {
265
            $this->urlIsValid($locNodesArray[$i]);
266
            $this->urlIsAlive($locNodesArray[$i]);
267
        }
268
    }
269
270
    /**
271
     * @Then /^the (index |multilanguage |)sitemap should not be valid$/
272
     */
273
    public function theSitemapShouldNotBeValid(string $sitemapType = ''): void
274
    {
275
        $this->assertInverse(
276
            function () use ($sitemapType) {
277
                $this->theSitemapShouldBeValid($sitemapType);
278
            },
279
            sprintf('The sitemap is a valid %s sitemap.', $sitemapType)
280
        );
281
    }
282
283
    /**
284
     * @throws InvalidOrderException
285
     *
286
     * @Then /^the (index |multilanguage |)sitemap should be valid$/
287
     */
288
    public function theSitemapShouldBeValid(string $sitemapType = ''): void
289
    {
290
        $this->assertSitemapHasBeenRead();
291
292
        switch (trim($sitemapType)) {
293
            case 'index':
294
                $sitemapSchemaFile = self::SITEMAP_INDEX_SCHEMA_FILE;
295
296
                break;
297
            case 'multilanguage':
298
                $sitemapSchemaFile = self::SITEMAP_XHTML_SCHEMA_FILE;
299
300
                break;
301
            default:
302
                $sitemapSchemaFile = self::SITEMAP_SCHEMA_FILE;
303
        }
304
305
        $this->assertValidSitemap($sitemapSchemaFile);
306
    }
307
308
    /**
309
     * @Then the multilanguage sitemap should not pass Google validation
310
     */
311
    public function theMultilanguageSitemapShouldNotPassGoogleValidation(): void
312
    {
313
        $this->assertInverse(
314
            [$this, 'theMultilanguageSitemapShouldPassGoogleValidation'],
315
            sprintf('The multilanguage sitemap passes Google validation.')
316
        );
317
    }
318
}
319