Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/add azure devops webhook #134

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions config/parameters.yml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ parameters:
gitlab.auto_add_repo: false
gitlab.auto_add_repo_type: ~
gitlab.prefer_ssh_url_type: false
devops.secret: ~
3 changes: 3 additions & 0 deletions src/Kernel.php
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@ protected function configureRoutes(RouteCollectionBuilder $routes)
$routes
->add('/webhook/gitlab', $controllerBase . 'WebhookController::gitlabAction', 'webhook_gitlab')
->setMethods(['GET', 'POST']);
$routes
->add('/webhook/devops', $controllerBase . 'WebhookController::devopsAction', 'webhook_devops')
->setMethods(['GET', 'POST']);
}

// optional, to use the standard Symfony cache directory
Expand Down
10 changes: 10 additions & 0 deletions src/Playbloom/Satisfy/Controller/WebhookController.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Playbloom\Satisfy\Webhook\AbstractWebhook;
use Playbloom\Satisfy\Webhook\BitbucketWebhook;
use Playbloom\Satisfy\Webhook\DevOpsWebhook;
use Playbloom\Satisfy\Webhook\GithubWebhook;
use Playbloom\Satisfy\Webhook\GitlabWebhook;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
Expand Down Expand Up @@ -39,6 +40,13 @@ public function gitlabAction(Request $request): Response
return $this->handleRequest($request, $webhook);
}

public function devopsAction(Request $request): Response
{
$webhook = $this->container->get(DevOpsWebhook::class);

return $this->handleRequest($request, $webhook);
}

private function handleRequest(Request $request, AbstractWebhook $webhook): Response
{
return $webhook->getResponse($request);
Expand All @@ -50,7 +58,9 @@ public static function getSubscribedServices(): array
$services[] = BitbucketWebhook::class;
$services[] = GithubWebhook::class;
$services[] = GitlabWebhook::class;
$services[] = DevOpsWebhook::class;

return $services;
}

}
6 changes: 6 additions & 0 deletions src/Playbloom/Satisfy/Resources/config/services.yml
Original file line number Diff line number Diff line change
Expand Up @@ -95,3 +95,9 @@ services:
$root: "%kernel.project_dir%"
$satisFilename: "%satis_filename%"
$composerHome: "%composer.home%"

Playbloom\Satisfy\Webhook\DevOpsWebhook:
autowire: true
public: true
arguments:
$secret: "%devops.secret%"
70 changes: 70 additions & 0 deletions src/Playbloom/Satisfy/Webhook/DevOpsWebhook.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?php


namespace Playbloom\Satisfy\Webhook;


use InvalidArgumentException;
use Playbloom\Satisfy\Model\RepositoryInterface;
use Playbloom\Satisfy\Service\Manager;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Request;

class DevOpsWebhook extends AbstractWebhook
{
private const HTTP_TOKEN = 'X-DEVOPS-TOKEN';

public function __construct(
Manager $manager,
EventDispatcherInterface $dispatcher,
?string $secret = null
) {
parent::__construct($manager, $dispatcher);
$this->manager = $manager;
$this->dispatcher = $dispatcher;
$this->secret = $secret;
}

public function setSecret(string $secret = null): self
{
$this->secret = $secret;

return $this;
}

/**
* @inheritDoc
*/
protected function validate(Request $request): void
{
if ($request->headers->get(self::HTTP_TOKEN) !== $this->secret) {
throw new InvalidArgumentException('Invalid Token');
}
}

/**
* @inheritDoc
*/
protected function getRepository(Request $request): RepositoryInterface
{
$content = json_decode($request->getContent(), true);
if (
!is_array($content)
|| !array_key_exists('resource', $content)
|| !is_array($content['resource'])
|| !array_key_exists('repository', $content['resource'])
|| !is_array($content['resource']['repository'])
|| !array_key_exists('url', $content['resource']['repository'])
|| empty($content['resource']['repository']['url'])
) {
throw new InvalidArgumentException('Invalid Request');
}
$repositoryUrlHttp = $content['resource']['repository']['url'];
$repositoryUrlPattern = preg_replace('/(https:\/\/)([^\/]+)(.+)/', '$3', $repositoryUrlHttp);
$repository = $this->manager->findByUrl('#'.$repositoryUrlPattern.'$#');
if(!$repository instanceof RepositoryInterface) {
throw new InvalidArgumentException('Invalid Repository');
}
return $repository;
}
}
118 changes: 118 additions & 0 deletions tests/Playbloom/Satisfy/Webhook/DevOpsWebhookTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
<?php /** @noinspection PhpUndefinedMethodInspection */
declare(strict_types=1);

