diff --git a/openapi.yaml b/openapi.yaml index bf75156e..531df490 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -11712,6 +11712,23 @@ paths: schema: $ref: '#/components/schemas/SlackInternalDepartmentGoalProgressConfigResponse' + /slack/config/internal-department-goal-progress/test: + post: + tags: [Config] + summary: Test Slack internal department goal progress webhook + operationId: testSlackInternalDepartmentGoalProgressWebhook + responses: + '200': + description: Slack internal department goal progress webhook test completed successfully + content: + application/json: + schema: + $ref: '#/components/schemas/SlackConfigTestResponse' + '400': + description: Slack internal department goal progress webhook URL is not configured + '502': + description: Slack internal department goal progress webhook test failed + /backups/config: get: tags: [Config] diff --git a/services/nginx/app/classes/slack.php b/services/nginx/app/classes/slack.php index c45bfdf0..27fbfc8b 100644 --- a/services/nginx/app/classes/slack.php +++ b/services/nginx/app/classes/slack.php @@ -196,6 +196,42 @@ class slack implements notification_i ]; } + /** + * Send a sanitized internal department goal progress test notification to the saved Slack webhook. + * + * @return array{configured:bool,sent:bool,message:string} + */ + public function test_internal_department_goal_progress_webhook(): array + { + $webhook = $this->get_internal_department_goal_progress_webhook_url(); + if ($webhook === '') { + return [ + 'configured' => false, + 'sent' => false, + 'message' => 'Slack internal department goal progress webhook URL is not configured.', + ]; + } + + $result = $this->send_webhook_message( + $this->format_internal_department_goal_progress_test(), + $webhook + ); + $sent = $this->is_webhook_send_successful($result); + + self::add_log($sent + ? 'Slack internal department goal progress test webhook sent successfully.' + : 'Slack internal department goal progress test webhook failed.' + ); + + return [ + 'configured' => true, + 'sent' => $sent, + 'message' => $sent + ? 'Slack test message sent successfully.' + : 'Slack test message failed.', + ]; + } + protected function get_customer_registration_webhook_url(): string { return trim((string)$this->getConfig()->customer_registration_webhook_url->getVariableValue()); @@ -287,4 +323,10 @@ class slack implements notification_i return "*Truck Wash Slack test*\n" . "Customer registration notifications are configured correctly."; } + + public function format_internal_department_goal_progress_test(): string + { + return "*Truck Wash Slack test*\n" + . "Internal department goal progress notifications are configured correctly."; + } } diff --git a/services/nginx/app/openapi.yaml b/services/nginx/app/openapi.yaml index b29736ad..17065f33 100644 --- a/services/nginx/app/openapi.yaml +++ b/services/nginx/app/openapi.yaml @@ -11712,6 +11712,23 @@ paths: schema: $ref: '#/components/schemas/SlackInternalDepartmentGoalProgressConfigResponse' + /slack/config/internal-department-goal-progress/test: + post: + tags: [Config] + summary: Test Slack internal department goal progress webhook + operationId: testSlackInternalDepartmentGoalProgressWebhook + responses: + '200': + description: Slack internal department goal progress webhook test completed successfully + content: + application/json: + schema: + $ref: '#/components/schemas/SlackConfigTestResponse' + '400': + description: Slack internal department goal progress webhook URL is not configured + '502': + description: Slack internal department goal progress webhook test failed + /backups/config: get: tags: [Config] diff --git a/services/nginx/app/routes/moduleConfigRoute.php b/services/nginx/app/routes/moduleConfigRoute.php index 8800507e..bb240b0a 100644 --- a/services/nginx/app/routes/moduleConfigRoute.php +++ b/services/nginx/app/routes/moduleConfigRoute.php @@ -250,6 +250,35 @@ class moduleConfigRoute ] ); + /** Slack internal department goal progress config > TEST */ + $this->post('/slack/config/internal-department-goal-progress/test', function () { + global $response; + $this->requirePermission('slack_config'); + $user = (new authentication())->get_user(); + if (!$user) { + (new logs_o())->add('slack_config', 'global', 1, 0, 'SLACK_INTERNAL_DEPARTMENT_GOAL_PROGRESS_CONFIG_TEST', 'No user found, or invalid session'); + $response->error('Invalid session', 400); + } + + $result = (new slack())->test_internal_department_goal_progress_webhook(); + if (($result['configured'] ?? false) !== true) { + (new logs_o())->add('slack_config', 'global', 0, $user->id, 'SLACK_INTERNAL_DEPARTMENT_GOAL_PROGRESS_CONFIG_TEST', 'Slack internal department goal progress webhook URL is not configured'); + $response->error($result['message'] ?? 'Slack internal department goal progress webhook URL is not configured.', 400); + } + + if (($result['sent'] ?? false) !== true) { + (new logs_o())->add('slack_config', 'global', 0, $user->id, 'SLACK_INTERNAL_DEPARTMENT_GOAL_PROGRESS_CONFIG_TEST', 'Slack internal department goal progress test webhook failed'); + $response->error($result['message'] ?? 'Slack test message failed.', 502); + } + + (new logs_o())->add('slack_config', 'global', 1, $user->id, 'SLACK_INTERNAL_DEPARTMENT_GOAL_PROGRESS_CONFIG_TEST', 'Successfully tested Slack internal department goal progress webhook'); + $response->success($result); + }, + [ + 'slack_config' => 'Test Slack internal department goal progress config' + ] + ); + /** Slack internal department goal progress config > GET */ $this->get('/slack/config/internal-department-goal-progress', function () { global $response; diff --git a/services/nginx/app/tests/Unit/Slack/SlackConfigRouteWiringTest.php b/services/nginx/app/tests/Unit/Slack/SlackConfigRouteWiringTest.php index 9e9260b0..93e071fd 100644 --- a/services/nginx/app/tests/Unit/Slack/SlackConfigRouteWiringTest.php +++ b/services/nginx/app/tests/Unit/Slack/SlackConfigRouteWiringTest.php @@ -8,10 +8,12 @@ it('registers Slack module config endpoints and customer registration webhook co ->and($routeContent)->toContain('/slack/config') ->and($routeContent)->toContain('/slack/config/test') ->and($routeContent)->toContain('/slack/config/internal-department-goal-progress') + ->and($routeContent)->toContain('/slack/config/internal-department-goal-progress/test') ->and($routeContent)->toContain("requirePermission('slack_config')") ->and($routeContent)->toContain("(new slack())->getConfig()->getConfigRequest()") ->and($routeContent)->toContain("(new slack())->getConfig()->postConfigRequest()") ->and($routeContent)->toContain("(new slack())->test_customer_registration_webhook()") + ->and($routeContent)->toContain("(new slack())->test_internal_department_goal_progress_webhook()") ->and($routeContent)->toContain("(new slack())->get_internal_department_goal_progress_config()") ->and($routeContent)->toContain("set_internal_department_goal_progress_config"); @@ -40,9 +42,11 @@ it('registers Slack module config endpoints and customer registration webhook co ->and($slackClassContent)->not->toBeFalse() ->and($slackClassContent)->toContain('send_customer_registration_notification') ->and($slackClassContent)->toContain('test_customer_registration_webhook') + ->and($slackClassContent)->toContain('test_internal_department_goal_progress_webhook') ->and($slackClassContent)->toContain('get_internal_department_goal_progress_config') ->and($slackClassContent)->toContain('get_internal_department_goal_progress_webhook_url') ->and($slackClassContent)->toContain('format_customer_registration_test') + ->and($slackClassContent)->toContain('format_internal_department_goal_progress_test') ->and($slackClassContent)->toContain('format_customer_registration') ->and($authRouteContent)->not->toBeFalse() ->and($authRouteContent)->toContain("AUTH_REGISTER_CVR_SLACK_NOTIFICATION_FAILED") @@ -54,6 +58,7 @@ it('registers Slack module config endpoints and customer registration webhook co ->and($openApiContent)->toContain('/slack/config') ->and($openApiContent)->toContain('/slack/config/test') ->and($openApiContent)->toContain('/slack/config/internal-department-goal-progress') + ->and($openApiContent)->toContain('/slack/config/internal-department-goal-progress/test') ->and($openApiContent)->toContain('SlackConfigListResponse') ->and($openApiContent)->toContain('SlackInternalDepartmentGoalProgressConfigResponse') ->and($openApiContent)->toContain('SlackConfigTestResponse') diff --git a/services/nginx/app/tests/Unit/Slack/SlackCustomerRegistrationWebhookTest.php b/services/nginx/app/tests/Unit/Slack/SlackCustomerRegistrationWebhookTest.php index a61a43ce..7a67f6dd 100644 --- a/services/nginx/app/tests/Unit/Slack/SlackCustomerRegistrationWebhookTest.php +++ b/services/nginx/app/tests/Unit/Slack/SlackCustomerRegistrationWebhookTest.php @@ -10,7 +10,8 @@ final class SlackCustomerRegistrationWebhookFake extends slack public function __construct( private readonly string $webhook, - private readonly string $sendResult = 'Message sent successfully. Response: ok' + private readonly string $sendResult = 'Message sent successfully. Response: ok', + private readonly ?string $internalDepartmentGoalProgressWebhook = null ) { // Skip parent config loading for unit isolation. } @@ -20,6 +21,11 @@ final class SlackCustomerRegistrationWebhookFake extends slack return $this->webhook; } + public function get_internal_department_goal_progress_webhook_url(): string + { + return $this->internalDepartmentGoalProgressWebhook ?? $this->webhook; + } + public function send_webhook_message(string $message, string $webhook): string { $this->messages[] = [ @@ -84,3 +90,63 @@ it('reports customer registration test notification failures without exposing th ->and(json_encode($slack->get_log(), JSON_UNESCAPED_SLASHES))->toContain('failed') ->and(json_encode($slack->get_log(), JSON_UNESCAPED_SLASHES))->not->toContain('secret-token'); }); + +it('does not send internal department goal progress test notifications without a saved webhook', function (): void { + $slack = new SlackCustomerRegistrationWebhookFake( + 'https://hooks.slack.test/services/customer-registration', + internalDepartmentGoalProgressWebhook: '' + ); + + $result = $slack->test_internal_department_goal_progress_webhook(); + + expect($result) + ->toBe([ + 'configured' => false, + 'sent' => false, + 'message' => 'Slack internal department goal progress webhook URL is not configured.', + ]) + ->and($slack->messages)->toBe([]); +}); + +it('sends internal department goal progress test notifications to the saved webhook', function (): void { + $slack = new SlackCustomerRegistrationWebhookFake( + 'https://hooks.slack.test/services/customer-registration', + internalDepartmentGoalProgressWebhook: 'https://hooks.slack.test/services/internal-goals-secret' + ); + + $result = $slack->test_internal_department_goal_progress_webhook(); + + expect($result) + ->toBe([ + 'configured' => true, + 'sent' => true, + 'message' => 'Slack test message sent successfully.', + ]) + ->and($slack->messages)->toHaveCount(1) + ->and($slack->messages[0]['webhook'])->toBe('https://hooks.slack.test/services/internal-goals-secret') + ->and($slack->messages[0]['message'])->toContain('Truck Wash Slack test') + ->and($slack->messages[0]['message'])->toContain('Internal department goal progress notifications are configured correctly.') + ->and(json_encode($slack->get_log(), JSON_UNESCAPED_SLASHES))->toContain('sent successfully') + ->and(json_encode($slack->get_log(), JSON_UNESCAPED_SLASHES))->not->toContain('internal-goals-secret'); +}); + +it('reports internal department goal progress test notification failures without exposing the webhook', function (): void { + $slack = new SlackCustomerRegistrationWebhookFake( + 'https://hooks.slack.test/services/customer-registration', + 'Failed to send message: cURL error for https://hooks.slack.test/services/internal-goals-secret', + 'https://hooks.slack.test/services/internal-goals-secret' + ); + + $result = $slack->test_internal_department_goal_progress_webhook(); + + expect($result) + ->toBe([ + 'configured' => true, + 'sent' => false, + 'message' => 'Slack test message failed.', + ]) + ->and($slack->messages)->toHaveCount(1) + ->and(json_encode($result, JSON_UNESCAPED_SLASHES))->not->toContain('internal-goals-secret') + ->and(json_encode($slack->get_log(), JSON_UNESCAPED_SLASHES))->toContain('failed') + ->and(json_encode($slack->get_log(), JSON_UNESCAPED_SLASHES))->not->toContain('internal-goals-secret'); +});