TypeError: Class extends value undefined is not a constructor or null Custom environment NODE - node.js

I'm writing a custom environment for e2e testing with Prisma, and first I encountered an error with NODE's promisify that apparently can be resolved with opitional[?]. Now it's giving an error with NodeEnvironment. Can someone help me?
My Custom test environment
import type { Config } from '#jest/types';
import { exec } from 'node:child_process';
import * as dotenv from 'dotenv';
import NodeEnvironment from 'jest-environment-node';
import { Client } from 'pg';
import util from 'node:util';
import crypto from 'node:crypto';
dotenv.config({ path: __dirname + '/.env.test' });
const execSync = util?.promisify(exec);
const prismaBinary = './node_modules/.bin/prisma';
export default class PrismaTestEnvironment extends NodeEnvironment {
private schema: string;
private connectionString: string;
constructor(config: Config.ProjectConfig) {
super(config);
const dbUser = process.env.DATABASE_USER;
const dbPass = process.env.DATABASE_PASS;
const dbHost = process.env.DATABASE_HOST;
const dbPort = process.env.DATABASE_PORT;
const dbName = process.env.DATABASE_NAME;
this.schema = `test_${crypto.randomUUID()}`;
this.connectionString = `postgresql://${dbUser}:${dbPass}#${dbHost}:${dbPort}/${dbName}?schema=${this.schema}`;
}
async setup() {
process.env.DATABASE_URL = this.connectionString;
this.global.process.env.DATABASE_URL = this.connectionString;
await execSync(`${prismaBinary} migrate deploy`);
return super.setup();
}
async teardown() {
const client = new Client({
connectionString: this.connectionString,
});
await client.connect();
await client.query(`DROP SCHEMA IF EXISTS "${this.schema}" CASCADE`);
await client.end();
}
}
Error
enter image description here

Related

How to mock #google-cloud/kms using jest

I'm trying to write unit test cases for decrypt. I've my own implementation of decrypting an encrypted file. While trying to import the decrypt.mjs facing the following error.
Must use import to load ES Module: /node_modules/bignumber.js/bignumber.mjs
My application is a react frontend and NodeJS backend. I've used ES6 modules for NodeJS. Here is my decrypt.mjs file
import { readFile } from 'fs/promises';
import path from 'path';
import { KeyManagementServiceClient } from '#google-cloud/kms';
const decrypt = async (APP_MODE, __dirname) => {
if (APP_MODE === 'LOCALHOST') {
const keys = await readFile(
new URL(`./stagingfile.json`, import.meta.url)
).then((data) => JSON.parse(data));
return keys;
}
const { projectId, locationId, keyRingId, cryptoKeyId, fileName } =
getKMSDefaults(APP_MODE);
const ciphertext = await readFile(
path.join(__dirname, `/${fileName}`)
);
const formattedName = client.cryptoKeyPath(
projectId,
locationId,
keyRingId,
cryptoKeyId
);
const request = {
name: formattedName,
ciphertext,
};
const client = new KeyManagementServiceClient();
const [result] = await client.decrypt(request);
return JSON.parse(result.plaintext.toString('utf8'));
};
const getKMSDefaults = (APP_MODE) => {
//Based on APP_MODE the following object contains different values
return {
projectId: PROJECT_ID,
locationId: LOCATION_ID,
keyRingId: KEY_RING_ID,
cryptoKeyId: CRYPTO_KEY_ID,
fileName: FILE_NAME,
};
};
export default decrypt;
I tried to mock the #google-cloud/kms using manual mock (jest) but it didn't work. I tried multiple solutions to mock but nothing worked and it ended with the Must use import to load ES Module error.
I've had successfully used jest to mock #google-cloud/kms with TypeScript, so hopefully this will be the same process for ES modules that you can use.
Example working code:
// jest will "hoist" jest.mock to top of the file on its own anyway
jest.mock("#google-cloud/kms", () => {
return {
KeyManagementServiceClient: jest.fn().mockImplementation(() => {
return {
encrypt: kmsEncryptMock,
decrypt: kmsDecryptMock,
cryptoKeyPath: () => kmsKeyPath,
};
}),
};
});
// give names to mocked functions for easier access in tests
const kmsEncryptMock = jest.fn();
const kmsDecryptMock = jest.fn();
const kmsKeyPath = `project/location/keyring/keyname`;
// import of SUT must be after the variables used in jest.mock() are defined, not before.
import { encrypt } from "../../src/crypto/google-kms";
describe("Google KMS encryption service wrapper", () => {
const plaintext = "some text to encrypt";
const plaintextCrc32 = 1897295827;
it("sends the correct request to kms service and raise error on empty response", async () => {
// encrypt function is async that throws a "new Error(...)"
await expect(encrypt(plaintext)).rejects.toMatchObject({
message: "Encrypt: no response from KMS",
});
expect(kmsEncryptMock).toHaveBeenNthCalledWith(1, {
name: kmsKeyPath,
plaintext: Buffer.from(plaintext),
plaintextCrc32c: { value: plaintextCrc32 },
});
});
});

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>

