Skip to content

Commit 2697741

Browse files
committed
feat(discord): Add frontend pipeline step for Discord integration setup
Register the Discord integration in the pipeline registry with a single OAuthLoginStep component. Forwards the guild_id from the OAuth callback alongside code and state so the backend can identify the Discord server. Refs [VDY-83](https://linear.app/getsentry/issue/VDY-83/discord-api-driven-integration-setup)
1 parent 1093817 commit 2697741

File tree

3 files changed

+164
-0
lines changed

3 files changed

+164
-0
lines changed
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import {act, render, screen, userEvent} from 'sentry-test/reactTestingLibrary';
2+
3+
import {discordIntegrationPipeline} from './pipelineIntegrationDiscord';
4+
import type {PipelineStepProps} from './types';
5+
6+
const DiscordOAuthLoginStep = discordIntegrationPipeline.steps[0].component;
7+
8+
function makeStepProps<D, A>(
9+
overrides: Partial<PipelineStepProps<D, A>> & {stepData: D}
10+
): PipelineStepProps<D, A> {
11+
return {
12+
advance: jest.fn(),
13+
advanceError: null,
14+
isAdvancing: false,
15+
stepIndex: 0,
16+
totalSteps: 1,
17+
...overrides,
18+
};
19+
}
20+
21+
let mockPopup: Window;
22+
23+
function dispatchPipelineMessage({
24+
data,
25+
origin = document.location.origin,
26+
source = mockPopup,
27+
}: {
28+
data: Record<string, string>;
29+
origin?: string;
30+
source?: Window | MessageEventSource | null;
31+
}) {
32+
act(() => {
33+
const event = new MessageEvent('message', {data, origin});
34+
Object.defineProperty(event, 'source', {value: source});
35+
window.dispatchEvent(event);
36+
});
37+
}
38+
39+
beforeEach(() => {
40+
mockPopup = {
41+
closed: false,
42+
close: jest.fn(),
43+
focus: jest.fn(),
44+
} as unknown as Window;
45+
jest.spyOn(window, 'open').mockReturnValue(mockPopup);
46+
});
47+
48+
afterEach(() => {
49+
jest.restoreAllMocks();
50+
});
51+
52+
describe('DiscordOAuthLoginStep', () => {
53+
it('renders the OAuth login step for Discord', () => {
54+
render(
55+
<DiscordOAuthLoginStep
56+
{...makeStepProps({
57+
stepData: {oauthUrl: 'https://discord.com/api/oauth2/authorize'},
58+
})}
59+
/>
60+
);
61+
62+
expect(screen.getByRole('button', {name: 'Authorize Discord'})).toBeInTheDocument();
63+
});
64+
65+
it('calls advance with code, state, and guildId on OAuth callback', async () => {
66+
const advance = jest.fn();
67+
render(
68+
<DiscordOAuthLoginStep
69+
{...makeStepProps({
70+
stepData: {oauthUrl: 'https://discord.com/api/oauth2/authorize'},
71+
advance,
72+
})}
73+
/>
74+
);
75+
76+
await userEvent.click(screen.getByRole('button', {name: 'Authorize Discord'}));
77+
78+
dispatchPipelineMessage({
79+
data: {
80+
_pipeline_source: 'sentry-pipeline',
81+
code: 'auth-code-123',
82+
state: 'state-xyz',
83+
guild_id: '1234567890',
84+
},
85+
});
86+
87+
expect(advance).toHaveBeenCalledWith({
88+
code: 'auth-code-123',
89+
state: 'state-xyz',
90+
guildId: '1234567890',
91+
});
92+
});
93+
94+
it('shows loading state when isAdvancing is true', () => {
95+
render(
96+
<DiscordOAuthLoginStep
97+
{...makeStepProps({
98+
stepData: {oauthUrl: 'https://discord.com/api/oauth2/authorize'},
99+
isAdvancing: true,
100+
})}
101+
/>
102+
);
103+
104+
expect(screen.getByRole('button', {name: 'Authorizing...'})).toBeDisabled();
105+
});
106+
107+
it('disables authorize button when oauthUrl is not provided', () => {
108+
render(<DiscordOAuthLoginStep {...makeStepProps({stepData: {}})} />);
109+
110+
expect(screen.getByRole('button', {name: 'Authorize Discord'})).toBeDisabled();
111+
});
112+
});
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import {useCallback} from 'react';
2+
3+
import {t} from 'sentry/locale';
4+
import type {IntegrationWithConfig} from 'sentry/types/integrations';
5+
6+
import type {OAuthCallbackData} from './shared/oauthLoginStep';
7+
import {OAuthLoginStep} from './shared/oauthLoginStep';
8+
import type {PipelineDefinition, PipelineStepProps} from './types';
9+
import {pipelineComplete} from './types';
10+
11+
function DiscordOAuthLoginStep({
12+
stepData,
13+
advance,
14+
isAdvancing,
15+
}: PipelineStepProps<
16+
{oauthUrl?: string},
17+
{code: string; guildId: string; state: string}
18+
>) {
19+
const handleOAuthCallback = useCallback(
20+
(data: OAuthCallbackData) => {
21+
advance({code: data.code, state: data.state, guildId: data.rest.guild_id ?? ''});
22+
},
23+
[advance]
24+
);
25+
26+
return (
27+
<OAuthLoginStep
28+
oauthUrl={stepData.oauthUrl}
29+
isLoading={isAdvancing}
30+
serviceName="Discord"
31+
onOAuthCallback={handleOAuthCallback}
32+
popup={{height: 900}}
33+
/>
34+
);
35+
}
36+
37+
export const discordIntegrationPipeline = {
38+
type: 'integration',
39+
provider: 'discord',
40+
actionTitle: t('Installing Discord Integration'),
41+
getCompletionData: pipelineComplete<IntegrationWithConfig>,
42+
completionView: null,
43+
steps: [
44+
{
45+
stepId: 'oauth_login',
46+
shortDescription: t('Authorizing via Discord OAuth'),
47+
component: DiscordOAuthLoginStep,
48+
},
49+
],
50+
} as const satisfies PipelineDefinition;

static/app/components/pipeline/registry.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {dummyIntegrationPipeline} from './pipelineDummyProvider';
22
import {awsLambdaIntegrationPipeline} from './pipelineIntegrationAwsLambda';
33
import {bitbucketIntegrationPipeline} from './pipelineIntegrationBitbucket';
4+
import {discordIntegrationPipeline} from './pipelineIntegrationDiscord';
45
import {githubIntegrationPipeline} from './pipelineIntegrationGitHub';
56
import {gitlabIntegrationPipeline} from './pipelineIntegrationGitLab';
67
import {slackIntegrationPipeline} from './pipelineIntegrationSlack';
@@ -11,6 +12,7 @@ import {slackIntegrationPipeline} from './pipelineIntegrationSlack';
1112
export const PIPELINE_REGISTRY = [
1213
awsLambdaIntegrationPipeline,
1314
bitbucketIntegrationPipeline,
15+
discordIntegrationPipeline,
1416
dummyIntegrationPipeline,
1517
githubIntegrationPipeline,
1618
gitlabIntegrationPipeline,

0 commit comments

Comments
 (0)