Skip to content
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
112 changes: 112 additions & 0 deletions static/app/components/pipeline/pipelineIntegrationDiscord.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import {act, render, screen, userEvent} from 'sentry-test/reactTestingLibrary';

import {discordIntegrationPipeline} from './pipelineIntegrationDiscord';
import type {PipelineStepProps} from './types';

const DiscordOAuthLoginStep = discordIntegrationPipeline.steps[0].component;

function makeStepProps<D, A>(
overrides: Partial<PipelineStepProps<D, A>> & {stepData: D}
): PipelineStepProps<D, A> {
return {
advance: jest.fn(),
advanceError: null,
isAdvancing: false,
stepIndex: 0,
totalSteps: 1,
...overrides,
};
}

let mockPopup: Window;

function dispatchPipelineMessage({
data,
origin = document.location.origin,
source = mockPopup,
}: {
data: Record<string, string>;
origin?: string;
source?: Window | MessageEventSource | null;
}) {
act(() => {
const event = new MessageEvent('message', {data, origin});
Object.defineProperty(event, 'source', {value: source});
window.dispatchEvent(event);
});
}

beforeEach(() => {
mockPopup = {
closed: false,
close: jest.fn(),
focus: jest.fn(),
} as unknown as Window;
jest.spyOn(window, 'open').mockReturnValue(mockPopup);
});

afterEach(() => {
jest.restoreAllMocks();
});

describe('DiscordOAuthLoginStep', () => {
it('renders the OAuth login step for Discord', () => {
render(
<DiscordOAuthLoginStep
{...makeStepProps({
stepData: {oauthUrl: 'https://discord.com/api/oauth2/authorize'},
})}
/>
);

expect(screen.getByRole('button', {name: 'Authorize Discord'})).toBeInTheDocument();
});

it('calls advance with code, state, and guildId on OAuth callback', async () => {
const advance = jest.fn();
render(
<DiscordOAuthLoginStep
{...makeStepProps({
stepData: {oauthUrl: 'https://discord.com/api/oauth2/authorize'},
advance,
})}
/>
);

await userEvent.click(screen.getByRole('button', {name: 'Authorize Discord'}));

dispatchPipelineMessage({
data: {
_pipeline_source: 'sentry-pipeline',
code: 'auth-code-123',
state: 'state-xyz',
guild_id: '1234567890',
},
});

expect(advance).toHaveBeenCalledWith({
code: 'auth-code-123',
state: 'state-xyz',
guildId: '1234567890',
});
});

it('shows loading state when isAdvancing is true', () => {
render(
<DiscordOAuthLoginStep
{...makeStepProps({
stepData: {oauthUrl: 'https://discord.com/api/oauth2/authorize'},
isAdvancing: true,
})}
/>
);

expect(screen.getByRole('button', {name: 'Authorizing...'})).toBeDisabled();
});

it('disables authorize button when oauthUrl is not provided', () => {
render(<DiscordOAuthLoginStep {...makeStepProps({stepData: {}})} />);

expect(screen.getByRole('button', {name: 'Authorize Discord'})).toBeDisabled();
});
});
50 changes: 50 additions & 0 deletions static/app/components/pipeline/pipelineIntegrationDiscord.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import {useCallback} from 'react';

import {t} from 'sentry/locale';
import type {IntegrationWithConfig} from 'sentry/types/integrations';

import type {OAuthCallbackData} from './shared/oauthLoginStep';
import {OAuthLoginStep} from './shared/oauthLoginStep';
import type {PipelineDefinition, PipelineStepProps} from './types';
import {pipelineComplete} from './types';

function DiscordOAuthLoginStep({
stepData,
advance,
isAdvancing,
}: PipelineStepProps<
{oauthUrl?: string},
{code: string; guildId: string; state: string}
>) {
const handleOAuthCallback = useCallback(
(data: OAuthCallbackData) => {
advance({code: data.code, state: data.state, guildId: data.rest.guild_id ?? ''});
},
[advance]
);

return (
<OAuthLoginStep
oauthUrl={stepData.oauthUrl}
isLoading={isAdvancing}
serviceName="Discord"
onOAuthCallback={handleOAuthCallback}
popup={{height: 900}}
/>
);
}

export const discordIntegrationPipeline = {
type: 'integration',
provider: 'discord',
actionTitle: t('Installing Discord Integration'),
getCompletionData: pipelineComplete<IntegrationWithConfig>,
completionView: null,
steps: [
{
stepId: 'oauth_login',
shortDescription: t('Authorizing via Discord OAuth'),
component: DiscordOAuthLoginStep,
},
],
} as const satisfies PipelineDefinition;
2 changes: 2 additions & 0 deletions static/app/components/pipeline/registry.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {dummyIntegrationPipeline} from './pipelineDummyProvider';
import {awsLambdaIntegrationPipeline} from './pipelineIntegrationAwsLambda';
import {bitbucketIntegrationPipeline} from './pipelineIntegrationBitbucket';
import {discordIntegrationPipeline} from './pipelineIntegrationDiscord';
import {githubIntegrationPipeline} from './pipelineIntegrationGitHub';
import {gitlabIntegrationPipeline} from './pipelineIntegrationGitLab';
import {slackIntegrationPipeline} from './pipelineIntegrationSlack';
Expand All @@ -11,6 +12,7 @@ import {slackIntegrationPipeline} from './pipelineIntegrationSlack';
export const PIPELINE_REGISTRY = [
awsLambdaIntegrationPipeline,
bitbucketIntegrationPipeline,
discordIntegrationPipeline,
dummyIntegrationPipeline,
githubIntegrationPipeline,
gitlabIntegrationPipeline,
Expand Down
Loading