I'm trying to unit test my http.get/post/etc responses.
I found this tutorial that was extremely helpful: https://medium.com/spektrakel-blog/angular-testing-snippets-httpclient-d1dc2f035eb8
Going through and following that, I've configured my unit tests and I'm able to get everything working, but there's one part that I have that is inconsistent with the tutorial...
In the tutorial, it shows to test the service login function like this:
it(`should emit 'true' for 200 Ok`, async(inject([HttpClientFeatureService, HttpTestingController],
(service: HttpClientFeatureService, backend: HttpTestingController) => {
service.login('foo', 'bar').subscribe((next) => {
expect(next).toBeTruthy();
});
backend.expectOne('auth/login').flush(null, { status: 200, statusText: 'Ok' });
})));
And here's the actual method on the service that is being tested:
login(user: string, password: string): Observable<boolean> {
const body = new HttpParams()
.set(`user`, user)
.set(`password`, password);
const headers = new HttpHeaders({ 'Content-Type': 'application/x-www-form-urlencoded' });
return this.http.post(`auth/login`, body.toString(), { headers, observe: 'response' })
.map((res: HttpResponse<Object>) => res.ok)
.catch((err: any) => Observable.of(false));
}
Here's my login function:
login(username: string, password: string): Observable<any> {
this.loggingService.log('LoginService | login | username: ' + username + '; password: xxxxx');
return this.http.post(this.loginUrl, { username: username, password: password })
.map((response: any) => {
console.log('response: ' + JSON.stringify(response));
if (response && response.length > 0) {
return response;
} else {
return this.parseErrorResponse(response);
}
});
}
And here's my unit test:
it('login should return a valid JWT', async(inject([LoginService, HttpTestingController], (service: LoginService, backend: HttpTestingController) => {
service.login('user', 'password').subscribe((next) => {
expect(next).toEqual('asdfasdfasdf');
});
backend.expectOne(environment.authenticationServiceBaseUrl + 'api/login')
.flush('asdfasdfasdf', { status: 200, statusText: 'Ok' });
})));
You'll notice the difference here is in the map response section. My version is getting back just a string from the unit test's http.post call, while the example shows that it's returning an HttpResponse object and is just checking that the statusText property is equal to 'Ok'.
Why is my version returning just the string, while the examples version is returning the actual HttpResponse (which includes status and statusText)? I WANT the tutorial version here...
The example shows that it returns null in the body of the response via the flush function call, while I had to add my dummy JWT value in there in order to get my test to pass. Even when I specify that as null to be like the test, then the response that I get in the unit test is null.
Where am I going wrong here?
The tutorial uses observe: 'response', which means that events emitted by the returned observable are responses, and not just the body.
This is covered in the http guide.
Related
Expectation: when wrong login credentials are provided, "non_field_errors: Unable to log in with provided credentials" is returned, such as below (screenshot from a tutorial which I'm following verbatim)
Reality: instead I'm getting the error below.
This gets printed to the console:
POST http://127.0.0.1:8000/api/v1/token/login 400 (Bad Request)
Interestingly I get this same error when I try to create users with passwords that are too short. I'm not having any issues with axios or the server when I provide the right credentials for log in, or use passwords of sufficient length when creating new users. When trying to catch errors such as these that I'm failing to get the expected result.
My code for catching the error is the same as in the tutorial:
methods: {
submitForm() {
axios.defaults.headers.common['Authorization'] = ''
localStorage.removeItem('token')
const formData = {
username: this.username,
password: this.password
}
axios
.post('/api/v1/token/login', formData)
.then(response => {
const token = response.data.auth_token
this.$store.commit('setToken', token)
axios.defaults.headers.common['Authorization'] = 'Token ' + token
localStorage.setItem('token', token)
this.$router.push('/dashboard/my-account')
})
.catch(error => {
if (error.response) {
for (const property in error.response) {
this.errors.push(`${property}: ${error.response.data[property]}`)
}
} else if (error.message) {
this.errors.push('Something went wrong. Please try again!')
}
})
}
}
Is there something in the server settings that I should change?
I'm using Django, rest framework, and djoser.
Don't know if you're using a custom exception handler in Django rest framework but it looks like the issue could be from the way you're handling the error in your frontend application.
You can handle the errors like this.
methods: {
submitForm() {
axios.defaults.headers.common['Authorization'] = ''
localStorage.removeItem('token')
const formData = {
username: this.username,
password: this.password
}
axios
.post('/api/v1/token/login', formData)
.then(response => {
const token = response.data.auth_token
this.$store.commit('setToken', token)
axios.defaults.headers.common['Authorization'] = 'Token ' + token
localStorage.setItem('token', token)
this.$router.push('/dashboard/my-account')
})
.catch(error => {
if (error.response) {
// The request was made and the server responded with a status code
// that falls out of the range of 2xx
console.log(error.response.data);
console.log(error.response.status);
console.log(error.response.headers);
} else if (error.request) {
// The request was made but no response was received
// `error.request` is an instance of XMLHttpRequest in the browser and an instance of
// http.ClientRequest in node.js
console.log(error.request);
} else {
// Something happened in setting up the request that triggered an Error
console.log('Error', error.message);
}
console.log(error.config);
})
}
Can be found here
Test case works in Postman, but not from Visual Studio Code or Jest command line.
Request Body in Postman that works and returns all 4 errors:
{ "Item": {} }
This is missing the FileName and Item.Data fields.
API.dto.ts
import { Type } from 'class-transformer';
import { IsNumberString, IsString, MinLength, ValidateNested } from 'class-validator';
// Expected Payload
// {
// FileName: 'abc',
// Item: {
// Data: '123'
// }
// }
export class ItemDataDTO {
#IsNumberString() #MinLength(2) public readonly Data: string;
}
/**
* This class is the Data Object for the API route
*/
export class ApiDTO {
#IsString() #MinLength(1) public readonly FileName: string;
#ValidateNested()
#Type(() => ItemDataDTO)
Item: ItemDataDTO;
}
api.controller.ts
import { Body, Controller, Get, Options, Put, Request, Response } from '#nestjs/common';
import { ApiDTO } from './api.dto';
#Controller('api')
export class ApiController {
constructor() { }
#Put('donotuse')
public DoNotUse(#Body() APIBody: ApiDTO) {
return 'OK';
}
}
API.dto.spec.ts
import { ArgumentMetadata, ValidationPipe } from '#nestjs/common';
import { ApiDTO } from './api.dto';
describe('ApiDto', () => {
it('should be defined', () => {
expect(new ApiDTO()).toBeDefined();
});
it('should validate the ApiDTO definition', async () => {
const target: ValidationPipe = new ValidationPipe({
transform: true,
whitelist: true,
});
const metadata: ArgumentMetadata = {
type: 'body',
metatype: ApiDTO,
data: '{ "Item": {} }',
};
const Expected: string[] = [
'FileName must be longer than or equal to 1 characters',
'FileName must be a string',
'Item.Data must be longer than or equal to 2 characters',
'Item.Data must be a number string',
];
await target.transform(<ApiDTO>{}, metadata).catch((err) => {
expect(err.getResponse().message).toEqual(Expected);
});
});
});
The expect fails.
await target.transform(<ApiDTO>{}, metadata).catch((err) => {
expect(err.getResponse().message).toEqual(Expected);
});
The 2 FileName errors are returned, but no the Item.Data fields. Setting data: '', to be data: '{ "Item": {} }' also fails the same way.
Actual expectation failure:
expect(received).toEqual(expected) // deep equality
- Expected - 2
+ Received + 0
Array [
"FileName must be longer than or equal to 1 characters",
"FileName must be a string",
- "Item.Data must be longer than or equal to 2 characters",
- "Item.Data must be a number string",
]
This is indicating that the FileName validation is there, those 2 lines are returned, but the Item.Data errors, are not coming back, and are 'extra' in my test case results.
However, calling this via Postman, PUT /api/donotuse with the request body:
{ "Item": {} }
returns all 4 of the errors. The HTTP Status code is also a 400 Bad Request, as NestJS would normally return on its own. I am not sure what is wrong in my test case to get the errors to all be returned.
EDIT
I have also then tried to do this via E2E testing as the answer suggested, but I still receive the same missing errors.
describe('ApiDto - E2E', () => {
let app: INestApplication;
afterAll(async () => {
await app.close();
});
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
app.useGlobalPipes(new ValidationPipe({ transform: true, errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY }));
await app.init();
});
it('should validate the ApiDTO definition', async () => {
const APIRequestDTO: unknown = { FileName: null, Item: {} };
const ResponseData$ = await request(app.getHttpServer())
.put('/api/donotuse')
.set('Accept', 'application/json')
.send(APIRequestDTO as ApiDTO);
const Expected: string[] = [
'FileName must be longer than or equal to 1 characters',
'FileName must be a string',
'Item.Data must be longer than or equal to 2 characters',
'Item.Data must be a number string',
];
expect(ResponseData$.status).toBe( HttpStatus.UNPROCESSABLE_ENTITY);
expect(ResponseData$.body.message).toBe(Expected);
});
});
This still does not provide all the errors, that are properly returned from the Postman call. I am not sure what is happening during testing that the sub type is not processed. Calling this via Postman, same body, same headers, etc., does return the proper errors:
"message": [
"FileName must be longer than or equal to 1 characters",
"FileName must be a string",
"Item.Data must be longer than or equal to 2 characters",
"Item.Data must be a number string"
],
I know it is going into the ValidationPipe as well, as my custom error code, 422 Unprocessable Entity is returned, indicating this is the validation that is failing. This same error is returned in both my unit test and the E2E test, but not the second set of errors about Item.Data.
I assume that in your app you're registering the ValidationPipe globally, eg:
app.useGlobalPipes(new ValidationPipe());
Due to the location of where Global Pipes are registered, they will work when you execute an actual request against your backend but will not be picked up in tests. This is why you're seeing it working through Postman, but not through Jest.
If you want the Validation pipe to be used in your tests you will need to manually set it up like so:
// Probably in your beforeEach where you're setting up the test module
const app = moduleFixture.createNestApplication();
app.useGlobalPipes(new ValidationPipe());
Duplicate of How to apply Global Pipes during e2e tests
Hi and thanks in advance,
I've successfully setup JWT authentication using django-rest-framework-simplejwt and React but I'm still very confused about the advantages and specifically database hits.
I'm using simplejwt with ROTATE_REFRESH_TOKENS': True 'BLACKLIST_AFTER_ROTATION': True, when my access_token expire I ask for a new one through /api/token/refresh and it blacklist old tokens, I'm using axios interceptors to perform that automatically.
But in my understanding the benefits of JWt is that they are stateless, meaning I don't have to hit the user database table everytime I want to make an a request that needs authentication permission.
The problem is even with a simple view like this :
class IsConnecteddAPI(APIView):
permission_classes = [permissions.IsAuthenticated]
def get(self, request, *args, **kwargs):
data = "You seem to be connected"
return Response(data, status=status.HTTP_200_OK)
using django-silk I see that it still performs 1 query to my user table when I call it with a valid access token, is that normal ? If so why do we say that JWT are stateless ? I'm really confused.
That's my axios code if needed :
import axios from "axios";
const baseURL = "http://localhost:5000";
const axiosInstance = axios.create({
baseURL: baseURL,
timeout: 5000,
headers: {
Authorization: localStorage.getItem("accesstoken")
? "JWT " + localStorage.getItem("accesstoken")
: null,
"Content-Type": "application/json",
accept: "application/json",
},
});
const axioAnonymousInstance = axios.create({
baseURL: baseURL,
timeout: 5000,
headers: {
"Content-Type": "application/json",
accept: "application/json",
},
});
axiosInstance.interceptors.response.use(
(response) => {
return response;
},
async function (error) {
const originalRequest = error.config;
if (typeof error.response === "undefined") {
alert(
"A server/network error occurred. " +
"Looks like CORS might be the problem. " +
"Sorry about this - we will get it fixed shortly."
);
return Promise.reject(error);
}
if (
error.response.status === 401 &&
originalRequest.url === baseURL + "token/refresh/"
) {
window.location.href = "/login/";
return Promise.reject(error);
}
if (
error.response.data.code === "token_not_valid" &&
error.response.status === 401 &&
error.response.statusText === "Unauthorized"
) {
const refreshToken = localStorage.getItem("refreshtoken");
if (refreshToken) {
const tokenParts = JSON.parse(atob(refreshToken.split(".")[1]));
// exp date in token is expressed in seconds, while now() returns milliseconds:
const now = Math.ceil(Date.now() / 1000);
console.log(tokenParts.exp);
if (tokenParts.exp > now) {
return axioAnonymousInstance
.post("/api/token/refresh/", { refresh: refreshToken })
.then((response) => {
localStorage.setItem("accesstoken", response.data.access);
localStorage.setItem("refreshtoken", response.data.refresh);
axiosInstance.defaults.headers["Authorization"] =
"JWT " + response.data.access;
originalRequest.headers["Authorization"] =
"JWT " + response.data.access;
return axiosInstance(originalRequest);
})
.catch((err) => {
// redirect ro /login here if wanted
console.log("axios Safe Instance error");
console.log(err);
// window.location.href = "/login/";
});
} else {
console.log("Refresh token is expired", tokenParts.exp, now);
window.location.href = "/login/";
}
} else {
console.log("Refresh token not available.");
window.location.href = "/login/";
}
}
// specific error handling done elsewhere
return Promise.reject(error);
}
);
export { axiosInstance, axioAnonymousInstance };
( I know I shouldn't use localStorage but whatever )
and I would typically just call this function to make the simple request to the view written above :
const IsConnected = () => {
axiosInstance
.get("/api/is_connected/")
.then((response) => {
if (response.status === 200) {
console.log(response.data);
console.log("Is connected : CONNECTED ");
} else {
console.log("IS connected : not connected");
}
})
.catch((error) => {
console.log("Is connected : NOT CONNECTED");
console.log(error);
});
};
Without the specifics of the exact query hit your db, it's hard to tell what is happening (the db query must have originated from a middleware because there's nothing in your code that does it, and I suspect it's django's CsrfViewMiddleware). However, as for your question of JWT being stateless, I suggest you to take a look at the official introduction.
Basically, what happens with a JWT is that your server performs a signature verification on the token using your server's secret key (please beware of some problems). If the verification passes, then the data stored inside the JWT is trusted and read as is, which is why no database query is necessary. Of course, this does mean that your user will know exactly what is stored inside their token because the data is a simple base64 encoded JSON object.
I've been trying to solve this issue for days;
create the test for this case using mocha:
app.post('/approval', function(req, response){
request.post('https://git.ecommchannel.com/api/v4/users/' + req.body.content.id + '/' + req.body.content.state + '?private_token=blabla', function (error, resp, body) {
if (resp.statusCode == 201) {
//do something
} else {
response.send("failed"), response.end();
}
});
} else {
response.send("failed"), response.end();
}
});
});
I've tried several ways, using supertest to test the '/approval' and using nock to test the post request to git api. But it always turn "statusCode" is undefined. I think that's because the request to git api in index.js is not inside a certain function(?)
So I can't implement something like this :
https://codeburst.io/testing-mocking-http-requests-with-nock-480e3f164851 or
https://scotch.io/tutorials/nodejs-tests-mocking-http-requests
const nockingGit = () => {
nock('https://git.ecommchannel.com/api/v4/users')
.post('/1/yes', 'private_token=blabla')
.reply(201, { "statusCode": 201 });
};
it('approval', (done) => {
let req = {
content: {
id: 1,
state: 'yes'
},
_id: 1
}
request(_import.app)
.post('/approval')
.send(req)
.expect(200)
.expect('Content-Type', /html/)
.end(function (err, res) {
if (!err) {
nockingGit();
} else {
done(err);
}
});
done();
})
Then I tried to use supertest as promise
it('approve-block-using-promise', () => {
return promise(_import.app)
.post('/approval')
.send(req = {
content: {
id: 1,
state: 'yes'
},
_id: 1
})
.expect(200)
.then(function(res){
return promise(_import.app)
.post("https://git.ecommchannel.com/api/v4/users/")
.send('1/yes', 'private_token=blabla')
.expect(201);
})
})
But it gives error: ECONNEREFUSED: Connection refused. I didn't find any solution to solve the error. Some sources said that it needs done() .. but it gives another error message, 'ensure "done()" is called" >.<
So then I've found another way, using async (https://code-examples.net/en/q/141ce32)
it('should respond to only certain methods', function(done) {
async.series([
function(cb) { request(_import.app).post('/approval')
.send(req = {
content: {
id: 1,
state: 'yes'
},
_id: 1
})
.expect(200, cb); },
function(cb) { request(_import.app).post('/https://git.ecommchannel.com/api/v4/users/').send('1/yes', 'private_token=blabla').expect(201, cb); },
], done);
});
and it gives this error : expected 201 "Created", got 404 "Not Found". Well, if I open https://git.ecommchannel.com/api/v4/users/1/yes?private_token=blabla in the browser it does return 404. But what I expect is I've injected the response to 201 from the unit test; so whatever the actual response is, the statusCode suppose to be 201, right?
But then since it gives that error, is it means the unit test really send the request to the api?
Pls help me to solve this; how to test the first code I shared.
I really new into unit test.
There are a few things wrong with your posted code, I'll try to list them out but I'm also including a full, passing example below.
First off, your call to git.ecommchannel in the controller, it's a POST with no body. While this isn't causing the errors you're seeing and is technically not incorrect, it is odd. So you should double check what the data you should be sending is.
Next, I'm assuming this was a copy/paste issue when you created the question, but the callback for the request in your controller is not valid JS. The brackets don't match up and the send "failed" is there twice.
Your Nock setup had two issues. First the argument to nock should only have origin, none of the path. So /api/v4/users had to be moved into the first argument of the post method. The other issue was with the second argument passed to post that is an optional match of the POST body. As stated above, you aren't currently sending a body so Nock will always fail to match and replace that request. In the example below, the private_token has been moved to match against the query string of the request, as that what was shown as happening.
The calling of nockingGit was happening too late. Nock needs to register the mock before you use Supertest to call your Express app. You have it being called in the end method, by that time it's too late.
The test labeled approve-block-using-promise has an issue with the second call to the app. It's calling post via Supertest on the Express app, however, the first argument to that post method is the path of the request you're making to your app. It has nothing to do with the call to git.ecommchannel. So in that case your Express app should have returned a 404 Not Found.
const express = require('express')
const nock = require('nock')
const request = require('request')
const supertest = require('supertest')
const app = express()
app.use(express.json())
app.post('/approval', function(req, response) {
const url = 'https://git.ecommchannel.com/api/v4/users/' + req.body.content.id + '/' + req.body.content.state
request.post({
url,
qs: {private_token: 'blabla'}
// body: {} // no body?
},
function(error, resp, body) {
if (error) {
response.status(500).json({message: error.message})
} else if (resp.statusCode === 201) {
response.status(200).send("OK")
} else {
response.status(500).send("failed").end();
}
});
});
const nockingGit = () => {
nock('https://git.ecommchannel.com')
.post('/api/v4/users/1/yes')
.query({private_token: 'blabla'})
.reply(201, {"data": "hello world"});
};
it('approval', (done) => {
const reqPayload = {
content: {
id: 1,
state: 'yes'
},
_id: 1
}
nockingGit();
supertest(app)
.post('/approval')
.send(reqPayload)
.expect(200)
.expect('Content-Type', /html/)
.end(function(err) {
done(err);
})
})
[This is a Vue app, using Vuex, created with vue-cli, using mocha, chai, karma, sinon]
I'm trying to create tests for my vuex state and I DON'T want to use a mock -- one of my big goals for these tests is to also test the API that data is coming from.
I am trying to follow the docs for chai-as-promised.
This is a simplification of the vuex action I'm trying to test:
const actions = {
login: (context, payload) => {
context.commit('setFlashMessage', "");
axios.get("https://first-api-call")
.then((response) => {
axios.post("https://second-api-call")
.then((response) => {
router.push({ name: "Home"});
context.commit('setFlashMessage', "Logged in successfully");
context.commit('setLogin', response.data);
});
},
Notice that the login action has two promises and doesn't return anything. The login action does two things: it sets some state and it changes the route.
The example that I've seen that using chai-as-promised expects that the promise is returned. That is:
var result = systemUnderTest();
return expect(result).to.eventually.equal(blah);
But in my case, login() doesn't return anything, and I'm not sure what I would return if it did.
This is what I have so far:
import store from '#/src/store/store'
describe('login', () => {
it('bad input', () => {
store.login({ username: "abcd", password: ""});
// What is the test I should use?
}
}
I would return the login response message and make two tests. One to make sure that invalid credentials return a failure message and one to make sure that valid credentials login successfully
My co-worker and I came up with the solution:
The vuex action needs to return the promise, and they can be chained together:
login: (context, payload) => {
context.commit('setFlashMessage', "");
return axios.get("https://first-api-call")
.then((response) => {
return axios.post("https://second-api-call")
})
.then((response) => {
// etc...
router.push({ name: "Home"});
context.commit('setFlashMessage', "Logged in successfully");
context.commit('setLogin', response.data);
return {status: "success"};
});
},
Then we didn't need chai-as-promised because the test looks like this:
it('bad password', () => {
const result = store.dispatch("login", { username: userName, password: password + "bad" });
return result.then((response) => {
expect(response).to.deep.equal({ status: "failed"});
store.getters.getFlashMessage.should.equal("Error logging in");
});
});