Before we begin testing our API endpoints, we need to set up our testing environment properly. This setup will ensure we can test efficiently and systematically.
Let's create a complete testing environment in Postman that we'll use throughout our testing:
baseUrl: http://localhost:8000
xsrfToken: [leave empty initially]
authToken: [leave empty initially]
pm.sendRequest({
url: pm.environment.get("baseUrl") + "/api/csrf/restore",
method: 'GET'
}, function (err, res) {
if (!err) {
pm.environment.set("xsrfToken", res.cookies.get("XSRF-TOKEN"));
}
});
For cURL testing, we'll need to store our CSRF token for repeated use. Let's create a script to help with this:
#!/bin/bash
# Save as get-csrf.sh
# Get CSRF token and save it
TOKEN=$(curl -s -X GET http://localhost:8000/api/csrf/restore \
--cookie-jar cookies.txt \
| grep -o '"XSRF-Token":"[^"]*' \
| cut -d'"' -f4)
echo "CSRF Token: $TOKEN"
echo "Token saved to cookies.txt"
Make the script executable:
chmod +x get-csrf.sh
{
"firstName": "John",
"lastName": "Smith",
"email": "john.smith@gmail.com",
"username": "JohnSmith",
"password": "secret password"
}
pm.test("Status code is 201", function () {
pm.response.to.have.status(201);
});
pm.test("User is created successfully", function () {
const responseJson = pm.response.json();
pm.expect(responseJson.user).to.have.property('id');
pm.expect(responseJson.user.username).to.eql('JohnSmith');
});
if (pm.response.code === 201) {
pm.environment.set("authToken", pm.response.headers.get("Set-Cookie"));
}
./get-csrf.sh
cat > register.json << EOF
{
"firstName": "John",
"lastName": "Smith",
"email": "john.smith@gmail.com",
"username": "JohnSmith",
"password": "secret password"
}
EOF
curl -X POST http://localhost:8000/api/users \
-H "Content-Type: application/json" \
-H "XSRF-TOKEN: $TOKEN" \
-b cookies.txt \
-d @register.json
Test each validation rule using both Postman and cURL:
Postman:
{
"firstName": "John",
"lastName": "Smith",
"email": "invalid-email",
"username": "JohnSmith",
"password": "secret password"
}
cURL:
curl -X POST http://localhost:8000/api/users \
-H "Content-Type: application/json" \
-H "XSRF-TOKEN: $TOKEN" \
-b cookies.txt \
-d '{"firstName":"John","lastName":"Smith","email":"invalid-email","username":"JohnSmith","password":"secret password"}'
{
"credential": "john.smith@gmail.com",
"password": "secret password"
}
pm.test("Status code is 200", function () {
pm.response.to.have.status(200);
});
pm.test("Login successful", function () {
const responseJson = pm.response.json();
pm.expect(responseJson.user).to.have.property('id');
pm.expect(responseJson.user.email).to.eql('john.smith@gmail.com');
});
if (pm.response.code === 200) {
pm.environment.set("authToken", pm.response.headers.get("Set-Cookie"));
}
cat > login.json << EOF
{
"credential": "john.smith@gmail.com",
"password": "secret password"
}
EOF
curl -X POST http://localhost:8000/api/session \
-H "Content-Type: application/json" \
-H "XSRF-TOKEN: $TOKEN" \
-b cookies.txt \
-c cookies.txt \
-d @login.json
pm.test("Status code is 200", function () {
pm.response.to.have.status(200);
});
pm.test("Session user data is correct", function () {
const responseJson = pm.response.json();
pm.expect(responseJson.user).to.not.be.null;
pm.expect(responseJson.user).to.have.property('email');
});
curl http://localhost:8000/api/session \
-b cookies.txt
pm.test("Status code is 200", function () {
pm.response.to.have.status(200);
});
pm.test("Logout successful", function () {
const responseJson = pm.response.json();
pm.expect(responseJson.message).to.eql('success');
});
pm.environment.set("authToken", null);
curl -X DELETE http://localhost:8000/api/session \
-H "XSRF-TOKEN: $TOKEN" \
-b cookies.txt
Now that we understand how to test each endpoint individually, let's create a complete test flow that simulates a user's journey through our application.
#!/bin/bash
# Save as test-flow.sh
echo "Starting API test flow..."
# Get CSRF token
./get-csrf.sh
# Register new user
echo "Testing registration..."
curl -X POST http://localhost:8000/api/users \
-H "Content-Type: application/json" \
-H "XSRF-TOKEN: $TOKEN" \
-b cookies.txt \
-c cookies.txt \
-d @register.json
# Login
echo "Testing login..."
curl -X POST http://localhost:8000/api/session \
-H "Content-Type: application/json" \
-H "XSRF-TOKEN: $TOKEN" \
-b cookies.txt \
-c cookies.txt \
-d @login.json
# Check current session
echo "Testing current session..."
curl http://localhost:8000/api/session \
-b cookies.txt
# Logout
echo "Testing logout..."
curl -X DELETE http://localhost:8000/api/session \
-H "XSRF-TOKEN: $TOKEN" \
-b cookies.txt
echo "API test flow completed."
Make the script executable:
chmod +x test-flow.sh
Proper error handling is crucial for a robust API. Let's systematically test error scenarios to ensure our API handles them gracefully.
Postman:
{
"credential": "john.smith@gmail.com",
"password": "wrong_password"
}
pm.test("Invalid credentials return 401", function () {
pm.response.to.have.status(401);
const responseJson = pm.response.json();
pm.expect(responseJson).to.have.property('message');
});
cURL:
curl -X POST http://localhost:8000/api/session \
-H "Content-Type: application/json" \
-H "XSRF-TOKEN: $TOKEN" \
-b cookies.txt \
-d '{"credential":"john.smith@gmail.com","password":"wrong_password"}'
Postman:
pm.test("Not found returns 404", function () {
pm.response.to.have.status(404);
});
cURL:
curl -i http://localhost:8000/api/nonexistent-endpoint
Postman:
pm.test("Unauthorized returns 401", function () {
pm.response.to.have.status(401);
});
cURL:
# First make sure we're logged out
curl -X DELETE http://localhost:8000/api/session -H "XSRF-TOKEN: $TOKEN" -b cookies.txt
# Then try to access a protected resource
curl -i http://localhost:8000/api/protected-route
Postman:
{
"firstName": "John",
"lastName": "Smith",
"email": "john.smith@gmail.com",
invalid format here
}
pm.test("Invalid format returns 400", function () {
pm.response.to.have.status(400);
});
cURL:
curl -X POST http://localhost:8000/api/users \
-H "Content-Type: application/json" \
-H "XSRF-TOKEN: $TOKEN" \
-b cookies.txt \
-d '{invalid json format}'
Security is paramount for any API. Let's test common security concerns to ensure our API is protected against common vulnerabilities.
Test that requests without CSRF tokens are rejected:
Postman:
pm.test("Missing CSRF token returns 403", function () {
pm.response.to.have.status(403);
});
cURL:
curl -X POST http://localhost:8000/api/session \
-H "Content-Type: application/json" \
-b cookies.txt \
-d @login.json
Test that only authenticated users can access protected routes:
Postman:
pm.test("Protected route requires authentication", function () {
// When unauthenticated
pm.response.to.have.status(401);
});
cURL:
# Clear cookies to ensure we're not authenticated
rm cookies.txt
# Attempt to access protected route
curl -i http://localhost:8000/api/protected-route
Understanding how your API performs under load is crucial for production readiness.
In Postman, you can measure response times for each request:
pm.test("Response time is acceptable", function () {
pm.expect(pm.response.responseTime).to.be.below(500);
});
Test how your API handles multiple concurrent requests:
# Install Apache Bench if not already installed
# For Ubuntu/Debian:
# sudo apt-get install apache2-utils
# For basic load test (10 concurrent users, 100 total requests):
ab -n 100 -c 10 http://localhost:8000/api/session
# For testing with authentication:
ab -n 100 -c 10 -C "yourCookieName=yourCookieValue" http://localhost:8000/api/protected-route
Set up continuous monitoring to track API performance over time:
#!/bin/bash
# Save as monitor.sh
while true; do
echo "$(date): Testing API performance..."
# Test login endpoint
start=$(date +%s%N)
curl -s -o /dev/null -w "%{http_code}" -X POST http://localhost:8000/api/session \
-H "Content-Type: application/json" \
-H "XSRF-TOKEN: $TOKEN" \
-b cookies.txt \
-d @login.json
end=$(date +%s%N)
# Calculate duration in milliseconds
duration=$(( (end - start) / 1000000 ))
echo "Login response time: ${duration}ms"
sleep 60 # Wait for 1 minute before next test
done
To ensure consistent testing, let's set up automated testing that can be integrated into your CI/CD pipeline.
Create a testing framework using Jest:
npm install --save-dev jest supertest
const request = require('supertest');
const app = require('../app'); // Your Express app
describe('API Authentication Tests', () => {
let authCookie;
let csrfToken;
// Before tests, get CSRF token
beforeAll(async () => {
const res = await request(app).get('/api/csrf/restore');
csrfToken = res.body.csrfToken;
});
// Test user registration
test('User registration', async () => {
const res = await request(app)
.post('/api/users')
.set('XSRF-TOKEN', csrfToken)
.send({
firstName: 'Test',
lastName: 'User',
email: 'test.user@example.com',
username: 'TestUser',
password: 'testpassword'
});
expect(res.statusCode).toBe(201);
expect(res.body.user).toHaveProperty('id');
expect(res.body.user.username).toBe('TestUser');
// Store cookie for future requests
authCookie = res.headers['set-cookie'];
});
// Test user login
test('User login', async () => {
const res = await request(app)
.post('/api/session')
.set('XSRF-TOKEN', csrfToken)
.send({
credential: 'test.user@example.com',
password: 'testpassword'
});
expect(res.statusCode).toBe(200);
expect(res.body.user).toHaveProperty('email');
expect(res.body.user.email).toBe('test.user@example.com');
// Update auth cookie
authCookie = res.headers['set-cookie'];
});
// Test get session
test('Get current session', async () => {
const res = await request(app)
.get('/api/session')
.set('Cookie', authCookie);
expect(res.statusCode).toBe(200);
expect(res.body.user).not.toBeNull();
});
// Test logout
test('User logout', async () => {
const res = await request(app)
.delete('/api/session')
.set('XSRF-TOKEN', csrfToken)
.set('Cookie', authCookie);
expect(res.statusCode).toBe(200);
expect(res.body.message).toBe('success');
});
});
"scripts": {
"test": "jest"
}
npm test
Example GitHub Actions workflow file (.github/workflows/api-tests.yml):
name: API Tests
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:13
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: express_api_test
ports:
- 5432:5432
options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
steps:
- uses: actions/checkout@v2
- name: Use Node.js
uses: actions/setup-node@v2
with:
node-version: '16'
- name: Install dependencies
run: npm ci
- name: Setup Database
run: npm run db:setup:test
- name: Run API tests
run: npm test
Symptoms: 403 Forbidden errors when sending POST/PUT/DELETE requests.
Solutions:
Symptoms: 401 Unauthorized errors when accessing protected routes.
Solutions:
Symptoms: 429 Too Many Requests errors during load testing.
Solutions:
A comprehensive testing strategy is essential for building reliable APIs. By following the procedures in this guide, you'll be able to thoroughly test your Express API's authentication flow, error handling, security, and performance characteristics.
Remember that testing is an ongoing process. As you add new features or modify existing ones, update your test suite accordingly to ensure continued reliability and performance.