How to stub nested dependecies with ts-sinon - node.js

I got a simple unit test with the following code:
my-pubsub.spec.ts
import * as tsSinon from 'ts-sinon';
import { myPubSubFunction } from './my-pubsub';
import * as sendEmail from './send-mail';
describe("Notifications PubSub tests", () => {
it("Should trigger audit", (done) => {
const today = new Date()
const data = {
( my data )
}
const spy = tsSinon.default.spy(sendEmail, "sendNotificationMessage")
const dataBuffer = Buffer.from(JSON.stringify(data))
// Call tested function and verify its behavior
myPubSubFunction(dataBuffer)
setTimeout(() => {
// check if spy was called
tsSinon.default.assert.calledOnce(spy)
done()
}, 100)
})
})
And my-pubsub.ts got a call to a function from send-mail with contains a function to set the Api key
import * as sgMail from '#sendgrid/mail';
sgMail.setApiKey(
"MyKey"
) // error in here
export function sendNotificationMessage(mailConfig: any) {
const defaultConfig = {
from: {
email: "noreply#mymail.com",
name: "my name",
},
template_id: "my template",
}
const msg = { ...defaultConfig, ...mailConfig }
return sgMail.send(msg)
}
However when running my tests I got the following error TypeError: sgMail.setApiKey is not a function
Edit: added a bit more code to the send-mail code.
Bellow you can find a bit more code about my-pubsub.ts
my-pubsub.ts
import * as admin from 'firebase-admin';
import * as functions from 'firebase-functions';
import moment = require('moment');
import { IModel, ModelType } from '../models/model.model';
import { sendNotificationMessage } from '../shared/send-mail';
const { PubSub } = require("#google-cloud/pubsub")
try {
admin.initializeApp()
} catch (e) {}
const db = admin.firestore()
const pubSubClient = new PubSub()
export const myPubSubTrigger = functions.pubsub
.topic("on-trigger")
.onPublish(async (message) => {
console.log("version 1")
const myMessage = Buffer.from(message.data, "base64").toString("utf-8")
const data: IModel = JSON.parse(myMessage)
( logic to create my object )
/**
* Send email
*/
const result: any = await sendNotificationMessage(myObject)
/**
* Check result
*/
if (result[0].statusCode === 202) {
await docRef.update({ emailSent: true })
}
( another publish to audit the action )
})

The problem is not with tests per se, but incorrect types definition of #sendgrid/mail:
// OK
import sgMail from "#sendgrid/mail";
import { default as sgMail2 } from "#sendgrid/mail";
console.log(sgMail === sgMail2);
sgMail.setApiKey("SG.key");
sgMail2.setApiKey("SG.key2");
// BROKEN
// type definition does not match runtime shape
import * as sgMailIncorrectlyTyped from "#sendgrid/mail";
console.log(sgMailIncorrectlyTyped, sgMailIncorrectlyTyped.setApiKey === undefined);
STACKBLITZ

Related

How to override url for RTK query

