How to mock or spy on recaptcha in Jasmine - jasmine

I added recaptcha-v2 in my Angular6 forms. How do I unit-test the form? I want to test that the form under test is invalid if recaptcha isn't clicked and also the form gets submitted if recaptcha is clicked.
The test I have written so far is
fit('it should emit form values if both username and password fields in the login form are correctly filled', () => {
const loginComponent = component;
spyOn(loginComponent.formOutputEvent, 'emit');
const formControls = loginComponent.loginForm.controls;
const email = 'test#test.com';
const password = 'testpassword';
formControls['userName'].setValue(email);
formControls['password'].setValue(password);
formControls['recaptcha'].setValue({}); //what value to set here?
loginComponent.loginFormSubmitted();
expect(loginComponent.formOutputEvent.emit).toHaveBeenCalledWith(new LoginFormValues(email, password));
});
I am getting error Expected spy emit to have been called with [ LoginFormValues({ username: 'test#test.com', password: 'testpassword', recaptcha: '' }) ] but actual calls were [ LoginFormValues({ username: 'test#test.com', password: 'testpassword', recaptcha: Object({ }) }) ].

Interesting, the following works even though LoginFormValues expects the 3rd argument to be of type string but {} isn't
expect(loginComponent.formOutputEvent.emit).toHaveBeenCalledWith(new LoginFormValues(email, password,{}));
But this fails because {}.toString(). gets converted to [Object object].
expect(loginComponent.formOutputEvent.emit).toHaveBeenCalledWith(new LoginFormValues(email, password,{}.toString));
I am still waiting for better answer

Related

Cypress cy.visit() is not redirecting to correct URL

