Passed
Pull Request — main (#504)
by Michiel
04:20 queued 02:05
created

MinkContext::theAdfsResponseShouldMatchXpath()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 17
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 13
nc 2
nop 1
dl 0
loc 17
rs 9.8333
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * Copyright 2020 SURFnet B.V.
5
 *
6
 * Licensed under the Apache License, Version 2.0 (the "License");
7
 * you may not use this file except in compliance with the License.
8
 * You may obtain a copy of the License at
9
 *
10
 *     http://www.apache.org/licenses/LICENSE-2.0
11
 *
12
 * Unless required by applicable law or agreed to in writing, software
13
 * distributed under the License is distributed on an "AS IS" BASIS,
14
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
 * See the License for the specific language governing permissions and
16
 * limitations under the License.
17
 */
18
19
namespace Surfnet\StepupGateway\Behat;
20
21
use Behat\Mink\Exception\ExpectationException;
22
use Behat\MinkExtension\Context\MinkContext as BaseMinkContext;
23
use DMore\ChromeDriver\ChromeDriver;
24
use DOMDocument;
25
use DOMXPath;
26
use RobRichards\XMLSecLibs\XMLSecurityDSig;
27
use RuntimeException;
28
use SAML2\XML\mdui\Common;
29
use SAML2\XML\shibmd\Scope;
30
31
/**
32
 * Mink-enabled context.
33
 */
34
class MinkContext extends BaseMinkContext
35
{
36
    /**
37
     * @var array a list of window names identified by the name the tester refers to them in the step definitions.
38
     * @example ['My tab' => 'WindowNameGivenByBrowser', 'My other tab' => 'WindowNameGivenByBrowser']
39
     */
40
    private $windows = [];
41
42
    #[\Behat\Step\Then('/^the response should contain \\\'([^\\\']*)\\\'$/')]
43
    public function theResponseShouldContain($string): void
44
    {
45
        $this->assertSession()->responseContains($string);
46
    }
47
48
    #[\Behat\Step\Then('/^the response should match xpath \\\'([^\\\']*)\\\'$/')]
49
    public function theResponseShouldMatchXpath($xpath): void
50
    {
51
        $document = new DOMDocument();
52
        if ($this->getSession()->getDriver() instanceof ChromeDriver) {
53
            // Chrome uses a user friendly viewer, get the xml from the dressed document and assert on that xml.
54
            $this->getSession()->wait(1000, "document.getElementById('webkit-xml-viewer-source-xml') !== null");
55
            $xml = $this->getSession()->evaluateScript("document.getElementById('webkit-xml-viewer-source-xml').innerHTML");
56
        } else {
57
            $xml = $this->getSession()->getPage()->getContent();
58
        }
59
        $document->loadXML($xml);
60
61
        $xpathObj = new DOMXPath($document);
62
        $xpathObj->registerNamespace('ds', XMLSecurityDSig::XMLDSIGNS);
63
        $xpathObj->registerNamespace('mdui', Common::NS);
64
        $xpathObj->registerNamespace('mdash', Common::NS);
65
        $xpathObj->registerNamespace('shibmd', Scope::NS);
66
        $nodeList = $xpathObj->query($xpath);
67
68
        if (!$nodeList || $nodeList->length === 0) {
0 ignored issues
show
introduced by
$nodeList is of type DOMNodeList, thus it always evaluated to true.
Loading history...
69
            $message = sprintf('The xpath "%s" did not result in at least one match.', $xpath);
70
            throw new ExpectationException($message, $this->getSession());
71
        }
72
    }
73
74
    #[\Behat\Step\Then('/^the ADFS response should match xpath \\\'([^\\\']*)\\\'$/')]
75
    public function theAdfsResponseShouldMatchXpath($xpath): void
76
    {
77
        $document = new DOMDocument();
78
        $xml = $this->getSession()->getPage()->findById('saml-response-xml')->getText();
79
        $document->loadXML($xml);
80
81
        $xpathObj = new DOMXPath($document);
82
        $xpathObj->registerNamespace('ds', XMLSecurityDSig::XMLDSIGNS);
83
        $xpathObj->registerNamespace('mdui', Common::NS);
84
        $xpathObj->registerNamespace('mdash', Common::NS);
85
        $xpathObj->registerNamespace('shibmd', Scope::NS);
86
        $nodeList = $xpathObj->query($xpath);
87
88
        if (!$nodeList || $nodeList->length === 0) {
0 ignored issues
show
introduced by
$nodeList is of type DOMNodeList, thus it always evaluated to true.
Loading history...
89
            $message = sprintf('The xpath "%s" did not result in at least one match.', $xpath);
90
            throw new ExpectationException($message, $this->getSession());
91
        }
92
    }
93
94
    #[\Behat\Step\Then('/^the ADFS response should carry the ADFS POST parameters$/')]
95
    public function theAdfsResponseShouldHaveAdfsPostParams(): void
96
    {
97
        $context = $this->getSession()->getPage()->findById('Context')->getText();
98
        $authMethod = $this->getSession()->getPage()->findById('AuthMethod')->getText();
99
        if ($context !== '<EncryptedData></EncryptedData>') {
100
            throw new ExpectationException('The Adfs Context POST parameter was not found or contained an invalid value', $this->getSession());
101
        }
102
        if ($authMethod !== 'ADFS.SCSA') {
103
            throw new ExpectationException('The Adfs AuthMethod POST parameter was not found or contained an invalid value', $this->getSession());
104
        }
105
    }
106
107
    #[\Behat\Step\Then('/^the response should not match xpath \\\'([^\\\']*)\\\'$/')]
108
    public function theResponseShouldNotMatchXpath($xpath): void
109
    {
110
        $document = new DOMDocument();
111
        $document->loadXML($this->getSession()->getPage()->getContent());
112
113
        $xpathObj = new DOMXPath($document);
114
        $xpathObj->registerNamespace('ds', XMLSecurityDSig::XMLDSIGNS);
115
        $xpathObj->registerNamespace('mdui', Common::NS);
116
        $nodeList = $xpathObj->query($xpath);
117
118
        if ($nodeList && $nodeList->length > 0) {
119
            $message = sprintf(
120
                'The xpath "%s" resulted in "%d" matches, where it should result in no matches"',
121
                $xpath,
122
                $nodeList->length
123
            );
124
            throw new ExpectationException($message, $this->getSession());
125
        }
126
    }
127
128
    #[\Behat\Step\Given('/^I should see URL "([^"]*)"$/')]
129
    public function iShouldSeeUrl($url): void
130
    {
131
        $this->assertSession()->responseContains($url);
132
    }
133
134
    #[\Behat\Step\Given('/^I should not see URL "([^"]*)"$/')]
135
    public function iShouldNotSeeUrl($url): void
136
    {
137
        $this->assertSession()->responseNotContains($url);
138
    }
139
140
    #[\Behat\Step\Given('/^I open (\d+) browser tabs identified by "([^"]*)"$/')]
141
    public function iOpenTwoBrowserTabsIdentifiedBy($numberOfTabs, $tabNames): void
142
    {
143
        // On successive scenarios, reset the session to get rid of browser (session) state from previous scenarios
144
        if ($this->getMink()->getSession()->isStarted()) {
145
            $this->getMink()->getSession()->restart();
146
        }
147
        // Make sure the browser is ready (without this other browser interactions fail)
148
        $this->getSession()->visit($this->locatePath('#'));
149
150
        $tabs = explode(',', $tabNames);
151
        if (count($tabs) != $numberOfTabs) {
152
            throw new RuntimeException(
153
                'Please identify all tabs you are opening in order to refer to them at a later stage'
154
            );
155
        }
156
157
        foreach ($tabs as $tab) {
158
            $tab = trim($tab);
159
            $windowsBeforeOpen = $this->getSession()->getWindowNames();
160
161
            $this->getMink()->getSession()->executeScript("window.open('/','_blank');");
162
163
            $newWindowName = $this->waitForNewlyOpenedWindow($windowsBeforeOpen);
164
165
            if ($newWindowName === null) {
166
                throw new RuntimeException(
167
                    sprintf('Failed to detect newly opened browser tab for "%s"', $tab)
168
                );
169
            }
170
171
            $this->windows[$tab] = $newWindowName;
172
        }
173
    }
174
175
    private function waitForNewlyOpenedWindow(array $windowsBeforeOpen): ?string
176
    {
177
        $maxAttempts = 10;
178
        $sleepMicroseconds = 100000;
179
180
        for ($attempt = 0; $attempt < $maxAttempts; $attempt++) {
181
            usleep($sleepMicroseconds);
182
183
            $currentWindows = $this->getSession()->getWindowNames();
184
            $newWindowName = $this->findNewlyOpenedWindow($windowsBeforeOpen, $currentWindows);
185
186
            if ($newWindowName !== null) {
187
                return $newWindowName;
188
            }
189
        }
190
191
        return null;
192
    }
193
194
    private function findNewlyOpenedWindow(array $before, array $after): ?string
195
    {
196
        $newWindows = array_values(array_diff($after, $before));
197
198
        if (count($newWindows) === 1) {
199
            return $newWindows[0];
200
        }
201
202
        return null;
203
    }
204
205
    #[\Behat\Step\Given('/^I switch to "([^"]*)"$/')]
206
    public function iSwitchToWindow($windowName): void
207
    {
208
        // (re) set the default session to the chrome session.
209
        $this->switchToWindow($windowName);
210
    }
211
212
    public function switchToWindow($windowName): void
213
    {
214
        if (!isset($this->windows[$windowName])) {
215
            throw new RuntimeException(sprintf('Unknown window/tab name "%s"', $windowName));
216
        }
217
        $this->getSession()->switchToWindow($this->windows[$windowName]);
218
    }
219
}
220