ajax request doesn't execute when run test with marble diagrams - reactjs

I use rxjs v6 and redux-observable v1
I have epic that send request to server and try to test epic like in doc. When I run test epic before send request emit 3 actions and it see on test result, but when epic comes to ajax call test finish. To mock request I use nock lib.
Epic:
import { ofType } from 'redux-observable'
import { of, merge, from } from 'rxjs'
import {
switchMap,
catchError
} from 'rxjs/operators'
import { apiRequest, handleAsyncError$ } from '../../async/lib'
import { actions as asyncActions } from '../../async'
import { LOADING_TYPES } from '../../async/constants'
import { actions as authActions } from '../reducer'
const setSignInLoading = (status) => of(asyncActions.setLoading({ type: LOADING_TYPES.signIn, status }))
const emitSignInPending = () => merge(
setSignInLoading(true),
of(authActions.signInPending())
)
const emitSignInSuccess = (payload) => merge(
setSignInLoading(false),
of(authActions.signInSuccess(payload))
)
const emitSignInFailed = (payload) => merge(
setSignInLoading(false),
handleAsyncError$({
action: authActions.signInFailure,
payload
})
)
// --------- ajax call -----------
const startSignIn = (payload) => apiRequest({
path: '/auth/signin/manager',
method: 'post',
body: payload
})
const mapSignInAction$ = ({ payload }) => merge(
// --------- emit 3 actions -----------
emitSignInPending(),
// --------- finish test -----------
startSignIn(payload)
.pipe(
switchMap((emitSignInSuccess)),
catchError(emitSignInFailed)
)
)
const signInEpic = action$ =>
action$
.pipe(
ofType(authActions.signIn),
switchMap(mapSignInAction$)
)
export default signInEpic
apiRequest:
import { get } from 'lodash'
import { throwError } from 'rxjs'
import { ajax } from 'rxjs/ajax'
import { map, catchError } from 'rxjs/operators'
import { API } from '../../../../config'
const apiRequest = ({ token, path, method, body }) => {
const settings = {
url: `${API}${path}`,
headers: { 'Content-Type': 'application/json' },
responseType: 'json',
crossDomain: true,
method,
body
}
if (token) {
settings.headers['Authorization'] = `Bearer: ${token}`
}
return ajax(settings)
.pipe(
catchError((request) => {
const error = get(request, 'response.error')
return throwError({ error, request })
}),
map(({ response }) => response)
)
}
export default apiRequest
Test:
nock(API)
.post('/auth/signin/manager')
.reply(200, response)
scheduler.run(({ hot, expectObservable }) => {
const source = hot('-a|', { a: authActions.signIn({ email: 'manager', password: '123123' }) })
const output$ = epic(source)
expectObservable(output$).toBe('-(bcde)', {
b: asyncAction.setLoading({ type: 'signIn', status: true }),
c: authActions.signInPending(),
d: asyncAction.setLoading({ type: 'signIn', status: false }),
e: authActions.signInSuccess(response)
})
})
Result:
Expected:
[{"frame": 1, "notification": {"error": undefined, "hasValue": true, "kind": "N", "value": {"error": false, "payload": {"status": true, "type": "signIn"}, "type": "[8] async/setLoading"}}}, {"frame": 1, "notification": {"error": undefined, "hasValue": true, "kind": "N", "value": {"error": false, "payload": undefined, "type": "[3] [2] auth/signIn/pending"}}}, {"frame": 1, "notification": {"error": undefined, "hasValue": true, "kind": "N", "value": {"error": false, "payload": {"status": false, "type": "signIn"}, "type": "[8] async/setLoading"}}}, {"frame": 1, "notification": {"error": undefined, "hasValue": true, "kind": "N", "value": {"error": false, "payload": {"data": {"token": "skldjf", "user": {"email": "manager", "id": 2, "passwordHash": "asdf", "passwordSalt": "6819c23dc7", "role": {"name": "user"}, "roleId": 1}}}, "type": "[4] [2] auth/signIn/success"}}}]
Received:
[{"frame": 1, "notification": {"error": undefined, "hasValue": true, "kind": "N", "value": {"error": false, "payload": {"status": true, "type": "signIn"}, "type": "[8] async/setLoading"}}}, {"frame": 1, "notification": {"error": undefined, "hasValue": true, "kind": "N", "value": {"error": false, "payload": undefined, "type": "[3] [2] auth/signIn/pending"}}}]

