Completed
Pull Request — develop (#653)
by Alejandro
05:03
created

defaultDomainIsDroppedIfProvided()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 13
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 9
nc 1
nop 0
dl 0
loc 13
rs 9.9666
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('INVALID_SLUG', $payload['type']);
53
        $this->assertEquals('Invalid custom slug', $payload['title']);
54
        $this->assertEquals($slug, $payload['customSlug']);
55
56
        if ($domain !== null) {
57
            $this->assertEquals($domain, $payload['domain']);
58
        } else {
59
            $this->assertArrayNotHasKey('domain', $payload);
60
        }
61
    }
62
63
    /** @test */
64
    public function createsNewShortUrlWithTags(): void
65
    {
66
        [$statusCode, ['tags' => $tags]] = $this->createShortUrl(['tags' => ['foo', 'bar', 'baz']]);
67
68
        $this->assertEquals(self::STATUS_OK, $statusCode);
69
        $this->assertEquals(['foo', 'bar', 'baz'], $tags);
70
    }
71
72
    /**
73
     * @test
74
     * @dataProvider provideMaxVisits
75
     */
76
    public function createsNewShortUrlWithVisitsLimit(int $maxVisits): void
77
    {
78
        [$statusCode, ['shortCode' => $shortCode]] = $this->createShortUrl(['maxVisits' => $maxVisits]);
79
80
        $this->assertEquals(self::STATUS_OK, $statusCode);
81
82
        // Last request to the short URL will return a 404, and the rest, a 302
83
        for ($i = 0; $i < $maxVisits; $i++) {
84
            $this->assertEquals(self::STATUS_FOUND, $this->callShortUrl($shortCode)->getStatusCode());
85
        }
86
        $lastResp = $this->callShortUrl($shortCode);
87
        $this->assertEquals(self::STATUS_NOT_FOUND, $lastResp->getStatusCode());
88
    }
89
90
    public function provideMaxVisits(): array
91
    {
92
        return map(range(10, 15), fn (int $i) => [$i]);
93
    }
94
95
    /** @test */
96
    public function createsShortUrlWithValidSince(): void
97
    {
98
        [$statusCode, ['shortCode' => $shortCode]] = $this->createShortUrl([
99
            'validSince' => Chronos::now()->addDay()->toAtomString(),
100
        ]);
101
102
        $this->assertEquals(self::STATUS_OK, $statusCode);
103
104
        // Request to the short URL will return a 404 since it's not valid yet
105
        $lastResp = $this->callShortUrl($shortCode);
106
        $this->assertEquals(self::STATUS_NOT_FOUND, $lastResp->getStatusCode());
107
    }
108
109
    /** @test */
110
    public function createsShortUrlWithValidUntil(): void
111
    {
112
        [$statusCode, ['shortCode' => $shortCode]] = $this->createShortUrl([
113
            'validUntil' => Chronos::now()->subDay()->toAtomString(),
114
        ]);
115
116
        $this->assertEquals(self::STATUS_OK, $statusCode);
117
118
        // Request to the short URL will return a 404 since it's no longer valid
119
        $lastResp = $this->callShortUrl($shortCode);
120
        $this->assertEquals(self::STATUS_NOT_FOUND, $lastResp->getStatusCode());
121
    }
122
123
    /**
124
     * @test
125
     * @dataProvider provideMatchingBodies
126
     */
127
    public function returnsAnExistingShortUrlWhenRequested(array $body): void
128
    {
129
        [$firstStatusCode, ['shortCode' => $firstShortCode]] = $this->createShortUrl($body);
130
131
        $body['findIfExists'] = true;
132
        [$secondStatusCode, ['shortCode' => $secondShortCode]] = $this->createShortUrl($body);
133
134
        $this->assertEquals(self::STATUS_OK, $firstStatusCode);
135
        $this->assertEquals(self::STATUS_OK, $secondStatusCode);
136
        $this->assertEquals($firstShortCode, $secondShortCode);
137
    }
138
139
    public function provideMatchingBodies(): iterable
140
    {
141
        $longUrl = 'https://www.alejandrocelaya.com';
142
143
        yield 'only long URL' => [['longUrl' => $longUrl]];
144
        yield 'long URL and tags' => [['longUrl' => $longUrl, 'tags' => ['boo', 'far']]];
145
        yield 'long URL and custom slug' => [['longUrl' => $longUrl, 'customSlug' => 'my cool slug']];
146
        yield 'several params' => [[
147
            'longUrl' => $longUrl,
148
            'tags' => ['boo', 'far'],
149
            'validSince' => Chronos::now()->toAtomString(),
150
            'maxVisits' => 7,
151
        ]];
152
    }
153
154
    /**
155
     * @test
156
     * @dataProvider provideConflictingSlugs
157
     */
