Skip to content

Commit

Permalink
[PM-9842] Verify email token services (#849)
Browse files Browse the repository at this point in the history
  • Loading branch information
andrebispo5 committed Aug 20, 2024
1 parent 94ea62e commit 0c98785
Show file tree
Hide file tree
Showing 7 changed files with 223 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import Foundation
import Networking

// MARK: - VerifyEmailTokenRequestRequestModel

/// The data to include in the body of a `VerifyEmailTokenRequestRequest`.
///
struct VerifyEmailTokenRequestModel: Equatable {
// MARK: Properties

/// The email being used to create the account.
let email: String

/// The token used to verify the email.
let emailVerificationToken: String
}

// MARK: JSONRequestBody

extension VerifyEmailTokenRequestModel: JSONRequestBody {
static let encoder = JSONEncoder()
}
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,13 @@ protocol AccountAPIService {
///
func updateTempPassword(_ requestModel: UpdateTempPasswordRequestModel) async throws

/// Verify if the verification token received by email is still valid.
///
/// - Parameter email: The email being used to create the account.
/// - Parameter emailVerificationToken: The token used to verify the email.
///
func verifyEmailToken(email: String, emailVerificationToken: String) async throws

/// Verifies that the entered one-time password matches the one sent to the user.
///
/// - Parameter otp: The user's one-time password to verify.
Expand Down Expand Up @@ -192,6 +199,16 @@ extension APIService: AccountAPIService {
_ = try await apiService.send(UpdateTempPasswordRequest(requestModel: requestModel))
}

func verifyEmailToken(email: String, emailVerificationToken: String) async throws {
let request = VerifyEmailTokenRequest(
requestModel: VerifyEmailTokenRequestModel(
email: email,
emailVerificationToken: emailVerificationToken
)
)
_ = try await identityService.send(request)
}

func verifyOtp(_ otp: String) async throws {
_ = try await apiService.send(VerifyOtpRequest(requestModel: VerifyOtpRequestModel(otp: otp)))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import XCTest

// MARK: - AccountAPIServiceTests

// swiftlint:disable file_length
class AccountAPIServiceTests: BitwardenTestCase { // swiftlint:disable:this type_body_length
// MARK: Properties

Expand Down Expand Up @@ -386,6 +387,36 @@ class AccountAPIServiceTests: BitwardenTestCase { // swiftlint:disable:this type
XCTAssertEqual(client.requests[0].url.absoluteString, "https://example.com/api/accounts/update-temp-password")
}

/// `verifyEmailToken()` performs a request to verify if the verification token received by email is still valid.
func test_verifyEmailToken() async throws {
client.result = .httpSuccess(testData: .emptyResponse)

try await subject.verifyEmailToken(
email: "example@email.com",
emailVerificationToken: "verification-token"
)

XCTAssertEqual(client.requests.count, 1)
XCTAssertNotNil(client.requests[0].body)
XCTAssertEqual(client.requests[0].method, .post)
XCTAssertEqual(
client.requests[0].url.absoluteString,
"https://example.com/identity/accounts/register/verification-email-clicked"
)
}

/// `verifyEmailToken()` throws error when call fails.
func test_verifyEmailToken_httpFailure() async throws {
client.result = .httpFailure()

await assertAsyncThrows {
try await subject.verifyEmailToken(
email: "example@email.com",
emailVerificationToken: "verification-token"
)
}
}

/// `verifyOtp()` performs a request to verify a one-time password for the user.
func test_verifyOtp() async throws {
client.result = .httpSuccess(testData: .emptyResponse)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,8 @@ extension APITestData {
static let startRegistrationInvalidEmailFormat = loadFromJsonBundle(resource: "StartRegistrationInvalidEmailFormat")
static let startRegistrationCaptchaFailure = loadFromJsonBundle(resource: "StartRegistrationCaptchaFailure")
static let startRegistrationSuccess = loadFromBundle(resource: "StartRegistrationSuccess", extension: "txt")

// MARK: Verify Email Token

static let verifyEmailTokenExpiredLink = loadFromJsonBundle(resource: "VerifyEmailTokenExpiredLink")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"message":"Expired link. Please restart registration or try logging in. You may already have an account",
"validationErrors": null,
"exceptionMessage":null,
"exceptionStackTrace":null,
"innerExceptionMessage":null,
"object":"error"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import Foundation
import Networking

// MARK: - VerifyEmailTokenRequestError

/// Errors that can occur when sending a `VerifyEmailTokenRequest`.
enum VerifyEmailTokenRequestError: Error, Equatable {
/// The token provided by email is expired or user is already used.
///
case tokenExpired
}

// MARK: - VerificationEmailClickedRequest

/// The API request sent when verifying the token received by email.
///
struct VerifyEmailTokenRequest: Request {
typealias Response = EmptyResponse

typealias Body = VerifyEmailTokenRequestModel

/// The body of this request.
var body: VerifyEmailTokenRequestModel? {
requestModel
}

/// The HTTP method for this request.
let method: HTTPMethod = .post

/// The URL path for this request.
var path: String = "/accounts/register/verification-email-clicked"

/// The request details to include in the body of the request.
let requestModel: VerifyEmailTokenRequestModel

// MARK: Methods

func validate(_ response: HTTPResponse) throws {
switch response.statusCode {
case 400 ..< 500:
guard let errorResponse = try? ErrorResponseModel(response: response) else { return }

if errorResponse.message.contains("Expired link") {
throw VerifyEmailTokenRequestError.tokenExpired
}

throw ServerError.error(errorResponse: errorResponse)
default:
return
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import Networking
import XCTest

@testable import BitwardenShared

// MARK: - VerifyEmailTokenRequestTests

class VerifyEmailTokenRequestTests: BitwardenTestCase {
// MARK: Properties

var subject: VerifyEmailTokenRequest!

// MARK: Setup & Teardown

override func setUp() {
super.setUp()

subject = VerifyEmailTokenRequest(
requestModel: VerifyEmailTokenRequestModel(
email: "example@email.com",
emailVerificationToken: "email-verification-token"
)
)
}

override func tearDown() {
super.tearDown()

subject = nil
}

// MARK: Tests

/// Validate that the method is correct.
func test_method() {
XCTAssertEqual(subject.method, .post)
}

/// Validate that the path is correct.
func test_path() {
XCTAssertEqual(subject.path, "/accounts/register/verification-email-clicked")
}

/// Validate that the body is not nil.
func test_body() {
XCTAssertNotNil(subject.body)
}

/// `validate(_:)` with a `400` status code and an expired link error in the response body
/// throws an `.tokenExpired` error.
func test_validate_with400ExpiredLink() throws {
let response = HTTPResponse.failure(
statusCode: 400,
body: APITestData.verifyEmailTokenExpiredLink.data
)

XCTAssertThrowsError(try subject.validate(response)) { error in
XCTAssertEqual(
error as? VerifyEmailTokenRequestError,
.tokenExpired
)
}
}

/// `validate(_:)` with a `400` status code but no captcha error does not throw a validation error.
func test_validate_with400() {
let response = HTTPResponse.failure(
statusCode: 400,
body: Data("example data".utf8)
)

XCTAssertNoThrow(try subject.validate(response))
}

/// `validate(_:)` with a valid response does not throw a validation error.
func test_validate_with200() {
let response = HTTPResponse.success()

XCTAssertNoThrow(try subject.validate(response))
}

// MARK: Init

/// Validate that the value provided to the init method is correct.
func test_init_body() {
XCTAssertEqual(subject.body?.email, "example@email.com")
XCTAssertEqual(subject.body?.emailVerificationToken, "email-verification-token")
}
}

0 comments on commit 0c98785

Please sign in to comment.