Ajax resolves as microtask so epic doesn't emit it sync, so marble diagrams can't handle it, I can't find how to do it with marble diagrams. So simple solutions is:
it('return token and user 2', async (done) => {
const response = {...}
nock(API)
.post('/auth/signin/manager')
.reply(200, response)
const source = authActions.signIn({ email: 'manager', password: '123123' })
const action$ = ActionsObservable.of(source)
epic(action$).pipe(toArray()).subscribe((actions) => {
expect(actions).toEqual([
asyncAction.setLoading({ type: 'signIn', status: true }),
authActions.signInPending(),
asyncAction.setLoading({ type: 'signIn', status: false }),
authActions.signInSuccess(response)
])
done()
})
})
Please write if you found how do it with marble diagrams.

Related

next-auth custom auth window not defined

I am trying to use next-auth with my backend but it doesn't work. I use version 4 with typescript. The error is
{error: 'window is not defined', status: 200, ok: true, url: null}
Why?????. Thanks a lot.
My custom API /login result is
{
"data": {
"username": "test",
"users": {
"id": 2,
"username": "test",
"email": "test#test.com",
"createdAt": "2021-05-24",
"updatedAt": "2021-05-24",
"name": "John Smith",
"id_groups": 99,
"groups": "guest",
"avatar": null
},
"timestamp": 1646808511,
"jwt": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiG9.eyJpc3MiOiJodHRwOlwvXC90d2luYXBwLml0IiwiYXVkIjoiaHR0cDpcL1wvdHdpbmFwcC5pdCIsImlhdCI6MTM1Njk5OTUyNCwibmJmIjoxMzU3MDAwMDAwLCJleHAiOjE2NDY4MTIxMTEsImRhdGEiOiJtYXJjb2JvbmNpIn0.R1aAX99GHmoSPRKv4Vnzso8iRjUhrDWhPEdq4oql_r0"
},
"status": "",
"code": 200
}
Now, I'm try to configure next auth
import NextAuth from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
import gApi from "../../../api/gApi";
export default NextAuth({
session: {
strategy: "jwt",
},
providers: [
CredentialsProvider({
name: "credentials",
credentials: {
username: {label: "Username",type: "text", placeholder: "username"},
password: { label: "Passwort", type: "password" },
},
async authorize(credentials) {
const resp = await gApi.post("/login", JSON.stringify(credentials));
const user = resp.data;
console.log('CALL MY API');
console.log(resp);
if ( resp.status && user) {
return user;
}
return null;
},
}),
],
callbacks: {
async jwt({ token, user, account, isNewUser }) {
if (user) {
if (user.jwt) {
token = { accessToken: user.jwt };
}
}
return token;
},
async session({ session, token }) { // this token return above jwt()
session.accessToken = token.accessToken;
return session;
},
},
pages: {
signIn: "/auth/Login",
},
});
In my login page I have e simple form and i call with:
const onSubmit: SubmitHandler<FormData> = async data => {
const resp: any = await signIn("credentials", {
username: data.username,
password: data.password,
redirect: false,
});
console.log('RESPO signin');
console.log(resp);
if (resp && !resp.error) {
router.replace('/')
} else return;
}

Unit test Axios POST/PUT/DELETE with Jest on ReactJS/Typescript

