diff --git a/.github/workflows/matomo-tests.yml b/.github/workflows/matomo-tests.yml index ca69b70..ef8959c 100644 --- a/.github/workflows/matomo-tests.yml +++ b/.github/workflows/matomo-tests.yml @@ -1,7 +1,7 @@ # Action for running tests # This file has been automatically created. # To recreate it you can run this command -# ./console generate:test-action --plugin="Slack" --php-versions="7.2,8.4" --schedule-cron="0 5 * * 6" +# ./console generate:test-action --plugin="Slack" --php-versions="7.2,8.4" --dependent-plugins="matomo-org/plugin-CustomAlerts" --schedule-cron="0 5 * * 6" name: Plugin Slack Tests @@ -56,6 +56,9 @@ jobs: redis-service: true artifacts-pass: ${{ secrets.ARTIFACTS_PASS }} upload-artifacts: ${{ matrix.php == '7.2' && matrix.target == 'maximum_supported_matomo' }} + artifacts-protected: true + dependent-plugins: 'matomo-org/plugin-CustomAlerts' + github-token: ${{ secrets.TESTS_ACCESS_TOKEN || secrets.GITHUB_TOKEN }} UI: runs-on: ubuntu-24.04 steps: @@ -74,3 +77,6 @@ jobs: redis-service: true artifacts-pass: ${{ secrets.ARTIFACTS_PASS }} upload-artifacts: true + artifacts-protected: true + dependent-plugins: 'matomo-org/plugin-CustomAlerts' + github-token: ${{ secrets.TESTS_ACCESS_TOKEN || secrets.GITHUB_TOKEN }} diff --git a/EnrichTriggeredAlerts.php b/EnrichTriggeredAlerts.php new file mode 100644 index 0000000..7049ce7 --- /dev/null +++ b/EnrichTriggeredAlerts.php @@ -0,0 +1,26 @@ + true, - ScheduledReports::EVOLUTION_GRAPH_PARAMETER => false, - ScheduledReports::DISPLAY_FORMAT_PARAMETER => true, + ScheduledReports::EVOLUTION_GRAPH_PARAMETER => false, + ScheduledReports::DISPLAY_FORMAT_PARAMETER => true, ); private static $managedReportTypes = array( @@ -38,26 +38,28 @@ class Slack extends Plugin ); private static $managedReportFormats = array( - ReportRenderer::PDF_FORMAT => 'plugins/Morpheus/icons/dist/plugins/pdf.png', - ReportRenderer::CSV_FORMAT => 'plugins/Morpheus/images/export.png', - ReportRenderer::TSV_FORMAT => 'plugins/Morpheus/images/export.png', + ReportRenderer::PDF_FORMAT => 'plugins/Morpheus/icons/dist/plugins/pdf.png', + ReportRenderer::CSV_FORMAT => 'plugins/Morpheus/images/export.png', + ReportRenderer::TSV_FORMAT => 'plugins/Morpheus/images/export.png', ); public function registerEvents() { return [ - 'ScheduledReports.getReportParameters' => 'getReportParameters', + 'ScheduledReports.getReportParameters' => 'getReportParameters', 'ScheduledReports.validateReportParameters' => 'validateReportParameters', - 'ScheduledReports.getReportMetadata' => 'getReportMetadata', - 'ScheduledReports.getReportTypes' => 'getReportTypes', - 'ScheduledReports.getReportFormats' => 'getReportFormats', - 'ScheduledReports.getRendererInstance' => 'getRendererInstance', - 'ScheduledReports.getReportRecipients' => 'getReportRecipients', - 'ScheduledReports.processReports' => 'processReports', - 'ScheduledReports.allowMultipleReports' => 'allowMultipleReports', - 'ScheduledReports.sendReport' => 'sendReport', + 'ScheduledReports.getReportMetadata' => 'getReportMetadata', + 'ScheduledReports.getReportTypes' => 'getReportTypes', + 'ScheduledReports.getReportFormats' => 'getReportFormats', + 'ScheduledReports.getRendererInstance' => 'getRendererInstance', + 'ScheduledReports.getReportRecipients' => 'getReportRecipients', + 'ScheduledReports.processReports' => 'processReports', + 'ScheduledReports.allowMultipleReports' => 'allowMultipleReports', + 'ScheduledReports.sendReport' => 'sendReport', 'Template.reportParametersScheduledReports' => 'templateReportParametersScheduledReports', 'Translate.getClientSideTranslationKeys' => 'getClientSideTranslationKeys', + 'CustomAlerts.validateReportParameters' => 'validateCustomAlertReportParameters', + 'CustomAlerts.sendNewAlerts' => 'sendNewAlerts', ]; } @@ -74,6 +76,16 @@ public function getClientSideTranslationKeys(&$translationKeys) $translationKeys[] = 'Slack_SlackEnterYourSlackChannelIdHelpText'; } + /** + * + * Validates the Schedule Report for Slack reportType + * + * @param $parameters + * @param $reportType + * @return void + * @throws \Piwik\Exception\DI\DependencyException + * @throws \Piwik\Exception\DI\NotFoundException + */ public function validateReportParameters(&$parameters, $reportType) { if (!self::isSlackEvent($reportType)) { @@ -106,9 +118,18 @@ public function validateReportParameters(&$parameters, $reportType) } } + /** + * + * Get report metadata for Slack scheduled report + * + * @param $availableReportMetadata + * @param $reportType + * @param $idSite + * @return void + */ public function getReportMetadata(&$availableReportMetadata, $reportType, $idSite) { - if (! self::isSlackEvent($reportType)) { + if (!self::isSlackEvent($reportType)) { return; } @@ -119,11 +140,26 @@ public function getReportMetadata(&$availableReportMetadata, $reportType, $idSit ); } + /** + * + * Adds Slack as a reportType in Schedule Reports + * + * @param $reportTypes + * @return void + */ public function getReportTypes(&$reportTypes) { $reportTypes = array_merge($reportTypes, self::$managedReportTypes); } + /** + * + * Adds allowed reportTypes for Slack, e.g. PDF, CSV and TSV + * + * @param $reportFormats + * @param $reportType + * @return void + */ public function getReportFormats(&$reportFormats, $reportType) { if (self::isSlackEvent($reportType)) { @@ -131,6 +167,14 @@ public function getReportFormats(&$reportFormats, $reportType) } } + /** + * + * Adds report parameter for Slack, e.g. SlackChannelID + * + * @param $availableParameters + * @param $reportType + * @return void + */ public function getReportParameters(&$availableParameters, $reportType) { if (self::isSlackEvent($reportType)) { @@ -138,9 +182,19 @@ public function getReportParameters(&$availableParameters, $reportType) } } + /** + * + * Process the Schedule report for reportType Slack + * + * @param $processedReports + * @param $reportType + * @param $outputType + * @param $report + * @return void + */ public function processReports(&$processedReports, $reportType, $outputType, $report) { - if (! self::isSlackEvent($reportType)) { + if (!self::isSlackEvent($reportType)) { return; } @@ -151,9 +205,20 @@ public function processReports(&$processedReports, $reportType, $outputType, $re ); } + /** + * + * Sets the rendered instance based on reportFormat for Slack + * + * @param $reportRenderer + * @param $reportType + * @param $outputType + * @param $report + * @return void + * @throws \Exception + */ public function getRendererInstance(&$reportRenderer, $reportType, $outputType, $report) { - if (! self::isSlackEvent($reportType)) { + if (!self::isSlackEvent($reportType)) { return; } @@ -162,6 +227,14 @@ public function getRendererInstance(&$reportRenderer, $reportType, $outputType, $reportRenderer = ReportRenderer::factory($reportFormat); } + /** + * + * To allow multiple reports in a single file + * + * @param $allowMultipleReports + * @param $reportType + * @return void + */ public function allowMultipleReports(&$allowMultipleReports, $reportType) { if (self::isSlackEvent($reportType)) { @@ -169,6 +242,15 @@ public function allowMultipleReports(&$allowMultipleReports, $reportType) } } + /** + * + * Displays the recipients in the list of Schedule Reports + * + * @param $recipients + * @param $reportType + * @param $report + * @return void + */ public function getReportRecipients(&$recipients, $reportType, $report) { if (!self::isSlackEvent($reportType) || empty($report['parameters'][self::SLACK_CHANNEL_ID_PARAMETER])) { @@ -179,6 +261,9 @@ public function getReportRecipients(&$recipients, $reportType, $report) } /** + * + * Code to send a Schedule Report via Slack + * * @param $reportType * @param $report * @param $contents @@ -187,8 +272,11 @@ public function getReportRecipients(&$recipients, $reportType, $report) * @param $reportSubject * @param $reportTitle * @param $additionalFiles - * @param Period|null $period + * @param $period * @param $force + * @return void + * @throws \Piwik\Exception\DI\DependencyException + * @throws \Piwik\Exception\DI\NotFoundException */ public function sendReport( $reportType, @@ -202,7 +290,7 @@ public function sendReport( $period, $force ) { - if (! self::isSlackEvent($reportType)) { + if (!self::isSlackEvent($reportType)) { return; } $logger = StaticContainer::get(LoggerInterface::class); @@ -230,6 +318,16 @@ public function sendReport( $scheduleReportSlack->send(); } + /** + * + * Add the view template for Slack report parameters + * + * @param $out + * @param $context + * @return void + * @throws \Piwik\Exception\DI\DependencyException + * @throws \Piwik\Exception\DI\NotFoundException + */ public function templateReportParametersScheduledReports(&$out, $context = '') { if (Piwik::isUserIsAnonymous()) { @@ -275,6 +373,116 @@ public function uninstall() return; } + /** + * + * Validation check for CustomAlert report parameters + * + * @param $parameters + * @param $alertMedium + * @return void + * @throws \Exception + */ + public function validateCustomAlertReportParameters($parameters, $alertMedium) + { + if ($alertMedium === self::SLACK_TYPE && empty($parameters[self::SLACK_CHANNEL_ID_PARAMETER])) { + throw new \Exception(Piwik::translate('Slack_SlackChannelIdRequiredErrorMessage')); + } + } + + /** + * + * Code to send CustomAlerts via Slack + * + * @param $triggeredAlerts + * @return void + * @throws \Piwik\Exception\DI\DependencyException + * @throws \Piwik\Exception\DI\NotFoundException + */ + public function sendNewAlerts($triggeredAlerts): void + { + if (!empty($triggeredAlerts)) { + $enrichTriggerAlerts = StaticContainer::get(EnrichTriggeredAlerts::class); + $triggeredAlerts = $enrichTriggerAlerts->enrichTriggeredAlerts($triggeredAlerts); + $settings = StaticContainer::get(SystemSettings::class); + $token = $settings->slackOauthToken->getValue(); + if (empty($token)) { + return; + } + $slackApi = new SlackApi($token); + $groupedAlerts = $this->groupAlertsByChannelId($triggeredAlerts); + foreach ($groupedAlerts as $slackChannelId => $alert) { + if (!$slackApi->sendMessage(implode("\n", $alert['message']), $slackChannelId)) { + $logger = StaticContainer::get(LoggerInterface::class); + $logger->debug('Slack alert failed for following alerts: ' . implode("\n", $alert['name'])); + } + } + } + } + + /** + * + * Group alerts by slackChannelID to reduce number of network calls for multiple alerts + * + * @param array $alerts + * @return array + */ + private function groupAlertsByChannelId(array $alerts): array + { + $groupedAlerts = []; + foreach ($alerts as $alert) { + if (!in_array(self::SLACK_TYPE, $alert['report_mediums']) || empty($alert['slack_channel_id'])) { + continue; + } + $metric = !empty($alert['reportMetric']) ? $alert['reportMetric'] : $alert['metric']; + $reportName = !empty($alert['reportName']) ? $alert['reportName'] : $alert['report']; + $groupedAlerts[$alert['slack_channel_id']]['message'][] = $this->getAlertMessage($alert, $metric, $reportName); + $groupedAlerts[$alert['slack_channel_id']]['name'][] = $alert['name']; + } + + return $groupedAlerts; + } + + /** + * + * Returns the alert message to send via Slack + * + * @param array $alert + * @param string $metric + * @param string $reportName + * @return string + */ + public function getAlertMessage(array $alert, string $metric, string $reportName): string + { + return Piwik::translate('Slack_SlackAlertContent', [$alert['name'], $alert['siteName'], $metric, $reportName, $this->transformAlertCondition($alert)]); + } + + /** + * + * Transform the alert condition to text + * + * @param array $alert + * @return string + */ + private function transformAlertCondition(array $alert): string + { + switch ($alert['metric_condition']) { + case 'less_than': + return Piwik::translate('CustomAlerts_ValueIsLessThan', [$alert['metric_matched'], $alert['value_new']]); + case 'greater_than': + return Piwik::translate('CustomAlerts_ValueIsGreaterThan', [$alert['metric_matched'], $alert['value_new']]); + case 'decrease_more_than': + return Piwik::translate('CustomAlerts_ValueDecreasedMoreThan', [$alert['metric_matched'], $alert['value_old'] ?? '-', $alert['value_new']]); + case 'increase_more_than': + return Piwik::translate('CustomAlerts_ValueIncreasedMoreThan', [$alert['metric_matched'], $alert['value_old'] ?? '-', $alert['value_new']]); + case 'percentage_decrease_more_than': + return Piwik::translate('CustomAlerts_ValuePercentageDecreasedMoreThan', [$alert['metric_matched'], $alert['value_old'] ?? '-', $alert['value_new']]); + case 'percentage_increase_more_than': + return Piwik::translate('CustomAlerts_ValuePercentageIncreasedMoreThan', [$alert['metric_matched'], $alert['value_old'] ?? '-', $alert['value_new']]); + } + + return ''; + } + private function reportAlreadySent($report, Period $period) { $key = ScheduledReports::OPTION_KEY_LAST_SENT_DATERANGE . $report['idreport']; diff --git a/SlackApi.php b/SlackApi.php index d47d8f4..02e6c3c 100644 --- a/SlackApi.php +++ b/SlackApi.php @@ -34,6 +34,7 @@ class SlackApi private const SLACK_UPLOAD_URL_EXTERNAL = 'https://slack.com/api/files.getUploadURLExternal'; private const SLACK_COMPLETE_UPLOAD_EXTERNAL = 'https://slack.com/api/files.completeUploadExternal'; + private const SLACK_POST_MESSAGE_URL = 'https://slack.com/api/chat.postMessage'; private const SLACK_TIMEOUT = 5000; @@ -64,6 +65,14 @@ public function uploadFile(string $subject, string $fileName, string $fileConten return false; } + /** + * + * Get the URL to upload the file + * + * @param string $fileName + * @param int $fileLength + * @return string + */ public function getUploadURLExternal(string $fileName, int $fileLength): string { try { @@ -92,6 +101,14 @@ public function getUploadURLExternal(string $fileName, int $fileLength): string return ''; } + /** + * + * Upload the file contents to the URL received from getUploadURLExternal method + * + * @param string $uploadURL + * @param string $fileContents + * @return bool + */ public function sendFile(string $uploadURL, string $fileContents): bool { try { @@ -110,6 +127,15 @@ public function sendFile(string $uploadURL, string $fileContents): bool return strtolower($response) === ('ok - ' . strlen($fileContents)); } + + /** + * + * Post the uploaded file to a channel + * + * @param string $channel + * @param string $subject + * @return bool + */ public function completeUploadExternal(string $channel, string $subject): bool { try { @@ -134,7 +160,55 @@ public function completeUploadExternal(string $channel, string $subject): bool return !empty($data['ok']); } - private function sendHttpRequest(string $url, int $timeout, array $requestBody, array $additionalHeaders = [], $requestBodyAsString = false) + /** + * + * Send a text message to a Slack channel + * + * @param string $message + * @param string $channel + * @return bool + */ + public function sendMessage(string $message, string $channel): bool + { + if (empty($message) || empty($channel)) { + $this->logger->debug('Empty message or channel for sending message'); + return false; + } + + try { + $response = $this->sendHttpRequest( + self::SLACK_POST_MESSAGE_URL, + self::SLACK_TIMEOUT, + [ + 'token' => $this->token, + 'channel' => $channel, + 'text' => $message, + ], + ['Content-Type' => 'multipart/form-data'] + ); + } catch (\Exception $e) { + $this->logger->debug('Slack error sendMessage:' . $e->getMessage()); + return false; + } + + $data = json_decode($response, true); + + return !empty($data['ok']); + } + + /** + * + * Wrapper to send HTTP request + * + * @param string $url + * @param int $timeout + * @param array $requestBody + * @param array $additionalHeaders + * @param $requestBodyAsString + * @return array|bool|int[]|string + * @throws \Exception + */ + public function sendHttpRequest(string $url, int $timeout, array $requestBody, array $additionalHeaders = [], $requestBodyAsString = false) { if ($requestBodyAsString && !empty($requestBody[0])) { $requestBody = $requestBody[0]; diff --git a/lang/en.json b/lang/en.json index c990685..f3e19b0 100644 --- a/lang/en.json +++ b/lang/en.json @@ -8,6 +8,7 @@ "PleaseFindYourReport": "Here is your %1$s report for %2$s", "SlackChannelIdRequiredErrorMessage": "Slack Channel ID cannot be empty.", "SlackChannel": "Slack Channel", - "SlackEnterYourSlackChannelIdHelpText": "Enter the Slack Channel ID of the channel that will receive these reports. To find the ID, go to Slack and open the channel details > About tab. %1$sLearn more%2$s" + "SlackEnterYourSlackChannelIdHelpText": "Enter the Slack Channel ID of the channel that will receive these reports. To find the ID, go to Slack and open the channel details > About tab. %1$sLearn more%2$s", + "SlackAlertContent": "%1$s has been triggered for website %2$s as the metric %3$s in report %4$s %5$s." } } \ No newline at end of file diff --git a/tests/Integration/CustomAlertsApiTest.php b/tests/Integration/CustomAlertsApiTest.php new file mode 100644 index 0000000..19acdec --- /dev/null +++ b/tests/Integration/CustomAlertsApiTest.php @@ -0,0 +1,121 @@ +extraPluginsToLoad = array('CustomAlerts'); + TestingEnvironmentManipulator::$extraPluginsToLoad = self::$fixture->extraPluginsToLoad; + + parent::setUp(); + + $pluginManager = \Piwik\Plugin\Manager::getInstance(); + $pluginManager->loadPlugin('CustomAlerts'); + $pluginManager->installLoadedPlugins(); + $pluginManager->activatePlugin('CustomAlerts'); + $this->api = API::getInstance(); + $this->idSite = Fixture::createWebsite('2012-08-09 11:22:33'); + } + + public function testAddAlertShouldThrowExceptionIfEmptySlackChannelId() + { + $this->expectException(\Exception::class); + $this->expectExceptionMessage('Slack_SlackChannelIdRequiredErrorMessage'); + $this->addAlert(); + } + + public function testAddAlertSuccess() + { + $id = $this->addAlert($slackChannelId = 'channelID'); + $this->assertEquals(1, $id); + $alert = $this->api->getAlert($id); + $this->assertEquals(['email','slack'], $alert['report_mediums']); + $this->assertEquals($slackChannelId, $alert['slack_channel_id']); + } + + public function testUpdateAlertShouldThrowExceptionIfEmptySlackChannelId() + { + $this->expectException(\Exception::class); + $this->expectExceptionMessage('Slack_SlackChannelIdRequiredErrorMessage'); + $idAlert = $this->addAlert('channelID'); + $this->updateAlert($idAlert); + } + + public function testUpdateAlertSuccess() + { + $idAlert = $this->addAlert('channelID'); + $this->updateAlert($idAlert, 'channelIDNew'); + $alert = $this->api->getAlert($idAlert); + $this->assertEquals(['email','slack'], $alert['report_mediums']); + $this->assertEquals('channelIDNew', $alert['slack_channel_id']); + } + + private function addAlert($slackChannelId = '') + { + return $this->api->addAlert( + 'Test Slack and Email', + $this->idSite, + 'day', + 1, + [], + [], + 'nb_visits', + 'less_than', + $metricMatched = 5, + 1, + 'MultiSites_getOne', + 'matches_exactly', + 'Piwik', + ['email', 'slack'], + $slackChannelId + ); + } + + private function updateAlert($idAlert, $slackChannelId = '') + { + return $this->api->editAlert( + $idAlert, + 'Test Slack and Email', + $this->idSite, + 'day', + 1, + [], + [], + 'nb_visits', + 'less_than', + $metricMatched = 5, + 1, + 'MultiSites_getOne', + 'matches_exactly', + 'Piwik', + ['email', 'slack'], + $slackChannelId + ); + } +} diff --git a/tests/Integration/CustomAlertsTest.php b/tests/Integration/CustomAlertsTest.php new file mode 100644 index 0000000..500169e --- /dev/null +++ b/tests/Integration/CustomAlertsTest.php @@ -0,0 +1,44 @@ +extraPluginsToLoad = array('CustomAlerts'); + TestingEnvironmentManipulator::$extraPluginsToLoad = self::$fixture->extraPluginsToLoad; + + parent::setUp(); + + $pluginManager = \Piwik\Plugin\Manager::getInstance(); + $pluginManager->loadPlugin('CustomAlerts'); + $pluginManager->installLoadedPlugins(); + $pluginManager->activatePlugin('CustomAlerts'); + } + public function testGetReportMediumOptions() + { + $this->assertEquals([ + ['key' => 'email', 'value' => 'CustomAlerts_MediumEmail', 'disabled' => false], + ['key' => 'mobile', 'value' => 'CustomAlerts_MediumMobile', 'disabled' => false], + ['key' => 'slack', 'value' => 'CustomAlerts_MediumSlack', 'disabled' => false], + ], CustomAlerts::getReportMediumOptions()); + } +} diff --git a/tests/Integration/SlackApiTest.php b/tests/Integration/SlackApiTest.php index 24c0736..d27fa49 100644 --- a/tests/Integration/SlackApiTest.php +++ b/tests/Integration/SlackApiTest.php @@ -92,4 +92,30 @@ public function testSendSuccess() $this->assertTrue($helperMock->uploadFile('test file', 'test.csv', "a,b,c", 'channelID')); } + + public function testSendMessageShouldReturnFalse() + { + $helperMock = $this->getMockBuilder(SlackApi::class) + ->setMethods([ + 'sendHttpRequest', + ]) + ->setConstructorArgs(array('token')) + ->getMock(); + $helperMock->expects($this->once())->method('sendHttpRequest')->willReturn(''); + + $this->assertFalse($helperMock->sendMessage('test message', 'channelID')); + } + + public function testSendMessageShouldReturnTrue() + { + $helperMock = $this->getMockBuilder(SlackApi::class) + ->setMethods([ + 'sendHttpRequest', + ]) + ->setConstructorArgs(array('token')) + ->getMock(); + $helperMock->expects($this->once())->method('sendHttpRequest')->willReturn('{"ok":true,"channel":"channelID"}'); + + $this->assertTrue($helperMock->sendMessage('test message', 'channelID')); + } } diff --git a/tests/Integration/SlackTest.php b/tests/Integration/SlackTest.php index de6dd6f..95258c3 100644 --- a/tests/Integration/SlackTest.php +++ b/tests/Integration/SlackTest.php @@ -91,6 +91,28 @@ public function testAddReportFailureWhenInvalidReportType() $this->addReport($userLogin = 'userlogin', $idSite = 3, 'html'); } + /** + * @dataProvider getAlertData + */ + public function testGetAlertMessage($alert, $expectedMessage) + { + $slack = StaticContainer::get(Slack::class); + Fixture::loadAllTranslations(); + $this->assertSame($expectedMessage, $slack->getAlertMessage($alert, 'Unique Visitors', 'Visits Summary')); + } + + public function getAlertData() + { + return [ + [['name' => 'Alert1', 'siteName' => 'test.com', 'metric_condition' => 'less_than', 'metric_matched' => '5', 'value_new' => '1', 'value_old' => '2'], 'Alert1 has been triggered for website test.com as the metric Unique Visitors in report Visits Summary is 1 which is less than 5.'], + [['name' => 'Alert2', 'siteName' => 'test.com', 'metric_condition' => 'greater_than', 'metric_matched' => '8', 'value_new' => '10', 'value_old' => '2'], 'Alert2 has been triggered for website test.com as the metric Unique Visitors in report Visits Summary is 10 which is greater than 8.'], + [['name' => 'Alert3', 'siteName' => 'test.com', 'metric_condition' => 'decrease_more_than', 'metric_matched' => '5', 'value_new' => '8', 'value_old' => '15'], 'Alert3 has been triggered for website test.com as the metric Unique Visitors in report Visits Summary decreased more than 5 from 15 to 8.'], + [['name' => 'Alert4', 'siteName' => 'test.com', 'metric_condition' => 'increase_more_than', 'metric_matched' => '5', 'value_new' => '30', 'value_old' => '20'], 'Alert4 has been triggered for website test.com as the metric Unique Visitors in report Visits Summary increased more than 5 from 20 to 30.'], + [['name' => 'Alert5', 'siteName' => 'test.com', 'metric_condition' => 'percentage_decrease_more_than', 'metric_matched' => '5', 'value_new' => '8', 'value_old' => '15'], 'Alert5 has been triggered for website test.com as the metric Unique Visitors in report Visits Summary decreased more than 5% from 15 to 8.'], + [['name' => 'Alert6', 'siteName' => 'test.com', 'metric_condition' => 'percentage_increase_more_than', 'metric_matched' => '5', 'value_new' => '30', 'value_old' => '20'], 'Alert6 has been triggered for website test.com as the metric Unique Visitors in report Visits Summary increased more than 5% from 20 to 30.'], + ]; + } + private function assertHasReport($login, $idSite) { $report = $this->getReport($login, $idSite); diff --git a/tests/UI/CustomAlerts_spec.js b/tests/UI/CustomAlerts_spec.js new file mode 100644 index 0000000..72eca20 --- /dev/null +++ b/tests/UI/CustomAlerts_spec.js @@ -0,0 +1,99 @@ +/*! + * Matomo - free/libre analytics platform + * + * Screenshot integration tests. + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + + describe("CustomAlerts", function () { + this.fixture = "Piwik\\Tests\\Fixtures\\EmptySite"; + + // required to ensure no provider is set initially + this.optionsOverride = { + 'persist-fixture-data': false, + }; + + async function captureScreen(screenshotName, theTest, selector) { + if (!selector) { + selector = reportSelector; + } + await theTest(); + await page.waitForNetworkIdle(); + await page.waitForSelector(selector); + expect(await page.screenshotSelector(selector)).to.matchImage(screenshotName); + } + + before(function () { + testEnvironment.pluginsToLoad = ['CustomAlerts', 'Slack']; + testEnvironment.save(); + }); + + it('should load the custom alerts as empty', async function () { + const selector = '.page'; + await captureScreen('empty_report', async () => { + await page.goto('?module=CustomAlerts&action=index&idSite=1&period=day&date=yesterday'); + }, selector); + }); + + it('should show load a new alert add screen', async function () { + const selector = '.page'; + await captureScreen('new_custom_alert', async () => { + await page.evaluate(() => $('.icon-add').click()); + }, selector); + }); + + it('should show send alert via Slack as an option enabled', async function () { + const selector = '.page'; + await captureScreen('send_via_slack_new', async () => { + await page.click('.report-mediums .select-dropdown'); + await page.waitForNetworkIdle(); + await page.waitForTimeout(350); // wait for animation + }, selector); + await page.click('.report-mediums .select-dropdown'); + }); + + it('should show slack channel ID input as disabled as Oauth token not configured', async function () { + const selector = '.page'; + await captureScreen('slack_report_disabled', async () => { + await page.evaluate(() => $('.report-mediums .select-wrapper ul li:contains("Slack")').click()); + }, selector); + }); + + it('should show slack channel ID input as enabled when Oauth token configured', async function () { + const selector = '.page'; + testEnvironment.configOverride.Slack = {slackOauthToken: 'token'}; + testEnvironment.save(); + await page.goto('?module=CustomAlerts&action=addNewAlert&idSite=1&period=day&date=yesterday'); + await page.waitForNetworkIdle(); + await captureScreen('slack_report_enabled', async () => { + await page.evaluate(() => $('.report-mediums .select-wrapper ul li:contains("Slack")').click()); + }, selector); + }); + + it('should show show error if channelID not set', async function () { + const selector = '.page'; + testEnvironment.configOverride.Slack = {slackOauthToken: 'token'}; + testEnvironment.save(); + await captureScreen('slack_report_error', async () => { + await page.type('#alertName', 'Test Slack Alert'); + await page.evaluate(() => $('.conditionAndValue .select-wrapper ul li:last').click()); + await page.type('#metricValue', '2'); + await page.click('.matomo-save-button input.btn'); + await page.waitForNetworkIdle(); + }, selector); + }); + + it('should save a report successfully', async function () { + const selector = '.page'; + testEnvironment.configOverride.Slack = {slackOauthToken: 'token'}; + testEnvironment.save(); + await captureScreen('slack_alert_report_save_success', async () => { + await page.type('input#channelID', 'ChannelID'); + await page.click('.matomo-save-button input.btn'); + await page.waitForNetworkIdle(); + }, selector); + }); + + }); \ No newline at end of file diff --git a/tests/UI/expected-ui-screenshots/CustomAlerts_empty_report.png b/tests/UI/expected-ui-screenshots/CustomAlerts_empty_report.png new file mode 100644 index 0000000..13409b5 Binary files /dev/null and b/tests/UI/expected-ui-screenshots/CustomAlerts_empty_report.png differ diff --git a/tests/UI/expected-ui-screenshots/CustomAlerts_new_custom_alert.png b/tests/UI/expected-ui-screenshots/CustomAlerts_new_custom_alert.png new file mode 100644 index 0000000..701dd5c Binary files /dev/null and b/tests/UI/expected-ui-screenshots/CustomAlerts_new_custom_alert.png differ diff --git a/tests/UI/expected-ui-screenshots/CustomAlerts_send_via_slack_new.png b/tests/UI/expected-ui-screenshots/CustomAlerts_send_via_slack_new.png new file mode 100644 index 0000000..0003f9c Binary files /dev/null and b/tests/UI/expected-ui-screenshots/CustomAlerts_send_via_slack_new.png differ diff --git a/tests/UI/expected-ui-screenshots/CustomAlerts_slack_alert_report_save_success.png b/tests/UI/expected-ui-screenshots/CustomAlerts_slack_alert_report_save_success.png new file mode 100644 index 0000000..b979a8e Binary files /dev/null and b/tests/UI/expected-ui-screenshots/CustomAlerts_slack_alert_report_save_success.png differ diff --git a/tests/UI/expected-ui-screenshots/CustomAlerts_slack_report_disabled.png b/tests/UI/expected-ui-screenshots/CustomAlerts_slack_report_disabled.png new file mode 100644 index 0000000..c0c9acc Binary files /dev/null and b/tests/UI/expected-ui-screenshots/CustomAlerts_slack_report_disabled.png differ diff --git a/tests/UI/expected-ui-screenshots/CustomAlerts_slack_report_enabled.png b/tests/UI/expected-ui-screenshots/CustomAlerts_slack_report_enabled.png new file mode 100644 index 0000000..ff6b989 Binary files /dev/null and b/tests/UI/expected-ui-screenshots/CustomAlerts_slack_report_enabled.png differ diff --git a/tests/UI/expected-ui-screenshots/CustomAlerts_slack_report_error.png b/tests/UI/expected-ui-screenshots/CustomAlerts_slack_report_error.png new file mode 100644 index 0000000..71df2c0 Binary files /dev/null and b/tests/UI/expected-ui-screenshots/CustomAlerts_slack_report_error.png differ