0

I am trying to use Pulumi to create an AWS Lambda that manipulates a DynamoDB table and is triggered by an API Gateway HTTP request.

My configuration works perfectly when I run pulumi up, but when I run Vitest, my test passes but exits with non-zero and this message:

⎯⎯⎯ Unhandled Rejection ⎯⎯⎯
Error: Could not find property info for real property on object: sdk

I can see that the error comes from this code in Pulumi, but I can't figure out what causes it. Am I doing something wrong or is this a bug (in which case I can create an issue)?

Below is a summary that I think has all the relevant info, but there is a minimal repo demonstrating the problem here (GitHub actions fail with the problem I'm describing).

I have an index.ts file that creates a database, gateway, and lambda:

import * as aws from '@pulumi/aws'
import * as apigateway from '@pulumi/aws-apigateway'
import handler from './handler'

const table = new aws.dynamodb.Table('Table', {...})

const tableAccessPolicy = new aws.iam.Policy('DbAccessPolicy', {
    // removed for brevity. Allows put, get, delete
})

const lambdaRole = new aws.iam.Role('lambdaRole', {...})

new aws.iam.RolePolicyAttachment('RolePolicyAttachment', {...})

const callbackFunction = new aws.lambda.CallbackFunction(
  'callbackFunction',
  {
    role: lambdaRole,
    callback: handler(table.name),
  }
)

const api = new apigateway.RestAPI('api', {
  routes: [{
    method: 'GET',
    path: '/',
    eventHandler: callbackFunction,
  }]
})

export const dbTable = table
export const url = api.url

The handler is imported from a separate file:

import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
import * as pulumi from '@pulumi/pulumi';
import * as aws from '@pulumi/aws';

export default function (tableName: pulumi.Output<string>) {
  return async function handleDocument(
    event: APIGatewayProxyEvent
  ): Promise<APIGatewayProxyResult> {
    try {
      const client = new aws.sdk.DynamoDB.DocumentClient();
      await client
        .put({
          TableName: tableName.get(),
          Item: { PK: 'hello', roomId: '12345' },
        })
        .promise();

      const result = await client
        .get({
          TableName: tableName.get(),
          Key: { PK: 'hello' },
        })
        .promise();

      await client
        .delete({
          TableName: tableName.get(),
          Key: { PK: 'hello' },
        })
        .promise();
      return {
        statusCode: 200,
        body: JSON.stringify({
          item: result.Item,
        }),
      };
    } catch (err) {
      return {
        statusCode: 200,
        body: JSON.stringify({
          error: err,
        }),
      };
    }
  };
}

Finally, I have a simple test:

import * as pulumi from '@pulumi/pulumi';
import { describe, it, expect, beforeAll } from 'vitest';

pulumi.runtime.setMocks(
  {
    newResource: function (args: pulumi.runtime.MockResourceArgs): {
      id: string;
      state: Record<string, any>;
    } {
      return {
        id: `${args.name}_id`,
        state: args.inputs,
      };
    },
    call: function (args: pulumi.runtime.MockCallArgs) {
      return args.inputs;
    },
  },
  'project',
  'stack',
  false
);

describe('infrastructure', () => {
  let infra: typeof import('./index');

  beforeAll(async function () {
    // It's important to import the program _after_ the mocks are defined.
    infra = await import('./index');
  });

  it('Creates a DynamoDB table', async () => {
    const tableId = await new Promise((resolve) => {
      infra?.dbTable?.id.apply((id) => resolve(id));
    });
    expect(tableId).toBe('Table_id');
  });
});
Dave
  • 967
  • 7
  • 13

1 Answers1

0

Your function is importing the Pulumi SDK, and you're trying to set the table name as a pulumi.Output<string>

Using the Pulumi SDK inside a lambda function isn't recommended or support.

I would recommend removing the Pulumi dependency from your function

import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';

export default function (tableName: string) {
  return async function handleDocument(
    event: APIGatewayProxyEvent
  ): Promise<APIGatewayProxyResult> {
    try {
      const client = new aws.sdk.DynamoDB.DocumentClient();
      await client
        .put({
          TableName: tableName.get(),
          Item: { PK: 'hello', roomId: '12345' },
        })
        .promise();

      const result = await client
        .get({
          TableName: tableName.get(),
          Key: { PK: 'hello' },
        })
        .promise();

      await client
        .delete({
          TableName: tableName.get(),
          Key: { PK: 'hello' },
        })
        .promise();
      return {
        statusCode: 200,
        body: JSON.stringify({
          item: result.Item,
        }),
      };
    } catch (err) {
      return {
        statusCode: 200,
        body: JSON.stringify({
          error: err,
        }),
      };
    }
  };
}

The callback function should take take non inputty types, which should then remove the need to call the Pulumi SDK during your test suite. You can see an example here:

https://github.com/pulumi/examples/blob/258d3bad0a00020704743e37911c51be63c06bb4/aws-ts-lambda-efs/index.ts#L32-L40

jaxxstorm
  • 12,422
  • 5
  • 57
  • 67
  • 1
    This is surprising to me because I was following examples from pulumi.com/serverless that definitely call `.get()` within inline functions. The only difference with my code (I think?) is that it's in another file. In either case, if I do just pass in a string, then I have to do `table.name.apply(name => handler(name))` outside `handler.ts` but then I get a TypeScript error about passing in the wrong type for the `callback` param in the args for `CallbackFunction`. Finally, this doesn't explain why it works in "real life" but not the test. – Dave Aug 13 '22 at 22:53
  • I created a [pull request](https://github.com/dave-burke/pulumi-vitest-example/pull/1) that demonstrates the same issue using an official example from https://github.com/pulumi/examples/blob/master/aws-ts-apigateway/index.ts – Dave Aug 13 '22 at 23:30
  • Apologies if my explanation didn't cover things correctly. The real problem here is that vitest isn't aware of the Pulumi engine, so isn't aware of Outputty types. Pulumi calls ts-node and passes the SDK definitions to the Pulumi engine. Vitest isn't aware of any of this, and runs the TypeScript code as is. That's why you get the unhandled rejection – jaxxstorm Aug 14 '22 at 17:36
  • Thanks for continuing to help me. So when Pulumi runs normally it executes `serializeSomeObjectPropertiesAsync` in a context where `aws.sdk` is available, but when Vitest runs that doesn't happen. Is that right? Does that mean Vitest can't be used in this context? Is that a limitation of Vitest or of Pulumi? Pulumi docs use mocha, but [say](pulumi.com/docs/guides/testing/unit/) you can use any framework. Do you know what mocha does differently that [it works fine](https://github.com/dave-burke/pulumi-vitest-example/pull/2)? Sorry if this is off topic. I can post elsewhere if more appropriate. – Dave Aug 14 '22 at 19:22