bespoken-cookbook / mock-alexa-dynamo

1 stars 1 forks source link

error trying to use mock-alexa-dynamo #1

Closed allthepies closed 6 years ago

allthepies commented 6 years ago

I'm trying to use mock-alexa-dynamo to mock a dynamodb session table for a skill I'm setting tests up for.

I get the error "Error: Error fetching user state: ConfigError: Missing region in config" when attempting to test launching the skill. This looks like the mocking isn't replacing the actual dynamodbclient.

The test case looks like:

const assert = require("chai").assert;
const virtualAlexa = require("virtual-alexa");

const alexa = virtualAlexa.VirtualAlexa.Builder()
  .handler("./src/index.handler") // Lambda function file and name
  .interactionModelFile("./resources/LanguageModel.json")
  .applicationID('redacted')
  .create();

require("mock-alexa-dynamo").enable();

it('Launches successfully', async () => {
  //this.timeout(10000);

  const reply = await alexa.launch();
  assert.include(reply.response.outputSpeech.ssml, "Welcome to the Guildford Recycling Skill");

});

Here's the error stack track

 1) Launches successfully:
    Error: Error fetching user state: ConfigError: Missing region in config
     at ValidateRequest.attributesHelper.get (src\node_modules\alexa-sdk\lib\alexa.js:168:35)
     at Response.<anonymous> (src\node_modules\alexa-sdk\lib\DynamoAttributesHelper.js:46:25)
     at Request.<anonymous> (src\node_modules\aws-sdk\lib\request.js:364:18)
     at Request.callListeners (src\node_modules\aws-sdk\lib\sequential_executor.js:105:20)
     at Request.emit (src\node_modules\aws-sdk\lib\sequential_executor.js:77:10)
     at Request.emit (src\node_modules\aws-sdk\lib\request.js:683:14)
     at Request.transition (src\node_modules\aws-sdk\lib\request.js:22:10)
     at AcceptorStateMachine.runTo (src\node_modules\aws-sdk\lib\state_machine.js:14:12)
     at src\node_modules\aws-sdk\lib\state_machine.js:26:10
     at Request.<anonymous> (src\node_modules\aws-sdk\lib\request.js:38:9)
     at Request.<anonymous> (src\node_modules\aws-sdk\lib\request.js:685:12)
     at Request.callListeners (src\node_modules\aws-sdk\lib\sequential_executor.js:115:18)
     at Request.emit (src\node_modules\aws-sdk\lib\sequential_executor.js:77:10)
     at Request.emit (src\node_modules\aws-sdk\lib\request.js:683:14)
     at Request.transition (src\node_modules\aws-sdk\lib\request.js:22:10)
     at AcceptorStateMachine.runTo (src\node_modules\aws-sdk\lib\state_machine.js:14:12)
     at src\node_modules\aws-sdk\lib\state_machine.js:26:10
     at Request.<anonymous> (src\node_modules\aws-sdk\lib\request.js:38:9)
     at Request.<anonymous> (src\node_modules\aws-sdk\lib\request.js:685:12)
     at Request.callListeners (src\node_modules\aws-sdk\lib\sequential_executor.js:115:18)
     at callNextListener (src\node_modules\aws-sdk\lib\sequential_executor.js:95:12)
     at src\node_modules\aws-sdk\lib\event_listeners.js:85:9
     at finish (src\node_modules\aws-sdk\lib\config.js:320:7)
     at src\node_modules\aws-sdk\lib\config.js:338:9
     at SharedIniFileCredentials.get (src\node_modules\aws-sdk\lib\credentials.js:126:7)
     at getAsyncCredentials (src\node_modules\aws-sdk\lib\config.js:332:24)
     at Config.getCredentials (src\node_modules\aws-sdk\lib\config.js:352:9)
     at Request.VALIDATE_CREDENTIALS (src\node_modules\aws-sdk\lib\event_listeners.js:80:26)
     at Request.callListeners (src\node_modules\aws-sdk\lib\sequential_executor.js:101:18)
     at Request.emit (src\node_modules\aws-sdk\lib\sequential_executor.js:77:10)
     at Request.emit (src\node_modules\aws-sdk\lib\request.js:683:14)
     at Request.transition (src\node_modules\aws-sdk\lib\request.js:22:10)
     at AcceptorStateMachine.runTo (src\node_modules\aws-sdk\lib\state_machine.js:14:12)
     at Request.runTo (src\node_modules\aws-sdk\lib\request.js:403:15)
     at Request.send (src\node_modules\aws-sdk\lib\request.js:367:10)
     at DocumentClient.get (src\node_modules\aws-sdk\lib\dynamodb\document_client.js:269:15)
     at Object.get (src\node_modules\alexa-sdk\lib\DynamoAttributesHelper.js:29:17)
     at AlexaRequestEmitter.ValidateRequest (src\node_modules\alexa-sdk\lib\alexa.js:166:51)
     at AlexaRequestEmitter.HandleLambdaEvent (src\node_modules\alexa-sdk\lib\alexa.js:126:25)
     at AlexaRequestEmitter.value (src\node_modules\alexa-sdk\lib\alexa.js:100:31)
     at exports.handler (src\index.js:16:9)
     at Promise (node_modules\virtual-alexa\lib\src\ModuleInvoker.js:24:13)
     at new Promise (<anonymous>)
     at Function.invokeFunction (node_modules\virtual-alexa\lib\src\ModuleInvoker.js:14:16)
     at Function.invokeHandler (node_modules\virtual-alexa\lib\src\ModuleInvoker.js:11:30)
     at LocalSkillInteractor.invoke (node_modules\virtual-alexa\lib\src\LocalSkillInteractor.js:13:50)
     at LocalSkillInteractor.<anonymous> (node_modules\virtual-alexa\lib\src\SkillInteractor.js:75:39)
     at Generator.next (<anonymous>)
     at node_modules\virtual-alexa\lib\src\SkillInteractor.js:7:71
     at new Promise (<anonymous>)
     at __awaiter (node_modules\virtual-alexa\lib\src\SkillInteractor.js:3:12)
     at LocalSkillInteractor.callSkill (node_modules\virtual-alexa\lib\src\SkillInteractor.js:67:16)
     at LocalSkillInteractor.launched (node_modules\virtual-alexa\lib\src\SkillInteractor.js:46:21)
     at VirtualAlexa.launch (node_modules\virtual-alexa\lib\src\VirtualAlexa.js:33:32)
     at Context.it (test\index-test.js:19:29)