I'm trying to add unit testing to my axios API calls
the .get call was easy to do... but when it comes to the POST/PUT/DELETE calls, I can't make it work.
here's what I tried (for POST)
my test:
const mockedAxios = axios as jest.Mocked<typeof axios>
...
test ('addGrocery()', async () => {
const newGrocery: IGrocery = {
_id: "1",
name: "Banana",
quantity: 2,
status: false,
}
const data = {
message: "Grocery added",
grocery: newGrocery,
groceries: [
{
_id: "1",
name: "Banana",
quantity: 2,
status: false,
},
{
_id: "2",
name: "Strawberry",
quantity: 1,
status: true,
}
]
}
mockedAxios.post.mockImplementationOnce(() => Promise.resolve(data))
await expect(addGrocery(newGrocery)).resolves.toEqual(data)
expect(axios.post).toHaveBeenCalled()
})
my API call:
export const addGrocery = async (formData: IGrocery): Promise<AxiosResponse<ApiDataType>> => {
try {
const grocery: Omit<IGrocery, "_id"> = {
name: formData.name,
quantity: formData.quantity,
status: false,
}
const apiUrl = baseUrl + '/add-grocery'
const saveGrocery: AxiosResponse<ApiDataType> = await axios.request({
method: 'POST',
url: apiUrl,
params: grocery
})
return saveGrocery
} catch (error: any) {
throw new Error(error)
}
}
and the controller:
const addGrocery = async (req: Request, res: Response): Promise<void> => {
try {
const query = req.query as unknown as Pick<IGrocery, "name" | "quantity" | "status">
const grocery: IGrocery = new Grocery({
name: query.name,
quantity: query.quantity,
status: query.status,
})
const newGrocery: IGrocery = await grocery.save()
const allGroceries: IGrocery[] = await Grocery.find()
res.status(201).json({
message: "Grocery added",
grocery: newGrocery,
groceries: allGroceries
})
} catch (error) {
throw error
}
}
I did a lot of research, pretty much everything I could find was only for the .get call, sometimes something just saying "change the mockeAxios.get to mockedAxios.post" (which is what I tried), but didn't work.
Getting the following error:
FAIL src/__tests__/API.test.tsx (16.851 s)
API tests
√ getGroceries() (6 ms)
× addGrocery() (5 ms)
● API tests › addGrocery()
expect(received).resolves.toEqual(expected) // deep equality
Expected: {"groceries": [{"_id": "1", "name": "Banana", "quantity": 2, "status": false}, {"_id": "2", "name": "Strawberry", "quantity": 1, "status": true}], "grocery": {"_id": "1", "name": "Banana", "quantity": 2, "status": false}, "message": "Grocery added"}
Received: undefined
59 | mockedAxios.post.mockImplementationOnce(() => Promise.resolve(data))
60 |
> 61 | await expect(addGrocery(newGrocery)).resolves.toEqual(data)
| ^
62 | expect(axios.post).toHaveBeenCalled()
63 | })
64 | })

React + fetch: adding extra characters and backslashes to my url

