Page MenuHomeDevCentral

No OneTemporary

diff --git a/app/Console/Commands/NotificationsPayload.php b/app/Console/Commands/NotificationsPayload.php
index dfcdfcd..ada2070 100644
--- a/app/Console/Commands/NotificationsPayload.php
+++ b/app/Console/Commands/NotificationsPayload.php
@@ -1,205 +1,222 @@
<?php
namespace Nasqueron\Notifications\Console\Commands;
+use Nasqueron\Notifications\Phabricator\PhabricatorStory;
+
use Illuminate\Console\Command;
use InvalidArgumentException;
use ReflectionClass;
class NotificationsPayload extends Command {
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'notifications:payload {service} {payload} {args*}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Gets a notification payload from a service payload';
/**
* The service to handle a payload for.
*
* @var string
*/
private $service;
/**
* The payload.
*
* @var string
*/
private $payload;
/**
* The parameters to pass to the notifications class constructor.
*
* An array with arguments' names as keys, arguments' values as values.
*
* @var array
*/
private $constructor;
/**
* Creates a new command instance.
*
* @return void
*/
public function __construct() {
parent::__construct();
}
/**
* Executes the console command.
*/
public function handle() {
if ($this->parseArguments()) {
$this->printNotification();
}
}
/**
* Parses arguments passed to the command.
*
* @return bool true if arguments looks good; otherwise, false.
*/
private function parseArguments () {
try {
$this->parseService();
$this->parsePayload();
$this->parseConstructorParameters();
} catch (InvalidArgumentException $ex) {
$this->error($ex->getMessage());
return false;
}
return true;
}
/**
* Parses service argument.
*
* Fills it to the service property.
*
* @throws InvalidArgumentException when a notification class can't be found for the requested service.
*/
private function parseService () {
$this->service = $this->argument('service');
if (!class_exists($this->getNotificationClass())) {
throw new InvalidArgumentException("Unknown service: $this->service");
}
}
/**
* Parses path to the payload argument.
*
* Fills the content of the file to the payload property.
*
* @throws InvalidArgumentException when payload file is not found.
*/
private function parsePayload () {
$payloadFile = $this->argument('payload');
if (!file_exists($payloadFile)) {
throw new InvalidArgumentException("File not found: $payloadFile");
}
$this->payload = file_get_contents($payloadFile);
}
/**
* Parses all the extra arguments and sets the constructor property
* as an array of constructor arguments.
*
* @throws InvalidArgumentException when too many or too few arguments have been given.
*/
private function parseConstructorParameters () {
$keys = $this->getNotificationConstructorParameters();
$values = $this->argument('args');
- $values['payload'] = json_decode($this->payload);
+ $values['payload'] = $this->payload;
$this->constructor = self::argumentsArrayCombine($keys, $values);
+ $this->constructor['payload'] = $this->formatPayload();
+ }
+
+ /**
+ * Formats payload to pass to constructor
+ *
+ * @return PhabricatorStory|stdClass A deserialization of the payload
+ */
+ private function formatPayload() {
+ if ($this->service === "Phabricator") {
+ $project = $this->constructor['project'];
+ return PhabricatorStory::loadFromJson($project, $this->payload);
+ }
+
+ return json_decode($this->payload);
}
/**
* Creates an array by using one array for keys and another for its values.
*
* @param array $keys
* @param array $values
* @return array
*
* @throws InvalidArgumentException when keys and values counts don't match
*/
public static function argumentsArrayCombine ($keys, $values) {
$countKeys = count($keys);
$countValues = count($values);
if ($countKeys != $countValues) {
throw new InvalidArgumentException("Number of arguments mismatch: got $countValues but expected $countKeys.");
}
return array_combine($keys, $values);
}
/**
* Initializes a new instance of the relevant notification class,
* with the arguments given in the constructor property.
*
* @return Nasqueron\Notifications\Notification
*/
private function getNotification () {
$class = $this->getNotificationClass();
$args = array_values($this->constructor);
return new $class(...$args);
}
/**
* Gets the notification in JSON format.
*
* @return string
*/
private function formatNotification () {
return json_encode($this->getNotification(), JSON_PRETTY_PRINT);
}
/**
* Prints the notification for the service, payload and specified arguments.
*/
private function printNotification () {
$this->line($this->formatNotification());
}
/**
* Gets the notification class for the specified service.
*
* @return string
*/
private function getNotificationClass () {
$namespace = "Nasqueron\Notifications\Notifications\\";
return $namespace . $this->service . "Notification";
}
/**
* Gets an array with the parameters to pass to the constructor
* of the notification class for the specified service.
*
* @return array
*/
private function getNotificationConstructorParameters () {
$parameters = [];
$class = new ReflectionClass($this->getNotificationClass());
foreach ($class->getConstructor()->getParameters() as $parameter) {
$parameters[] = $parameter->getName();
}
return $parameters;
}
}
diff --git a/app/Notifications/PhabricatorNotification.php b/app/Notifications/PhabricatorNotification.php
index a7526b2..83c520c 100644
--- a/app/Notifications/PhabricatorNotification.php
+++ b/app/Notifications/PhabricatorNotification.php
@@ -1,75 +1,68 @@
<?php
namespace Nasqueron\Notifications\Notifications;
use Nasqueron\Notifications\Analyzers\Phabricator\PhabricatorPayloadAnalyzer;
use Nasqueron\Notifications\Notification;
use Nasqueron\Notifications\Phabricator\PhabricatorStory;
class PhabricatorNotification extends Notification {
/**
* @var PhabricatorPayloadAnalyzer
*/
private $analyzer = null;
- private $story;
-
-
/**
* Initializes a new PhabricatorNotification instance
*
* @param string $project The project for this notification
- * @param PhabricatorStory $story The story to convert into a notification
- * @param string[] $projects the list of the projects for this story
+ * @param PhabricatorStory $payload The story to convert into a notification
*/
- public function __construct ($project, PhabricatorStory $story) {
- // Private property used by the analyzer
- $this->story = $story;
-
+ public function __construct ($project, PhabricatorStory $payload) {
// Straightforward properties
$this->service = "Phabricator";
$this->project = $project;
- $this->rawContent = json_encode($story);
- $this->text = $story->text;
+ $this->rawContent = $payload;
+ $this->text = $payload->text;
// Analyzes and fills
- $this->type = $story->getObjectType();
+ $this->type = $payload->getObjectType();
$this->group = $this->getGroup();
$this->link = $this->getLink();
}
/**
* Gets analyzer
*
* @return Nasqueron\Notifications\Analyzers\Phabricator\PhabricatorPayloadAnalyzer
*/
private function getAnalyzer () {
if ($this->analyzer === null) {
$this->analyzer = new PhabricatorPayloadAnalyzer(
$this->project,
- $this->story
+ $this->rawContent
);
}
return $this->analyzer;
}
/**
* Gets the target notificatrion group
*
* @return string the target group for the notification
*/
public function getGroup () {
return $this->getAnalyzer()->getGroup();
}
/**
* Gets the notification URL. Intended to be a widget or icon link.
*
* @return string
*/
public function getLink () {
return "";
}
}
diff --git a/app/Phabricator/PhabricatorStory.php b/app/Phabricator/PhabricatorStory.php
index 8ef8d62..0482e3c 100644
--- a/app/Phabricator/PhabricatorStory.php
+++ b/app/Phabricator/PhabricatorStory.php
@@ -1,287 +1,308 @@
<?php
namespace Nasqueron\Notifications\Phabricator;
+use InvalidArgumentException;
+
class PhabricatorStory {
///
/// Properties
///
/**
* The Phabricator instance name
*
* @var string
*/
public $instanceName;
/**
* The unique identifier Phabricator assigns to each story
*
* @var int
*/
public $id;
/**
* Type of story (e.g. PhabricatorApplicationTransactionFeedStory)
*
* @var string
*/
public $type;
/**
* @var array
*/
public $data;
/**
* The person logged to Phabricator and triggering the event
*
* @var string
*/
public $authorPHID;
/**
* A short English textual description of the event
*
* @var string
*/
public $text;
/**
* The unixtime the event occured
*
* @var int
*/
public $epoch;
/**
* The projects attached to this story.
*
* When there is no project, [].
* When not yet queried, null.
*
* @var string[]|null
*/
private $projects = null;
///
/// Constructors
///
/**
* Initializes a new instance of the Phabricator story class
*
* @param string $instanceName The Phabricator instance name
*/
public function __construct ($instanceName) {
$this->instanceName = $instanceName;
}
/**
* Initializes a new instance of PhabricatorStory from an array.
*
* This is intended to parse the feed.hooks payloads.
*
* @param string $instanceName The Phabricator instance name
* @param string $payload The data submitted by Phabricator
* @return PhabricatorStory
*/
public static function loadFromArray ($instanceName, $payload) {
$instance = new self($instanceName);
foreach ($payload as $key => $value) {
$property = self::mapPhabricatorFeedKey($key);
$instance->$property = $value;
}
return $instance;
}
+ /**
+ * Initializes a new instance of PhabricatorStory from a JSON payload.
+ *
+ * This is intended to parse files saved by LastPayloadSaver::savePayload.
+ *
+ * @param string $instanceName The Phabricator instance name
+ * @param string $payload The data submitted by Phabricator's JSON representation
+ * @return PhabricatorStory
+ */
+ public static function loadFromJson ($instanceName, $payload) {
+ $array = json_decode($payload, true);
+
+ if (!is_array($array)) {
+ throw new InvalidArgumentException("Payload should be deserializable as an array.");
+ }
+
+ return self::loadFromArray($instanceName, $array);
+ }
+
///
/// Helper methods
///
/**
* Gets object type (e.g. TASK for PHID-TASK-l34fw5wievp6n6rnvpuk)
*
* @return string The object type, as a 4 letters string (e.g. 'TASK')
*/
public function getObjectType () {
if ($this->data === null || !array_key_exists('objectPHID', $this->data)) {
return 'VOID';
}
return substr($this->data['objectPHID'], 5, 4);
}
/**
* Gets the identifier of the projets related to this task
*
* return string[] The list of project PHIDs
*/
public function getProjectsPHIDs () {
if (!array_key_exists('objectPHID', $this->data)) {
return [];
}
$objectPHID = $this->data['objectPHID'];
$objectType = $this->getObjectType();
switch ($objectType) {
case 'DREV':
return $this->getItemProjectsPHIDs(
'repository.query',
$this->getRepositoryPHID('differential.query')
);
case 'TASK':
return $this->getItemProjectsPHIDs(
'maniphest.query',
$objectPHID
);
case 'CMIT':
return $this->getItemProjectsPHIDs(
'repository.query',
$this->getRepositoryPHID('diffusion.querycommits')
);
case 'PSTE':
return $this->getItemProjectsPHIDsThroughApplicationSearch(
'paste.search',
$objectPHID
);
default:
return [];
}
}
/**
* Gets the PHID of a repository
*
* @param string $method The API method to call (e.g. differential.query)
* @return string The repository PHID or "" if not found
*/
public function getRepositoryPHID ($method) {
$objectPHID = $this->data['objectPHID'];
$api = PhabricatorAPI::forProject($this->instanceName);
$reply = $api->call(
$method,
[ 'phids[0]' => $objectPHID ]
);
if ($reply === []) {
return "";
}
return PhabricatorAPI::getFirstResult($reply)->repositoryPHID;
}
/**
* Gets the projects for a specific item
*
* @param string $method The API method to call (e.g. differential.query)
* @param string $objectPHID The object PHID to pass as method parameter
* @return string[] The list of project PHIDs
*/
public function getItemProjectsPHIDs ($method, $objectPHID) {
if (!$objectPHID) {
return [];
}
$api = PhabricatorAPI::forProject($this->instanceName);
$reply = $api->call(
$method,
[ 'phids[0]' => $objectPHID ]
);
if ($reply === []) {
return [];
}
return PhabricatorAPI::getFirstResult($reply)->projectPHIDs;
}
/**
* Gets the project for a specific item, using the new ApplicationSearch.
*
* This is a transitional method: when every Phabricator will have been
* migrated from info (generation 1) or query (generation 2) to search
* (generation 3), we'll rename it to getItemProjectsPHIDs and overwrite it.
*/
protected function getItemProjectsPHIDsThroughApplicationSearch ($method, $objectPHID) {
if (!$objectPHID) {
return [];
}
$api = PhabricatorAPI::forProject($this->instanceName);
$reply = $api->call(
$method,
[
'constraints[phids][0]' => $objectPHID,
'attachments[projects]' => 1
]
);
return PhabricatorAPI::getFirstResult($reply)->attachments->projects->projectPHIDs;
}
/**
* Gets the list of the projects associated to the story
*
* @return string[] The list of project PHIDs
*/
public function getProjects () {
if ($this->projects === null) {
$this->attachProjects();
}
return $this->projects;
}
/**
* Queries the list of the projects associated to the story
* and attached it to the projects property.
*/
public function attachProjects () {
$this->projects = [];
$PHIDs = $this->getProjectsPHIDs();
if (count($PHIDs) == 0) {
// No project is attached to the story's object
return;
}
$map = ProjectsMap::load($this->instanceName);
foreach ($PHIDs as $PHID) {
$this->projects[] = $map->getProjectName($PHID);
}
}
///
/// Static helper methods
///
/**
* Maps a field of the API reply to a property of the PhabricatorStory class
*
* @param string $key The field of the API reply
* @return string The property's name
*/
public static function mapPhabricatorFeedKey ($key) {
if ($key == "storyID") {
return "id";
}
if (starts_with($key, "story")) {
$key = substr($key, 5);
$key[0] = strtolower($key[0]); // lowercase
}
return $key;
}
}
diff --git a/tests/Console/Commands/NotificationsPayloadTest.php b/tests/Console/Commands/NotificationsPayloadTest.php
index ab81229..821889a 100644
--- a/tests/Console/Commands/NotificationsPayloadTest.php
+++ b/tests/Console/Commands/NotificationsPayloadTest.php
@@ -1,75 +1,91 @@
<?php
namespace Nasqueron\Notifications\Tests\Console\Commands;
use Nasqueron\Notifications\Console\Commands\NotificationsPayload;
class NotificationsPayloadTest extends TestCase {
/**
* @var string
*/
protected $class = 'Nasqueron\Notifications\Console\Commands\NotificationsPayload';
public function testRegularExecute () {
$path = __DIR__ . '/../../data/payloads/DockerHubPushPayload.json';
$this->tester->execute([
'command' => $this->command->getName(),
'service' => 'DockerHub',
'payload' => $path,
'args' => [
'Acme',
'push'
],
]);
$this->assertContains('"service": "DockerHub"', $this->tester->getDisplay());
$this->assertContains('"project": "Acme"', $this->tester->getDisplay());
$this->assertContains('svendowideit\/testhook', $this->tester->getDisplay());
}
+ public function testPhabricatorPayload () {
+ $path = __DIR__ . '/../../data/payloads/PhabricatorPastePayload.json';
+ $this->tester->execute([
+ 'command' => $this->command->getName(),
+ 'service' => 'Phabricator',
+ 'payload' => $path,
+ 'args' => [
+ 'Acme',
+ ],
+ ]);
+
+ $this->assertContains('"service": "Phabricator"', $this->tester->getDisplay());
+ $this->assertContains('"project": "Acme"', $this->tester->getDisplay());
+ $this->assertContains('"type": "PSTE"', $this->tester->getDisplay());
+ }
+
/**
* @expectedException InvalidArgumentException
*/
public function testArgumentsArrayCombine () {
NotificationsPayload::argumentsArrayCombine(['foo'], []);
}
public function testFileNotFound () {
$this->tester->execute([
'command' => $this->command->getName(),
'service' => 'DockerHub',
'payload' => "/tmp/not.found",
'args' => [
'Acme',
'push'
],
]);
$this->assertContains(
'File not found: /tmp/not.found',
$this->tester->getDisplay()
);
}
public function testServiceNotFound () {
$path = __DIR__ . '/../../data/payloads/DockerHubPushPayload.json';
$this->tester->execute([
'command' => $this->command->getName(),
'service' => 'InterdimensionalTeleport',
'payload' => $path,
'args' => [
'Acme',
'push'
],
]);
$this->assertContains(
'Unknown service: InterdimensionalTeleport',
$this->tester->getDisplay()
);
}
}
diff --git a/tests/Http/PayloadFullTest.php b/tests/Http/PayloadFullTest.php
index fd654a6..86973e6 100644
--- a/tests/Http/PayloadFullTest.php
+++ b/tests/Http/PayloadFullTest.php
@@ -1,172 +1,183 @@
<?php
namespace Nasqueron\Notifications\Tests;
use Keruald\Broker\BlackholeBroker;
use Nasqueron\Notifications\Features;
class PayloadFullTest extends TestCase {
public function setUp () {
parent::setUp();
$this->disableBroker();
}
/**
* Sends a GitHub ping payload to the application, with a valid signature
*/
protected function sendValidTestPayload () {
return $this->sendTestPayload('sha1=25f6cbd17ea4c6c69958b95fb88c879de4b66dcc');
}
/**
* Sends a GitHub ping payload to the application, with a valid signature
*/
protected function sendInvalidTestPayload () {
return $this->sendTestPayload('sha1=somethingwrong');
}
protected function sendTestPayload ($signature) {
$payload = file_get_contents(__DIR__ . '/../data/payloads/GitHubPingPayload.json');
$this->sendPayload(
'/gate/GitHub/Acme', // A gate existing in data/credentials.json
$payload,
'POST',
[
'X-Github-Event' => 'ping',
'X-Github-Delivery' => 'e5dd9fc7-17ac-11e5-9427-73dad6b9b17c',
'X-Hub-Signature' => $signature,
]
);
return $this;
}
/**
* Tests a GitHub gate payload.
*/
public function testPost () {
$this->sendValidTestPayload()->seeJson([
'gate' => 'GitHub',
'door' => 'Acme',
'action' => 'AMQPAction'
]);
$this->assertResponseOk();
}
/**
* Tests a DockerHub gate payload.
*/
public function testDockerHubPayload () {
$payload = file_get_contents(__DIR__ . '/../data/payloads/DockerHubPushPayload.json');
$this->sendPayload(
'/gate/DockerHub/Acme', // A gate existing in data/credentials.json
$payload,
'POST',
[]
)->seeJson([
'gate' => 'DockerHub',
'door' => 'Acme',
'action' => 'AMQPAction'
]);
$this->assertResponseOk();
}
/**
* Tests a Jenkins gate payload.
*/
public function testJenkinsPayload () {
$payload = file_get_contents(__DIR__ . '/../data/payloads/JenkinsPayload.json');
$this->sendPayload(
'/gate/Jenkins/Acme', // A gate existing in data/credentials.json
$payload,
'POST',
[]
)->seeJson([
'gate' => 'Jenkins',
'door' => 'Acme',
'action' => 'AMQPAction'
]);
$this->assertResponseOk();
}
- /**
- * Tests a Phabricator gate payload.
- */
- public function testPhabricatorPayload () {
- $data = [
+ private function getDataForPhabricatorPayloadTests () {
+ return [
'storyID' => 3849,
'storyType' => 'PhabricatorApplicationTransactionFeedStory',
'storyData[objectPHID]' => 'PHID-TASK-l34fw5wievp6n6rnvpuk',
'storyData[transactionPHIDs][PHID-XACT-TASK-by2g3dtlfq3l2wc]' => 'PHID-XACT-TASK-by2g3dtlfq3l2wc',
'storyAuthorPHID' => 'PHID-USER-fnetlprx7zdotfm2hdrz',
'storyText' => 'quux moved T123: Lorem ipsum dolor to Backlog on the Foo workboard.',
'epoch' => 1450654419,
];
+ }
+
+ /**
+ * Tests a Phabricator gate payload.
+ */
+ public function testPhabricatorPayload () {
+ $data = $this->getDataForPhabricatorPayloadTests();
$this->post('/gate/Phabricator/Acme', $data)->seeJson([
'gate' => 'Phabricator',
'door' => 'Acme',
'action' => 'AMQPAction'
]);
$this->assertResponseOk();
+ }
+
+ /**
+ * Tests a Phabricator gate payload, when the door doesn't exist.
+ */
+ public function testPhabricatorPayloadOnNotExistingDoor () {
+ $data = $this->getDataForPhabricatorPayloadTests();
$this->post('/gate/Phabricator/NotExistingDoor', $data);
$this->assertResponseStatus(404);
}
/**
* Same than testPost, but without actions report.
*/
public function testPostWithoutActionsReport () {
Features::disable("ActionsReport");
$this->sendValidTestPayload();
$this->assertEmpty($this->response->getContent());
$this->assertResponseOk();
// Let's throw an Exception at broker level.
// Without ActionsReport, the client must always receive a 200 OK.
$this->app->instance('broker', function ($app) {
// A non omnipotent instance, so it doesn't mock connect().
return new BlackholeBroker;
});
$this->sendValidTestPayload();
$this->assertEmpty($this->response->getContent());
$this->assertResponseOk();
}
/**
* Tests a GitHub gate payload.
*/
public function testInvalidSignature () {
$this->sendInvalidTestPayload()
->assertResponseStatus(403);
}
public function testBrokerIssue () {
$this->mockNotOperationalBroker();
$payload = file_get_contents(__DIR__ . '/../data/payloads/GitHubPingPayload.json');
$this->sendPayload(
'/gate/GitHub/Acme', // A gate existing in data/credentials.json
$payload,
'POST',
[
'X-Github-Event' => 'ping',
'X-Github-Delivery' => 'e5dd9fc7-17ac-11e5-9427-73dad6b9b17c',
'X-Hub-Signature' => 'sha1=25f6cbd17ea4c6c69958b95fb88c879de4b66dcc',
]
)->seeJson([
'gate' => 'GitHub',
'door' => 'Acme',
'action' => 'AMQPAction',
'type' => 'RuntimeException',
]);
$this->assertResponseStatus(503);
}
}
diff --git a/tests/data/payloads/PhabricatorPastePayload.json b/tests/data/payloads/PhabricatorPastePayload.json
new file mode 100644
index 0000000..3dae2f0
--- /dev/null
+++ b/tests/data/payloads/PhabricatorPastePayload.json
@@ -0,0 +1 @@
+{"storyID":"3850","storyType":"PhabricatorApplicationTransactionFeedStory","storyData":{"objectPHID":"PHID-PSTE-2lppddolr46bbyj6ti66","transactionPHIDs":{"PHID-XACT-PSTE-2j7d6wjobjz5ncb":"PHID-XACT-PSTE-2j7d6wjobjz5ncb","PHID-XACT-PSTE-d2g4pwcx3iuu2n7":"PHID-XACT-PSTE-d2g4pwcx3iuu2n7","PHID-XACT-PSTE-6krgu766clik3vl":"PHID-XACT-PSTE-6krgu766clik3vl","PHID-XACT-PSTE-uztpigtlxp6gmtr":"PHID-XACT-PSTE-uztpigtlxp6gmtr"}},"storyAuthorPHID":"PHID-USER-fnetlprx7zdotfm2hdrz","storyText":"dereckson updated the title for P145 `pkg audit` on Ysul.","epoch":"1450694711"}

File Metadata

Mime Type
text/x-diff
Expires
Mon, Feb 2, 15:13 (14 h, 13 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3404605
Default Alt Text
(27 KB)

Event Timeline