// Copyright 2016-2018, 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. /* eslint-disable */ import * as assert from "assert"; import { all, output, Output, unknown } from "../index"; function test(val: any, expected: any) { return async () => { const unwrapped = output(val); const actual = await unwrapped.promise(); assert.deepStrictEqual(actual, expected); }; } function testUntouched(val: any) { return test(val, val); } function testPromise(val: any) { return test(Promise.resolve(val), val); } function testOutput(val: any) { return test(output(val), val); } function testResources( val: any, expected: any, resources: TestResource[], allResources: TestResource[], withUnknowns?: boolean, ) { return async () => { const unwrapped = output(val); const actual = await unwrapped.promise(withUnknowns); const syncResources = unwrapped.resources(); const asyncResources = await unwrapped.allResources!(); assert.deepStrictEqual(actual, expected); assert.deepStrictEqual(syncResources, new Set(resources)); assert.deepStrictEqual(asyncResources, new Set(allResources)); for (const res of syncResources) { if (!asyncResources.has(<any>res)) { assert.fail(`async resources did not contain: ${(<TestResource>(<any>res)).name}`); } } }; } class TestResource { // fake being a pulumi resource. We can't actually derive from Resource as that then needs an // engine and whatnot. All things we don't want during simple unit tests. private readonly __pulumiResource: boolean = true; constructor(public name: string) {} } // Helper type to try to do type asserts. Note that it's not totally safe. If TS thinks a type is // the 'any' type, it will succeed here. Talking to the TS team, it does not look like there's a // way to write a totally airtight type assertion. type EqualsType<X, Y> = X extends Y ? (Y extends X ? X : never) : never; describe("unwrap", () => { describe("handles simple", () => { it("null", testUntouched(null)); it("undefined", testUntouched(undefined)); it("true", testUntouched(true)); it("false", testUntouched(false)); it("0", testUntouched(0)); it("numbers", testUntouched(4)); it("empty string", testUntouched("")); it("strings", testUntouched("foo")); it("arrays", testUntouched([])); it("object", testUntouched({})); it( "function", testUntouched(() => {}), ); }); describe("handles promises", () => { it("with null", testPromise(null)); it("with undefined", testPromise(undefined)); it("with true", testPromise(true)); it("with false", testPromise(false)); it("with 0", testPromise(0)); it("with numbers", testPromise(4)); it("with empty string", testPromise("")); it("with strings", testPromise("foo")); it("with array", testPromise([])); it("with object", testPromise({})); it( "with function", testPromise(() => {}), ); it("with nested promise", test(Promise.resolve(Promise.resolve(4)), 4)); }); describe("handles outputs", () => { it("with null", testOutput(null)); it("with undefined", testOutput(undefined)); it("with true", testOutput(true)); it("with false", testOutput(false)); it("with 0", testOutput(0)); it("with numbers", testOutput(4)); it("with empty string", testOutput("")); it("with strings", testOutput("foo")); it("with array", testOutput([])); it("with object", testOutput({})); it( "with function", testOutput(() => {}), ); it("with nested output", test(output(output(4)), 4)); it("with output of promise", test(output(Promise.resolve(4)), 4)); }); describe("handles arrays", () => { it("empty", testUntouched([])); it("with primitives", testUntouched([1, true])); it("with inner promise", test([1, true, Promise.resolve("")], [1, true, ""])); it("with inner and outer promise", test(Promise.resolve([1, true, Promise.resolve("")]), [1, true, ""])); it("recursion", test([1, Promise.resolve(""), [true, Promise.resolve(4)]], [1, "", [true, 4]])); }); describe("handles complex object", () => { it("empty", testUntouched({})); it("with primitives", testUntouched({ a: 1, b: true, c: () => {} })); it("with inner promise", test({ a: 1, b: true, c: Promise.resolve("") }, { a: 1, b: true, c: "" })); it( "with inner and outer promise", test(Promise.resolve({ a: 1, b: true, c: Promise.resolve("") }), { a: 1, b: true, c: "" }), ); it( "recursion", test( { a: 1, b: Promise.resolve(""), c: { d: true, e: Promise.resolve(4) } }, { a: 1, b: "", c: { d: true, e: 4 } }, ), ); }); function createOutput<T>(cv: T, ...resources: TestResource[]): Output<T> { return Output.isInstance<T>(cv) ? cv : new Output( <any>new Set(resources), Promise.resolve(cv), Promise.resolve(true), Promise.resolve(false), Promise.resolve(<any>new Set(resources)), ); } describe("preserves resources", () => { const r1 = new TestResource("r1"); const r2 = new TestResource("r2"); const r3 = new TestResource("r3"); const r4 = new TestResource("r4"); const r5 = new TestResource("r5"); const r6 = new TestResource("r6"); // assert.deepEqual(r1, r2); it("with single output", testResources(createOutput(3, r1, r2), 3, [r1, r2], [r1, r2])); it("inside array", testResources([createOutput(3, r1, r2)], [3], [r1, r2], [r1, r2])); it( "inside multi array", testResources([createOutput(1, r1, r2), createOutput(2, r2, r3)], [1, 2], [r1, r2, r3], [r1, r2, r3]), ); it( "inside nested array", testResources( [createOutput(1, r1, r2), createOutput(2, r2, r3), [createOutput(3, r5)]], [1, 2, [3]], [r1, r2, r3, r5], [r1, r2, r3, r5], ), ); it("inside object", testResources({ a: createOutput(3, r1, r2) }, { a: 3 }, [r1, r2], [r1, r2])); it( "inside multi object", testResources( { a: createOutput(1, r1, r2), b: createOutput(2, r2, r3) }, { a: 1, b: 2 }, [r1, r2, r3], [r1, r2, r3], ), ); it( "inside nested object", testResources( { a: createOutput(1, r1, r2), b: createOutput(2, r2, r3), c: { d: createOutput(3, r5) } }, { a: 1, b: 2, c: { d: 3 } }, [r1, r2, r3, r5], [r1, r2, r3, r5], ), ); it("across inner promise", testResources(createOutput(Promise.resolve(3), r1, r2), 3, [r1, r2], [r1, r2])); describe("with unknowns", () => { it( "across 'all' without unknowns", testResources( all([ Promise.resolve({ a: createOutput(unknown, r1, r2) }), Promise.resolve({ b: createOutput(unknown, r3, r4) }), ]), undefined, [], [r1, r2, r3, r4], ), ); it( "across 'all' with unknowns", testResources( all([ Promise.resolve({ a: createOutput(unknown, r1, r2) }), Promise.resolve({ b: createOutput(unknown, r3, r4) }), ]), [{ a: unknown }, { b: unknown }], [], [r1, r2, r3, r4], /*withUnknowns:*/ true, ), ); }); describe("across promise boundaries", () => { it( "inside and outside of array", testResources(createOutput([createOutput(3, r1, r2)], r2, r3), [3], [r2, r3], [r1, r2, r3]), ); it( "inside and outside of object", testResources(createOutput({ a: createOutput(3, r1, r2) }, r2, r3), { a: 3 }, [r2, r3], [r1, r2, r3]), ); it( "inside nested object and array", testResources( { a: createOutput(1, r1, r2), b: createOutput(2, r2, r3), c: { d: createOutput([createOutput(3, r5)], r6) }, }, { a: 1, b: 2, c: { d: [3] } }, [r1, r2, r3, r6], [r1, r2, r3, r5, r6], ), ); it( "inside nested array and object", testResources( { a: createOutput(1, r1, r2), b: createOutput(2, r2, r3), c: createOutput([{ d: createOutput(3, r5) }], r6), }, { a: 1, b: 2, c: [{ d: 3 }] }, [r1, r2, r3, r6], [r1, r2, r3, r5, r6], ), ); it("across outer promise", testResources(Promise.resolve(createOutput(3, r1, r2)), 3, [], [r1, r2])); it( "across inner and outer promise", testResources(Promise.resolve(createOutput(Promise.resolve(3), r1, r2)), 3, [], [r1, r2]), ); it( "across promise and inner object", testResources( Promise.resolve(createOutput(Promise.resolve({ a: createOutput(1, r4, r5) }), r1, r2)), { a: 1 }, [], [r1, r2, r4, r5], ), ); it( "across promise and inner array and object", testResources( Promise.resolve(createOutput([Promise.resolve({ a: createOutput(1, r4, r5) })], r1, r2)), [{ a: 1 }], [], [r1, r2, r4, r5], ), ); it( "across inner object", testResources( createOutput(Promise.resolve({ a: createOutput(1, r4, r5) }), r1, r2), { a: 1 }, [r1, r2], [r1, r2, r4, r5], ), ); it( "across 'all'", testResources( all([ Promise.resolve({ a: createOutput(1, r1, r2) }), Promise.resolve({ b: createOutput(2, r3, r4) }), ]), [{ a: 1 }, { b: 2 }], [], [r1, r2, r3, r4], ), ); }); }); describe("type system", () => { it("across promises", async () => { var v = { a: 1, b: Promise.resolve(""), c: { d: true, e: Promise.resolve(4) } }; var xOutput = output(v); var x = await xOutput.promise(); // Ensure that ts thinks that 'e' is a number. const z: EqualsType<typeof x.c.e, number> = 1; // The runtime value better be a number; x.c.e.toExponential(); }); it("across nested promises", async () => { var v = { a: 1, b: Promise.resolve(""), c: Promise.resolve({ d: true, e: Promise.resolve(4) }) }; var xOutput = output(v); var x = await xOutput.promise(); // Ensure that ts thinks that 'e' is a number. const z: EqualsType<typeof x.c.e, number> = 1; // The runtime value better be a number; x.c.e.toExponential(); }); it("across outputs", async () => { var v = { a: 1, b: Promise.resolve(""), c: output({ d: true, e: [4, 5, 6] }) }; var xOutput = output(v); var x = await xOutput.promise(); // Ensure that ts thinks that 'e' is an array of numbers; const z: EqualsType<typeof x.c.e, number[]> = x.c.e; // The runtime value better be a number[] x.c.e.push(1); }); it("across nested outputs", async () => { var v = { a: 1, b: Promise.resolve(""), c: output({ d: true, e: output([4, 5, 6]) }) }; var xOutput = output(v); var x = await xOutput.promise(); // Ensure that ts thinks that 'e' is an array of numbers; const z: EqualsType<typeof x.c.e, number[]> = x.c.e; // The runtime value better be a number[] x.c.e.push(1); }); it("across promise and output", async () => { var v = { a: 1, b: Promise.resolve(""), c: Promise.resolve({ d: true, e: output([4, 5, 6]) }) }; var xOutput = output(v); var x = await xOutput.promise(); // Ensure that ts thinks that 'e' is an array of numbers; const z: EqualsType<typeof x.c.e, number[]> = x.c.e; // The runtime value better be a number[] x.c.e.push(1); }); it("across output and promise", async () => { var v = { a: 1, b: Promise.resolve(""), c: output({ d: true, e: Promise.resolve([4, 5, 6]) }) }; var xOutput = output(v); var x = await xOutput.promise(); // Ensure that ts thinks that 'e' is an array of numbers; const z: EqualsType<typeof x.c.e, number[]> = x.c.e; // The runtime value better be a number[] x.c.e.push(1); }); it("does not wrap functions", async () => { var sentinel = function (_: () => void) {}; // `v` should be type `() => void` rather than `UnwrappedObject<void>`. output(function () {}).apply((v) => sentinel(v)); }); }); it( "handles all in one", test(Promise.resolve([1, output({ a: [Promise.resolve([1, 2, { b: true, c: null }, undefined])] })]), [ 1, { a: [[1, 2, { b: true, c: null }, undefined]] }, ]), ); });