Testing Authenticated Endpoints

Welcome to the final chapter of our journey through automated API testing with Dart. In this lesson, we'll focus on testing authenticated API endpoints. While you've already learned to organize tests and handle CRUD operations using Dart's test package, today we'll delve into how APIs manage secure access through different authentication methods — API Keys, Sessions, and JWT (JSON Web Tokens).

Authentication is crucial for securing API endpoints against unauthorized access. By understanding how to test these mechanisms, you'll ensure that your API maintains its integrity and protects sensitive data. We'll explore practical examples for each authentication method, giving you the tools to verify that only authorized users can interact with protected resources.

API Key Authentication

API Key authentication is one of the simplest ways to secure an API. It involves sending a unique key as part of the request headers, allowing access to protected endpoints. Let's look at an example of how you can set up and test API Key authentication using Dart:

Dart
1import 'package:http/http.dart' as http; 2import 'package:test/test.dart'; 3 4void main() { 5 final String baseUrl = "http://localhost:8000"; 6 final String apiKey = "123e4567-e89b-12d3-a456-426614174000"; 7 8 test('API Key authentication', () async { 9 // Arrange 10 final headers = {"X-API-Key": apiKey}; 11 12 // Act 13 final response = await http.get(Uri.parse('$baseUrl/todos'), headers: headers); 14 15 // Assert 16 expect(response.statusCode, equals(200)); 17 }); 18}

Here, we define a test using Dart's test package. Within the test, we specify the headers, including the X-API-Key. We use the http.get method to make a call to the /todos endpoint and pass the headers, ensuring the API key is included in the request. The test then asserts that the response status code is 200, indicating successful authentication and access to the endpoint.

Session Authentication: Setup

To facilitate session-based authentication, we create a login helper function. This function is responsible for sending a POST request to the login endpoint with user credentials and extracting the session ID from the response headers. This allows each test to authenticate independently, ensuring that tests do not interfere with each other and providing a consistent environment for verifying both login functionality and access to protected endpoints:

Dart
1import 'dart:convert'; 2import 'package:http/http.dart' as http; 3import 'package:test/test.dart'; 4 5void main() { 6 final String baseUrl = "http://localhost:8000"; 7 final authDetails = { 8 "username": "testuser", 9 "password": "testpass123" 10 }; 11 12 Future<String> login(Map<String, String> credentials) async { 13 final response = await http.post( 14 Uri.parse('$baseUrl/auth/login'), 15 headers: {"Content-Type": "application/json"}, 16 body: jsonEncode(credentials), 17 ); 18 if (response.statusCode != 200) { 19 throw Exception("Login failed"); 20 } 21 22 // Extract the session ID from the 'set-cookie' header 23 final sessionId = response.headers['set-cookie']; 24 return sessionId ?? ""; 25 }

This function is designed to be reusable across multiple tests, ensuring that each test can independently authenticate and manage session IDs without relying on the results of previous tests.

Session Authentication: Testing Login

In this test, we utilize the login function to authenticate independently, verifying that user credentials successfully create a session and extract the session ID:

Dart
1test('Session Login', () async { 2 // Arrange & Act 3 final sessionId = await login(authDetails); 4 5 // Assert 6 expect(sessionId, isNotEmpty); 7});

By using the login function, this test ensures isolated verification of login success, confirming that the session is established correctly and the session ID is extracted.

Session Authentication: Testing Protected Endpoint

In this test, we independently authenticate using the login function, extract the session ID, and confirm that an established session grants access to a protected endpoint:

Dart
1test('Session Protected Route', () async { 2 // Arrange 3 final sessionId = await login(authDetails); 4 5 // Act 6 final headers = {"Cookie": sessionId}; 7 final response = await http.get(Uri.parse('$baseUrl/todos'), headers: headers); 8 9 // Assert 10 expect(response.statusCode, equals(200)); 11});

The login function's strategic use ensures that each test individually verifies session-based access, unaffected by the results of prior tests.

Session Authentication: Testing Logout

By using the login function, this test independently authenticates, extracts the session ID, and then verifies the ability to terminate a session, ensuring logout functionality is effective:

Dart
1test('Session Logout', () async { 2 // Arrange 3 final sessionId = await login(authDetails); 4 5 // Act 6 final headers = {"Cookie": sessionId}; 7 final logoutResponse = await http.post(Uri.parse('$baseUrl/auth/logout'), headers: headers); 8 9 // Assert 10 expect(logoutResponse.statusCode, equals(200)); 11});

This approach confirms isolation, as each session is independently managed, ensuring logout integrity without relying on previous tests.

JWT Authentication: Setup

For JWT-based authentication, we create a jwtLogin helper function. This function sends a POST request to the login endpoint with user credentials and retrieves the JWT tokens. This setup allows each test to independently obtain and manage JWTs, ensuring that tests are not dependent on the order or success of other tests:

Dart
1import 'dart:convert'; 2import 'package:http/http.dart' as http; 3import 'package:test/test.dart'; 4 5void main() { 6 final String baseUrl = "http://localhost:8000"; 7 final authDetails = { 8 "username": "testuser", 9 "password": "testpass123" 10 }; 11 12 Future<Map<String, dynamic>> jwtLogin(Map<String, String> credentials) async { 13 final response = await http.post( 14 Uri.parse('$baseUrl/auth/login'), 15 headers: {"Content-Type": "application/json"}, 16 body: jsonEncode(credentials), 17 ); 18 if (response.statusCode != 200) { 19 throw Exception("Login failed"); 20 } 21 return jsonDecode(response.body); 22 }

The jwtLogin function is crafted to be reusable, allowing each test to independently authenticate and retrieve tokens.

JWT Authentication: Testing Login

Using the jwtLogin function, this test independently verifies that successful authentication results in token issuance, forming the basis for accessing protected resources:

Dart
1test('JWT Login', () async { 2 // Act 3 final tokens = await jwtLogin(authDetails); 4 5 // Assert 6 final accessToken = tokens['access_token']; 7 expect(accessToken, isNotNull); 8 final refreshToken = tokens['refresh_token']; 9 expect(refreshToken, isNotNull); 10});

By independently logging in, the test ensures token validity, establishing the foundation for secure access to protected endpoints.

JWT Authentication: Testing Protected Endpoint

This test independently authenticates using the jwtLogin function and verifies that a JWT access token provides entry to a protected endpoint:

Dart
1test('JWT Protected Route', () async { 2 // Act 3 final tokens = await jwtLogin(authDetails); 4 final accessToken = tokens['access_token']; 5 6 // Access a protected endpoint 7 final headers = {"Authorization": "Bearer $accessToken"}; 8 final response = await http.get(Uri.parse('$baseUrl/todos'), headers: headers); 9 10 // Assert 11 expect(response.statusCode, equals(200)); 12});

By ensuring the test is independent, this approach validates the token's effectiveness in protecting resources, confirming stateless authentication.

JWT Authentication: Testing Logout

Finally, this test independently authenticates to verify that access and refresh tokens can be invalidated, securing the system against unauthorized reuse:

Dart
1test('JWT Logout', () async { 2 // Act 3 final tokens = await jwtLogin(authDetails); 4 final accessToken = tokens['access_token']; 5 final refreshToken = tokens['refresh_token']; 6 7 final headers = { 8 "Authorization": "Bearer $accessToken", 9 "Content-Type": "application/json" 10 }; 11 12 // Logout 13 final logoutResponse = await http.post( 14 Uri.parse('$baseUrl/auth/logout'), 15 headers: headers, 16 body: jsonEncode({"refresh_token": refreshToken}), 17 ); 18 19 // Assert 20 expect(logoutResponse.statusCode, equals(200)); 21});

This test confirms the secure handling and invalidation of tokens, ensuring each test independently checks the integrity of the logout process.

Summary and Practices

Throughout this lesson, we've explored different methods of API authentication: API Keys, Sessions, and JWTs. Each method has distinct ways to test and secure access, and you've seen how to implement tests for them effectively using Dart.

When testing any form of authentication, it’s crucial to handle API credentials with care. Avoid hardcoding sensitive information in your codebase, and consider using environment variables or secure vaults in production environments.

As we conclude this lesson and the course, take pride in the journey you've completed. You've gained a comprehensive understanding of how to automate API tests using Dart, mastering basic requests through to secure, authenticated endpoints. Continue to practice and explore, applying these skills to ensure robust, reliable APIs in your projects. Congratulations on reaching the end of this course!

Sign up
Join the 1M+ learners on CodeSignal
Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal