Completed
Pull Request — master (#554)
by Alejandro
13:38
created

failsToCreateShortUrlWithInvalidOriginalUrl()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 15
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 11
nc 1
nop 0
dl 0
loc 15
rs 9.9
c 1
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace ShlinkioApiTest\Shlink\Rest\Action;
6
7
use Cake\Chronos\Chronos;
8
use GuzzleHttp\RequestOptions;
9
use Shlinkio\Shlink\TestUtils\ApiTest\ApiTestCase;
10
11
use function Functional\map;
12
use function range;
13
use function sprintf;
14
15
class CreateShortUrlActionTest extends ApiTestCase
16
{
17
    /** @test */
18
    public function createsNewShortUrlWhenOnlyLongUrlIsProvided(): void
19
    {
20
        $expectedKeys = ['shortCode', 'shortUrl', 'longUrl', 'dateCreated', 'visitsCount', 'tags'];
21
        [$statusCode, $payload] = $this->createShortUrl();
22
23
        $this->assertEquals(self::STATUS_OK, $statusCode);
24
        foreach ($expectedKeys as $key) {
25
            $this->assertArrayHasKey($key, $payload);
26
        }
27
    }
28
29
    /** @test */
30
    public function createsNewShortUrlWithCustomSlug(): void
31
    {
32
        [$statusCode, $payload] = $this->createShortUrl(['customSlug' => 'my cool slug']);
33
34
        $this->assertEquals(self::STATUS_OK, $statusCode);
35
        $this->assertEquals('my-cool-slug', $payload['shortCode']);
36
    }
37
38
    /**
39
     * @test
40
     * @dataProvider provideConflictingSlugs
41
     */
42
    public function failsToCreateShortUrlWithDuplicatedSlug(string $slug, ?string $domain): void
43
    {
44
        $suffix = $domain === null ? '' : sprintf(' for domain "%s"', $domain);
45
        $detail = sprintf('Provided slug "%s" is already in use%s.', $slug, $suffix);
46
47
        [$statusCode, $payload] = $this->createShortUrl(['customSlug' => $slug, 'domain' => $domain]);
48
49
        $this->assertEquals(self::STATUS_BAD_REQUEST, $statusCode);
50
        $this->assertEquals(self::STATUS_BAD_REQUEST, $payload['status']);
51
        $this->assertEquals($detail, $payload['detail']);
52
        $this->assertEquals($detail, $payload['message']); // Deprecated
53
        $this->assertEquals('INVALID_SLUG', $payload['type']);
54
        $this->assertEquals('INVALID_SLUG', $payload['error']); // Deprecated
55
        $this->assertEquals('Invalid custom slug', $payload['title']);
56
        $this->assertEquals($slug, $payload['customSlug']);
57
58
        if ($domain !== null) {
59
            $this->assertEquals($domain, $payload['domain']);
60
        } else {
61
            $this->assertArrayNotHasKey('domain', $payload);
62
        }
63
    }
64
65
    /** @test */
66
    public function createsNewShortUrlWithTags(): void
67
    {
68
        [$statusCode, ['tags' => $tags]] = $this->createShortUrl(['tags' => ['foo', 'bar', 'baz']]);
69
70
        $this->assertEquals(self::STATUS_OK, $statusCode);
71
        $this->assertEquals(['foo', 'bar', 'baz'], $tags);
72
    }
73
74
    /**
75
     * @test
76
     * @dataProvider provideMaxVisits
77
     */
78
    public function createsNewShortUrlWithVisitsLimit(int $maxVisits): void
79
    {
80
        [$statusCode, ['shortCode' => $shortCode]] = $this->createShortUrl(['maxVisits' => $maxVisits]);
81
82
        $this->assertEquals(self::STATUS_OK, $statusCode);
83
84
        // Last request to the short URL will return a 404, and the rest, a 302
85
        for ($i = 0; $i < $maxVisits; $i++) {
86
            $this->assertEquals(self::STATUS_FOUND, $this->callShortUrl($shortCode)->getStatusCode());
87
        }
88
        $lastResp = $this->callShortUrl($shortCode);
89
        $this->assertEquals(self::STATUS_NOT_FOUND, $lastResp->getStatusCode());
90
    }
91
92
    public function provideMaxVisits(): array
93
    {
94
        return map(range(10, 15), function (int $i) {
95
            return [$i];
96
        });
97
    }
98
99
    /** @test */
100
    public function createsShortUrlWithValidSince(): void
101
    {
102
        [$statusCode, ['shortCode' => $shortCode]] = $this->createShortUrl([
103
            'validSince' => Chronos::now()->addDay()->toAtomString(),
104
        ]);
105
106
        $this->assertEquals(self::STATUS_OK, $statusCode);
107
108
        // Request to the short URL will return a 404 since it's not valid yet
109
        $lastResp = $this->callShortUrl($shortCode);
110
        $this->assertEquals(self::STATUS_NOT_FOUND, $lastResp->getStatusCode());
111
    }
112
113
    /** @test */
114
    public function createsShortUrlWithValidUntil(): void
115
    {
116
        [$statusCode, ['shortCode' => $shortCode]] = $this->createShortUrl([
117
            'validUntil' => Chronos::now()->subDay()->toAtomString(),
118
        ]);
119
120
        $this->assertEquals(self::STATUS_OK, $statusCode);
121
122
        // Request to the short URL will return a 404 since it's no longer valid
123
        $lastResp = $this->callShortUrl($shortCode);
124
        $this->assertEquals(self::STATUS_NOT_FOUND, $lastResp->getStatusCode());
125
    }
126
127
    /**
128
     * @test
129
     * @dataProvider provideMatchingBodies
130
     */
131
    public function returnsAnExistingShortUrlWhenRequested(array $body): void
132
    {
133
        [$firstStatusCode, ['shortCode' => $firstShortCode]] = $this->createShortUrl($body);
134
135
        $body['findIfExists'] = true;
136
        [$secondStatusCode, ['shortCode' => $secondShortCode]] = $this->createShortUrl($body);
137
138
        $this->assertEquals(self::STATUS_OK, $firstStatusCode);
139
        $this->assertEquals(self::STATUS_OK, $secondStatusCode);
140
        $this->assertEquals($firstShortCode, $secondShortCode);
141
    }
142
143
    public function provideMatchingBodies(): iterable
144
    {
145
        $longUrl = 'https://www.alejandrocelaya.com';
146
147
        yield 'only long URL' => [['longUrl' => $longUrl]];
148
        yield 'long URL and tags' => [['longUrl' => $longUrl, 'tags' => ['boo', 'far']]];
149
        yield 'long URL and custom slug' => [['longUrl' => $longUrl, 'customSlug' => 'my cool slug']];
150
        yield 'several params' => [[
151
            'longUrl' => $longUrl,
152
            'tags' => ['boo', 'far'],
153
            'validSince' => Chronos::now()->toAtomString(),
154
            'maxVisits' => 7,
155
        ]];
156
    }
157
158
    /**
159
     * @test
160
     * @dataProvider provideConflictingSlugs
161
     */
162
    public function returnsErrorWhenRequestingReturnExistingButCustomSlugIsInUse(string $slug, ?string $domain): void
163
    {
164
        $longUrl = 'https://www.alejandrocelaya.com';
165
166
        [$firstStatusCode] = $this->createShortUrl(['longUrl' => $longUrl]);
167
        [$secondStatusCode] = $this->createShortUrl([
168
            'longUrl' => $longUrl,
169
            'customSlug' => $slug,
170
            'findIfExists' => true,
171
            'domain' => $domain,
172
        ]);
173
174
        $this->assertEquals(self::STATUS_OK, $firstStatusCode);
175
        $this->assertEquals(self::STATUS_BAD_REQUEST, $secondStatusCode);
176
    }
177
178
    public function provideConflictingSlugs(): iterable
179
    {
180
        yield 'without domain' => ['custom', null];
181
        yield 'with domain' => ['custom-with-domain', 'some-domain.com'];
182
    }
183
184
    /** @test */
185
    public function createsNewShortUrlIfRequestedToFindButThereIsNoMatch(): void
186
    {
187
        [$firstStatusCode, ['shortCode' => $firstShortCode]] = $this->createShortUrl([
188
            'longUrl' => 'https://www.alejandrocelaya.com',
189
        ]);
190
        [$secondStatusCode, ['shortCode' => $secondShortCode]] = $this->createShortUrl([
191
            'longUrl' => 'https://www.alejandrocelaya.com/projects',
192
            'findIfExists' => true,
193
        ]);
194
195
        $this->assertEquals(self::STATUS_OK, $firstStatusCode);
196
        $this->assertEquals(self::STATUS_OK, $secondStatusCode);
197
        $this->assertNotEquals($firstShortCode, $secondShortCode);
198
    }
199
200
    /**
201
     * @test
202
     * @dataProvider provideIdn
203
     */
204
    public function createsNewShortUrlWithInternationalizedDomainName(string $longUrl): void
205
    {
206
        [$statusCode, $payload] = $this->createShortUrl(['longUrl' => $longUrl]);
207
208
        $this->assertEquals(self::STATUS_OK, $statusCode);
209
        $this->assertEquals($payload['longUrl'], $longUrl);
210
    }
211
212
    public function provideIdn(): iterable
213
    {
214
        yield ['http://tést.shlink.io']; // Redirects to https://shlink.io
215
        yield ['http://test.shlink.io']; // Redirects to http://tést.shlink.io
216
        yield ['http://téstb.shlink.io']; // Redirects to http://tést.shlink.io
217
    }
218
219
    /** @test */
220
    public function failsToCreateShortUrlWithInvalidOriginalUrl(): void
221
    {
222
        $url = 'https://this-has-to-be-invalid.com';
223
        $expectedDetail = sprintf('Provided URL %s is invalid. Try with a different one.', $url);
224
225
        [$statusCode, $payload] = $this->createShortUrl(['longUrl' => $url]);
226
227
        $this->assertEquals(self::STATUS_BAD_REQUEST, $statusCode);
228
        $this->assertEquals(self::STATUS_BAD_REQUEST, $payload['status']);
229
        $this->assertEquals('INVALID_URL', $payload['type']);
230
        $this->assertEquals('INVALID_URL', $payload['error']); // Deprecated
231
        $this->assertEquals($expectedDetail, $payload['detail']);
232
        $this->assertEquals($expectedDetail, $payload['message']); // Deprecated
233
        $this->assertEquals('Invalid URL', $payload['title']);
234
        $this->assertEquals($url, $payload['url']);
235
    }
236
237
    /**
238
     * @return array {
239
     *     @var int $statusCode
240
     *     @var array $payload
241
     * }
242
     */
243
    private function createShortUrl(array $body = []): array
244
    {
245
        if (! isset($body['longUrl'])) {
246
            $body['longUrl'] = 'https://app.shlink.io';
247
        }
248
        $resp = $this->callApiWithKey(self::METHOD_POST, '/short-urls', [RequestOptions::JSON => $body]);
249
        $payload = $this->getJsonResponsePayload($resp);
250
251
        return [$resp->getStatusCode(), $payload];
252
    }
253
}
254