diff --git a/BitwardenShared/Core/Auth/Models/API/CreateAccount/CreateAccountRequestModel.swift b/BitwardenShared/Core/Auth/Models/API/CreateAccount/CreateAccountRequestModel.swift new file mode 100644 index 000000000..18f61dc92 --- /dev/null +++ b/BitwardenShared/Core/Auth/Models/API/CreateAccount/CreateAccountRequestModel.swift @@ -0,0 +1,57 @@ +import Foundation +import Networking + +// MARK: - CreateAccountRequestModel + +/// The data to include in the body of a `CreateAccountRequest`. +/// +struct CreateAccountRequestModel: Equatable { + // MARK: Properties + + /// The captcha response used in validating a user for this request. + let captchaResponse: String? = nil + + /// The user's email address. + let email: String + + /// The type of kdf for this request. + let kdf: KdfType? = nil + + /// The number of kdf iterations performed in this request. + let kdfIterations: Int? = nil + + /// The kdf memory allocated for the computed password hash. + let kdfMemory: Int? = nil + + /// The number of threads upon which the kdf iterations are performed. + let kdfParallelism: Int? = nil + + /// The key used for this request. + let key: String? = nil + + /// The keys used for this request. + let keys: KeysRequestModel? = nil + + /// The master password hash used to authenticate a user. + let masterPasswordHash: String // swiftlint:disable:this inclusive_language + + /// The master password hint. + let masterPasswordHint: String? = nil // swiftlint:disable:this inclusive_language + + /// The user's name. + let name: String? = nil + + /// The organization's user ID. + let organizationUserId: String? = nil + + /// The token used when making this request. + let token: String? = nil +} + +// MARK: JSONRequestBody + +extension CreateAccountRequestModel: JSONRequestBody { + static var encoder: JSONEncoder { + JSONEncoder() + } +} diff --git a/BitwardenShared/Core/Auth/Models/API/CreateAccount/CreateAccountResponseModel.swift b/BitwardenShared/Core/Auth/Models/API/CreateAccount/CreateAccountResponseModel.swift new file mode 100644 index 000000000..6bac6db81 --- /dev/null +++ b/BitwardenShared/Core/Auth/Models/API/CreateAccount/CreateAccountResponseModel.swift @@ -0,0 +1,15 @@ +import Foundation +import Networking + +// MARK: - CreateAccountResponseModel + +/// The response returned from the API upon creating an account. +/// +struct CreateAccountResponseModel: JSONResponse { + static var decoder = JSONDecoder() + + // MARK: Properties + + /// The captcha bypass token returned in this response. + var captchaBypassToken: String? +} diff --git a/BitwardenShared/Core/Auth/Models/API/CreateAccount/CreateAccountResponseModelTests.swift b/BitwardenShared/Core/Auth/Models/API/CreateAccount/CreateAccountResponseModelTests.swift new file mode 100644 index 000000000..6c18fdac4 --- /dev/null +++ b/BitwardenShared/Core/Auth/Models/API/CreateAccount/CreateAccountResponseModelTests.swift @@ -0,0 +1,21 @@ +import XCTest + +@testable import BitwardenShared + +// MARK: - CreateAccountResponseModelTests + +class CreateAccountResponseModelTests: BitwardenTestCase { + /// Tests that a response is initialized correctly. + func test_init() { + let subject = CreateAccountResponseModel(captchaBypassToken: "captchaBypassToken") + XCTAssertEqual(subject.captchaBypassToken, "captchaBypassToken") + } + + /// Tests the successful decoding of a JSON response. + func test_decode_success() throws { + let json = APITestData.createAccountResponse.data + let decoder = JSONDecoder() + let subject = try decoder.decode(CreateAccountResponseModel.self, from: json) + XCTAssertEqual(subject.captchaBypassToken, "captchaBypassToken") + } +} diff --git a/BitwardenShared/Core/Auth/Models/API/CreateAccount/Fixtures/APITestData+CreateAccountRequest.swift b/BitwardenShared/Core/Auth/Models/API/CreateAccount/Fixtures/APITestData+CreateAccountRequest.swift new file mode 100644 index 000000000..7dfb12d7d --- /dev/null +++ b/BitwardenShared/Core/Auth/Models/API/CreateAccount/Fixtures/APITestData+CreateAccountRequest.swift @@ -0,0 +1,4 @@ +extension APITestData { + static let createAccountRequest = loadFromBundle(resource: "Request", extension: "json") + static let createAccountResponse = loadFromBundle(resource: "Success", extension: "json") +} diff --git a/BitwardenShared/Core/Auth/Models/API/CreateAccount/Fixtures/Request.json b/BitwardenShared/Core/Auth/Models/API/CreateAccount/Fixtures/Request.json new file mode 100644 index 000000000..32c75525a --- /dev/null +++ b/BitwardenShared/Core/Auth/Models/API/CreateAccount/Fixtures/Request.json @@ -0,0 +1,23 @@ +{ + "name": "name", + "email": "email", + "masterPasswordHash": "masterPasswordHash", + "masterPasswordHint": "masterPasswordHint", + "captchaResponse": "captchaResponse", + "key": "key", + "keys": { + "publicKey": "publicKey", + "encryptedPrivateKey": "encryptedPrivateKey" + }, + "token": "token", + "organizationUserId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "kdf": 0, + "kdfIterations": 0, + "kdfMemory": 0, + "kdfParallelism": 0, + "referenceData": { + "additionalProp1": "string", + "additionalProp2": "string", + "additionalProp3": "string" + } +} diff --git a/BitwardenShared/Core/Auth/Models/API/CreateAccount/Fixtures/Success.json b/BitwardenShared/Core/Auth/Models/API/CreateAccount/Fixtures/Success.json new file mode 100644 index 000000000..2da50ea69 --- /dev/null +++ b/BitwardenShared/Core/Auth/Models/API/CreateAccount/Fixtures/Success.json @@ -0,0 +1,4 @@ +{ + "object": "object", + "captchaBypassToken": "captchaBypassToken" +} diff --git a/BitwardenShared/Core/Auth/Models/API/CreateAccount/KdfType.swift b/BitwardenShared/Core/Auth/Models/API/CreateAccount/KdfType.swift new file mode 100644 index 000000000..3887cb25a --- /dev/null +++ b/BitwardenShared/Core/Auth/Models/API/CreateAccount/KdfType.swift @@ -0,0 +1,11 @@ +// MARK: - KdfType + +/// The type of key derivation function. +/// +enum KdfType: Int, Codable, Equatable { + /// The PBKDF2 SHA256 type. + case pbkdf2sha256 = 0 + + /// The Argon2id type. + case argon2id = 1 +} diff --git a/BitwardenShared/Core/Auth/Models/API/CreateAccount/KeysRequestModel.swift b/BitwardenShared/Core/Auth/Models/API/CreateAccount/KeysRequestModel.swift new file mode 100644 index 000000000..90ecdc5b4 --- /dev/null +++ b/BitwardenShared/Core/Auth/Models/API/CreateAccount/KeysRequestModel.swift @@ -0,0 +1,13 @@ +// MARK: - KeysRequestModel + +/// A model for keys used in the `CreateAccountRequest`. +/// +struct KeysRequestModel: Codable, Equatable { + // MARK: Properties + + /// The public key used in a `CreateAccountRequest`. + var publicKey: String? + + /// The encrypted private key used in a `CreateAccountRequest`. + let encryptedPrivateKey: String +} diff --git a/BitwardenShared/Core/Auth/Services/API/AccountAPIService.swift b/BitwardenShared/Core/Auth/Services/API/AccountAPIService.swift index 352fc6d45..b93be3dd9 100644 --- a/BitwardenShared/Core/Auth/Services/API/AccountAPIService.swift +++ b/BitwardenShared/Core/Auth/Services/API/AccountAPIService.swift @@ -1,5 +1,22 @@ +// MARK: - AccountAPIService + /// A protocol for an API service used to make account requests. /// -protocol AccountAPIService {} +protocol AccountAPIService { + /// Creates an API call for when the user submits an account creation form. + /// + /// - Parameter body: The body to be included in the request. + /// + /// - Returns data returned from the `CreateAccountRequest`. + /// + func createNewAccount(body: CreateAccountRequestModel) async throws -> CreateAccountResponseModel +} + +// MARK: - APIService -extension APIService: AccountAPIService {} +extension APIService: AccountAPIService { + func createNewAccount(body: CreateAccountRequestModel) async throws -> CreateAccountResponseModel { + let request = CreateAccountRequest(body: body) + return try await apiService.send(request) + } +} diff --git a/BitwardenShared/Core/Auth/Services/API/AccountAPIServiceTests.swift b/BitwardenShared/Core/Auth/Services/API/AccountAPIServiceTests.swift new file mode 100644 index 000000000..c7d34b31f --- /dev/null +++ b/BitwardenShared/Core/Auth/Services/API/AccountAPIServiceTests.swift @@ -0,0 +1,74 @@ +import XCTest + +@testable import BitwardenShared + +// MARK: - AccountAPIServiceTests + +class AccountAPIServiceTests: BitwardenTestCase { + // MARK: Properties + + var client: MockHTTPClient! + var subject: APIService! + + override func setUp() { + super.setUp() + client = MockHTTPClient() + subject = APIService(client: client) + } + + override func tearDown() { + super.tearDown() + client = nil + subject = nil + } + + // MARK: Account creation + + /// `createNewAccount(email:masterPasswordHash)` throws an error if the request fails. + func test_create_account_httpFailure() async { + client.result = .httpFailure() + + await assertAsyncThrows { + _ = try await subject.createNewAccount( + body: CreateAccountRequestModel( + email: "example@email.com", + masterPasswordHash: "1234" + ) + ) + } + } + + /// `createNewAccount(email:masterPasswordHash)` throws a decoding error if the response is not the expected type. + func test_create_account_failure() async throws { + let resultData = APITestData(data: Data("this should fail".utf8)) + client.result = .httpSuccess(testData: resultData) + + await assertAsyncThrows { + _ = try await subject.createNewAccount( + body: CreateAccountRequestModel( + email: "example@email.com", + masterPasswordHash: "1234" + ) + ) + } + } + + /// `createNewAccount(email:masterPasswordHash)` returns the correct value from the API with a successful request. + func test_create_account_success() async throws { + let resultData = APITestData.createAccountResponse + client.result = .httpSuccess(testData: resultData) + + let successfulResponse = try await subject.createNewAccount( + body: CreateAccountRequestModel( + email: "example@email.com", + masterPasswordHash: "1234" + ) + ) + + let request = try XCTUnwrap(client.requests.first) + XCTAssertEqual(request.method, .post) + XCTAssertEqual(request.url.relativePath, "/api/accounts/register") + XCTAssertEqual(successfulResponse.captchaBypassToken, "captchaBypassToken") + XCTAssertNotNil(request.body) + } +} diff --git a/BitwardenShared/Core/Auth/Services/API/CreateAccount/CreateAccountRequest.swift b/BitwardenShared/Core/Auth/Services/API/CreateAccount/CreateAccountRequest.swift new file mode 100644 index 000000000..51d803360 --- /dev/null +++ b/BitwardenShared/Core/Auth/Services/API/CreateAccount/CreateAccountRequest.swift @@ -0,0 +1,28 @@ +import Foundation +import Networking + +// MARK: - CreateAccountRequest + +/// The API request sent when submitting an account creation form. +/// +struct CreateAccountRequest: Request { + typealias Response = CreateAccountResponseModel + typealias Body = CreateAccountRequestModel + + /// The body of this request. + var body: CreateAccountRequestModel? + + /// The HTTP method for this request. + let method: HTTPMethod = .post + + /// The URL path for this request. + var path: String = "/accounts/register" + + /// Creates a new `CreateAccountRequest` instance. + /// + /// - Parameter body: The body of the request. + /// + init(body: CreateAccountRequestModel) { + self.body = body + } +} diff --git a/BitwardenShared/Core/Auth/Services/API/CreateAccount/CreateAccountRequestTests.swift b/BitwardenShared/Core/Auth/Services/API/CreateAccount/CreateAccountRequestTests.swift new file mode 100644 index 000000000..cb401fed1 --- /dev/null +++ b/BitwardenShared/Core/Auth/Services/API/CreateAccount/CreateAccountRequestTests.swift @@ -0,0 +1,54 @@ +import XCTest + +@testable import BitwardenShared + +// MARK: - CreateAccountRequestTests + +class CreateAccountRequestTests: BitwardenTestCase { + /// Validate that the method is correct. + func test_method() { + let subject = CreateAccountRequest( + body: CreateAccountRequestModel( + email: "email@example.com", + masterPasswordHash: "1234" + ) + ) + XCTAssertEqual(subject.method, .post) + } + + /// Validate that the path is correct. + func test_path() { + let subject = CreateAccountRequest( + body: CreateAccountRequestModel( + email: "email@example.com", + masterPasswordHash: "1234" + ) + ) + XCTAssertEqual(subject.path, "/accounts/register") + } + + /// Validate that the body is not nil. + func test_body() { + let subject = CreateAccountRequest( + body: CreateAccountRequestModel( + email: "email@example.com", + masterPasswordHash: "1234" + ) + ) + XCTAssertNotNil(subject.body) + } + + // MARK: Init + + /// Validate that the value provided to the init method is correct. + func test_init_body() { + let subject = CreateAccountRequest( + body: CreateAccountRequestModel( + email: "email@example.com", + masterPasswordHash: "1234" + ) + ) + XCTAssertEqual(subject.body?.email, "email@example.com") + XCTAssertEqual(subject.body?.masterPasswordHash, "1234") + } +}