Skip to content

Commit ecc9b5f

Browse files
authored
fix(property-provider): avoid generating default rejected promise when chaining (#4843)
1 parent 8668bab commit ecc9b5f

File tree

2 files changed

+109
-34
lines changed

2 files changed

+109
-34
lines changed
Lines changed: 92 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,117 @@
11
import { chain } from "./chain";
2-
import { fromStatic } from "./fromStatic";
32
import { ProviderError } from "./ProviderError";
43

4+
const resolveStatic = (staticValue: unknown) => jest.fn().mockResolvedValue(staticValue);
5+
const rejectWithError = (errorMsg: string) => jest.fn().mockRejectedValue(new Error(errorMsg));
6+
const rejectWithProviderError = (errorMsg: string) => jest.fn().mockRejectedValue(new ProviderError(errorMsg));
7+
58
describe("chain", () => {
69
it("should distill many credential providers into one", async () => {
7-
const provider = chain(fromStatic("foo"), fromStatic("bar"));
8-
10+
const provider = chain(resolveStatic("foo"), resolveStatic("bar"));
911
expect(typeof (await provider())).toBe("string");
1012
});
1113

1214
it("should return the resolved value of the first successful promise", async () => {
13-
const provider = chain(
14-
() => Promise.reject(new ProviderError("Move along")),
15-
() => Promise.reject(new ProviderError("Nothing to see here")),
16-
fromStatic("foo")
17-
);
15+
const expectedOutput = "foo";
16+
const providers = [
17+
rejectWithProviderError("Move along"),
18+
rejectWithProviderError("Nothing to see here"),
19+
resolveStatic(expectedOutput),
20+
];
1821

19-
expect(await provider()).toBe("foo");
22+
try {
23+
const result = await chain(...providers)();
24+
expect(result).toBe(expectedOutput);
25+
} catch (error) {
26+
throw error;
27+
}
28+
29+
expect(providers[0]).toHaveBeenCalledTimes(1);
30+
expect(providers[1]).toHaveBeenCalledTimes(1);
31+
expect(providers[2]).toHaveBeenCalledTimes(1);
2032
});
2133

2234
it("should not invoke subsequent providers once one resolves", async () => {
35+
const expectedOutput = "foo";
2336
const providers = [
24-
jest.fn().mockRejectedValue(new ProviderError("Move along")),
25-
jest.fn().mockResolvedValue("foo"),
26-
jest.fn(() => fail("This provider should not be invoked")),
37+
rejectWithProviderError("Move along"),
38+
resolveStatic(expectedOutput),
39+
rejectWithProviderError("This provider should not be invoked"),
2740
];
2841

29-
expect(await chain(...providers)()).toBe("foo");
30-
expect(providers[0].mock.calls.length).toBe(1);
31-
expect(providers[1].mock.calls.length).toBe(1);
32-
expect(providers[2].mock.calls.length).toBe(0);
42+
try {
43+
const result = await chain(...providers)();
44+
expect(result).toBe(expectedOutput);
45+
} catch (error) {
46+
throw error;
47+
}
48+
49+
expect(providers[0]).toHaveBeenCalledTimes(1);
50+
expect(providers[1]).toHaveBeenCalledTimes(1);
51+
expect(providers[2]).not.toHaveBeenCalled();
52+
});
53+
54+
describe("should throw if no provider resolves", () => {
55+
const expectedErrorMsg = "Last provider failed";
56+
57+
it.each([
58+
[ProviderError, rejectWithProviderError(expectedErrorMsg)],
59+
[Error, rejectWithError(expectedErrorMsg)],
60+
])("case %p", async (errorType, errorProviderMockFn) => {
61+
const firstProviderWhichRejects = rejectWithProviderError("Move along");
62+
try {
63+
await chain(firstProviderWhichRejects, errorProviderMockFn)();
64+
throw new Error("Should not get here");
65+
} catch (error) {
66+
expect(error).toEqual(new errorType(expectedErrorMsg));
67+
}
68+
expect(firstProviderWhichRejects).toHaveBeenCalledTimes(1);
69+
expect(errorProviderMockFn).toHaveBeenCalledTimes(1);
70+
});
3371
});
3472

3573
it("should halt if an unrecognized error is encountered", async () => {
36-
const provider = chain(
37-
() => Promise.reject(new ProviderError("Move along")),
38-
() => Promise.reject(new Error("Unrelated failure")),
39-
fromStatic("foo")
40-
);
74+
const expectedErrorMsg = "Unrelated failure";
75+
const providers = [rejectWithProviderError("Move along"), rejectWithError(expectedErrorMsg), resolveStatic("foo")];
76+
77+
try {
78+
await chain(...providers)();
79+
throw new Error("Should not get here");
80+
} catch (error) {
81+
expect(error).toEqual(new Error(expectedErrorMsg));
82+
}
83+
84+
expect(providers[0]).toHaveBeenCalledTimes(1);
85+
expect(providers[1]).toHaveBeenCalledTimes(1);
86+
expect(providers[2]).not.toHaveBeenCalled();
87+
});
88+
89+
it("should halt if ProviderError explicitly requests it", async () => {
90+
const expectedError = new ProviderError("ProviderError with tryNextLink set to false", false);
91+
const providers = [
92+
rejectWithProviderError("Move along"),
93+
jest.fn().mockRejectedValue(expectedError),
94+
resolveStatic("foo"),
95+
];
96+
97+
try {
98+
await chain(...providers)();
99+
throw new Error("Should not get here");
100+
} catch (error) {
101+
expect(error).toEqual(expectedError);
102+
}
41103

42-
await expect(provider()).rejects.toMatchObject(new Error("Unrelated failure"));
104+
expect(providers[0]).toHaveBeenCalledTimes(1);
105+
expect(providers[1]).toHaveBeenCalledTimes(1);
106+
expect(providers[2]).not.toHaveBeenCalled();
43107
});
44108

45109
it("should reject chains with no links", async () => {
46-
await expect(chain()()).rejects.toMatchObject(new Error("No providers in chain"));
110+
try {
111+
await chain()();
112+
throw new Error("Should not get here");
113+
} catch (error) {
114+
expect(error).toEqual(new Error("No providers in chain"));
115+
}
47116
});
48117
});

packages/property-provider/src/chain.ts

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { ProviderError } from "./ProviderError";
44

55
/**
66
* @internal
7-
*
7+
*
88
* Compose a single credential provider function from multiple credential
99
* providers. The first provider in the argument list will always be invoked;
1010
* subsequent providers in the list will be invoked in the order in which the
@@ -13,19 +13,25 @@ import { ProviderError } from "./ProviderError";
1313
* If no providers were received or no provider resolves successfully, the
1414
* returned promise will be rejected.
1515
*/
16-
export function chain<T>(...providers: Array<Provider<T>>): Provider<T> {
17-
return () => {
18-
let promise: Promise<T> = Promise.reject(new ProviderError("No providers in chain"));
16+
export const chain =
17+
<T>(...providers: Array<Provider<T>>): Provider<T> =>
18+
async () => {
19+
if (providers.length === 0) {
20+
throw new ProviderError("No providers in chain");
21+
}
22+
23+
let lastProviderError: Error | undefined;
1924
for (const provider of providers) {
20-
promise = promise.catch((err: any) => {
25+
try {
26+
const credentials = await provider();
27+
return credentials;
28+
} catch (err) {
29+
lastProviderError = err;
2130
if (err?.tryNextLink) {
22-
return provider();
31+
continue;
2332
}
24-
2533
throw err;
26-
});
34+
}
2735
}
28-
29-
return promise;
36+
throw lastProviderError;
3037
};
31-
}

0 commit comments

Comments
 (0)