I have this code in React 17
useEffect(() => {
getLocalJson('../json/login/login.json', props.headers)
.then(resp => {
setFields(resp);
});
}, [props.headers]);
And the getLocalJson method is in a different file:
export const getLocalJson = async (url, headers) => {
console.log(url)
const resp = await fetch(url, {'headers': headers});
const json = await resp.json();
return json;
}
However the call to load the local JSON file from the public folder is:
Request URL: http://localhost:3000/json/login/%5Cdx%5Cjson%5Clogin%5C%5Cdx%5Cjson%5Clogin%5C%5Cdx%5Cjson%5Clogin%5C%5Cdx%5Cjson%5Clogin%5C%5Cdx%5Cjson%5Clogin%5C%5Cdx%5Cjson%5Clogin%5C%5Cdx%5Cjson%5Clogin%5C%5Cdx%5Cjson%5Clogin%5C%5Cdx%5Cjson%5Clogin%5C%5Cdx%5Cjson%5Clogin%5C%5Cdx%5Cjson%5Clogin%5C%5Cdx%5Cjson%5Clogin%5C%5Cdx%5Cjson%5Clogin%5C%5Cdx%5Cjson%5Clogin%5C%5Cdx%5Cjson%5Clogin%5C%5Cdx%5Cjson%5Clogin%5C%5Cdx%5Cjson%5Clogin%5C%5Cdx%5Cjson%5Clogin%5Clogin.json
Ths is the JSON
[
{
"order": 0,
"systemName": "title",
"friendlyName": "Login",
"dataType": {
"type": "TITLE"
}
},
{
"order": 1,
"required": true,
"systemName": "username",
"friendlyName": "Username",
"errorMsg": "Invalid username",
"dataType": {
"type": "TEXT"
}
},
{
"order": 2,
"required": true,
"systemName": "password",
"friendlyName": "Password",
"errorMsg": "Invalid password",
"dataType": {
"type": "PASSWORD"
}
},
{
"order": 3,
"systemName": "title",
"friendlyName": "Login",
"dataType": {
"type": "BUTTON",
"submit": true
}
}
]
And it makes the call over and over and over
This exact code works on my ubuntu dev box, but is failing as abovw on my windows box
I think there is some issue with the way you are passing down the headers, look into the documentation to have a better idea.
Put your function in the body of your component where you're using useEffect and wrap it with useCallback like this:
const getLocalJson = useCallback( async (url, headers) => {
console.log(url)
const resp = await fetch(url, {'headers': headers});
const json = await resp.json();
return json;
},[])

Axios send strange array to React