I'm writing pact integration tests which require to perform actual call to specific mock server during running tests.
I found that I cannot find a way to change RTK query baseUrl after initialisation of api.
it('works with rtk', async () => {
// ... setup pact expectations
const reducer = {
[rtkApi.reducerPath]: rtkApi.reducer,
};
// proxy call to configureStore()
const { store } = setupStoreAndPersistor({
enableLog: true,
rootReducer: reducer,
isProduction: false,
});
// eslint-disable-next-line #typescript-eslint/no-explicit-any
const dispatch = store.dispatch as any;
dispatch(rtkApi.endpoints.GetModules.initiate();
// sleep for 1 second
await new Promise((resolve) => setTimeout(resolve, 1000));
const data = store.getState().api;
expect(data.queries['GetModules(undefined)']).toEqual({modules: []});
});
Base api
import { createApi } from '#reduxjs/toolkit/query/react';
import { graphqlRequestBaseQuery } from '#rtk-query/graphql-request-base-query';
import { GraphQLClient } from 'graphql-request';
export const client = new GraphQLClient('http://localhost:12355/graphql');
export const api = createApi({
baseQuery: graphqlRequestBaseQuery({ client }),
endpoints: () => ({}),
});
query is very basic
query GetModules {
modules {
name
}
}
I tried digging into customizing baseQuery but were not able to get it working.

How test listener on my custom event emitter in node typescript

I'm trying to test a service that has a listener of the a custom Event Emitter in node with typescript and mocha, sinon.
My custom emmiter;
class PublishEmitter extends EventEmitter {
publish(id: string) {
this.emit('publish', id);
}
}
My service use case:
export default class PublishVehicle {
constructor(
private findVehicle: FindVehicle, // Service that contains find methods on repository
private updateVehicle: UpdateVehicle, // Service that contains update methods on repository
private logger: ILogger,
) {
this.producer = producer;
this.logger = logger;
}
listen() {
this.logger.log('debug', 'Creating listener on PublishEmitter');
this.publishListener = this.publishListener.bind(this);
pubsub.on('publish', this.publishListener);
}
/**
* Listener on PublishEmitter.
*
* #param event
*/
async publishListener(event: string) {
try {
const vehicle = await this.findVehicle.findById(event);
if (vehicle?.state === State.PENDING_PUBLISH) {
//
const input = { state: State.PUBLISH };
await this.updateVehicle.update(vehicle.id, input);
this.logger.log('debug', `Message sent at ${Date.now() - now} ms`);
}
this.logger.log('debug', `End Vehicle's Publish Event: ${event}`);
} catch (error) {
this.logger.log('error', {
message: `publishListener: ${event}`,
stackTrace: error,
});
}
}
}
and in my test file:
import chai from 'chai';
const { expect } = chai;
import sinon from 'sinon';
import { StubbedInstance, stubInterface } from 'ts-sinon';
import pubsub from './PublishEmitter';
describe('Use Case - Publish Vehicle', function () {
let mockRepository: MockVehicleRepository;
let publishVehicle: PublishVehicle;
let findVehicleUseCase: FindVehicle;
let updateVehicleUseCase: UpdateVehicle;
before(() => {
const logger = Logger.getInstance();
mockRepository = new MockVehicleRepository();
findVehicleUseCase = new FindVehicle(mockRepository, logger);
updateVehicleUseCase = new UpdateVehicle(mockRepository);
publishVehicle = new PublishVehicle(
findVehicleUseCase,
updateVehicleUseCase,
logger,
);
});
afterEach(() => {
// Restore the default sandbox here
sinon.restore();
});
it('Should emit event to publish vehicle', async () => {
const vehicle = { ... }; // dummy data
const stubFindById = sinon
.stub(mockRepository, 'findById')
.returns(Promise.resolve(vehicle));
const stubUpdate = sinon
.stub(mockRepository, 'update')
.returns(Promise.resolve(vehicle));
const spy = sinon.spy(publishVehicle, 'publishListener');
publishVehicle.listen();
pubsub.publish(vehicle.id);
expect(spy.calledOnce).to.be.true; // OK
expect(stubFindById.calledOnce).to.be.true; // Error (0 call)
expect(stubUpdate.calledOnce).to.be.true; // Error (0 call)
});
});
When I debug this test, indeed the methods are called but they seem to be executed after it has gone through the last expect lines.
The output:
1 failing
1) Use Case - Publish Vehicle
Should emit event to publish vehicle:
AssertionError: expected false to be true
+ expected - actual
-false
+true
UPDATE
Finally I was be able to solve my problem wrapping expect lines in setTimeout.
setTimeout(() => {
expect(spy.calledOnce).to.be.true; // OK
expect(stubFindById.calledOnce).to.be.true; // OK
expect(stubUpdate.calledOnce).to.be.true; // OK
done();
}, 0);

How to mock prisma with jest-mock

