1<?php declare(strict_types=1);
2/* Copyright (c) 1998-2016 ILIAS open source, Extended GPL, see docs/LICENSE */
3
4require_once 'Services/Password/classes/encoders/class.ilBcryptPhpPasswordEncoder.php';
5require_once 'Services/Password/test/ilPasswordBaseTest.php';
6
7/**
8 * Class ilBcryptPhpPasswordEncoderTest
9 * @author  Michael Jansen <mjansen@databay.de>
10 * @package ServicesPassword
11 */
12class ilBcryptPhpPasswordEncoderTest extends ilPasswordBaseTest
13{
14    /** @var string */
15    const VALID_COSTS = '08';
16
17    /** @var string */
18    const PASSWORD = 'password';
19
20    /** @var string */
21    const WRONG_PASSWORD = 'wrong_password';
22
23    /**
24     *
25     */
26    private function skipIfPhpVersionIsNotSupported() : void
27    {
28        if (version_compare(phpversion(), '5.5.0', '<')) {
29            $this->markTestSkipped('Requires PHP >= 5.5.0');
30        }
31    }
32
33    /**
34     * @return array
35     */
36    public function costsProvider() : array
37    {
38        $data = [];
39        for ($i = 4; $i <= 31; $i++) {
40            $data[sprintf("Costs: %s", (string) $i)] = [(string) $i];
41        }
42
43        return $data;
44    }
45
46    /**
47     * @return ilBcryptPhpPasswordEncoder
48     * @throws ilPasswordException
49     */
50    public function testInstanceCanBeCreated() : ilBcryptPhpPasswordEncoder
51    {
52        $this->skipIfPhpVersionIsNotSupported();
53
54        $default_costs_encoder = new ilBcryptPhpPasswordEncoder();
55        $this->assertTrue((int) $default_costs_encoder->getCosts() > 4 && (int) $default_costs_encoder->getCosts() < 32);
56
57        $encoder = new ilBcryptPhpPasswordEncoder([
58            'cost' => self::VALID_COSTS
59        ]);
60        $this->assertInstanceOf('ilBcryptPhpPasswordEncoder', $encoder);
61        $this->assertEquals(self::VALID_COSTS, $encoder->getCosts());
62        return $encoder;
63    }
64
65    /**
66     * @depends testInstanceCanBeCreated
67     * @param ilBcryptPhpPasswordEncoder $encoder
68     * @throws ilPasswordException
69     */
70    public function testCostsCanBeRetrievedWhenCostsAreSet(ilBcryptPhpPasswordEncoder $encoder) : void
71    {
72        $expected = '04';
73
74        $encoder->setCosts($expected);
75        $this->assertEquals($expected, $encoder->getCosts());
76    }
77
78    /**
79     * @depends testInstanceCanBeCreated
80     * @param ilBcryptPhpPasswordEncoder $encoder
81     * @throws ilPasswordException
82     */
83    public function testCostsCannotBeSetAboveRange(ilBcryptPhpPasswordEncoder $encoder) : void
84    {
85        $this->expectException(ilPasswordException::class);
86        $encoder->setCosts('32');
87    }
88
89    /**
90     * @depends testInstanceCanBeCreated
91     * @param ilBcryptPhpPasswordEncoder $encoder
92     * @throws ilPasswordException
93     */
94    public function testCostsCannotBeSetBelowRange(ilBcryptPhpPasswordEncoder $encoder) : void
95    {
96        $this->expectException(ilPasswordException::class);
97        $encoder->setCosts('3');
98    }
99
100    /**
101     * @depends      testInstanceCanBeCreated
102     * @dataProvider costsProvider
103     * @doesNotPerformAssertions
104     * @param string                     $costs
105     * @param ilBcryptPhpPasswordEncoder $encoder
106     * @throws ilPasswordException
107     */
108    public function testCostsCanBeSetInRange(string $costs, ilBcryptPhpPasswordEncoder $encoder) : void
109    {
110        $encoder->setCosts($costs);
111    }
112
113    /**
114     * @depends testInstanceCanBeCreated
115     * @param ilBcryptPhpPasswordEncoder $encoder
116     * @return ilBcryptPhpPasswordEncoder
117     * @throws ilPasswordException
118     */
119    public function testPasswordShouldBeCorrectlyEncodedAndVerified(
120        ilBcryptPhpPasswordEncoder $encoder
121    ) : ilBcryptPhpPasswordEncoder {
122        $encoder->setCosts(self::VALID_COSTS);
123        $encoded_password = $encoder->encodePassword(self::PASSWORD, '');
124        $this->assertTrue($encoder->isPasswordValid($encoded_password, self::PASSWORD, ''));
125        $this->assertFalse($encoder->isPasswordValid($encoded_password, self::WRONG_PASSWORD, ''));
126        return $encoder;
127    }
128
129    /**
130     * @depends testInstanceCanBeCreated
131     * @param ilBcryptPhpPasswordEncoder $encoder
132     * @throws ilPasswordException
133     */
134    public function testExceptionIsRaisedIfThePasswordExceedsTheSupportedLengthOnEncoding(
135        ilBcryptPhpPasswordEncoder $encoder
136    ) : void {
137        $this->expectException(ilPasswordException::class);
138        $encoder->setCosts(self::VALID_COSTS);
139        $encoder->encodePassword(str_repeat('a', 5000), '');
140    }
141
142    /**
143     * @depends testInstanceCanBeCreated
144     * @param ilBcryptPhpPasswordEncoder $encoder
145     * @throws ilPasswordException
146     */
147    public function testPasswordVerificationShouldFailIfTheRawPasswordExceedsTheSupportedLength(
148        ilBcryptPhpPasswordEncoder $encoder
149    ) : void {
150        $encoder->setCosts(self::VALID_COSTS);
151        $this->assertFalse($encoder->isPasswordValid('encoded', str_repeat('a', 5000), ''));
152    }
153
154    /**
155     * @depends testInstanceCanBeCreated
156     * @param ilBcryptPhpPasswordEncoder $encoder
157     */
158    public function testNameShouldBeBcryptPhp(ilBcryptPhpPasswordEncoder $encoder) : void
159    {
160        $this->assertEquals('bcryptphp', $encoder->getName());
161    }
162
163    /**
164     * @depends testInstanceCanBeCreated
165     * @param ilBcryptPhpPasswordEncoder $encoder
166     * @throws ilPasswordException
167     */
168    public function testCostsCanBeDeterminedDynamically(ilBcryptPhpPasswordEncoder $encoder) : void
169    {
170        $costs_default = $encoder->benchmarkCost();
171        $costs_target = $encoder->benchmarkCost(0.5);
172
173        $this->assertTrue($costs_default > 4 && $costs_default < 32);
174        $this->assertTrue($costs_target > 4 && $costs_target < 32);
175        $this->assertIsInt($costs_default);
176        $this->assertIsInt($costs_target);
177        $this->assertNotEquals($costs_default, $costs_target);
178    }
179
180    /**
181     * @depends testInstanceCanBeCreated
182     * @param ilBcryptPhpPasswordEncoder $encoder
183     */
184    public function testEncoderDoesNotRelyOnSalts(ilBcryptPhpPasswordEncoder $encoder) : void
185    {
186        $this->assertFalse($encoder->requiresSalt());
187    }
188
189    /**
190     * @depends testInstanceCanBeCreated
191     * @param ilBcryptPhpPasswordEncoder $encoder
192     * @throws ilPasswordException
193     */
194    public function testReencodingIsDetectedWhenNecessary(ilBcryptPhpPasswordEncoder $encoder) : void
195    {
196        $raw = self::PASSWORD;
197
198        $encoder->setCosts('8');
199        $encoded = $encoder->encodePassword($raw, '');
200        $encoder->setCosts('8');
201        $this->assertFalse($encoder->requiresReencoding($encoded));
202
203        $encoder->setCosts('9');
204        $this->assertTrue($encoder->requiresReencoding($encoded));
205    }
206}
207