electron-updater yields ERR_SSL_CLIENT_AUTH_CERT_NEEDED - node.js

Electron-Builder Version: 23.0.3
Node Version: v14.13.1
Electron Version: 18.2.3
Electron Type (current, beta, nightly): current
Target: nsis win x64
Hello, I'm trying do to a simple PoC on how we can update our electron app and I stumbled across this issue:
Error: Error: net::ERR_SSL_CLIENT_AUTH_CERT_NEEDED at SimpleURLLoaderWrapper.<anonymous> (node:electron/js2c/browser_init:101:7169) at SimpleURLLoaderWrapper.emit (node:events:390:28) 11:18:44.437 > Error: net::ERR_SSL_CLIENT_AUTH_CERT_NEEDED at SimpleURLLoaderWrapper.<anonymous> (node:electron/js2c/browser_init:101:7169) at SimpleURLLoaderWrapper.emit (node:events:390:28) 11:18:44.439 > Error: Error: net::ERR_SSL_CLIENT_AUTH_CERT_NEEDED at SimpleURLLoaderWrapper.<anonymous> (node:electron/js2c/browser_init:101:7169) at SimpleURLLoaderWrapper.emit (node:events:390:28) status: 'error', error: 'Error: net::ERR_SSL_CLIENT_AUTH_CERT_NEEDED\n' + ' at SimpleURLLoaderWrapper.<anonymous> (node:electron/js2c/browser_init:101:7169)\n' + ' at SimpleURLLoaderWrapper.emit (node:events:390:28)' } (node:13428) UnhandledPromiseRejectionWarning: Error: net::ERR_SSL_CLIENT_AUTH_CERT_NEEDED at SimpleURLLoaderWrapper.<anonymous> (node:electron/js2c/browser_init:101:7169) at SimpleURLLoaderWrapper.emit (node:events:390:28)
My build config looks like this:
"build": { "appId": "myapptestid", "files": [ "app/**/*", "node_modules/**/*", "package.json" ], "publish": [ { "provider": "generic", "url": "my_artifactory_url" } ], "directories": { "buildResources": "resources" }, "win": { "target": "nsis" }, "nsis": { "oneClick": false } },
And my main looks like this:
import url from "url";
import { app, Menu, ipcMain, shell } from "electron";
import appMenuTemplate from "./menu/app_menu_template";
import editMenuTemplate from "./menu/edit_menu_template";
import devMenuTemplate from "./menu/dev_menu_template";
import createWindow from "./helpers/window";
const isDev = require("electron-is-dev");
const log = require("electron-log");
log.transports.file.resolvePath = () => path.join("./logs.log");
import env from "env";
import { NsisUpdater } from "electron-updater";
const options = {
provider: "generic",
url: "my_artifactory_url",
};
const autoUpdater = new NsisUpdater(options);
autoUpdater.logger = require("electron-log");
autoUpdater.logger.transports.file.level = "info";
autoUpdater.addAuthHeader(Basic ${base64PlainData});
autoUpdater.on("error", (error) => {
log.info(error);
log.info(
"Error: ",
error == null ? "unknown" : (error.stack || error).toString()
);
});
autoUpdater.on("checking-for-update", () => {
log.info({ status: "checking" });
});
autoUpdater.on("update-available", () => {
log.info({ status: "update-available" });
});
autoUpdater.on("update-not-available", () => {
log.info({ status: "current" });
});
autoUpdater.on("error", (err) => {
log.info({ status: "error", error: err });
});
autoUpdater.on("download-progress", (progressObj) => {
log.info(progressObj);
log.info({
status: "downloading",
download: {
bytesPerSecond: progressObj.bytesPerSecond,
percent: progressObj.percent,
transferred: progressObj.transferred,
total: progressObj.total,
},
});
});
autoUpdater.on("update-downloaded", () => {
log.info({ status: "completed" });
});
if (env.name !== "production") {
const userDataPath = app.getPath("userData");
app.setPath("userData", ${userDataPath} (${env.name}));
}
const setApplicationMenu = () => {
const menus = [appMenuTemplate, editMenuTemplate];
if (env.name !== "production") {
menus.push(devMenuTemplate);
}
Menu.setApplicationMenu(Menu.buildFromTemplate(menus));
};
const initIpc = () => {
ipcMain.on("need-app-path", (event, arg) => {
event.reply("app-path", app.getAppPath());
});
ipcMain.on("open-external-link", (event, href) => {
shell.openExternal(href);
});
};
app.on("ready", () => {
setApplicationMenu();
initIpc();
const mainWindow = createWindow("main", {
width: 1000,
height: 600,
webPreferences: {
nodeIntegration: true,
contextIsolation: false,
enableRemoteModule: env.name === "test",
},
});
mainWindow.loadURL(
url.format({
pathname: path.join(__dirname, "app.html"),
protocol: "file:",
slashes: true,
})
);
// if (env.name === "development") {
mainWindow.openDevTools();
// }
if (isDev) {
log.info("dev");
autoUpdater.checkForUpdates();
} else {
log.info("prod");
autoUpdater.checkForUpdatesAndNotify();
}
setInterval(() => {
autoUpdater.checkForUpdatesAndNotify();
}, 1000 * 60 * 15);
});
app.on("window-all-closed", () => {
app.quit();
});
I'm trying to update through a generic repo from Jfrog Artifactory and that's why I need to use Auth header.
I've also tried to hook up to "select-client-certificate" and "certificate-error" from app, but none of this are triggered.
I'm doing the exactly same request in Postman and it works, no matter if the "SSL certificate verification" is on or off.
Can someone please give me a hint on this?

