integration-react: more tests for ScmAuth + some redesign

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2021-08-29 16:58:46 +02:00
parent 41d129304a
commit 7f0aa89786
3 changed files with 148 additions and 48 deletions
@@ -20,26 +20,20 @@ import { ScmAuth } from './ScmAuth';
class MockOAuthApi implements OAuthApi {
constructor(private readonly accessToken: string) {}
async getAccessToken() {
getAccessToken = jest.fn(async () => {
return this.accessToken;
}
});
}
const mockApis = {
github: new MockOAuthApi('github-access-token'),
ghe: new MockOAuthApi('ghe-access-token'),
};
describe('ScmAuth', () => {
afterEach(() => {
jest.clearAllMocks();
});
it('should provide credentials for GitHub and GHE', async () => {
const mockGithubAuth = new MockOAuthApi('github-access-token');
const mockGheAuth = new MockOAuthApi('ghe-access-token');
const api = ScmAuth.mux(
ScmAuth.forGithub(mockApis.github),
ScmAuth.forGithub(mockApis.ghe, {
hostname: 'ghe.example.com',
ScmAuth.forGithub(mockGithubAuth),
ScmAuth.forGithub(mockGheAuth, {
host: 'ghe.example.com',
}),
);
@@ -54,6 +48,9 @@ describe('ScmAuth', () => {
await expect(
api.getCredentials({
url: 'https://ghe.example.com/backstage/backstage',
additionalScope: {
repoWrite: true,
},
}),
).resolves.toEqual({
token: 'ghe-access-token',
@@ -61,5 +58,116 @@ describe('ScmAuth', () => {
Authorization: 'Bearer ghe-access-token',
},
});
expect(mockGithubAuth.getAccessToken).toHaveBeenCalledTimes(1);
expect(mockGithubAuth.getAccessToken).toHaveBeenCalledWith(
['repo', 'read:org', 'read:user'],
{},
);
expect(mockGheAuth.getAccessToken).toHaveBeenCalledTimes(1);
expect(mockGheAuth.getAccessToken).toHaveBeenCalledWith(
['repo', 'read:org', 'read:user', 'gist'],
{},
);
});
it('should use correct scopes for each provider', async () => {
const mockAuthApi = {
getAccessToken: async (scopes: string[]) => {
return scopes.join(' ');
},
};
const githubAuth = ScmAuth.forGithub(mockAuthApi);
await expect(
githubAuth.getCredentials({ url: 'http://example.com' }),
).resolves.toMatchObject({
token: 'repo read:org read:user',
});
await expect(
githubAuth.getCredentials({
url: 'http://example.com',
additionalScope: { repoWrite: true },
}),
).resolves.toMatchObject({
token: 'repo read:org read:user gist',
});
const gitlabAuth = ScmAuth.forGitlab(mockAuthApi);
await expect(
gitlabAuth.getCredentials({ url: 'http://example.com' }),
).resolves.toMatchObject({
token: 'read_user read_api read_repository',
});
await expect(
gitlabAuth.getCredentials({
url: 'http://example.com',
additionalScope: { repoWrite: true },
}),
).resolves.toMatchObject({
token: 'read_user read_api write_repository api',
});
const azureAuth = ScmAuth.forAzure(mockAuthApi);
await expect(
azureAuth.getCredentials({ url: 'http://example.com' }),
).resolves.toMatchObject({
token: 'vso.build vso.code vso.graph vso.project vso.profile',
});
await expect(
azureAuth.getCredentials({
url: 'http://example.com',
additionalScope: { repoWrite: true },
}),
).resolves.toMatchObject({
token: 'vso.build vso.code_manage vso.graph vso.project vso.profile',
});
const bitbucketAuth = ScmAuth.forBitbucket(mockAuthApi);
await expect(
bitbucketAuth.getCredentials({ url: 'http://example.com' }),
).resolves.toMatchObject({
token: 'account team pullrequest snippet issue',
});
await expect(
bitbucketAuth.getCredentials({
url: 'http://example.com',
additionalScope: { repoWrite: true },
}),
).resolves.toMatchObject({
token: 'account team pullrequest:write snippet:write issue:write',
});
});
it('should throw an error for unknown URLs', async () => {
const emptyMux = ScmAuth.mux();
await expect(
emptyMux.getCredentials({ url: 'http://example.com' }),
).rejects.toThrow(
"No authentication provider available for access to 'http://example.com'",
);
const scmAuth = ScmAuth.mux(
ScmAuth.forAuthApi(new MockOAuthApi('token'), {
host: 'example.com',
scopeMapping: {
default: ['a'],
repoWrite: ['b'],
},
}),
);
await expect(
scmAuth.getCredentials({ url: 'http://example.com' }),
).resolves.toMatchObject({ token: 'token' });
await expect(
scmAuth.getCredentials({ url: 'http://not.example.com' }),
).rejects.toThrow(
"No authentication provider available for access to 'http://not.example.com'",
);
await expect(
scmAuth.getCredentials({ url: 'http://example.com:8080' }),
).rejects.toThrow(
"No authentication provider available for access to 'http://example.com:8080'",
);
});
});
+25 -31
View File
@@ -33,14 +33,14 @@ class ScmAuthMux implements ScmAuthApi {
this.#providers = providers;
}
getCredentials(
async getCredentials(
options: ScmAuthTokenOptions,
): Promise<ScmAuthTokenResponse | undefined> {
): Promise<ScmAuthTokenResponse> {
const url = new URL(options.url);
const provider = this.#providers.find(p => p.isUrlSupported(url));
if (!provider) {
throw new Error(
`No authentication provider available for SCM access to '${options.url}'`,
`No authentication provider available for access to '${options.url}'`,
);
}
@@ -52,24 +52,24 @@ export class ScmAuth implements ScmAuthApi {
static forAuthApi(
authApi: OAuthApi,
options: {
hostname: string;
host: string;
scopeMapping: {
default: string[];
repoWrite: string[];
};
},
): ScmAuthApi {
return new ScmAuth(authApi, options.hostname, options.scopeMapping);
): ScmAuth {
return new ScmAuth(authApi, options.host, options.scopeMapping);
}
static forGithub(
githubAuthApi: OAuthApi,
options?: {
hostname?: string;
host?: string;
},
): ScmAuth {
const hostname = options?.hostname ?? 'github.com';
return new ScmAuth(githubAuthApi, hostname, {
const host = options?.host ?? 'github.com';
return new ScmAuth(githubAuthApi, host, {
default: ['repo', 'read:org', 'read:user'],
repoWrite: ['repo', 'read:org', 'read:user', 'gist'],
});
@@ -78,11 +78,11 @@ export class ScmAuth implements ScmAuthApi {
static forGitlab(
gitlabAuthApi: OAuthApi,
options?: {
hostname?: string;
host?: string;
},
): ScmAuth {
const hostname = options?.hostname ?? 'gitlab.com';
return new ScmAuth(gitlabAuthApi, hostname, {
const host = options?.host ?? 'gitlab.com';
return new ScmAuth(gitlabAuthApi, host, {
default: ['read_user', 'read_api', 'read_repository'],
repoWrite: ['read_user', 'read_api', 'write_repository', 'api'],
});
@@ -91,11 +91,11 @@ export class ScmAuth implements ScmAuthApi {
static forAzure(
microsoftAuthApiRef: OAuthApi,
options?: {
hostname?: string;
host?: string;
},
): ScmAuth {
const hostname = options?.hostname ?? 'dev.azure.com';
return new ScmAuth(microsoftAuthApiRef, hostname, {
const host = options?.host ?? 'dev.azure.com';
return new ScmAuth(microsoftAuthApiRef, host, {
default: [
'vso.build',
'vso.code',
@@ -116,11 +116,11 @@ export class ScmAuth implements ScmAuthApi {
static forBitbucket(
bitbucketAuthApi: OAuthApi,
options?: {
hostname?: string;
host?: string;
},
): ScmAuth {
const hostname = options?.hostname ?? 'bitbucket.org';
return new ScmAuth(bitbucketAuthApi, hostname, {
const host = options?.host ?? 'bitbucket.org';
return new ScmAuth(bitbucketAuthApi, host, {
default: ['account', 'team', 'pullrequest', 'snippet', 'issue'],
repoWrite: [
'account',
@@ -137,16 +137,12 @@ export class ScmAuth implements ScmAuthApi {
}
#api: OAuthApi;
#hostname: string;
#host: string;
#scopeMapping: ScopeMapping;
private constructor(
api: OAuthApi,
hostname: string,
scopeMapping: ScopeMapping,
) {
private constructor(api: OAuthApi, host: string, scopeMapping: ScopeMapping) {
this.#api = api;
this.#hostname = hostname;
this.#host = host;
this.#scopeMapping = scopeMapping;
}
@@ -154,20 +150,18 @@ export class ScmAuth implements ScmAuthApi {
* Checks whether the implementation is able to provide authentication for the given URL.
*/
isUrlSupported(url: URL): boolean {
return url.hostname === this.#hostname;
return url.host === this.#host;
}
async getCredentials(
options: ScmAuthTokenOptions,
): Promise<ScmAuthTokenResponse> {
const scopes = options.additionalScope?.repoWrite
const { url, additionalScope, ...restOptions } = options;
const scopes = additionalScope?.repoWrite
? this.#scopeMapping.repoWrite
: this.#scopeMapping.default;
const token = await this.#api.getAccessToken(scopes, {
instantPopup: options.instantPopup,
optional: options.optional,
});
const token = await this.#api.getAccessToken(scopes, restOptions);
return {
token,
headers: {
@@ -64,9 +64,7 @@ export interface ScmAuthApi {
/**
* Requests credentials for accessing an SCM resource.
*/
getCredentials(
options: ScmAuthTokenOptions,
): Promise<ScmAuthTokenResponse | undefined>;
getCredentials(options: ScmAuthTokenOptions): Promise<ScmAuthTokenResponse>;
}
export const scmAuthApiRef: ApiRef<ScmAuthApi> = createApiRef({