Any words of wisdom ? I would imagine it's the way I've structured my project which could be the issue.

I have a /src dir containing my skill code and within that src dir is a node_modules dir containing the alexa-sdk package. Then I have a /test dir containing my test case. In the root dir / I also have a node_modules dir containing all the dev dependencies I use, chai, mocha, gulp, the aws sdk etc (I believe there may be another alexa-sdk in there too).

allthepies commented 6 years ago

This ended up being an issue caused by a modification to the Node Alexa SDK. The change was to allow a custom DynamoDBClient to be used in the ASK.

https://github.com/alexa/alexa-skills-kit-sdk-for-nodejs/commit/0e5fe517685e3cad6ba04e164f0c05a42e8b4ca2#diff-d7a2af39f161f82f95b63ac6a394867d

Temporarily reverting back to v1.0.22 of the ASK got me back up and running while the bespoken team look at enhancing the mock to work with the new version of ASK.

jkelvie commented 6 years ago

This issue is addressed by #2. Please take a look when you have a chance @allthepies.

I also added some more docs to the README, that I believe address your other questions - presetting and resetting state specifically.

jkelvie commented 6 years ago

And I should add - new version is already available 0.2.1.

allthepies commented 6 years ago

Hi,

The new version addresses the missing region in config issue, thanks!

The mock works fine in basic-mode but the pre-loading of mock db content is giving me issues.

I can pre-load the cache but it then remains loaded with those initial values as far as the skill code is concerned. I suspect that the alexa-sdk code isn't calling the mock "get" method for subsequent test cases.

What I want to do is run a sequence of tests with each test looking like:

  1. Preload the mock with some attributes.
  2. Test a skill intent which relies upon those values.

This works for the first test case, but for all subsequent test cases the skill code is still picking up the mock attributes from the first tests case and is ignoring the most recently "loaded" mock values.

So the following test example:

const chai = require('chai');
var assert = chai.assert;
const vax = require("virtual-alexa");

const alexa = vax.VirtualAlexa.Builder()
  .handler("src/index.handler") // Lambda function file and name
  .applicationID('amzn1.ask.skill.a2008bc7-f6b0-4306-8077-3f647126a2d1')
  .interactionModelFile("./resource/voiceModel.json")
  .create();

let db = require("mock-alexa-dynamo");
db.enable();

let userId = alexa.context().user().id();