Related

StreamMessageReader is not a constructor in Electron + Monaco app

I'm trying to build an application using Electron with Monaco editor. I used monaco language client to integrate the editor with clangd. But somehow it didn't work... After manually connecting to the LSP server through web socket, I get the following error in the terminal:
TypeError: node.exports.StreamMessageReader is not a constructor
at createStreamConnection (/Volumes/Data/Develop/cpcode/dist/electron/main.js:3100:18)
at createProcessStreamConnection (/Volumes/Data/Develop/cpcode/dist/electron/main.js:3094:12)
at createServerProcess (/Volumes/Data/Develop/cpcode/dist/electron/main.js:3090:10)
at launch (/Volumes/Data/Develop/cpcode/dist/electron/main.js:3114:28)
at /Volumes/Data/Develop/cpcode/dist/electron/main.js:3160:13
at WebSocketServer.completeUpgrade (/Volumes/Data/Develop/cpcode/node_modules/ws/lib/websocket-server.js:431:5)
at WebSocketServer.handleUpgrade (/Volumes/Data/Develop/cpcode/node_modules/ws/lib/websocket-server.js:339:10)
at Server.<anonymous> (/Volumes/Data/Develop/cpcode/dist/electron/main.js:3147:13)
at Server.emit (node:events:526:28)
at onParserExecuteCommon (node:_http_server:727:14)
I am starting the server in Electron main process. Here's the code for it:
process.env.DIST = join(__dirname, '..')
process.env.PUBLIC = app.isPackaged ? process.env.DIST : join(process.env.DIST, '../public')
import { join } from 'path'
import { app, BrowserWindow } from 'electron'
import ws from "ws";
import http from "http";
import url from "url";
import net from "net";
import express from "express";
import * as rpc from "vscode-ws-jsonrpc";
import * as server from "vscode-ws-jsonrpc/server";
import * as lsp from "vscode-languageserver";
import { Message } from "vscode-languageserver";
export function launch(socket: rpc.IWebSocket) {
const reader = new rpc.WebSocketMessageReader(socket);
const writer = new rpc.WebSocketMessageWriter(socket);
// start the language server as an external process
const socketConnection = server.createConnection(reader, writer, () =>
socket.dispose()
);
const serverConnection = server.createServerProcess("CPP", "clangd");
if (serverConnection) {
server.forward(socketConnection, serverConnection, (message) => {
if (Message.isRequest(message)) {
console.log("server request!!!");
if (message.method === lsp.InitializeRequest.type.method) {
const initializeParams = message.params as lsp.InitializeParams;
initializeParams.processId = process.pid;
}
}
return message;
});
}
}
process.on("uncaughtException", function (err: any) {
console.error("Uncaught Exception: ", err.toString());
if (err.stack) {
console.error(err.stack);
}
});
export const startLSP = () => {
// create the express application
const app = express();
// server the static content, i.e. index.html
app.use(express.static(__dirname));
// start the server
const server = app.listen(3000);
// create the web socket
const wss = new ws.Server({
noServer: true,
perMessageDeflate: false
});
server.on(
"upgrade",
(request: http.IncomingMessage, socket: net.Socket, head: Buffer) => {
// eslint-disable-next-line n/no-deprecated-api
const pathname = request.url
? url.parse(request.url).pathname
: undefined;
if (pathname === "/lsp") {
wss.handleUpgrade(request, socket, head, (webSocket) => {
const socket: rpc.IWebSocket = {
send: (content) =>
webSocket.send(content, (error) => {
if (error) {
throw error;
}
}),
onMessage: (cb) => webSocket.on("message", cb),
onError: (cb) => webSocket.on("error", cb),
onClose: (cb) => webSocket.on("close", cb),
dispose: () => webSocket.close()
};
// launch the server when the web socket is opened
if (webSocket.readyState === webSocket.OPEN) {
launch(socket);
} else {
webSocket.on("open", () => launch(socket));
}
});
}
}
);
};
let win: BrowserWindow | null
// Here, you can also use other preload
const preload = join(__dirname, './preload.js')
// 🚧 Use ['ENV_NAME'] avoid vite:define plugin - Vite#2.x
const serverURL = process.env['VITE_DEV_SERVER_URL']
function createWindow() {
win = new BrowserWindow({
icon: join(process.env.PUBLIC, 'logo.svg'),
webPreferences: {
contextIsolation: false,
nodeIntegration: true,
preload,
},
titleBarStyle: process.platform === 'darwin' ? 'hiddenInset' : 'hidden',
})
startLSP();
// Test active push message to Renderer-process.
win.webContents.on('did-finish-load', () => {
win?.webContents.send('main-process-message', (new Date).toLocaleString())
})
if (app.isPackaged) {
win.loadFile(join(process.env.DIST, 'index.html'))
} else {
win.loadURL(serverURL)
}
}
app.on('window-all-closed', () => {
win = null
})
app.whenReady().then(createWindow)
I am using Vite as a bundler and I'm not sure if it has anything to do with this. Anyway, here's my vite.config.ts:
import fs from 'fs'
import path from 'path'
import { defineConfig } from 'vite'
import electron from 'vite-plugin-electron'
import renderer from 'vite-plugin-electron-renderer';
import { nodePolyfills } from 'vite-plugin-node-polyfills'
fs.rmSync('dist', { recursive: true, force: true }) // v14.14.0
export default defineConfig({
plugins: [
electron({
main: {
entry: 'electron/main.ts',
},
preload: {
input: {
// Must be use absolute path, this is the restrict of Rollup
preload: path.join(__dirname, 'electron/preload.ts'),
},
},
// Enables use of Node.js API in the Renderer-process
// https://github.com/electron-vite/vite-plugin-electron/tree/main/packages/electron-renderer#electron-renderervite-serve
renderer: {},
}),
nodePolyfills({
// Whether to polyfill `node:` protocol imports.
protocolImports: true,
}),
],
})
After Googling, I tried to install vscode-languageserver and vscode-languageserver-protocol, but no luck. The problem still continues.
Does anyone has a solution to this? Thanks in advance!

