Skip to content
This repository was archived by the owner on Dec 9, 2024. It is now read-only.

Commit ea7678d

Browse files
authored
fix: Don't throw error if subscription ID already specified (#295)
1 parent 7ffbb8b commit ea7678d

File tree

7 files changed

+83
-54
lines changed

7 files changed

+83
-54
lines changed

package-lock.json

Lines changed: 7 additions & 15 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/plugins/login/azureLoginPlugin.test.ts

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { MockFactory } from "../../test/mockFactory";
44
import { invokeHook, setEnvVariables, unsetEnvVariables } from "../../test/utils";
55
import { AzureLoginPlugin } from "./azureLoginPlugin";
66
import { loginHooks } from "./loginHooks";
7+
import { ServerlessAzureConfig } from "../../models/serverless";
78

89
describe("Login Plugin", () => {
910

@@ -36,8 +37,8 @@ describe("Login Plugin", () => {
3637
}
3738

3839
beforeEach(() => {
39-
AzureLoginService.interactiveLogin = createMockLoginFunction();
40-
AzureLoginService.servicePrincipalLogin = createMockLoginFunction();
40+
AzureLoginService.prototype.interactiveLogin = createMockLoginFunction();
41+
AzureLoginService.prototype.servicePrincipalLogin = createMockLoginFunction();
4142
});
4243

4344
it("contains the hooks as contained in loginHooks", () => {
@@ -46,28 +47,28 @@ describe("Login Plugin", () => {
4647

4748
it("returns if azure credentials are set", async () => {
4849
await invokeLoginHook(true);
49-
expect(AzureLoginService.interactiveLogin).not.toBeCalled();
50-
expect(AzureLoginService.servicePrincipalLogin).not.toBeCalled();
50+
expect(AzureLoginService.prototype.interactiveLogin).not.toBeCalled();
51+
expect(AzureLoginService.prototype.servicePrincipalLogin).not.toBeCalled();
5152
});
5253

5354
it("calls login if azure credentials are not set", async () => {
5455
unsetServicePrincipalEnvVariables();
5556
await invokeLoginHook();
56-
expect(AzureLoginService.interactiveLogin).toBeCalled();
57-
expect(AzureLoginService.servicePrincipalLogin).not.toBeCalled();
57+
expect(AzureLoginService.prototype.interactiveLogin).toBeCalled();
58+
expect(AzureLoginService.prototype.servicePrincipalLogin).not.toBeCalled();
5859
});
5960

6061
it("calls service principal login if environment variables are set", async () => {
6162
setServicePrincipalEnvVariables();
6263
const sls = MockFactory.createTestServerless();
6364
await invokeLoginHook(false, sls);
64-
expect(AzureLoginService.servicePrincipalLogin).toBeCalledWith(
65+
expect(AzureLoginService.prototype.servicePrincipalLogin).toBeCalledWith(
6566
"azureServicePrincipalClientId",
6667
"azureServicePrincipalPassword",
6768
"azureServicePrincipalTenantId",
6869
undefined // would be options
6970
)
70-
expect(AzureLoginService.interactiveLogin).not.toBeCalled();
71+
expect(AzureLoginService.prototype.interactiveLogin).not.toBeCalled();
7172
expect(JSON.stringify(sls.variables["azureCredentials"])).toEqual(JSON.stringify(credentials));
7273
expect(sls.variables["subscriptionId"]).toEqual("azureSubId");
7374
});
@@ -76,41 +77,57 @@ describe("Login Plugin", () => {
7677
unsetServicePrincipalEnvVariables();
7778
const sls = MockFactory.createTestServerless();
7879
await invokeLoginHook(false, sls);
79-
expect(AzureLoginService.servicePrincipalLogin).not.toBeCalled();
80-
expect(AzureLoginService.interactiveLogin).toBeCalled();
80+
expect(AzureLoginService.prototype.servicePrincipalLogin).not.toBeCalled();
81+
expect(AzureLoginService.prototype.interactiveLogin).toBeCalled();
8182
expect(JSON.stringify(sls.variables["azureCredentials"])).toEqual(JSON.stringify(credentials));
8283
expect(sls.variables["subscriptionId"]).toEqual("azureSubId");
8384
});
8485

8586
it("logs an error from authentication and crashes with it", async () => {
8687
unsetServicePrincipalEnvVariables();
8788
const error = new Error("This is my error message")
88-
AzureLoginService.interactiveLogin = jest.fn(() => {
89+
AzureLoginService.prototype.interactiveLogin = jest.fn(() => {
8990
throw error;
9091
});
9192
const sls = MockFactory.createTestServerless();
9293
await expect(invokeLoginHook(false, sls)).rejects.toThrow(error);
93-
expect(AzureLoginService.interactiveLogin).toBeCalled();
94-
expect(AzureLoginService.servicePrincipalLogin).not.toBeCalled();
94+
expect(AzureLoginService.prototype.interactiveLogin).toBeCalled();
95+
expect(AzureLoginService.prototype.servicePrincipalLogin).not.toBeCalled();
9596
expect(sls.cli.log).lastCalledWith("Error logging into azure");
9697
});
9798

9899
it("Uses the default subscription ID" , async () => {
99100
const sls = MockFactory.createTestServerless();
100101
const opt = MockFactory.createTestServerlessOptions();
101102
await invokeLoginHook(false, sls, opt);
102-
expect(AzureLoginService.interactiveLogin).toBeCalled();
103+
expect(AzureLoginService.prototype.interactiveLogin).toBeCalled();
103104
expect(sls.variables["subscriptionId"]).toEqual("azureSubId");
104105
expect(sls.cli.log).toBeCalledWith("Using subscription ID: azureSubId");
105106
});
106107

107108
it("Throws an error with empty subscription list", async () => {
109+
unsetServicePrincipalEnvVariables();
108110
const authResponse = MockFactory.createTestAuthResponse();
109111
authResponse.subscriptions = [];
110-
AzureLoginService.interactiveLogin = createMockLoginFunction(authResponse);
112+
AzureLoginService.prototype.interactiveLogin = createMockLoginFunction(authResponse);
111113
const sls = MockFactory.createTestServerless();
114+
delete sls.variables["subscriptionId"];
112115
const opt = MockFactory.createTestServerlessOptions();
113116
await expect(invokeLoginHook(false, sls, opt)).rejects.toThrow();
114-
expect(AzureLoginService.interactiveLogin).toBeCalled();
117+
expect(AzureLoginService.prototype.interactiveLogin).toBeCalled();
118+
});
119+
120+
it("Does not throw an error with empty subscription list if subscription previously specified", async () => {
121+
unsetServicePrincipalEnvVariables();
122+
const authResponse = MockFactory.createTestAuthResponse();
123+
authResponse.subscriptions = [];
124+
AzureLoginService.prototype.interactiveLogin = createMockLoginFunction(authResponse);
125+
const sls = MockFactory.createTestServerless();
126+
delete sls.variables["subscriptionId"];
127+
const subId = "my subscription id";
128+
(sls.service as any as ServerlessAzureConfig).provider.subscriptionId = subId;
129+
const opt = MockFactory.createTestServerlessOptions();
130+
await invokeLoginHook(false, sls, opt)
131+
expect(AzureLoginService.prototype.interactiveLogin).toBeCalled();
115132
});
116133
});

src/plugins/login/azureLoginPlugin.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,18 @@ export class AzureLoginPlugin extends AzureBasePlugin<AzureLoginOptions> {
2323
this.log("Logging into Azure");
2424

2525
try {
26-
const authResult = await AzureLoginService.login();
26+
const loginService = new AzureLoginService(this.serverless, this.options);
27+
const authResult = await loginService.login();
28+
2729
this.serverless.variables["azureCredentials"] = authResult.credentials;
2830
// Use environment variable for sub ID or use the first subscription in the list (service principal can
2931
// have access to more than one subscription)
30-
if (!authResult.subscriptions.length) {
32+
let subId = loginService.getSubscriptionId();
33+
if (!subId && !authResult.subscriptions.length) {
3134
throw new Error("Authentication returned an empty list of subscriptions. " +
3235
"Try another form of authentication. See the serverless-azure-functions README for more help");
3336
}
34-
const subId = authResult.subscriptions[0].id;
37+
subId = subId || authResult.subscriptions[0].id;
3538
this.serverless.variables["subscriptionId"] = subId;
3639
this.serverless.cli.log(`Using subscription ID: ${subId}`);
3740
}

src/services/azureBlobStorageService.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ describe("Azure Blob Storage Service", () => {
4949
}) as any;
5050

5151
BlockBlobURL.fromContainerURL = jest.fn(() => blockBlobUrl) as any;
52-
AzureLoginService.login = jest.fn(() => Promise.resolve({
52+
AzureLoginService.prototype.login = jest.fn(() => Promise.resolve({
5353
credentials: {
5454
getToken: jest.fn(() => {
5555
return {
@@ -142,7 +142,7 @@ describe("Azure Blob Storage Service", () => {
142142
expect(ContainerURL.fromServiceURL).not.toBeCalled();
143143
expect(ContainerURL.prototype.create).not.toBeCalled
144144
})
145-
145+
146146
it("should delete a container", async () => {
147147
const containerToDelete = "delete container";
148148
ContainerURL.fromServiceURL = jest.fn(() => new ContainerURL(null, null));

src/services/azureBlobStorageService.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ export class AzureBlobStorageService extends BaseService {
4545
if (this.storageCredential) {
4646
return;
4747
}
48-
this.storageCredential = (this.authType === AzureStorageAuthType.SharedKey)
48+
this.storageCredential = (this.authType === AzureStorageAuthType.SharedKey)
4949
?
5050
new SharedKeyCredential(this.storageAccountName, await this.getKey())
5151
:
@@ -91,10 +91,10 @@ export class AzureBlobStorageService extends BaseService {
9191
blockSize: 4 * 1024 * 1024, // 4MB block size
9292
parallelism: 20, // 20 concurrency
9393
}
94-
);
94+
);
9595
fs.writeFileSync(targetPath, buffer, "binary");
9696
}
97-
97+
9898
/**
9999
* Delete a blob from Azure Blob Storage
100100
* @param containerName Name of container containing blob
@@ -195,7 +195,7 @@ export class AzureBlobStorageService extends BaseService {
195195
public async generateBlobSasTokenUrl(containerName: string, blobName: string, days: number = 365): Promise<string> {
196196
this.checkInitialization();
197197
if (this.authType !== AzureStorageAuthType.SharedKey) {
198-
throw new Error("Need to authenticate with shared key in order to generate SAS tokens. " +
198+
throw new Error("Need to authenticate with shared key in order to generate SAS tokens. " +
199199
"Initialize Blob Service with SharedKey auth type");
200200
}
201201

@@ -219,7 +219,7 @@ export class AzureBlobStorageService extends BaseService {
219219
version: "2016-05-31"
220220
},
221221
this.storageCredential as SharedKeyCredential);
222-
222+
223223
const blobUrl = this.getBlockBlobURL(containerName, blobName);
224224
return `${blobUrl.url}?${blobSas}`
225225
}
@@ -274,7 +274,8 @@ export class AzureBlobStorageService extends BaseService {
274274
* Get access token by logging in (again) with a storage-specific context
275275
*/
276276
private async getToken(): Promise<string> {
277-
const authResponse = await AzureLoginService.login({
277+
const loginService = new AzureLoginService(this.serverless, this.options);
278+
const authResponse = await loginService.login({
278279
tokenAudience: "https://storage.azure.com/"
279280
});
280281
const token = await authResponse.credentials.getToken();
@@ -296,8 +297,8 @@ export class AzureBlobStorageService extends BaseService {
296297
* the credentials will not be available for any operation
297298
*/
298299
private checkInitialization() {
299-
Guard.null(this.storageCredential, "storageCredential",
300-
"Azure Blob Storage Service has not been initialized. Make sure .initialize() has been called " +
300+
Guard.null(this.storageCredential, "storageCredential",
301+
"Azure Blob Storage Service has not been initialized. Make sure .initialize() has been called " +
301302
"before performing any operation");
302303
}
303304
}

src/services/loginService.test.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,19 @@ import * as nodeauth from "@azure/ms-rest-nodeauth";
77

88
jest.mock("../plugins/login/utils/simpleFileTokenCache");
99
import { SimpleFileTokenCache } from "../plugins/login/utils/simpleFileTokenCache";
10+
import { MockFactory } from "../test/mockFactory";
1011

1112
describe("Login Service", () => {
1213

14+
let loginService: AzureLoginService;
15+
16+
beforeEach(() => {
17+
loginService = new AzureLoginService(
18+
MockFactory.createTestServerless(),
19+
MockFactory.createTestServerlessOptions()
20+
)
21+
})
22+
1323
it("logs in interactively with no cached login", async () => {
1424
// Ensure env variables are not set
1525
delete process.env.azureSubId;
@@ -25,7 +35,7 @@ describe("Login Service", () => {
2535
{ value: jest.fn(() => emptyObj) }
2636
);
2737

28-
await AzureLoginService.login();
38+
await loginService.login();
2939
expect(SimpleFileTokenCache).toBeCalled();
3040
expect(open).toBeCalledWith("https://microsoft.com/devicelogin");
3141
expect(nodeauth.interactiveLoginWithAuthResponse).toBeCalled();
@@ -42,7 +52,7 @@ describe("Login Service", () => {
4252
SimpleFileTokenCache.prototype.isEmpty = jest.fn(() => false);
4353
SimpleFileTokenCache.prototype.first = jest.fn(() => ({ userId: "" }));
4454

45-
await AzureLoginService.login();
55+
await loginService.login();
4656
expect(SimpleFileTokenCache).toBeCalled();
4757
expect(nodeauth.DeviceTokenCredentials).toBeCalled();
4858
expect(SimpleFileTokenCache.prototype.listSubscriptions).toBeCalled();
@@ -55,7 +65,7 @@ describe("Login Service", () => {
5565
process.env.azureServicePrincipalPassword = "azureServicePrincipalPassword";
5666
process.env.azureServicePrincipalTenantId = "azureServicePrincipalTenantId";
5767

58-
await AzureLoginService.login();
68+
await loginService.login();
5969
expect(nodeauth.loginWithServicePrincipalSecretWithAuthResponse).toBeCalledWith(
6070
"azureServicePrincipalClientId",
6171
"azureServicePrincipalPassword",

0 commit comments

Comments
 (0)