// 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; }, ); }); }); });