jest how to merge two xx.spec.ts to be one?

in order to cover all statements/branch/lines, I need to write two or more fn.spec.ts to test fn.ts, how can I merge fn.spec.ts and fn2.spec.ts to be one file ?
// fn.ts
export const getEnv = () => {
if (location.href.indexOf('localhost') !== -1 || /\d+\.\d+\.\d+\.\d+/.test(location.href)) {
return 'Test'
}
return 'Product'
}
// fn.spec.ts
describe('fn getEnv',()=>{
Object.defineProperty(window, 'location', {
value: {
href: 'http://192.168.2.3:9001'
},
})
const { getEnv } = require('./fn')
test('getEnv',()=>{
expect(getEnv()).toBe('Test')
})
})
// fn2.spec.ts
describe('fn getEnv',()=>{
Object.defineProperty(window, 'location', {
value: {
href: 'https://xx.com'
},
})
const { getEnv } = require('./fn')
test('getEnv',()=>{
expect(getEnv()).toBe('Product')
})
})
// jest.config.js
module.exports = {
testEnvironment: 'jest-environment-jsdom', // browser environment
}
Just merge it into one file with a clear expectation message.
import { getEnv } from './fn';
describe('fn', () => {
describe('getEnv', () => {
test('should return "Test" when window location is an IP address', () => {
Object.defineProperty(window, 'location', {
value: {
href: 'http://192.168.2.3:9001'
},
});
const actual = getEnv();
expect(actual).toBe('Test');
});
test('should return "Product" when window location is a domain', () => {
Object.defineProperty(window, 'location', {
value: {
href: 'https://xx.com'
},
});
const actual = getEnv();
expect(actual).toBe('Product');
})
});
});

