Skip to content

Commit

Permalink
Enable basic Maintainer token role UI inputs
Browse files Browse the repository at this point in the history
  • Loading branch information
subbuvenk-atlas committed Dec 10, 2024
1 parent 98fd1be commit 8480cde
Show file tree
Hide file tree
Showing 12 changed files with 8,417 additions and 8,881 deletions.
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ module.exports = {
},
],
'@typescript-eslint/no-floating-promises': 'error',
'@typescript-eslint/no-empty-function': 'off',
},
},
],
Expand Down
3 changes: 3 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ export const STORAGE_KEYS = {

export const STORAGE_SECRETS = {
GROUP_TOKEN_KEY_PREFIX: 'groupToken-',
WEBHOOK_SECRET_TOKEN_KEY_PREFIX: 'webhookSecretToken-',
TOKEN_ROLE_PREFIX: 'tokenRole-',
GROUP_NAME_PREFIX: 'groupName-',
};

export const REQUIRED_SCOPES = ['api', 'write_repository'];
Expand Down
16 changes: 12 additions & 4 deletions src/resolvers/admin-resolvers.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import Resolver from '@forge/resolver';

import graphqlGateway, { CompassComponentTypeObject } from '@atlassian/forge-graphql';
import { getGroupProjects } from '../services/fetch-projects';
import graphqlGateway from '@atlassian/forge-graphql';
import { AuthErrorTypes, GitlabAPIGroup, ResolverResponse, DefaultErrorTypes, FeaturesList } from '../resolverTypes';
import { connectGroup, InvalidGroupTokenError } from '../services/group';

Expand All @@ -10,6 +9,7 @@ import { disconnectGroup } from '../services/disconnect-group';
import { getForgeAppId } from '../utils/get-forge-app-id';
import { getLastSyncTime } from '../services/last-sync-time';
import { appId, connectedGroupsInfo, getFeatures, groupsAllExisting } from './shared-resolvers';
import { ConnectGroupInput } from '../types';

const resolver = new Resolver();

Expand Down Expand Up @@ -37,11 +37,19 @@ resolver.define('groups/connectedInfo', async (): Promise<ResolverResponse<Gitla

resolver.define('groups/connect', async (req): Promise<ResolverResponse> => {
const {
payload: { groupToken, groupTokenName },
payload: { groupToken, groupTokenName, groupRole, groupName, webhookId, webhookSecretToken },
context: { cloudId },
} = req;
try {
const groupId = await connectGroup(groupToken, groupTokenName);
const input: ConnectGroupInput = {
token: groupToken,
tokenName: groupTokenName,
groupRole,
groupName,
webhookId,
webhookSecretToken,
};
const groupId = await connectGroup(input);

await setupAndValidateWebhook(groupId);

Expand Down
10 changes: 5 additions & 5 deletions src/services/group.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Result, startsWith, storage } from '@forge/api';

import { GitLabAccessLevels, GitlabAPIGroup, GroupAccessToken } from '../types';
import { ConnectGroupInput, GitLabAccessLevels, GitlabAPIGroup, GroupAccessToken } from '../types';
import { getGroupAccessTokens, getGroupsData } from '../client/gitlab';
import { REQUIRED_SCOPES, STORAGE_KEYS, STORAGE_SECRETS } from '../constants';
import { AuthErrorTypes } from '../resolverTypes';
Expand Down Expand Up @@ -28,17 +28,17 @@ const validateGroupTokenScopes = (requiredScopes: string[], tokenScopes: string[
return requiredScopes.every((requiredScope) => tokenScopes.includes(requiredScope));
};

export const connectGroup = async (token: string, tokenName: string): Promise<number> => {
export const connectGroup = async (input: ConnectGroupInput): Promise<number> => {
let groupId;
let groupName;
try {
const [group] = await getGroupsData(token, 'true');
const [group] = await getGroupsData(input.token, 'true');
({ id: groupId, name: groupName } = group);
} catch (e) {
throw new InvalidGroupTokenError(AuthErrorTypes.INVALID_GROUP_TOKEN);
}

const groupToken = await findGroupToken(token, tokenName, groupId);
const groupToken = await findGroupToken(input.token, input.tokenName, groupId);
if (!groupToken) {
throw new InvalidGroupTokenError(AuthErrorTypes.INVALID_GROUP_TOKEN_NAME);
}
Expand All @@ -49,7 +49,7 @@ export const connectGroup = async (token: string, tokenName: string): Promise<nu
}

await storage.set(`${STORAGE_KEYS.GROUP_KEY_PREFIX}${groupId}`, groupName);
await storage.setSecret(`${STORAGE_SECRETS.GROUP_TOKEN_KEY_PREFIX}${groupId}`, token);
await storage.setSecret(`${STORAGE_SECRETS.GROUP_TOKEN_KEY_PREFIX}${groupId}`, input.token);

return groupId;
};
Expand Down
16 changes: 16 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,20 @@ type BackfillData = {
};
};

enum GitLabRoles {
OWNER = 'owner',
MAINTAINER = 'maintainer',
}

type ConnectGroupInput = {
token: string;
tokenName: string;
groupRole: GitLabRoles;
groupName?: string;
webhookSecretToken?: string;
webhookId?: string;
};

export type {
WebtriggerRequest,
WebtriggerResponse,
Expand Down Expand Up @@ -468,6 +482,7 @@ export type {
TeamsWithMembershipStatus,
CompareProjectWithExistingComponent,
BackfillData,
ConnectGroupInput,
};

export {
Expand All @@ -479,4 +494,5 @@ export {
GitlabPipelineStates,
GitLabAccessLevels,
WebhookTypes,
GitLabRoles,
};
1 change: 1 addition & 0 deletions ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"@atlaskit/onboarding": "^11.3.0",
"@atlaskit/primitives": "^12.1.0",
"@atlaskit/progress-bar": "^0.5.6",
"@atlaskit/radio": "^6.5.5",
"@atlaskit/section-message": "^6.1.10",
"@atlaskit/select": "^15.2.11",
"@atlaskit/spinner": "^15.1.9",
Expand Down
199 changes: 197 additions & 2 deletions ui/src/components/AuthPage/__tests__/AuthPage.test.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { fireEvent, render, screen } from '@testing-library/react';
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import { act } from 'react-dom/test-utils';
import '@testing-library/jest-dom/extend-expect';

import { AppRouter } from '../../../AppRouter';
import { AppContextProvider } from '../../../context/AppContext';
import { ErrorMessages } from '../../../errorMessages';
import { defaultMocks, mockInvoke, mockGetContext } from '../../../helpers/mockHelpers';
import { defaultMocks, mockInvoke, mockGetContext, gitlabFFDisabledMocks } from '../../../helpers/mockHelpers';
import { AuthErrorTypes } from '../../../resolverTypes';

jest.mock('@forge/bridge', () => ({
Expand Down Expand Up @@ -103,3 +103,198 @@ describe('Auth flow validation', () => {
);
});
});

describe('Auth page role selection', () => {
beforeEach(() => {
mockGetContext('admin-page-ui');
mockInvoke(defaultMocks);
});

it('renders role selection radio group with default owner selected', async () => {
const { findByRole } = render(
<AppContextProvider>
<AppRouter />
</AppContextProvider>,
);

const ownerRadio = await findByRole('radio', { name: 'Owner' });
const maintainerRadio = await findByRole('radio', { name: 'Maintainer' });

expect(ownerRadio).toBeChecked();
expect(maintainerRadio).not.toBeChecked();
});

it('shows group name input when maintainer role is selected', async () => {
const { findByRole, queryByTestId } = render(
<AppContextProvider>
<AppRouter />
</AppContextProvider>,
);

const maintainerRadio = await findByRole('radio', { name: 'Maintainer' });

await act(async () => {
fireEvent.click(maintainerRadio);
});

expect(queryByTestId('group-name')).toBeInTheDocument();
expect(queryByTestId('webhook-secret-token')).toBeInTheDocument();
});

it('hides group name input when owner role is selected', async () => {
const { findByRole, queryByTestId } = render(
<AppContextProvider>
<AppRouter />
</AppContextProvider>,
);

const maintainerRadio = await findByRole('radio', { name: 'Maintainer' });
const ownerRadio = await findByRole('radio', { name: 'Owner' });

await act(async () => {
fireEvent.click(maintainerRadio);
});
await act(async () => {
fireEvent.click(ownerRadio);
});

expect(queryByTestId('group-name')).not.toBeInTheDocument();
});

it('disables connect button when maintainer selected with missing required fields', async () => {
const { findByRole, findByTestId } = render(
<AppContextProvider>
<AppRouter />
</AppContextProvider>,
);

// Wait for the auth page to load
await findByTestId('gitlab-auth-page');

// Fill in basic required fields
await act(async () => {
fireEvent.change(await findByTestId('group-access-token'), {
target: { value: 'test-token' },
});
});
await act(async () => {
fireEvent.change(await findByTestId('access-token-name'), {
target: { value: 'test-name' },
});
});

const maintainerRadio = await findByRole('radio', { name: 'Maintainer' });
await act(async () => {
fireEvent.click(maintainerRadio);
});

// Test Case 1: Only group name filled
await act(async () => {
fireEvent.change(await findByTestId('group-name'), {
target: { value: 'test-group' },
});
});

let connectButton = await findByRole('button', { name: 'Connect' });
expect(connectButton).toHaveAttribute('disabled');

// Test Case 2: Only webhook token filled
await act(async () => {
fireEvent.change(await findByTestId('group-name'), {
target: { value: '' },
});
});
await act(async () => {
fireEvent.change(await findByTestId('webhook-secret-token'), {
target: { value: 'webhook-token' },
});
});

connectButton = await findByRole('button', { name: 'Connect' });
expect(connectButton).toHaveAttribute('disabled');

// Test Case 3: Only webhook id filled
await act(async () => {
fireEvent.change(await findByTestId('webhook-secret-token'), {
target: { value: '' },
});
});
await act(async () => {
fireEvent.change(await findByTestId('webhook-id'), {
target: { value: '12343' },
});
});

connectButton = await findByRole('button', { name: 'Connect' });
expect(connectButton).toHaveAttribute('disabled');
});

it('enables connect button when maintainer selected with all required fields', async () => {
const { findByRole, findByTestId } = render(
<AppContextProvider>
<AppRouter />
</AppContextProvider>,
);

// Wait for the auth page to load
await findByTestId('gitlab-auth-page');

// Fill in all required fields
await act(async () => {
fireEvent.change(await findByTestId('group-access-token'), {
target: { value: 'test-token' },
});
});
await act(async () => {
fireEvent.change(await findByTestId('access-token-name'), {
target: { value: 'test-name' },
});
});

const maintainerRadio = await findByRole('radio', { name: 'Maintainer' });
await act(async () => {
fireEvent.click(maintainerRadio);
});

await act(async () => {
fireEvent.change(await findByTestId('group-name'), {
target: { value: 'test-group' },
});
});

await act(async () => {
fireEvent.change(await findByTestId('webhook-secret-token'), {
target: { value: 'webhook-token' },
});
});

await act(async () => {
fireEvent.change(await findByTestId('webhook-id'), {
target: { value: '12343' },
});
});

const connectButton = await findByRole('button', { name: 'Connect' });
expect(connectButton).not.toHaveAttribute('disabled');
});

describe('Gitlab Maintainer Token FF disabled', () => {
beforeEach(() => {
mockGetContext('admin-page-ui');
mockInvoke(gitlabFFDisabledMocks);
});

it('does not render maintainer role selection radio group', async () => {
const { queryByRole } = render(
<AppContextProvider>
<AppRouter />
</AppContextProvider>,
);

await waitFor(() => {
const maintainerRadio = queryByRole('radio', { name: 'Maintainer' });
expect(maintainerRadio).not.toBeInTheDocument();
});
});
});
});
Loading

0 comments on commit 8480cde

Please sign in to comment.