Joi validation errors using allow('') - joi

I've come out with a Joi schema that validates data to data5, however, it also validates data6 which is not what I want as it should error.
const Joi = require('joi');
const schema = Joi.object().keys({
a: [Joi.string()],
b: [Joi.string()],
c: Joi.string().required().allow('')
}).with('a', 'c')
.with('b', 'c');
const data = {
a: 'true',
b: 'true',
c: 'some str'
};
const data2 = {
a: 'true',
c: 'some str'
};
const data3 = {
b: 'true',
c: 'some str'
};
const data4 = {
c: 'some str'
};
const data5 = {
c: ''
};
const result = Joi.validate(dataX, schema, { abortEarly: false });
I'm trying to get this to error but it validates due to the .allow('')
const data6 = {
a: 'true',
b: 'true',
c: ''
};
If I remove the .allow() then it works, however, data5 then errors.
How can I change the schema so it errors data6 but continues to validate data to data5.
Cheers,
Paul

Related

Error while executing unit test: argument type error

I am writing a unit test for a converter that converts a group from a local model to a database one and vice versa. When running a unit test, an error occurs:
I divided the test into two groups: In one I convert from the database model to the local one, and in the other - the reverse. The first test works well, but when I add the second test, this error occurs. How can it be solved?
My Test:
main() {
group('Group converter(db model to a local model)', () {
test('The result should be a database model converted to a local model',
() {
final groupConverter = GroupDbConverter();
GroupEntity dbModel = GroupEntity(
id: 1,
groupName: 'Test group',
students: ToMany(),
);
Group expected = Group(
id: 1,
groupName: 'Test group',
students: [],
);
final actual = groupConverter.inToOut(dbModel);
// func for compare students list
Function deepEq = const DeepCollectionEquality().equals;
bool isEqual = deepEq(expected.students, actual.students);
expect(actual.id == expected.id, true);
expect(actual.groupName == expected.groupName, true);
expect(isEqual, true);
});
});
group('Group converter(local model to a db model)', () {
test('The result should be a local model converted to a database model',
() {
final groupConverter = GroupDbConverter();
Group grModel = Group(
id: 3,
groupName: 'Test',
students: [],
);
GroupEntity expected = GroupEntity(
id: 3,
groupName: 'Test',
students: ToMany(),
);
final actual = groupConverter.outToIn(grModel);
// func for compare students list
Function deepEq = const DeepCollectionEquality().equals;
bool isEqual = deepEq(expected.students, actual.students);
expect(actual.id == expected.id, true);
expect(actual.groupName == expected.groupName, true);
expect(isEqual, true);
});
});
}

Promise.all with mongoose

I'm trying to await multiple mongoose promises.
The first version of the code :
const func = async (param) => {
let a = undefined, b = undefined;
a = mongoose.model('a').findOne({ name: '1' }).exec()
if (param === 1) b = mongoose.model('b').findOne({ name: '1' }).exec()
await Promise.all([a, b]);
console.log(a, b)
}
The second version of the code :
const func = async (param) => {
const requests = await Promise.all([
mongoose.model('a').findOne({ name: '1' }),
param === 1 ? mongoose.model('b').findOne({ name: '1' }) : undefined
])
const a = requests[0], b = requests[1];
console.log(a, b)
}
Expected output of the first version would be : "<ModelA> <ModelB>"
But I have :
"Promise {<ModelA>} Promise {<ModelB>}"
The second version is working as expected
Any idea of what I'm doing wrong in the first version ?

Cypress & Microsoft Authentication don't work properly