Supertest return a different body

I have an e2e test where I test the registration (email unique)
The Test is:
it('Register a default user: /api/v1/auth/email/register (POST)', async () => {
return request(app.getHttpServer())
.post('/auth/email/register')
.send({
"name": newUserName,
"username": newUsername,
"email": TESTER_EMAIL,
"password": TESTER_PASSWORD
})
.expect(201);
});
The second time, with the same values, I expect a 400 Status code, and I got it.
it('Register a default user: /api/v1/auth/email/register (POST)', async () => {
return request(app.getHttpServer())
.post('/auth/email/register')
.send({
"name": newUserName,
"username": newUsername,
"email": TESTER_EMAIL,
"password": TESTER_PASSWORD
})
.expect(400)
.expect(({ body }) => {
console.log(body);
});
});
If I analyze the Body, I can see:
{
index: 0,
code: 11000,
keyPattern: { email: 1 },
keyValue: { email: 'john.doe#example.com' }
}
and it is correct, Because I have an index unique on my mongoDB.
But I expect the same response that I receive from my production API.
{
"statusCode": 400,
"message": [
"username already exist",
"email already exist"
],
"error": "Bad Request"
}
The controller is simple, I have a route like:
#Post('email/register')
#HttpCode(HttpStatus.CREATED)
async register(#Body() authRegisterLoginDto: AuthRegisterLoginDto) {
return this.authService.register(authRegisterLoginDto);
}
In my service:
async register(authRegisterLoginDto: AuthRegisterLoginDto) {
const hash = crypto.createHash('sha256').update(randomStringGenerator()).digest('hex');
const user = await this.usersService.create({
...authRegisterLoginDto,
hash,
});
await this.mailService.userSignUp({
to: user.email,
data: {
hash,
},
});
}
and in my userService(wehre I get the error) is:
async create(userDto: UserDto): Promise<IUsers> {
try {
return await this.userModel.create(userDto);
} catch (err) {
throw new HttpException(err, HttpStatus.BAD_REQUEST);
}
}
How can I get the same response that I get from my "prod" API?
UPDATE.
main.ts
import { NestFactory } from '#nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe, VersioningType } from '#nestjs/common';
import { DocumentBuilder, SwaggerModule } from '#nestjs/swagger';
import { TransformationInterceptor } from './interceptors/transformInterceptor';
import { TransformError } from './interceptors/transformErrorInterceptor';
import { useContainer } from 'class-validator';
import { ConfigService } from '#nestjs/config';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const configService = app.get(ConfigService);
//added for custom validator
useContainer(app.select(AppModule), {fallbackOnErrors: true});
//custom response
app.useGlobalInterceptors(new TransformationInterceptor)
app.useGlobalInterceptors(new TransformError)
app.setGlobalPrefix(configService.get('app.apiPrefix'), {
exclude: ['/'],
});
app.enableVersioning({
type: VersioningType.URI,
});
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
transform: true,
forbidNonWhitelisted: true,
transformOptions: {
enableImplicitConversion: true,
},
}),
);
const config = new DocumentBuilder()
.setTitle('API')
.setDescription('The API description')
.setVersion('1.0')
.addBearerAuth(
{
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT',
name: 'JWT',
description: 'Enter JWT token',
in: 'header',
},
'JWT-auth', // This name here is important for matching up with #ApiBearerAuth() in your controller!
)
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api/doc', app, document);
app.enableCors();
await app.listen(configService.get('app.port'));
}
bootstrap();
jest-e2e.json
{
"moduleFileExtensions": ["js", "json", "ts"],
"rootDir": ".",
"testEnvironment": "node",
"testRegex": ".e2e-spec.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
}
}
I don't see how your E2E test bootstraps the app but make sure all transformation pipes are included and everything else that might be involved altering error response.
To get the same effect in the e2e test always include the setup you have in main.ts except swagger docs or some unrelated stuff.
in your case, I'd try this
let app: INestApplication;
beforeEach(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
app.useGlobalInterceptors(new TransformationInterceptor);
app.useGlobalInterceptors(new TransformError);
await app.init();
});
Thanks to #n1md7, I imported
useContainer(app.select(AppModule), { fallbackOnErrors: true });
into my e2e test. I modified it because I want to use the
#Validate(UniqueValidator, ['username'], {
message: 'username already exist',
})
in my Dto. (MongoDB and class-validator unique validation - NESTJS)

