urlResolver = $this->createMock(ShortUrlResolverInterface::class); } /** @test */ public function aNotFoundShortCodeWillDelegateIntoNextMiddleware(): void { $shortCode = 'abc123'; $this->urlResolver->expects($this->once())->method('resolveEnabledShortUrl')->with( $this->equalTo(ShortUrlIdentifier::fromShortCodeAndDomain($shortCode, '')) )->willThrowException(ShortUrlNotFoundException::fromNotFound(ShortUrlIdentifier::fromShortCodeAndDomain(''))); $delegate = $this->createMock(RequestHandlerInterface::class); $delegate->expects($this->once())->method('handle')->withAnyParameters()->willReturn(new Response()); $this->action()->process((new ServerRequest())->withAttribute('shortCode', $shortCode), $delegate); } /** @test */ public function aCorrectRequestReturnsTheQrCodeResponse(): void { $shortCode = 'abc123'; $this->urlResolver->expects($this->once())->method('resolveEnabledShortUrl')->with( $this->equalTo(ShortUrlIdentifier::fromShortCodeAndDomain($shortCode, '')), )->willReturn(ShortUrl::createEmpty()); $delegate = $this->createMock(RequestHandlerInterface::class); $delegate->expects($this->never())->method('handle'); $resp = $this->action()->process((new ServerRequest())->withAttribute('shortCode', $shortCode), $delegate); self::assertInstanceOf(QrCodeResponse::class, $resp); self::assertEquals(200, $resp->getStatusCode()); } /** * @test * @dataProvider provideQueries */ public function imageIsReturnedWithExpectedContentTypeBasedOnProvidedFormat( string $defaultFormat, array $query, string $expectedContentType, ): void { $code = 'abc123'; $this->urlResolver->method('resolveEnabledShortUrl')->with( $this->equalTo(ShortUrlIdentifier::fromShortCodeAndDomain($code, '')), )->willReturn(ShortUrl::createEmpty()); $delegate = $this->createMock(RequestHandlerInterface::class); $req = (new ServerRequest())->withAttribute('shortCode', $code)->withQueryParams($query); $resp = $this->action(new QrCodeOptions(format: $defaultFormat))->process($req, $delegate); self::assertEquals($expectedContentType, $resp->getHeaderLine('Content-Type')); } public function provideQueries(): iterable { yield 'no format, png default' => ['png', [], 'image/png']; yield 'no format, svg default' => ['svg', [], 'image/svg+xml']; yield 'png format, png default' => ['png', ['format' => 'png'], 'image/png']; yield 'png format, svg default' => ['svg', ['format' => 'png'], 'image/png']; yield 'svg format, png default' => ['png', ['format' => 'svg'], 'image/svg+xml']; yield 'svg format, svg default' => ['svg', ['format' => 'svg'], 'image/svg+xml']; yield 'unsupported format, png default' => ['png', ['format' => 'jpg'], 'image/png']; yield 'unsupported format, svg default' => ['svg', ['format' => 'jpg'], 'image/svg+xml']; } /** * @test * @dataProvider provideRequestsWithSize */ public function imageIsReturnedWithExpectedSize( QrCodeOptions $defaultOptions, ServerRequestInterface $req, int $expectedSize, ): void { $code = 'abc123'; $this->urlResolver->method('resolveEnabledShortUrl')->with( $this->equalTo(ShortUrlIdentifier::fromShortCodeAndDomain($code, '')), )->willReturn(ShortUrl::createEmpty()); $delegate = $this->createMock(RequestHandlerInterface::class); $resp = $this->action($defaultOptions)->process($req->withAttribute('shortCode', $code), $delegate); [$size] = getimagesizefromstring($resp->getBody()->__toString()); self::assertEquals($expectedSize, $size); } public function provideRequestsWithSize(): iterable { yield 'different margin and size defaults' => [ new QrCodeOptions(size: 660, margin: 40), ServerRequestFactory::fromGlobals(), 740, ]; yield 'no size' => [new QrCodeOptions(), ServerRequestFactory::fromGlobals(), 300]; yield 'no size, different default' => [new QrCodeOptions(size: 500), ServerRequestFactory::fromGlobals(), 500]; yield 'size in query' => [ new QrCodeOptions(), ServerRequestFactory::fromGlobals()->withQueryParams(['size' => '123']), 123, ]; yield 'size in query, default margin' => [ new QrCodeOptions(margin: 25), ServerRequestFactory::fromGlobals()->withQueryParams(['size' => '123']), 173, ]; yield 'margin' => [ new QrCodeOptions(), ServerRequestFactory::fromGlobals()->withQueryParams(['margin' => '35']), 370, ]; yield 'margin and different default' => [ new QrCodeOptions(size: 400), ServerRequestFactory::fromGlobals()->withQueryParams(['margin' => '35']), 470, ]; yield 'margin and size' => [ new QrCodeOptions(), ServerRequestFactory::fromGlobals()->withQueryParams(['margin' => '100', 'size' => '200']), 400, ]; yield 'negative margin' => [ new QrCodeOptions(), ServerRequestFactory::fromGlobals()->withQueryParams(['margin' => '-50']), 300, ]; yield 'negative margin, default margin' => [ new QrCodeOptions(margin: 10), ServerRequestFactory::fromGlobals()->withQueryParams(['margin' => '-50']), 300, ]; yield 'non-numeric margin' => [ new QrCodeOptions(), ServerRequestFactory::fromGlobals()->withQueryParams(['margin' => 'foo']), 300, ]; yield 'negative margin and size' => [ new QrCodeOptions(), ServerRequestFactory::fromGlobals()->withQueryParams(['margin' => '-1', 'size' => '150']), 150, ]; yield 'negative margin and size, default margin' => [ new QrCodeOptions(margin: 5), ServerRequestFactory::fromGlobals()->withQueryParams(['margin' => '-1', 'size' => '150']), 150, ]; yield 'non-numeric margin and size' => [ new QrCodeOptions(), ServerRequestFactory::fromGlobals()->withQueryParams(['margin' => 'foo', 'size' => '538']), 538, ]; } /** * @test * @dataProvider provideRoundBlockSize */ public function imageCanRemoveExtraMarginWhenBlockRoundIsDisabled( QrCodeOptions $defaultOptions, ?string $roundBlockSize, int $expectedColor, ): void { $code = 'abc123'; $req = ServerRequestFactory::fromGlobals() ->withQueryParams(['size' => 250, 'roundBlockSize' => $roundBlockSize]) ->withAttribute('shortCode', $code); $this->urlResolver->method('resolveEnabledShortUrl')->with( $this->equalTo(ShortUrlIdentifier::fromShortCodeAndDomain($code, '')), )->willReturn(ShortUrl::withLongUrl('https://shlink.io')); $delegate = $this->createMock(RequestHandlerInterface::class); $resp = $this->action($defaultOptions)->process($req, $delegate); $image = imagecreatefromstring($resp->getBody()->__toString()); $color = imagecolorat($image, 1, 1); self::assertEquals($color, $expectedColor); } public function provideRoundBlockSize(): iterable { yield 'no round block param' => [new QrCodeOptions(), null, self::WHITE]; yield 'no round block param, but disabled by default' => [ new QrCodeOptions(roundBlockSize: false), null, self::BLACK, ]; yield 'round block: "true"' => [new QrCodeOptions(), 'true', self::WHITE]; yield 'round block: "true", but disabled by default' => [ new QrCodeOptions(roundBlockSize: false), 'true', self::WHITE, ]; yield 'round block: "false"' => [new QrCodeOptions(), 'false', self::BLACK]; yield 'round block: "false", but enabled by default' => [ new QrCodeOptions(roundBlockSize: true), 'false', self::BLACK, ]; } public function action(?QrCodeOptions $options = null): QrCodeAction { return new QrCodeAction( $this->urlResolver, new ShortUrlStringifier(['domain' => 'doma.in']), new NullLogger(), $options ?? new QrCodeOptions(), ); } }