I am trying to bypass the UI login by using cy.request() to log a user in and cy.visit() to go to the restricted route. I have followed this doc: https://docs.cypress.io/guides/end-to-end-testing/testing-your-app#Bypassing-your-UI
However, the test fails because the visit URL ('http://localhost:3000/join-or-create') is not loaded and instead the home page URL is loaded (http://localhost:3000/).
This is my test code:
describe('when user is signed in but has not joined a group', () => {
beforeEach(() => {
cy.request('POST', 'http://localhost:5000/api/testing/reset');
const user = {
name: 'Joe Bloggs',
email: 'Joe#Bloggs.com',
password: 'Password',
};
cy.request('POST', 'http://localhost:5000/register', user);
cy.request('POST', 'http://localhost:5000/sign-in', user);
cy.visit('http://localhost:3000/join-or-create');
});
it.only('should allow a logged in user to join or create a group', () => {
cy.contains('Join a group');
cy.contains('Create a group');
});
});
If I change cy.contains('Join a group'); to cy.contains('Welcome'); (which is content on the URL 'http://localhost:3000/') then the test passes.
If I use:
cy.visit('http://localhost:3000');
cy.get('[data-testid="email"]').type('Joe#Bloggs.com');
cy.get('[data-testid="password"]').type('Password');
cy.contains('sign in').click();
instead of cy.visit('http://localhost:3000/join-or-create'); the test passes.
The output of the test body shows that is redirecting to a new URL 'http://localhost:3000/' (as shown in the screenshot below) but I can't figure out why.
Thanks for any help.
In the bypass code, check the response from the POST request
cy.request('POST', 'http://localhost:5000/sign-in', user)
.then(response => console.log(response))
to see what tokens are returned.
Then in the form-login code, look at what happens to the same token after cy.contains('sign in').click() and see where the browser stores same token.
That's probably the step missing in the bypass code. You'll need to add something to do the same, e.g
cy.request('POST', 'http://localhost:5000/sign-in', user)
.then(response => {
const token = response.body.token; // figure out where the token is in response
cy.setCookie(token); // figure out where the app want's the token
})
It's also difficult to tell from the question what URL you need to cy.visit() in the bypass code, but there's only a couple of them so try both permutations.

Convenient format for validation errors

I'm developing application with next tech stack:
Backend (NestJs, GraphQL)
Frontend (React, Apollo^3.5.0)
I have REST and Gql apis. I'm stacked in defining errors format for both apis types: REST and Gql. How to make it universal.
I will explain my problem on real example. It's registration form.
Example
In frontend app I have simple react form with 2 fields: email and password. Of course, I do validation on both sides.
On client side:
Email field should pass regexp
Password field should have >6 characters
On backend side (class-validator decorators):
Email ( #IsEmail, #UniqEmail - custom decorator, that is searching user with provided email in db )
Password ( #MinLength(6) )
Problem
I want to find the easiest and flexible way (or format) of validation errors to provide them to the client side. To make a simple map and show them in ui.
I found solution but it seems to me weird (because of low typing level). On backend side I make a special errors formatting:
app.useGlobalPipes(
new ValidationPipe({
exceptionFactory: (errors) => {
// property - property that caused error
// constraints - type of error(minLength, uniq, ...)
const formatted = errors.map((error) => ({ property: error.property, constraints: error.constraints }));
return new BadRequestException(formatted);
},
}),
);
And format provided errors for gql:
formatError: (error) => {
if (GraphqlErrorFormatter.getErrorCode(error) === 'INTERNAL_SERVER_ERROR') {
return GraphqlErrorFormatter.formatInternalError(error);
}
return GraphqlErrorFormatter.formatError(error);
},
And map errors on client side in such way:
// Dictionary to map errors in current language
const map = {
email: {
uniq: 'Email is already registered',
},
password: {
minLength: 'Bla-bla-bla',
},
};
// MUTATION PROPS
onError: (err) => {
err.graphQLErrors.forEach((graphQLError) => {
graphQLError.extensions.response.message.forEach((message) => {
const { property, constraints } = message;
const [constraint] = Object.keys(constraints);
const errorMessage = map[property][constraint];
if (errorMessage) setFieldError(property, { message: errorMessage });
});
});
}
Other solutions
Gql union type for response payload approach (e.g. put errors in data key in resp object).
I know about this but I decided to use it for bussiness errors, not validation errors.
Question
What is your experience? What approach do you use in your applications? Thanks in advance for any answer.

Bypass UI Login using Cypress

I am having problem bypassing UI login. My web application doesn't use API to authenticate users. There are no endpoints like /login. index.php will just open the login page and submit the form to login.
The application authenticate the user by
auth($_REQUEST['username'], $_REQUEST['password_tx']);
This is what cypress printed after UI login submit.
I have no idea how to move on from here.
// This doesn't work. The application doesn't get the user details from the body. It is in the submitted form.
cy.request({
method: 'POST',
url: '/index.php?p=sys001',
form: true,
body: {
username: 'user',
password_tx: 'pass'
}
})
This is the complete testcase for the issue. Added comments to make them understandable.
it("login via form spoof", () => {
cy.get("div#mDiv > form").invoke("attr", "action").then(($action) => { //get
the attribute of 'action' and pass encoded uname and pwd to it
let username = Cypress.env("username");
let password = Cypress.env("password");
cy.intercept("POST", $action, (req) => { //post request and populate body
// intercepting the POST form to spoof it.
req.body = $action + encodeURIComponent(username)+ encodeURIComponent(password)
})
.as("loginForm"); //alias
});
cy.get("div#mDiv > div.login > form")
.submit(); //Submit the form after locating it.
});

Cypress w/graphql - having issues getting AUTH with testing via UI. Better way to stub mutation?

So, if I am testing pages in a vacuum without much interaction with the backend, it works great. I am having issues with actually interacting with my UI if it hits any type of service. Basically, nothing is Auth'd. I try programmatically setCookie, no dice. I try to read the cookie, nope. Btw, my whole site requires a login.
cy.setCookie('sess', ';askjdfa;skdjfa;skdjfa;skdjfa;skfjd');<-- does not work
cy.getCookie('sess').should('exist') <-- does not work
I am having an issue on really the best way to "test" this. For example, I have an account section that a user can "update" their personals. I try, fill out the form (via UI testing), but the submission is rejected, no Auth. EVEN THOUGH I just logged in (via UI testing). - I know I need to remove that since it is bad practice to UI-Login for every section of my site.
So, I don't know how to stub graphql calls with cy.request(). Here is my mutation.
mutation Login($email: Email!, $password: String!) {
login(email: $email, password: $password) {
userID
firstName
lastName
}
}
Right now, I am importing the login spec for each section of the site i am testing. I know this is an anti-pattern. Like to solve this problem.
My AUTH (cookie) is not being set. Even when I try to set it, programmatically, doesn't work.
Maybe I should just stub out my graphql mutations? How?
Lastly, IF I am stubbing out my graphql mututations, how do I update the session ( via my main session query ). If I can get these mutations to work, then refreshing the page will get my my updated data, so I'm not completely needing this part.
Any ideas?
I didn't do the stub and all those, as you were asking how the mutation would work with cy.request in my other post. I did it this way and it just basically works. Hopefully this would help
I created a const first though
export const join_graphQL = (query, extra={}) => {
return `mutation {
${query}(join: { email: "${extra.email}", id: "${extra.id}" }) {
id, name, email
}
}`
};
request config const
export const graphqlReqConfig = (body={}, api=graphQlapi, method='POST') => {
return {
method,
body,
url: api,
failOnStatusCode: false
}
};
mutation query with cy.request
const mutationQuery = join_graphQL('mutationName', {
email: "email",
id: 38293
});
cy.request(graphqlReqConfig({
query: mutationQuery
})).then((res) => {
const data = res.body.data['mutationName']; // your result
});
hopefully it's not too messy to see.
basically the fields need to be string such as "${extra.email}" else it will give you error. Not sure how the graphql works deeply but if I just do ${extra.email} I would get an error which I forgot what error it was.
Here's a simpler way of handling a mutation with cy.request
const mutation = `
mutation {
updateUser(id: 1, firstName: "test") {
firstName
lastName
id
role
}
}`
cy.request({
url: url,
method: 'POST',
body: { query: mutation },
headers: {
Authorization: `Bearer ${token}`,
},
})

AngularJS Ajax get issue

I am a bit new to angular and am scratching my head over this one. my factory console.log's show that the first time through everything is defined correctly, but the factory is for some reason re-evaluating the data I pass it to undefined. Here is my code starting with the html input that is passing the data to the controller then to the factory:
HTML:
<input type="text" placeholder="Enter Channel" ng-model="channel" ng-keydown="$event.which === 13 && cname(channel)"/>
This will send to my controller what is typed in the input field once the enter key is pressed cnam(channel)
Controller and Factory:
angular.module('youtubePlaylistAppApp')
.factory('ytVids', function($http){
return{
getChannel: function(name){
console.log('name: '+name);
var url = 'https://www.googleapis.com/youtube/v3/channels?part=contentDetails&forUsername='+ name +'&key=[my api key]';
console.log(url);
return $http.get(url);
},
getVids: function(channel){
console.log('channel: '+ channel);
return $http.get('https://www.googleapis.com/youtube/v3/playlistItems?part=snippet%2CcontentDetails&maxResults=50&playlistId='+channel+'&key=[my api key]')
.success(function(response){
//console.log(response.items);
return response.items;
});
}
};
})
.controller('indexCtrl', function ($scope, ytVids) {
$scope.cname = function(channel){
console.log(channel);
ytVids.getChannel(channel);
ytVids.getChannel().success(function(response){
console.log(response);
console.log(response.items[0].contentDetails.relatedPlaylists.uploads);
ytVids.getVids(response.items[0].contentDetails.relatedPlaylists.uploads)
.success(function(data){
console.log(data.items);
$scope.ytChannel = data.items;
});
});
};
});
Here is the output I am seeing in my console dev tools after enter is pressed when typing in the input field:
the first console log console.log(channel); console logs the correct response. If I type "freddiew" and press enter, that is what is logged
Then I pass that into my factory ytVids.getChannel(channel); and console log in the factory console.log('name: '+name); This logs the correct response as in name: freddiew
I then declare a variable var url = and give it the youtube url and concat the channel name with the url
I then console log the variable and get what is expected https://www.googleapis.com/youtube/v3/channels?part=contentDetails&forUsername=freddiew&key=[my api key]'
This url works when I have tried it in postman and just hard coding a name in. but passing in the name, the console then spits out some more logs which I dont know why, and it looks like it overides the url I just built out to make the call because this is what is logged from the factory:
name: freddiew
https://www.googleapis.com/youtube/v3/channels?part=contentDetails&forUsername=freddiew&key=[my key]
It is then logged after this as undefined and the api call concats undefined with the url and makes the api call with this undefined url
name: undefined
https://www.googleapis.com/youtube/v3/channels?part=contentDetails&forUsername=undefined&key=[my key]
forUsername= is where the channel name should go.
anyone know why it is being defined as undefined?
I think you have wrong extra service call.
.controller('indexCtrl', function ($scope, ytVids) {
$scope.cname = function(channel){
console.log(channel);
//ytVids.getChannel(channel); <-- you should remove this & passed channel param in below call
ytVids.getChannel(channel).success(function(response){
console.log(response);
console.log(response.items[0].contentDetails.relatedPlaylists.uploads);
ytVids.getVids(response.items[0].contentDetails.relatedPlaylists.uploads)
.success(function(data){
console.log(data.items);
$scope.ytChannel = data.items;
});
});
};
});

Resources