I am trying to launch a Microsoft authentication via Cypress following the authentication structure proposed by Juunas11 available here (demo on youtube).
However, the request sent to the microsoft authority falls in timeout at the first execution. The following ones are launched without any problem.
Would there be a better way to manage this Microsoft authentication?
Below, the whole of my code written in a step definition but which will be inserted later in a Cypress command.
import { sign, decode, JwtPayload } from 'jsonwebtoken';
Given('le conseiller {string}', () => {
let cachedTokenExpiryTime = new Date().getTime();
let cachedTokenResponse: any = null;
if (cachedTokenExpiryTime <= new Date().getTime()) {
cachedTokenResponse = null;
}
const { tenantId, clientId, clientSecret, apiScopes, username, password } = Cypress.env('loginMicrosoft');
const authority = `https://login.microsoftonline.com/${tenantId}`;
const environment = 'login.windows.net';
const buildAccountEntity = (
homeAccountId: string,
realm: string,
localAccountId: string,
clientInfo: object,
username: string,
name: string
) => {
return {
authorityType: "MSSTS",
clientInfo: sign(clientInfo, clientSecret).split('.')[1],
homeAccountId,
environment,
realm,
localAccountId,
username,
name
};
};
const buildIdTokenEntity = (homeAccountId: string, idToken: string, realm: string) => {
return {
credentialType: "IdToken",
homeAccountId,
environment,
clientId,
secret: idToken,
realm,
};
};
const buildAccessTokenEntity = (
homeAccountId: string,
accessToken: string,
expiresIn: number,
extExpiresIn: number,
realm: string,
scopes: string[]
) => {
const now = Math.floor(Date.now() / 1000);
return {
homeAccountId,
credentialType: 'AccessToken',
secret: accessToken,
cachedAt: now.toString(),
expiresOn: (now + expiresIn).toString(),
extendedExpiresOn: (now + extExpiresIn).toString(),
environment,
clientId,
realm,
target: scopes.map((s) => s.toLowerCase()).join(' '),
tokenType: 'Bearer'
};
};
const injectTokens = (tokenResponse: any) => {
const idToken: JwtPayload = decode(tokenResponse.access_token) as JwtPayload;
const localAccountId = idToken.oid || idToken.sid;
const realm = idToken.tid;
const homeAccountId = `${localAccountId}.${realm}`;
const name = idToken.name;
const clientInfo = {
"uid": localAccountId,
"utid": realm
}
const accountKey = `${homeAccountId}-${environment}-${realm}`;
const accountEntity = buildAccountEntity(
homeAccountId,
realm,
localAccountId,
clientInfo,
username,
name
);
const idTokenKey = `${homeAccountId}-${environment}-idtoken-${clientId}-${realm}--`;
const idTokenEntity = buildIdTokenEntity(
homeAccountId,
tokenResponse.id_token,
realm
);
const accessTokenKey = `${homeAccountId}-${environment}-accesstoken-${clientId}-${realm}-${apiScopes.join('')}-`;
const accessTokenEntity = buildAccessTokenEntity(
homeAccountId,
tokenResponse.access_token,
tokenResponse.expires_in,
tokenResponse.ext_expires_in,
realm,
apiScopes
);
const msalAccountCle = `msal.${clientId}.active-account`;
const msalAccountEntitee = localAccountId;
cy.window().then((win) => {
win.localStorage.setItem(accountKey, JSON.stringify(accountEntity));
win.localStorage.setItem(idTokenKey, JSON.stringify(idTokenEntity));
win.localStorage.setItem(accessTokenKey, JSON.stringify(accessTokenEntity));
win.localStorage.setItem(msalAccountCle, msalAccountEntitee);
})
};
const login = (cachedTokenResponse: any) => {
let tokenResponse: any = null;
let chainable: Cypress.Chainable = cy.visit('');
if (!cachedTokenResponse) {
chainable = chainable.request({
url: `${authority}/oauth2/v2.0/token`,
method: 'POST',
body: {
grant_type: 'password',
client_id: clientId,
client_secret: clientSecret,
scope: 'openid profile user.read',
username: username,
password: password,
},
timeout: 120000,
form: true,
log: true,
retryOnStatusCodeFailure: true,
retryOnNetworkFailure: true
});
} else {
chainable = chainable.then(() => {
return {
body: cachedTokenResponse,
};
});
}
chainable
.then((response) => {
console.log('réponse de microsoft : ', response.allRequestResponses);
injectTokens(response.body);
cy.window().then((win) => {
expect(win.localStorage.length).to.be.gte(0);
});
tokenResponse = response.body;
cy.getCookies().log;
})
.visit('')
.then(() => {
return tokenResponse;
})
.waitUntil(() => cy.get('.header-sub'));
return chainable;
};
login(cachedTokenResponse)
.then((tokenResponse) => {
cachedTokenResponse = tokenResponse;
cachedTokenExpiryTime = new Date().getTime() + 50 * 60 * 1000;
});
});

Joi validation of with when() on external property

