Skip to content

Commit

Permalink
Add tests
Browse files Browse the repository at this point in the history
Signed-off-by: Oliver Sand <oliver.sand@sda-se.com>
  • Loading branch information
Fox32 committed Oct 21, 2021
1 parent 3ba87f5 commit 665cfed
Show file tree
Hide file tree
Showing 6 changed files with 217 additions and 49 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
jest.mock('@octokit/graphql');
import { getVoidLogger } from '@backstage/backend-common';
import { LocationSpec } from '@backstage/catalog-model';
import { ConfigReader } from '@backstage/config';
Expand All @@ -24,6 +23,8 @@ import {
import { graphql } from '@octokit/graphql';
import { GithubOrgReaderProcessor } from './GithubOrgReaderProcessor';

jest.mock('@octokit/graphql');

describe('GithubOrgReaderProcessor', () => {
describe('implementation', () => {
const logger = getVoidLogger();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import {
} from './github';
import * as results from './results';
import { CatalogProcessor, CatalogProcessorEmit } from './types';
import { buildOrgHierarchy } from './util/org';
import { assignGroupsToUsers, buildOrgHierarchy } from './util/org';

type GraphQL = typeof graphql;

Expand Down Expand Up @@ -82,16 +82,7 @@ export class GithubOrgReaderProcessor implements CatalogProcessor {
`Read ${users.length} GitHub users and ${groups.length} GitHub groups in ${duration} seconds`,
);

// Fill out the hierarchy
const usersByName = new Map(users.map(u => [u.metadata.name, u]));
for (const [groupName, userNames] of groupMemberUsers.entries()) {
for (const userName of userNames) {
const user = usersByName.get(userName);
if (user && !user.spec.memberOf.includes(groupName)) {
user.spec.memberOf.push(groupName);
}
}
}
assignGroupsToUsers(users, groupMemberUsers);
buildOrgHierarchy(groups);

// Done!
Expand Down
49 changes: 32 additions & 17 deletions plugins/catalog-backend/src/ingestion/processors/util/org.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,16 @@
*/

import { GroupEntity, UserEntity } from '@backstage/catalog-model';
import { buildMemberOf, buildOrgHierarchy } from './org';
import { assignGroupsToUsers, buildMemberOf, buildOrgHierarchy } from './org';

function u(name: string, memberOf: string[] = []): UserEntity {
return {
apiVersion: 'backstage.io/v1alpha1',
kind: 'User',
metadata: { name },
spec: { memberOf },
};
}

function g(
name: string,
Expand Down Expand Up @@ -56,39 +65,45 @@ describe('buildOrgHierarchy', () => {
});
});

describe('assignGroupsToUsers', () => {
it('should assign groups to users', () => {
const users: UserEntity[] = [u('u1'), u('u2')];
const groupMemberUsers = new Map<string, string[]>([
['g1', ['u1', 'u2']],
['g2', ['u2']],
['g3', ['u3']],
]);

assignGroupsToUsers(users, groupMemberUsers);

expect(users[0].spec.memberOf).toEqual(['g1']);
expect(users[1].spec.memberOf).toEqual(['g1', 'g2']);
});
});

describe('buildMemberOf', () => {
it('fills indirect member of groups', () => {
const a = g('a', undefined, []);
const b = g('b', 'a', []);
const c = g('c', 'b', []);
const u: UserEntity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'User',
metadata: { name: 'n' },
spec: { profile: {}, memberOf: ['c'] },
};
const user = u('n', ['c']);

const groups = [a, b, c];
buildOrgHierarchy(groups);
buildMemberOf(groups, [u]);
expect(u.spec.memberOf).toEqual(expect.arrayContaining(['a', 'b', 'c']));
buildMemberOf(groups, [user]);
expect(user.spec.memberOf).toEqual(expect.arrayContaining(['a', 'b', 'c']));
});

it('takes group spec.members into account', () => {
const a = g('a', undefined, []);
const b = g('b', 'a', []);
const c = g('c', 'b', []);
c.spec.members = ['n'];
const u: UserEntity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'User',
metadata: { name: 'n' },
spec: { profile: {}, memberOf: [] },
};
const user = u('n');

const groups = [a, b, c];
buildOrgHierarchy(groups);
buildMemberOf(groups, [u]);
expect(u.spec.memberOf).toEqual(expect.arrayContaining(['a', 'b', 'c']));
buildMemberOf(groups, [user]);
expect(user.spec.memberOf).toEqual(expect.arrayContaining(['a', 'b', 'c']));
});
});
16 changes: 16 additions & 0 deletions plugins/catalog-backend/src/ingestion/processors/util/org.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,22 @@ export function buildOrgHierarchy(groups: GroupEntity[]) {
}
}

// Ensure that users have their direct group memberships.
export function assignGroupsToUsers(
users: UserEntity[],
groupMemberUsers: Map<string, string[]>,
) {
const usersByName = new Map(users.map(u => [u.metadata.name, u]));
for (const [groupName, userNames] of groupMemberUsers.entries()) {
for (const userName of userNames) {
const user = usersByName.get(userName);
if (user && !user.spec.memberOf.includes(groupName)) {
user.spec.memberOf.push(groupName);
}
}
}
}

// Ensure that users have their transitive group memberships. Requires that
// the groups were previously processed with buildOrgHierarchy()
export function buildMemberOf(groups: GroupEntity[], users: UserEntity[]) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,162 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { getVoidLogger } from '@backstage/backend-common';
import { GroupEntity, UserEntity } from '@backstage/catalog-model';
import {
GithubCredentialsProvider,
GitHubIntegrationConfig,
} from '@backstage/integration';
import { GitHubOrgEntityProvider } from '.';
import { EntityProviderConnection } from '../../providers';
import { withLocations } from './GitHubOrgEntityProvider';
import { graphql } from '@octokit/graphql';

jest.mock('@octokit/graphql');

describe('GitHubOrgEntityProvider', () => {
describe('read', () => {
afterEach(() => jest.resetAllMocks());

it('should read org data and apply mutation', async () => {
const mockClient = jest.fn();

mockClient
.mockResolvedValueOnce({
organization: {
membersWithRole: {
pageInfo: { hasNextPage: false },
nodes: [
{
login: 'a',
name: 'b',
bio: 'c',
email: 'd',
avatarUrl: 'e',
},
],
},
},
})
.mockResolvedValueOnce({
organization: {
teams: {
pageInfo: { hasNextPage: false },
nodes: [
{
slug: 'team',
combinedSlug: 'blah/team',
name: 'Team',
description: 'The one and only team',
avatarUrl: 'http://example.com/team.jpeg',
parentTeam: {
slug: 'parent',
combinedSlug: '',
members: { pageInfo: { hasNextPage: false }, nodes: [] },
},
members: {
pageInfo: { hasNextPage: false },
nodes: [{ login: 'a' }],
},
},
],
},
},
});

(graphql.defaults as jest.Mock).mockReturnValue(mockClient);

const entityProviderConnection: EntityProviderConnection = {
applyMutation: jest.fn(),
};

const logger = getVoidLogger();
const gitHubConfig: GitHubIntegrationConfig = {
host: 'https://github.com',
};

const mockGetCredentials = jest.fn().mockReturnValue({
headers: { token: 'blah' },
type: 'app',
});

jest.spyOn(GithubCredentialsProvider, 'create').mockReturnValue({
getCredentials: mockGetCredentials,
} as any);

const entityProvider = new GitHubOrgEntityProvider({
id: 'my-id',
orgUrl: 'https://github.com/backstage',
gitHubConfig,
logger,
});

entityProvider.connect(entityProviderConnection);

await entityProvider.read();

expect(entityProviderConnection.applyMutation).toBeCalledWith({
entities: [
{
entity: {
apiVersion: 'backstage.io/v1alpha1',
kind: 'User',
metadata: {
annotations: {
'backstage.io/managed-by-location':
'url:https://https://github.com/a',
'backstage.io/managed-by-origin-location':
'url:https://https://github.com/a',
'github.com/user-login': 'a',
},
description: 'c',
name: 'a',
},
spec: {
memberOf: ['team'],
profile: {
displayName: 'b',
email: 'd',
picture: 'e',
},
},
},
locationKey: 'github-org-provider:my-id',
},
{
entity: {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Group',
metadata: {
annotations: {
'backstage.io/managed-by-location':
'url:https://https://github.com/orgs/backstage/teams/team',
'backstage.io/managed-by-origin-location':
'url:https://https://github.com/orgs/backstage/teams/team',
'github.com/team-slug': 'blah/team',
},
name: 'team',
description: 'The one and only team',
},
spec: {
children: [],
parent: 'parent',
profile: {
displayName: 'Team',
picture: 'http://example.com/team.jpeg',
},
type: 'team',
},
},
locationKey: 'github-org-provider:my-id',
},
],
type: 'full',
});
});
});

describe('withLocations', () => {
it('should set location for user', () => {
const entity: UserEntity = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,21 +27,22 @@ import {
import { graphql } from '@octokit/graphql';
import { merge } from 'lodash';
import { Logger } from 'winston';
import {
EntityProvider,
EntityProviderConnection,
} from '../../providers/types';
import {
getOrganizationTeams,
getOrganizationUsers,
parseGitHubOrgUrl,
} from '../processors/github';
import { buildOrgHierarchy } from '../processors/util/org';
import {
EntityProvider,
EntityProviderConnection,
} from '../../providers/types';
import { assignGroupsToUsers, buildOrgHierarchy } from '../processors/util/org';

// TODO: Consider supporting an (optional) webhook that reacts on org changes

export class GitHubOrgEntityProvider implements EntityProvider {
private connection?: EntityProviderConnection;
private readonly credentialsProvider: GithubCredentialsProvider;

static fromConfig(
config: Config,
Expand Down Expand Up @@ -75,7 +76,11 @@ export class GitHubOrgEntityProvider implements EntityProvider {
gitHubConfig: GitHubIntegrationConfig;
logger: Logger;
},
) {}
) {
this.credentialsProvider = GithubCredentialsProvider.create(
options.gitHubConfig,
);
}

getProviderName() {
return `GitHubOrgEntityProvider:${this.options.id}`;
Expand All @@ -92,11 +97,8 @@ export class GitHubOrgEntityProvider implements EntityProvider {

const { markReadComplete } = trackProgress(this.options.logger);

const credentialsProvider = GithubCredentialsProvider.create(
this.options.gitHubConfig,
);
const { headers, type: tokenType } =
await credentialsProvider.getCredentials({
await this.credentialsProvider.getCredentials({
url: this.options.orgUrl,
});
const client = graphql.defaults({
Expand All @@ -110,16 +112,7 @@ export class GitHubOrgEntityProvider implements EntityProvider {
client,
org,
);
// Fill out the hierarchy
const usersByName = new Map(users.map(u => [u.metadata.name, u]));
for (const [groupName, userNames] of groupMemberUsers.entries()) {
for (const userName of userNames) {
const user = usersByName.get(userName);
if (user && !user.spec.memberOf.includes(groupName)) {
user.spec.memberOf.push(groupName);
}
}
}
assignGroupsToUsers(users, groupMemberUsers);
buildOrgHierarchy(groups);

const { markCommitComplete } = markReadComplete({ users, groups });
Expand Down

0 comments on commit 665cfed

Please sign in to comment.