namespace Tests\Playbloom\Satisfy\Webhook;

use Generator;
use PHPUnit\Framework\TestCase;
use Playbloom\Satisfy\Event\BuildEvent;
use Playbloom\Satisfy\Model\Repository;
use Playbloom\Satisfy\Service\Manager;
use Playbloom\Satisfy\Webhook\DevOpsWebhook;
use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;

class DevOpsWebhookTest extends TestCase
{
protected const secret = '12345';

/**
* @dataProvider invalidRequestProvider
*
* @param Request $request
*/
public function testInvalidRequest(Request $request): void
{
$this->expectException(BadRequestHttpException::class);

/** @noinspection PhpParamsInspection */
$handler = new DevOpsWebhook($this->getManagerMock()->reveal(), $this->getDispatcherMock()->reveal(), self::secret);
$handler->getResponse($request);
}

public function testValidRequest(): void
{
$manager = $this->getManagerMock();
$manager
->findByUrl(Argument::type('string'))
->willReturn(new Repository('https://dev.azure.com/path/to/repo/_git/satisfy'))
->shouldBeCalledTimes(1);

$dispatcher = $this->getDispatcherMock();
$dispatcher
->dispatch(Argument::type(BuildEvent::class))
->will(
function ($args) {
$args[0]->setStatus(0);
}
)
->shouldBeCalledTimes(1);

$request = self::createRequest(file_get_contents(__DIR__ . '/../../../fixtures/devops-push.json'), self::secret);
/** @noinspection PhpParamsInspection */
$handler = new DevOpsWebhook($manager->reveal(), $dispatcher->reveal(), self::secret);
$response = $handler->getResponse($request);

self::assertEquals(Response::HTTP_OK, $response->getStatusCode());
self::assertEquals(0, $response->getContent());
}

public function testInvalidTokenRequest()
{
$manager = $this->getManagerMock();
$manager
->findByUrl(Argument::type('string'))
->willReturn(new Repository('https://dev.azure.com/path/to/repo/_git/satisfy'))
->shouldBeCalledTimes(0);

$dispatcher = $this->getDispatcherMock();
$dispatcher
->dispatch(Argument::type(BuildEvent::class))
->shouldBeCalledTimes(0);

$this->expectException(BadRequestHttpException::class);
$this->expectExceptionMessage('Invalid Token');
$request = self::createRequest(file_get_contents(__DIR__ . '/../../../fixtures/devops-push.json'), 'invalid-token');
/** @noinspection PhpParamsInspection */
$handler = new DevOpsWebhook($manager->reveal(), $dispatcher->reveal(), self::secret);
$response = $handler->getResponse($request);

self::assertEquals(Response::HTTP_BAD_REQUEST, $response->getStatusCode());
}

public function invalidRequestProvider(): Generator
{
yield [self::createRequest([], '')];

yield [self::createRequest([], self::secret)];

yield [self::createRequest(['resource' => [ 'repository' => ['url' => ''] ]])];
}

protected static function createRequest($content, string $token = null): Request
{
if (!is_string($content)) {
$content = json_encode($content);
}
$request = Request::create('', 'GET', [], [], [], [], $content);
if (null !== $token) {
$request->headers->set('X-DEVOPS-TOKEN', $token);
}

return $request;
}

protected function getManagerMock(): ObjectProphecy
{
return $this->prophesize(Manager::class);
}

protected function getDispatcherMock(): ObjectProphecy
{
return $this->prophesize(EventDispatcher::class);
}
}
7 changes: 7 additions & 0 deletions tests/fixtures/devops-push.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"resource": {
"repository": {
"url": "https://dev.azure.com/path/to/repo/_git/satisfy"
}
}
}