When to load .env variables in NodeJS app?

I am coding a simple NodeJS Express REST API, using TypeScript. I have some environment variables that I load with dotenv.
I access my .env variables at two different stages in my code: index.ts, which is my start file, and in a MyControllerClass.ts file. To access these variables, the code is process.env.MY_ENV_VAR. To load them for the application, the code is dotenv.config().
As my index.ts file seems to be the root of my program (I configure my app in it), I use dotenv.config() to load my .env file for the rest of the program. However, in my MyControllerClass.ts file, in the constructor, if I do console.log(process.env.MY_ENV_VAR), I get "undefined". I could workaround this by adding a dotenv.config() in my constructor (it works) but it's nonsense to me to have it here.
How do I use dotenv.config() once and for all in my program, in a readable manner (like in an appropriate .ts file)? and more generally: what is a NodeJS Express loading cycle?
Here is a sample of the file structure of my code
src
├── index.ts
├── Authentication
│ └── authentication.router.ts
│ └── authentication.controller.ts
Here is the code of index.js
/**
* Required External Modules
*/
import * as dotenv from "dotenv";
import express from "express";
import cors from "cors";
import helmet from "helmet";
import { authenticationRouter } from "./authentication/authentication.router"
dotenv.config();
/**
* App Variables
*/
if(!process.env.PORT) {
process.exit(1);
}
const PORT: number = parseInt(process.env.PORT as string, 10);
const app = express();
/**
* App Configuration
*/
app.use(helmet());
app.use(cors());
app.use(express.json());
app.use(authenticationRouter);
app.use("api/authenticate/", authenticationRouter);
/**
* Server Activation
*/
app.listen(PORT, () => {
console.log(`Listening on port ${PORT}`);
});
Here is the code of authentication.router.ts
import express, { Request, Response } from "express";
import { AuthenticatorController } from "./authentication.controller";
export const authenticationRouter = express.Router();
const authenticatorController = AuthenticatorController.getInstance();
authenticationRouter.post("/api/authenticate", async (req: Request, res: Response) => {
try {
if (await authenticatorController.authenticate(req.body.login, req.body.password)) {
res.send({"status": "ok"})
} else
res.send({"status": "Error"})
} catch (e) {
console.debug(e)
res.send({"status": "500"});
}
});
Here is the code of authentication.controller.ts
import { ClientSecretCredential } from "#azure/identity";
import { SecretClient } from "#azure/keyvault-secrets";
import { Authenticator } from "./api/Authenticator";
import * as dotenv from "dotenv";
dotenv.config();
export class AuthenticatorController implements Authenticator {
private static singleInstance: AuthenticatorController | null = null;
private azureSecretCredential= new ClientSecretCredential(
process.env.AZURE_TENANT_ID as string,
process.env.AZURE_CLIENT_ID as string,
process.env.AZURE_CLIENT_SECRET as string);
private azureSecretClient = new SecretClient(
process.env.KEY_VAULT_URL as string,
this.azureSecretCredential);
private constructor () {}
public static getInstance(): AuthenticatorController {
if (this.singleInstance === null) {
this.singleInstance = new AuthenticatorController();
}
return this.singleInstance;
}
public async authenticate(login: string, password: string): Promise<Boolean> {
let isAuthenticated = false;
try {
const secret = await this.azureSecretClient.getSecret(login)
if (secret.name === login) {
if (secret.value === password) {
isAuthenticated = true;
}
}
} catch (e) {
console.debug(e);
}
return isAuthenticated;
}
}
You only call dotenv.config() once:
As early as possible in your application, require and configure
dotenv.
require('dotenv').config()
Therefore index.ts seems to be correct, process.env should then hold your parsed values. Maybe you can use something like this to make sure, data is parsed correctly:
const result = dotenv.config();
if (result.error) {
throw result.error;
}
console.log(result.parsed);
Edit:
You can try the following. I changed your exports a bit, because there is no need for a singleton within your controller.
authentication.router.ts:
// Imports (no dotenv; no dotenv.config())
// [...]
// Import controller
import { authenticatorController } from "./authentication.controller";
export const authenticationRouter = express.Router();
// Adding routes
// [...]
authentication.controller.ts:
// Imports (no dotenv; no dotenv.config())
// [...]
class AuthenticatorController implements Authenticator {
// [...]
}
export const authenticatorController = new AuthenticatorController();
index.ts:
// Imports (dotenv)
// [...]
const { error, parsed } = dotenv.config();
if (error) {
throw error;
}
console.log(parsed);
// [...]
app.use("api/authenticate/", authenticationRouter);
// [...]