I use prisma to interact with my database and i would like to use jest-mock to mock the findMany call. https://jestjs.io/docs/jest-object#jestmockedtitem-t-deep--false
brands.test.ts
import { PrismaService } from "#services/mysql.service";
import { mocked } from "jest-mock";
import faker from "#faker-js/faker";
import { GetBrands } from "./brand";
jest.mock("#services/mysql.service");
/**
* #group unit
*/
describe("Brand", () => {
afterAll(async () => {});
const mockedPrismaService = mocked(PrismaService, true);
it("should get a list of brands", async () => {
const mockedData = [
{
id: faker.datatype.uuid(),
name: faker.datatype.string(),
image: {
source: "some_source",
dtype: "some_dtype",
},
},
];
//#ts-ignore - because of relational data mockedData.image
mockedPrismaService.brand.findMany.mockResolvedValueOnce(mockedData);
const [response, error] = await GetBrands();
console.log(response, error);
});
});
mysql.service.ts
import mysql from "mysql2/promise";
import { Config } from "#config";
import { PrismaClient, Prisma } from "#prisma/client";
export const MySQLEscape = mysql.escape;
export const MySQLPreparedStatement = mysql.format;
export const PrismaService = new PrismaClient({});
export const PrismaHelper = Prisma;
However when i run this test i get the following error.
TypeError: Cannot read properties of undefined (reading 'brand')
Factory Mock
One option is to option use the factory approach when mocking your client.
jest.mock("#services/mysql.service", () => ({
PrismaService: {
brand: {
findMany: jest.fn(() => { })
}
},
}));
Then within your test, you can mock the findMany function to return your test data, then call the function being tested.
const mockedData = [...];
PrismaService.brand.findMany.mockResolvedValueOnce(mockedData);
const result = await GetBrands();
It's a bit cumbersome, but it works.
Note that in my example, I've implemented GetBrands as follows:
import { PrismaService } from "#services/mysql.service"
export const GetBrands = async () => {
const data = await PrismaService.brand.findMany();
return data;
}
Your example
In your example, you're using automatic mocking, and I'm not too familiar with it so I'm not sure how to get it working.
What seems to be happening to cause the error is your PrismaService is undefined when it's imported here:
import { PrismaService } from "#services/mysql.service";
And then calling the mocked function with an undefined parameter returns undefined:
const mockedPrismaService = mocked(undefined, true); // returns undefined
And finally, calling the following is what throws the error:
mockedPrismaService.brand.findMany.mockResolvedValueOnce(mockedData);
// TypeError: Cannot read properties of undefined (reading 'brand')
I would have thought something like this would be what you're after, but this throws an error:
jest.mock("#services/mysql.service", () => ({
PrismaService: mocked(PrismaService, true)
}));
// 6 |
// 7 | jest.mock("#services/mysql.service", () => ({
//> 8 | PrismaService: mocked(PrismaClient, true)
// | ^
// 9 | }));
Check out the docs
Might be worth checking out the Prismas documentation on unit testing, as they suggest a couple of pretty different approaches.

When I run my bot, the `ready` event doesn't fire but the bot is online

When I run my bot, the ready event doesn't fire but the bot is online. My event handler is in the start method in the Client.ts class,
and I execute the start method in the index.ts file.
My client class: ./classes/Client.ts
import { Client as DiscordClient, ClientOptions, Collection } from 'discord.js';
import fs from 'fs';
import path from 'path';
export class Client extends DiscordClient {
commandarray: any[] = [];
commands: Collection<string, any> = new Collection();
constructor (options: ClientOptions, token: string) {
super(options);
this.login(token);
}
async start() {
//Event Handler
const eventDirectories = await fs.readdirSync('./events');
for (const dir of eventDirectories) {
const eventFiles = await fs.readdirSync(`./events/${dir}`).filter(file => file.endsWith(".ts"));
if (eventFiles.length <= 0)
return console.log("[EVENT HANDLER] - Cannot find any events!");
for (const file of eventFiles) {
const event = require(`../events/${dir}/${file}`);
if (event.once) {
this.once(event.name, (...args) => event.execute(...args, this));
} else {
this.on(event.name, (...args) => event.execute(...args));
}
}
}
// Slash Command Handler
const cmdDirectories = await fs.readdirSync('./commands');
for (const dir of cmdDirectories) {
const cmdFiles = await fs.readdirSync(`./commands/${dir}`).filter(file => file.endsWith(".ts"));
if (cmdFiles.length <= 0)
return console.log("[COMMAND HANDLER] - Cannot find any commands!");
for (const file of cmdFiles) {
const command = require(`../commands/${dir}/${file}`)
await this.commandarray.push(command);
await this.commands.set(command.name, command);
}
}
}
};
My index.ts file: ./index.ts
import { Client } from "./classes/Client";
import { config } from "dotenv";
config();
export const client: Client = new Client({ intents: 515 }, process.env.token!);
client.start();
My ready event: ./events/Client/ready.ts
import { Client } from '../../classes/Client';
export default {
name: 'ready',
once: true,
async execute(client: Client) {
await console.log(`Logged in as ${client.user?.tag}`);
await client.application?.commands.set(client.commandarray);
}
}
Edit, I fixed the first issue thanks to Rahuletto, but found a new one and have updated the question accordingly.
It's a wrong path. You should show ur path structure (file format).
I think it's ../events/Client/ready.ts.
I used export default to export the event
So, instead of using event.<property/function> in the handler, I should use event.default.<property/function>

