Browse Source

SONAR-11321 Prevent binding already bound application

tags/7.5
Julien Lancelot 5 years ago
parent
commit
2507163633
16 changed files with 296 additions and 111 deletions
  1. 6
    1
      server/sonar-db-dao/src/main/java/org/sonar/db/alm/OrganizationAlmBindingDao.java
  2. 3
    0
      server/sonar-db-dao/src/main/java/org/sonar/db/alm/OrganizationAlmBindingMapper.java
  3. 3
    0
      server/sonar-db-dao/src/main/java/org/sonar/db/organization/OrganizationDto.java
  4. 14
    5
      server/sonar-db-dao/src/main/resources/org/sonar/db/alm/OrganizationAlmBindingMapper.xml
  5. 31
    3
      server/sonar-db-dao/src/test/java/org/sonar/db/alm/OrganizationAlmBindingDaoTest.java
  6. 22
    7
      server/sonar-web/src/main/js/api/alm-integration.ts
  7. 1
    0
      server/sonar-web/src/main/js/app/types.ts
  8. 24
    19
      server/sonar-web/src/main/js/apps/create/organization/AutoOrganizationCreate.tsx
  9. 12
    16
      server/sonar-web/src/main/js/apps/create/organization/AutoPersonalOrganizationBind.tsx
  10. 59
    11
      server/sonar-web/src/main/js/apps/create/organization/ChooseRemoteOrganizationStep.tsx
  11. 31
    20
      server/sonar-web/src/main/js/apps/create/organization/CreateOrganization.tsx
  12. 11
    1
      server/sonar-web/src/main/js/apps/create/organization/__tests__/ChooseRemoteOrganizationStep-test.tsx
  13. 22
    16
      server/sonar-web/src/main/js/apps/create/organization/__tests__/CreateOrganization-test.tsx
  14. 2
    2
      server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/AutoPersonalOrganizationBind-test.tsx.snap
  15. 54
    10
      server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/ChooseRemoteOrganizationStep-test.tsx.snap
  16. 1
    0
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

+ 6
- 1
server/sonar-db-dao/src/main/java/org/sonar/db/alm/OrganizationAlmBindingDao.java View File

@@ -29,6 +29,7 @@ import org.sonar.db.Dao;
import org.sonar.db.DbSession;
import org.sonar.db.organization.OrganizationDto;

import static java.util.Optional.ofNullable;
import static org.sonar.db.DatabaseUtils.executeLargeInputs;