How to stub nested dependecies with ts-sinon

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

Bookshelf circular dependency with ES5

I have the following code:
const bookshelf = require('../config/bookshelf');
const BaseModel = require('bookshelf-modelbase')(bookshelf);
const moment = require("moment");
const User = require("./User");
const Meta = require("./Meta");
const Log = require("./Log");
class Session extends BaseModel {
get tableName() {
return "sessions";
}
get hasTimestamps() {
return false;
}
user() {
return this.belongsTo(User);
}
meta() {
return this.belongsTo(Meta);
}
logs() {
return this.hasMany(Log);
}
};
module.exports = Session;
and
const bookshelf = require('../config/bookshelf');
const BaseModel = require('bookshelf-modelbase')(bookshelf);
const Session = require("./Session");
const moment = require("moment");
class Log extends BaseModel {
get tableName() {
return "logs";
}
get hasTimestamps() {
return false;
}
session() {
return this.belongsTo(Session);
}
getDate() {
return moment(this.get("date")).format("MMM DD, YYYY - HH:mm:ss")
}
};
module.exports = Log;
belongsTo relation works properly, but when I try hasMany, I get: "Unhandled rejection Error: A valid target model must be defined for the sessions hasMany relation" error.
I had a look at https://github.com/tgriesser/bookshelf/wiki/Plugin:-Model-Registry but it is being done using pre-ES5 syntax.
I guess I need to make sure "Log" class is available before I appoint into a hasMany relationship but stuck here.
Any ideas?
Edit: Doing
logs() {
var log = require("./Log");
return this.hasMany(log);
}
works but it looks bad.
You may use Bookshelf registry. It exactly fits your needs.
You can load Bookshelf registry plugin like this :
const knex = require('knex')({
client: 'pg',
connection: {
host: config.db_host,
user: config.db_user, // user name for your database
password: config.db_password, // user password
database: config.db_database, // database name
charset: 'utf8',
},
});
const bookshelf = require('bookshelf')(knex);
bookshelf.plugin('registry');
module.exports = {
knex,
bookshelf,
};
Then, register your models in module.exports like this :
module.exports = {
session: db.bookshelf.model('Session', Session),
log: db.bookshelf.model('Log', Log),
db,
};

Resources