From 8eb0b4c731371433d5b40d19588d7afc012172c9 Mon Sep 17 00:00:00 2001 From: lilichen-F <147837261+lilichen-F@users.noreply.github.com> Date: Sat, 12 Sep 2026 09:07:10 +0800 Subject: [PATCH] fix: hasAccess() silently loses error context when no token is available When there's no cached token and no way to get one (no refresh token, no private key, no grant type configured), hasAccess() returned false without touching lastError_, so getLastError() returned either undefined or a stale error from a previous, unrelated failure -- misleading whoever calls getLastError() to explain the false return to a user (see #543). Set lastError_ to a message describing the actual situation (redirect to getAuthorizationUrl(), or configure setPrivateKey()/setGrantType()) before returning false. Regenerated dist/OAuth2.gs via `npm run dist` (gulp) to keep it in sync with the src/ change, following the same pattern as #513. Verified against the existing mocha suite (59 passing before, 60 after) plus a new test covering this exact branch, asserting getLastError() now returns a message matching the new explanation. Co-Authored-By: Claude Sonnet 5 Reviewed-by: si-kui-a --- dist/OAuth2.gs | 9 ++++++++- src/Service.js | 9 ++++++++- test/test.js | 17 +++++++++++++++++ 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/dist/OAuth2.gs b/dist/OAuth2.gs index ce97757c..8701ef4f 100644 --- a/dist/OAuth2.gs +++ b/dist/OAuth2.gs @@ -570,7 +570,14 @@ Service_.prototype.hasAccess = function() { if (token && !this.isExpired_(token)) return true; // Token still has access. var canGetToken = (token && this.canRefresh_(token)) || this.privateKey_ || this.grantType_; - if (!canGetToken) return false; + if (!canGetToken) { + this.lastError_ = new Error('No token found and no way to generate one. ' + + 'The user needs to be redirected to the authorization URL returned ' + + 'by getAuthorizationUrl(), or the service needs to be configured ' + + 'with setPrivateKey() or setGrantType() for a token to be ' + + 'generated automatically.'); + return false; + } return this.lockable_(function() { // Get the token again, bypassing the local memory cache. diff --git a/src/Service.js b/src/Service.js index e7527fe1..e91253c2 100644 --- a/src/Service.js +++ b/src/Service.js @@ -477,7 +477,14 @@ Service_.prototype.hasAccess = function() { if (token && !this.isExpired_(token)) return true; // Token still has access. var canGetToken = (token && this.canRefresh_(token)) || this.privateKey_ || this.grantType_; - if (!canGetToken) return false; + if (!canGetToken) { + this.lastError_ = new Error('No token found and no way to generate one. ' + + 'The user needs to be redirected to the authorization URL returned ' + + 'by getAuthorizationUrl(), or the service needs to be configured ' + + 'with setPrivateKey() or setGrantType() for a token to be ' + + 'generated automatically.'); + return false; + } return this.lockable_(function() { // Get the token again, bypassing the local memory cache. diff --git a/test/test.js b/test/test.js index fe7aca71..4301f605 100644 --- a/test/test.js +++ b/test/test.js @@ -359,6 +359,23 @@ describe('Service', () => { service.hasAccess(); assert.equal(lock.counter, 0); }); + + it('should set lastError_ when there is no token and no way to get one', + () => { + var properties = new MockProperties(); + var service = OAuth2.createService('test') + .setClientId('abc') + .setClientSecret('def') + .setTokenUrl('http://www.example.com') + .setAuthorizationBaseUrl('http://www.example.com') + .setPropertyStore(properties); + + var result = service.hasAccess(); + + assert.isFalse(result); + assert.exists(service.getLastError()); + assert.match(service.getLastError().message, /no way to generate/); + }); }); describe('#refresh()', () => {