I have the following spec file which I am running with jest ^27.4.7. This files is testing a nodeV14 lambda function that handles updating users and their farms.
When mocking the UpdateItemCommand
, this is returning undefined with specific input.
const { mockClient } = require("aws-sdk-client-mock");
const {
DynamoDBClient,
QueryCommand,
BatchWriteItemCommand,
UpdateItemCommand,
} = require("@aws-sdk/client-dynamodb");
const { marshall } = require("@aws-sdk/util-dynamodb");
const {
CognitoIdentityProviderClient,
AdminGetUserCommand,
} = require("@aws-sdk/client-cognito-identity-provider");
const { getFarmsOfUser, updateUserFarm, updateUser } = require("./dbCommands");
const { getUserEmail } = require("./cognitoCommands");
const { handler } = require("./index");
const dbMock = mockClient(DynamoDBClient);
const cgMock = mockClient(CognitoIdentityProviderClient);
const fakeUuid = "21153f1a-0345-4ea2-bbd2-0e7d16922b29";
jest.mock("uuid", () => ({
v4: () => fakeUuid,
}));
describe("update-users", () => {
const OLD_ENV = process.env;
const email = "[email protected]";
const date = "22022-01-29T00:00:00.000Z";
const userPoolId = "user-pool-id";
const userFarmsTableName = "user-farm-table";
const usersTableName = "users-table";
function calcExprAttrVal(farmIds, updatedAt) {
const marshalled = marshall({ farmIds, updatedAt });
const expressionAttVal = {};
for (const [key, value] of Object.entries(marshalled)) {
expressionAttVal[`:${key}`] = value;
}
return expressionAttVal;
}
beforeAll(() => {
jest.useFakeTimers("modern");
jest.setSystemTime(new Date(2022, 0, 29));
});
beforeEach(() => {
jest.resetModules(); // Most important - it clears the cache
process.env = { ...OLD_ENV }; // Make a copy
});
afterAll(() => {
process.env = OLD_ENV; // Restore old environment
jest.useRealTimers();
});
describe("index", () => {
const event = {
users: [
{ id: "1", farmIds: ["1", "2"] },
{ id: "2", farmIds: ["3"] },
],
};
beforeEach(() => {
dbMock.reset();
cgMock.reset();
process.env.USER_POOL_ID = userPoolId;
process.env.USER_FARMS_TABLE = userFarmsTableName;
process.env.USERS_TABLE_NAME = usersTableName;
});
it("should add a farm to user 2 and remove and add a farm from user 1", async () => {
const firstGetFarmInput = {
TableName: userFarmsTableName,
KeyConditionExpression: `#key = :val`,
IndexName: "user-id-index",
ExpressionAttributeNames: {
"#key": "userId",
},
ExpressionAttributeValues: {
":val": {
S: "1",
},
},
};
const secondGetFarmInput = {
TableName: userFarmsTableName,
KeyConditionExpression: `#key = :val`,
IndexName: "user-id-index",
ExpressionAttributeNames: {
"#key": "userId",
},
ExpressionAttributeValues: {
":val": {
S: "2",
},
},
};
const firstUpdateUsrIpt = {
TableName: usersTableName,
Key: {
id: { S: "1" },
},
UpdateExpression: `SET #F=:farmIds, #U=:updatedAt`,
ExpressionAttributeNames: {
"#F": "farmIds",
"#U": "updatedAt",
},
ExpressionAttributeValues: calcExprAttrVal(["1", "2"], date),
ReturnValues: "ALL_NEW",
};
const secondUpdateUsrIpt = {
TableName: usersTableName,
Key: {
id: { S: "2" },
},
UpdateExpression: `SET #F=:farmIds, #U=:updatedAt`,
ExpressionAttributeNames: {
"#F": "farmIds",
"#U": "updatedAt",
},
ExpressionAttributeValues: calcExprAttrVal(["3"], date),
ReturnValues: "ALL_NEW",
};
cgMock.on(AdminGetUserCommand).resolves({
UserAttributes: [{ Name: "email", Value: email }],
});
dbMock
.on(QueryCommand, { ...firstGetFarmInput })
.resolves({
Items: [
marshall({ farmId: "3", userId: "1", id: "22" }),
marshall({ farmId: "1", userId: "1", id: "33" }),
],
})
.on(QueryCommand, { ...secondGetFarmInput })
.resolves({ Items: [] });
dbMock.on(BatchWriteItemCommand).resolves();
const expOne = { id: "1", farmIds: ["1", "2"], email, updatedAt: date };
const expTwo = { id: "2", farmIds: ["3"], email, updatedAt: date };
// FAILING COMMANDS
dbMock
.on(UpdateItemCommand, { ...firstUpdateUsrIpt })
.resolves({ Attributes: marshall(expOne) })
.on(UpdateItemCommand, { ...secondUpdateUsrIpt })
.resolves({ Attributes: marshall(expTwo) });
const expected = [expOne, expTwo];
const res = await handler(event);
expect(expected).toEqual(res);
});
});
});
My index file:
const { getFarmsOfUser, updateUserFarm, updateUser } = require("./dbCommands");
const { getUserEmail } = require("./cognitoCommands");
const { defineToAdd, defineToRemove } = require("./utils");
exports.handler = async (event) => {
console.log(`Event ${JSON.stringify(event, null, 2)}`);
try {
const { users } = event;
return await Promise.all(
users.map(async (data) => {
let { farmIds, id } = data;
const date = new Date();
const updatedAt = date.toISOString();
const existingFarms = await getFarmsOfUser(id);
if (!Array.isArray(farmIds) || !farmIds || !farmIds.length) {
farmIds = [];
}
const toDelete = defineToRemove(existingFarms, farmIds);
const toAdd = defineToAdd(farmIds, existingFarms);
await updateUserFarm(id, toAdd, toDelete, updatedAt);
const userEmail = await getUserEmail(id);
if (!toAdd.length && !toDelete.length) {
data.email = userEmail;
return data;
}
const user = await updateUser(id, farmIds, updatedAt);
user.email = userEmail;
return user;
})
);
} catch (err) {
console.error(`Error updating user: ${err}`);
throw err;
}
};
My dbCommands:
const {
DynamoDBClient,
QueryCommand,
BatchWriteItemCommand,
UpdateItemCommand,
} = require("@aws-sdk/client-dynamodb");
const { marshall, unmarshall } = require("@aws-sdk/util-dynamodb");
const { v4: uuid } = require("uuid");
const { AWS_REGION } = process.env;
const client = new DynamoDBClient({ region: AWS_REGION });
async function getFarmsOfUser(userId) {
const { USER_FARMS_TABLE } = process.env;
const input = {
TableName: USER_FARMS_TABLE,
KeyConditionExpression: `#key = :val`,
IndexName: "user-id-index",
ExpressionAttributeNames: {
"#key": "userId",
},
ExpressionAttributeValues: {
":val": {
S: userId,
},
},
};
const command = new QueryCommand(input);
const { Items } = await client.send(command);
return Items.length ? Items.map((item) => unmarshall(item)) : [];
}
async function updateUserFarm(userId, toAdd, toDelete, updatedAt) {
const { USER_FARMS_TABLE } = process.env;
let reqs = [];
if (toAdd.length) {
const putReqs = toAdd.reduce((acc, farmId) => {
const id = uuid();
const newItem = {
id,
farmId,
userId,
createdAt: updatedAt,
};
acc.push({ PutRequest: { Item: marshall(newItem) } });
return acc;
}, []);
reqs = reqs.concat(putReqs);
}
if (toDelete.length) {
const deleteReqs = toDelete.reduce((acc, id) => {
acc.push({ DeleteRequest: { Key: marshall({ id }) } });
return acc;
}, []);
reqs = reqs.concat(deleteReqs);
}
if (!reqs.length) {
return Promise.resolve();
}
const input = {
RequestItems: {
[USER_FARMS_TABLE]: reqs,
},
};
const command = new BatchWriteItemCommand(input);
await client.send(command);
}
async function updateUser(id, farmIds, updatedAt) {
const { USERS_TABLE_NAME } = process.env;
const marshalled = marshall({ farmIds, updatedAt });
const expressionAttVal = {};
for (const [key, value] of Object.entries(marshalled)) {
expressionAttVal[`:${key}`] = value;
}
const input = {
TableName: USERS_TABLE_NAME,
Key: marshall({ id }),
UpdateExpression: `SET #F=:farmIds, #U=:updatedAt`,
ExpressionAttributeNames: {
"#F": "farmIds",
"#U": "updatedAt",
},
ExpressionAttributeValues: expressionAttVal,
ReturnValues: "ALL_NEW",
};
const command = new UpdateItemCommand(input);
const res = await client.send(command);
console.log(res); // --> undefined ???
const { Attributes } = res;
return unmarshall(Attributes);
}
module.exports = {
updateUserFarm: updateUserFarm,
updateUser: updateUser,
getFarmsOfUser: getFarmsOfUser,
};
Utils:
function defineToAdd(farmIds, existingFarms) {
return farmIds.reduce((acc, cv) => {
const exists = existingFarms.find(({ farmId }) => farmId === cv);
if (!exists) {
acc.push(cv);
}
return acc;
}, []);
}
function defineToRemove(existingFarms, farmIds) {
return existingFarms.reduce((acc, cv) => {
const { id, farmId } = cv;
if (!farmIds.includes(farmId)) {
acc.push(id);
}
return acc;
}, []);
}
module.exports = {
defineToAdd: defineToAdd,
defineToRemove: defineToRemove,
};
Cognito commands:
const {
CognitoIdentityProviderClient,
AdminGetUserCommand,
} = require("@aws-sdk/client-cognito-identity-provider");
const { AWS_REGION } = process.env;
const client = new CognitoIdentityProviderClient({ region: AWS_REGION });
async function getUser(sub) {
const { USER_POOL_ID } = process.env;
const input = {
UserPoolId: USER_POOL_ID,
Username: sub,
};
const command = new AdminGetUserCommand(input);
return await client.send(command);
}
async function getUserEmail(id) {
const { UserAttributes } = await getUser(id);
const { Value: email } = UserAttributes.find(({ Name }) => Name === "email");
return email;
}
module.exports = {
getUserEmail: getUserEmail,
};