diff --git a/.code-samples.meilisearch.yaml b/.code-samples.meilisearch.yaml index 523ae8ac..d4a15526 100644 --- a/.code-samples.meilisearch.yaml +++ b/.code-samples.meilisearch.yaml @@ -162,15 +162,128 @@ get_all_tasks_1: |- print(error) } } -get_keys_1: |- - client.keys { (result: Result) in - switch result { - case .success(let key): - print(key) - case .failure(let error): - print(error) - } +get_on_key_1: |- + self.client.getKey(key: "d0552b41536279a0ad88bd595327b96f01176a60c2243e906c52ac02375f9bc4") { result in + switch result { + case .success(let key): + print(key) + case .failure(let error): + print(error) + } + } +get_all_keys_1: |- + self.client.getKeys() { result in + switch result { + case .success(let keys): + print(keys) + case .failure(let error): + print(error) + } + } +create_a_key_1: |- + let keyParams = KeyParams( + description: "Add documents: Products API key", + actions: ["documents.add"], + indexes: ["products"], + expiresAt: "2042-04-02T00:42:42Z" + ) + self.client.createKey(keyParams) { result in + switch result { + case .success(let key): + print(keys) + case .failure(let error): + print(error) + } + } +update_a_key_1: |- + let keyParams = KeyParams( + description: "Add documents: Products API key", + actions: ["documents.add", "documents.delete"], + indexes: ["products", "reviews"], + expiresAt: "2042-04-02T00:42:42Z" + ) + self.client.updateKey( + key: "d0552b41536279a0ad88bd595327b96f01176a60c2243e906c52ac02375f9bc4" + keyparams: keyParams + ) { result in + switch result { + case .success(let key): + print(keys) + case .failure(let error): + print(error) + } + } +delete_a_key_1: |- + self.client.deleteKey(key: "d0552b41536279a0ad88bd595327b96f01176a60c2243e906c52ac02375f9bc4") + { result in + switch result { + case .success: + print("success") + case .failure(let error): + print(error) + } + } + +security_guide_search_key_1: |- + client = MeiliSearch(host: "http://localhost:7700", apiKey: "apiKey") + client.index("patient_medical_records") + .search(parameters) { (result: Result, Swift.Error>) in + switch result { + case .success(let searchResult): + print(searchResult) + case .failure(let error): + print(error) + } + } +security_guide_update_key_1: |- + let keyParams = KeyParams( + indexes: ["doctors"] + ) + self.client.updateKey( + key: "d0552b41536279a0ad88bd595327b96f01176a60c2243e906c52ac02375f9bc4", + keyParams: keyParams, + ) { result in + switch result { + case .success(let key): + print(keys) + case .failure(let error): + print(error) + } + } +security_guide_create_key_1: |- + let keyParams = KeyParams( + description: "Search patient records key", + actions: ["search"], + indexes: ["patient_medical_records"], + expiresAt: "2023-01-01T00:00:00Z" + ) + self.client.createKey(keyParams) { result in + switch result { + case .success(let key): + print(keys) + case .failure(let error): + print(error) } + } +security_guide_list_keys_1: |- + self.client.getKeys() { result in + switch result { + case .success(let keys): + print(keys) + case .failure(let error): + print(error) + } + } +security_guide_delete_key_1: |- + self.client.deleteKey(key: "d0552b41536279a0ad88bd595327b96f01176a60c2243e906c52ac02375f9bc4") + { result in + switch result { + case .success: + print("success") + case .failure(let error): + print(error) + } + } get_settings_1: |- client.index("movies").getSettings { (result: Result) in switch result { diff --git a/Sources/MeiliSearch/Client.swift b/Sources/MeiliSearch/Client.swift index 327df11b..a8f6db39 100755 --- a/Sources/MeiliSearch/Client.swift +++ b/Sources/MeiliSearch/Client.swift @@ -212,17 +212,80 @@ public struct MeiliSearch { // MARK: Keys /** - Each instance of MeiliSearch has three keys: a master, a private, and a public. Each key has a given - set of permissions on the API routes. + Get all keys. - - parameter masterKey: Master key to access the `keys` function. - parameter completion: The completion closure used to notify when the server completes the query request, it returns a `Result` object that contains `Key` value. If the request was sucessful or `Error` if a failure occured. */ - public func keys( + public func getKeys( + _ completion: @escaping (Result, Swift.Error>) -> Void) { + self.keys.getAll(completion) + } + + /** + Get one key's information using the key value. + + - parameter key: The key value. + - parameter completion: The completion closure used to notify when the server + completes the query request, it returns a `Result` object that contains `Key` value. + If the request was sucessful or `Error` if a failure occured. + */ + public func getKey( + key: String, + _ completion: @escaping (Result) -> Void) { + self.keys.get(key: key, completion) + } + + /** + Create an API key. + + - parameter keyParams: Parameters object required to create a key. + - parameter completion: The completion closure used to notify when the server + completes the query request, it returns a `Result` object that contains `Key` value. + If the request was sucessful or `Error` if a failure occured. + */ + public func createKey( + _ keyParams: KeyParams, _ completion: @escaping (Result) -> Void) { - self.keys.get(completion) + self.keys.create(keyParams, completion) + } + + /** + Update an API key. + + - parameter key: The key value. + - parameter keyParams: Parameters object required to update a key. + - parameter completion: The completion closure used to notify when the server + completes the query request, it returns a `Result` object that contains `Key` value. + If the request was sucessful or `Error` if a failure occured. + */ + public func updateKey( + key: String, + keyParams: KeyParams, + _ completion: @escaping (Result) -> Void) { + self.keys.update( + key: key, + keyParams: keyParams, + completion + ) + } + + /** + Delete an API key. + + - parameter key: The key value. + - parameter completion: The completion closure used to notify when the server + completes the query request, it returns a `Result` object that contains `Key` value. + If the request was sucessful or `Error` if a failure occured. + */ + public func deleteKey( + key: String, + _ completion: @escaping (Result<(), Swift.Error>) -> Void) { + self.keys.delete( + key: key, + completion + ) } // MARK: Stats diff --git a/Sources/MeiliSearch/Keys.swift b/Sources/MeiliSearch/Keys.swift index 9e7870bc..b8a36ff8 100644 --- a/Sources/MeiliSearch/Keys.swift +++ b/Sources/MeiliSearch/Keys.swift @@ -11,11 +11,10 @@ struct Keys { self.request = request } - func get(_ completion: @escaping (Result) -> Void) { - self.request.get(api: "/keys") { result in + func get(key: String, _ completion: @escaping (Result) -> Void) { + self.request.get(api: "/keys/\(key)") { result in switch result { case .success(let data): - guard let data: Data = data else { completion(.failure(MeiliSearch.Error.dataNotFound)) return @@ -32,4 +31,93 @@ struct Keys { } } } + + func getAll(_ completion: @escaping (Result, Swift.Error>) -> Void) { + self.request.get(api: "/keys") { result in + switch result { + case .success(let data): + guard let data: Data = data else { + completion(.failure(MeiliSearch.Error.dataNotFound)) + return + } + do { + let keys: Results = try Constants.customJSONDecoder.decode(Results.self, from: data) + completion(.success(keys)) + } catch { + completion(.failure(error)) + } + case .failure(let error): + completion(.failure(error)) + } + } + } + + public func create( + _ keyParams: KeyParams, + _ completion: @escaping (Result) -> Void) { + let data: Data + do { + data = try Constants.customJSONEecoder.encode(keyParams) + } catch { + completion(.failure(MeiliSearch.Error.invalidJSON)) + return + } + self.request.post(api: "/keys", data) { result in + switch result { + case .success(let result): + do { + let key: Key = try Constants.customJSONDecoder.decode( + Key.self, + from: result) + completion(.success(key)) + } catch { + completion(.failure(error)) + } + case .failure(let error): + completion(.failure(error)) + } + } + } + + public func update( + key: String, + keyParams: KeyParams, + _ completion: @escaping (Result) -> Void) { + let data: Data + do { + let encoder = JSONEncoder() + data = try encoder.encode(keyParams) + } catch { + completion(.failure(MeiliSearch.Error.invalidJSON)) + return + } + self.request.patch(api: "/keys/\(key)", data) { result in + switch result { + case .success(let result): + do { + let key: Key = try Constants.customJSONDecoder.decode( + Key.self, + from: result) + completion(.success(key)) + } catch { + completion(.failure(error)) + } + case .failure(let error): + completion(.failure(error)) + } + } + } + + public func delete( + key: String, + _ completion: @escaping (Result<(), Swift.Error>) -> Void) { + self.request.delete(api: "/keys/\(key)") { result in + switch result { + case .success: + completion(.success(())) + case .failure(let error): + completion(.failure(error)) + } + } + } } diff --git a/Sources/MeiliSearch/Model/Key.swift b/Sources/MeiliSearch/Model/Key.swift index d36d7347..b32b1c83 100644 --- a/Sources/MeiliSearch/Model/Key.swift +++ b/Sources/MeiliSearch/Model/Key.swift @@ -1,15 +1,16 @@ import Foundation /** - Each instance of MeiliSearch has three keys: a master, a private, and a public. Each key has a given set of permissions on the API routes. */ public struct Key: Codable, Equatable { // MARK: Properties - /// Private key used to access a determined set of API routes. - public let `private`: String - - /// Public key used to access a determined set of API routes. - public let `public`: String + public let description: String + public let key: String + public let actions: [String] + public let indexes: [String] + public let expiresAt: String? + public let createdAt: String + public let updatedAt: String } diff --git a/Sources/MeiliSearch/Model/KeyParams.swift b/Sources/MeiliSearch/Model/KeyParams.swift new file mode 100644 index 00000000..4dbff5bc --- /dev/null +++ b/Sources/MeiliSearch/Model/KeyParams.swift @@ -0,0 +1,21 @@ +import Foundation + +/** + `KeyParams` contains all the parameters to create an API key. + */ +public struct KeyParams: Codable, Equatable { + public let description: String + public let actions: [String] + public let indexes: [String] + public let expiresAt: String? + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(description, forKey: .description) + try container.encode(actions, forKey: .actions) + try container.encode(indexes, forKey: .indexes) + try container.encode(expiresAt, forKey: .expiresAt) + } + + // MARK: Properties +} diff --git a/Sources/MeiliSearch/Request.swift b/Sources/MeiliSearch/Request.swift index 06af9809..7439a087 100755 --- a/Sources/MeiliSearch/Request.swift +++ b/Sources/MeiliSearch/Request.swift @@ -145,6 +145,44 @@ public final class Request { task.resume() } + func patch( + api: String, + _ data: Data, + _ completion: @escaping (Result) -> Void) { + guard let url = URL(string: config.url(api: api)) else { + completion(.failure(MeiliSearch.Error.invalidURL())) + return + } + + var request = URLRequest(url: url) + request.httpMethod = "PATCH" + request.httpBody = data + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.setValue("application/json; charset=utf-8", forHTTPHeaderField: "Accept") + + if let apiKey: String = config.apiKey { + let bearer = "Bearer \(apiKey)" + request.setValue(bearer, forHTTPHeaderField: "Authorization") + } + + let task: URLSessionDataTaskProtocol = session.execute(with: request) { data, response, error in + do { + try MeiliSearch.errorHandler(url: url, data: data, response: response, error: error) + if let unwrappedData: Data = data { + completion(.success(unwrappedData)) + return + } + completion(.failure(MeiliSearch.Error.invalidJSON)) + return + } catch let error { + completion(.failure(error)) + return + } + } + + task.resume() + } + func delete( api: String, _ completion: @escaping (Result) -> Void) { diff --git a/Tests/MeiliSearchIntegrationTests/KeysTests.swift b/Tests/MeiliSearchIntegrationTests/KeysTests.swift new file mode 100644 index 00000000..7ab92008 --- /dev/null +++ b/Tests/MeiliSearchIntegrationTests/KeysTests.swift @@ -0,0 +1,217 @@ +@testable import MeiliSearch +import XCTest +import Foundation + +// swiftlint:disable force_try +private struct Movie: Codable, Equatable { + let id: Int + let title: String + let comment: String? + + init(id: Int, title: String, comment: String? = nil) { + self.id = id + self.title = title + self.comment = comment + } +} + +class KeysTests: XCTestCase { + private var client: MeiliSearch! + private var key: String = "" + private var session: URLSessionProtocol! + + // MARK: Setup + + override func setUp() { + super.setUp() + session = URLSession(configuration: .ephemeral) + client = try! MeiliSearch(host: "http://localhost:7700", apiKey: "masterKey", session: session) + let keyExpectation = XCTestExpectation(description: "Get all keys") + + self.client.getKeys { result in + switch result { + case .success(let keys): + if keys.results.count > 0 { + let key = keys.results.first + if let firstKey: Key = key { + self.key = firstKey.key + } + } else { + XCTFail("Failed to get keys") + } + keyExpectation.fulfill() + case .failure(let error): + dump(error) + XCTFail("Failed to get keys") + keyExpectation.fulfill() + } + } + self.wait(for: [keyExpectation], timeout: 10.0) + } + + func testGetKeys() { + let keyExpectation = XCTestExpectation(description: "Get all keys") + + self.client.getKeys { result in + switch result { + case .success(let keys): + XCTAssertNotEqual(keys.results.count, 0) + keyExpectation.fulfill() + case .failure(let error): + dump(error) + XCTFail("Failed to get all keys") + keyExpectation.fulfill() + } + } + self.wait(for: [keyExpectation], timeout: 10.0) + } + + func testGetKey() { + let keyExpectation = XCTestExpectation(description: "Get one key") + + self.client.getKey(key: self.key) { result in + switch result { + case .success(let key): + XCTAssertNotNil(key.description) + keyExpectation.fulfill() + case .failure(let error): + dump(error) + XCTFail("Failed to get a key") + keyExpectation.fulfill() + } + } + self.wait(for: [keyExpectation], timeout: 10.0) + } + + func testCreateKey() { + let keyExpectation = XCTestExpectation(description: "Create a key") + + let keyParams = KeyParams(description: "Custom", actions: ["*"], indexes: ["*"], expiresAt: nil) + self.client.createKey(keyParams) { result in + switch result { + case .success(let key): + XCTAssertEqual(key.expiresAt, nil) + XCTAssertEqual(key.description, keyParams.description) + XCTAssertEqual(key.actions, keyParams.actions) + XCTAssertEqual(key.indexes, keyParams.indexes) + keyExpectation.fulfill() + case .failure(let error): + dump(error) + XCTFail("Failed to create a key") + keyExpectation.fulfill() + } + } + + self.wait(for: [keyExpectation], timeout: 10.0) + } + + func testCreateKeyWithExpire() { + let keyExpectation = XCTestExpectation(description: "Create a key with an expireAt value") + + let formatter = DateFormatter() + formatter.dateFormat = "yyyy-MM-dd" + let someDateTime = formatter.string(from: Date.distantFuture) + + let keyParams = KeyParams(description: "Custom", actions: ["*"], indexes: ["*"], expiresAt: someDateTime) + self.client.createKey(keyParams) { result in + switch result { + case .success(let key): + XCTAssertEqual(key.description, keyParams.description) + XCTAssertEqual(key.actions, keyParams.actions) + XCTAssertEqual(key.indexes, keyParams.indexes) + XCTAssertNotNil(key.expiresAt) + keyExpectation.fulfill() + case .failure(let error): + dump(error) + XCTFail("Failed to create a key") + keyExpectation.fulfill() + } + } + self.wait(for: [keyExpectation], timeout: 10.0) + } + + func testUpdateKey() { + let keyExpectation = XCTestExpectation(description: "Update a key") + + let keyParams = KeyParams(description: "Custom", actions: ["*"], indexes: ["*"], expiresAt: nil) + self.client.createKey(keyParams) { result in + switch result { + case .success(let key): + let formatter = DateFormatter() + formatter.dateFormat = "yyyy-MM-dd" + let newDate = formatter.string(from: Date.distantFuture) + let keyParams = KeyParams(description: "Custom", actions: ["*"], indexes: ["*"], expiresAt: newDate) + self.client.updateKey(key: key.key, keyParams: keyParams) { result in + switch result { + case .success(let key): + XCTAssertEqual(key.description, keyParams.description) + XCTAssertEqual(key.actions, keyParams.actions) + XCTAssertEqual(key.indexes, keyParams.indexes) + XCTAssertNotNil(key.expiresAt) + keyExpectation.fulfill() + case .failure(let error): + dump(error) + XCTFail("Failed to update a key") + keyExpectation.fulfill() + } + } + case .failure(let error): + dump(error) + XCTFail("Failed to create a key") + keyExpectation.fulfill() + } + } + self.wait(for: [keyExpectation], timeout: 10.0) + } + + func testDeleteKey() { + let keyExpectation = XCTestExpectation(description: "Delete a key") + + let keyParams = KeyParams(description: "Custom", actions: ["*"], indexes: ["*"], expiresAt: nil) + self.client.createKey(keyParams) { result in + switch result { + case .success(let key): + self.client.deleteKey(key: key.key) { result in + switch result { + case .success: + self.client.getKey(key: key.key) { result in + switch result { + case .success(let key): + XCTAssertNotNil(key.description) + XCTFail("Failed to get a key") + keyExpectation.fulfill() + case .failure(let error as MeiliSearch.Error): + switch error { + case MeiliSearch.Error.meiliSearchApiError(_, let msErrorResponse, _, _): + if let msError: MeiliSearch.MSErrorResponse = msErrorResponse { + XCTAssertEqual(msError.code, "api_key_not_found") + } else { + XCTFail("Failed to get the correct error code") + } + default: + dump(error) + XCTFail("Failed to delete the key") + } + keyExpectation.fulfill() + case .failure(let error): + dump(error) + XCTFail("Failed to get the correct error type") + keyExpectation.fulfill() + } + } + case .failure(let error): + dump(error) + XCTFail("Failed to delete a key") + keyExpectation.fulfill() + } + } + case .failure(let error): + dump(error) + XCTFail("Failed to create a key") + keyExpectation.fulfill() + } + } + self.wait(for: [keyExpectation], timeout: 10.0) + } +} +// swiftlint:enable force_unwrapping diff --git a/Tests/MeiliSearchUnitTests/KeysTests.swift b/Tests/MeiliSearchUnitTests/KeysTests.swift deleted file mode 100644 index 16aba0bd..00000000 --- a/Tests/MeiliSearchUnitTests/KeysTests.swift +++ /dev/null @@ -1,47 +0,0 @@ -@testable import MeiliSearch -import XCTest - -// swiftlint:disable force_unwrapping -// swiftlint:disable force_try -class KeysTests: XCTestCase { - private var client: MeiliSearch! - private let session = MockURLSession() - - override func setUp() { - super.setUp() - client = try! MeiliSearch(host: "http://localhost:7700", apiKey: "masterKey", session: session) - } - - func testKeys() { - // Prepare the mock server - - let jsonString = """ - { - "private": "8dcbb482663333d0280fa9fedf0e0c16d52185cb67db494ce4cd34da32ce2092", - "public": "3b3bf839485f90453acc6159ba18fbed673ca88523093def11a9b4f4320e44a5" - } - """ - - let decoder = JSONDecoder() - let jsonData = jsonString.data(using: .utf8)! - let stubKey: Key = try! decoder.decode(Key.self, from: jsonData) - - session.pushData(jsonString) - - let expectation = XCTestExpectation(description: "Get public and private key") - - self.client.keys { result in - switch result { - case .success(let key): - XCTAssertEqual(stubKey, key) - expectation.fulfill() - case .failure: - XCTFail("Failed to get public and private key") - } - } - - self.wait(for: [expectation], timeout: 20.0) - } -} -// swiftlint:enable force_unwrapping -// swiftlint:enable force_try