diff --git a/src/auth/googleauth.ts b/src/auth/googleauth.ts index 54f63f1a..60a0a197 100644 --- a/src/auth/googleauth.ts +++ b/src/auth/googleauth.ts @@ -170,19 +170,24 @@ export class GoogleAuth { // - Cloud SDK: `gcloud config config-helper --format json` // - GCE project ID from metadata server) if (!this._getDefaultProjectIdPromise) { - this._getDefaultProjectIdPromise = - new Promise(async (resolve, reject) => { - try { - const projectId = this.getProductionProjectId() || - await this.getFileProjectId() || - await this.getDefaultServiceProjectId() || - await this.getGCEProjectId(); - this._cachedProjectId = projectId; - resolve(projectId); - } catch (e) { - reject(e); - } - }); + this._getDefaultProjectIdPromise = new Promise(async (resolve, reject) => { + try { + const projectId = this.getProductionProjectId() || + await this.getFileProjectId() || + await this.getDefaultServiceProjectId() || + await this.getGCEProjectId(); + this._cachedProjectId = projectId; + if (!projectId) { + throw new Error( + 'Unable to detect a Project Id in the current environment. \n' + + 'To learn more about authentication and Google APIs, visit: \n' + + 'https://cloud.google.com/docs/authentication/getting-started'); + } + resolve(projectId); + } catch (e) { + reject(e); + } + }); } return this._getDefaultProjectIdPromise; } diff --git a/test/fixtures/private.json b/test/fixtures/private.json index 608d325c..d7760471 100644 --- a/test/fixtures/private.json +++ b/test/fixtures/private.json @@ -3,5 +3,6 @@ "private_key": "privatekey", "client_email": "hello@youarecool.com", "client_id": "client123", - "type": "service_account" + "type": "service_account", + "project_id": "not-a-project-id" } \ No newline at end of file diff --git a/test/test.googleauth.ts b/test/test.googleauth.ts index 50ad4dd5..09324ef0 100644 --- a/test/test.googleauth.ts +++ b/test/test.googleauth.ts @@ -78,7 +78,7 @@ describe('googleauth', () => { return nock(host).get(instancePath).reply(404); } - function createGetProjectIdNock(projectId: string) { + function createGetProjectIdNock(projectId = 'not-real') { return nock(host) .get(`${BASE_PATH}/project/project-id`) .reply(200, projectId, HEADERS); @@ -108,11 +108,6 @@ describe('googleauth', () => { return {auth, scopes: [scope1, scope2]}; } - // Matches the ending of a string. - function stringEndsWith(str: string, suffix: string) { - return str.indexOf(suffix, str.length - suffix.length) !== -1; - } - // Simulates a path join. function pathJoin(item1: string, item2: string) { return item1 + ':' + item2; @@ -871,16 +866,13 @@ describe('googleauth', () => { it('getApplicationDefault should return a new credential the first time and a cached credential the second time', async () => { - const scope = nockNotGCE(); // Create a function which will set up a GoogleAuth instance to match // on an environment variable json file, but not on anything else. mockEnvVar( 'GOOGLE_APPLICATION_CREDENTIALS', './test/fixtures/private.json'); - auth._fileExists = () => false; // Ask for credentials, the first time. const result = await auth.getApplicationDefault(); - scope.isDone(); assert.notEqual(null, result); // Capture the returned credential. @@ -931,11 +923,11 @@ describe('googleauth', () => { async () => { blockGoogleApplicationCredentialEnvironmentVariable(); auth._fileExists = () => false; - const scope = nockIsGCE(); + const scopes = [nockIsGCE(), createGetProjectIdNock()]; // Ask for credentials, the first time. const result = await auth.getApplicationDefault(); - scope.done(); + scopes.forEach(x => x.done()); assert.notEqual(null, result); // Capture the returned credential. @@ -1012,9 +1004,9 @@ describe('googleauth', () => { auth._pathJoin = pathJoin; auth._osPlatform = () => 'win32'; auth._fileExists = () => false; - const scope = nockIsGCE(); + const scopes = [nockIsGCE(), createGetProjectIdNock()]; const res = await auth.getApplicationDefault(); - scope.done(); + scopes.forEach(x => x.done()); // This indicates that we got a ComputeClient instance back, rather than // a JWTClient. assert.strictEqual( @@ -1141,7 +1133,7 @@ describe('googleauth', () => { } }; const scopes = [ - nockIsGCE(), + nockIsGCE(), createGetProjectIdNock(), nock(host).get(svcAccountPath).reply(200, response, HEADERS) ]; blockGoogleApplicationCredentialEnvironmentVariable(); @@ -1157,8 +1149,10 @@ describe('googleauth', () => { }); it('getCredentials should error if metadata server is not reachable', async () => { - const scopes = - [nockIsGCE(), nock(HOST_ADDRESS).get(svcAccountPath).reply(404)]; + const scopes = [ + nockIsGCE(), createGetProjectIdNock(), + nock(HOST_ADDRESS).get(svcAccountPath).reply(404) + ]; blockGoogleApplicationCredentialEnvironmentVariable(); auth._fileExists = () => false; await auth._checkIsGCE(); @@ -1172,8 +1166,10 @@ describe('googleauth', () => { it('getCredentials should error if body is empty', async () => { blockGoogleApplicationCredentialEnvironmentVariable(); auth._fileExists = () => false; - const scopes = - [nockIsGCE(), nock(HOST_ADDRESS).get(svcAccountPath).reply(200, {})]; + const scopes = [ + nockIsGCE(), createGetProjectIdNock(), + nock(HOST_ADDRESS).get(svcAccountPath).reply(200, {}) + ]; await auth._checkIsGCE(); assert.strictEqual(true, auth.isGCE); await assertRejects( @@ -1305,6 +1301,7 @@ describe('googleauth', () => { it('should get an access token', async () => { const {auth, scopes} = mockGCE(); + scopes.push(createGetProjectIdNock()); const token = await auth.getAccessToken(); scopes.forEach(s => s.done()); assert.strictEqual(token, 'abc123'); @@ -1312,6 +1309,7 @@ describe('googleauth', () => { it('should get request headers', async () => { const {auth, scopes} = mockGCE(); + scopes.push(createGetProjectIdNock()); const headers = await auth.getRequestHeaders(); scopes.forEach(s => s.done()); assert.deepStrictEqual(headers, {Authorization: 'Bearer abc123'}); @@ -1319,6 +1317,7 @@ describe('googleauth', () => { it('should authorize the request', async () => { const {auth, scopes} = mockGCE(); + scopes.push(createGetProjectIdNock()); const opts = await auth.authorizeRequest({url: 'http://example.com'}); scopes.forEach(s => s.done()); assert.deepStrictEqual(opts.headers, {Authorization: 'Bearer abc123'}); @@ -1359,9 +1358,9 @@ describe('googleauth', () => { it('should make the request', async () => { const url = 'http://example.com'; const {auth, scopes} = mockGCE(); + scopes.push(createGetProjectIdNock()); const data = {breakfast: 'coffee'}; - const scope = nock(url).get('/').reply(200, data); - scopes.push(scope); + scopes.push(nock(url).get('/').reply(200, data)); const res = await auth.request({url}); scopes.forEach(s => s.done()); assert.deepStrictEqual(res.data, data); @@ -1467,4 +1466,14 @@ describe('googleauth', () => { const client = await auth.getClient() as JWT; assert.strictEqual(client.subject, subject); }); + + it('should throw if getProjectId cannot find a projectId', async () => { + blockGoogleApplicationCredentialEnvironmentVariable(); + auth._fileExists = () => false; + // tslint:disable-next-line no-any + sinon.stub(auth as any, 'getDefaultServiceProjectId').resolves(); + await assertRejects( + auth.getProjectId(), + /Unable to detect a Project Id in the current environment/); + }); });