158
    public function returnsErrorWhenRequestingReturnExistingButCustomSlugIsInUse(string $slug, ?string $domain): void
159
    {
160
        $longUrl = 'https://www.alejandrocelaya.com';
161
162
        [$firstStatusCode] = $this->createShortUrl(['longUrl' => $longUrl]);
163
        [$secondStatusCode] = $this->createShortUrl([
164
            'longUrl' => $longUrl,
165
            'customSlug' => $slug,
166
            'findIfExists' => true,
167
            'domain' => $domain,
168
        ]);
169
170
        $this->assertEquals(self::STATUS_OK, $firstStatusCode);
171
        $this->assertEquals(self::STATUS_BAD_REQUEST, $secondStatusCode);
172
    }
173
174
    public function provideConflictingSlugs(): iterable
175
    {
176
        yield 'without domain' => ['custom', null];
177
        yield 'with domain' => ['custom-with-domain', 'some-domain.com'];
178
    }
179
180
    /** @test */
181
    public function createsNewShortUrlIfRequestedToFindButThereIsNoMatch(): void
182
    {
183
        [$firstStatusCode, ['shortCode' => $firstShortCode]] = $this->createShortUrl([
184
            'longUrl' => 'https://www.alejandrocelaya.com',
185
        ]);
186
        [$secondStatusCode, ['shortCode' => $secondShortCode]] = $this->createShortUrl([
187
            'longUrl' => 'https://www.alejandrocelaya.com/projects',
188
            'findIfExists' => true,
189
        ]);
190
191
        $this->assertEquals(self::STATUS_OK, $firstStatusCode);
192
        $this->assertEquals(self::STATUS_OK, $secondStatusCode);
193
        $this->assertNotEquals($firstShortCode, $secondShortCode);
194
    }
195
196
    /**
197
     * @test
198
     * @dataProvider provideIdn
199
     */
200
    public function createsNewShortUrlWithInternationalizedDomainName(string $longUrl): void
201
    {
202
        [$statusCode, $payload] = $this->createShortUrl(['longUrl' => $longUrl]);
203
204
        $this->assertEquals(self::STATUS_OK, $statusCode);
205
        $this->assertEquals($payload['longUrl'], $longUrl);
206
    }
207
208
    public function provideIdn(): iterable
209
    {
210
        yield ['http://tést.shlink.io']; // Redirects to https://shlink.io
211
        yield ['http://test.shlink.io']; // Redirects to http://tést.shlink.io
212
        yield ['http://téstb.shlink.io']; // Redirects to http://tést.shlink.io
213
    }
214
215
    /** @test */
216
    public function failsToCreateShortUrlWithInvalidLongUrl(): void
217
    {
218
        $url = 'https://this-has-to-be-invalid.com';
219
        $expectedDetail = sprintf('Provided URL %s is invalid. Try with a different one.', $url);
220
221
        [$statusCode, $payload] = $this->createShortUrl(['longUrl' => $url]);
222
223
        $this->assertEquals(self::STATUS_BAD_REQUEST, $statusCode);
224
        $this->assertEquals(self::STATUS_BAD_REQUEST, $payload['status']);
225
        $this->assertEquals('INVALID_URL', $payload['type']);
226
        $this->assertEquals($expectedDetail, $payload['detail']);
227
        $this->assertEquals('Invalid URL', $payload['title']);
228
        $this->assertEquals($url, $payload['url']);
229
    }
230
231
    /** @test */
232
    public function defaultDomainIsDroppedIfProvided(): void
233
    {
234
        [$createStatusCode, ['shortCode' => $shortCode]] = $this->createShortUrl([
235
            'longUrl' => 'https://www.alejandrocelaya.com',
236
            'domain' => 'doma.in',
237
        ]);
238
        $getResp = $this->callApiWithKey(self::METHOD_GET, '/short-urls/' . $shortCode);
239
        $payload = $this->getJsonResponsePayload($getResp);
240
241
        $this->assertEquals(self::STATUS_OK, $createStatusCode);
242
        $this->assertEquals(self::STATUS_OK, $getResp->getStatusCode());
243
        $this->assertArrayHasKey('domain', $payload);
244
        $this->assertNull($payload['domain']);
245
    }
246
247
    /**
248
     * @return array {
249
     *     @var int $statusCode
250
     *     @var array $payload
251
     * }
252
     */
253
    private function createShortUrl(array $body = []): array
254
    {
255
        if (! isset($body['longUrl'])) {
256
            $body['longUrl'] = 'https://app.shlink.io';
257
        }
258
        $resp = $this->callApiWithKey(self::METHOD_POST, '/short-urls', [RequestOptions::JSON => $body]);
259
        $payload = $this->getJsonResponsePayload($resp);
260
261
        return [$resp->getStatusCode(), $payload];
262
    }
263
}
264