Joi form validation: removing displayed errors - javascript

Here is a link to codesandbox example (Svelte). It represents a trivial register from being validated with Joi. The issue I'm facing is with clearing Joi validation error messages. I'm recording them in errors object, keys named by the input name (email, password, passwordConfirm).
Validations happens via function attached to form on:input
const validate = async e => {
const fields = { [e.target.name]: e.target.value };
try {
errors[Object.keys(fields)[0]] = null;
await registerSchema.validateAsync(
{ ...fields },
{ abortEarly: false, allowUnknown: true }
);
} catch (err) {
errors[Object.keys(fields)[0]] = err;
}
};
Its crude but it mostly works. It clears email and password errors alright but whatever I do passwordConfirm persists.
I don't think the issue is with my Joi schema:
export const registerSchema = Joi.object({
email: Joi.string().email({ tlds: { allow: false } }),
password: Joi.string().pattern(new RegExp("^[a-zA-Z0-9]{6,30}$")).trim(),
passwordConfirm: Joi.string().valid(Joi.ref("password")).trim()
});
Am pretty sure that the bug hides somewhere in the validate function itself but - for the death of me - I can find it. I would appreciate couple of hints here.
Again, example can be seen on codesandbox.

Your problem is indeed in your validate function. When you build your fields object, the only key you add to it is the currently active input:
const fields = { [e.target.name]: e.target.value };, meaning that when you compare passwordConfirm to Joi.ref('password') in your schema, the latter is always an empty value because there is no password key in the object being validated!
One (crude) way around that is to make sure the password field is passed into the object being validated when the active input is passwordConfirm:
const validate = async e => {
const fields = { [e.target.name]: e.target.value };
if (e.target.name === 'passwordConfirm') {
fields.password = password;
}
// console.log(fields); // log object being validated
try {
errors[Object.keys(fields)[0]] = null;
await registerSchema.validateAsync(
{ ...fields },
{ abortEarly: false, allowUnknown: true }
);
} catch (err) {
errors[Object.keys(fields)[0]] = err;
}
};
Tested in your CodeSandbox and seemed to work as intended.

Related

How to reset a quasar q-form from a method

I am curious how to reset a q-form when the submit action is triggered. I run a function onSubmit but I am not sure how in that method to reset the q-form without having to do each field individually which is annoying. Here is my code:
//methods
const onSubmit = (event) => {
let jsonData =
{
FirstName: firstName.value,
LastName: lastName.value,
PhoneNumber: phoneNumber.value,
EmailAddress: emailAddress.value,
Message: message.value,
Token: token.value
}
api.post('/api/contactus', jsonData)
.then((response) => {
})
.catch(() => {
console.log('API request failed')
})
}
The documentation has an example of exactly what you want
// <q-form ref="myForm">
// to reset validations:
function reset () {
myForm.value.resetValidation()
}
or Options API:
this.$refs.myForm.resetValidation()
This function is shown in the documentation as being tied to a "Reset" button but there's no reason you can't use it after submitting as well.

post api request with json input fields in reactjs

i have form with three input fields with type as
i have design the form but how to send data as object in gcloud field ?
code for gcloud input field:
<label>gcloud keyfile json</label>
<TextareaAutosize
name="gcloud_keyfile"
type="text"
id="gcloud_keyfile"
value={values.gcloud_keyfile}
onChange={handleChange}
/>
while for form submission i have used formik and code is shown below :
const initialValues = {
organ: "",
env: "",
gcloud_keyfile: "" ,
};
const { values, errors, touched, handleBlur, handleChange, handleSubmit } =
useFormik({
initialValues,
validationSchema: GcpSchema,
onSubmit: async (values, action) => {
try {
let response = await axios.post(
`${state.baseUrl}accounts/gcp/`,
{
organization: values.organ,
environment: values.env,
gcloud_keyfile_json: JSON.stringify(values.gcloud_keyfile),
});
if (response.status === 200) {
setMsg("You Are Successfully Create the Account");
action.resetForm();
return Promise.resolve();
}
} catch (error) {
console.error("There was an error!", error);
}
},
});
i have used json stringify but that doesn't work
i used JSON.parse(gcloud_keyfile_json)
to convert string into object and apply validation on the input field to take only object value