Auth0 with Electron working in development but not when packaged

Im using Auth0 in an electron app to manage a log-in system. I referenced this tutorial here:
https://auth0.com/blog/securing-electron-applications-with-openid-connect-and-oauth-2/
to get started with using it.
Auth0 has been working great when I've been working in development but for some reason fails after I call "yarn package" to build the app. My electron app used electron-react-boilerplate (https://github.com/electron-react-boilerplate/electron-react-boilerplate).
Here are the important files:
// imports
...
import {
getAuthenticationURL,
refreshTokens,
loadTokens,
logout,
getLogOutUrl,
getProfile,
getResponse,
} from './services/authservice';
export default class AppUpdater {
constructor() {
...
}
}
let mainWindow: BrowserWindow | null = null;
if (process.env.NODE_ENV === 'production') {
const sourceMapSupport = require('source-map-support');
sourceMapSupport.install();
}
if (
process.env.NODE_ENV === 'development' ||
process.env.DEBUG_PROD === 'true'
) {
require('electron-debug')();
}
const installExtensions = async () => {
...
};
const createWindow = async () => {
console.log('now starting the main process');
if (
process.env.NODE_ENV === 'development' ||
process.env.DEBUG_PROD === 'true'
) {
await installExtensions();
}
const RESOURCES_PATH = app.isPackaged
? path.join(process.resourcesPath, 'assets')
: path.join(__dirname, '../assets');
const getAssetPath = (...paths: string[]): string => {
return path.join(RESOURCES_PATH, ...paths);
};
mainWindow = new BrowserWindow({
show: true,
width: 1024,
height: 728,
titleBarStyle: 'hidden', // add this line
frame: false,
//icon: getAssetPath('icon.png'),
webPreferences: {
nodeIntegration: true,
},
});
mainWindow.loadURL(`file://${__dirname}/index.html`);
const devtools = new BrowserWindow();
mainWindow.webContents.setDevToolsWebContents(devtools.webContents);
mainWindow.webContents.openDevTools({ mode: 'detach' });
};
/**
* Add event listeners...
*/
let win = null;
function createAuthWindow() {
destroyAuthWin();
win = new BrowserWindow({
width: 1000,
height: 600,
webPreferences: {
nodeIntegration: false,
enableRemoteModule: false,
},
});
console.log(getAuthenticationURL());
win.loadURL(getAuthenticationURL());
const {
session: { webRequest },
} = win.webContents;
const filter = {
urls: [
'file:///callback*',
],
};
webRequest.onBeforeRequest(filter, async ({ url }) => {
console.log(url);
await loadTokens(url)
.then((res) => {
console.log(res);
})
.catch(console.log);
console.log('from web request');
createWindow();
return destroyAuthWin();
});
win.on('authenticated', () => {
console.log('WE HAVE AUTHENTICATED');
destroyAuthWin();
});
win.on('closed', () => {
win = null;
});
}
function destroyAuthWin() {
if (!win) return;
win.close();
win = null;
}
// logout logic: removed for simplicity
ipcMain.on('profileRequest', (event, arg) => {
//event.reply('profileResponse', getProfile());
event.returnValue = getProfile();
});
const showWindow = async () => {
try {
await refreshTokens();
return createWindow();
} catch (err) {
createAuthWindow();
}
};
this is my auth services file:
let accessToken = null;
let profile = {};
let refreshToken = null;
export function getAccessToken() {
return accessToken;
}
export function getProfile() {
return profile;
}
export function getAuthenticationURL() {
return (
'https://' +
auth0Domain +
'/authorize?' +
'scope=openid%20profile%20offline_access&' +
'response_type=code&' +
'client_id=' +
clientId +
'&' +
'redirect_uri=' +
redirectUri
);
}
export async function refreshTokens() {
const refreshToken = await keytar.getPassword(keytarService, keytarAccount);
if (refreshToken) {
const refreshOptions = {
method: 'POST',
url: `https://${auth0Domain}/oauth/token`,
headers: { 'content-type': 'application/json' },
data: {
grant_type: 'refresh_token',
client_id: clientId,
refresh_token: refreshToken,
},
};
try {
const response = await axios(refreshOptions);
res = response;
accessToken = response.data.access_token;
profile = jwtDecode(response.data.id_token);
} catch (error) {
await logout();
throw error;
}
} else {
throw new Error('No available refresh token.');
}
}
export async function loadTokens(callbackURL) {
console.log('loading tokens:');
console.log(callbackURL);
res = callbackURL;
const urlParts = url.parse(callbackURL, true);
const query = urlParts.query;
console.log(query);
const exchangeOptions = {
grant_type: 'authorization_code',
client_id: clientId,
code: query.code,
redirect_uri: redirectUri,
};
const options = {
method: 'POST',
url: `https://${auth0Domain}/oauth/token`,
headers: {
'content-type': 'application/json',
},
data: JSON.stringify(exchangeOptions),
};
try {
const response = await axios(options);
console.log('from token:');
console.log(response);
res = response;
accessToken = response.data.access_token;
profile = jwtDecode(response.data.id_token);
refreshToken = response.data.refresh_token;
console.log(getProfile());
if (refreshToken) {
await keytar.setPassword(keytarService, keytarAccount, refreshToken);
}
} catch (error) {
await logout();
throw error;
}
}
I have a file in my components folder called "Auth.jsx" which has a "get profile" methods which interacts with the main process to get the profile
const getProfile = () => {
return ipcRenderer.sendSync('profileRequest', true);
};
After I package the electron app, the getProfile method always returns null/undefined.
Here are the auth0 logs:
Auth0 Logs
It shows that there is a successful Login and Exchange.
Finally, here's my webpack file: "webpack.config.main.prod.babel"
/**
* Webpack config for production electron main process
*/
import path from 'path';
import webpack from 'webpack';
import { merge } from 'webpack-merge';
import TerserPlugin from 'terser-webpack-plugin';
import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer';
import baseConfig from './webpack.config.base';
import CheckNodeEnv from '../scripts/CheckNodeEnv';
import DeleteSourceMaps from '../scripts/DeleteSourceMaps';
import dotenv from 'dotenv';
CheckNodeEnv('production');
DeleteSourceMaps();
const devtoolsConfig =
process.env.DEBUG_PROD === 'true'
? {
devtool: 'source-map',
}
: {};
export default merge(baseConfig, {
...devtoolsConfig,
mode: 'production',
target: 'electron-main',
entry: './src/main.dev.ts',
output: {
path: path.join(__dirname, '../../'),
filename: './src/main.prod.js',
},
optimization: {
minimizer: [
new TerserPlugin({
parallel: true,
}),
],
},
plugins: [
new BundleAnalyzerPlugin({
analyzerMode:
process.env.OPEN_ANALYZER === 'true' ? 'server' : 'disabled',
openAnalyzer: process.env.OPEN_ANALYZER === 'true',
}),
new webpack.EnvironmentPlugin({
NODE_ENV: 'production',
DEBUG_PROD: true,
START_MINIMIZED: false,
/*
other environment variables, including auth0 domain name and clientID
*/
}),
],
/**
* Disables webpack processing of __dirname and __filename.
* If you run the bundle in node.js it falls back to these values of node.js.
* https://github.com/webpack/webpack/issues/2010
*/
node: {
__dirname: false,
__filename: false,
},
});
Im suspecting the problem might have something to do with webpack since it's only the packaged version of the application that doesn't work properly. Im not sure exactly what the problem is, whether is a problem in the code or if I need to specifically change something within my Auth0 dashboard. If you have any suggestions or any ideas on how to debug let me know!
I had the exact same issue! I fixed it by changing the import method for jwt-decode from require to import
import jwtDecode from 'jwt-decode'