I geting the data back from my API in React from a post request and I get just the first object of the entire Array.prototype
My API for the upload:
router.post("/uploads", upload.any(), async (req, res) => {
try {
if (!req.files) {
res.send({
status: false,
message: "No file uploaded",
});
} else {
let data = req.files;
res.send({
status: true,
message: "Files are uploaded",
data: data,
});
}
} catch (error) {
res.status(500).send(err);
}
});
POSTMAN gives me back:
{
"status": true,
"message": "Files are uploaded",
"data": [
{
"fieldname": "uploads\n",
"originalname": "46335256.jpg",
"encoding": "7bit",
"mimetype": "image/jpeg",
"destination": "client/uploads/",
"filename": "46335256-2020-08-04.jpg",
"path": "client/uploads/46335256-2020-08-04.jpg",
"size": 19379
},
{
"fieldname": "uploads\n",
"originalname": "120360358.jpg",
"encoding": "7bit",
"mimetype": "image/jpeg",
"destination": "client/uploads/",
"filename": "120360358-2020-08-04.jpg",
"path": "client/uploads/120360358-2020-08-04.jpg",
"size": 78075
}
]
}
perfect!
this is my function in React to upload
const uploadFiles = () => {
uploadModalRef.current.style.display = "block"
uploadRef.current.innerHTML = "File(s) Uploading..."
for (let i = 0; i < validFiles.length; i++) {
const formData = new FormData()
formData.append("images", validFiles[i])
axios
.post("http://localhost:5000/api/db/uploads", formData, {
onUploadProgress: progressEvent => {
const uploadPercentage = Math.floor(
(progressEvent.loaded / progressEvent.total) * 100
)
...// code for graphic upload
},
})
.then(resp => {
console.log(resp.data.data)
resp.data.data.map(item => {
console.log(item)
})
})
.catch(() => {
... // code
}
}
and with this I get (from the console):
[{…}]
0:
destination: "client/uploads/"
encoding: "7bit"
fieldname: "images"
filename: "46335256-2020-08-04.jpg"
mimetype: "image/jpeg"
originalname: "46335256.jpg"
path: "client/uploads/46335256-2020-08-04.jpg"
size: 19379
__proto__: Object
length: 1
__proto__: Array(0)
is an array(if I map it works) but with just the first object.
How is it possible ??
I tried even with async/await but nothing changes
Where I'm mistaking?
Thanks!

Mongoose Update array in a document does not work as expected

I'm scratching my head since a couple day on how to update the content of an array with Mongoose.
Here is my schema to begin with:
const playedGameSchema = new Schema ({
created: Date,
updated: Date,
game: {
type: Schema.Types.ObjectId,
ref: 'game'
},
creator: {
id: {
type: Schema.Types.ObjectId,
ref: 'user'
},
score: Number
},
partners: [{
id: {
type: Schema.Types.ObjectId,
ref: 'user'
},
score: Number
}]
});
module.exports = mongoose.model('PlayedGame', playedGameSchema);
Basically, what I want to achieve is to, at the same time:
- Update the creator.score (successful with dot notation).
- Update the score key for each partner (unsuccessful).
Here is the result of a document created:
{
"creator": {
"id": "5b8544fa11235d9f02a9b4f1",
"score": 0
},
"_id": "5bb6375f5f68cc5c52bc93ae",
"game": "5b45080bb1806be939bfde03",
"partners": [
{
"_id": "5bb637605f68cc5cafbc93b0",
"id": "5b85497111235d677ba9b4f2",
"score": 0
},
{
"_id": "5bb637605f68ccc70ebc93af",
"id": "5b85497111235d677ba9b4f2",
"score": 0
}
],
"created": "2018-10-04T15:53:03.386Z",
"updated": "2018-10-04T15:53:03.386Z",
"__v": 0
}
As I said, I was able to change the score of the score creator by passing something like { "creator.score": 500 } as a second parameter, then I switch to trying to update the array.
Here is my lambda function to update the score for each partner:
export const update: Handler = (event: APIGatewayEvent, context: Context, cb: Callback) => {
context.callbackWaitsForEmptyEventLoop = false;
const body = JSON.parse(event.body);
let partnersScore: object = {};
if(body.update.partners) {
body.update.partners.forEach((score, index) => {
const key = `partners.${index}.$.score`;
partnersScore = Object.assign(partnersScore, { [key]: score});
console.log(partnersScore);
});
}
connectToDatabase().then(() => {
console.log('connected', partnersScore)
PlayedGame.findByIdAndUpdate(body.id, { $set: { partners: partnersScore } },{ new: true})
.then(game => cb(null, {
statusCode: 200,
headers: defaultResponseHeader,
body: JSON.stringify(game)
}))
.catch(err => {
cb(null, {
statusCode: err.statusCode || 500,
headers: { 'Content-Type': 'text/plain' },
body: err
})});
});
}
Which passes a nice { 'partners.0.$.score': 500, 'partners.1.$.score': 1000 } to the $set.
Unfortunately, the result to my request is a partners array that contains only one empty object.
{
"creator": {
"id": "5b8544fa11235d9f02a9b4f1",
"score": 0
},
"_id": "5bb6375f5f68cc5c52bc93ae",
"game": "5b45080bb1806be939bfde03",
"partners": [
{
"_id": "5bb63775f6d99b7b76443741"
}
],
"created": "2018-10-04T15:53:03.386Z",
"updated": "2018-10-04T15:53:03.386Z",
"__v": 0
}
Can anyone guide me into updating the creator score and all partners score at the same time?
My thoughs about findOneAndUpdate method on a model is that it's better because it doesn't require the data to be changed outside of the BDD, but wanting to update array keys and another key seems very difficult.
Instead, I relied on a set/save logic, like this:
PlayedGame.findById(body.id)
.then(game => {
game.set('creator.score', update.creatorScore);
update.partners.forEach((score, index) => game.set(`partners.${index}.score`, score));
game.save()
.then(result => {
cb(null, {
statusCode: 200,
headers: defaultResponseHeader,
body: JSON.stringify(result)
})
})
.catch(err => {
cb(null, {
statusCode: err.statusCode || 500,
headers: { 'Content-Type': 'text/plain' },
body: JSON.stringify({ 'Update failed: ': err })
})});
})

Resources