From c8d09748e8d4d8650d4d00be0c5ef36b7999a103 Mon Sep 17 00:00:00 2001 From: Phil Cherner Date: Thu, 1 Aug 2019 15:11:39 -0700 Subject: [PATCH 01/13] Add simple file token cache --- package-lock.json | 41 +++------ src/plugins/login/azureLoginPlugin.ts | 7 +- .../login/utils/simpleFileTokenCache.ts | 92 +++++++++++++++++++ src/services/baseService.ts | 3 +- src/services/loginService.ts | 26 +++++- 5 files changed, 133 insertions(+), 36 deletions(-) create mode 100644 src/plugins/login/utils/simpleFileTokenCache.ts diff --git a/package-lock.json b/package-lock.json index d057c0fe..af0a8815 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4723,8 +4723,7 @@ }, "ansi-regex": { "version": "2.1.1", - "bundled": true, - "optional": true + "bundled": true }, "aproba": { "version": "1.2.0", @@ -4742,13 +4741,11 @@ }, "balanced-match": { "version": "1.0.0", - "bundled": true, - "optional": true + "bundled": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -4761,18 +4758,15 @@ }, "code-point-at": { "version": "1.1.0", - "bundled": true, - "optional": true + "bundled": true }, "concat-map": { "version": "0.0.1", - "bundled": true, - "optional": true + "bundled": true }, "console-control-strings": { "version": "1.1.0", - "bundled": true, - "optional": true + "bundled": true }, "core-util-is": { "version": "1.0.2", @@ -4875,8 +4869,7 @@ }, "inherits": { "version": "2.0.3", - "bundled": true, - "optional": true + "bundled": true }, "ini": { "version": "1.3.5", @@ -4886,7 +4879,6 @@ "is-fullwidth-code-point": { "version": "1.0.0", "bundled": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -4899,20 +4891,17 @@ "minimatch": { "version": "3.0.4", "bundled": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } }, "minimist": { "version": "0.0.8", - "bundled": true, - "optional": true + "bundled": true }, "minipass": { "version": "2.3.5", "bundled": true, - "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -4929,7 +4918,6 @@ "mkdirp": { "version": "0.5.1", "bundled": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -5002,8 +4990,7 @@ }, "number-is-nan": { "version": "1.0.1", - "bundled": true, - "optional": true + "bundled": true }, "object-assign": { "version": "4.1.1", @@ -5013,7 +5000,6 @@ "once": { "version": "1.4.0", "bundled": true, - "optional": true, "requires": { "wrappy": "1" } @@ -5089,8 +5075,7 @@ }, "safe-buffer": { "version": "5.1.2", - "bundled": true, - "optional": true + "bundled": true }, "safer-buffer": { "version": "2.1.2", @@ -5120,7 +5105,6 @@ "string-width": { "version": "1.0.2", "bundled": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -5138,7 +5122,6 @@ "strip-ansi": { "version": "3.0.1", "bundled": true, - "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -5177,13 +5160,11 @@ }, "wrappy": { "version": "1.0.2", - "bundled": true, - "optional": true + "bundled": true }, "yallist": { "version": "3.0.3", - "bundled": true, - "optional": true + "bundled": true } } }, diff --git a/src/plugins/login/azureLoginPlugin.ts b/src/plugins/login/azureLoginPlugin.ts index 14e12a1e..372d30f3 100644 --- a/src/plugins/login/azureLoginPlugin.ts +++ b/src/plugins/login/azureLoginPlugin.ts @@ -2,13 +2,17 @@ import Serverless from "serverless"; import { AzureLoginOptions, AzureLoginService } from "../../services/loginService"; import { AzureBasePlugin } from "../azureBasePlugin"; import { loginHooks } from "./loginHooks"; +import { SimpleFileTokenCache } from "./utils/simpleFileTokenCache"; export class AzureLoginPlugin extends AzureBasePlugin { + private fileTokenCache: SimpleFileTokenCache; public hooks: { [eventName: string]: Promise }; public constructor(serverless: Serverless, options: AzureLoginOptions) { super(serverless, options); + this.fileTokenCache = new SimpleFileTokenCache(); + this.hooks = {}; for (const h of loginHooks) { this.hooks[`before:${h}`] = this.login.bind(this); @@ -24,12 +28,13 @@ export class AzureLoginPlugin extends AzureBasePlugin { this.log("Logging into Azure"); try { - const authResult = await AzureLoginService.login(); + const authResult = await AzureLoginService.login({tokenCache: this.fileTokenCache}); this.serverless.variables["azureCredentials"] = authResult.credentials; // Use environment variable for sub ID or use the first subscription in the list (service principal can // have access to more than one subscription) this.serverless.variables["subscriptionId"] = this.options.subscriptionId || process.env.azureSubId || this.serverless.service.provider["subscriptionId"] || authResult.subscriptions[0].id; this.serverless.cli.log(`Using subscription ID: ${this.serverless.variables["subscriptionId"]}`); + this.fileTokenCache.addSubs(authResult.subscriptions); } catch (e) { this.log("Error logging into azure"); diff --git a/src/plugins/login/utils/simpleFileTokenCache.ts b/src/plugins/login/utils/simpleFileTokenCache.ts new file mode 100644 index 00000000..c9dc14f8 --- /dev/null +++ b/src/plugins/login/utils/simpleFileTokenCache.ts @@ -0,0 +1,92 @@ +import * as fs from "fs"; +import path from "path"; +import os from "os"; + +const CONFIG_DIRECTORY = path.join(os.homedir(), ".azure"); +const SLS_TOKEN_FILE = path.join(CONFIG_DIRECTORY, "slsTokenCache.json"); + +export class SimpleFileTokenCache { + private tokens: any[] = []; + private subscriptions: any[] = []; + public constructor() { + console.log("new Simple file toke cache"); + this.load(); + } + + public isSecureCache() { + throw "isSecureCache not implemented"; + } + + public add(entries: any, cb: any) { + console.log("adding") + this.tokens.push(...entries); + this.save(); + cb(); + } + + public addSubs(entries: any) { + console.log("adding") + this.subscriptions.push(...entries); + this.save(); + } + + public remove(entries: any, cb: any) { + console.log("remove") + this.tokens = this.tokens.filter(e => { + return !Object.keys(entries[0]).every(key => e[key] === entries[0][key]); + }); + this.save(); + cb(); + } + + public clear(cb: any) { + console.log("clear"); + this.tokens = []; + this.subscriptions = []; + this.save(); + cb(); + } + + public find(query, cb) { + console.log("find") + let result = this.tokens.filter(e => { + return Object.keys(query).every(key => e[key] === query[key]); + }); + cb(null, result); + return result; + } + + public empty() { + console.log("empty") + // this.deleteOld(); + return this.tokens.length === 0; + } + + public first() { + return this.tokens[0]; + } + + public listSubscriptions() { + return this.subscriptions; + } + + private load() { + try { + let savedCache = JSON.parse(fs.readFileSync(SLS_TOKEN_FILE).toString()); + this.tokens = savedCache.tokens; + this.tokens.map(t => t.expiresOn = new Date(t.expiresOn)) + this.subscriptions = savedCache.subscriptions; + } catch (e) {} + } + + public save() { + console.log("save") + fs.writeFileSync(SLS_TOKEN_FILE, JSON.stringify({tokens: this.tokens, subscriptions: this.subscriptions})); + } + + private deleteOld() { + this.tokens = this.tokens.filter( + t => t.expiresOn > Date.now() - 5 * 60 * 1000 + ); + } +} diff --git a/src/services/baseService.ts b/src/services/baseService.ts index c5715f45..a40269d4 100644 --- a/src/services/baseService.ts +++ b/src/services/baseService.ts @@ -14,6 +14,7 @@ import { } from "../models/serverless"; import { Guard } from "../shared/guard"; import { Utils } from "../shared/utils"; +import { SimpleFileTokenCache } from "../plugins/login/utils/simpleFileTokenCache"; export abstract class BaseService { protected baseUrl: string; @@ -124,7 +125,7 @@ export abstract class BaseService { * Get the access token from credentials token cache */ protected getAccessToken(): string { - return (this.credentials.tokenCache as any)._entries[0].accessToken; + return (this.credentials.tokenCache as SimpleFileTokenCache).first().accessToken; } /** diff --git a/src/services/loginService.ts b/src/services/loginService.ts index 94d00b9f..494ea9af 100644 --- a/src/services/loginService.ts +++ b/src/services/loginService.ts @@ -5,8 +5,10 @@ import { loginWithServicePrincipalSecretWithAuthResponse, AuthResponse, AzureTokenCredentialsOptions, - InteractiveLoginOptions, + InteractiveLoginOptions, + DeviceTokenCredentials, } from "@azure/ms-rest-nodeauth"; +import { SimpleFileTokenCache } from "../plugins/login/utils/simpleFileTokenCache"; export interface AzureLoginOptions extends Serverless.Options { subscriptionId?: string; @@ -32,9 +34,25 @@ export class AzureLoginService { } } - public static async interactiveLogin(options: InteractiveLoginOptions): Promise { - await open("https://microsoft.com/devicelogin"); - return await interactiveLoginWithAuthResponse(options); + public static async interactiveLogin(options?: InteractiveLoginOptions): Promise { + // await open("https://microsoft.com/devicelogin"); + // return await interactiveLoginWithAuthResponse(options); + var authResp: AuthResponse = {credentials: undefined, subscriptions: []}; + if(!(options.tokenCache as SimpleFileTokenCache).empty()){ + console.log("exisitng token"); + var devOptions = { + tokenCache: options.tokenCache as SimpleFileTokenCache + } + // I don't think DeviceTokenCredentials is what we want... maybe MSITokenCredentials? + authResp.credentials = new DeviceTokenCredentials(undefined, undefined, devOptions.tokenCache.first().userId, undefined, undefined, options.tokenCache); + authResp.subscriptions = devOptions.tokenCache.listSubscriptions(); + } else { + console.log("need to do interactive login now"); + await open("https://microsoft.com/devicelogin"); + authResp = await interactiveLoginWithAuthResponse(options); + } + return authResp;//iOptions ? interactiveLoginWithAuthResponse(iOptions) : interactiveLoginWithAuthResponse(); + } public static async servicePrincipalLogin(clientId: string, secret: string, tenantId: string, options: AzureTokenCredentialsOptions): Promise { From 923806627d45fc84302f52c0b570f9d85c5e5301 Mon Sep 17 00:00:00 2001 From: Phil Cherner Date: Fri, 2 Aug 2019 15:35:54 -0700 Subject: [PATCH 02/13] Address pr feedback --- src/plugins/login/azureLoginPlugin.ts | 7 +- .../login/utils/simpleFileTokenCache.ts | 65 ++++++++++--------- src/services/baseService.ts | 5 +- src/services/functionAppService.ts | 2 +- src/services/loginService.ts | 24 +++---- 5 files changed, 48 insertions(+), 55 deletions(-) diff --git a/src/plugins/login/azureLoginPlugin.ts b/src/plugins/login/azureLoginPlugin.ts index 372d30f3..14e12a1e 100644 --- a/src/plugins/login/azureLoginPlugin.ts +++ b/src/plugins/login/azureLoginPlugin.ts @@ -2,17 +2,13 @@ import Serverless from "serverless"; import { AzureLoginOptions, AzureLoginService } from "../../services/loginService"; import { AzureBasePlugin } from "../azureBasePlugin"; import { loginHooks } from "./loginHooks"; -import { SimpleFileTokenCache } from "./utils/simpleFileTokenCache"; export class AzureLoginPlugin extends AzureBasePlugin { - private fileTokenCache: SimpleFileTokenCache; public hooks: { [eventName: string]: Promise }; public constructor(serverless: Serverless, options: AzureLoginOptions) { super(serverless, options); - this.fileTokenCache = new SimpleFileTokenCache(); - this.hooks = {}; for (const h of loginHooks) { this.hooks[`before:${h}`] = this.login.bind(this); @@ -28,13 +24,12 @@ export class AzureLoginPlugin extends AzureBasePlugin { this.log("Logging into Azure"); try { - const authResult = await AzureLoginService.login({tokenCache: this.fileTokenCache}); + const authResult = await AzureLoginService.login(); this.serverless.variables["azureCredentials"] = authResult.credentials; // Use environment variable for sub ID or use the first subscription in the list (service principal can // have access to more than one subscription) this.serverless.variables["subscriptionId"] = this.options.subscriptionId || process.env.azureSubId || this.serverless.service.provider["subscriptionId"] || authResult.subscriptions[0].id; this.serverless.cli.log(`Using subscription ID: ${this.serverless.variables["subscriptionId"]}`); - this.fileTokenCache.addSubs(authResult.subscriptions); } catch (e) { this.log("Error logging into azure"); diff --git a/src/plugins/login/utils/simpleFileTokenCache.ts b/src/plugins/login/utils/simpleFileTokenCache.ts index c9dc14f8..5045f44d 100644 --- a/src/plugins/login/utils/simpleFileTokenCache.ts +++ b/src/plugins/login/utils/simpleFileTokenCache.ts @@ -1,69 +1,69 @@ import * as fs from "fs"; import path from "path"; import os from "os"; +import * as adal from "adal-node"; const CONFIG_DIRECTORY = path.join(os.homedir(), ".azure"); const SLS_TOKEN_FILE = path.join(CONFIG_DIRECTORY, "slsTokenCache.json"); -export class SimpleFileTokenCache { - private tokens: any[] = []; +export class SimpleFileTokenCache implements adal.TokenCache{ + private _entries: any[] = []; private subscriptions: any[] = []; public constructor() { console.log("new Simple file toke cache"); this.load(); } - public isSecureCache() { - throw "isSecureCache not implemented"; - } - public add(entries: any, cb: any) { console.log("adding") - this.tokens.push(...entries); + this._entries.push(...entries); this.save(); cb(); } - public addSubs(entries: any) { - console.log("adding") - this.subscriptions.push(...entries); - this.save(); - } - public remove(entries: any, cb: any) { console.log("remove") - this.tokens = this.tokens.filter(e => { + this._entries = this._entries.filter(e => { return !Object.keys(entries[0]).every(key => e[key] === entries[0][key]); }); this.save(); cb(); } - public clear(cb: any) { - console.log("clear"); - this.tokens = []; - this.subscriptions = []; - this.save(); - cb(); - } - public find(query, cb) { console.log("find") - let result = this.tokens.filter(e => { + let result = this._entries.filter(e => { return Object.keys(query).every(key => e[key] === query[key]); }); cb(null, result); return result; } - public empty() { + //-------- File toke cache specific methods + + public addSubs(entries: any) { + console.log("adding") + this.subscriptions.push(...entries); + this.save(); + } + + + public clear(cb: any) { + console.log("clear"); + this._entries = []; + this.subscriptions = []; + this.save(); + cb(); + } + + public isEmpty() { console.log("empty") // this.deleteOld(); - return this.tokens.length === 0; + return this._entries.length === 0; } public first() { - return this.tokens[0]; + return this._entries[0]; } public listSubscriptions() { @@ -73,19 +73,22 @@ export class SimpleFileTokenCache { private load() { try { let savedCache = JSON.parse(fs.readFileSync(SLS_TOKEN_FILE).toString()); - this.tokens = savedCache.tokens; - this.tokens.map(t => t.expiresOn = new Date(t.expiresOn)) + this._entries = savedCache._entries; + this._entries.map(t => t.expiresOn = new Date(t.expiresOn)) this.subscriptions = savedCache.subscriptions; - } catch (e) {} + console.log("loaded"); + } catch (e) { + console.log("not loaded"); + } } public save() { console.log("save") - fs.writeFileSync(SLS_TOKEN_FILE, JSON.stringify({tokens: this.tokens, subscriptions: this.subscriptions})); + fs.writeFileSync(SLS_TOKEN_FILE, JSON.stringify({_entries: this._entries, subscriptions: this.subscriptions})); } private deleteOld() { - this.tokens = this.tokens.filter( + this._entries = this._entries.filter( t => t.expiresOn > Date.now() - 5 * 60 * 1000 ); } diff --git a/src/services/baseService.ts b/src/services/baseService.ts index a40269d4..97431526 100644 --- a/src/services/baseService.ts +++ b/src/services/baseService.ts @@ -14,7 +14,6 @@ import { } from "../models/serverless"; import { Guard } from "../shared/guard"; import { Utils } from "../shared/utils"; -import { SimpleFileTokenCache } from "../plugins/login/utils/simpleFileTokenCache"; export abstract class BaseService { protected baseUrl: string; @@ -124,8 +123,8 @@ export abstract class BaseService { /** * Get the access token from credentials token cache */ - protected getAccessToken(): string { - return (this.credentials.tokenCache as SimpleFileTokenCache).first().accessToken; + protected async getAccessToken(): Promise { + return this.credentials.getToken().then((result) => result.accessToken); } /** diff --git a/src/services/functionAppService.ts b/src/services/functionAppService.ts index b0eb5cd5..ce5ff697 100644 --- a/src/services/functionAppService.ts +++ b/src/services/functionAppService.ts @@ -159,7 +159,7 @@ export class FunctionAppService extends BaseService { uri: `https://${scmDomain}/api/zipdeploy/`, json: true, headers: { - Authorization: `Bearer ${this.getAccessToken()}`, + Authorization: `Bearer ${await this.getAccessToken()}`, Accept: "*/*", ContentType: "application/octet-stream", } diff --git a/src/services/loginService.ts b/src/services/loginService.ts index 494ea9af..0973cf97 100644 --- a/src/services/loginService.ts +++ b/src/services/loginService.ts @@ -15,7 +15,6 @@ export interface AzureLoginOptions extends Serverless.Options { } export class AzureLoginService { - /** * Logs in via service principal login if environment variables are * set or via interactive login if environment variables are not set @@ -35,23 +34,20 @@ export class AzureLoginService { } public static async interactiveLogin(options?: InteractiveLoginOptions): Promise { - // await open("https://microsoft.com/devicelogin"); - // return await interactiveLoginWithAuthResponse(options); - var authResp: AuthResponse = {credentials: undefined, subscriptions: []}; - if(!(options.tokenCache as SimpleFileTokenCache).empty()){ - console.log("exisitng token"); - var devOptions = { - tokenCache: options.tokenCache as SimpleFileTokenCache - } - // I don't think DeviceTokenCredentials is what we want... maybe MSITokenCredentials? - authResp.credentials = new DeviceTokenCredentials(undefined, undefined, devOptions.tokenCache.first().userId, undefined, undefined, options.tokenCache); - authResp.subscriptions = devOptions.tokenCache.listSubscriptions(); + let authResp: AuthResponse = {credentials: undefined, subscriptions: []}; + const fileTokenCache = new SimpleFileTokenCache(); + if(!fileTokenCache.isEmpty()){ + console.log("not empty"); + authResp.credentials = new DeviceTokenCredentials(undefined, undefined, fileTokenCache.first().userId, undefined, undefined, fileTokenCache); + authResp.subscriptions = fileTokenCache.listSubscriptions(); } else { console.log("need to do interactive login now"); await open("https://microsoft.com/devicelogin"); - authResp = await interactiveLoginWithAuthResponse(options); + authResp = await interactiveLoginWithAuthResponse({...options, tokenCache: fileTokenCache}); + fileTokenCache.addSubs(authResp.subscriptions); } - return authResp;//iOptions ? interactiveLoginWithAuthResponse(iOptions) : interactiveLoginWithAuthResponse(); + + return authResp; } From 1299cc1fc908399d6da0ad77268943edfafce6e4 Mon Sep 17 00:00:00 2001 From: Jeremy De La Cruz Date: Mon, 5 Aug 2019 14:09:38 -0700 Subject: [PATCH 03/13] Added tests for existing and nonexisting cached login --- src/services/loginService.test.ts | 45 +++++++++++++++++++++++++------ 1 file changed, 37 insertions(+), 8 deletions(-) diff --git a/src/services/loginService.test.ts b/src/services/loginService.test.ts index a43e5114..c54fad22 100644 --- a/src/services/loginService.test.ts +++ b/src/services/loginService.test.ts @@ -2,21 +2,50 @@ import { AzureLoginService } from "./loginService"; jest.mock("open"); import open from "open"; -jest.mock("@azure/ms-rest-nodeauth") -import { interactiveLoginWithAuthResponse, loginWithServicePrincipalSecretWithAuthResponse } from "@azure/ms-rest-nodeauth"; +jest.mock("@azure/ms-rest-nodeauth"); +import * as nodeauth from "@azure/ms-rest-nodeauth"; + +jest.mock("../plugins/login/utils/simpleFileTokenCache"); +import { SimpleFileTokenCache } from "../plugins/login/utils/simpleFileTokenCache"; describe("Login Service", () => { - - it("logs in interactively", async () => { + + it("logs in interactively with no cached login", async () => { // Ensure env variables are not set delete process.env.azureSubId; delete process.env.azureServicePrincipalClientId; delete process.env.azureServicePrincipalPassword; delete process.env.azureServicePrincipalTenantId; + SimpleFileTokenCache.prototype.isEmpty = jest.fn(() => true); + + const emptyObj = { subscriptions: [] }; + Object.defineProperty(nodeauth, + "interactiveLoginWithAuthResponse", + { value: jest.fn(_obj => emptyObj) } + ); + + await AzureLoginService.login(); + expect(SimpleFileTokenCache).toBeCalled(); + expect(open).toBeCalledWith("https://microsoft.com/devicelogin"); + expect(nodeauth.interactiveLoginWithAuthResponse).toBeCalled(); + expect(SimpleFileTokenCache.prototype.addSubs).toBeCalledWith(emptyObj.subscriptions); + }); + + it("logs in interactively with a cached login", async () => { + // Ensure env variables are not set + delete process.env.azureSubId; + delete process.env.azureServicePrincipalClientId; + delete process.env.azureServicePrincipalPassword; + delete process.env.azureServicePrincipalTenantId; + + SimpleFileTokenCache.prototype.isEmpty = jest.fn(() => false); + SimpleFileTokenCache.prototype.first = jest.fn(() => ({ userId: "" })); + await AzureLoginService.login(); - expect(open).toBeCalledWith("https://microsoft.com/devicelogin") - expect(interactiveLoginWithAuthResponse).toBeCalled(); + expect(SimpleFileTokenCache).toBeCalled(); + expect(nodeauth.DeviceTokenCredentials).toBeCalled(); + expect(SimpleFileTokenCache.prototype.listSubscriptions).toBeCalled(); }); it("logs in with a service principal", async () => { @@ -27,11 +56,11 @@ describe("Login Service", () => { process.env.azureServicePrincipalTenantId = "azureServicePrincipalTenantId"; await AzureLoginService.login(); - expect(loginWithServicePrincipalSecretWithAuthResponse).toBeCalledWith( + expect(nodeauth.loginWithServicePrincipalSecretWithAuthResponse).toBeCalledWith( "azureServicePrincipalClientId", "azureServicePrincipalPassword", "azureServicePrincipalTenantId", undefined // would be options ); }); -}); \ No newline at end of file +}); From 68f111e832c6a6c1ee6a4f0e09838ac61505bc39 Mon Sep 17 00:00:00 2001 From: Jeremy De La Cruz Date: Mon, 5 Aug 2019 14:12:29 -0700 Subject: [PATCH 04/13] Cleaned up cached login test for login service --- src/services/loginService.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/loginService.test.ts b/src/services/loginService.test.ts index c54fad22..941b4fa3 100644 --- a/src/services/loginService.test.ts +++ b/src/services/loginService.test.ts @@ -32,7 +32,7 @@ describe("Login Service", () => { expect(SimpleFileTokenCache.prototype.addSubs).toBeCalledWith(emptyObj.subscriptions); }); - it("logs in interactively with a cached login", async () => { + it("logs in with a cached login", async () => { // Ensure env variables are not set delete process.env.azureSubId; delete process.env.azureServicePrincipalClientId; From 072272c5b1aa59cda3ef167c8ad923c201dd20a8 Mon Sep 17 00:00:00 2001 From: Phil Cherner Date: Tue, 6 Aug 2019 16:31:51 -0700 Subject: [PATCH 05/13] Clean up simpleFileTokenCache, add tests --- .../login/utils/simpleFileTokenCache.test.ts | 50 +++++++++++++++++++ .../login/utils/simpleFileTokenCache.ts | 40 +++++++-------- src/services/baseService.ts | 2 +- src/services/loginService.ts | 2 - src/test/mockFactory.ts | 13 ++++- 5 files changed, 81 insertions(+), 26 deletions(-) create mode 100644 src/plugins/login/utils/simpleFileTokenCache.test.ts diff --git a/src/plugins/login/utils/simpleFileTokenCache.test.ts b/src/plugins/login/utils/simpleFileTokenCache.test.ts new file mode 100644 index 00000000..30591747 --- /dev/null +++ b/src/plugins/login/utils/simpleFileTokenCache.test.ts @@ -0,0 +1,50 @@ +import fs from "fs"; +import mockFs from "mock-fs"; +import path from "path"; +import os from "os"; +import { MockFactory } from "../../../test/mockFactory"; +import { SimpleFileTokenCache } from "./simpleFileTokenCache"; + +describe("Simple File Token Cache", () => { + const CONFIG_DIRECTORY = path.join(os.homedir(), ".azure"); + const SLS_TOKEN_FILE = path.join(CONFIG_DIRECTORY, "slsTokenCache.json"); + const fileContent = JSON.stringify({ + _entries: [], + subscriptions: [], + }); + + beforeAll(() => { + const fakeFs = {}; + fakeFs[SLS_TOKEN_FILE] = fileContent; + mockFs(fakeFs, { createCwd: true, createTmp: true }); + // mockFs({}); + }); + + afterEach(() => { + jest.resetAllMocks(); + mockFs.restore(); + }); + + it("Tries to load file on creation", () => { + const readFileSpy = jest.spyOn(fs, "readFileSync"); + const calls = readFileSpy.mock.calls; + const testFileCache = new SimpleFileTokenCache(); + + expect(calls).toHaveLength(1); + expect([calls[0][0], calls[0][1]]).toEqual([SLS_TOKEN_FILE, {_entries: testEntries, subscriptions: []}]); + }) + + it("Saves to file after token is added", () => { + const writeFileSpy = jest.spyOn(fs, "writeFileSync"); + const calls = writeFileSpy.mock.calls; + const testFileCache = new SimpleFileTokenCache(); + const testEntries = MockFactory.createTestTokenCacheEntries(); + + + testFileCache.add(testEntries); + + expect(writeFileSpy).toBeCalledTimes(1); + // expect(calls).toHaveLength(1); + // expect([calls[0][0], calls[0][1]]).toEqual([SLS_TOKEN_FILE, {_entries: testEntries, subscriptions: []}]); + }) +}); \ No newline at end of file diff --git a/src/plugins/login/utils/simpleFileTokenCache.ts b/src/plugins/login/utils/simpleFileTokenCache.ts index 5045f44d..85b19d92 100644 --- a/src/plugins/login/utils/simpleFileTokenCache.ts +++ b/src/plugins/login/utils/simpleFileTokenCache.ts @@ -10,28 +10,28 @@ export class SimpleFileTokenCache implements adal.TokenCache{ private _entries: any[] = []; private subscriptions: any[] = []; public constructor() { - console.log("new Simple file toke cache"); this.load(); } - public add(entries: any, cb: any) { - console.log("adding") + public add(entries: any, cb?: any) { this._entries.push(...entries); this.save(); - cb(); + if(cb) { + cb(); + } } - public remove(entries: any, cb: any) { - console.log("remove") + public remove(entries: any, cb?: any) { this._entries = this._entries.filter(e => { return !Object.keys(entries[0]).every(key => e[key] === entries[0][key]); }); this.save(); - cb(); + if(cb) { + cb(); + } } - public find(query, cb) { - console.log("find") + public find(query: any, cb?: any) { let result = this._entries.filter(e => { return Object.keys(query).every(key => e[key] === query[key]); }); @@ -42,14 +42,20 @@ export class SimpleFileTokenCache implements adal.TokenCache{ //-------- File toke cache specific methods public addSubs(entries: any) { - console.log("adding") this.subscriptions.push(...entries); + this.subscriptions = this.subscriptions.reduce((acc, current) => { + const x = acc.find(item => item.id === current.id); + if (!x) { + return acc.concat([current]); + } else { + return acc; + } + }, []); this.save(); } public clear(cb: any) { - console.log("clear"); this._entries = []; this.subscriptions = []; this.save(); @@ -57,8 +63,6 @@ export class SimpleFileTokenCache implements adal.TokenCache{ } public isEmpty() { - console.log("empty") - // this.deleteOld(); return this._entries.length === 0; } @@ -76,20 +80,12 @@ export class SimpleFileTokenCache implements adal.TokenCache{ this._entries = savedCache._entries; this._entries.map(t => t.expiresOn = new Date(t.expiresOn)) this.subscriptions = savedCache.subscriptions; - console.log("loaded"); } catch (e) { - console.log("not loaded"); + console.log("Error Loading"); } } public save() { - console.log("save") fs.writeFileSync(SLS_TOKEN_FILE, JSON.stringify({_entries: this._entries, subscriptions: this.subscriptions})); } - - private deleteOld() { - this._entries = this._entries.filter( - t => t.expiresOn > Date.now() - 5 * 60 * 1000 - ); - } } diff --git a/src/services/baseService.ts b/src/services/baseService.ts index 97431526..cac2b790 100644 --- a/src/services/baseService.ts +++ b/src/services/baseService.ts @@ -139,7 +139,7 @@ export abstract class BaseService { options: any = {} ) { const defaultHeaders = { - Authorization: `Bearer ${this.getAccessToken()}` + Authorization: `Bearer ${await this.getAccessToken()}` }; const allHeaders = { diff --git a/src/services/loginService.ts b/src/services/loginService.ts index 0973cf97..01ec1e87 100644 --- a/src/services/loginService.ts +++ b/src/services/loginService.ts @@ -37,11 +37,9 @@ export class AzureLoginService { let authResp: AuthResponse = {credentials: undefined, subscriptions: []}; const fileTokenCache = new SimpleFileTokenCache(); if(!fileTokenCache.isEmpty()){ - console.log("not empty"); authResp.credentials = new DeviceTokenCredentials(undefined, undefined, fileTokenCache.first().userId, undefined, undefined, fileTokenCache); authResp.subscriptions = fileTokenCache.listSubscriptions(); } else { - console.log("need to do interactive login now"); await open("https://microsoft.com/devicelogin"); authResp = await interactiveLoginWithAuthResponse({...options, tokenCache: fileTokenCache}); fileTokenCache.addSubs(authResp.subscriptions); diff --git a/src/test/mockFactory.ts b/src/test/mockFactory.ts index ed5f074c..e02724a9 100644 --- a/src/test/mockFactory.ts +++ b/src/test/mockFactory.ts @@ -138,12 +138,23 @@ export class MockFactory { // TODO: Reduce usage on tokenCache._entries[0] credentials["tokenCache"] = { - _entries: [{ accessToken: "ABC123" }] + _entries: [this.createTestTokenCacheEntries()] }; return credentials; } + public static createTestTokenCacheEntries(count: number = 1): any[] { + const token: TokenResponse = { + tokenType: "Bearer", + accessToken: "ABC123", + userId: "example@user.com", + }; + const result = Array(count).fill(token); + + return result; + } + public static createTestTimestamp(): string { return "1562184492"; } From b9f4b2d9ba7ae1866d6bdfb20a71e51b1fc4982b Mon Sep 17 00:00:00 2001 From: Phil Cherner Date: Wed, 7 Aug 2019 15:09:28 -0700 Subject: [PATCH 06/13] Add param to simpleFileTokenCache constructor for path --- .../login/utils/simpleFileTokenCache.test.ts | 63 ++++++++++++++----- .../login/utils/simpleFileTokenCache.ts | 17 ++--- src/test/mockFactory.ts | 14 +++++ 3 files changed, 73 insertions(+), 21 deletions(-) diff --git a/src/plugins/login/utils/simpleFileTokenCache.test.ts b/src/plugins/login/utils/simpleFileTokenCache.test.ts index 30591747..16c2ccd3 100644 --- a/src/plugins/login/utils/simpleFileTokenCache.test.ts +++ b/src/plugins/login/utils/simpleFileTokenCache.test.ts @@ -6,45 +6,80 @@ import { MockFactory } from "../../../test/mockFactory"; import { SimpleFileTokenCache } from "./simpleFileTokenCache"; describe("Simple File Token Cache", () => { - const CONFIG_DIRECTORY = path.join(os.homedir(), ".azure"); - const SLS_TOKEN_FILE = path.join(CONFIG_DIRECTORY, "slsTokenCache.json"); - const fileContent = JSON.stringify({ + let fileContent = { _entries: [], subscriptions: [], + }; + + beforeEach(() => { + fileContent = { + _entries: [], + subscriptions: [], + }; }); beforeAll(() => { - const fakeFs = {}; - fakeFs[SLS_TOKEN_FILE] = fileContent; - mockFs(fakeFs, { createCwd: true, createTmp: true }); + // const fakeFs = {}; + // fakeFs[SLS_TOKEN_FILE] = fileContent; + // mockFs(fakeFs, { createCwd: true, createTmp: true }); // mockFs({}); }); afterEach(() => { - jest.resetAllMocks(); mockFs.restore(); + jest.resetAllMocks(); }); - it("Tries to load file on creation", () => { + it("Creates a load file on creation if none", () => { + mockFs({}); + const writeFileSpy = jest.spyOn(fs, "writeFileSync"); + // const calls = readFileSpy.mock.calls; + const testFileCache = new SimpleFileTokenCache("./slsTokenCache.json"); + + expect(writeFileSpy).toBeCalledTimes(1); + // expect(calls).toHaveLength(1); + // expect([calls[0][0], calls[0][1]]).toEqual([SLS_TOKEN_FILE, {_entries: testEntries, subscriptions: []}]); + }); + + it("Load file on creation if available", () => { + const fakeFs = {}; + fileContent._entries = MockFactory.createTestTokenCacheEntries(); + fakeFs["./slsTokenCache.json"] = fileContent; + mockFs(fakeFs, { createCwd: true, createTmp: true }); const readFileSpy = jest.spyOn(fs, "readFileSync"); const calls = readFileSpy.mock.calls; - const testFileCache = new SimpleFileTokenCache(); + const testFileCache = new SimpleFileTokenCache("./slsTokenCache.json"); - expect(calls).toHaveLength(1); - expect([calls[0][0], calls[0][1]]).toEqual([SLS_TOKEN_FILE, {_entries: testEntries, subscriptions: []}]); + expect(readFileSpy).toBeCalledTimes(1); + // expect(calls).toHaveLength(1); + // expect([calls[0][0], calls[0][1]]).toEqual([SLS_TOKEN_FILE, {_entries: testEntries, subscriptions: []}]); }) it("Saves to file after token is added", () => { const writeFileSpy = jest.spyOn(fs, "writeFileSync"); const calls = writeFileSpy.mock.calls; - const testFileCache = new SimpleFileTokenCache(); + const testFileCache = new SimpleFileTokenCache("./"); const testEntries = MockFactory.createTestTokenCacheEntries(); - testFileCache.add(testEntries); + expect(testFileCache.isEmpty()).toBe(false); expect(writeFileSpy).toBeCalledTimes(1); // expect(calls).toHaveLength(1); // expect([calls[0][0], calls[0][1]]).toEqual([SLS_TOKEN_FILE, {_entries: testEntries, subscriptions: []}]); - }) + }); + + it("Saves to file after subscription is added", () => { + const writeFileSpy = jest.spyOn(fs, "writeFileSync"); + const calls = writeFileSpy.mock.calls; + const testFileCache = new SimpleFileTokenCache("./"); + const testSubs = MockFactory.createTestSubscriptions(); + + + testFileCache.addSubs(testSubs); + + expect(writeFileSpy).toBeCalledTimes(1); + // expect(calls).toHaveLength(1); + // expect([calls[0][0], calls[0][1]]).toEqual([SLS_TOKEN_FILE, {_entries: testEntries, subscriptions: []}]); + }); }); \ No newline at end of file diff --git a/src/plugins/login/utils/simpleFileTokenCache.ts b/src/plugins/login/utils/simpleFileTokenCache.ts index 85b19d92..093ed0d2 100644 --- a/src/plugins/login/utils/simpleFileTokenCache.ts +++ b/src/plugins/login/utils/simpleFileTokenCache.ts @@ -4,12 +4,14 @@ import os from "os"; import * as adal from "adal-node"; const CONFIG_DIRECTORY = path.join(os.homedir(), ".azure"); -const SLS_TOKEN_FILE = path.join(CONFIG_DIRECTORY, "slsTokenCache.json"); +const DEFAULT_SLS_TOKEN_FILE = path.join(CONFIG_DIRECTORY, "slsTokenCache.json"); export class SimpleFileTokenCache implements adal.TokenCache{ private _entries: any[] = []; private subscriptions: any[] = []; - public constructor() { + private tokePath: string; + public constructor(tokenPath: string = DEFAULT_SLS_TOKEN_FILE) { + this.tokePath = tokenPath; this.load(); } @@ -75,17 +77,18 @@ export class SimpleFileTokenCache implements adal.TokenCache{ } private load() { - try { - let savedCache = JSON.parse(fs.readFileSync(SLS_TOKEN_FILE).toString()); + if(fs.existsSync(this.tokePath)){ + let savedCache = JSON.parse(fs.readFileSync(this.tokePath).toString()); this._entries = savedCache._entries; this._entries.map(t => t.expiresOn = new Date(t.expiresOn)) this.subscriptions = savedCache.subscriptions; - } catch (e) { - console.log("Error Loading"); + } else { + // console.log("No saved cache to load, creating new file cache"); + this.save(); } } public save() { - fs.writeFileSync(SLS_TOKEN_FILE, JSON.stringify({_entries: this._entries, subscriptions: this.subscriptions})); + fs.writeFileSync(this.tokePath, JSON.stringify({_entries: this._entries, subscriptions: this.subscriptions})); } } diff --git a/src/test/mockFactory.ts b/src/test/mockFactory.ts index e02724a9..bbc51c7d 100644 --- a/src/test/mockFactory.ts +++ b/src/test/mockFactory.ts @@ -155,6 +155,20 @@ export class MockFactory { return result; } + public static createTestSubscriptions(count: number = 1): any[] { + const sub = { + id: "abc-1234-5678", + state: "Enabled", + authorizationSource: "RoleBased", + user: { name: "example@user.com", type: "user" }, + environmentName: "AzureCloud", + name: "Test Sub" + }; + const result = Array(count).fill(sub); + + return result; + } + public static createTestTimestamp(): string { return "1562184492"; } From d7acb10b467834114e31906c45dfca4751375e29 Mon Sep 17 00:00:00 2001 From: Wallace Breza Date: Wed, 7 Aug 2019 17:12:52 -0700 Subject: [PATCH 07/13] Updated simple file token cache tests --- .../login/utils/simpleFileTokenCache.test.ts | 91 +++++++++---------- .../login/utils/simpleFileTokenCache.ts | 40 ++++---- 2 files changed, 63 insertions(+), 68 deletions(-) diff --git a/src/plugins/login/utils/simpleFileTokenCache.test.ts b/src/plugins/login/utils/simpleFileTokenCache.test.ts index 16c2ccd3..ada9a4d1 100644 --- a/src/plugins/login/utils/simpleFileTokenCache.test.ts +++ b/src/plugins/login/utils/simpleFileTokenCache.test.ts @@ -1,85 +1,82 @@ import fs from "fs"; import mockFs from "mock-fs"; -import path from "path"; -import os from "os"; import { MockFactory } from "../../../test/mockFactory"; import { SimpleFileTokenCache } from "./simpleFileTokenCache"; describe("Simple File Token Cache", () => { + const tokenFilePath = "slsTokenCache.json"; + let fileContent = { - _entries: [], + entries: [], subscriptions: [], }; - - beforeEach(() => { - fileContent = { - _entries: [], - subscriptions: [], - }; - }); - - beforeAll(() => { - // const fakeFs = {}; - // fakeFs[SLS_TOKEN_FILE] = fileContent; - // mockFs(fakeFs, { createCwd: true, createTmp: true }); - // mockFs({}); - }); afterEach(() => { mockFs.restore(); - jest.resetAllMocks(); }); it("Creates a load file on creation if none", () => { - mockFs({}); + mockFs(); + const writeFileSpy = jest.spyOn(fs, "writeFileSync"); - // const calls = readFileSpy.mock.calls; - const testFileCache = new SimpleFileTokenCache("./slsTokenCache.json"); + new SimpleFileTokenCache(tokenFilePath); - expect(writeFileSpy).toBeCalledTimes(1); - // expect(calls).toHaveLength(1); - // expect([calls[0][0], calls[0][1]]).toEqual([SLS_TOKEN_FILE, {_entries: testEntries, subscriptions: []}]); + const expected = { + entries: [], + subscriptions: [], + }; + + expect(writeFileSpy).toBeCalledWith(tokenFilePath, JSON.stringify(expected)); + writeFileSpy.mockRestore(); }); it("Load file on creation if available", () => { - const fakeFs = {}; - fileContent._entries = MockFactory.createTestTokenCacheEntries(); - fakeFs["./slsTokenCache.json"] = fileContent; - mockFs(fakeFs, { createCwd: true, createTmp: true }); + fileContent.entries = MockFactory.createTestTokenCacheEntries(); + + mockFs({ + "slsTokenCache.json": JSON.stringify(fileContent), + }); + const readFileSpy = jest.spyOn(fs, "readFileSync"); - const calls = readFileSpy.mock.calls; - const testFileCache = new SimpleFileTokenCache("./slsTokenCache.json"); + const tokenCache = new SimpleFileTokenCache(tokenFilePath); - expect(readFileSpy).toBeCalledTimes(1); - // expect(calls).toHaveLength(1); - // expect([calls[0][0], calls[0][1]]).toEqual([SLS_TOKEN_FILE, {_entries: testEntries, subscriptions: []}]); + expect(readFileSpy).toBeCalled(); + expect(tokenCache.first()).not.toBeNull(); + readFileSpy.mockRestore(); }) it("Saves to file after token is added", () => { + mockFs(); + const writeFileSpy = jest.spyOn(fs, "writeFileSync"); - const calls = writeFileSpy.mock.calls; - const testFileCache = new SimpleFileTokenCache("./"); + const tokenCache = new SimpleFileTokenCache(tokenFilePath); const testEntries = MockFactory.createTestTokenCacheEntries(); - testFileCache.add(testEntries); + tokenCache.add(testEntries); + + const expected = { + entries: testEntries, + subscriptions: [], + }; - expect(testFileCache.isEmpty()).toBe(false); - expect(writeFileSpy).toBeCalledTimes(1); - // expect(calls).toHaveLength(1); - // expect([calls[0][0], calls[0][1]]).toEqual([SLS_TOKEN_FILE, {_entries: testEntries, subscriptions: []}]); + expect(tokenCache.isEmpty()).toBe(false); + expect(writeFileSpy).toBeCalledWith(tokenFilePath, JSON.stringify(expected)); }); it("Saves to file after subscription is added", () => { + mockFs(); + const writeFileSpy = jest.spyOn(fs, "writeFileSync"); - const calls = writeFileSpy.mock.calls; - const testFileCache = new SimpleFileTokenCache("./"); + const testFileCache = new SimpleFileTokenCache(tokenFilePath); const testSubs = MockFactory.createTestSubscriptions(); - testFileCache.addSubs(testSubs); - expect(writeFileSpy).toBeCalledTimes(1); - // expect(calls).toHaveLength(1); - // expect([calls[0][0], calls[0][1]]).toEqual([SLS_TOKEN_FILE, {_entries: testEntries, subscriptions: []}]); + const expected = { + entries: [], + subscriptions: testSubs, + }; + + expect(writeFileSpy).toBeCalledWith(tokenFilePath, JSON.stringify(expected)); }); -}); \ No newline at end of file +}); diff --git a/src/plugins/login/utils/simpleFileTokenCache.ts b/src/plugins/login/utils/simpleFileTokenCache.ts index 093ed0d2..231d99fa 100644 --- a/src/plugins/login/utils/simpleFileTokenCache.ts +++ b/src/plugins/login/utils/simpleFileTokenCache.ts @@ -1,4 +1,4 @@ -import * as fs from "fs"; +import fs from "fs"; import path from "path"; import os from "os"; import * as adal from "adal-node"; @@ -6,35 +6,34 @@ import * as adal from "adal-node"; const CONFIG_DIRECTORY = path.join(os.homedir(), ".azure"); const DEFAULT_SLS_TOKEN_FILE = path.join(CONFIG_DIRECTORY, "slsTokenCache.json"); -export class SimpleFileTokenCache implements adal.TokenCache{ - private _entries: any[] = []; +export class SimpleFileTokenCache implements adal.TokenCache { + private entries: any[] = []; private subscriptions: any[] = []; - private tokePath: string; - public constructor(tokenPath: string = DEFAULT_SLS_TOKEN_FILE) { - this.tokePath = tokenPath; + + public constructor(private tokenPath: string = DEFAULT_SLS_TOKEN_FILE) { this.load(); } public add(entries: any, cb?: any) { - this._entries.push(...entries); + this.entries.push(...entries); this.save(); - if(cb) { + if (cb) { cb(); } } public remove(entries: any, cb?: any) { - this._entries = this._entries.filter(e => { + this.entries = this.entries.filter(e => { return !Object.keys(entries[0]).every(key => e[key] === entries[0][key]); }); this.save(); - if(cb) { + if (cb) { cb(); } } public find(query: any, cb?: any) { - let result = this._entries.filter(e => { + let result = this.entries.filter(e => { return Object.keys(query).every(key => e[key] === query[key]); }); cb(null, result); @@ -58,37 +57,36 @@ export class SimpleFileTokenCache implements adal.TokenCache{ public clear(cb: any) { - this._entries = []; + this.entries = []; this.subscriptions = []; this.save(); cb(); } public isEmpty() { - return this._entries.length === 0; + return this.entries.length === 0; } public first() { - return this._entries[0]; + return this.entries[0]; } public listSubscriptions() { return this.subscriptions; } - + private load() { - if(fs.existsSync(this.tokePath)){ - let savedCache = JSON.parse(fs.readFileSync(this.tokePath).toString()); - this._entries = savedCache._entries; - this._entries.map(t => t.expiresOn = new Date(t.expiresOn)) + if (fs.existsSync(this.tokenPath)) { + let savedCache = JSON.parse(fs.readFileSync(this.tokenPath).toString()); + this.entries = savedCache.entries; + this.entries.map(t => t.expiresOn = new Date(t.expiresOn)) this.subscriptions = savedCache.subscriptions; } else { - // console.log("No saved cache to load, creating new file cache"); this.save(); } } public save() { - fs.writeFileSync(this.tokePath, JSON.stringify({_entries: this._entries, subscriptions: this.subscriptions})); + fs.writeFileSync(this.tokenPath, JSON.stringify({ entries: this.entries, subscriptions: this.subscriptions })); } } From 5063132588f73b21e0bfb7429544fbcf2be53759 Mon Sep 17 00:00:00 2001 From: Wallace Breza Date: Wed, 7 Aug 2019 17:18:30 -0700 Subject: [PATCH 08/13] Restore fs mocks at end of each test --- src/plugins/login/utils/simpleFileTokenCache.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/plugins/login/utils/simpleFileTokenCache.test.ts b/src/plugins/login/utils/simpleFileTokenCache.test.ts index ada9a4d1..09919d92 100644 --- a/src/plugins/login/utils/simpleFileTokenCache.test.ts +++ b/src/plugins/login/utils/simpleFileTokenCache.test.ts @@ -61,6 +61,7 @@ describe("Simple File Token Cache", () => { expect(tokenCache.isEmpty()).toBe(false); expect(writeFileSpy).toBeCalledWith(tokenFilePath, JSON.stringify(expected)); + writeFileSpy.mockRestore(); }); it("Saves to file after subscription is added", () => { @@ -78,5 +79,6 @@ describe("Simple File Token Cache", () => { }; expect(writeFileSpy).toBeCalledWith(tokenFilePath, JSON.stringify(expected)); + writeFileSpy.mockRestore(); }); }); From 90e7e72c85bc908cc99643f9495c1c31f8e94887 Mon Sep 17 00:00:00 2001 From: Phil Cherner Date: Wed, 7 Aug 2019 18:08:57 -0700 Subject: [PATCH 09/13] Fix getTokens in mock --- src/services/functionAppService.test.ts | 6 +++--- src/test/mockFactory.ts | 10 +--------- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/src/services/functionAppService.test.ts b/src/services/functionAppService.test.ts index 9de1cf7a..715819a7 100644 --- a/src/services/functionAppService.test.ts +++ b/src/services/functionAppService.test.ts @@ -211,7 +211,7 @@ describe("Function App Service", () => { uri: expectedUploadUrl, json: true, headers: { - Authorization: `Bearer ${variables["azureCredentials"].tokenCache._entries[0].accessToken}`, + Authorization: `Bearer ${(await variables["azureCredentials"].getToken()).accessToken}`, Accept: "*/*", ContentType: "application/octet-stream", } @@ -242,7 +242,7 @@ describe("Function App Service", () => { uri: expectedUploadUrl, json: true, headers: { - Authorization: `Bearer ${variables["azureCredentials"].tokenCache._entries[0].accessToken}`, + Authorization: `Bearer ${(await variables["azureCredentials"].getToken()).accessToken}`, Accept: "*/*", ContentType: "application/octet-stream", } @@ -276,7 +276,7 @@ describe("Function App Service", () => { uri: expectedUploadUrl, json: true, headers: { - Authorization: `Bearer ${variables["azureCredentials"].tokenCache._entries[0].accessToken}`, + Authorization: `Bearer ${(await variables["azureCredentials"].getToken()).accessToken}`, Accept: "*/*", ContentType: "application/octet-stream", } diff --git a/src/test/mockFactory.ts b/src/test/mockFactory.ts index bbc51c7d..1fe96471 100644 --- a/src/test/mockFactory.ts +++ b/src/test/mockFactory.ts @@ -376,15 +376,7 @@ export class MockFactory { public static createTestVariables() { return { - azureCredentials: { - tokenCache: { - _entries: [ - { - accessToken: "token" - } - ] - } - }, + azureCredentials: this.createTestAzureCredentials(), subscriptionId: "azureSubId", } } From 144598d9d6e4a6b0e9ee77be1d151a9d9b68a90d Mon Sep 17 00:00:00 2001 From: Phil Cherner Date: Thu, 8 Aug 2019 11:42:42 -0700 Subject: [PATCH 10/13] Fixing broken tests --- src/plugins/login/azureLoginPlugin.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plugins/login/azureLoginPlugin.test.ts b/src/plugins/login/azureLoginPlugin.test.ts index 9e715952..51e43b3f 100644 --- a/src/plugins/login/azureLoginPlugin.test.ts +++ b/src/plugins/login/azureLoginPlugin.test.ts @@ -68,7 +68,7 @@ describe("Login Plugin", () => { undefined // would be options ) expect(AzureLoginService.interactiveLogin).not.toBeCalled(); - expect(sls.variables["azureCredentials"]).toEqual(credentials); + expect(JSON.stringify(sls.variables["azureCredentials"])).toEqual(JSON.stringify(credentials)); expect(sls.variables["subscriptionId"]).toEqual("azureSubId"); }); @@ -78,7 +78,7 @@ describe("Login Plugin", () => { await invokeLoginHook(false, sls); expect(AzureLoginService.servicePrincipalLogin).not.toBeCalled(); expect(AzureLoginService.interactiveLogin).toBeCalled(); - expect(sls.variables["azureCredentials"]).toEqual(credentials); + expect(JSON.stringify(sls.variables["azureCredentials"])).toEqual(JSON.stringify(credentials)); expect(sls.variables["subscriptionId"]).toEqual("azureSubId"); }); From 8cf6f3333b8ba4f637db696224d1f36b12581ae1 Mon Sep 17 00:00:00 2001 From: Phil Cherner Date: Thu, 8 Aug 2019 13:34:37 -0700 Subject: [PATCH 11/13] Address pr feedback --- src/services/baseService.ts | 3 ++- src/services/loginService.ts | 8 ++++---- src/test/mockFactory.ts | 12 ++---------- 3 files changed, 8 insertions(+), 15 deletions(-) diff --git a/src/services/baseService.ts b/src/services/baseService.ts index cac2b790..50d312cb 100644 --- a/src/services/baseService.ts +++ b/src/services/baseService.ts @@ -124,7 +124,8 @@ export abstract class BaseService { * Get the access token from credentials token cache */ protected async getAccessToken(): Promise { - return this.credentials.getToken().then((result) => result.accessToken); + const token = await this.credentials.getToken(); + return token ? token.accessToken : null; } /** diff --git a/src/services/loginService.ts b/src/services/loginService.ts index 01ec1e87..6bf433f2 100644 --- a/src/services/loginService.ts +++ b/src/services/loginService.ts @@ -36,13 +36,13 @@ export class AzureLoginService { public static async interactiveLogin(options?: InteractiveLoginOptions): Promise { let authResp: AuthResponse = {credentials: undefined, subscriptions: []}; const fileTokenCache = new SimpleFileTokenCache(); - if(!fileTokenCache.isEmpty()){ - authResp.credentials = new DeviceTokenCredentials(undefined, undefined, fileTokenCache.first().userId, undefined, undefined, fileTokenCache); - authResp.subscriptions = fileTokenCache.listSubscriptions(); - } else { + if(fileTokenCache.isEmpty()){ await open("https://microsoft.com/devicelogin"); authResp = await interactiveLoginWithAuthResponse({...options, tokenCache: fileTokenCache}); fileTokenCache.addSubs(authResp.subscriptions); + } else { + authResp.credentials = new DeviceTokenCredentials(undefined, undefined, fileTokenCache.first().userId, undefined, undefined, fileTokenCache); + authResp.subscriptions = fileTokenCache.listSubscriptions(); } return authResp; diff --git a/src/test/mockFactory.ts b/src/test/mockFactory.ts index 1fe96471..74264e58 100644 --- a/src/test/mockFactory.ts +++ b/src/test/mockFactory.ts @@ -126,25 +126,17 @@ export class MockFactory { public static createTestAzureCredentials(): TokenClientCredentials { const credentials = { getToken: jest.fn(() => { - const token: TokenResponse = { - tokenType: "Bearer", - accessToken: "ABC123", - }; + const token: TokenResponse = this.createTestTokenCacheEntries()[0]; return Promise.resolve(token); }), signRequest: jest.fn((resource) => Promise.resolve(resource)), }; - // TODO: Reduce usage on tokenCache._entries[0] - credentials["tokenCache"] = { - _entries: [this.createTestTokenCacheEntries()] - }; - return credentials; } - public static createTestTokenCacheEntries(count: number = 1): any[] { + public static createTestTokenCacheEntries(count: number = 1): TokenResponse[] { const token: TokenResponse = { tokenType: "Bearer", accessToken: "ABC123", From 4771971f2d9a6a5cb431f311414c364312dbc862 Mon Sep 17 00:00:00 2001 From: Phil Cherner Date: Thu, 8 Aug 2019 14:08:10 -0700 Subject: [PATCH 12/13] Pushing again --- src/services/baseService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/baseService.ts b/src/services/baseService.ts index 50d312cb..cb253f0e 100644 --- a/src/services/baseService.ts +++ b/src/services/baseService.ts @@ -132,7 +132,7 @@ export abstract class BaseService { * Sends an API request using axios HTTP library * @param method The HTTP method * @param relativeUrl The relative url - * @param options Additional HTTP options including headers, etc + * @param options Additional HTTP options including headers, etc. */ protected async sendApiRequest( method: string, From 6dadb40144addfc105c79af127b4e64a23f8b007 Mon Sep 17 00:00:00 2001 From: Phil Cherner Date: Thu, 8 Aug 2019 15:58:36 -0700 Subject: [PATCH 13/13] Address more pr feedback --- .../login/utils/simpleFileTokenCache.ts | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/plugins/login/utils/simpleFileTokenCache.ts b/src/plugins/login/utils/simpleFileTokenCache.ts index 231d99fa..7f734cf1 100644 --- a/src/plugins/login/utils/simpleFileTokenCache.ts +++ b/src/plugins/login/utils/simpleFileTokenCache.ts @@ -14,53 +14,53 @@ export class SimpleFileTokenCache implements adal.TokenCache { this.load(); } - public add(entries: any, cb?: any) { + public add(entries: any[], callback?: (err?: Error, result?: boolean) => void) { this.entries.push(...entries); this.save(); - if (cb) { - cb(); + if (callback) { + callback(); } } - public remove(entries: any, cb?: any) { + public remove(entries: any[], callback?: (err?: Error, result?: null) => void) { this.entries = this.entries.filter(e => { return !Object.keys(entries[0]).every(key => e[key] === entries[0][key]); }); this.save(); - if (cb) { - cb(); + if (callback) { + callback(); } } - public find(query: any, cb?: any) { + public find(query: any, callback: (err?: Error, result?: any[]) => void) { let result = this.entries.filter(e => { return Object.keys(query).every(key => e[key] === query[key]); }); - cb(null, result); + callback(null, result); return result; } //-------- File toke cache specific methods - public addSubs(entries: any) { - this.subscriptions.push(...entries); - this.subscriptions = this.subscriptions.reduce((acc, current) => { - const x = acc.find(item => item.id === current.id); + public addSubs(subscriptions: any) { + this.subscriptions.push(...subscriptions); + this.subscriptions = this.subscriptions.reduce((accumulator , current) => { + const x = accumulator .find(item => item.id === current.id); if (!x) { - return acc.concat([current]); + return accumulator .concat([current]); } else { - return acc; + return accumulator ; } }, []); this.save(); } - public clear(cb: any) { + public clear(callback: any) { this.entries = []; this.subscriptions = []; this.save(); - cb(); + callback(); } public isEmpty() {