describe("My Tests", async function () {

  beforeEach(async () => {
    // Nothing but I've tried having db.enable() here with same outcome
  });

  afterEach(async () => {
    await db.reset();
  });

  it('Launches successfully', async function () {
    await db.load(userId, {
      userInfo: {
        'testPersist': 'hello world',
        'apples': 'oranges',
        'pears': 1
      }
    });

    let reply = await alexa.launch();
    assert.include(reply.response.outputSpeech.ssml, "Welcome to my test skill");
    assert.isNotOk(reply.response.shouldEndSession, "Session remains open");

  });

  it('Provides help', async function () {

    await db.load(userId, {
      userInfo: {
        'testPersist': 'hello world',
        'apples': 'bananas',
        'pears': 2
      }
    });
    let help = await alexa.utter('help');
    assert.include(help.response.outputSpeech.ssml, "Some help text.");
    assert.isNotOk(help.response.shouldEndSession, "Session remains open");

  });
});

Gives the following output (the skill code is writing "this.attributes" to stdout )

First test case output:

  console.log src\eventHandlers.js:13
    attributes: {"userInfo":{"testPersist":"hello world","apples":"oranges","pears":1}}

Second test case output:

  console.log src\eventHandlers.js:21
    attributes: {"userInfo":{"testPersist":"hello world","apples":"oranges","pears":1}}

The session attributes are unchanged for the second test, I would expect to see:

  console.log src\eventHandlers.js:21
    attributes: {"userInfo":{"testPersist":"hello world","apples":"bananas","pears":2}}

I've tried mocha and jest with the same result.

allthepies commented 6 years ago

When I add instrumentation into the mock code as follows:

  get: function (table, userId, callback) {
    console.log('IN MOCK GET');
    let data = MockDynamo.userIdMap[userId];
    if (data === undefined || data === null || isEmptyObject(data)) {
      callback(null, {});
    } else {
      console.log(data.Item["mapAttr"].STATE);
      callback(null, data.Item["mapAttr"]);
    }
  },

  fetch: function (userId) {
    console.log('IN MOCK FETCH');
    let data = undefined;
    if (userId in this.userIdMap) {
      data = this.userIdMap[userId].Item.mapAttr;
    }
    return data;
  },

  load: function (userId, data) {
    console.log('IN MOCK LOAD');
    this.set(undefined, userId, data);
  },

  reset: function () {
    MockDynamo.userIdMap = {};
  },

  set: function (table, userId, data, callback) {
    console.log('IN MOCK SET');
    const parameters = {
      Item: {
        mapAttr: data,
        userId: userId,
      },
      TableName: table
    };

And rerun the above tests then I get the output:

First test

  console.log node_modules\mock-alexa-dynamo\index.js:55
    IN MOCK LOAD

  console.log node_modules\mock-alexa-dynamo\index.js:64
    IN MOCK SET

  console.log node_modules\mock-alexa-dynamo\index.js:35
    IN MOCK GET

  console.log src\eventHandlers.js:13
    attributes: {"userInfo":{"testPersist":"hello world","apples":"oranges","pears":1}}

Second test

  console.log node_modules\mock-alexa-dynamo\index.js:55
    IN MOCK LOAD

  console.log node_modules\mock-alexa-dynamo\index.js:64
    IN MOCK SET

  console.log src\eventHandlers.js:21
    attributes: {"userInfo":{"testPersist":"hello world","apples":"oranges","pears":1}}

For the first test we can see the db.load() operating being called, this in turn invokes db.set() to store the attributes in the internal datastructure. Then we see the db.get() call which I guess is from the alexa-sdk on entry to the skill code.

The second test output differs in that there is no db.get() call from the alexa-sdk and it seems to retain the attributes from the first test.

allthepies commented 6 years ago

OK, the reason the db.get() method isn't being invoked for subsequent tests cases is down to the alexa session still being open.

In the alexa-sdk (added debug code)

  console.log('Before dynamo decision');
    console.log('dynamoDBTableName: ' + this.dynamoDBTableName)
    console.log('sessionId: ' + event.session.sessionId);
    console.log('session: ' + JSON.stringify(event.session));
    if (this.dynamoDBTableName && (!event.session.sessionId || event.session['new'])) {
      console.log('Invoking helper get');
      attributesHelper(this.dynamoDBClient).get(this.dynamoDBTableName, userId, (err, data) => {
        if (err) {
          const error = new Error('Error fetching user state: ' + err);
          if (typeof callback === 'undefined') {
            return context.fail(error);
          } else {
            return callback(error);
          }
        }

event.session['new'] is only true for the first invocation, it's false for subsequent invocations and so the db.get() isn't called. How can I convince the alexa-sdk that subsequent test cases are in new sessions ?

allthepies commented 6 years ago

This seems to sort things

  beforeEach(async () => {
    await alexa.context().newSession();    // This does the trick and causes the session attributes to be reloaded from the mock
    await db.reset();
  });

Sorry about the multiple msgs, got there in the end. Worth adding a sidenote in the mock README ?