public class OrganizationAlmBindingDao implements Dao {
@@ -42,7 +43,7 @@ public class OrganizationAlmBindingDao implements Dao {
}

public Optional<OrganizationAlmBindingDto> selectByOrganization(DbSession dbSession, OrganizationDto organization) {
return Optional.ofNullable(getMapper(dbSession).selectByOrganizationUuid(organization.getUuid()));
return ofNullable(getMapper(dbSession).selectByOrganizationUuid(organization.getUuid()));
}

public List<OrganizationAlmBindingDto> selectByOrganizations(DbSession dbSession, Collection<OrganizationDto> organizations) {
@@ -50,6 +51,10 @@ public class OrganizationAlmBindingDao implements Dao {
organizationUuids -> getMapper(dbSession).selectByOrganizationUuids(organizationUuids));
}

public Optional<OrganizationAlmBindingDto> selectByAlmAppInstall(DbSession dbSession, AlmAppInstallDto almAppInstall) {
return ofNullable(getMapper(dbSession).selectByInstallationUuid(almAppInstall.getUuid()));
}

public void insert(DbSession dbSession, OrganizationDto organization, AlmAppInstallDto almAppInstall, String url, String userUuid) {
long now = system2.now();
getMapper(dbSession).insert(new OrganizationAlmBindingDto()

+ 3
- 0
server/sonar-db-dao/src/main/java/org/sonar/db/alm/OrganizationAlmBindingMapper.java View File

@@ -31,6 +31,9 @@ public interface OrganizationAlmBindingMapper {

List<OrganizationAlmBindingDto> selectByOrganizationUuids(@Param("organizationUuids") Collection<String> organizationUuids);

@CheckForNull
OrganizationAlmBindingDto selectByInstallationUuid(@Param("installationUuid") String installationUuid);

void insert(@Param("dto") OrganizationAlmBindingDto dto);

void deleteByOrganizationUuid(@Param("organizationUuid") String organizationUuid);

+ 3
- 0
server/sonar-db-dao/src/main/java/org/sonar/db/organization/OrganizationDto.java View File

@@ -109,6 +109,7 @@ public class OrganizationDto {
return this;
}

@CheckForNull
public String getDescription() {
return description;
}
@@ -118,6 +119,7 @@ public class OrganizationDto {
return this;
}

@CheckForNull
public String getUrl() {
return url;
}
@@ -127,6 +129,7 @@ public class OrganizationDto {
return this;
}

@CheckForNull
public String getAvatarUrl() {
return avatarUrl;
}

+ 14
- 5
server/sonar-db-dao/src/main/resources/org/sonar/db/alm/OrganizationAlmBindingMapper.xml View File

@@ -26,12 +26,21 @@
select
<include refid="columns"/>
from
organization_alm_bindings
organization_alm_bindings
where
organization_uuid in
<foreach collection="organizationUuids" open="(" close=")" item="organizationUuid" separator=",">
#{organizationUuid , jdbcType=VARCHAR}
</foreach>
</select>

<select id="selectByInstallationUuid" parameterType="String" resultType="org.sonar.db.alm.OrganizationAlmBindingDto">
select
<include refid="columns"/>
from
organization_alm_bindings
where
organization_uuid in
<foreach collection="organizationUuids" open="(" close=")" item="organizationUuid" separator=",">
#{organizationUuid , jdbcType=VARCHAR}
</foreach>
alm_app_install_uuid = #{installationUuid, jdbcType=VARCHAR}
</select>

<insert id="insert" parameterType="Map" useGeneratedKeys="false">

+ 31
- 3
server/sonar-db-dao/src/test/java/org/sonar/db/alm/OrganizationAlmBindingDaoTest.java View File

@@ -25,14 +25,12 @@ import org.junit.Test;
import org.sonar.api.utils.System2;
import org.sonar.api.utils.internal.TestSystem2;
import org.sonar.core.util.UuidFactory;
import org.sonar.core.util.Uuids;
import org.sonar.db.DbTester;
import org.sonar.db.organization.OrganizationDto;
import org.sonar.db.user.UserDto;

import static java.util.Arrays.asList;
import static java.util.Collections.singletonList;
import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.entry;
import static org.assertj.core.groups.Tuple.tuple;
@@ -75,7 +73,7 @@ public class OrganizationAlmBindingDaoTest {
OrganizationDto organization = db.organizations().insert();
AlmAppInstallDto almAppInstall = db.alm().insertAlmAppInstall();
db.alm().insertOrganizationAlmBinding(organization, almAppInstall);
// No binding on other organization
// No binding on other installation
OrganizationDto otherOrganization = db.organizations().insert();

Optional<OrganizationAlmBindingDto> result = underTest.selectByOrganization(db.getSession(), otherOrganization);
@@ -100,6 +98,36 @@ public class OrganizationAlmBindingDaoTest {
assertThat(underTest.selectByOrganizations(db.getSession(), singletonList(organizationNotBound))).isEmpty();
}

@Test
public void selectByAlmAppInstall() {
OrganizationDto organization = db.organizations().insert();
AlmAppInstallDto almAppInstall = db.alm().insertAlmAppInstall();
OrganizationAlmBindingDto dto = db.alm().insertOrganizationAlmBinding(organization, almAppInstall);

Optional<OrganizationAlmBindingDto> result = underTest.selectByAlmAppInstall(db.getSession(), almAppInstall);

assertThat(result.get())
.extracting(OrganizationAlmBindingDto::getUuid, OrganizationAlmBindingDto::getOrganizationUuid, OrganizationAlmBindingDto::getAlmAppInstallUuid,
OrganizationAlmBindingDto::getUrl, OrganizationAlmBindingDto::getAlm,
OrganizationAlmBindingDto::getUserUuid, OrganizationAlmBindingDto::getCreatedAt)
.containsExactlyInAnyOrder(dto.getUuid(), organization.getUuid(), dto.getAlmAppInstallUuid(),
dto.getUrl(), ALM.GITHUB,
dto.getUserUuid(), NOW);
}

@Test
public void selectByAlmAppInstall_returns_empty_when_installation_is_not_bound_to_organization() {
OrganizationDto organization = db.organizations().insert();
AlmAppInstallDto almAppInstall = db.alm().insertAlmAppInstall();
db.alm().insertOrganizationAlmBinding(organization, almAppInstall);
// No binding on other organization
AlmAppInstallDto otherAlmAppInstall = db.alm().insertAlmAppInstall();

Optional<OrganizationAlmBindingDto> result = underTest.selectByAlmAppInstall(db.getSession(), otherAlmAppInstall);

assertThat(result).isEmpty();
}

@Test
public void insert() {
when(uuidFactory.create()).thenReturn("ABCD");

+ 22
- 7
server/sonar-web/src/main/js/api/alm-integration.ts View File

@@ -22,7 +22,8 @@ import {
AlmApplication,
AlmOrganization,
AlmRepository,
AlmUnboundApplication
AlmUnboundApplication,
OrganizationBase
} from '../app/types';
import throwGlobalError from '../app/utils/throwGlobalError';

@@ -51,10 +52,20 @@ function fetchAlmOrganization(data: { installationId: string }, remainingTries:
);
}

export function getAlmOrganization(data: { installationId: string }): Promise<AlmOrganization> {
return fetchAlmOrganization(data, 5).then(({ organization }) => ({
...organization,
name: organization.name || organization.key
export interface GetAlmOrganizationResponse {
almOrganization: AlmOrganization;
boundOrganization?: OrganizationBase;
}

export function getAlmOrganization(data: {
installationId: string;
}): Promise<GetAlmOrganizationResponse> {
return fetchAlmOrganization(data, 5).then(({ almOrganization, boundOrganization }) => ({
almOrganization: {
...almOrganization,
name: almOrganization.name || almOrganization.key
},
boundOrganization
}));
}

@@ -64,8 +75,12 @@ export function getRepositories(data: {
return getJSON('/api/alm_integration/list_repositories', data).catch(throwGlobalError);
}

export function listUnboundApplications(): Promise<{ applications: AlmUnboundApplication[] }> {
return getJSON('/api/alm_integration/list_unbound_applications').catch(throwGlobalError);
export function listUnboundApplications(): Promise<AlmUnboundApplication[]> {
return getJSON('/api/alm_integration/list_unbound_applications').then(
({ applications }) =>
applications.map((app: AlmUnboundApplication) => ({ ...app, name: app.name || app.key })),
throwGlobalError
);
}

export function provisionProject(data: {

+ 1
- 0
server/sonar-web/src/main/js/app/types.ts View File

@@ -42,6 +42,7 @@ export interface AlmRepository {

export interface AlmUnboundApplication {
installationId: string;
key: string;
name: string;
}


+ 24
- 19
server/sonar-web/src/main/js/apps/create/organization/AutoOrganizationCreate.tsx View File

@@ -38,8 +38,7 @@ import { getBaseUrl } from '../../../helpers/urls';

export enum Filters {
Bind = 'bind',
Create = 'create',
None = 'none'
Create = 'create'
}

interface Props {
@@ -47,6 +46,7 @@ interface Props {
almInstallId?: string;
almOrganization?: AlmOrganization;
almUnboundApplications: AlmUnboundApplication[];
boundOrganization?: OrganizationBase;
createOrganization: (
organization: OrganizationBase & { installationId?: string }
) => Promise<Organization>;
@@ -55,14 +55,14 @@ interface Props {
}

interface State {
filter: Filters;
filter?: Filters;
}

export default class AutoOrganizationCreate extends React.PureComponent<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
filter: props.unboundOrganizations.length === 0 ? Filters.Create : Filters.None
filter: props.unboundOrganizations.length === 0 ? Filters.Create : undefined
};
}

@@ -71,19 +71,16 @@ export default class AutoOrganizationCreate extends React.PureComponent<Props, S
};

handleCreateOrganization = (organization: Required<OrganizationBase>) => {
if (organization) {
return this.props
.createOrganization({
avatar: organization.avatar,
description: organization.description,
installationId: this.props.almInstallId,
key: organization.key,
name: organization.name || organization.key,
url: organization.url
})
.then(({ key }) => this.props.onOrgCreated(key));
}
return Promise.reject();
return this.props
.createOrganization({
avatar: organization.avatar,
description: organization.description,
installationId: this.props.almInstallId,
key: organization.key,
name: organization.name || organization.key,
url: organization.url
})
.then(({ key }) => this.props.onOrgCreated(key));
};

handleBindOrganization = (organization: string) => {
@@ -97,8 +94,14 @@ export default class AutoOrganizationCreate extends React.PureComponent<Props, S
};

render() {
const { almApplication, almInstallId, almOrganization, unboundOrganizations } = this.props;
if (almInstallId && almOrganization) {
const {
almApplication,
almInstallId,
almOrganization,
boundOrganization,
unboundOrganizations
} = this.props;
if (almInstallId && almOrganization && !boundOrganization) {
const { filter } = this.state;
const hasUnboundOrgs = unboundOrganizations.length > 0;
return (
@@ -168,7 +171,9 @@ export default class AutoOrganizationCreate extends React.PureComponent<Props, S
<ChooseRemoteOrganizationStep
almApplication={this.props.almApplication}
almInstallId={almInstallId}
almOrganization={almOrganization}
almUnboundApplications={this.props.almUnboundApplications}
boundOrganization={boundOrganization}
/>
);
}

+ 12
- 16
server/sonar-web/src/main/js/apps/create/organization/AutoPersonalOrganizationBind.tsx View File

@@ -45,20 +45,16 @@ interface Props {

export default class AutoPersonalOrganizationBind extends React.PureComponent<Props> {
handleCreateOrganization = (organization: Required<OrganizationBase>) => {
if (organization) {
return this.props
.updateOrganization({
avatar: organization.avatar,
description: organization.description,
installationId: this.props.almInstallId,
key: this.props.importPersonalOrg.key,
name: organization.name || organization.key,
url: organization.url
})
.then(({ key }) => this.props.onOrgCreated(key));
} else {
return Promise.reject();
}
return this.props
.updateOrganization({
avatar: organization.avatar,
description: organization.description,
installationId: this.props.almInstallId,
key: this.props.importPersonalOrg.key,
name: organization.name || organization.key,
url: organization.url
})
.then(({ key }) => this.props.onOrgCreated(key));
};

render() {
@@ -69,7 +65,7 @@ export default class AutoPersonalOrganizationBind extends React.PureComponent<Pr
onOpen={() => {}}
open={true}
organization={importPersonalOrg}>
<p className="huge-spacer-bottom">
<div className="huge-spacer-bottom">
<FormattedMessage
defaultMessage={translate('onboarding.import_personal_organization_x')}
id="onboarding.import_personal_organization_x"
@@ -89,7 +85,7 @@ export default class AutoPersonalOrganizationBind extends React.PureComponent<Pr
personalName: importPersonalOrg && <strong>{importPersonalOrg.name}</strong>
}}
/>
</p>
</div>
<OrganizationDetailsForm
keyReadOnly={true}
onContinue={this.handleCreateOrganization}

+ 59
- 11
server/sonar-web/src/main/js/apps/create/organization/ChooseRemoteOrganizationStep.tsx View File

@@ -19,14 +19,21 @@
*/
import * as React from 'react';
import { WithRouterProps, withRouter } from 'react-router';
import { FormattedMessage } from 'react-intl';
import { sortBy } from 'lodash';
import { serializeQuery } from './utils';
import IdentityProviderLink from '../../../components/ui/IdentityProviderLink';
import OrganizationAvatar from '../../../components/common/OrganizationAvatar';
import Select from '../../../components/controls/Select';
import Step from '../../tutorials/components/Step';
import { Alert } from '../../../components/ui/Alert';
import { SubmitButton } from '../../../components/ui/buttons';
import { AlmApplication, AlmUnboundApplication } from '../../../app/types';
import {
AlmApplication,
AlmOrganization,
AlmUnboundApplication,
OrganizationBase
} from '../../../app/types';
import { getBaseUrl } from '../../../helpers/urls';
import { sanitizeAlmId } from '../../../helpers/almIntegrations';
import { translate } from '../../../helpers/l10n';
@@ -34,7 +41,9 @@ import { translate } from '../../../helpers/l10n';
interface Props {
almApplication: AlmApplication;
almInstallId?: string;
almOrganization?: AlmOrganization;
almUnboundApplications: AlmUnboundApplication[];
boundOrganization?: OrganizationBase;
}

interface State {
@@ -82,19 +91,58 @@ export class ChooseRemoteOrganizationStep extends React.PureComponent<
};

renderForm = () => {
const { almApplication, almInstallId, almUnboundApplications } = this.props;
const {
almApplication,
almInstallId,
almOrganization,
almUnboundApplications,
boundOrganization
} = this.props;
const { unboundInstallationId } = this.state;
return (
<div className="boxed-group-inner">
{almInstallId && (
<Alert className="markdown big-spacer-bottom width-60" variant="error">
{translate('onboarding.import_organization.org_not_found')}
<ul>
<li>{translate('onboarding.import_organization.org_not_found.tips_1')}</li>
<li>{translate('onboarding.import_organization.org_not_found.tips_2')}</li>
</ul>
</Alert>
)}
{almInstallId &&
!almOrganization && (
<Alert className="big-spacer-bottom width-60" variant="error">
<div className="markdown">
{translate('onboarding.import_organization.org_not_found')}
<ul>
<li>{translate('onboarding.import_organization.org_not_found.tips_1')}</li>
<li>{translate('onboarding.import_organization.org_not_found.tips_2')}</li>
</ul>
</div>
</Alert>
)}
{almOrganization &&
boundOrganization && (
<Alert className="big-spacer-bottom width-60" variant="error">
<FormattedMessage
defaultMessage={translate('onboarding.import_organization.already_bound_x')}
id="onboarding.import_organization.already_bound_x"
values={{
avatar: (
<img
alt={almApplication.name}
className="little-spacer-left"
src={`${getBaseUrl()}/images/sonarcloud/${sanitizeAlmId(
almApplication.key
)}.svg`}
width={16}
/>
),
name: <strong>{almOrganization.name}</strong>,
boundAvatar: (
<OrganizationAvatar
className="little-spacer-left"
organization={boundOrganization}
small={true}
/>
),
boundName: <strong>{boundOrganization.name}</strong>
}}
/>
</Alert>
)}
<div className="display-flex-center">
<div className="display-inline-block abs-width-400">
<IdentityProviderLink

+ 31
- 20
server/sonar-web/src/main/js/apps/create/organization/CreateOrganization.tsx View File

@@ -42,6 +42,7 @@ import {
bindAlmOrganization,
getAlmAppInfo,
getAlmOrganization,
GetAlmOrganizationResponse,
listUnboundApplications
} from '../../../api/alm-integration';
import { getSubscriptionPlans } from '../../../api/billing';
@@ -59,8 +60,7 @@ import { translate } from '../../../helpers/l10n';
import { get, remove } from '../../../helpers/storage';
import { slugify } from '../../../helpers/strings';
import { getOrganizationUrl } from '../../../helpers/urls';
import { skipOnboarding as skipOnboardingAction } from '../../../store/users';
import { skipOnboarding } from '../../../api/users';
import { skipOnboarding } from '../../../store/users';
import * as api from '../../../api/organizations';
import * as actions from '../../../store/organizations';
import '../../../app/styles/sonarcloud.css';
@@ -76,7 +76,7 @@ interface Props {
organization: OrganizationBase & { installationId?: string }
) => Promise<Organization>;
userOrganizations: Organization[];
skipOnboardingAction: () => void;
skipOnboarding: () => void;
}

interface State {
@@ -84,6 +84,7 @@ interface State {
almOrganization?: AlmOrganization;
almOrgLoading: boolean;
almUnboundApplications: AlmUnboundApplication[];
boundOrganization?: OrganizationBase;
loading: boolean;
organization?: Organization;
subscriptionPlans?: SubscriptionPlan[];
@@ -127,7 +128,7 @@ export class CreateOrganization extends React.PureComponent<Props & WithRouterPr
if (query.almInstallId) {
this.fetchAlmOrganization(query.almInstallId);
} else {
this.setState({ almOrganization: undefined, loading: true });
this.setState({ almOrganization: undefined, boundOrganization: undefined, loading: true });
this.fetchAlmUnboundApplications().then(this.stopLoading, this.stopLoading);
}
}
@@ -136,6 +137,9 @@ export class CreateOrganization extends React.PureComponent<Props & WithRouterPr
componentWillUnmount() {
this.mounted = false;
document.body.classList.remove('white-page');
if (document.documentElement) {
document.documentElement.classList.remove('white-page');
}
}

fetchAlmApplication = () => {
@@ -147,14 +151,14 @@ export class CreateOrganization extends React.PureComponent<Props & WithRouterPr
};

fetchAlmUnboundApplications = () => {
return listUnboundApplications().then(({ applications }) => {
return listUnboundApplications().then(almUnboundApplications => {
if (this.mounted) {
this.setState({ almUnboundApplications: applications });
this.setState({ almUnboundApplications });
}
});
};

fetchValidOrgKey = (almOrganization: AlmOrganization) => {
setValidOrgKey = (almOrganization: AlmOrganization) => {
const key = slugify(almOrganization.key);
const keys = [key, ...times(9, i => `${key}-${i + 1}`)];
return api
@@ -167,24 +171,31 @@ export class CreateOrganization extends React.PureComponent<Props & WithRouterPr
() => key
)
.then(key => {
return { ...almOrganization, key };
return { almOrganization: { ...almOrganization, key } };
});
};

fetchAlmOrganization = (installationId: string) => {
this.setState({ almOrgLoading: true });
return getAlmOrganization({ installationId })
.then(this.fetchValidOrgKey)
.then(almOrganization => {
if (this.mounted) {
this.setState({ almOrganization, almOrgLoading: false });
.then(({ almOrganization, boundOrganization }) => {
if (boundOrganization) {
return Promise.resolve({ almOrganization, boundOrganization });
}
return this.setValidOrgKey(almOrganization);
})
.catch(() => {
if (this.mounted) {
this.setState({ almOrgLoading: false });
.then(
({ almOrganization, boundOrganization }: GetAlmOrganizationResponse) => {
if (this.mounted) {
this.setState({ almOrganization, almOrgLoading: false, boundOrganization });
}
},
() => {
if (this.mounted) {
this.setState({ almOrgLoading: false });
}
}
});
);
};

fetchSubscriptionPlans = () => {
@@ -196,8 +207,7 @@ export class CreateOrganization extends React.PureComponent<Props & WithRouterPr
};

handleOrgCreated = (organization: string, justCreated = true) => {
skipOnboarding().catch(() => {});
this.props.skipOnboardingAction();
this.props.skipOnboarding();
const redirectProjectTimestamp = get(ORGANIZATION_IMPORT_REDIRECT_TO_PROJECT_TIMESTAMP);
remove(ORGANIZATION_IMPORT_REDIRECT_TO_PROJECT_TIMESTAMP);
if (
@@ -237,7 +247,7 @@ export class CreateOrganization extends React.PureComponent<Props & WithRouterPr
renderContent = (almInstallId?: string, importPersonalOrg?: Organization) => {
const { currentUser, location } = this.props;
const { almApplication, almOrganization } = this.state;
const state = (location.state || {}) as LocationState;
const state: LocationState = location.state || {};

if (importPersonalOrg && almOrganization && almApplication) {
return (
@@ -287,6 +297,7 @@ export class CreateOrganization extends React.PureComponent<Props & WithRouterPr
almInstallId={almInstallId}
almOrganization={almOrganization}
almUnboundApplications={this.state.almUnboundApplications}
boundOrganization={this.state.boundOrganization}
createOrganization={this.props.createOrganization}
onOrgCreated={this.handleOrgCreated}
unboundOrganizations={this.props.userOrganizations.filter(
@@ -392,7 +403,7 @@ const mapDispatchToProps = {
createOrganization: createOrganization as any,
deleteOrganization: deleteOrganization as any,
updateOrganization: updateOrganization as any,
skipOnboardingAction: skipOnboardingAction as any
skipOnboarding: skipOnboarding as any
};

export default whenLoggedIn(

+ 11
- 1
server/sonar-web/src/main/js/apps/create/organization/__tests__/ChooseRemoteOrganizationStep-test.tsx View File

@@ -31,7 +31,7 @@ it('should display an alert message', () => {
});

it('should display unbound installations', () => {
const installation = { installationId: '12345', name: 'Foo' };
const installation = { installationId: '12345', key: 'foo', name: 'Foo' };
const push = jest.fn();
const wrapper = shallowRender({
almUnboundApplications: [installation],
@@ -47,6 +47,16 @@ it('should display unbound installations', () => {
});
});

it('should display already bound alert message', () => {
expect(
shallowRender({
almInstallId: 'foo',
almOrganization: { avatar: 'foo-avatar', key: 'foo', name: 'Foo', personal: false },
boundOrganization: { avatar: 'bound-avatar', key: 'bound', name: 'Bound' }
}).find('Alert')
).toMatchSnapshot();
});

function shallowRender(props: Partial<ChooseRemoteOrganizationStep['props']> = {}) {
return shallow(
// @ts-ignore avoid passing everything from WithRouterProps

+ 22
- 16
server/sonar-web/src/main/js/apps/create/organization/__tests__/CreateOrganization-test.tsx View File

@@ -50,14 +50,16 @@ jest.mock('../../../../api/alm-integration', () => ({
}
}),
getAlmOrganization: jest.fn().mockResolvedValue({
avatar: 'my-avatar',
description: 'Continuous Code Quality',
key: 'sonarsource',
name: 'SonarSource',
personal: false,
url: 'https://www.sonarsource.com'
almOrganization: {
avatar: 'my-avatar',
description: 'Continuous Code Quality',
key: 'sonarsource',
name: 'SonarSource',
personal: false,
url: 'https://www.sonarsource.com'
}
}),
listUnboundApplications: jest.fn().mockResolvedValue({ applications: [] })
listUnboundApplications: jest.fn().mockResolvedValue([])
}));

jest.mock('../../../../api/organizations', () => ({
@@ -127,10 +129,12 @@ it('should render with auto tab selected and manual disabled', async () => {

it('should render with auto personal organization bind page', async () => {
(getAlmOrganization as jest.Mock<any>).mockResolvedValueOnce({
key: 'foo',
name: 'Foo',
avatar: 'my-avatar',
personal: true
almOrganization: {
key: 'foo',
name: 'Foo',
avatar: 'my-avatar',
personal: true
}
});
const wrapper = shallowRender({
currentUser: { ...user, externalProvider: 'github', personalOrganization: 'foo' },
@@ -143,10 +147,12 @@ it('should render with auto personal organization bind page', async () => {

it('should slugify and find a uniq organization key', async () => {
(getAlmOrganization as jest.Mock<any>).mockResolvedValueOnce({
avatar: 'https://avatars3.githubusercontent.com/u/37629810?v=4',
key: 'Foo&Bar',
name: 'Foo & Bar',
personal: true
almOrganization: {
avatar: 'https://avatars3.githubusercontent.com/u/37629810?v=4',
key: 'Foo&Bar',
name: 'Foo & Bar',
personal: true
}
});
(getOrganizations as jest.Mock<any>).mockResolvedValueOnce({
organizations: [{ key: 'foo-and-bar' }, { key: 'foo-and-bar-1' }]
@@ -246,7 +252,7 @@ function shallowRender(props: Partial<CreateOrganization['props']> = {}) {
// @ts-ignore avoid passing everything from WithRouterProps
location={{}}
router={mockRouter()}
skipOnboardingAction={jest.fn()}
skipOnboarding={jest.fn()}
updateOrganization={jest.fn()}
userOrganizations={[
{ actions: { admin: true }, key: 'foo', name: 'Foo' },

+ 2
- 2
server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/AutoPersonalOrganizationBind-test.tsx.snap View File

@@ -12,7 +12,7 @@ exports[`should render correctly 1`] = `
}
}
>
<p
<div
className="huge-spacer-bottom"
>
<FormattedMessage
@@ -44,7 +44,7 @@ exports[`should render correctly 1`] = `
}
}
/>
</p>
</div>
<OrganizationDetailsForm
keyReadOnly={true}
onContinue={[Function]}

+ 54
- 10
server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/ChooseRemoteOrganizationStep-test.tsx.snap View File

@@ -1,19 +1,62 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should display already bound alert message 1`] = `
<Alert
className="big-spacer-bottom width-60"
variant="error"
>
<FormattedMessage
defaultMessage="onboarding.import_organization.already_bound_x"
id="onboarding.import_organization.already_bound_x"
values={
Object {
"avatar": <img
alt="GitHub"
className="little-spacer-left"
src="/images/sonarcloud/github.svg"
width={16}
/>,
"boundAvatar": <OrganizationAvatar
className="little-spacer-left"
organization={
Object {
"avatar": "bound-avatar",
"key": "bound",
"name": "Bound",
}
}
small={true}
/>,
"boundName": <strong>
Bound
</strong>,
"name": <strong>
Foo
</strong>,
}
}
/>
</Alert>
`;

exports[`should display an alert message 1`] = `
<Alert
className="markdown big-spacer-bottom width-60"
className="big-spacer-bottom width-60"
variant="error"
>
onboarding.import_organization.org_not_found
<ul>
<li>
onboarding.import_organization.org_not_found.tips_1
</li>
<li>
onboarding.import_organization.org_not_found.tips_2
</li>
</ul>
<div
className="markdown"
>
onboarding.import_organization.org_not_found
<ul>
<li>
onboarding.import_organization.org_not_found.tips_1
</li>
<li>
onboarding.import_organization.org_not_found.tips_2
</li>
</ul>
</div>
</Alert>
`;

@@ -103,6 +146,7 @@ exports[`should display unbound installations 1`] = `
Array [
Object {
"installationId": "12345",
"key": "foo",
"name": "Foo",
},
]

+ 1
- 0
sonar-core/src/main/resources/org/sonar/l10n/core.properties View File

@@ -2775,6 +2775,7 @@ onboarding.import_organization.bitbucket=Import from BitBucket teams
onboarding.import_organization.github=Import from GitHub organizations
onboarding.import_organization.bind_existing=Bind to an existing sonarcloud organization
onboarding.import_organization.create_new=Create new SonarCloud organization from it
onboarding.import_organization.already_bound_x=Your organization {avatar} {name} is already bound to the SonarCloud organization {boundAvatar} {boundName}. Try again and choose a different organization.
onboarding.import_organization_x=Import {avatar} {name} into SonarCloud organization
onboarding.import_personal_organization_x=Bind {avatar} {name} with your personal SonarCloud organization {personalAvatar} {personalName}


Loading…
Cancel
Save