React and Easybase - Invalid hook call. Hooks can only be called inside of the body of a function component

I am trying to use React and Easybase (database). I'm having some issues however.
This is in the SolanaSignature.tsx file.
import { useWallet } from '#solana/wallet-adapter-react';
import bs58 from 'bs58';
import React, { FC, useCallback } from 'react';
import ReactDOM from 'react-dom';
import { sign } from 'tweetnacl';
import AddUser from './mainstorage';
export const SignMessageButton : FC = () => {
const { publicKey, signMessage } = useWallet();
const onClick = useCallback(async () => {
try {
if (!publicKey) throw new Error('Wallet not connected!');
if (!signMessage) throw new Error('Wallet does not support message signing! Please use a wallet such as Phantom or Solflare! NOTE: Some Ledgers wallets are not supported!');
const message = new TextEncoder().encode('Omega Protocol - Signature verification for Bold Badgers.');
const signature = await signMessage(message);
if (!sign.detached.verify(message, signature, publicKey.toBytes())) throw new Error('Invalid signature!');
//alert(`Message signature: ${bs58.encode(signature)}`);
AddUser();
} catch (error: any) {
alert(`Signing failed: ${error?.message}`);
}
}, [publicKey, signMessage]);
return signMessage ? (<button className="wallet-adapter-button wallet-adapter-button-trigger shine" onClick={onClick} disabled={!publicKey}>Verify</button>) : null;
};
and then the mainstorage file:
import { useEffect } from 'react';
import { useEasybase } from 'easybase-react';
const AddUser = () => {
const { db } = useEasybase();
useEffect(() => {
db('OMEGABB').insert({ walletid: "test", discordid: "test", signature: "test", valid: false, lastvalid: new Date() }).one()
.then(() => console.log("Success!"));
}, [])
return (
{/* ... */}
);
}
export default AddUser;
What is happening however when I click the button is that it comes up with a warning: Hooks can only be called inside the body of a function component.
This does work in the initial index file (aka the parent file) but does not work here. Right now this is only a dummy/test but trying to get it writing to the database.
Thanks!
As per React's documentation:
Don’t call Hooks inside loops, conditions, or nested functions. Instead, always use Hooks at the top level of your React function, before any early returns.
Currently, you're attempting to call a hook inside the onClick handler - AddUser is a custom hook since it also uses hooks and the better name for it should be useAddUser.
I suggest to make some improvements by returning a function from your custom hook that you can call to add a new user, e.g.:
export const useAddUser = () => {
const {db} = useEasybase()
const addUser = React.useCallback(() => {
db('OMEGABB')
.insert(/*...*/)
.then(/*...*/)
.catch(/*...*/)
}, [db])
return {
addUser,
/*...*/
}
}
Then, you can use useAddUser in the following way:
const {useAddUser} from './mainstorage'
const SignMessageButton: FC = () => {
const {publicKey, signMessage} = useWallet()
const {addUser} = useAddUser();
const onClick = React.useCallback(
async () => {
try {
// ...
addUser()
} catch (error) {/*...*/}
},
[publicKey, signMessage, addUser]
)
/*...*/
}

Resources