My application has an auth micro frontend which creates a singleton instance for sharing the auth state with all the micro frontends in the application.
The auth class exposes some methods like so
{
getUser: () => {...}
isAuthenticated: () => true
...
}
In the app I load in the MF, get the instance, and call the methods I need.
import { Auth } from 'auth/services/Auth';
const auth = Auth.getInstance();
const component = () => {
const isAuthenticated = auth.isAuthenticated();
...
}
I use webpack module federation to load in the MF at run time.
plugins: [
new ModuleFederationPlugin({
name: 'mf',
remotes: {
auth: 'auth#https://localhost:3099/remoteEntry.js',
...
},
}),
],
Is there a way I can mock the auth instance in Cypress? I would like to call the methods the auth instance exposes with mock data.
Not to advertise some tool, but for this I would use Mockoon to record and mock the authentication part.
One way of mocking the internals of an app is to expose a reference for the test to modify.
import { Auth } from 'auth/services/Auth';
const auth = Auth.getInstance();
if (window.Cypress) {
window.auth
}
const component = () => {
const isAuthenticated = auth.isAuthenticated();
...
}
test
cy.visit('/', {
onBeforeLoad: (win) => {
cy.stub(win.auth, 'isAuthenticated').returns(true)
},
})
Related
I'm playing around with Microservice architecture using NestJs. I've made a simplified repository with a few services that communicate over TCP with a mix of message and event patterns.
I have moved on to writing E2E tests for the using Supertest, and while I'm able to run the needed microservice, the requests respond with {"error": "There is no matching message handler defined in the remote service.", "statusCode": 500}
GatewayService: HTTP Rest Api where the E2E tests are run. Calls the service
AuthService: NestJs microservice running on 0.0.0.0:3001 by default
configService: a simple service that returns information needed to set up the services, like host and port. I have tried eliminating it from the test and hardcoding the values.
The E2E test file
import { INestApplication, ValidationPipe } from '#nestjs/common';
import { ClientProxy, ClientsModule, Transport } from '#nestjs/microservices';
import { Test, TestingModule } from '#nestjs/testing';
import * as request from 'supertest';
import { configService } from '../src/config.service';
import { RpcExceptionFilter } from '../src/filters/rpc-exception.filter';
import { AppModule } from './../src/app.module';
describe('AuthenticationController (e2e)', () => {
let app: INestApplication;
let authClient: ClientProxy;
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [
AppModule,
ClientsModule.register([
{
...configService.getServiceConfigs().authService,
transport: Transport.TCP,
},
]),
],
}).compile();
// Setup the app instance
app = moduleFixture.createNestApplication();
// Setup the relevant micorservice(s)
app.connectMicroservice({
transport: Transport.TCP,
name: configService.getServiceConfigs().authService.name,
options: configService.getServiceConfigs().authService.options,
});
app.startAllMicroservices();
// Add request validation
app.useGlobalPipes(
new ValidationPipe({
transform: true,
whitelist: true,
forbidNonWhitelisted: true,
forbidUnknownValues: true,
}),
);
// Add needed filters
app.useGlobalFilters(new RpcExceptionFilter());
await app.init();
authClient = app.get(configService.getServiceConfigs().authService.name);
await authClient.connect();
console.log('authClient', authClient);
});
describe('POST /auth/login', () => {
it('Should return status 200 and a user object with access token', () => {
return (
request(app.getHttpServer())
.post('/auth/login')
.send({ username: 'exmple#user.com', password: 'password' })
// .expect(200)
.expect((response) => {
console.log('response', response.body);
expect(response.body).toHaveProperty('id');
expect(response.body).toHaveProperty('username');
expect(response.body).toHaveProperty('accessToken');
})
);
});
});
afterAll(async () => {
await app.close();
await authClient.close();
});
});
I have attempted adding a provider which I've used before when working with Grpc as the transport layer (this is TCP). Didn't change anything.
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
...
providers: [
{
provide: 'AUTH_SERVICE',
useFactory: () => {
return ClientProxyFactory.create({
transport: Transport.TCP,
options: { host: 'localhost', port: 3001 },
});
},
},
],
I know that the microservice starts up and the gateway service is able to connect to it since when printing the authClient: Client proxy it returns a correct object with URL 0.0.0.0:3001. If I change the URL, or the name of the service in any part of the setup then errors about missing providers show, further confirming that it is supposedly correctly set up.
One of the best guides I've found on this matter. Sadly it doesn't work for my code.
I am trying to unit test a function which makes an async call using an Axios helper instance. I have attempted multiple ways of trying to unit test this but I can not seem to find any material online which has helped. I've been stuck on this problem for a few days which is frustrating so any help would be appreciated! Below are the Axios Helper file (api.js)
api.js
import axios from 'axios'
const API = (token = null) => {
let headers = {
'Content-Type': 'application/json',
'Ocp-Apim-Subscription-key': process.env.NEXT_PUBLIC_API_HEADER_SUBSCRIPTION_KEY
}
if (token) {
const tokenHeader = { Authorization: 'Bearer ' + token }
headers = { ...headers, ...tokenHeader }
}
const url = process.env.NEXT_PUBLIC_API_BASE_URL
const API = axios.create({
baseURL: url,
headers
})
return API
}
export default API
mocked API
export default {
post: jest.fn(() =>
Promise.resolve({
data: {}
})
),
get: jest.fn(() =>
Promise.resolve({
data: {}
})
)
}
action file
export const initiate2FA = (destinationValue) => async () => {
const twoFactorAuth = destinationValue
const res = await API().post('/foo', {
Destination: twoFactorAuth
})
return res
}
Action.test.js
import API from 'api/api'
import { initiate2FA } from 'actions/userActions'
jest.mock('api/api')
const mockedAxios = API
const dispatch = jest.fn()
describe('Initiate2FA function', () => {
it('bar', async () => {
mockedAxios.get.mockImplementationOnce(() => Promise.resolve({ status: 200 }))
const t = await dispatch(initiate2FA('test#test.com'))
console.log(t)
})
})
My issue with the above test file is that it returns an anonymous function and I do not know how to handle this to pass the unit test. The goal of the test is to make sure the function is called. I am not sure if I am approaching this the correct way or should change my approach.
Again, any suggestions would be great!
Mocking an API call is something you can mock on your own React component, instead of a function, and the best option would be to not mock anything on your component. Here you can read all about why you should not mock your API functions. At the end of the article, you're going to find a library called Mock Service Worker which you can use for your purpose.
The way you declare you have an actual HTTP called that needs to be mocked would be something like this:
rest.get('/foo', async (req, res, ctx) => {
const mockedResponse = {bar: ''};
return res(ctx.json(mockedResponse))
}),
If you just need to unit test a function, you can still use Mock Service Worker to resolve the HTTP request, and then test what happens after that. This would still be your first choice. And the test would look like:
// this could be in another file or on top of your tests.
rest.get('/foo', async (req, res, ctx) => {
const mockedResponse = {bar: ''};
return res(ctx.json(mockedResponse))
}),
// and this would be your test
describe('Initiate2FA function', () => {
it('bar', async () => {
const res = await initiate2FA('test#test.com');
expect(res).toBe({bar: '');
})
})
I created acceptance test of controller that using memory datasource.
Before the test start, I was trying to have clean database and only add 1 user credential to login by calling endpoint since the rest of test require authenticated access with access token.
I can get the user info in getUser after created the user record from givenUser.
However, the login request can't find the credential. It seems that the helpers and client are not sharing the same memory datasource.
I am not sure what configuration setup is wrong in this case.
src/tests/fixtures/datasources/testdb.datasource.ts
import {juggler} from '#loopback/repository';
export const testdb: juggler.DataSource = new juggler.DataSource({
name: 'db',
connector: 'memory',
});
src/datasources/mongodb.datasource.ts
import {inject} from '#loopback/core';
import {juggler} from '#loopback/repository';
import * as config from './mongodb.datasource.json';
export class MongodbDataSource extends juggler.DataSource {
static dataSourceName = 'mongodb';
constructor(
#inject('datasources.config.mongodb', {optional: true})
dsConfig: object = config,
) {
super(dsConfig);
}
}
user.controller.acceptance.ts
import {Client, expect, supertest} from '#loopback/testlab';
import {ApiApplication} from '../..';
import {setupApplication} from './test-helper';
import {givenEmptyDatabase, givenUser,getUser} from '../helpers/database.helpers';
describe('UserController', () => {
let app: ApiApplication;
let client: supertest.SuperTest<supertest.Test>;;
let jwtToken: string;
before('setupApplication', async () => {
({app, client} = await setupApplication());
});
before(givenEmptyDatabase);
before(givenUser);
before(getUser);
before(async () => {
const response = await client
.post('/login')
.send({username: 'user1', password: 'password'});
jwtToken = response.body.token;
});
after(async () => {
await app.stop();
});
it('invokes GET /info without authentication', async () => {
const expectedError = {
error: {
statusCode: 401,
name: 'UnauthorizedError',
message: 'Unauthorized'
}
};
const res = await client.get('/user/info').expect(401);
expect(res.body).to.containEql(expectedError);
});
test-help.ts
import {ApiApplication} from '../..';
import {
createRestAppClient,
givenHttpServerConfig,
Client,
} from '#loopback/testlab';
import {testdb} from '../fixtures/datasources/testdb.datasource';
export async function setupApplication(): Promise<AppWithClient> {
const app = new ApiApplication();
await app.bind('datasources.config.mongodb').to({
name: 'mongodb',
connector: 'memory',
debug: true,
});
await app.boot();
await app.start();
const client = createRestAppClient(app);
return {app, client};
}
export interface AppWithClient {
app: ApiApplication;
client: Client;
}
database.helper.ts
import { UserRepository } from '../../repositories';
import { User } from '../../models';
import { testdb } from '../fixtures/datasources/testdb.datasource';
export async function givenEmptyDatabase() {
let userRepo: UserRepository;
userRepo = new UserRepository(testdb);
userRepo.deleteAll();
}
export async function givenUser() {
let userRepo = new UserRepository(testdb);
const user = {
username: 'user1',
password: 'password',
created_at: new Date('2019-08-08'),
updated_at: new Date('2019-08-08'),
}
await userRepo.create(operator);
}
export async function getUser() {
let userRepo = new UserRepository(testdb);
const users = await userRepo.find();
console.log(users);
}
It seems that the helpers and client are not sharing the same memory datasource.
Exactly.
It's important to realize that you can have multiple datasources using the same connector, for example you can one logDb using one MongoDB server and userDb connecting to another one. The same applies to memory datasources, you can have multiple datasources using the memory connector and each instance will have its own space for data.
Since you are already changing the configuration of your main mongodb datasource in the tests, my recommendation is to use that modified datasource when setting up the initial test data.
In database.helper.ts:
export async function givenEmptyDatabase(db) {
let userRepo: UserRepository;
userRepo = new UserRepository(db);
userRepo.deleteAll();
}
export async function givenUser(db) {
// ...
}
// and so on
In your test:
before(async () => {
const db = await app.get<juggler.DataSource>('datasources.config.mongodb');
await givenEmptyDatabase(db);
});
Personally, I strongly recommend use the same database in your tests as you will use in production. The memory database behaves differently from MongoDB. Certain operations may pass when storing data in memory but fail when MongoDB is used.
You can learn more about testing in LoopBack's "Best Practice" guides: Testing your application
With the normal graphql server we can define the context object like this:
app.use('/graphql', graphqlExpress(async (req) => {
return {
schema,
context: {
app,
params,
}
};
}));
** subscription server **
How can I do the same for the subscription server? (Doing the hybrid http / websocket approach). Can't seem to find a solution from the docs.
new SubscriptionServer({
execute,
subscribe,
schema,
onConnect: (connectionParams, webSocket) => {
console.log(connectionParams);
}
}, {
server,
path: '/subscriptions'
});
You can add a middleware before the execute function and add the required context before resolving the subscription.
It could look like this:
const middleware = (args) => new Promise((resolve, reject) => {
const [schema, document, root, context, variables, operation] = args;
context.app = <your app parameter>;
context.params = <your params>;
resolve(args);
});
SubscriptionServer.create({
schema,
subscribe,
execute: (...args) => middleware(args).then(args => { return execute(...args); }) },
{
server: httpServer,
path: "/subscription",
},
);
As you can see you have all the data from the request in the args of the execute function.
I'm new to apollo/graphql and I'm trying to get my authentication done properly in a greenfield project. My authentication provider is AWS cognito. I wrote a cognito helper module to interact with it.
Though I'm not quite sure how to sync my apollo client with my auth state.
export const authenticate = (username: string, password: string) => {
const authDetails = new AuthenticationDetails({
Username: username,
Password: password,
})
const cognitoUser = getCognitoUser(username)
return new Promise((resolve, reject) => {
cognitoUser.authenticateUser(authDetails, {
onSuccess: result => {
resolve(result)
},
onFailure: err => {
reject(err)
},
})
})
}
export const getCurrentUserToken = () => {
return new Promise((resolve, reject) => {
const currentUser = userPool.getCurrentUser()
if (currentUser) {
currentUser.getSession((error, session) => {
if (error) {
reject(error)
}
resolve(session.getIdToken().getJwtToken())
})
} else {
resolve(null)
}
})
}
export const logout = () => {
const currentUser = userPool.getCurrentUser()
if (currentUser) {
currentUser.signOut()
}
}
Right now I'm just using these function to handle my login by calling them in my react component handlers. I configured an apollo-link for adding the auth header. Inject my JWT token data into context at the backend and implemented a currentUser query resolver in the backend.
const resolvers = {
RootQuery: {
currentUser: (obj, args, context) =>
context.tokenData
? {
id: context.tokenData.sub,
name: context.tokenData.name,
email: context.tokenData.email,
username: context.tokenData['cognito:username'],
}
: null,
},
}
In my react App layout i got a component UserPanel which queries that currentUser query.
const CURRENT_USER_QUERY = gql`
query {
currentUser {
name
}
}
`
export default graphql(CURRENT_USER_QUERY)(UserPanel)
When i am logging in now obviously the UserPanel does not update its currentUser query except I'm reloading the page ofc. Though im also having troubles finding a good solution to sync them.
I was thinking about implementing my login via graphql mutation using apollo-link-state to do it locally and watch these to refetch if someone logged in/out. I'm not sure if this is fine since it seems to me that this link cannot resolve async stuff (e.g. promises) in its mutation resolvers.
Another option I was thinking about was to decouple the auth process from the apollo client completely and implement some auth pubsub system maybe with Observables and let the react components refetch the queries if the authentication state changes.
I'm very uncertain how to continue and every solution I'm thinking about doesn't feel like the recommended way to go.
I don't have the full picture with regards to your React setup but here I go. It might be that Apollo-client is caching CURRENT_USER_QUERY locally and is showing you the results of a previous query. You could try the network-only option on the query:
export default graphql(CURRENT_USER_QUERY, { options: {fetchPolicy: 'network-only' }})(UserPanel)
What I have in React is an AppContainer which is my parent component. It checks if the user is logged in:
const loggedInUser = gql`
query loggedInUser{
user {
id
role
}
}`
export default graphql(loggedInUser, { options: {fetchPolicy: 'network-only' }})(AppContainer)
Then on my UserProfile page, I use a data container to fetch the data before passing it down to the UserProfile child component. I think the loggedInUser query automatically updates the user in the apollo store. With it apollo-client realizes that it needs to refetch userQuery. Does that help?
const userQuery = gql`
query userQuery {
user {
id
name
email
role
company
}
}
`
export default graphql(userQuery, {name: 'userQuery'})(UserDataContainer);