Redux store not being populated when mocking API call

I wrote a Register component in react, it is a simple form that on submit will post to an API. The call to the API will return an object with certain data, this data will be then added to the redux store.
I wrote some tests for this. I'm using Mock Service Worker (MSW) to mock the API call. This is my first time for writing these kind of tests so I'm not sure if I'm doing anything wrong, but my understanding was that MSW would intercept the call to the API and return whatever I specify in the MSW config, after that it should follow the regular flow.
Here's my reducer:
const authReducer = (state = INITIAL_STATE, action) => {
switch (action.type) {
case actionTypes.REGISTER_NEW_USER:
const newUser = new User().register(
action.payload.email,
action.payload.firstName,
action.payload.lastName,
action.payload.password
)
console.log("User registered data back:");
console.log(newUser);
return {
...state,
'user': newUser
}
default:
return state;
}
}
this is my User class where the actual call is performed:
import axios from "axios";
import { REGISTER_API_ENDPOINT } from "../../api";
export default class User {
/**
* Creates a new user in the system
*
* #param {string} email - user's email address
* #param {string} firstName - user's first name
* #param {string} lastName - user's last name
* #param {string} password - user's email address
*/
register(email, firstName, lastName, password) {
// console.log("registering...")
axios.post(REGISTER_API_ENDPOINT, {
email,
firstName,
lastName,
password
})
.then(function (response) {
return {
'email': response.data.email,
'token': response.data.token,
'active': response.data.active,
'loggedIn': response.data.loggedIn,
}
})
.catch(function (error) {
console.log('error');
console.log(error);
});
}
}
this is my action creator:
export function createNewUser(userData) {
return {
type: REGISTER_NEW_USER,
payload: userData
}
}
this is the onSubmit method in my Register component:
const onSubmit = data => {
// console.log(data);
if (data.password !== data.confirmPassword) {
console.log("Invalid password")
setError('password', {
type: "password",
message: "Passwords don't match"
})
return;
}
// if we got up to this point we don't need to submit the password confirmation
// todo but we might wanna pass it all the way through to the backend TBD
delete data.confirmPassword
dispatch(createNewUser(data))
}
and this is my actual test:
describe('Register page functionality', () => {
const server = setupServer(
rest.post(REGISTER_API_ENDPOINT, (req, res, ctx) => {
console.log("HERE in mock server call")
// Respond with a mocked user object
return res(
ctx.status(200),
ctx.json({
'email': faker.internet.email(),
'token': faker.datatype.uuid(),
'active': true,
'loggedIn': true,
}))
})
)
// Enable API mocking before tests
beforeEach(() => server.listen());
// Reset any runtime request handlers we may add during the tests.
afterEach(() => server.resetHandlers())
// Disable API mocking after the tests are done.
afterAll(() => server.close())
it('should perform an api call for successful registration', async () => {
// generate random data to be used in the form
const email = faker.internet.email();
const firstName = faker.name.firstName();
const lastName = faker.name.lastName();
const password = faker.internet.password();
// Render the form
const { store } = renderWithRedux(<Register />);
// Add values to the required input fields
const emailInput = screen.getByTestId('email-input')
userEvent.type(emailInput, email);
const firstNameInput = screen.getByTestId('first-name-input');
userEvent.type(firstNameInput, firstName);
const lastNameInput = screen.getByTestId('last-name-input');
userEvent.type(lastNameInput, lastName);
const passwordInput = screen.getByTestId('password-input');
userEvent.type(passwordInput, password);
const confirmPasswordInput = screen.getByTestId('confirm-password-input');
userEvent.type(confirmPasswordInput, password);
// Click on the Submit button
await act(async () => {
userEvent.click(screen.getByTestId('register-submit-button'));
// verify the store was populated
console.log(await store.getState())
});
});
So I was expecting my call to be intercepted whenever the REGISTER_API_ENDPOINT url is detected, and the value of the mocked call to be added to my redux state instead of the value of the actual API call in register method but that doesn't seem to be happening. If that's not the way to test a value in the store, how else can I achieve that?
So at the end of my test, when printing the store I was expecting to see:
{ auth: { user:
{
'email': faker.internet.email(),
'token': faker.datatype.uuid(),
'active': true,
'loggedIn': true,
}
}
but instead I'm seeing:
{ auth: { user: null } }
Is this the right approach for this test?
Thanks
EDIT
Doing some refactoring based on the comments. Now my onSubmit method looks like:
const onSubmit = async data => {
if (data.password !== data.confirmPassword) {
console.log("Invalid password")
setError('password', {
type: "password",
message: "Passwords don't match"
})
return;
}
// if we got up to this point we don't need to submit the password confirmation
// todo but we might wanna pass it all the way through to the backend TBD
delete data.confirmPassword
let user = new User()
await user.register(data).
then(
data => {
// console.log("Response:")
// console.log(data)
// create cookies
cookie.set("user", data.email);
cookie.set("token", data.token);
dispatch(createNewUser(data))
}
).catch(err => console.log(err))
Notice that now I'm dispatching the response from User.register in here instead of doing it in User.register. Also notice that this function is now async and await for the register function call to be finalized, at that moment it'll populate the store.
The register method now looks like the following:
async register(data) {
let res = await axios.post(REGISTER_API_ENDPOINT, {
'email': data.email,
'firstName': data.firstName,
'lastName': data.lastName,
'password': data.password
})
.then(function (response) {
return response
})
.catch(function (error) {
console.log('error');
console.log(error);
});
return await res.data;
}
now it's only in charge of performing the API call and returning the response.
The reducer was also simplified not to have any side effect changes, so it looks like:
const authReducer = (state = INITIAL_STATE, action) => {
switch (action.type) {
case actionTypes.REGISTER_NEW_USER:
const newUser = action.payload
return {
...state,
'user': newUser
}
default:
return state;
}
}
my test is mostly the same, the only difference is the part where I'm inspecting the store value:
// Click on the Submit button
await act(async () => {
userEvent.click(screen.getByTestId('register-submit-button'));
});
await waitFor(() => {
// verify the store was populated
console.log("Store:")
console.log(store.getState())
})
Now, this sometimes work and sometimes does not. Meaning, sometimes I get correct store printed as follows:
console.log
Store:
at test/pages/Register.test.js:219:21
console.log
{
auth: {
user: {
email: 'Selena.Tremblay#hotmail.com',
token: '1a0fadc7-7c13-433b-b86d-368b4e2311eb',
active: true,
loggedIn: true
}
}
}
at test/pages/Register.test.js:220:21
but sometimes I'm getting null:
console.log
Store:
at test/pages/Register.test.js:219:21
console.log
{ auth: { user: null } }
at test/pages/Register.test.js:220:21
I guess I'm missing some async code somewhere but I cannot put a pin on where is it.
There are some Redux rules that are being broken here:
Don't do side effects in reducers:
reducers should be pure functions: for the same input, return always
the same output. This is not the place to do API calls.
State should be immutable: you should never change a state value by reference, always provide a new state with a new object containing the changes.
So, the classical redux approach would be to have three actions in Redux: REGISTER_USER, REGISTER_USER_SUCCEEDED, REGISTER_USER_FAILED .
reducer:
const authReducer = (state = INITIAL_STATE, action) => {
switch (action.type) {
case actionTypes.REGISTER_USER:
return {
...state,
status: 'loading'
}
case actionTypes.REGISTER_USER_SUCCEEDED:
return {
...state,
status: 'idle',
user: action.user
}
case actionTypes.REGISTER_USER_FAILED:
return {
...state,
status: 'error'
}
default:
return state;
}
}
Then, async work should be done in your event handlers:
onSubmit:
const onSubmit = async data => {
// ...
dispatch(registerNewUser());
const user = new User()
try {
await user.register(data);
dispatch(registerNewUserSucceeded(user));
} catch(e) {
console.error(e);
dispatch(registerNewUserFailed());
}
}
**Don't forget to return the promise from axios inside your register function, so you can await on the promise. Currently, you are only calling axios, but not updating or returning anything...
What's great about this, is that testing your store doesn't require you to do any network calls! You could ditch MSW (although it's a great lib, just not needed here).
In your tests, just check your store state before and after every transition:
const mockUser = {...} // provide a mock user for your test
const store = createStore(authReducer);
store.dispatch(registerNewUserSucceeded(mockUser);
expect(store.getState()).toEqual({user: mockUser, status: 'idle'});
Edit
In response to the asker's edit, there is now a bug because of the confusing combination of await with .then.
Specifically, in onSubmit, you are doing both await and .then on the same promise. In this case, there is a race condition. The .then call happens first, and after that the await happens.
So instead of await user.register(data).then(...):
const onSubmit = async data => {
// ...
try {
await user.register(data);
} catch(e) {
console.log(e);
}
dispatch(createNewUser(data));
}
Here I'm only using await. the try/catch clause is instead of calling .catch on the promise.
using await lets you write as if you are writing synchronic code, so just write whatever you would put inside .then on the next line after an await expression.
Also in your register function:
async register(data) {
try {
let res = await axios.post(...);
return res;
} catch(e) {
console.log("error: ", e);
}
}
The state won't be updated instantly, as the server call is a promise. You should await something on the page the indicates the process is complete like this:
// Click on the Submit button
await act(async () => {
userEvent.click(screen.getByTestId('register-submit-button'));
await wait(() => getByText('Some text that appears after success '));
// verify the store was populated
console.log(await store.getState())
});
Or you can wait for the update:
// Click on the Submit button
await act(async () => {
userEvent.click(screen.getByTestId('register-submit-button'));
await act(() => sleep(500));
// verify the store was populated
console.log(await store.getState())
});

How to test for mongoose model validation errors with Jest using async/await?

I'm trying to test a simple model validation using Jest.
The model is as follows:
const { Schema, model } = require('mongoose')
const { isEmail, isAlphanumeric, isNumeric, isAlpha } = require('validator')
const userSchema = new Schema({
name: {
type: String,
required: true
},
email: {
type: String,
validate(v) {
return isEmail(v)
}
}
})
const User = model('User', userSchema)
Then, in my test file, I have the following setup:
const User = require("../models/user");
const invalidUser = {
name: "David",
email: "david#invalid"
};
const validUser = {
name: "David",
email: "david#example.com"
};
Now, when I try running a test using callbacks, I get the desired result (err is null):
test("Should not validate user without valid email", done => {
const user = new User(invalidUser);
user.validate(err => {
expect(err).not.toBeNull();
done();
});
});
However, when I refactor that test to use async/await, it just passes every time, even if I change invalidUser to have a valid email (which should case the test to fail):
test("ASYNC Should not validate user without valid email", async () => {
try {
const user = new User(invalidUser);
await user.validate();
} catch (e) {
expect(e).not.toBeNull();
}
});
My guess is e is not correctly populating with the error from validate(), but why is that?
Thanks!
Your async version is not right, as it will always finish successfully. Take into account that:
When you use an invalidUser, the catch() block will be called with your expected error, so you’ll check not.toBeNull() and everything will be ok.
When you use a validUser, the user.validate() promise will be resolved without errors, so your catch block will not be executed and your test case will finish successfully, too.
So you need a different approach, to ensure your test is rejected when the validate() promise is resolved successfully:
test("ASYNC Should not validate user without valid email", async () => {
let error = null;
try {
const user = new User(invalidUser);
await user.validate();
} catch (e) {
error = e;
}
expect(error).not.toBeNull();
});
A more correct approach would probably be to use jest's .rejects and .toThrow() like this. In your case:
test("ASYNC Should not validate user without valid email", async () => {
const user = new User(invalidUser);
await expect(user.validate()).rejects.toThrow();
});
And if you want a specific error message you can test it by adding it as an argument like this: .toThrow('error message goes here'). More on this in jest docs.

React Formik not handling errors properly

I'm using the basic Formik template to work on a Login Form.
onSubmit={(
values,
{ setSubmitting, setErrors /* setValues and other goodies */ }
) => {
props.logMeIn(values);
// LoginToSystem(values).then(
// user => {
// setSubmitting(false);
// // do whatevs...
// // props.updateUser(user)
// },
// errors => {
// setSubmitting(false);
// // Maybe transform your API's errors into the same shape as Formik's
// //setErrors(transformMyApiErrors(errors));
// console.log(errors);
// }
// );
}}
This problem is within the onSubmit section; The demo code is commented out but it uses a LoginToSystem function that seems to be a promise. I can not figure out 'what' this function is supposed to me. My function that handles this would be props.logMeIn() - Which also does not work as intended
If the login is successful, it will currently work as expected, and everything is fine. However, if the login fails (404, 401, whatever) the form will remain there, and the setSubmitting log stays there so Submit is grayed out but nothing is done.
If I try to replace LoginToSystem with my function, I get an error on the .then that I can't perform .then on undefined.
I'm wondering if perhaps this is because my function is not set up like a Promise?
loginClickHandler = (user) => {
let userObj = {
email: user.email,
password: user.password
}
axios.post('api/v1/auth/sign_in', userObj)
.then((res) => {
console.log(res.headers);
let loggedInUser = {
'access_token': res.headers['access-token'],
'client': res.headers['client'],
'uid':res.headers['uid'],
'signedIn': true
};
this.setState({
user: loggedInUser
})
this.props.retrieve(user.email);
})
.catch((err) => {
console.log(err);
return err
})
};
My function does properly catch (Thanks to axios) on the .then/.catch, but perhaps I am supposed to modify those to provide a callback so that onSubmit can properly fire?
With some guidance I was able to resolve this one simpler. Axios is natively returning a 'promise' so I just needed to ensure the outcome of the function was axios' method in the end.
loginClickHandler = (user) => {
let userObj = {
email: user.email,
password: user.password
}
const request = axios.post('api/v1/auth/sign_in', userObj);
request.then((res) => {
console.log(res.headers);
let loggedInUser = {
'access_token': res.headers['access-token'],
'client': res.headers['client'],
'uid': res.headers['uid'],
'signedIn': true
};
this.setState({user: loggedInUser, auth: true, anchorEl: null})
}).catch((err) => {
console.log(err);
// setErrors({ test: 'This was an error' })
})
return request;
};
In onSubmit there's a second argument for setting your errors. I added flow to be able to see the types better in this answer for you.
<Formik
initialValues={...}
... // Other Props
onSubmit={this.handleSubmit} // This is where you handle your login logic
render={this.renderForm} // Render your form here.
You have a callback to help you set errors in the second argument
handleSubmit = (
user: FormValues,
{ setErrors }: FormikActions<FormValues>
) => {
return axios.post('api/v1/auth/sign_in', userObj)
...
.catch(e) => {
setErrors({ username: 'Invalid Username' }) // Object you want to put here.
}
}
In your render form function you now have errors that you can use based on what you called in your setErrors
renderForm = ({
... // These are your other FormikProps you're using
errors // You want to use this
}: FormikProps<FormValues>) => (
... // Your rendering here
// errors.username
)
For flow types on Formik
https://github.com/flowtype/flow-typed/blob/master/definitions/npm/formik_v0.9.x/flow_v0.53.x-/formik_v0.9.x.js

Categories