I have something like this:
let someCoolString = 'mememe';
const schema = Joi.object({
someFieldName: Joi.string()
.when(someCoolString, {
is: "mememe",
then: Joi.required(),
otherwise: Joi.forbidden(),
})
});
But this obviously doesn't work, as someCoolString is not the Joi.object's property. Any idea of how to check for it?
You can use context:
const schema = Joi.object().keys({
someFieldName: Joi.number().when('$condition', {
is: Joi.boolean().valid(true).required(),
then: Joi.required(),
otherwise: Joi.forbidden()
})
});
let someCoolString = 'mememe';
let someCoolString2 = 'not_meme';
function isMeme(str) {
return str == 'mememe'
};
// error: null
schema.validate({});
// someFieldName required
schema.validate({}, {context: {condition: isMeme(someCoolString)}});
// someFieldName forbidden
schema.validate({ someField: 10 }, {context: {condition: isMeme(someCoolString2)}});

Yup validation: Require all or none fields of a language

We have quite a large form with multilang inputs - a few fields:
name_german
name_french
name_italian
description_german
description_french
description_italian
... many more
In case that one german field is filled, all other german fields should be required. Same goes with the other languages. It should be possible to have both german and french fields filled, but the italian ones can be empty.
Somehow I can't get it working. This is what I tried:
Yup.object().shape({
name_german: Yup.string().test(
'requireAllIfOneIsFilled',
'formvalidation.required.message',
function () {
return multiLanguageTest(this.parent, 'german');
},
),
... // same for other fields
});
Then do the test like this:
const multiLanguageTest = (formValues, language: 'german' | 'french' | 'italian'): boolean => {
const errorCount = dependentFields.reduce((errorCount, rawFieldName) => {
const postFixedField = `${rawFieldName}_${language}`;
if (!formValues[postFixedField]) {
return errorCount + 1;
}
return errorCount;
}, 0);
return errorCount === 0;
};
This gives me quite an undebuggable behavior. Am I using the concept of .test wrong?
It is possible to validate in this manner using the context option passed in when calling .validate.
A simple but illustrative example of the context option - passing a require_name key (with truthy value) to the .validate call will change the schema to be .required():
const NameSchema = yup.string().when(
'$require_name',
(value, schema) => value ? schema.required() : schema
);
const name = await NameSchema.validate('joe', {
context: {
require_name: true,
}
});
If we can pass in the required fields to the schema during validation, we can use the following schema for your validation case:
const AllOrNoneSchema = yup.object({
name_german: yup.string()
.when('$name_german', (value, schema) => value ? schema.required() : schema),
description_german: yup.string()
.when('$description_german', (value, schema) => value ? schema.required() : schema),
date_german: yup.string()
.when('$date_german', (value, schema) => value ? schema.required() : schema),
// ...other_languages
});
In your case, we must pass in a required context key for each language if one or more of the fields is filled out. i.e. If one German field has been filled out, we want to pass an object with all ${something}_german keys whose values are true:
{
name_german: true,
description_german: true,
...
}
To do this, we can create a helper function that takes your form object and returns a record with boolean values as in the example above:
const createValidationContext = (form) => {
const entries = Object.entries(form);
const requiredLanguages = entries.reduce((acc, [key, value]) => {
return !value.length ? acc : [...acc, key.split('_')[1]];
}, []);
return Object.fromEntries(
entries.map(([key, value]) => [key, requiredLanguages.includes(key.split('_')[1])])
)
};
Putting it all together, we have the following working solution:
const yup = require("yup");
const AllOrNoneSchema = yup.object({
name_german: yup.string()
.when('$name_german', (value, schema) => value ? schema.required() : schema),
description_german: yup.string()
.when('$description_german', (value, schema) => value ? schema.required() : schema),
date_german: yup.string()
.when('$date_german', (value, schema) => value ? schema.required() : schema),
});
const createValidationContext = (form) => {
const entries = Object.entries(form);
const requiredLanguages = entries.reduce((acc, [key, value]) => {
return !value.length ? acc : [...acc, key.split('_')[1]];
}, []);
return Object.fromEntries(
entries.map(([key, value]) => [key, requiredLanguages.includes(key.split('_')[1])])
)
};
const form = {
description_german: "nein",
date_german: "juli",
name_german: "nena",
}
const formContext = createValidationContext(form);
console.log(formContext); // { description_german: true, date_german: true, name_german: true }
const validatedForm = await AllOrNoneSchema.validate(form, { context: formContext });
console.log(validatedForm); // { description_german: "nein", date_german: "juli", name_german: "nena" }
A live example of this solution, with multiple languages, can be seen an tested on RunKit, here: https://runkit.com/joematune/6138ca762dc6340009691d8a
For more information on Yup's context api, check out their docs here: https://github.com/jquense/yup#mixedwhenkeys-string--arraystring-builder-object--value-schema-schema-schema