Welcome to the final lesson of our journey through automated API testing with Swift. In this lesson, we'll focus on testing authenticated API endpoints using Swift's networking capabilities and the XCTest
framework. 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 — API Keys, Sessions, and JWT (JSON Web Tokens) — giving you the tools to verify that only authorized users can interact with protected resources.
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 Swift:
Swift1import XCTest
2import FoundationNetworking
3
4class TestAPIKeyAuthentication: XCTestCase {
5
6 let baseURL = "http://localhost:8000"
7 let apiKey = ProcessInfo.processInfo.environment["API_KEY"] ?? ""
8
9 func testAPIKeyAuthentication() {
10 // Arrange
11 let url = URL(string: "\(self.baseURL)/todos")!
12 let request = URLRequest(url: url)
13 request.addValue(apiKey, forHTTPHeaderField: "X-API-Key")
14
15 // Act
16 let expectation = self.expectation(description: "API Key Authentication")
17 let task = URLSession.shared.dataTask(with: request) { data, response, error in
18 // Assert
19 if let httpResponse = response as? HTTPURLResponse {
20 XCTAssertEqual(httpResponse.statusCode, 200)
21 }
22 expectation.fulfill()
23 }
24 task.resume()
25 waitForExpectations(timeout: 5, handler: nil)
26 }
27}
Here, we define a test class TestAPIKeyAuthentication
. Within the test method, testAPIKeyAuthentication
, we specify the headers, including the X-API-Key
. We use URLSession
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.
For session-based authentication, we establish a method for user credentials and a helper method for login. This enables each test to authenticate independently, providing a consistent environment to test both login functionality and access to protected endpoints:
Swift1import XCTest
2import FoundationNetworking
3
4class TestSessionAuthentication: XCTestCase {
5
6 let baseURL = "http://localhost:8000"
7
8 func authDetails() -> [String: String] {
9 // Arrange - Providing user credentials from environment variables
10 return [
11 "username": ProcessInfo.processInfo.environment["USERNAME"] ?? "",
12 "password": ProcessInfo.processInfo.environment["PASSWORD"] ?? ""
13 ]
14 }
15
16 // Helper method for sending a login POST request
17 func login(session: URLSession, authDetails: [String: String], completion: @escaping (HTTPURLResponse?) -> Void) {
18 let request = URLRequest(url: URL(string: "\(self.baseURL)/auth/login")!)
19 request.httpMethod = "POST"
20 request.httpBody = try? JSONSerialization.data(withJSONObject: authDetails, options: [])
21 request.addValue("application/json", forHTTPHeaderField: "Content-Type")
22
23 let task = session.dataTask(with: request) { data, response, error in
24 completion(response as? HTTPURLResponse)
25 }
26 task.resume()
27 }
28}
The authDetails
method supplies the required credentials, and the login
helper method facilitates independent authentication for each test.
This test leverages the login
helper method to authenticate independently, validating that user credentials create a session successfully:
Swift1func testLogin() {
2 // Arrange - Initialize session
3 let session = URLSession.shared
4
5 // Act - Execute the login process using the helper method
6 let expectation = self.expectation(description: "Login")
7 login(session: session, authDetails: authDetails()) { response in
8 // Assert - Verify that the login was successful
9 XCTAssertEqual(response?.statusCode, 200)
10 expectation.fulfill()
11 }
12 waitForExpectations(timeout: 5, handler: nil)
13}
The use of the helper ensures that this test does not rely on any previous operations, providing isolated verification of login success via status code.
In this test, we independently authenticate using the helper method before confirming that an established session grants access to a protected endpoint:
Swift1func testAccessWithSession() {
2 // Arrange - Use the login helper method to authenticate
3 let session = URLSession.shared
4 let expectation = self.expectation(description: "Access with Session")
5
6 login(session: session, authDetails: authDetails()) { response in
7 guard response?.statusCode == 200 else {
8 XCTFail("Login failed")
9 expectation.fulfill()
10 return
11 }
12
13 // Act - Request the protected resource using the session
14 let request = URLRequest(url: URL(string: "\(self.baseURL)/todos")!)
15 let task = session.dataTask(with: request) { data, response, error in
16 // Assert - Confirm access is granted with a successful status code
17 if let httpResponse = response as? HTTPURLResponse {
18 XCTAssertEqual(httpResponse.statusCode, 200)
19 }
20 expectation.fulfill()
21 }
22 task.resume()
23 }
24 waitForExpectations(timeout: 5, handler: nil)
25}
The helper method's strategic use ensures that each test individually verifies session-based access, unaffected by the results of prior tests.
By using the login helper method, this test independently authenticates and then verifies the ability to terminate a session, ensuring logout functionality is effective:
Swift1func testLogoutWithSession() {
2 // Arrange - Use the login helper method to authenticate
3 let session = URLSession.shared
4 let expectation = self.expectation(description: "Logout with Session")
5
6 login(session: session, authDetails: authDetails()) { response in
7 guard response?.statusCode == 200 else {
8 XCTFail("Login failed")
9 expectation.fulfill()
10 return
11 }
12
13 // Act - Perform a logout operation to terminate the session
14 let request = URLRequest(url: URL(string: "\(self.baseURL)/auth/logout")!)
15 request.httpMethod = "POST"
16 let task = session.dataTask(with: request) { data, response, error in
17 // Assert - Verify logout success via status code
18 if let httpResponse = response as? HTTPURLResponse {
19 XCTAssertEqual(httpResponse.statusCode, 200)
20 }
21 expectation.fulfill()
22 }
23 task.resume()
24 }
25 waitForExpectations(timeout: 5, handler: nil)
26}
This approach confirms isolation, as each session is independently managed, ensuring logout integrity without relying on previous tests.
Unlike session-based authentication, where the server maintains session state, JWT authentication is stateless. The server does not store user session information; instead, all authentication details are embedded within the token itself.
For JWT-based authentication, we replicate this strategy with a method for credentials and a login helper method, enabling each test to independently obtain and manage JWTs:
Swift1import XCTest
2import FoundationNetworking
3
4class TestJWTAuthentication: XCTestCase {
5
6 let baseURL = "http://localhost:8000"
7
8 func authDetails() -> [String: String] {
9 // Arrange - User credentials for JWT from environment variables
10 return [
11 "username": ProcessInfo.processInfo.environment["USERNAME"] ?? "",
12 "password": ProcessInfo.processInfo.environment["PASSWORD"] ?? ""
13 ]
14 }
15
16 // Helper method for sending a login POST request
17 func login(authDetails: [String: String], completion: @escaping (HTTPURLResponse?, [String: Any]?) -> Void) {
18 let request = URLRequest(url: URL(string: "\(self.baseURL)/auth/login")!)
19 request.httpMethod = "POST"
20 request.httpBody = try? JSONSerialization.data(withJSONObject: authDetails, options: [])
21 request.addValue("application/json", forHTTPHeaderField: "Content-Type")
22
23 let task = URLSession.shared.dataTask(with: request) { data, response, error in
24 let json = try? JSONSerialization.jsonObject(with: data ?? Data(), options: []) as? [String: Any]
25 completion(response as? HTTPURLResponse, json)
26 }
27 task.resume()
28 }
29}
This setup empowers each test to authenticate independently, retrieving tokens without relying on the order or success of other tests.
Using the login
helper method, this test independently verifies that successful authentication results in token issuance, forming the basis for accessing protected resources:
Swift1func testLogin() {
2 // Act - Authenticate and retrieve JWT tokens
3 let expectation = self.expectation(description: "JWT Login")
4 login(authDetails: authDetails()) { response, json in
5 // Assert - Confirm receipt of tokens and check their existence
6 XCTAssertEqual(response?.statusCode, 200)
7 XCTAssertNotNil(json?["access_token"])
8 XCTAssertNotNil(json?["refresh_token"])
9 expectation.fulfill()
10 }
11 waitForExpectations(timeout: 5, handler: nil)
12}
By independently logging in, the test ensures token validity regardless of prior tests, establishing the foundation for secure access.
This test independently authenticates using the helper method and verifies that a JWT access token provides entry to a protected endpoint:
Swift1func testAccessWithJWT() {
2 // Arrange - Authenticate and obtain an access token using the helper method
3 let expectation = self.expectation(description: "Access with JWT")
4 login(authDetails: authDetails()) { response, json in
5 guard let accessToken = json?["access_token"] as? String else {
6 XCTFail("Failed to obtain access token")
7 expectation.fulfill()
8 return
9 }
10
11 // Act - Use the access token to request a protected endpoint
12 let request = URLRequest(url: URL(string: "\(self.baseURL)/todos")!)
13 request.addValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")
14 let task = URLSession.shared.dataTask(with: request) { data, response, error in
15 // Assert - Confirm the request was successful by checking the status code
16 if let httpResponse = response as? HTTPURLResponse {
17 XCTAssertEqual(httpResponse.statusCode, 200)
18 }
19 expectation.fulfill()
20 }
21 task.resume()
22 }
23 waitForExpectations(timeout: 5, handler: nil)
24}
By ensuring the test is independent, this approach validates the token's effectiveness in protecting resources, confirming stateless authentication.
Finally, this test independently authenticates to verify that access and refresh tokens can be invalidated, securing the system against unauthorized reuse:
Swift1func testJWTLogout() {
2 // Arrange - Authenticate and prepare tokens using the helper method
3 let expectation = self.expectation(description: "JWT Logout")
4 login(authDetails: authDetails()) { response, json in
5 guard let accessToken = json?["access_token"] as? String,
6 let refreshToken = json?["refresh_token"] as? String else {
7 XCTFail("Failed to obtain tokens")
8 expectation.fulfill()
9 return
10 }
11
12 let request = URLRequest(url: URL(string: "\(self.baseURL)/auth/logout")!)
13 request.httpMethod = "POST"
14 request.addValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")
15 request.httpBody = try? JSONSerialization.data(withJSONObject: ["refresh_token": refreshToken], options: [])
16 request.addValue("application/json", forHTTPHeaderField: "Content-Type")
17
18 // Act - Use access and refresh tokens to process logout
19 let task = URLSession.shared.dataTask(with: request) { data, response, error in
20 // Assert - Verify logout success through confirmation of status code
21 if let httpResponse = response as? HTTPURLResponse {
22 XCTAssertEqual(httpResponse.statusCode, 200)
23 }
24 expectation.fulfill()
25 }
26 task.resume()
27 }
28 waitForExpectations(timeout: 5, handler: nil)
29}
This test confirms the secure handling and invalidation of tokens, ensuring each test independently checks the integrity of the logout process.
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 Swift and XCTest
.
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 Swift, 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!
