pulumi/sdk/nodejs/tests/runtime/props.spec.ts

476 lines
20 KiB
TypeScript

// Copyright 2016-2021, Pulumi Corporation.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import * as assert from "assert";
import {
ComponentResource,
CustomResource,
DependencyResource,
Inputs,
Output,
Resource,
ResourceOptions,
runtime,
secret,
} from "../../index";
import * as state from "../../runtime/state";
import * as gstruct from "google-protobuf/google/protobuf/struct_pb";
class TestComponentResource extends ComponentResource {
constructor(name: string, opts?: ResourceOptions) {
super("test:index:component", name, {}, opts);
super.registerOutputs({});
}
}
class TestCustomResource extends CustomResource {
constructor(name: string, type?: string, opts?: ResourceOptions) {
super(type || "test:index:custom", name, {}, opts);
}
}
class TestErrorResource extends CustomResource {
constructor(name: string) {
super("error", name, {});
}
}
class TestResourceModule implements runtime.ResourceModule {
construct(name: string, type: string, urn: string): Resource {
switch (type) {
case "test:index:component":
return new TestComponentResource(name, { urn });
case "test:index:custom":
return new TestCustomResource(name, type, { urn });
default:
throw new Error(`unknown resource type ${type}`);
}
}
}
class TestMocks implements runtime.Mocks {
call(args: runtime.MockCallArgs): Record<string, any> {
throw new Error(`unknown function ${args.token}`);
}
newResource(args: runtime.MockResourceArgs): { id: string | undefined; state: Record<string, any> } {
switch (args.type) {
case "test:index:component":
return { id: undefined, state: {} };
case "test:index:custom":
case "test2:index:custom":
return {
id: runtime.isDryRun() ? undefined : "test-id",
state: {},
};
case "error":
throw new Error("this is an intentional error");
default:
throw new Error(`unknown resource type ${args.type}`);
}
}
}
// eslint-disable-next-line @typescript-eslint/naming-convention,no-underscore-dangle,id-blacklist,id-match
const TestStrEnum = {
Foo: "foo",
Bar: "bar",
} as const;
// eslint-disable-next-line @typescript-eslint/no-redeclare
type TestStrEnum = (typeof TestStrEnum)[keyof typeof TestStrEnum];
// eslint-disable-next-line @typescript-eslint/naming-convention,no-underscore-dangle,id-blacklist,id-match
const TestIntEnum = {
One: 1,
Zero: 0,
} as const;
// eslint-disable-next-line @typescript-eslint/no-redeclare
type TestIntEnum = (typeof TestIntEnum)[keyof typeof TestIntEnum];
// eslint-disable-next-line @typescript-eslint/naming-convention,no-underscore-dangle,id-blacklist,id-match
const TestNumEnum = {
One: 1.0,
ZeroPointOne: 0.1,
} as const;
// eslint-disable-next-line @typescript-eslint/no-redeclare
type TestNumEnum = (typeof TestNumEnum)[keyof typeof TestNumEnum];
// eslint-disable-next-line @typescript-eslint/naming-convention,no-underscore-dangle,id-blacklist,id-match
const TestBoolEnum = {
One: true,
Zero: false,
} as const;
// eslint-disable-next-line @typescript-eslint/no-redeclare
type TestBoolEnum = (typeof TestBoolEnum)[keyof typeof TestBoolEnum];
interface TestInputs {
aNum: number;
bStr: string;
cUnd: undefined;
dArr: Promise<Array<any>>;
id: string;
urn: string;
strEnum: TestStrEnum;
intEnum: TestIntEnum;
numEnum: TestNumEnum;
boolEnum: TestBoolEnum;
}
describe("runtime", () => {
beforeEach(() => {
runtime._reset();
runtime._resetResourcePackages();
runtime._resetResourceModules();
});
describe("transferProperties", () => {
describe("output values", () => {
function* generateTests() {
const testValues = [
{ value: undefined, expected: null },
{ value: null, expected: null },
{ value: 0, expected: 0 },
{ value: 1, expected: 1 },
{ value: "", expected: "" },
{ value: "hi", expected: "hi" },
{ value: {}, expected: {} },
{ value: [], expected: [] },
];
for (const tv of testValues) {
for (const deps of [[], ["fakeURN1", "fakeURN2"]]) {
for (const isKnown of [true, false]) {
for (const isSecret of [true, false]) {
const resources = deps.map((dep) => new DependencyResource(dep));
yield {
name:
`Output(${JSON.stringify(deps)}, ${JSON.stringify(tv.value)}, ` +
`isKnown=${isKnown}, isSecret=${isSecret})`,
input: new Output(
resources,
Promise.resolve(tv.value),
Promise.resolve(isKnown),
Promise.resolve(isSecret),
Promise.resolve([]),
),
expected: {
[runtime.specialSigKey]: runtime.specialOutputValueSig,
...(isKnown && { value: tv.expected }),
...(isSecret && { secret: isSecret }),
...(deps.length > 0 && { dependencies: deps }),
},
expectedRoundTrip: new Output(
resources,
Promise.resolve(isKnown ? tv.expected : undefined),
Promise.resolve(isKnown),
Promise.resolve(isSecret),
Promise.resolve([]),
),
};
}
}
}
}
}
async function assertOutputsEqual<T>(a: Output<T>, e: Output<T>) {
async function urns(res: Set<Resource>): Promise<Set<string>> {
const result = new Set<string>();
for (const r of res) {
result.add(await r.urn.promise());
}
return result;
}
assert.deepStrictEqual(await urns(a.resources()), await urns(e.resources()));
assert.deepStrictEqual(await a.isKnown, await e.isKnown);
assert.deepStrictEqual(await a.promise(), await e.promise());
assert.deepStrictEqual(await a.isSecret, await e.isSecret);
assert.deepStrictEqual(await urns(await a.allResources!()), await urns(await e.allResources!()));
}
for (const test of generateTests()) {
it(`marshals ${test.name} correctly`, async () => {
state.getStore().supportsOutputValues = true;
const inputs = { value: test.input };
const expected = { value: test.expected };
const actual = await runtime.serializeProperties("test", inputs, { keepOutputValues: true });
assert.deepStrictEqual(actual, expected);
// Roundtrip.
const back = runtime.deserializeProperties(gstruct.Struct.fromJavaScript(actual));
await assertOutputsEqual(back.value, test.expectedRoundTrip);
});
}
});
it("marshals basic properties correctly", async () => {
const inputs: TestInputs = {
aNum: 42,
bStr: "a string",
cUnd: undefined,
dArr: Promise.resolve(["x", 42, Promise.resolve(true), Promise.resolve(undefined)]),
id: "foo",
urn: "bar",
strEnum: TestStrEnum.Foo,
intEnum: TestIntEnum.One,
numEnum: TestNumEnum.One,
boolEnum: TestBoolEnum.One,
};
// Serialize and then deserialize all the properties, checking that they round-trip as expected.
const transfer = gstruct.Struct.fromJavaScript(await runtime.serializeProperties("test", inputs));
const result = runtime.deserializeProperties(transfer);
assert.strictEqual(result.aNum, 42);
assert.strictEqual(result.bStr, "a string");
assert.strictEqual(result.cUnd, undefined);
assert.deepStrictEqual(result.dArr, ["x", 42, true, null]);
assert.strictEqual(result.id, "foo");
assert.strictEqual(result.urn, "bar");
assert.strictEqual(result.strEnum, TestStrEnum.Foo);
assert.strictEqual(result.intEnum, TestIntEnum.One);
assert.strictEqual(result.numEnum, TestNumEnum.One);
assert.strictEqual(result.boolEnum, TestBoolEnum.One);
});
it("marshals secrets correctly", async () => {
const inputs: Inputs = {
secret1: secret(1),
secret2: secret(undefined),
};
// Serialize and then deserialize all the properties, checking that they round-trip as expected.
state.getStore().supportsSecrets = true;
let transfer = gstruct.Struct.fromJavaScript(await runtime.serializeProperties("test", inputs));
let result = runtime.deserializeProperties(transfer);
assert.ok(runtime.isRpcSecret(result.secret1));
assert.ok(runtime.isRpcSecret(result.secret2));
assert.strictEqual(runtime.unwrapRpcSecret(result.secret1), 1);
assert.strictEqual(runtime.unwrapRpcSecret(result.secret2), null);
// Serialize and then deserialize all the properties, checking that they round-trip as expected.
state.getStore().supportsSecrets = false;
transfer = gstruct.Struct.fromJavaScript(await runtime.serializeProperties("test", inputs));
result = runtime.deserializeProperties(transfer);
assert.ok(!runtime.isRpcSecret(result.secret1));
assert.ok(!runtime.isRpcSecret(result.secret2));
assert.strictEqual(result.secret1, 1);
assert.strictEqual(result.secret2, undefined);
});
it("marshals resource references correctly during preview", async () => {
runtime._setIsDryRun(true);
runtime.setMocks(new TestMocks());
const component = new TestComponentResource("test");
const custom = new TestCustomResource("test");
const componentURN = await component.urn.promise();
const customURN = await custom.urn.promise();
const customID = await custom.id.promise();
const inputs: Inputs = {
component: component,
custom: custom,
};
state.getStore().supportsResourceReferences = true;
let serialized = await runtime.serializeProperties("test", inputs);
assert.deepEqual(serialized, {
component: {
[runtime.specialSigKey]: runtime.specialResourceSig,
urn: componentURN,
},
custom: {
[runtime.specialSigKey]: runtime.specialResourceSig,
urn: customURN,
id: customID,
},
});
state.getStore().supportsResourceReferences = false;
serialized = await runtime.serializeProperties("test", inputs);
assert.deepEqual(serialized, {
component: componentURN,
custom: customID ? customID : runtime.unknownValue,
});
});
it("marshals resource references correctly during update", async () => {
runtime.setMocks(new TestMocks());
const component = new TestComponentResource("test");
const custom = new TestCustomResource("test");
const componentURN = await component.urn.promise();
const customURN = await custom.urn.promise();
const customID = await custom.id.promise();
const inputs: Inputs = {
component: component,
custom: custom,
};
state.getStore().supportsResourceReferences = true;
let serialized = await runtime.serializeProperties("test", inputs);
assert.deepEqual(serialized, {
component: {
[runtime.specialSigKey]: runtime.specialResourceSig,
urn: componentURN,
},
custom: {
[runtime.specialSigKey]: runtime.specialResourceSig,
urn: customURN,
id: customID,
},
});
state.getStore().supportsResourceReferences = false;
serialized = await runtime.serializeProperties("test", inputs);
assert.deepEqual(serialized, {
component: componentURN,
custom: customID,
});
});
});
describe("deserializeProperty", () => {
it("fails on unsupported secret values", () => {
assert.throws(() =>
runtime.deserializeProperty({
[runtime.specialSigKey]: runtime.specialSecretSig,
}),
);
});
it("fails on unknown signature keys", () => {
assert.throws(() =>
runtime.deserializeProperty({
[runtime.specialSigKey]: "foobar",
}),
);
});
it("pushed secretness up correctly", () => {
const secretValue = {
[runtime.specialSigKey]: runtime.specialSecretSig,
value: "a secret value",
};
const props = gstruct.Struct.fromJavaScript({
regular: "a normal value",
list: ["a normal value", "another value", secretValue],
map: { regular: "a normal value", secret: secretValue },
mapWithList: {
regular: "a normal value",
list: ["a normal value", secretValue],
},
listWithMap: [
{
regular: "a normal value",
secret: secretValue,
},
],
});
const result = runtime.deserializeProperties(props);
// Regular had no secrets in it, so it is returned as is.
assert.strictEqual(result.regular, "a normal value");
// One of the elements in the list was a secret, so the secretness is promoted to top level.
assert.strictEqual(result.list[runtime.specialSigKey], runtime.specialSecretSig);
assert.strictEqual(result.list.value[0], "a normal value");
assert.strictEqual(result.list.value[1], "another value");
assert.strictEqual(result.list.value[2], "a secret value");
// One of the values of the map was a secret, so the secretness is promoted to top level.
assert.strictEqual(result.map[runtime.specialSigKey], runtime.specialSecretSig);
assert.strictEqual(result.map.value.regular, "a normal value");
assert.strictEqual(result.map.value.secret, "a secret value");
// The nested map had a secret in one of the values, so the entire thing becomes a secret.
assert.strictEqual(result.mapWithList[runtime.specialSigKey], runtime.specialSecretSig);
assert.strictEqual(result.mapWithList.value.regular, "a normal value");
assert.strictEqual(result.mapWithList.value.list[0], "a normal value");
assert.strictEqual(result.mapWithList.value.list[1], "a secret value");
// An array element contained a secret (via a nested map), so the entrie array becomes a secret.
assert.strictEqual(result.listWithMap[runtime.specialSigKey], runtime.specialSecretSig);
assert.strictEqual(result.listWithMap.value[0].regular, "a normal value");
assert.strictEqual(result.listWithMap.value[0].secret, "a secret value");
});
it("deserializes resource references properly during preview", async () => {
runtime.setMocks(new TestMocks());
state.getStore().supportsResourceReferences = true;
runtime.registerResourceModule("test", "index", new TestResourceModule());
const component = new TestComponentResource("test");
const custom = new TestCustomResource("test");
const unregistered = new TestCustomResource("test", "test2:index:custom");
const componentURN = await component.urn.promise();
const customURN = await custom.urn.promise();
const customID = await custom.id.promise();
const unregisteredURN = await unregistered.urn.promise();
const unregisteredID = await unregistered.id.promise();
const outputs = {
component: {
[runtime.specialSigKey]: runtime.specialResourceSig,
urn: componentURN,
},
custom: {
[runtime.specialSigKey]: runtime.specialResourceSig,
urn: customURN,
id: customID,
},
unregistered: {
[runtime.specialSigKey]: runtime.specialResourceSig,
urn: unregisteredURN,
id: unregisteredID,
},
};
const deserialized = runtime.deserializeProperty(outputs);
assert.ok((<ComponentResource>deserialized["component"]).__pulumiComponentResource);
assert.ok((<CustomResource>deserialized["custom"]).__pulumiCustomResource);
assert.deepEqual(deserialized["unregistered"], unregisteredID);
});
});
describe("resource error handling", () => {
it("registerResource errors propagate appropriately", async () => {
runtime.setMocks(new TestMocks());
await assert.rejects(
async () => {
const errResource = new TestErrorResource("test");
const customURN = await errResource.urn.promise();
const customID = await errResource.id.promise();
},
(err: Error) => {
const containsMessage = err.stack!.indexOf("this is an intentional error") >= 0;
const containsRegisterResource = err.stack!.indexOf("registerResource") >= 0;
return containsMessage && containsRegisterResource;
},
);
});
});
});