diff --git a/plugins/splunk-on-call/src/components/Escalation/Escalation.test.tsx b/plugins/splunk-on-call/src/components/Escalation/Escalation.test.tsx
new file mode 100644
index 0000000000..305d29b8b8
--- /dev/null
+++ b/plugins/splunk-on-call/src/components/Escalation/Escalation.test.tsx
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import React from 'react';
+import { render, waitFor } from '@testing-library/react';
+import { EscalationPolicy } from './EscalationPolicy';
+import { wrapInTestApp } from '@backstage/test-utils';
+import { User } from '../types';
+import { ApiProvider, ApiRegistry } from '@backstage/core';
+import { splunkOnCallApiRef } from '../../api';
+
+const MOCKED_USER: User = {
+ createdAt: '2021-02-01T23:38:38Z',
+ displayName: 'Test User',
+ email: 'test@example.com',
+ firstName: 'FirstNameTest',
+ lastName: 'LastNameTest',
+ passwordLastUpdated: '2021-02-01T23:38:38Z',
+ username: 'test_user',
+ verified: true,
+ _selfUrl: '/api-public/v1/user/test_user',
+};
+
+const MOCKED_ON_CALL = [
+ {
+ team: { name: 'team_example', slug: 'team-zEalMCgwYSA0Lt40' },
+ oncallNow: [
+ {
+ escalationPolicy: { name: 'Example', slug: 'team-zEalMCgwYSA0Lt40' },
+ users: [{ onCalluser: { username: 'test_user' } }],
+ },
+ ],
+ },
+];
+
+const mockSplunkOnCallApi = {
+ getOnCallUsers: () => [],
+};
+const apis = ApiRegistry.from([[splunkOnCallApiRef, mockSplunkOnCallApi]]);
+
+describe('Escalation', () => {
+ it('Handles an empty response', async () => {
+ mockSplunkOnCallApi.getOnCallUsers = jest
+ .fn()
+ .mockImplementationOnce(async () => []);
+
+ const { getByText, queryByTestId } = render(
+ wrapInTestApp(
+
+
+ ,
+ ),
+ );
+ await waitFor(() => !queryByTestId('progress'));
+
+ expect(getByText('Empty escalation policy')).toBeInTheDocument();
+ expect(mockSplunkOnCallApi.getOnCallUsers).toHaveBeenCalled();
+ });
+
+ it('Render a list of users', async () => {
+ mockSplunkOnCallApi.getOnCallUsers = jest
+ .fn()
+ .mockImplementationOnce(async () => MOCKED_ON_CALL);
+
+ const { getByText, queryByTestId } = render(
+ wrapInTestApp(
+
+
+ ,
+ ),
+ );
+ await waitFor(() => !queryByTestId('progress'));
+
+ expect(getByText('FirstNameTest LastNameTest')).toBeInTheDocument();
+ expect(getByText('test@example.com')).toBeInTheDocument();
+ expect(mockSplunkOnCallApi.getOnCallUsers).toHaveBeenCalled();
+ });
+
+ it('Handles errors', async () => {
+ mockSplunkOnCallApi.getOnCallUsers = jest
+ .fn()
+ .mockRejectedValueOnce(new Error('Error message'));
+
+ const { getByText, queryByTestId } = render(
+ wrapInTestApp(
+
+
+ ,
+ ),
+ );
+ await waitFor(() => !queryByTestId('progress'));
+
+ expect(
+ getByText('Error encountered while fetching information. Error message'),
+ ).toBeInTheDocument();
+ });
+});
diff --git a/plugins/splunk-on-call/src/components/Incident/Incidents.test.tsx b/plugins/splunk-on-call/src/components/Incident/Incidents.test.tsx
new file mode 100644
index 0000000000..51bbe50aef
--- /dev/null
+++ b/plugins/splunk-on-call/src/components/Incident/Incidents.test.tsx
@@ -0,0 +1,150 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import React from 'react';
+import { render, waitFor } from '@testing-library/react';
+import { Incidents } from './Incidents';
+import { wrapInTestApp } from '@backstage/test-utils';
+import {
+ ApiProvider,
+ ApiRegistry,
+ IdentityApi,
+ identityApiRef,
+} from '@backstage/core';
+import { splunkOnCallApiRef } from '../../api';
+import { Incident, Team } from '../types';
+
+const MOCK_INCIDENT: Incident = {
+ alertCount: 1,
+ currentPhase: 'ACKED',
+ entityDisplayName: 'test-incident',
+ entityId: 'entityId',
+ entityState: 'CRITICAL',
+ entityType: 'SERVICE',
+ incidentNumber: '1',
+ lastAlertId: 'lastAlertId',
+ lastAlertTime: '2021-02-03T00:13:11Z',
+ routingKey: 'routingdefault',
+ service: 'test',
+ startTime: '2021-02-03T00:13:11Z',
+ pagedTeams: ['team-O9SqT13fsnCstjMi'],
+ pagedUsers: [],
+ pagedPolicies: [
+ {
+ policy: {
+ name: 'Generated Direct User Policy for test',
+ slug: 'directUserPolicySlug-test',
+ _selfUrl: '/test',
+ },
+ },
+ ],
+ transitions: [{ name: 'ACKED', at: '2021-02-03T01:20:00Z', by: 'test' }],
+ monitorName: 'vouser-user',
+ monitorType: 'Manual',
+ firstAlertUuid: 'firstAlertUuid',
+ incidentLink: 'https://portal.victorops.com/example',
+};
+
+const MOCK_TEAM: Team = {
+ _selfUrl: '/api-public/v1/team/team-O9SqT13fsnCstjMi',
+ _membersUrl: '/api-public/v1/team/team-O9SqT13fsnCstjMi/members',
+ _policiesUrl: '/api-public/v1/team/team-O9SqT13fsnCstjMi/policies',
+ _adminsUrl: '/api-public/v1/team/team-O9SqT13fsnCstjMi/admins',
+ name: 'test',
+ slug: 'team-O9SqT13fsnCstjMi',
+ memberCount: 1,
+ version: 1,
+ isDefaultTeam: false,
+};
+
+const mockIdentityApi: Partial = {
+ getUserId: () => 'test',
+};
+
+const mockSplunkOnCallApi = {
+ getIncidents: () => [],
+ getTeams: () => [],
+};
+const apis = ApiRegistry.from([
+ [identityApiRef, mockIdentityApi],
+ [splunkOnCallApiRef, mockSplunkOnCallApi],
+]);
+
+describe('Incidents', () => {
+ it('Renders an empty state when there are no incidents', async () => {
+ mockSplunkOnCallApi.getTeams = jest
+ .fn()
+ .mockImplementationOnce(async () => [MOCK_TEAM]);
+
+ const { getByText, queryByTestId } = render(
+ wrapInTestApp(
+
+
+ ,
+ ),
+ );
+ await waitFor(() => !queryByTestId('progress'));
+ expect(getByText('Nice! No incidents found!')).toBeInTheDocument();
+ });
+
+ it('Renders all incidents', async () => {
+ mockSplunkOnCallApi.getIncidents = jest
+ .fn()
+ .mockImplementationOnce(async () => [MOCK_INCIDENT]);
+
+ mockSplunkOnCallApi.getTeams = jest
+ .fn()
+ .mockImplementationOnce(async () => [MOCK_TEAM]);
+ const {
+ getByText,
+ getByTitle,
+ getAllByTitle,
+ getByLabelText,
+ queryByTestId,
+ } = render(
+ wrapInTestApp(
+
+
+ ,
+ ),
+ );
+ await waitFor(() => !queryByTestId('progress'));
+ expect(getByText('user')).toBeInTheDocument();
+ expect(getByText('test-incident')).toBeInTheDocument();
+ expect(getByTitle('ACKED')).toBeInTheDocument();
+ expect(getByLabelText('Status warning')).toBeInTheDocument();
+
+ // assert links, mailto and hrefs, date calculation
+ expect(getAllByTitle('View in Splunk On-Call').length).toEqual(1);
+ });
+
+ it('Handle errors', async () => {
+ mockSplunkOnCallApi.getIncidents = jest
+ .fn()
+ .mockRejectedValueOnce(new Error('Error occurred'));
+
+ const { getByText, queryByTestId } = render(
+ wrapInTestApp(
+
+
+ ,
+ ),
+ );
+ await waitFor(() => !queryByTestId('progress'));
+ expect(
+ getByText('Error encountered while fetching information. Error occurred'),
+ ).toBeInTheDocument();
+ });
+});
diff --git a/plugins/splunk-on-call/src/components/SplunkOnCallCard.test.tsx b/plugins/splunk-on-call/src/components/SplunkOnCallCard.test.tsx
new file mode 100644
index 0000000000..9ca61614a4
--- /dev/null
+++ b/plugins/splunk-on-call/src/components/SplunkOnCallCard.test.tsx
@@ -0,0 +1,147 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import React from 'react';
+import { render, waitFor, fireEvent, act } from '@testing-library/react';
+import { SplunkOnCallCard } from './SplunkOnCallCard';
+import { Entity } from '@backstage/catalog-model';
+import { wrapInTestApp } from '@backstage/test-utils';
+import {
+ alertApiRef,
+ ApiProvider,
+ ApiRegistry,
+ createApiRef,
+} from '@backstage/core';
+import {
+ splunkOnCallApiRef,
+ UnauthorizedError,
+ SplunkOnCallClient,
+} from '../api';
+import { User } from './types';
+
+const MOCKED_USER: User = {
+ createdAt: '2021-02-01T23:38:38Z',
+ displayName: 'Test User',
+ email: 'remi.d45@gmail.com',
+ firstName: 'FirstNameTest',
+ lastName: 'LastNameTest',
+ passwordLastUpdated: '2021-02-01T23:38:38Z',
+ username: 'test_user',
+ verified: true,
+ _selfUrl: '/api-public/v1/user/test_user',
+};
+
+const mockSplunkOnCallApi: Partial = {
+ getUsers: async () => [],
+};
+
+const apis = ApiRegistry.from([
+ [splunkOnCallApiRef, mockSplunkOnCallApi],
+ [
+ alertApiRef,
+ createApiRef({
+ id: 'core.alert',
+ description: 'Used to report alerts and forward them to the app',
+ }),
+ ],
+]);
+const entity: Entity = {
+ apiVersion: 'backstage.io/v1alpha1',
+ kind: 'Component',
+ metadata: {
+ name: 'splunkoncall-test',
+ annotations: {
+ 'splunkoncall.com/integration-key': 'abc123',
+ },
+ },
+};
+
+describe('PageDutyCard', () => {
+ it('Render splunkoncall', async () => {
+ mockSplunkOnCallApi.getUsers = jest
+ .fn()
+ .mockImplementationOnce(async () => [MOCKED_USER]);
+
+ const { getByText, queryByTestId } = render(
+ wrapInTestApp(
+
+
+ ,
+ ),
+ );
+ await waitFor(() => !queryByTestId('progress'));
+ expect(getByText('Create Incident')).toBeInTheDocument();
+ expect(getByText('Nice! No incidents found!')).toBeInTheDocument();
+ expect(getByText('Empty escalation policy')).toBeInTheDocument();
+ });
+
+ it('Handles custom error for missing token', async () => {
+ mockSplunkOnCallApi.getUsers = jest
+ .fn()
+ .mockRejectedValueOnce(new UnauthorizedError());
+
+ const { getByText, queryByTestId } = render(
+ wrapInTestApp(
+
+
+ ,
+ ),
+ );
+ await waitFor(() => !queryByTestId('progress'));
+ expect(
+ getByText('Missing or invalid SplunkOnCall Token'),
+ ).toBeInTheDocument();
+ });
+
+ it('handles general error', async () => {
+ mockSplunkOnCallApi.getUsers = jest
+ .fn()
+ .mockRejectedValueOnce(new Error('An error occurred'));
+ const { getByText, queryByTestId } = render(
+ wrapInTestApp(
+
+
+ ,
+ ),
+ );
+ await waitFor(() => !queryByTestId('progress'));
+
+ expect(
+ getByText(
+ 'Error encountered while fetching information. An error occurred',
+ ),
+ ).toBeInTheDocument();
+ });
+ it('opens the dialog when trigger button is clicked', async () => {
+ mockSplunkOnCallApi.getUsers = jest
+ .fn()
+ .mockImplementationOnce(async () => [MOCKED_USER]);
+
+ const { getByText, queryByTestId, getByTestId, getByRole } = render(
+ wrapInTestApp(
+
+
+ ,
+ ),
+ );
+ await waitFor(() => !queryByTestId('progress'));
+ expect(getByText('Create Incident')).toBeInTheDocument();
+ const triggerButton = getByTestId('trigger-button');
+ await act(async () => {
+ fireEvent.click(triggerButton);
+ });
+ expect(getByRole('dialog')).toBeInTheDocument();
+ });
+});
diff --git a/plugins/splunk-on-call/src/components/TriggerDialog/TriggerDialog.test.tsx b/plugins/splunk-on-call/src/components/TriggerDialog/TriggerDialog.test.tsx
new file mode 100644
index 0000000000..8ffe55fe01
--- /dev/null
+++ b/plugins/splunk-on-call/src/components/TriggerDialog/TriggerDialog.test.tsx
@@ -0,0 +1,128 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import React from 'react';
+import { render, fireEvent, act } from '@testing-library/react';
+import { wrapInTestApp } from '@backstage/test-utils';
+import {
+ ApiRegistry,
+ alertApiRef,
+ createApiRef,
+ ApiProvider,
+ IdentityApi,
+ identityApiRef,
+} from '@backstage/core';
+import { splunkOnCallApiRef } from '../../api';
+import { TriggerDialog } from './TriggerDialog';
+import { User } from '../types';
+
+const MOCKED_USER: User = {
+ createdAt: '2021-02-01T23:38:38Z',
+ displayName: 'Test User',
+ email: 'remi.d45@gmail.com',
+ firstName: 'FirstNameTest',
+ lastName: 'LastNameTest',
+ passwordLastUpdated: '2021-02-01T23:38:38Z',
+ username: 'test_user',
+ verified: true,
+ _selfUrl: '/api-public/v1/user/test_user',
+};
+
+describe('TriggerDialog', () => {
+ const mockIdentityApi: Partial = {
+ getUserId: () => 'guest@example.com',
+ };
+
+ const mockTriggerAlarmFn = jest.fn();
+ const mockSplunkOnCallApi = {
+ triggerAlarm: mockTriggerAlarmFn,
+ getEscalationPolicies: async () => ({
+ policies: [
+ {
+ policy: {
+ name: 'Example',
+ slug: 'team-zEalMCgwYSA0Lt40',
+ _selfUrl: '/api-public/v1/policies/team-zEalMCgwYSA0Lt40',
+ },
+ team: { name: 'Example', slug: 'team-zEalMCgwYSA0Lt40' },
+ },
+ ],
+ }),
+ };
+
+ const apis = ApiRegistry.from([
+ [
+ alertApiRef,
+ createApiRef({
+ id: 'core.alert',
+ description: 'Used to report alerts and forward them to the app',
+ }),
+ ],
+ [identityApiRef, mockIdentityApi],
+ [splunkOnCallApiRef, mockSplunkOnCallApi],
+ ]);
+
+ it('open the dialog and trigger an alarm', async () => {
+ const { getByText, getByRole, getByTestId } = render(
+ wrapInTestApp(
+
+ {}}
+ users={[MOCKED_USER]}
+ onIncidentCreated={() => {}}
+ />
+ ,
+ ),
+ );
+
+ expect(getByRole('dialog')).toBeInTheDocument();
+ expect(
+ getByText('This action will trigger an incident', {
+ exact: false,
+ }),
+ ).toBeInTheDocument();
+ const summary = getByTestId('trigger-summary-input');
+ const body = getByTestId('trigger-body-input');
+ const userTarget = getByTestId('trigger-select-user-target');
+ const behavior = getByTestId('trigger-select-behavior');
+ const policiesTarget = getByTestId('trigger-select-policies-target');
+ const description = 'Test Trigger Alarm';
+ await act(async () => {
+ fireEvent.change(summary, { target: { value: description } });
+ fireEvent.change(body, { target: { value: description } });
+ fireEvent.change(userTarget, { target: { value: ['test_user'] } });
+ fireEvent.change(behavior, { target: { value: '0' } });
+ fireEvent.change(policiesTarget, {
+ target: { value: ['team-zEalMCgwYSA0Lt40'] },
+ });
+ });
+ const triggerButton = getByTestId('trigger-button');
+ await act(async () => {
+ fireEvent.click(triggerButton);
+ });
+ expect(mockTriggerAlarmFn).toHaveBeenCalled();
+ expect(mockTriggerAlarmFn).toHaveBeenCalledWith({
+ summary: description,
+ details: description,
+ userName: 'guest@example.com',
+ targets: [
+ { slug: 'test_user', type: 'User' },
+ { slug: 'team-zEalMCgwYSA0Lt40', type: 'EscalationPolicy' },
+ ],
+ isMultiResponder: false,
+ });
+ });
+});