Passed
Push — master ( df9ead...528f96 )
by Marc
02:31
created

SitemapContext::randomSitemapUrlsShouldBeAlive()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 26
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 17
c 1
b 0
f 0
nc 2
nop 1
dl 0
loc 26
rs 9.7
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 DOMNodeList;
9
use DOMXPath;
10
use InvalidArgumentException;
11
use MOrtola\BehatSEOContexts\Exception\InvalidOrderException;
12
use Symfony\Component\Routing\Exception\RouteNotFoundException;
13
use Webmozart\Assert\Assert;
14
15
class SitemapContext extends BaseContext
16
{
17
    const SITEMAP_SCHEMA_FILE = __DIR__ . '/../Resources/schemas/sitemap.xsd';
18
    const SITEMAP_XHTML_SCHEMA_FILE = __DIR__ . '/../Resources/schemas/sitemap_xhtml.xsd';
19
    const SITEMAP_INDEX_SCHEMA_FILE = __DIR__ . '/../Resources/schemas/sitemap_index.xsd';
20
21
    /**
22
     * @var DOMDocument
23
     */
24
    private $sitemapXml;
25
26
    /**
27
     * @Given the sitemap :sitemapUrl
28
     */
29
    public function theSitemap(string $sitemapUrl): void
30
    {
31
        $this->sitemapXml = $this->getSitemapXml($sitemapUrl);
32
    }
33
34
    private function getSitemapXml(string $sitemapUrl): DOMDocument
35
    {
36
        $xml = new DOMDocument();
37
        @$xmlLoaded = $xml->load($this->toAbsoluteUrl($sitemapUrl));
38
39
        Assert::true($xmlLoaded, 'Error loading %s Sitemap using DOMDocument');
40
41
        return $xml;
42
    }
43
44
    /**
45
     * @throws InvalidOrderException
46
     *
47
     * @Then the index sitemap should have a child with URL :childSitemapUrl
48
     */
49
    public function theIndexSitemapShouldHaveAChildWithUrl(string $childSitemapUrl): void
50
    {
51
        $this->assertSitemapHasBeenRead();
52
53
        $xpathExpression = sprintf(
54
            '//sm:sitemapindex/sm:sitemap/sm:loc[contains(text(),"%s")]',
55
            $childSitemapUrl
56
        );
57
58
        Assert::greaterThanEq(
59
            $this->getXpathInspector()->query($xpathExpression)->length,
60
            1,
61
            sprintf(
62
                'Sitemap index %s has not child sitemap %s',
63
                $this->sitemapXml->documentURI,
64
                $childSitemapUrl
65
            )
66
        );
67
    }
68
69
    /**
70
     * @throws InvalidOrderException
71
     */
72
    private function assertSitemapHasBeenRead(): void
73
    {
74
        if (!isset($this->sitemapXml)) {
75
            throw new InvalidOrderException(
76
                'You should execute "Given the sitemap :sitemapUrl" step before executing this step.'
77
            );
78
        }
79
    }
80
81
    private function getXpathInspector(): DOMXPath
82
    {
83
        $xpath = new DOMXPath($this->sitemapXml);
84
        $xpath->registerNamespace('sm', 'http://www.sitemaps.org/schemas/sitemap/0.9');
85
        $xpath->registerNamespace('xhtml', 'http://www.w3.org/1999/xhtml');
86
87
        return $xpath;
88
    }
89
90
    /**
91
     * @throws InvalidOrderException
92
     *
93
     * @Then /^the sitemap should have ([0-9]+) children$/
94
     */
95
    public function theSitemapShouldHaveChildren(int $expectedChildrenCount): void
96
    {
97
        $this->assertSitemapHasBeenRead();
98
99
        $sitemapChildrenCount = $this
100
            ->getXpathInspector()
101
            ->query('/*[self::sm:sitemapindex or self::sm:urlset]/*[self::sm:sitemap or self::sm:url]/sm:loc')
102
            ->length;
103
104
        Assert::eq(
105
            $expectedChildrenCount,
106
            $sitemapChildrenCount,
107
            sprintf(
108
                'Sitemap %s has %d children, expected value was: %d',
109
                $this->sitemapXml->documentURI,
110
                $sitemapChildrenCount,
111
                $expectedChildrenCount
112
            )
113
        );
114
    }
115
116
    /**
117
     * @throws InvalidOrderException
118
     *
119
     * @Then the multilanguage sitemap should pass Google validation
120
     */
121
    public function theMultilanguageSitemapShouldPassGoogleValidation(): void
122
    {
123
        $this->assertSitemapHasBeenRead();
124
125
        $this->assertValidSitemap(self::SITEMAP_XHTML_SCHEMA_FILE);
126
127
        $urlsNodes = $this->getXpathInspector()->query('//sm:urlset/sm:url');
128
129
        /** @var DOMElement $urlNode */
130
        foreach ($urlsNodes as $urlNode) {
131
            $urlElement = $urlNode->getElementsByTagName('loc')->item(0);
132
133
            Assert::notNull($urlElement);
134
135
            $urlLoc = $urlElement->nodeValue;
136
137
            /** @var DOMElement $alternateLink */
138
            foreach ($urlNode->getElementsByTagName('link') as $alternateLink) {
139
                $alternateLinkHref = $alternateLink->getAttribute('href');
140
141
                if ($alternateLinkHref !== $urlLoc) {
142
                    $alternateLinkNodes = $this->getXpathInspector()->query(
143
                        sprintf('//sm:urlset/sm:url/sm:loc[text()="%s"]', $alternateLinkHref)
144
                    );
145
146
                    Assert::greaterThanEq(
147
                        $alternateLinkNodes->length,
148
                        1,
149
                        sprintf(
150
                            'Url %s has not reciprocous URL for alternative link %s in Sitemap %s',
151
                            $urlLoc,
152
                            $alternateLinkHref,
153
                            $this->sitemapXml->documentURI
154
                        )
155
                    );
156
                }
157
            }
158
        }
159
    }
160
161
    private function assertValidSitemap(string $sitemapSchemaFile): void
162
    {
163
        Assert::fileExists(
164
            $sitemapSchemaFile,
165
            sprintf('Sitemap schema file %s does not exist', $sitemapSchemaFile)
166
        );
167
168
        Assert::true(
169
            @$this->sitemapXml->schemaValidate($sitemapSchemaFile),
170
            sprintf(
171
                'Sitemap %s does not pass validation using %s schema',
172
                $this->sitemapXml->documentURI,
173
                $sitemapSchemaFile
174
            )
175
        );
176
    }
177
178
    /**
179
     * @throws InvalidOrderException
180
     * @throws DriverException
181
     *
182
     * @Then the sitemap URLs should be alive
183
     */
184
    public function theSitemapUrlsShouldBeAlive(): void
185
    {
186
        $this->assertSitemapHasBeenRead();
187
188
        $locNodes = $this->getXpathInspector()->query('//sm:urlset/sm:url/sm:loc');
189
190
        Assert::isInstanceOf($locNodes, DOMNodeList::class);
191
192
        foreach ($locNodes as $locNode) {
193
            $this->urlIsValid($locNode);
194
            $this->urlIsAlive($locNode);
195
        }
196
    }
197
198
    /**
199
     * @throws DriverException
200
     */
201
    private function urlIsValid($locNode): void
202
    {
203
        try {
204
            $this->visit($locNode->nodeValue);
205
        } catch (RouteNotFoundException $e) {
206
            throw new InvalidArgumentException(
207
                sprintf(
208
                    'Sitemap Url %s is not valid in Sitemap: %s. Exception: %s',
209
                    $locNode->nodeValue,
210
                    $this->sitemapXml->documentURI,
211
                    $e->getMessage()
212
                ),
213
                0,
214
                $e
215
            );
216
        }
217
    }
218
219
    private function urlIsAlive($locNode): void
220
    {
221
        Assert::eq(
222
            200,
223
            $this->getStatusCode(),
224
            sprintf(
225
                'Sitemap Url %s is not valid in Sitemap: %s. Response status code: %s',
226
                $locNode->nodeValue,
227
                $this->sitemapXml->documentURI,
228
                $this->getStatusCode()
229
            )
230
        );
231
    }
232
233
    /**
234
     * @throws DriverException
235
     * @throws InvalidOrderException
236
     *
237
     * @Then :randomUrlsCount random sitemap URL/URLs should be alive
238
     */
239
    public function randomSitemapUrlsShouldBeAlive(int $randomUrlsCount): void
240
    {
241
        $this->assertSitemapHasBeenRead();
242
243
        $locNodes = $this->getXpathInspector()->query('//sm:urlset/sm:url/sm:loc');
244
245
        Assert::isInstanceOf($locNodes, DOMNodeList::class);
246
247
        $locNodesCount = $locNodes->length;
248
249
        Assert::greaterThan(
250
            $locNodesCount,
251
            $randomUrlsCount,
252
            sprintf(
253
                'Sitemap %s only has %d children, minimum expected value was: %d',
254
                $this->sitemapXml->documentURI,
255
                $locNodesCount,
256
                $randomUrlsCount
257
            )
258
        );
259
260
        $locNodesArray = iterator_to_array($locNodes);
0 ignored issues
show
Bug introduced by
It seems like $locNodes can also be of type false; however, parameter $iterator of iterator_to_array() does only seem to accept Traversable, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

260
        $locNodesArray = iterator_to_array(/** @scrutinizer ignore-type */ $locNodes);
Loading history...
261
        shuffle($locNodesArray);
262
        for ($i = 0; $i <= $randomUrlsCount - 1; $i++) {
263
            $this->urlIsValid($locNodesArray[$i]);
264
            $this->urlIsAlive($locNodesArray[$i]);
265
        }
266
    }
267
268
    /**
269
     * @Then /^the (index |multilanguage |)sitemap should not be valid$/
270
     */
271
    public function theSitemapShouldNotBeValid(string $sitemapType = ''): void
272
    {
273
        $this->assertInverse(
274
            function () use ($sitemapType) {
275
                $this->theSitemapShouldBeValid($sitemapType);
276
            },
277
            sprintf('The sitemap is a valid %s sitemap.', $sitemapType)
278
        );
279
    }
280
281
    /**
282
     * @throws InvalidOrderException
283
     *
284
     * @Then /^the (index |multilanguage |)sitemap should be valid$/
285
     */
286
    public function theSitemapShouldBeValid(string $sitemapType = ''): void
287
    {
288
        $this->assertSitemapHasBeenRead();
289
290
        switch (trim($sitemapType)) {
291
            case 'index':
292
                $sitemapSchemaFile = self::SITEMAP_INDEX_SCHEMA_FILE;
293
294
                break;
295
            case 'multilanguage':
296
                $sitemapSchemaFile = self::SITEMAP_XHTML_SCHEMA_FILE;
297
298
                break;
299
            default:
300
                $sitemapSchemaFile = self::SITEMAP_SCHEMA_FILE;
301
        }
302
303
        $this->assertValidSitemap($sitemapSchemaFile);
304
    }
305
306
    /**
307
     * @Then the multilanguage sitemap should not pass Google validation
308
     */
309
    public function theMultilanguageSitemapShouldNotPassGoogleValidation(): void
310
    {
311
        $this->assertInverse(
312
            [$this, 'theMultilanguageSitemapShouldPassGoogleValidation'],
313
            sprintf('The multilanguage sitemap passes Google validation.')
314
        );
315
    }
316
}
317