How to mock AWS TimestreamWrite by jest

This project is to record data by AWS Timestream, and it works well.
However, I'm failed to mock AWS TimestreamWrite by using jest. I tried some ways but not working. Can someone help me?
My files as below:
ledger-service.js
const AWS = require("aws-sdk");
const enums = require("./enums");
var https = require("https");
var agent = new https.Agent({
maxSockets: 5000,
});
const tsClient = new AWS.TimestreamWrite({
maxRetries: 10,
httpOptions: {
timeout: 20000,
agent: agent,
},
});
module.exports = {
log: async function (audit) {
try {
if (Object.keys(audit).length !== 0) {
if (!isPresent(audit, "name")) {
throw new Error("Name shouldn't be empty");
}
if (!isPresent(audit, "value")) {
throw new Error("Value shouldn't be empty");
}
return await writeRecords(recordParams(audit));
} else {
throw new Error("Audit object is empty");
}
} catch (e) {
throw new Error(e);
}
},
};
function isPresent(obj, key) {
return obj[key] != undefined && obj[key] != null && obj[key] != "";
}
function recordParams(audit) {
const currentTime = Date.now().toString(); // Unix time in milliseconds
const dimensions = [
// { Name: "client", Value: audit["clientId"] },
{ Name: "user", Value: audit["userId"] },
{ Name: "entity", Value: audit["entity"] },
{ Name: "action", Value: audit["action"] },
{ Name: "info", Value: audit["info"] },
];
return {
Dimensions: dimensions,
MeasureName: audit["name"],
MeasureValue: audit["value"],
MeasureValueType: "VARCHAR",
Time: currentTime.toString(),
};
}
function writeRecords(records) {
try {
const params = {
DatabaseName: enums.AUDIT_DB,
TableName: enums.AUDIT_TABLE,
Records: [records],
};
return tsClient.writeRecords(params).promise();
} catch (e) {
throw new Error(e);
}
}
ledger-service.spec.js
const AWS = require("aws-sdk");
const audit = require("./ledger-service");
describe("ledger-service", () => {
beforeEach(async () => {
jest.resetModules();
});
afterEach(async () => {
jest.resetAllMocks();
});
it("It should write records when all success", async () => {
const mockAudit={
name: 'testName',
value: 'testValue',
userId: 'testUserId',
entity: 'testEntity',
action: 'testAction',
info: 'testInfo',
};
const mockWriteRecords = jest.fn(() =>{
console.log('mock success')
return { promise: ()=> Promise.resolve()}
});
const mockTsClient={
writeRecords: mockWriteRecords
}
jest.spyOn(AWS,'TimestreamWrite');
AWS.TimestreamWrite.mockImplementation(()=>mockTsClient);
//a=new AWS.TimestreamWrite();
//a.writeRecords(); //these two lines will pass the test and print "mock success"
await audit.log(mockAudit); //this line will show "ConfigError: Missing region in config"
expect(mockWriteRecords).toHaveBeenCalled();
});
});
I just think the the AWS I mocked doesn't pass into the ledger-service.js. Is there a way to fix that?
Thanks
updates: Taking hoangdv's suggestion
I am thinking jest.resetModules(); jest.resetAllMocks(); don't work. If I put the "It should write records when all success" as the first test, it will pass the test. However, it will fail if there is one before it.
Pass
it("It should write records when all success", async () => {
const mockAudit = {
name: 'testName',
value: 'testValue',
userId: 'testUserId',
entity: 'testEntity',
action: 'testAction',
info: 'testInfo',
};
await audit.log(mockAudit);
expect(AWS.TimestreamWrite).toHaveBeenCalledWith({
maxRetries: 10,
httpOptions: {
timeout: 20000,
agent: expect.any(Object),
},
});
expect(mockWriteRecords).toHaveBeenCalled();
});
it("It should throw error when audit is empty", async () => {
const mockAudit = {};
await expect(audit.log(mockAudit)).rejects.toThrow(`Audit object is empty`);
});
Failed
it("It should throw error when audit is empty", async () => {
const mockAudit = {};
await expect(audit.log(mockAudit)).rejects.toThrow(`Audit object is empty`);
});
it("It should write records when all success", async () => {
const mockAudit = {
name: 'testName',
value: 'testValue',
userId: 'testUserId',
entity: 'testEntity',
action: 'testAction',
info: 'testInfo',
};
await audit.log(mockAudit);
expect(AWS.TimestreamWrite).toHaveBeenCalledWith({
maxRetries: 10,
httpOptions: {
timeout: 20000,
agent: expect.any(Object),
},
});
expect(mockWriteRecords).toHaveBeenCalled();
});
In ledger-service.js you call new AWS.TimestreamWrite "before" module.exports, this means it will be called with actual logic instead of mock.
The solution is just mock AWS before you call require("./ledger-service");
ledger-service.spec.js
const AWS = require("aws-sdk");
describe("ledger-service", () => {
let audit;
let mockWriteRecords;
beforeEach(() => {
mockWriteRecords = jest.fn(() => {
return { promise: () => Promise.resolve() }
});
jest.spyOn(AWS, 'TimestreamWrite');
AWS.TimestreamWrite.mockImplementation(() => ({
writeRecords: mockWriteRecords
}));
audit = require("./ledger-service"); // this line
});
afterEach(() => {
jest.resetModules(); // reset module to update change for each require call
jest.resetAllMocks();
});
it("It should write records when all success", async () => {
const mockAudit = {
name: 'testName',
value: 'testValue',
userId: 'testUserId',
entity: 'testEntity',
action: 'testAction',
info: 'testInfo',
};
await audit.log(mockAudit);
expect(AWS.TimestreamWrite).toHaveBeenCalledWith({
maxRetries: 10,
httpOptions: {
timeout: 20000,
agent: expect.any(Object),
},
});
expect(mockWriteRecords).toHaveBeenCalled();
});
});

Resources