integration-react: more tests for ScmAuth + some redesign
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
@@ -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'",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user