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

import {bitbucketIntegrationPipeline} from './pipelineIntegrationBitbucket';
import type {PipelineStepProps} from './types';

const BitbucketAuthorizeStep = bitbucketIntegrationPipeline.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('BitbucketAuthorizeStep', () => {
it('renders the authorize button', () => {
render(
<BitbucketAuthorizeStep
{...makeStepProps({
stepData: {
authorizeUrl:
'https://bitbucket.org/site/addons/authorize?descriptor_uri=test',
},
})}
/>
);

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

it('calls advance with JWT on callback', async () => {
const advance = jest.fn();
render(
<BitbucketAuthorizeStep
{...makeStepProps({
stepData: {
authorizeUrl:
'https://bitbucket.org/site/addons/authorize?descriptor_uri=test',
},
advance,
})}
/>
);

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

dispatchPipelineMessage({
data: {
_pipeline_source: 'sentry-pipeline',
jwt: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.test',
},
});

expect(advance).toHaveBeenCalledWith({
jwt: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.test',
});
});

it('shows reopen button when waiting for callback', async () => {
render(
<BitbucketAuthorizeStep
{...makeStepProps({
stepData: {
authorizeUrl:
'https://bitbucket.org/site/addons/authorize?descriptor_uri=test',
},
})}
/>
);

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

expect(
screen.getByRole('button', {name: 'Reopen authorization window'})
).toBeInTheDocument();
});

it('shows loading state when isAdvancing is true', () => {
render(
<BitbucketAuthorizeStep
{...makeStepProps({
stepData: {
authorizeUrl:
'https://bitbucket.org/site/addons/authorize?descriptor_uri=test',
},
isAdvancing: true,
})}
/>
);

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

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

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

it('shows popup blocked notice when popup fails to open', async () => {
jest.spyOn(window, 'open').mockReturnValue(null);
render(
<BitbucketAuthorizeStep
{...makeStepProps({
stepData: {
authorizeUrl:
'https://bitbucket.org/site/addons/authorize?descriptor_uri=test',
},
})}
/>
);

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

expect(screen.getByText(/authorization popup was blocked/)).toBeInTheDocument();
});
});
96 changes: 96 additions & 0 deletions static/app/components/pipeline/pipelineIntegrationBitbucket.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import {useCallback} from 'react';

import {Button} from '@sentry/scraps/button';
import {Stack} from '@sentry/scraps/layout';
import {Text} from '@sentry/scraps/text';

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

import {useRedirectPopupStep} from './shared/useRedirectPopupStep';
import type {PipelineDefinition, PipelineStepProps} from './types';
import {pipelineComplete} from './types';

interface AuthorizeStepData {
authorizeUrl?: string;
}

interface AuthorizeAdvanceData {
jwt: string;
}

function BitbucketAuthorizeStep({
stepData,
advance,
isAdvancing,
}: PipelineStepProps<AuthorizeStepData, AuthorizeAdvanceData>) {
const handleCallback = useCallback(
(data: Record<string, string>) => {
if (data.jwt) {
advance({jwt: data.jwt});
}
},
[advance]
);

const {openPopup, isWaitingForCallback, popupStatus} = useRedirectPopupStep({
redirectUrl: stepData.authorizeUrl,
onCallback: handleCallback,
});

return (
<Stack gap="lg" align="start">
<Stack gap="sm">
<Text>
{t(
'Connect your Bitbucket account by authorizing the Sentry add-on for Bitbucket.'
)}
</Text>
{isWaitingForCallback && (
<Text variant="muted" size="sm">
{t('A popup should have opened to authorize with Bitbucket.')}
</Text>
)}
{popupStatus === 'failed-to-open' && (
<Text variant="danger" size="sm">
{t(
'The authorization popup was blocked by your browser. Please ensure popups are allowed and try again.'
)}
</Text>
)}
</Stack>
{isAdvancing ? (
<Button size="sm" disabled>
{t('Authorizing...')}
</Button>
) : isWaitingForCallback ? (
<Button size="sm" onClick={openPopup}>
{t('Reopen authorization window')}
</Button>
) : (
<Button
size="sm"
priority="primary"
onClick={openPopup}
disabled={!stepData.authorizeUrl}
>
{t('Authorize Bitbucket')}
</Button>
)}
</Stack>
);
Comment thread
evanpurkhiser marked this conversation as resolved.
}

export const bitbucketIntegrationPipeline = {
type: 'integration',
provider: 'bitbucket',
actionTitle: t('Installing Bitbucket Integration'),
getCompletionData: pipelineComplete<IntegrationWithConfig>,
steps: [
{
stepId: 'authorize',
shortDescription: t('Authorizing Bitbucket'),
component: BitbucketAuthorizeStep,
},
],
} 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,11 +1,13 @@
import {dummyIntegrationPipeline} from './pipelineDummyProvider';
import {bitbucketIntegrationPipeline} from './pipelineIntegrationBitbucket';
import {githubIntegrationPipeline} from './pipelineIntegrationGitHub';
import {gitlabIntegrationPipeline} from './pipelineIntegrationGitLab';

/**
* All registered pipeline definitions.
*/
export const PIPELINE_REGISTRY = [
bitbucketIntegrationPipeline,
dummyIntegrationPipeline,
githubIntegrationPipeline,
gitlabIntegrationPipeline,
Expand Down
Loading