diff --git a/README.md b/README.md index 2f68ddbe..2ad2537e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # skyflow-iOS --- -Skyflow’s iOS SDK can be used to securely collect, tokenize, and display sensitive data in the mobile without exposing your front-end infrastructure to sensitive data. +Skyflow's iOS SDK can be used to securely collect, tokenize, and display sensitive data in the mobile without exposing your front-end infrastructure to sensitive data. [![CI](https://img.shields.io/static/v1?label=CI&message=passing&color=green?style=plastic&logo=github)](https://github.com/skyflowapi/skyflow-ios/actions) [![GitHub release](https://img.shields.io/github/v/release/skyflowapi/skyflow-ios.svg)](https://github.com/skyflowapi/skyflow-ios/releases) @@ -136,7 +136,6 @@ For `env` parameter, there are 2 accepted values in Skyflow.Env --- # Securely collecting data client-side -- [**Inserting data into the vault**](#insert-data-into-the-vault) - [**Using Skyflow Elements to collect data**](#using-skyflow-elements-to-collect-data) - [**Using Skyflow Elements to update data**](#using-skyflow-elements-to-update-data) - [**Using validations on Collect Elements**](#validations) @@ -144,75 +143,6 @@ For `env` parameter, there are 2 accepted values in Skyflow.Env - [**UI Error for Collect Elements**](#ui-error-for-collect-elements) - [**Set and Clear value for Collect Elements (DEV ENV ONLY)**](#set-and-clear-value-for-collect-elements-dev-env-only) -## Insert data into the vault - - To insert data into your vault, use the `skyflowClient.insert()` method. - ```swift - insert(records: [String: Any], options: InsertOptions?= InsertOptions() , callback: Skyflow.Callback) - ``` - The `insert()` method requires a records parameter. - The `records` parameter takes an array of records to insert into the vault. - -#### Insert call example - ```swift - let insertCallback = InsertCallback() //A Custom callback using Skyflow.Callback - skyflowClient.insert( - records: [ - "records": [ - [ - "table": "cards", - "fields": [ - "cardNumber": "41111111111", - "cvv": "123", - ] - ] - ] - ], - callback: insertCallback -) -``` -Skyflow returns tokens for the record you just inserted. -```json -{ - "records": [ { - "table": "cards", - "fields": { - "cardNumber": "f37186-e7e2-466f-91e5-48e12c2bcbc1", - "cvv": "1989cb56-63da-4482-a2df-1f74cd0dd1a5" - } - }] -} -``` - - The `options` parameter takes a Skyflow.InsertOptions object. - - InsertOptions includes a `tokens` boolean that controls whether you receive tokens after inserting a record into the vault. - -InsertOptions also supports the **upsert** feature, that lets you conditionally insert or update an existing record by specifying the unique column to use as the unique value. - -For example, if you specify the ‘customer_id’ column to use for upsert, and the same customer_id already exists, the existing record will update. If the customer_id doesn't exist, the vault creates a new record. - -#### Insert call example -```swift -let records = [ - "records" : [ - [ - "table": "customers", //The table where you are inserting the record. - "fields": [ - "name" : "Francis", - "customer_id" : "12345" - ] - ] - ] -] - -let upsertOptions = [["table": "customers", "column": "customer_id"]] as [[String : Any]] -let insertOptions = Skyflow.InsertOptions(tokens: false, upsert: upsertOptions) -let insertCallback = InsertCallback() //Custom callback - implementation of Skyflow.Callback - -skyflowClient.insert(records: records, options: insertOptions, callback: insertCallback) -``` - ## Using Skyflow Elements to collect data **Skyflow Elements** provide developers with pre-built form elements to securely collect sensitive data client-side. This reduces your PCI compliance scope by not exposing your front-end application to sensitive data. Follow the steps below to securely collect data with Skyflow Elements in your application. @@ -242,9 +172,10 @@ let collectElementInput = Skyflow.CollectElementInput( type: Skyflow.ElementType, // Skyflow.ElementType enum ) ``` -The `table` and `column` fields indicate which table and column in the vault the Element corresponds to. **Note**: +The `table` and `column` fields indicate which table and column in the vault the Element corresponds to. +**Note**: - Use dot delimited strings to specify columns nested inside JSON fields (e.g. `address.street.line1`) -- `table` and `column` are optional only if the element is being used in invokeConnection() + The `inputStyles` parameter accepts a Skyflow.Styles object which consists of multiple `Skyflow.Styles` objects which should be applied to the form element in the following states: @@ -451,18 +382,32 @@ func clearFieldsOnSubmit(_ elements: [TextField]) { ``` #### Step 4 : Collect data from Elements -When you submit the form, call the `collect(options: Skyflow.CollectOptions? = nil, callback: Skyflow.Callback)` method on the container object. The options parameter takes a `Skyflow.CollectOptions` object as shown below: +When you submit the form, call the `collect(callback: Skyflow.CollectCallback, options: Skyflow.CollectOptions? = nil)` method on the container object. The options parameter takes a `Skyflow.CollectOptions` object as shown below: ```swift // Non-PCI records -let nonPCIRecords = ["table": "persons", "fields": [["gender": "MALE"]]] +let nonPCIRecords = Skyflow.AdditionalFields(records: [ + Skyflow.AdditionalFieldsRecord(table: "persons", fields: ["gender": "MALE"]) +]) // Upsert -let upsertOptions = [["table": "cards", "column": "cardNumber"]] as [[String : Any]] -// Send the non-PCI records as additionalFields of InsertOptions (optional) and apply upsert using `upsert` field of InsertOptions (optional) - -let options = Skyflow.CollectOptions(tokens: true, additionalFields: nonPCIRecords) - -//Custom callback - implementation of Skyflow.callback -let insertCallback = InsertCallback() +let upsertOptions = [Skyflow.UpsertOption(table: "cards", uniqueColumns: ["cardNumber"], updateType: .UPDATE)] +// Send the non-PCI records as additionalFields of CollectOptions (optional) and apply upsert using `upsert` field of CollectOptions (optional) + +let options = Skyflow.CollectOptions(additionalFields: nonPCIRecords) + +let insertCallback = Skyflow.CollectCallback( + onSuccess: { (response: Skyflow.CollectResponse) in + for record in response.records { + if let error = record.error { + print("failed:", error, "httpCode:", record.httpCode) + } else { + print("success:", record) + } + } + }, + onFailure: { (skyflowError: Skyflow.SkyflowError) in + print(skyflowError.httpCode, skyflowError.message, skyflowError.grpcCode ?? "", skyflowError.httpStatus ?? "", skyflowError.details ?? "") + } +) container?.collect(callback: insertCallback, options: options) ``` @@ -514,47 +459,103 @@ let skyflowElement = container?.create(input: input, options: requiredOption) // Can interact with this object as a normal UIView Object and add to View // Non-PCI records -let nonPCIRecords = ["table": "persons", "fields": [["gender": "MALE"]]] +let nonPCIRecords = Skyflow.AdditionalFields(records: [ + Skyflow.AdditionalFieldsRecord(table: "persons", fields: ["gender": "MALE"]) +]) //Upsert options - let upsertOptions = [["table": "cards", "column": "cardNumber"]] as [[String : Any]] + let upsertOptions = [Skyflow.UpsertOption(table: "cards", uniqueColumns: ["cardNumber"], updateType: .UPDATE)] // Send the Non-PCI records as additionalFields of CollectOptions (optional) and apply upsert using optional field `upsert` of CollectOptions. -let collectOptions = Skyflow.CollectOptions(tokens: true, additionalFields: nonPCIRecords, upsert: upsertOptions) - - -//Implement a custom Skyflow.Callback to call on Insertion success/failure. -public class InsertCallback: Skyflow.Callback { - public func onSuccess(_ responseBody: Any) { - print(responseBody) - } - public func onFailure(_ error: Any) { - print(error) - } -} - - -// Initialize custom Skyflow.Callback. -let insertCallback = InsertCallback() +let collectOptions = Skyflow.CollectOptions(additionalFields: nonPCIRecords, upsert: upsertOptions) + + +// Initialize a Skyflow.CollectCallback - required by CollectContainer's collect(callback:options:). +// It parses onSuccess into a typed Skyflow.CollectResponse, and onFailure into a typed +// Skyflow.SkyflowError whenever the vault returns that structured shape (see below); +// otherwise onFailure receives the raw error (e.g. a validation SkyflowError) unchanged. +let insertCallback = Skyflow.CollectCallback( + onSuccess: { (response: Skyflow.CollectResponse) in + for record in response.records { + if let error = record.error { + print("failed:", error, "httpCode:", record.httpCode) + } else { + print("success:", record) + } + } + }, + onFailure: { (skyflowError: Skyflow.SkyflowError) in + print(skyflowError.httpCode, skyflowError.message, skyflowError.grpcCode ?? "", skyflowError.httpStatus ?? "", skyflowError.details ?? "") + } +) // Call collect method on CollectContainer. container?.collect(callback: insertCallback, options: collectOptions) ``` #### Skyflow returns tokens for the record you just inserted: -``` +```json { - "records": [ { - "table": "cards", - "fields": { - "cardNumber": "f3907186-e7e2-466f-91e5-48e12c2bcbc1" + "records": [ + { + "tableName": "cards", + "skyflowID": "f1714ef8-8deb-489a-a18d-77e0e007f403", + "fields": { + "cardNumber": [{"token": "f3907186-e7e2-466f-91e5-48e12c2bcbc1", "tokenGroupName": "deterministic_string"}] + }, + "httpCode": 200 + }, + { + "tableName": "persons", + "skyflowID": "77dc3caf-c452-49e1-8625-07219d7567bf", + "fields": { + "gender": [{"token": "12f670af-6c7d-4837-83fb-30365fbc0b1e", "tokenGroupName": "deterministic_string"}] + }, + "httpCode": 200 } - }, { - "table": "persons", - "fields": { - "gender": "12f670af-6c7d-4837-83fb-30365fbc0b1e", + ] +} +``` +**Note:** Successful and failed records are both returned in the same `records` array, each carrying its own `httpCode`. + +#### Sample partial error response: +Successful and failed records are both returned together in the same `records` array, each carrying its own `httpCode`: +```json +{ + "records": [ + { + "tableName": "cards", + "skyflowID": "f1714ef8-8deb-489a-a18d-77e0e007f403", + "fields": { + "cardNumber": [{"token": "f3907186-e7e2-466f-91e5-48e12c2bcbc1", "tokenGroupName": "deterministic_string"}] + }, + "httpCode": 200 + }, + { + "error": "Invalid request. Table name table not present for record. Specify a valid table name.", + "skyflowID": null, + "tableName": "", + "httpCode": 400 } - }] + ] +} +``` + +#### If the entire request fails (for example, the vault itself can't be found): +This is the API's raw wire-format error - `onFailure` doesn't hand you this dictionary as-is, it's normalized into a `Skyflow.SkyflowError` object with these same fields as flat properties (no `error` wrapper key): +```json +{ + "grpcCode": 5, + "httpCode": 404, + "message": "Invalid request. Vault not found for vaultID: sd0ff0b064c04faabd4d392512b2e5. Specify a valid vaultID. - request-id: cb397-8521-42c2-870c-92dbeec", + "httpStatus": "Not Found", + "details": [] + } +``` +`Skyflow.CollectCallback`/`Skyflow.RevealCallback`'s `onFailure` is typed `(Skyflow.SkyflowError) -> Void` - this shape (and every other failure, including client-side validation) is always delivered as a `Skyflow.SkyflowError`, with `httpCode`, `message`, `grpcCode`, `httpStatus`, and `details` all available as direct properties (`grpcCode`/`httpStatus`/`details` are only populated for this whole-request-failure shape - `nil` for validation failures): +```swift +onFailure: { (skyflowError: Skyflow.SkyflowError) in + print(skyflowError.httpCode, skyflowError.message, skyflowError.grpcCode ?? "", skyflowError.httpStatus ?? "", skyflowError.details ?? "") } ``` @@ -584,7 +585,7 @@ let collectElementInput = Skyflow.CollectElementInput( altText: String, // (DEPRECATED) optional that acts as an initial value for the collect element validations: ValidationSet, // optional set of validations for the input element type: Skyflow.ElementType, // Skyflow.ElementType enum - skyflowID: String, // The skyflow_id of the record to be updated. + skyflowId: String, // The skyflow_id of the record to be updated. ) ``` @@ -592,7 +593,6 @@ The `table` and `column` fields indicate which table and column in the vault the **Note**: - Use dot delimited strings to specify columns nested inside JSON fields (e.g. `address.street.line1`) -- `table` and `column` are optional only if the element is being used in invokeConnection() Along with `CollectElementInput`, you can define other options in the `CollectElementOptions` object which is described below. @@ -629,25 +629,38 @@ func clearFieldsOnSubmit(_ elements: [TextField]) { ### Step 4 : Update data from Elements When the form is ready to submit, call the `collect(options?)` method on the container object. The `options` parameter takes a object of optional parameters as shown below: -- `tokens`: indicates whether tokens for the collected data should be returned or not. Defaults to 'true' -- `additionalFields`: Non-PCI elements data to update or insert into the vault which should be in the records object format. -- `upsert`: To support upsert operations while collecting data from Skyflow elements, pass the table and column marked as unique in the table. +- `additionalFields`: A `Skyflow.AdditionalFields` object - non-PCI records to update or insert into the vault alongside whatever's collected from the mounted elements. +- `upsert`: An array of `Skyflow.UpsertOption` objects to support upsert while collecting data from Skyflow elements. Each option specifies the `table`, the `uniqueColumns` used to match existing records, and an optional `updateType` (`UpdateType.UPDATE` merges the new fields into the matched record, `UpdateType.REPLACE` replaces it). ```swift // Non-PCI records -let nonPCIRecords = ["table": "persons", "fields": [["gender": "MALE", "skyflowID": "value"]]] +let nonPCIRecords = Skyflow.AdditionalFields(records: [ + Skyflow.AdditionalFieldsRecord(table: "persons", fields: ["gender": "MALE"], skyflowId: "value") +]) // Upsert -let upsertOptions = [["table": "cards", "column": "cardNumber"]] as [[String : Any]] -// Send the non-PCI records as additionalFields of InsertOptions (optional) and apply upsert using `upsert` field of InsertOptions (optional) - -let options = Skyflow.CollectOptions(tokens: true, additionalFields: nonPCIRecords) - -//Custom callback - implementation of Skyflow.callback -let insertCallback = InsertCallback() +let upsertOptions = [Skyflow.UpsertOption(table: "cards", uniqueColumns: ["cardNumber"], updateType: .UPDATE)] +// Send the non-PCI records as additionalFields of CollectOptions (optional) and apply upsert using `upsert` field of CollectOptions (optional) + +let options = Skyflow.CollectOptions(additionalFields: nonPCIRecords) + +let insertCallback = Skyflow.CollectCallback( + onSuccess: { (response: Skyflow.CollectResponse) in + for record in response.records { + if let error = record.error { + print("failed:", error, "httpCode:", record.httpCode) + } else { + print("success:", record) + } + } + }, + onFailure: { (skyflowError: Skyflow.SkyflowError) in + print(skyflowError.httpCode, skyflowError.message, skyflowError.grpcCode ?? "", skyflowError.httpStatus ?? "", skyflowError.details ?? "") + } +) container?.collect(callback: insertCallback, options: options) ``` -**Note:** `skyflowID` is required if you want to update the data. If `skyflowID` isn't specified, the `collect(options?)` method creates a new record in the vault. +**Note:** `skyflowId` is required if you want to update the data. If `skyflowId` isn't specified, the `collect(options?)` method creates a new record in the vault. ### End to end example of updating data with Skyflow Elements ```swift @@ -683,7 +696,7 @@ let input = Skyflow.CollectElementInput( label: "card number", placeholder: "card number", type: Skyflow.ElementType.CARD_NUMBER, - skyflowID: "431eaa6c-5c15-4513-aa15-29f50babe882" + skyflowId: "431eaa6c-5c15-4513-aa15-29f50babe882" ) // Create an option to require the element. @@ -695,28 +708,34 @@ let skyflowElement = container?.create(input: input, options: requiredOption) // Can interact with this object as a normal UIView Object and add to View // Non-PCI records -let nonPCIRecords = [["table": "persons", "fields": ["gender": "MALE"]], ["table": "cards", "fields": ["first_name": "Joe"], "skyflowID": "431eaa6c-5c15-4513-aa15-29f50babe882"]] +let nonPCIRecords = Skyflow.AdditionalFields(records: [ + Skyflow.AdditionalFieldsRecord(table: "persons", fields: ["gender": "MALE"]), + Skyflow.AdditionalFieldsRecord(table: "cards", fields: ["first_name": "Joe"], skyflowId: "431eaa6c-5c15-4513-aa15-29f50babe882") +]) //Upsert options - let upsertOptions = [["table": "cards", "column": "cardNumber"]] as [[String : Any]] + let upsertOptions = [Skyflow.UpsertOption(table: "cards", uniqueColumns: ["cardNumber"], updateType: .UPDATE)] // Send the Non-PCI records as additionalFields of CollectOptions (optional) and apply upsert using optional field `upsert` of CollectOptions. -let collectOptions = Skyflow.CollectOptions(tokens: true, additionalFields: nonPCIRecords, upsert: upsertOptions) - - -//Implement a custom Skyflow.Callback to call on Insertion success/failure. -public class InsertCallback: Skyflow.Callback { - public func onSuccess(_ responseBody: Any) { - print(responseBody) - } - public func onFailure(_ error: Any) { - print(error) - } -} - - -// Initialize custom Skyflow.Callback. -let insertCallback = InsertCallback() +let collectOptions = Skyflow.CollectOptions(additionalFields: nonPCIRecords, upsert: upsertOptions) + + +// Initialize a Skyflow.CollectCallback - required by CollectContainer's collect(callback:options:). +// See "If the entire request fails" above for how onFailure surfaces a typed Skyflow.SkyflowError. +let insertCallback = Skyflow.CollectCallback( + onSuccess: { (response: Skyflow.CollectResponse) in + for record in response.records { + if let error = record.error { + print("failed:", error, "httpCode:", record.httpCode) + } else { + print("success:", record) + } + } + }, + onFailure: { (skyflowError: Skyflow.SkyflowError) in + print(skyflowError.httpCode, skyflowError.message, skyflowError.grpcCode ?? "", skyflowError.httpStatus ?? "", skyflowError.details ?? "") + } +) // Call collect method on CollectContainer. container?.collect(callback: insertCallback, options: collectOptions) @@ -728,23 +747,61 @@ container?.collect(callback: insertCallback, options: collectOptions) { "records": [ { - "table": "persons", + "tableName": "persons", + "skyflowID": "77dc3caf-c452-49e1-8625-07219d7567bf", "fields": { - "gender": "12f670af-6c7d-4837-83fb-30365fbc0b1e", - "skyflow_id": "77dc3caf-c452-49e1-8625-07219d7567bf" - } + "gender": [{"token": "12f670af-6c7d-4837-83fb-30365fbc0b1e", "tokenGroupName": "deterministic_string"}] + }, + "httpCode": 200 }, { - "table": "cards", + "tableName": "cards", + "skyflowID": "431eaa6c-5c15-4513-aa15-29f50babe882", "fields": { - "skyflow_id": "431eaa6c-5c15-4513-aa15-29f50babe882", - "cardNumber": "f3907186-e7e2-466f-91e5-48e12c2bcbc1", - "first_name": "131e70dc-6f76-4319-bdd3-96281e051051" - } + "cardNumber": [{"token": "f3907186-e7e2-466f-91e5-48e12c2bcbc1", "tokenGroupName": "deterministic_string"}], + "first_name": [{"token": "131e70dc-6f76-4319-bdd3-96281e051051", "tokenGroupName": "deterministic_string"}] + }, + "httpCode": 200 } ] } ``` +**Note:** Successful and failed records are both returned in the same `records` array, each carrying its own `httpCode`. + +#### Sample partial error response: +```json +{ + "records": [ + { + "tableName": "cards", + "skyflowID": "431eaa6c-5c15-4513-aa15-29f50babe882", + "fields": { + "cardNumber": [{"token": "f3907186-e7e2-466f-91e5-48e12c2bcbc1", "tokenGroupName": "deterministic_string"}] + }, + "httpCode": 200 + }, + { + "error": "Update failed. skyflow_ids [77dc3caf-c452-49e1-8625-07219d7567bf] are invalid. Specify valid Skyflow IDs.", + "skyflowID": null, + "tableName": "", + "httpCode": 400 + } + ] +} +``` + +#### If the entire request fails +This is the API's error - `onFailure` doesn't hand you this dictionary as-is, it's normalized into a `Skyflow.SkyflowError` object with these same fields as flat properties (no `error` wrapper key): +```json +{ + "grpcCode": 5, + "httpCode": 404, + "message": "Invalid request. Vault not found for vaultID: sd0ff0b064c04faabd4d392512b2e5. Specify a valid vaultID. - request-id: cb397-8521-42c2-870c-92dbeec", + "httpStatus": "Not Found", + "details": [] +} +``` +`Skyflow.CollectCallback`'s `onFailure` is typed `(Skyflow.SkyflowError) -> Void` - see [If the entire request fails](#if-the-entire-request-fails-for-example-the-vault-itself-cant-be-found) above for how to read the properties on `Skyflow.SkyflowError`. ## Validations @@ -1207,24 +1264,38 @@ func clearFieldsOnSubmit(_ elements: [TextField]) { } ``` ### Step 4: Collect data from elements -When you submit the form, call the `collect(options: Skyflow.CollectOptions? = nil, callback: Skyflow.Callback)` method on the container object. +When you submit the form, call the `collect(callback: Skyflow.Callback, options: Skyflow.CollectOptions? = Skyflow.CollectOptions())` method on the container object. The options parameter takes a `Skyflow.CollectOptions` object as shown below: - `tokens`: Whether or not tokens for the collected data are returned. Defaults to 'true' -- `additionalFields`: Non-PCI elements data to insert into the vault, specified in the records object format. -- `upsert`: To support upsert operations, the table containing the data and a column marked as unique in that table. +- `additionalFields`: A `Skyflow.AdditionalFields` object - non-PCI records to insert into the vault alongside whatever's collected from the mounted elements. +- `upsert`: An array of `Skyflow.UpsertOption` objects to support upsert while collecting data from Skyflow elements. Each option specifies the `table`, the `uniqueColumns` used to match existing records, and an optional `updateType` (`UpdateType.UPDATE` merges the new fields into the matched record, `UpdateType.REPLACE` replaces it). ```swift // Non-PCI records -let nonPCIRecords = ["table": "persons", "fields": [["gender": "MALE"]]] +let nonPCIRecords = Skyflow.AdditionalFields(records: [ + Skyflow.AdditionalFieldsRecord(table: "persons", fields: ["gender": "MALE"]) +]) // Upsert -let upsertOptions = [["table": "cards", "column": "cardNumber"]] as [[String : Any]] -// Send the non-PCI records as additionalFields of InsertOptions (optional) and apply upsert using `upsert` field of InsertOptions (optional) - -let options = Skyflow.CollectOptions(tokens: true, additionalFields: nonPCIRecords, upsert: upsertOptions) - -//Custom callback - implementation of Skyflow.callback -let insertCallback = InsertCallback() +let upsertOptions = [Skyflow.UpsertOption(table: "cards", uniqueColumns: ["cardNumber"], updateType: .UPDATE)] +// Send the non-PCI records as additionalFields of CollectOptions (optional) and apply upsert using `upsert` field of CollectOptions (optional) + +let options = Skyflow.CollectOptions(additionalFields: nonPCIRecords, upsert: upsertOptions) + +let insertCallback = Skyflow.CollectCallback( + onSuccess: { (response: Skyflow.CollectResponse) in + for record in response.records { + if let error = record.error { + print("failed:", error, "httpCode:", record.httpCode) + } else { + print("success:", record) + } + } + }, + onFailure: { (skyflowError: Skyflow.SkyflowError) in + print(skyflowError.httpCode, skyflowError.message, skyflowError.grpcCode ?? "", skyflowError.httpStatus ?? "", skyflowError.details ?? "") + } +) container?.collect(callback: insertCallback, options: options) ``` End to end example of collecting data with Composable Elements @@ -1305,46 +1376,81 @@ do { } // Non-PCI records -let nonPCIRecords = ["table": "persons", "fields": [["gender": "MALE"]]] +let nonPCIRecords = Skyflow.AdditionalFields(records: [ + Skyflow.AdditionalFieldsRecord(table: "persons", fields: ["gender": "MALE"]) +]) //Upsert options -let upsertOptions = [["table": "cards", "column": "cardNumber"]] as [[String : Any]] +let upsertOptions = [Skyflow.UpsertOption(table: "cards", uniqueColumns: ["cardNumber"], updateType: .UPDATE)] // Send the Non-PCI records as additionalFields of CollectOptions (optional) and apply upsert using optional field `upsert` of CollectOptions. -let collectOptions = Skyflow.CollectOptions(tokens: true, additionalFields: nonPCIRecords, upsert: upsertOptions) +let collectOptions = Skyflow.CollectOptions(additionalFields: nonPCIRecords, upsert: upsertOptions) -//Implement a custom Skyflow.Callback to call on Insertion success/failure. -public class InsertCallback: Skyflow.Callback { - public func onSuccess(_ responseBody: Any) { - print(responseBody) - } - public func onFailure(_ error: Any) { - print(error) - } -} - -// Initialize custom Skyflow.Callback. -let insertCallback = InsertCallback() +// Initialize a Skyflow.CollectCallback - required by CollectContainer's collect(callback:options:). +let insertCallback = Skyflow.CollectCallback( + onSuccess: { (response: Skyflow.CollectResponse) in + for record in response.records { + if let error = record.error { + print("failed:", error, "httpCode:", record.httpCode) + } else { + print("success:", record) + } + } + }, + onFailure: { (skyflowError: Skyflow.SkyflowError) in + print(skyflowError.httpCode, skyflowError.message, skyflowError.grpcCode ?? "", skyflowError.httpStatus ?? "", skyflowError.details ?? "") + } +) // Call collect method on CollectContainer. container?.collect(callback: insertCallback, options: collectOptions) ``` ### Sample Response: -``` +```json { - "records": [ { - "table": "cards", - "fields": { - "cardNumber": "f3907186-e7e2-466f-91e5-48e12c2bcbc1" + "records": [ + { + "tableName": "cards", + "skyflowID": "f1714ef8-8deb-489a-a18d-77e0e007f403", + "fields": { + "cardNumber": [{"token": "f3907186-e7e2-466f-91e5-48e12c2bcbc1", "tokenGroupName": "deterministic_string"}] + }, + "httpCode": 200 + }, + { + "tableName": "persons", + "skyflowID": "77dc3caf-c452-49e1-8625-07219d7567bf", + "fields": { + "gender": [{"token": "12f670af-6c7d-4837-83fb-30365fbc0b1e", "tokenGroupName": "deterministic_string"}] + }, + "httpCode": 200 } - }, { - "table": "persons", - "fields": { - "gender": "12f670af-6c7d-4837-83fb-30365fbc0b1e", + ] +} +``` +**Note:** Successful and failed records are both returned in the same `records` array, each carrying its own `httpCode`. + +#### Sample partial error response: +```json +{ + "records": [ + { + "tableName": "cards", + "skyflowID": "f1714ef8-8deb-489a-a18d-77e0e007f403", + "fields": { + "cardNumber": [{"token": "f3907186-e7e2-466f-91e5-48e12c2bcbc1", "tokenGroupName": "deterministic_string"}] + }, + "httpCode": 200 + }, + { + "error": "Invalid request. Table name table not present for record. Specify a valid table name.", + "skyflowID": null, + "tableName": "", + "httpCode": 400 } - }] + ] } ``` [For information on validations, see validations.](#validations) @@ -1543,7 +1649,7 @@ let lengthRule = LengthMatchRule( // Update validations and placeholder property on cardHolderName. cardHolderName.update(update: CollectElementInput( placeholder: "cardHolderName", - validations: ValidationSet(rules: [lengthRule])) + validations: ValidationSet(rules: [lengthRule]) )) ``` @@ -1581,7 +1687,7 @@ do { // Subscribing to the `SUBMIT` event, which gets triggered when the user hits `enter` key in any container element input. composableContainer?.on(eventName: .SUBMIT){ // Your implementation when the SUBMIT(enter) event occurs. - print('Submit Event Listener is being Triggered.') + print("Submit Event Listener is being Triggered.") } ``` @@ -1616,7 +1722,7 @@ let composableElementInput = Skyflow.CollectElementInput( altText: String, // (DEPRECATED) optional that acts as an initial value for the collect element validations: ValidationSet, // optional set of validations for the input element type: Skyflow.ElementType, // Skyflow.ElementType enum - skyflowID: String, // The skyflow_id of the record to be updated. + skyflowId: String, // The skyflow_id of the record to be updated. ) ``` The `table` and `column` fields indicate which table and column in the vault the Element correspond to. @@ -1665,24 +1771,38 @@ func clearFieldsOnSubmit(_ elements: [TextField]) { } ``` ### Step 4: Update data from Elements -When you submit the form, call the `collect(options: Skyflow.CollectOptions? = nil, callback: Skyflow.Callback)` method on the container object. +When you submit the form, call the `collect(callback: Skyflow.Callback, options: Skyflow.CollectOptions? = Skyflow.CollectOptions())` method on the container object. The options parameter takes a `Skyflow.CollectOptions` object as shown below: - `tokens`: Whether or not tokens for the collected data are returned. Defaults to 'true' -- `additionalFields`: Non-PCI elements data to insert into the vault, specified in the records object format. -- `upsert`: To support upsert operations, the table containing the data and a column marked as unique in that table. +- `additionalFields`: A `Skyflow.AdditionalFields` object - non-PCI records to insert into the vault alongside whatever's collected from the mounted elements. +- `upsert`: An array of `Skyflow.UpsertOption` objects to support upsert while collecting data from Skyflow elements. Each option specifies the `table`, the `uniqueColumns` used to match existing records, and an optional `updateType` (`UpdateType.UPDATE` merges the new fields into the matched record, `UpdateType.REPLACE` replaces it). ```swift // Non-PCI records -let nonPCIRecords = ["table": "persons", "fields": [["gender": "MALE"]]] +let nonPCIRecords = Skyflow.AdditionalFields(records: [ + Skyflow.AdditionalFieldsRecord(table: "persons", fields: ["gender": "MALE"]) +]) // Upsert -let upsertOptions = [["table": "cards", "column": "cardNumber"]] as [[String : Any]] -// Send the non-PCI records as additionalFields of InsertOptions (optional) and apply upsert using `upsert` field of InsertOptions (optional) - -let options = Skyflow.CollectOptions(tokens: true, additionalFields: nonPCIRecords, upsert: upsertOptions) - -//Custom callback - implementation of Skyflow.callback -let insertCallback = InsertCallback() +let upsertOptions = [Skyflow.UpsertOption(table: "cards", uniqueColumns: ["cardNumber"], updateType: .UPDATE)] +// Send the non-PCI records as additionalFields of CollectOptions (optional) and apply upsert using `upsert` field of CollectOptions (optional) + +let options = Skyflow.CollectOptions(additionalFields: nonPCIRecords, upsert: upsertOptions) + +let insertCallback = Skyflow.CollectCallback( + onSuccess: { (response: Skyflow.CollectResponse) in + for record in response.records { + if let error = record.error { + print("failed:", error, "httpCode:", record.httpCode) + } else { + print("success:", record) + } + } + }, + onFailure: { (skyflowError: Skyflow.SkyflowError) in + print(skyflowError.httpCode, skyflowError.message, skyflowError.grpcCode ?? "", skyflowError.httpStatus ?? "", skyflowError.details ?? "") + } +) container?.collect(callback: insertCallback, options: options) ``` End to end example of collecting data with Composable Elements @@ -1719,7 +1839,7 @@ let cardHolderNameElementInput = Skyflow.CollectElementInput( label: "first name", placeholder: "first name", type: Skyflow.ElementType.CARDHOLDER_NAME, - skyflowID: "431eaa6c-5c15-4513-aa15-29f50babe882" + skyflowId: "431eaa6c-5c15-4513-aa15-29f50babe882" ) // Create an option to require the element. let requiredOption = Skyflow.CollectElementOptions(required: true) @@ -1737,7 +1857,7 @@ let cardNumberElementInput = Skyflow.CollectElementInput( label: "card number", placeholder: "card number", type: Skyflow.ElementType.CARD_NUMBER, - skyflowID: "431eaa6c-5c15-4513-aa15-29f50babe882" + skyflowId: "431eaa6c-5c15-4513-aa15-29f50babe882" ) let cardNumberElement = container?.create(input: cardNumberElementInput, options: requiredOption) @@ -1751,7 +1871,7 @@ let cvvElementInput = Skyflow.CollectElementInput( label: "cvv", placeholder: "cvv", type: Skyflow.ElementType.CVV, - skyflowID: "431eaa6c-5c15-4513-aa15-29f50babe882" + skyflowId: "431eaa6c-5c15-4513-aa15-29f50babe882" ) let cvvElement = container?.create(input: cvvElementInput, options: requiredOption) @@ -1766,26 +1886,31 @@ do { } // Non-PCI records -let nonPCIRecords = [["table": "persons", "fields": ["gender": "MALE"],"skyflowID": "77dc3caf-c452-49e1-8625-07219d7567bf"]] +let nonPCIRecords = Skyflow.AdditionalFields(records: [ + Skyflow.AdditionalFieldsRecord(table: "persons", fields: ["gender": "MALE"], skyflowId: "77dc3caf-c452-49e1-8625-07219d7567bf") +]) //Upsert options - let upsertOptions = [["table": "cards", "column": "cardNumber"]] as [[String : Any]] + let upsertOptions = [Skyflow.UpsertOption(table: "cards", uniqueColumns: ["cardNumber"], updateType: .UPDATE)] // Send the Non-PCI records as additionalFields of CollectOptions (optional) and apply upsert using optional field `upsert` of CollectOptions. -let collectOptions = Skyflow.CollectOptions(tokens: true, additionalFields: nonPCIRecords, upsert: upsertOptions) - -//Implement a custom Skyflow.Callback to call on Insertion success/failure. -public class InsertCallback: Skyflow.Callback { - public func onSuccess(_ responseBody: Any) { - print(responseBody) - } - public func onFailure(_ error: Any) { - print(error) - } -} - -// Initialize custom Skyflow.Callback. -let insertCallback = InsertCallback() +let collectOptions = Skyflow.CollectOptions(additionalFields: nonPCIRecords, upsert: upsertOptions) + +// Initialize a Skyflow.CollectCallback - required by CollectContainer's collect(callback:options:). +let insertCallback = Skyflow.CollectCallback( + onSuccess: { (response: Skyflow.CollectResponse) in + for record in response.records { + if let error = record.error { + print("failed:", error, "httpCode:", record.httpCode) + } else { + print("success:", record) + } + } + }, + onFailure: { (skyflowError: Skyflow.SkyflowError) in + print(skyflowError.httpCode, skyflowError.message, skyflowError.grpcCode ?? "", skyflowError.httpStatus ?? "", skyflowError.details ?? "") + } +) // Call collect method on CollectContainer. container?.collect(callback: insertCallback, options: collectOptions) @@ -1795,250 +1920,69 @@ container?.collect(callback: insertCallback, options: collectOptions) { "records": [ { - "table": "persons", + "tableName": "persons", + "skyflowID": "77dc3caf-c452-49e1-8625-07219d7567bf", "fields": { - "gender": "12f670af-6c7d-4837-83fb-30365fbc0b1e", - "skyflow_id": "77dc3caf-c452-49e1-8625-07219d7567bf" - } + "gender": [{"token": "12f670af-6c7d-4837-83fb-30365fbc0b1e", "tokenGroupName": "deterministic_string"}] + }, + "httpCode": 200 }, { - "table": "cards", + "tableName": "cards", + "skyflowID": "431eaa6c-5c15-4513-aa15-29f50babe882", "fields": { - "skyflow_id": "431eaa6c-5c15-4513-aa15-29f50babe882", - "cardNumber": "f3907186-e7e2-466f-91e5-48e12c2bcbc1", - "first_name": "131e70dc-6f76-4319-bdd3-96281e051051", - "cvv": "098834fe-de99-4fc8-abdf-88c18a28a2cf" - } + "cardNumber": [{"token": "f3907186-e7e2-466f-91e5-48e12c2bcbc1", "tokenGroupName": "deterministic_string"}], + "first_name": [{"token": "131e70dc-6f76-4319-bdd3-96281e051051", "tokenGroupName": "deterministic_string"}], + "cvv": [{"token": "098834fe-de99-4fc8-abdf-88c18a28a2cf", "tokenGroupName": "deterministic_string"}] + }, + "httpCode": 200 } ] } ``` ### Sample Partial Error Response: +Successful and failed records are both returned together in the same `records` array, each carrying its own `httpCode`: ```json { "records": [ { - "table": "cards", + "tableName": "cards", + "skyflowID": "431eaa6c-5c15-4513-aa15-29f50babe882", "fields": { - "skyflow_id": "431eaa6c-5c15-4513-aa15-29f50babe882", - "cardNumber": "f3907186-e7e2-466f-91e5-48e12c2bcbc1", - "first_name": "131e70dc-6f76-4319-bdd3-96281e051051", - "cvv": "098834fe-de99-4fc8-abdf-88c18a28a2cf" - } - } - ], - "errors": [ + "cardNumber": [{"token": "f3907186-e7e2-466f-91e5-48e12c2bcbc1", "tokenGroupName": "deterministic_string"}], + "first_name": [{"token": "131e70dc-6f76-4319-bdd3-96281e051051", "tokenGroupName": "deterministic_string"}], + "cvv": [{"token": "098834fe-de99-4fc8-abdf-88c18a28a2cf", "tokenGroupName": "deterministic_string"}] + }, + "httpCode": 200 + }, { - "error": { - "code": 400, - "message": "Update failed. skyflow_ids [77dc3caf-c452-49e1-8625-07219d7567bf] are invalid. Specify valid Skyflow IDs. - request-id: cb397-8521-42c2-870c-92dbeec" - } + "error": "Update failed. skyflow_ids [77dc3caf-c452-49e1-8625-07219d7567bf] are invalid. Specify valid Skyflow IDs.", + "skyflowID": null, + "tableName": "", + "httpCode": 400 } ] } ``` +If the entire request fails (for example, the vault itself can't be found), `onFailure` receives a single structured `Skyflow.SkyflowError` instead of a `records` array. +Below is the API's error that `SkyflowError` is built from - the object itself exposes these as flat properties (`grpcCode`, `httpCode`, `message`, `httpStatus`, `details`): +```json + + "grpcCode": 5, + "httpCode": 404, + "message": "Invalid request. Vault not found for vaultID: sd0ff0b064c04faabd4d392512b2e5. Specify a valid vaultID. - request-id: cb397-8521-42c2-870c-92dbeec", + "httpStatus": "Not Found", + "details": [] + } +``` + # Securely revealing data client-side -- [**Retrieving data from the vault**](#retrieving-data-from-the-vault) - [**Using Skyflow Elements to reveal data**](#using-skyflow-elements-to-reveal-data) - [**UI Error for Reveal Elements**](#ui-error-for-reveal-elements) - [**Set token for Reveal Elements**](#set-token-for-reveal-elements) - [**Set and clear altText for Reveal Elements**](#set-and-clear-alttext-for-reveal-elements) -## Retrieving data from the vault -For non-PCI use-cases, retrieving data from the vault and revealing it in the mobile can be done either using the SkyflowID's or tokens as described below - -- ### Using Skyflow tokens - To retrieve record data using tokens, use the `detokenize(records)` method. The records parameter takes a Dictionary object that contains tokens for `record` values to fetch - ```swift - [ - "records": [ - [ - "token": String, - "redaction": Skyflow.RedactionType // Optional. Redaction to apply for retrieved data. - ] - ] - ] - ``` - Note: `redaction` defaults to [RedactionType.PLAIN_TEXT](#redaction-types). - -The following example code makes a detokenize call to reveal the masked value of a token: - -```swift - let getCallback = GetCallback() // Custom callback - implementation of Skyflow.Callback - - let records = [ - "records": [ - [ - "token": "45012507-f72b-4f5c-9bf9-86b133bae719", - ], - [ - "token": "1r434532-6f76-4319-bdd3-96281e051051", - "redaction": Skyflow.RedactionType.MASKED - ] - ] - ] as [String: Any] - - skyflowClient.detokenize(records: records, callback: getCallback) - ``` - The sample response: - ```json - { - "records": [ - { - "token": "131e70dc-6f76-4319-bdd3-96281e051051", - "value": "1990-01-01" - }, - { - "token": "1r434532-6f76-4319-bdd3-96281e051051", - "value": "xxxxxxer", - } - ] - } - ``` - -- ### Using Skyflow ID's or Unique Column Values - For retrieving data from the vault, use the `get(records: JSONObject, options: GetOptions? = GetOptions(), callback: Skyflow.Callback)` method. - The `records` parameter takes a Dictionary object that contains `records` to be fetched as shown below. Each object inside array should contain: - - - Either an array of Skyflow IDs to fetch - - Or a column name and an array of column values - - The second parameter, `options`, is a `GetOptions` object that retrieves tokens of Skyflow IDs. - - Notes: - - You can use either Skyflow IDs or unique values to retrieve records. You can't use both at the same time. - - GetOptions parameter is applicable only for retrieving tokens using Skyflow ID. - - You can't pass GetOptions along with the redaction type. - - tokens defaults to false. - - ```swift - [ - "records": [ - [ - "ids": ArrayList(), // Array of SkyflowID's of the records to be fetched - "table": String, // Name of table holding the above skyflow_id's - "redaction": Skyflow.RedactionType // Redaction to be applied to retrieved data - ], - [ - "columnValues": ArrayList(), // Array of column values of the records to be fetched - "table": String, // Name of table holding the above skyflow_id's - "columnName": String, // A unique column name - "redaction": Skyflow.RedactionType // Redaction to be applied to retrieved data - ], - ] - ] - ``` - ### Redaction Types - There are 4 accepted values in Skyflow.RedactionTypes: - - `PLAIN_TEXT` - - `MASKED` - - `REDACTED` - - `DEFAULT` - - An example of get call to fetch records: - ```swift - let getCallback = GetCallback() // Custom callback - implementation of Skyflow.Callback - - let skyflowIDs = ["f8d8a622-b557-4c6b-a12c-c5ebe0b0bfd9"] - let record = ["ids": skyflowIDs, "table": "cards", "redaction": Skyflow.RedactionType.PLAIN_TEXT] as [String : Any] - - let columnValues = ["12345"] - let columnRecord = ["columnValues": columnValues,"columnName": "card_pin", "table": "cards", "redaction": Skyflow.RedactionType.PLAIN_TEXT] as [String : Any] - - let invalidID = ["invalid skyflow ID"] - let badRecord = ["ids": invalidID, "table": "cards", "redaction": Skyflow.RedactionType.PLAIN_TEXT] as [String : Any] - - let records = ["records": [record,columnRecord, badRecord]] - - skyflowClient.get(records: records, callback: getCallback) - ``` - - The sample response: - ```json - { - "records": [{ - "fields": { - "card_number": "4111111111111111", - "cvv": "127", - "expiry_date": "11/35", - "fullname": "myname", - "id": "f8d8a622-b557-4c6b-a12c-c5ebe0b0bfd9" - }, - "table": "cards" - },{ - "fields": { - "card_number": "4111111111111111", - "cvv": "345", - "card_pin":"12345" - "expiry_date": "12/29", - "fullname": "joey", - "id": "ed2e7851-9f84-4d70-9e5d-182f6d8d4fa3" - }, - "table": "cards" - } - ], - "errors": [ { - "error": { - "code": "404", - "description": "No Records Found" - }, - "ids": ["invalid skyflow id"] - }] - } - ``` - - An example of get call to fetch tokens: - - ```swift - let getCallback = GetCallback() // Custom callback - implementation of Skyflow.Callback - - let skyflowIDs = ["f8d8a622-b557-4c6b-a12c-c5ebe0b0bfd9", "da26de53-95d5-4bdb-99db-8d8c66a35ff9"] - let record = ["ids": skyflowIDs, "table": "cards", "redaction": Skyflow.RedactionType.PLAIN_TEXT] as [String : Any] - - let invalidID = ["invalid skyflow ID"] - let badRecord = ["ids": invalidID, "table": "cards", "redaction": Skyflow.RedactionType.PLAIN_TEXT] as [String : Any] - - let records = ["records": [record, badRecord]] - - skyflowClient.get(records: records, options: GetOptions(tokens: true), callback: getCallback) - ``` - The sample Response: - ```json - { - "records": [ - { - "fields": { - "card_number": "9802-3257-3113-0294", - "expiry_date": "45012507-f72b-4f5c-9bf9-86b133bae719", - "fullname": "131e2507-f72b-4f5c-9bf9-86b133bae719", - "id": "f8d8a622-b557-4c6b-a12c-c5ebe0b0bfd9" - }, - "table": "cards" - }, - { - "fields": { - "card_number": "0294-3213-3157-9802", - "expiry_date": "131e2507-f72b-4f5c-9bf9-86b133bae719", - "fullname": "45012507-f72b-4f5c-9bf9-86b133bae719", - "id": "ed2e7851-9f84-4d70-9e5d-182f6d8d4fa3" - }, - "table": "cards" - } - ], - "errors": [ - { - "error": { - "code": "404", - "description": "No Records Found" - }, - "ids": ["invalid skyflow id"] - } - ] -} - ``` - - ## Using Skyflow Elements to reveal data Skyflow Elements can be used to securely reveal data in an application without exposing your front end to the sensitive data. This is great for use-cases like card issuance where you may want to reveal the card number to a user without increasing your PCI compliance scope. ### Step 1: Create a container @@ -2058,12 +2002,10 @@ let revealElementInput = Skyflow.RevealElementInput( errorTextStyles: Skyflow.Styles(), // optional styles that will be applied to the errorText of the reveal element label: "cardNumber", // optional, label for the element, altText: "XXXX XXXX XXXX XXXX", // optional, string that is shown before reveal, will show token if it is not provided - redaction: Skyflow.RedactionType, // optional. Redaction to apply for retrieved data. E.g. RedactionType.MASKED ) ``` `Note`: -- `token` is optional only if it is being used in invokeConnection() -- `redaction` defaults to [`RedactionType.PLAIN_TEXT`](#redaction-types). +- To apply a redaction to a token group as part of the detokenize call, use `tokenGroupRedactions` on `Skyflow.RevealOptions` (passed to `reveal(callback:options:)`) rather than on the individual `RevealElementInput` - see [Step 4: Reveal data](#step-4-reveal-data). If not provided, the vault-configured default redaction is applied. Supported redaction values are `PLAIN_TEXT`, `MASKED`, `REDACTED` and `DEFAULT`. The `inputStyles` parameter accepts a styles object as described in the [previous section](#step-2-create-a-collect-element) for collecting data but the only state available for a reveal element is the base state. @@ -2140,9 +2082,33 @@ Elements used for revealing data are mounted to the screen the same way as Eleme ### Step 4: Reveal data When the sensitive data is ready to be retrieved and revealed, call the `reveal()` method on the container as shown below: ```swift -let revealCallback = RevealCallback() // Custom callback - implementation of Skyflow.Callback +let revealCallback = Skyflow.RevealCallback( + onSuccess: { (response: Skyflow.RevealResponse) in + for record in response.records { + if let error = record.error { + print("failed:", error, "httpCode:", record.httpCode) + } else { + print("revealed:", record) + } + } + }, + onFailure: { (skyflowError: Skyflow.SkyflowError) in + print(skyflowError.httpCode, skyflowError.message, skyflowError.grpcCode ?? "", skyflowError.httpStatus ?? "", skyflowError.details ?? "") + } +) container.reveal(callback: revealCallback) ``` + +To apply a redaction to one or more token groups as part of the detokenize call, pass `tokenGroupRedactions` via `Skyflow.RevealOptions`: +```swift +let revealOptions = Skyflow.RevealOptions( + tokenGroupRedactions: [ + Skyflow.TokenGroupRedaction(tokenGroupName: "deterministic_string", redaction: "MASKED") + ] +) +container.reveal(callback: revealCallback, options: revealOptions) +``` +This is a request-level setting - the redaction applies to every token in the named group, not to individual reveal elements. ### UI Error for Reveal Elements @@ -2188,8 +2154,7 @@ let cardNumberInput = Skyflow.RevealElementInput( labelStyles: labelStyles, errorTextStyles: errorTextStyles, label: "cardnumber", - altText: "XXXX XXXX XXXX XXXX", - redaction: SKyflow.RedactionType.MASKED + altText: "XXXX XXXX XXXX XXXX" ) let cardNumberElement = container?.create(input: cardNumberInput) @@ -2221,42 +2186,80 @@ cvvElement!.setError("custom error") // reset error to the element cvvElement!.resetError() -// Implement a custom Skyflow.Callback to be called on Reveal success/failure -public class RevealCallback: Skyflow.Callback { - public func onSuccess(_ responseBody: Any) { - print(responseBody) - } - public func onFailure(_ error: Any) { - print(error) +// Initialize a Skyflow.RevealCallback - required by RevealContainer's reveal(callback:options:). +let revealCallback = Skyflow.RevealCallback( + onSuccess: { (response: Skyflow.RevealResponse) in + for record in response.records { + if let error = record.error { + print("failed:", error, "httpCode:", record.httpCode) + } else { + print("revealed:", record) + } + } + }, + onFailure: { (skyflowError: Skyflow.SkyflowError) in + print(skyflowError.httpCode, skyflowError.message, skyflowError.grpcCode ?? "", skyflowError.httpStatus ?? "", skyflowError.details ?? "") } -} +) -// Initialize custom Skyflow.Callback -let revealCallback = RevealCallback() +// Optional: apply a redaction to a token group as part of the detokenize call +let revealOptions = Skyflow.RevealOptions( + tokenGroupRedactions: [ + Skyflow.TokenGroupRedaction(tokenGroupName: "deterministic_string", redaction: "MASKED") + ] +) // Call reveal method on RevealContainer -container?.reveal(callback: revealCallback) +container?.reveal(callback: revealCallback, options: revealOptions) ``` -The response below shows that some tokens assigned to the reveal elements get revealed successfully, while others fail and remain unrevealed. - #### Sample Response: ```json { - "success": [ { - "token": "b63ec4e0-bbad-4e43-96e6-6bd50f483f75" - }, - { - "token": "89024714-6a26-4256-b9d4-55ad69aa4047" - }], - "errors": [ { - "id": "a4b24714-6a26-4256-b9d4-55ad69aa4047", - "error": { - "code": 404, - "description": "Tokens not found for a4b24714-6a26-4256-b9d4-55ad69aa4047" + "records": [ + { + "token": "b63ec4e0-bbad-4e43-96e6-6bd50f483f75", + "tokenGroupName": "deterministic_string", + "metadata": { + "skyflowId": "3ac0424e-fe45-43a9-9193-2e6d2913cbd2", + "tableName": "cards" + }, + "httpCode": 200 + }, + { + "token": "89024714-6a26-4256-b9d4-55ad69aa4047", + "tokenGroupName": "deterministic_string", + "metadata": { + "skyflowId": "3ac0424e-fe45-43a9-9193-2e6d2913cbd2", + "tableName": "cards" + }, + "httpCode": 200 } - }] + ] +} +``` + +#### Sample Partial Error Response: +Some tokens assigned to the reveal elements get revealed successfully, while others fail and remain unrevealed - both are returned together in the same `records` array, each carrying its own `httpCode`. A successful record carries `metadata` (e.g. `skyflowId`, `tableName`), a failed one carries `error` instead: +```json +{ + "records": [ + { + "token": "b63ec4e0-bbad-4e43-96e6-6bd50f483f75", + "tokenGroupName": "deterministic_string", + "metadata": { + "skyflowId": "3ac0424e-fe45-43a9-9193-2e6d2913cbd2", + "tableName": "cards" + }, + "httpCode": 200 + }, + { + "token": "a4b24714-6a26-4256-b9d4-55ad69aa4047", + "error": "Tokens not found for a4b24714-6a26-4256-b9d4-55ad69aa4047", + "httpCode": 404 + } + ] } ``` diff --git a/Samples/CollectAndRevealSample/CollectAndRevealSample.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Samples/CollectAndRevealSample/CollectAndRevealSample.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 00000000..31459b56 --- /dev/null +++ b/Samples/CollectAndRevealSample/CollectAndRevealSample.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,15 @@ +{ + "originHash" : "9d7a641543031e54b3470012de29bf3cd9a042920b5fe71a347a8d0e07ccefe2", + "pins" : [ + { + "identity" : "skyflow-ios", + "kind" : "remoteSourceControl", + "location" : "https://github.com/skyflowapi/skyflow-iOS.git", + "state" : { + "revision" : "cb30c6b6b8a36105fa4bf8ba6a879af8b777d2c7", + "version" : "1.25.1" + } + } + ], + "version" : 3 +} diff --git a/Samples/CollectAndRevealSample/CollectAndRevealSample/CollectAndRevealViewController.swift b/Samples/CollectAndRevealSample/CollectAndRevealSample/CollectAndRevealViewController.swift index 48368c2d..d360b5e4 100644 --- a/Samples/CollectAndRevealSample/CollectAndRevealSample/CollectAndRevealViewController.swift +++ b/Samples/CollectAndRevealSample/CollectAndRevealSample/CollectAndRevealViewController.swift @@ -22,6 +22,7 @@ class CollectAndRevealViewController: UIViewController { private var revealButton: UIButton! private var revealed = false + private var tokenGroupRedactions: [Skyflow.TokenGroupRedaction] = [] override func viewDidLoad() { @@ -127,23 +128,23 @@ class CollectAndRevealViewController: UIViewController { stackView.addArrangedSubview(collectExpMonth!) stackView.addArrangedSubview(collectExpYear!) stackView.addArrangedSubview(collectButton) - + stackView.axis = .vertical stackView.distribution = .fill stackView.spacing = 5 stackView.alignment = .fill stackView.translatesAutoresizingMaskIntoConstraints = false - + let scrollView = UIScrollView(frame: .zero) scrollView.isScrollEnabled = true scrollView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(scrollView) - + scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 2).isActive = true scrollView.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor, constant: 2).isActive = true scrollView.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor, constant: -10).isActive = true scrollView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor).isActive = true - + scrollView.addSubview(stackView) stackView.widthAnchor.constraint(equalTo: scrollView.widthAnchor, constant: -10).isActive = true stackView.topAnchor.constraint(equalTo: scrollView.topAnchor).isActive = true @@ -153,26 +154,51 @@ class CollectAndRevealViewController: UIViewController { } } @objc func revealForm() { - self.revealContainer?.reveal(callback: ExampleAPICallback()) + let revealCallback = Skyflow.RevealCallback( + onSuccess: { (response: Skyflow.RevealResponse) in + for record in response.records { + if let error = record.error { + print("reveal failed:", error, "httpCode:", record.httpCode) + } else { + print("revealed:", record.token ?? "", record.tokenGroupName ?? "", record.metadata ?? [:]) + } + } + }, + onFailure: { (error: Skyflow.SkyflowError) in print("reveal failure:", error.httpCode, error.message) } + ) + let revealOptions = Skyflow.RevealOptions(tokenGroupRedactions: self.tokenGroupRedactions.isEmpty ? nil : self.tokenGroupRedactions) + self.revealContainer?.reveal(callback: revealCallback, options: revealOptions) } @objc func submitForm() { - let exampleAPICallback = ExampleAPICallback(updateSuccess: updateSuccess, updateFailure: updateFailure) - container!.collect(callback: exampleAPICallback, options: Skyflow.CollectOptions(tokens: true)) + let collectCallback = Skyflow.CollectCallback(onSuccess: updateSuccess, onFailure: updateFailure) + container!.collect(callback: collectCallback, options: Skyflow.CollectOptions()) } - internal func updateSuccess(_ response: SuccessResponse) { + internal func updateSuccess(_ response: Skyflow.CollectResponse) { print(response) retryCount = 0 - updateRevealInputs(tokens: response.records[0].fields) + for result in response.records { + if let error = result.error { + print("Record failed:", error, "httpCode:", result.httpCode) + } + } + if let fields = response.records.first?.fields { + updateRevealInputs(tokens: fields) + } print("Successfully got response:", response) } - internal func updateFailure(error: Any) { - if((error as AnyObject).contains("Invalid Bearer token") && retryCount <= 2){ // To do, it will be replaced with error code in the future + internal func updateFailure(error: Skyflow.SkyflowError) { + if(error.message.contains("Invalid Bearer token") && retryCount <= 2){ // To do, it will be replaced with error code in the future retryCount += 1 submitForm() } - print("Failed Operation", error) + print("Failed Operation", error.httpCode, error.message, error.grpcCode ?? "", error.httpStatus ?? "", error.details ?? "") } - internal func updateRevealInputs(tokens: Fields) { + // fields is [String: Any] - column name -> array of {"token","tokenGroupName"} dicts. + internal func firstToken(_ fields: [String: Any], column: String) -> (token: String, tokenGroupName: String?) { + guard let entries = fields[column] as? [[String: Any]], let first = entries.first else { return ("", nil) } + return (first["token"] as? String ?? "", first["tokenGroupName"] as? String) + } + internal func updateRevealInputs(tokens: [String: Any]) { let revealBaseStyle = Skyflow.Style( borderColor: UIColor.black, cornerRadius: 20, @@ -188,40 +214,48 @@ class CollectAndRevealViewController: UIViewController { } else { self.revealed = true } + // tokenGroupRedactions: request-level redaction per token group, applied when + // reveal() is called (see revealForm()) - not per reveal element. + self.tokenGroupRedactions = [] + func addTokenGroupRedaction(_ redaction: String, forColumn column: String) { + if let tokenGroupName = self.firstToken(tokens, column: column).tokenGroupName { + self.tokenGroupRedactions.append(Skyflow.TokenGroupRedaction(tokenGroupName: tokenGroupName, redaction: redaction)) + } + } + addTokenGroupRedaction("REDACTED", forColumn: "card_number") + addTokenGroupRedaction("MASKED", forColumn: "cvv") + addTokenGroupRedaction("DEFAULT", forColumn: "cardholder_name") + addTokenGroupRedaction("PLAIN_TEXT", forColumn: "expiry_month") + let revealCardNumberInput = Skyflow.RevealElementInput( - token: tokens.card_number, + token: self.firstToken(tokens, column: "card_number").token, inputStyles: revealStyles, - label: "Card Number", - redaction: .REDACTED + label: "Card Number" ) self.revealCardNumber = self.revealContainer?.create( input: revealCardNumberInput, options: Skyflow.RevealElementOptions() ) let revealCVVtInput = Skyflow.RevealElementInput( - token: tokens.cvv, + token: self.firstToken(tokens, column: "cvv").token, inputStyles: revealStyles, - label: "CVV", - redaction: .MASKED + label: "CVV" ) self.revealCVV = self.revealContainer?.create(input: revealCVVtInput) let revealNameInput = Skyflow.RevealElementInput( - token: tokens.cardholder_name, + token: self.firstToken(tokens, column: "cardholder_name").token, inputStyles: revealStyles, - label: "Card Holder Name", - redaction: .DEFAULT - + label: "Card Holder Name" ) self.revealName = self.revealContainer?.create(input: revealNameInput) let revealExpirationMonthInput = Skyflow.RevealElementInput( - token: tokens.expiry_month, + token: self.firstToken(tokens, column: "expiry_month").token, inputStyles: revealStyles, - label: "Expiration Month", - redaction: .PLAIN_TEXT + label: "Expiration Month" ) self.revealExpirationMonth = self.revealContainer?.create(input: revealExpirationMonthInput) let revealExpirationYearInput = Skyflow.RevealElementInput( - token: tokens.expiry_year, + token: self.firstToken(tokens, column: "expiry_year").token, inputStyles: revealStyles, label: "Expiration Year" ) diff --git a/Samples/CollectAndRevealSample/CollectAndRevealSample/ExampleAPICallback.swift b/Samples/CollectAndRevealSample/CollectAndRevealSample/ExampleAPICallback.swift index 0d8be466..12e3bd84 100644 --- a/Samples/CollectAndRevealSample/CollectAndRevealSample/ExampleAPICallback.swift +++ b/Samples/CollectAndRevealSample/CollectAndRevealSample/ExampleAPICallback.swift @@ -2,47 +2,6 @@ * Copyright (c) 2022 Skyflow */ -import Foundation -import Skyflow - -public class ExampleAPICallback: Skyflow.Callback { - private var updateSuccess: ((SuccessResponse) -> Void)? - private var updateFailure: ((Any) -> Void)? - private var decoder: JSONDecoder? - - internal init(updateSuccess: @escaping (SuccessResponse) -> Void, updateFailure: @escaping (Any) -> Void) { - self.updateSuccess = updateSuccess - self.updateFailure = updateFailure - self.decoder = JSONDecoder() - } - - internal init() { - } - - public func onSuccess(_ responseBody: Any) { - if updateSuccess == nil { - print("success:", responseBody) - return - } - do { - let dataString = String(data: try! JSONSerialization.data(withJSONObject: responseBody), encoding: .utf8) - let responseData = Data(dataString!.utf8) - let jsonResponse = try decoder!.decode(SuccessResponse.self, from: responseData) - - updateSuccess!(jsonResponse) - - print("success:", jsonResponse) - } catch { - print("error deserializing", error) - } - } - - public func onFailure(_ error: Any) { - print(error) - if updateFailure != nil { - print("failure:", error) - return - } - print("onfailure", error) - } -} +// No longer needed - use Skyflow.CollectCallback / Skyflow.RevealCallback directly +// (see CollectAndRevealViewController.swift), which already deliver typed responses +// (Skyflow.CollectResponse / Skyflow.RevealResponse) without a custom Callback subclass. diff --git a/Samples/CollectAndRevealSample/CollectAndRevealSample/ResponseStructs.swift b/Samples/CollectAndRevealSample/CollectAndRevealSample/ResponseStructs.swift index 959044f5..67c28811 100644 --- a/Samples/CollectAndRevealSample/CollectAndRevealSample/ResponseStructs.swift +++ b/Samples/CollectAndRevealSample/CollectAndRevealSample/ResponseStructs.swift @@ -2,22 +2,6 @@ * Copyright (c) 2022 Skyflow */ -import Foundation - -struct SuccessResponse: Codable { - let records: [Records] -} - -struct Records: Codable { - let fields: Fields - let table: String -} - -struct Fields: Codable { - let cardholder_name: String - let card_number: String - let expiry_month: String - let expiry_year: String - let cvv: String - let skyflow_id: String -} +// No longer needed - the SDK now provides typed responses directly +// (Skyflow.CollectResponse / Skyflow.RevealResponse), so this sample no longer +// needs to declare its own local response structs. diff --git a/Samples/CollectAndRevealSample/Podfile b/Samples/CollectAndRevealSample/Podfile index a02fa6bd..ddb60497 100644 --- a/Samples/CollectAndRevealSample/Podfile +++ b/Samples/CollectAndRevealSample/Podfile @@ -5,9 +5,8 @@ source 'https://github.com/CocoaPods/Specs.git' target 'CollectAndRevealSample' do #use_frameworks! - pod 'AEXML', '4.6.1' # Pods for skyflow-iOS-sample-app - pod 'Skyflow', ">= 1.23.0" + pod 'Skyflow', ">= 1.25.0" end diff --git a/Samples/ComposableElements/ComposableElements/ExampleAPICallback.swift b/Samples/ComposableElements/ComposableElements/ExampleAPICallback.swift index 0d8be466..548a8e4c 100644 --- a/Samples/ComposableElements/ComposableElements/ExampleAPICallback.swift +++ b/Samples/ComposableElements/ComposableElements/ExampleAPICallback.swift @@ -2,47 +2,6 @@ * Copyright (c) 2022 Skyflow */ -import Foundation -import Skyflow - -public class ExampleAPICallback: Skyflow.Callback { - private var updateSuccess: ((SuccessResponse) -> Void)? - private var updateFailure: ((Any) -> Void)? - private var decoder: JSONDecoder? - - internal init(updateSuccess: @escaping (SuccessResponse) -> Void, updateFailure: @escaping (Any) -> Void) { - self.updateSuccess = updateSuccess - self.updateFailure = updateFailure - self.decoder = JSONDecoder() - } - - internal init() { - } - - public func onSuccess(_ responseBody: Any) { - if updateSuccess == nil { - print("success:", responseBody) - return - } - do { - let dataString = String(data: try! JSONSerialization.data(withJSONObject: responseBody), encoding: .utf8) - let responseData = Data(dataString!.utf8) - let jsonResponse = try decoder!.decode(SuccessResponse.self, from: responseData) - - updateSuccess!(jsonResponse) - - print("success:", jsonResponse) - } catch { - print("error deserializing", error) - } - } - - public func onFailure(_ error: Any) { - print(error) - if updateFailure != nil { - print("failure:", error) - return - } - print("onfailure", error) - } -} +// No longer needed - use Skyflow.CollectCallback / Skyflow.RevealCallback directly +// (see ViewController.swift), which already deliver typed responses +// (Skyflow.CollectResponse / Skyflow.RevealResponse) without a custom Callback subclass. diff --git a/Samples/ComposableElements/ComposableElements/ResponseStructs.swift b/Samples/ComposableElements/ComposableElements/ResponseStructs.swift index 959044f5..67c28811 100644 --- a/Samples/ComposableElements/ComposableElements/ResponseStructs.swift +++ b/Samples/ComposableElements/ComposableElements/ResponseStructs.swift @@ -2,22 +2,6 @@ * Copyright (c) 2022 Skyflow */ -import Foundation - -struct SuccessResponse: Codable { - let records: [Records] -} - -struct Records: Codable { - let fields: Fields - let table: String -} - -struct Fields: Codable { - let cardholder_name: String - let card_number: String - let expiry_month: String - let expiry_year: String - let cvv: String - let skyflow_id: String -} +// No longer needed - the SDK now provides typed responses directly +// (Skyflow.CollectResponse / Skyflow.RevealResponse), so this sample no longer +// needs to declare its own local response structs. diff --git a/Samples/ComposableElements/ComposableElements/ViewController.swift b/Samples/ComposableElements/ComposableElements/ViewController.swift index 5597e404..9bc833bf 100644 --- a/Samples/ComposableElements/ComposableElements/ViewController.swift +++ b/Samples/ComposableElements/ComposableElements/ViewController.swift @@ -22,6 +22,7 @@ class ViewController: UIViewController { private var revealButton: UIButton! private var revealed = false + private var tokenGroupRedactions: [Skyflow.TokenGroupRedaction] = [] override func loadView() { let view = UIView() @@ -170,26 +171,65 @@ class ViewController: UIViewController { } } @objc func revealForm() { - self.revealContainer?.reveal(callback: ExampleAPICallback()) + let revealCallback = Skyflow.RevealCallback( + onSuccess: { (response: Skyflow.RevealResponse) in + for record in response.records { + if let error = record.error { + print("reveal failed:", error, "httpCode:", record.httpCode) + } else { + print("revealed:", record.token ?? "", record.tokenGroupName ?? "", record.metadata ?? [:]) + } + } + }, + onFailure: { (error: Skyflow.SkyflowError) in print("reveal failure:", error.httpCode, error.message) } + ) + let revealOptions = Skyflow.RevealOptions(tokenGroupRedactions: self.tokenGroupRedactions.isEmpty ? nil : self.tokenGroupRedactions) + self.revealContainer?.reveal(callback: revealCallback, options: revealOptions) } @objc func submitForm() { - let exampleAPICallback = ExampleAPICallback(updateSuccess: updateSuccess, updateFailure: updateFailure) - container!.collect(callback: exampleAPICallback, options: Skyflow.CollectOptions(tokens: true)) + let collectCallback = Skyflow.CollectCallback(onSuccess: updateSuccess, onFailure: updateFailure) + + // Upsert example: insert-or-update credit_cards records, matching on card_number. + // If a record with a matching card_number already exists, its fields are merged + // in (updateType: .UPDATE) instead of creating a duplicate row. + let upsertOptions = [ + Skyflow.UpsertOption(table: "credit_cards", uniqueColumns: ["card_number"], updateType: .UPDATE) + ] + + // additionalFields: extra records submitted alongside whatever's collected from + // the mounted elements, one entry per table. + let additionalFields = Skyflow.AdditionalFields(records: [ + Skyflow.AdditionalFieldsRecord(table: "credit_cards", fields: ["billing_zip": "94105"]) + ]) + + container!.collect(callback: collectCallback, options: Skyflow.CollectOptions(additionalFields: additionalFields, upsert: upsertOptions)) } - internal func updateSuccess(_ response: SuccessResponse) { + internal func updateSuccess(_ response: Skyflow.CollectResponse) { print(response) retryCount = 0 - updateRevealInputs(tokens: response.records[0].fields) + for result in response.records { + if let error = result.error { + print("Record failed:", error, "httpCode:", result.httpCode) + } + } + if let fields = response.records.first?.fields { + updateRevealInputs(tokens: fields) + } print("Successfully got response:", response) } - internal func updateFailure(error: Any) { - if((error as AnyObject).contains("Invalid Bearer token") && retryCount <= 2){ // To do, it will be replaced with error code in the future + internal func updateFailure(error: Skyflow.SkyflowError) { + if(error.message.contains("Invalid Bearer token") && retryCount <= 2){ // To do, it will be replaced with error code in the future retryCount += 1 submitForm() } - print("Failed Operation", error) + print("Failed Operation", error.httpCode, error.message, error.grpcCode ?? "", error.httpStatus ?? "", error.details ?? "") + } + // fields is [String: Any] - column name -> array of {"token","tokenGroupName"} dicts. + internal func firstToken(_ fields: [String: Any], column: String) -> (token: String, tokenGroupName: String?) { + guard let entries = fields[column] as? [[String: Any]], let first = entries.first else { return ("", nil) } + return (first["token"] as? String ?? "", first["tokenGroupName"] as? String) } - internal func updateRevealInputs(tokens: Fields) { + internal func updateRevealInputs(tokens: [String: Any]) { let revealBaseStyle = Skyflow.Style( borderColor: UIColor.black, cornerRadius: 20, @@ -205,40 +245,48 @@ class ViewController: UIViewController { } else { self.revealed = true } + // tokenGroupRedactions: request-level redaction per token group, applied when + // reveal() is called (see revealForm()) - not per reveal element. + self.tokenGroupRedactions = [] + func addTokenGroupRedaction(_ redaction: String, forColumn column: String) { + if let tokenGroupName = self.firstToken(tokens, column: column).tokenGroupName { + self.tokenGroupRedactions.append(Skyflow.TokenGroupRedaction(tokenGroupName: tokenGroupName, redaction: redaction)) + } + } + addTokenGroupRedaction("REDACTED", forColumn: "card_number") + addTokenGroupRedaction("MASKED", forColumn: "cvv") + addTokenGroupRedaction("DEFAULT", forColumn: "cardholder_name") + addTokenGroupRedaction("PLAIN_TEXT", forColumn: "expiry_month") + let revealCardNumberInput = Skyflow.RevealElementInput( - token: tokens.card_number, + token: self.firstToken(tokens, column: "card_number").token ?? "", inputStyles: revealStyles, - label: "Card Number", - redaction: .REDACTED + label: "Card Number" ) self.revealCardNumber = self.revealContainer?.create( input: revealCardNumberInput, options: Skyflow.RevealElementOptions() ) let revealCVVtInput = Skyflow.RevealElementInput( - token: tokens.cvv, + token: self.firstToken(tokens, column: "cvv").token ?? "", inputStyles: revealStyles, - label: "CVV", - redaction: .MASKED + label: "CVV" ) self.revealCVV = self.revealContainer?.create(input: revealCVVtInput) let revealNameInput = Skyflow.RevealElementInput( - token: tokens.cardholder_name, + token: self.firstToken(tokens, column: "cardholder_name").token ?? "", inputStyles: revealStyles, - label: "Card Holder Name", - redaction: .DEFAULT - + label: "Card Holder Name" ) self.revealName = self.revealContainer?.create(input: revealNameInput) let revealExpirationMonthInput = Skyflow.RevealElementInput( - token: tokens.expiry_month, + token: self.firstToken(tokens, column: "expiry_month").token ?? "", inputStyles: revealStyles, - label: "Expiration Month", - redaction: .PLAIN_TEXT + label: "Expiration Month" ) self.revealExpirationMonth = self.revealContainer?.create(input: revealExpirationMonthInput) let revealExpirationYearInput = Skyflow.RevealElementInput( - token: tokens.expiry_year, + token: self.firstToken(tokens, column: "expiry_year").token ?? "", inputStyles: revealStyles, label: "Expiration Year" ) diff --git a/Samples/ComposableElements/Podfile b/Samples/ComposableElements/Podfile index 9d8d3f0e..77ef17f8 100644 --- a/Samples/ComposableElements/Podfile +++ b/Samples/ComposableElements/Podfile @@ -4,8 +4,7 @@ target 'ComposableElements' do # Comment the next line if you don't want to use dynamic frameworks use_frameworks! - pod 'AEXML', '4.6.1' - pod 'Skyflow', ">= 1.22.0" + pod 'Skyflow', ">= 1.25.0" # Pods for ComposableElements target 'ComposableElementsTests' do diff --git a/Samples/GetSample/GetSample.xcodeproj/project.pbxproj b/Samples/GetSample/GetSample.xcodeproj/project.pbxproj deleted file mode 100644 index bdef97b9..00000000 --- a/Samples/GetSample/GetSample.xcodeproj/project.pbxproj +++ /dev/null @@ -1,640 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 56; - objects = { - -/* Begin PBXBuildFile section */ - 7C7BCD1D2A49A5B900C6FBCF /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C7BCD1C2A49A5B900C6FBCF /* AppDelegate.swift */; }; - 7C7BCD1F2A49A5B900C6FBCF /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C7BCD1E2A49A5B900C6FBCF /* SceneDelegate.swift */; }; - 7C7BCD212A49A5B900C6FBCF /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C7BCD202A49A5B900C6FBCF /* ViewController.swift */; }; - 7C7BCD242A49A5B900C6FBCF /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7C7BCD222A49A5B900C6FBCF /* Main.storyboard */; }; - 7C7BCD262A49A5BA00C6FBCF /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7C7BCD252A49A5BA00C6FBCF /* Assets.xcassets */; }; - 7C7BCD292A49A5BA00C6FBCF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7C7BCD272A49A5BA00C6FBCF /* LaunchScreen.storyboard */; }; - 7C7BCD342A49A5BB00C6FBCF /* GetSampleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C7BCD332A49A5BB00C6FBCF /* GetSampleTests.swift */; }; - 7C7BCD3E2A49A5BB00C6FBCF /* GetSampleUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C7BCD3D2A49A5BB00C6FBCF /* GetSampleUITests.swift */; }; - 7C7BCD402A49A5BB00C6FBCF /* GetSampleUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C7BCD3F2A49A5BB00C6FBCF /* GetSampleUITestsLaunchTests.swift */; }; - 7C7BCD4D2A49A70900C6FBCF /* ExampleAPICallback.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C7BCD4C2A49A70900C6FBCF /* ExampleAPICallback.swift */; }; - 7C7BCD4F2A49A72900C6FBCF /* ResponseStructs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C7BCD4E2A49A72900C6FBCF /* ResponseStructs.swift */; }; - 7C7BCD512A49A75900C6FBCF /* ExampleTokenProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C7BCD502A49A75900C6FBCF /* ExampleTokenProvider.swift */; }; - 7C7BCD542A49AD6400C6FBCF /* Skyflow in Frameworks */ = {isa = PBXBuildFile; productRef = 7C7BCD532A49AD6400C6FBCF /* Skyflow */; }; -/* End PBXBuildFile section */ - -/* Begin PBXContainerItemProxy section */ - 7C7BCD302A49A5BB00C6FBCF /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 7C7BCD112A49A5B900C6FBCF /* Project object */; - proxyType = 1; - remoteGlobalIDString = 7C7BCD182A49A5B900C6FBCF; - remoteInfo = GetSample; - }; - 7C7BCD3A2A49A5BB00C6FBCF /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 7C7BCD112A49A5B900C6FBCF /* Project object */; - proxyType = 1; - remoteGlobalIDString = 7C7BCD182A49A5B900C6FBCF; - remoteInfo = GetSample; - }; -/* End PBXContainerItemProxy section */ - -/* Begin PBXFileReference section */ - 7C7BCD192A49A5B900C6FBCF /* GetSample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = GetSample.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 7C7BCD1C2A49A5B900C6FBCF /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - 7C7BCD1E2A49A5B900C6FBCF /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; - 7C7BCD202A49A5B900C6FBCF /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; - 7C7BCD232A49A5B900C6FBCF /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; - 7C7BCD252A49A5BA00C6FBCF /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 7C7BCD282A49A5BA00C6FBCF /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; - 7C7BCD2A2A49A5BA00C6FBCF /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 7C7BCD2F2A49A5BB00C6FBCF /* GetSampleTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = GetSampleTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 7C7BCD332A49A5BB00C6FBCF /* GetSampleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetSampleTests.swift; sourceTree = ""; }; - 7C7BCD392A49A5BB00C6FBCF /* GetSampleUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = GetSampleUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 7C7BCD3D2A49A5BB00C6FBCF /* GetSampleUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetSampleUITests.swift; sourceTree = ""; }; - 7C7BCD3F2A49A5BB00C6FBCF /* GetSampleUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetSampleUITestsLaunchTests.swift; sourceTree = ""; }; - 7C7BCD4C2A49A70900C6FBCF /* ExampleAPICallback.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleAPICallback.swift; sourceTree = ""; }; - 7C7BCD4E2A49A72900C6FBCF /* ResponseStructs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResponseStructs.swift; sourceTree = ""; }; - 7C7BCD502A49A75900C6FBCF /* ExampleTokenProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleTokenProvider.swift; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 7C7BCD162A49A5B900C6FBCF /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 7C7BCD542A49AD6400C6FBCF /* Skyflow in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 7C7BCD2C2A49A5BB00C6FBCF /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 7C7BCD362A49A5BB00C6FBCF /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 7C7BCD102A49A5B900C6FBCF = { - isa = PBXGroup; - children = ( - 7C7BCD1B2A49A5B900C6FBCF /* GetSample */, - 7C7BCD322A49A5BB00C6FBCF /* GetSampleTests */, - 7C7BCD3C2A49A5BB00C6FBCF /* GetSampleUITests */, - 7C7BCD1A2A49A5B900C6FBCF /* Products */, - ); - sourceTree = ""; - }; - 7C7BCD1A2A49A5B900C6FBCF /* Products */ = { - isa = PBXGroup; - children = ( - 7C7BCD192A49A5B900C6FBCF /* GetSample.app */, - 7C7BCD2F2A49A5BB00C6FBCF /* GetSampleTests.xctest */, - 7C7BCD392A49A5BB00C6FBCF /* GetSampleUITests.xctest */, - ); - name = Products; - sourceTree = ""; - }; - 7C7BCD1B2A49A5B900C6FBCF /* GetSample */ = { - isa = PBXGroup; - children = ( - 7C7BCD1C2A49A5B900C6FBCF /* AppDelegate.swift */, - 7C7BCD1E2A49A5B900C6FBCF /* SceneDelegate.swift */, - 7C7BCD202A49A5B900C6FBCF /* ViewController.swift */, - 7C7BCD222A49A5B900C6FBCF /* Main.storyboard */, - 7C7BCD252A49A5BA00C6FBCF /* Assets.xcassets */, - 7C7BCD272A49A5BA00C6FBCF /* LaunchScreen.storyboard */, - 7C7BCD2A2A49A5BA00C6FBCF /* Info.plist */, - 7C7BCD4C2A49A70900C6FBCF /* ExampleAPICallback.swift */, - 7C7BCD4E2A49A72900C6FBCF /* ResponseStructs.swift */, - 7C7BCD502A49A75900C6FBCF /* ExampleTokenProvider.swift */, - ); - path = GetSample; - sourceTree = ""; - }; - 7C7BCD322A49A5BB00C6FBCF /* GetSampleTests */ = { - isa = PBXGroup; - children = ( - 7C7BCD332A49A5BB00C6FBCF /* GetSampleTests.swift */, - ); - path = GetSampleTests; - sourceTree = ""; - }; - 7C7BCD3C2A49A5BB00C6FBCF /* GetSampleUITests */ = { - isa = PBXGroup; - children = ( - 7C7BCD3D2A49A5BB00C6FBCF /* GetSampleUITests.swift */, - 7C7BCD3F2A49A5BB00C6FBCF /* GetSampleUITestsLaunchTests.swift */, - ); - path = GetSampleUITests; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 7C7BCD182A49A5B900C6FBCF /* GetSample */ = { - isa = PBXNativeTarget; - buildConfigurationList = 7C7BCD432A49A5BB00C6FBCF /* Build configuration list for PBXNativeTarget "GetSample" */; - buildPhases = ( - 7C7BCD152A49A5B900C6FBCF /* Sources */, - 7C7BCD162A49A5B900C6FBCF /* Frameworks */, - 7C7BCD172A49A5B900C6FBCF /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = GetSample; - packageProductDependencies = ( - 7C7BCD532A49AD6400C6FBCF /* Skyflow */, - ); - productName = GetSample; - productReference = 7C7BCD192A49A5B900C6FBCF /* GetSample.app */; - productType = "com.apple.product-type.application"; - }; - 7C7BCD2E2A49A5BB00C6FBCF /* GetSampleTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = 7C7BCD462A49A5BB00C6FBCF /* Build configuration list for PBXNativeTarget "GetSampleTests" */; - buildPhases = ( - 7C7BCD2B2A49A5BB00C6FBCF /* Sources */, - 7C7BCD2C2A49A5BB00C6FBCF /* Frameworks */, - 7C7BCD2D2A49A5BB00C6FBCF /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - 7C7BCD312A49A5BB00C6FBCF /* PBXTargetDependency */, - ); - name = GetSampleTests; - productName = GetSampleTests; - productReference = 7C7BCD2F2A49A5BB00C6FBCF /* GetSampleTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; - 7C7BCD382A49A5BB00C6FBCF /* GetSampleUITests */ = { - isa = PBXNativeTarget; - buildConfigurationList = 7C7BCD492A49A5BB00C6FBCF /* Build configuration list for PBXNativeTarget "GetSampleUITests" */; - buildPhases = ( - 7C7BCD352A49A5BB00C6FBCF /* Sources */, - 7C7BCD362A49A5BB00C6FBCF /* Frameworks */, - 7C7BCD372A49A5BB00C6FBCF /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - 7C7BCD3B2A49A5BB00C6FBCF /* PBXTargetDependency */, - ); - name = GetSampleUITests; - productName = GetSampleUITests; - productReference = 7C7BCD392A49A5BB00C6FBCF /* GetSampleUITests.xctest */; - productType = "com.apple.product-type.bundle.ui-testing"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 7C7BCD112A49A5B900C6FBCF /* Project object */ = { - isa = PBXProject; - attributes = { - BuildIndependentTargetsInParallel = 1; - LastSwiftUpdateCheck = 1400; - LastUpgradeCheck = 1400; - TargetAttributes = { - 7C7BCD182A49A5B900C6FBCF = { - CreatedOnToolsVersion = 14.0; - }; - 7C7BCD2E2A49A5BB00C6FBCF = { - CreatedOnToolsVersion = 14.0; - TestTargetID = 7C7BCD182A49A5B900C6FBCF; - }; - 7C7BCD382A49A5BB00C6FBCF = { - CreatedOnToolsVersion = 14.0; - TestTargetID = 7C7BCD182A49A5B900C6FBCF; - }; - }; - }; - buildConfigurationList = 7C7BCD142A49A5B900C6FBCF /* Build configuration list for PBXProject "GetSample" */; - compatibilityVersion = "Xcode 14.0"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = 7C7BCD102A49A5B900C6FBCF; - packageReferences = ( - 7C7BCD522A49AD6400C6FBCF /* XCRemoteSwiftPackageReference "skyflow-iOS" */, - ); - productRefGroup = 7C7BCD1A2A49A5B900C6FBCF /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 7C7BCD182A49A5B900C6FBCF /* GetSample */, - 7C7BCD2E2A49A5BB00C6FBCF /* GetSampleTests */, - 7C7BCD382A49A5BB00C6FBCF /* GetSampleUITests */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 7C7BCD172A49A5B900C6FBCF /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 7C7BCD292A49A5BA00C6FBCF /* LaunchScreen.storyboard in Resources */, - 7C7BCD262A49A5BA00C6FBCF /* Assets.xcassets in Resources */, - 7C7BCD242A49A5B900C6FBCF /* Main.storyboard in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 7C7BCD2D2A49A5BB00C6FBCF /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 7C7BCD372A49A5BB00C6FBCF /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 7C7BCD152A49A5B900C6FBCF /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 7C7BCD4F2A49A72900C6FBCF /* ResponseStructs.swift in Sources */, - 7C7BCD512A49A75900C6FBCF /* ExampleTokenProvider.swift in Sources */, - 7C7BCD212A49A5B900C6FBCF /* ViewController.swift in Sources */, - 7C7BCD1D2A49A5B900C6FBCF /* AppDelegate.swift in Sources */, - 7C7BCD4D2A49A70900C6FBCF /* ExampleAPICallback.swift in Sources */, - 7C7BCD1F2A49A5B900C6FBCF /* SceneDelegate.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 7C7BCD2B2A49A5BB00C6FBCF /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 7C7BCD342A49A5BB00C6FBCF /* GetSampleTests.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 7C7BCD352A49A5BB00C6FBCF /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 7C7BCD3E2A49A5BB00C6FBCF /* GetSampleUITests.swift in Sources */, - 7C7BCD402A49A5BB00C6FBCF /* GetSampleUITestsLaunchTests.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXTargetDependency section */ - 7C7BCD312A49A5BB00C6FBCF /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 7C7BCD182A49A5B900C6FBCF /* GetSample */; - targetProxy = 7C7BCD302A49A5BB00C6FBCF /* PBXContainerItemProxy */; - }; - 7C7BCD3B2A49A5BB00C6FBCF /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 7C7BCD182A49A5B900C6FBCF /* GetSample */; - targetProxy = 7C7BCD3A2A49A5BB00C6FBCF /* PBXContainerItemProxy */; - }; -/* End PBXTargetDependency section */ - -/* Begin PBXVariantGroup section */ - 7C7BCD222A49A5B900C6FBCF /* Main.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 7C7BCD232A49A5B900C6FBCF /* Base */, - ); - name = Main.storyboard; - sourceTree = ""; - }; - 7C7BCD272A49A5BA00C6FBCF /* LaunchScreen.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 7C7BCD282A49A5BA00C6FBCF /* Base */, - ); - name = LaunchScreen.storyboard; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - -/* Begin XCBuildConfiguration section */ - 7C7BCD412A49A5BB00C6FBCF /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.0; - MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; - MTL_FAST_MATH = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - }; - name = Debug; - }; - 7C7BCD422A49A5BB00C6FBCF /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.0; - MTL_ENABLE_DEBUG_INFO = NO; - MTL_FAST_MATH = YES; - SDKROOT = iphoneos; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; - VALIDATE_PRODUCT = YES; - }; - name = Release; - }; - 7C7BCD442A49A5BB00C6FBCF /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = GetSample/Info.plist; - INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; - INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; - INFOPLIST_KEY_UIMainStoryboardFile = Main; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.skyflow.InputFormatting.GetSample; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - 7C7BCD452A49A5BB00C6FBCF /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = GetSample/Info.plist; - INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; - INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; - INFOPLIST_KEY_UIMainStoryboardFile = Main; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.skyflow.InputFormatting.GetSample; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Release; - }; - 7C7BCD472A49A5BB00C6FBCF /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.0; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.skyflow.InputFormatting.GetSampleTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/GetSample.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/GetSample"; - }; - name = Debug; - }; - 7C7BCD482A49A5BB00C6FBCF /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.0; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.skyflow.InputFormatting.GetSampleTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/GetSample.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/GetSample"; - }; - name = Release; - }; - 7C7BCD4A2A49A5BB00C6FBCF /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.skyflow.InputFormatting.GetSampleUITests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_TARGET_NAME = GetSample; - }; - name = Debug; - }; - 7C7BCD4B2A49A5BB00C6FBCF /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.skyflow.InputFormatting.GetSampleUITests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_TARGET_NAME = GetSample; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 7C7BCD142A49A5B900C6FBCF /* Build configuration list for PBXProject "GetSample" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 7C7BCD412A49A5BB00C6FBCF /* Debug */, - 7C7BCD422A49A5BB00C6FBCF /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 7C7BCD432A49A5BB00C6FBCF /* Build configuration list for PBXNativeTarget "GetSample" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 7C7BCD442A49A5BB00C6FBCF /* Debug */, - 7C7BCD452A49A5BB00C6FBCF /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 7C7BCD462A49A5BB00C6FBCF /* Build configuration list for PBXNativeTarget "GetSampleTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 7C7BCD472A49A5BB00C6FBCF /* Debug */, - 7C7BCD482A49A5BB00C6FBCF /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 7C7BCD492A49A5BB00C6FBCF /* Build configuration list for PBXNativeTarget "GetSampleUITests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 7C7BCD4A2A49A5BB00C6FBCF /* Debug */, - 7C7BCD4B2A49A5BB00C6FBCF /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - -/* Begin XCRemoteSwiftPackageReference section */ - 7C7BCD522A49AD6400C6FBCF /* XCRemoteSwiftPackageReference "skyflow-iOS" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/skyflowapi/skyflow-iOS.git"; - requirement = { - branch = main; - kind = branch; - }; - }; -/* End XCRemoteSwiftPackageReference section */ - -/* Begin XCSwiftPackageProductDependency section */ - 7C7BCD532A49AD6400C6FBCF /* Skyflow */ = { - isa = XCSwiftPackageProductDependency; - package = 7C7BCD522A49AD6400C6FBCF /* XCRemoteSwiftPackageReference "skyflow-iOS" */; - productName = Skyflow; - }; -/* End XCSwiftPackageProductDependency section */ - }; - rootObject = 7C7BCD112A49A5B900C6FBCF /* Project object */; -} diff --git a/Samples/GetSample/GetSample.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Samples/GetSample/GetSample.xcodeproj/project.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 919434a6..00000000 --- a/Samples/GetSample/GetSample.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/Samples/GetSample/GetSample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Samples/GetSample/GetSample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d98100..00000000 --- a/Samples/GetSample/GetSample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/Samples/GetSample/GetSample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Samples/GetSample/GetSample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved deleted file mode 100644 index f5762feb..00000000 --- a/Samples/GetSample/GetSample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ /dev/null @@ -1,23 +0,0 @@ -{ - "pins" : [ - { - "identity" : "aexml", - "kind" : "remoteSourceControl", - "location" : "https://github.com/tadija/AEXML.git", - "state" : { - "revision" : "38f7d00b23ecd891e1ee656fa6aeebd6ba04ecc3", - "version" : "4.6.1" - } - }, - { - "identity" : "skyflow-ios", - "kind" : "remoteSourceControl", - "location" : "https://github.com/skyflowapi/skyflow-iOS.git", - "state" : { - "branch" : "main", - "revision" : "d746cff983d44dd4690780c14aa7ca60387d7850" - } - } - ], - "version" : 2 -} diff --git a/Samples/GetSample/GetSample/AppDelegate.swift b/Samples/GetSample/GetSample/AppDelegate.swift deleted file mode 100644 index aec625c4..00000000 --- a/Samples/GetSample/GetSample/AppDelegate.swift +++ /dev/null @@ -1,36 +0,0 @@ -// -// AppDelegate.swift -// GetSample -// -// Created by Bharti Sagar on 26/06/23. -// - -import UIKit - -@main -class AppDelegate: UIResponder, UIApplicationDelegate { - - - - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - // Override point for customization after application launch. - return true - } - - // MARK: UISceneSession Lifecycle - - func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { - // Called when a new scene session is being created. - // Use this method to select a configuration to create the new scene with. - return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) - } - - func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { - // Called when the user discards a scene session. - // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. - // Use this method to release any resources that were specific to the discarded scenes, as they will not return. - } - - -} - diff --git a/Samples/GetSample/GetSample/Assets.xcassets/AccentColor.colorset/Contents.json b/Samples/GetSample/GetSample/Assets.xcassets/AccentColor.colorset/Contents.json deleted file mode 100644 index eb878970..00000000 --- a/Samples/GetSample/GetSample/Assets.xcassets/AccentColor.colorset/Contents.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "colors" : [ - { - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Samples/GetSample/GetSample/Assets.xcassets/AppIcon.appiconset/Contents.json b/Samples/GetSample/GetSample/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index 13613e3e..00000000 --- a/Samples/GetSample/GetSample/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "platform" : "ios", - "size" : "1024x1024" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Samples/GetSample/GetSample/Assets.xcassets/Contents.json b/Samples/GetSample/GetSample/Assets.xcassets/Contents.json deleted file mode 100644 index 73c00596..00000000 --- a/Samples/GetSample/GetSample/Assets.xcassets/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Samples/GetSample/GetSample/Base.lproj/LaunchScreen.storyboard b/Samples/GetSample/GetSample/Base.lproj/LaunchScreen.storyboard deleted file mode 100644 index 865e9329..00000000 --- a/Samples/GetSample/GetSample/Base.lproj/LaunchScreen.storyboard +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Samples/GetSample/GetSample/Base.lproj/Main.storyboard b/Samples/GetSample/GetSample/Base.lproj/Main.storyboard deleted file mode 100644 index 25a76385..00000000 --- a/Samples/GetSample/GetSample/Base.lproj/Main.storyboard +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Samples/GetSample/GetSample/ExampleAPICallback.swift b/Samples/GetSample/GetSample/ExampleAPICallback.swift deleted file mode 100644 index 530d74fd..00000000 --- a/Samples/GetSample/GetSample/ExampleAPICallback.swift +++ /dev/null @@ -1,50 +0,0 @@ -// -// ExampleAPICallback.swift -// GetSample -// -// Created by Bharti Sagar on 26/06/23. -// -import Foundation -import Skyflow - -public class ExampleAPICallback: Skyflow.Callback { - private var updateSuccess: ((SuccessResponse) -> Void)? - private var updateFailure: ((Any) -> Void)? - private var decoder: JSONDecoder? - - internal init(updateSuccess: @escaping (SuccessResponse) -> Void, updateFailure: @escaping (Any) -> Void) { - self.updateSuccess = updateSuccess - self.updateFailure = updateFailure - self.decoder = JSONDecoder() - } - - internal init() { - } - - public func onSuccess(_ responseBody: Any) { - if updateSuccess == nil { - print("success:", responseBody) - return - } - do { - let dataString = String(data: try! JSONSerialization.data(withJSONObject: responseBody), encoding: .utf8) - let responseData = Data(dataString!.utf8) - let jsonResponse = try decoder!.decode(SuccessResponse.self, from: responseData) - - updateSuccess!(jsonResponse) - - print("success:", jsonResponse) - } catch { - print("error deserializing", error) - } - } - - public func onFailure(_ error: Any) { - print(error) - if updateFailure != nil { - print("failure:", error) - return - } - print("onfailure", error) - } -} diff --git a/Samples/GetSample/GetSample/ExampleTokenProvider.swift b/Samples/GetSample/GetSample/ExampleTokenProvider.swift deleted file mode 100644 index 75a0efee..00000000 --- a/Samples/GetSample/GetSample/ExampleTokenProvider.swift +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright (c) 2022 Skyflow - */ - -import Foundation -import Skyflow - -public class ExampleTokenProvider: TokenProvider { - public func getBearerToken(_ apiCallback: Skyflow.Callback) { - if let url = URL(string: "") { - let session = URLSession(configuration: .default) - let task = session.dataTask(with: url) { data, _, error in - if error != nil { - print(error!) - return - } - if let safeData = data { - do { - let x = try JSONSerialization.jsonObject(with: safeData, options: []) as? [String: String] - if let accessToken = x?["accessToken"] { - apiCallback.onSuccess(accessToken) - } - } catch { - print("access token wrong format") - } - } - } - task.resume() - } - } -} diff --git a/Samples/GetSample/GetSample/Info.plist b/Samples/GetSample/GetSample/Info.plist deleted file mode 100644 index dd3c9afd..00000000 --- a/Samples/GetSample/GetSample/Info.plist +++ /dev/null @@ -1,25 +0,0 @@ - - - - - UIApplicationSceneManifest - - UIApplicationSupportsMultipleScenes - - UISceneConfigurations - - UIWindowSceneSessionRoleApplication - - - UISceneConfigurationName - Default Configuration - UISceneDelegateClassName - $(PRODUCT_MODULE_NAME).SceneDelegate - UISceneStoryboardFile - Main - - - - - - diff --git a/Samples/GetSample/GetSample/ResponseStructs.swift b/Samples/GetSample/GetSample/ResponseStructs.swift deleted file mode 100644 index 959044f5..00000000 --- a/Samples/GetSample/GetSample/ResponseStructs.swift +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright (c) 2022 Skyflow - */ - -import Foundation - -struct SuccessResponse: Codable { - let records: [Records] -} - -struct Records: Codable { - let fields: Fields - let table: String -} - -struct Fields: Codable { - let cardholder_name: String - let card_number: String - let expiry_month: String - let expiry_year: String - let cvv: String - let skyflow_id: String -} diff --git a/Samples/GetSample/GetSample/SceneDelegate.swift b/Samples/GetSample/GetSample/SceneDelegate.swift deleted file mode 100644 index a968b5bc..00000000 --- a/Samples/GetSample/GetSample/SceneDelegate.swift +++ /dev/null @@ -1,52 +0,0 @@ -// -// SceneDelegate.swift -// GetSample -// -// Created by Bharti Sagar on 26/06/23. -// - -import UIKit - -class SceneDelegate: UIResponder, UIWindowSceneDelegate { - - var window: UIWindow? - - - func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { - // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. - // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. - // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). - guard let _ = (scene as? UIWindowScene) else { return } - } - - func sceneDidDisconnect(_ scene: UIScene) { - // Called as the scene is being released by the system. - // This occurs shortly after the scene enters the background, or when its session is discarded. - // Release any resources associated with this scene that can be re-created the next time the scene connects. - // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). - } - - func sceneDidBecomeActive(_ scene: UIScene) { - // Called when the scene has moved from an inactive state to an active state. - // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. - } - - func sceneWillResignActive(_ scene: UIScene) { - // Called when the scene will move from an active state to an inactive state. - // This may occur due to temporary interruptions (ex. an incoming phone call). - } - - func sceneWillEnterForeground(_ scene: UIScene) { - // Called as the scene transitions from the background to the foreground. - // Use this method to undo the changes made on entering the background. - } - - func sceneDidEnterBackground(_ scene: UIScene) { - // Called as the scene transitions from the foreground to the background. - // Use this method to save data, release shared resources, and store enough scene-specific state information - // to restore the scene back to its current state. - } - - -} - diff --git a/Samples/GetSample/GetSample/ViewController.swift b/Samples/GetSample/GetSample/ViewController.swift deleted file mode 100644 index df14f26a..00000000 --- a/Samples/GetSample/GetSample/ViewController.swift +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Copyright (c) 2022 Skyflow - */ - -import UIKit -import Skyflow - -class ViewController: UIViewController { - private var skyflow: Skyflow.Client? - private var stackView: UIStackView! - - override func loadView() { - let view = UIView() - view.backgroundColor = .white - view.translatesAutoresizingMaskIntoConstraints = false - self.view = view - } - - override func viewDidLoad() { - super.viewDidLoad() - - let tokenProvider = ExampleTokenProvider() - self.stackView = UIStackView() - - let config = Skyflow.Configuration( - vaultID: "", - vaultURL: "", - tokenProvider: tokenProvider, - options: Skyflow.Options( - logLevel: Skyflow.LogLevel.DEBUG - ) - ) - - self.skyflow = Skyflow.initialize(config) - - if self.skyflow != nil { - let getWithRedactionType = UIButton(frame: CGRect(x: 100, y: 400, width: 100, height: 70)) - getWithRedactionType.backgroundColor = .gray - getWithRedactionType.setTitle("Get Records With Redaction Type", for: .normal) - getWithRedactionType.addTarget(self, action: #selector(getRecordsWithRedactionType), for: .touchUpInside) - - let getWithGetOptionTokens = UIButton(frame: CGRect(x: 100, y: 400, width: 100, height: 70)) - getWithGetOptionTokens.backgroundColor = .gray - getWithGetOptionTokens.setTitle("Get Records with tokens", for: .normal) - getWithGetOptionTokens.addTarget(self, action: #selector(getRecordsWithGetOptionTokens), for: .touchUpInside) - - stackView.addArrangedSubview(getWithRedactionType) - stackView.addArrangedSubview(getWithGetOptionTokens) - - stackView.axis = .vertical - stackView.distribution = .fill - stackView.spacing = 10 - stackView.alignment = .fill - stackView.translatesAutoresizingMaskIntoConstraints = false - let scrollView = UIScrollView(frame: .zero) - scrollView.isScrollEnabled = true - scrollView.backgroundColor = .white - scrollView.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(scrollView) - scrollView.topAnchor.constraint( - equalTo: view.safeAreaLayoutGuide.topAnchor, - constant: 20 - ).isActive = true - scrollView.leftAnchor.constraint( - equalTo: view.safeAreaLayoutGuide.leftAnchor, - constant: 10 - ).isActive = true - scrollView.rightAnchor.constraint( - equalTo: view.safeAreaLayoutGuide.rightAnchor, - constant: -10 - ).isActive = true - scrollView.bottomAnchor.constraint( - equalTo: view.safeAreaLayoutGuide.bottomAnchor - ).isActive = true - scrollView.addSubview(stackView) - stackView.widthAnchor.constraint(equalTo: scrollView.widthAnchor, constant: -10).isActive = true - stackView.topAnchor.constraint(equalTo: scrollView.topAnchor).isActive = true - stackView.leftAnchor.constraint(equalTo: scrollView.leftAnchor).isActive = true - stackView.rightAnchor.constraint(equalTo: scrollView.rightAnchor, constant: -10).isActive = true - stackView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor).isActive = true - } - } - @objc func getRecordsWithRedactionType() { - let exampleAPICallback = ExampleAPICallback(updateSuccess: updateSuccess, updateFailure: updateFailure) - let record = ["ids": ["", ""], "table": "", "redaction": RedactionType.PLAIN_TEXT] as [String : Any] - let recordColumn = ["columnValues": ["", ""],"table": "", "columnName": "","redaction": RedactionType.PLAIN_TEXT] as [String : Any] - - let invalidID = ["invalid skyflow ID"] - let badRecord = ["ids": invalidID, "table": "table", "redaction": RedactionType.PLAIN_TEXT] as [String : Any] - - let records = ["records": [record, recordColumn, badRecord]] - self.skyflow?.get(records: records, callback: exampleAPICallback) - } - - @objc func getRecordsWithGetOptionTokens() { - let exampleAPICallback = ExampleAPICallback(updateSuccess: updateSuccess, updateFailure: updateFailure) - let record = ["ids": ["", ""], "table": ""] as [String : Any] - - let invalidID = ["invalid skyflow ID"] - let badRecord = ["ids": invalidID, "table": "table", "redaction": RedactionType.PLAIN_TEXT] as [String : Any] - - let records = ["records": [record, badRecord]] - self.skyflow?.get(records: records, options: GetOptions(tokens: true), callback: exampleAPICallback) - } - internal func updateSuccess(_ response: SuccessResponse) { - print(response) - print("Successfully got response:", response) - } - internal func updateFailure(error: Any) { - print("Failed Operation", error) - } -} diff --git a/Samples/GetSample/GetSampleTests/GetSampleTests.swift b/Samples/GetSample/GetSampleTests/GetSampleTests.swift deleted file mode 100644 index 4837e722..00000000 --- a/Samples/GetSample/GetSampleTests/GetSampleTests.swift +++ /dev/null @@ -1,36 +0,0 @@ -// -// GetSampleTests.swift -// GetSampleTests -// -// Created by Bharti Sagar on 26/06/23. -// - -import XCTest -@testable import GetSample - -final class GetSampleTests: XCTestCase { - - override func setUpWithError() throws { - // Put setup code here. This method is called before the invocation of each test method in the class. - } - - override func tearDownWithError() throws { - // Put teardown code here. This method is called after the invocation of each test method in the class. - } - - func testExample() throws { - // This is an example of a functional test case. - // Use XCTAssert and related functions to verify your tests produce the correct results. - // Any test you write for XCTest can be annotated as throws and async. - // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. - // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. - } - - func testPerformanceExample() throws { - // This is an example of a performance test case. - self.measure { - // Put the code you want to measure the time of here. - } - } - -} diff --git a/Samples/GetSample/GetSampleUITests/GetSampleUITests.swift b/Samples/GetSample/GetSampleUITests/GetSampleUITests.swift deleted file mode 100644 index 8b26e9ab..00000000 --- a/Samples/GetSample/GetSampleUITests/GetSampleUITests.swift +++ /dev/null @@ -1,41 +0,0 @@ -// -// GetSampleUITests.swift -// GetSampleUITests -// -// Created by Bharti Sagar on 26/06/23. -// - -import XCTest - -final class GetSampleUITests: XCTestCase { - - override func setUpWithError() throws { - // Put setup code here. This method is called before the invocation of each test method in the class. - - // In UI tests it is usually best to stop immediately when a failure occurs. - continueAfterFailure = false - - // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. - } - - override func tearDownWithError() throws { - // Put teardown code here. This method is called after the invocation of each test method in the class. - } - - func testExample() throws { - // UI tests must launch the application that they test. - let app = XCUIApplication() - app.launch() - - // Use XCTAssert and related functions to verify your tests produce the correct results. - } - - func testLaunchPerformance() throws { - if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { - // This measures how long it takes to launch your application. - measure(metrics: [XCTApplicationLaunchMetric()]) { - XCUIApplication().launch() - } - } - } -} diff --git a/Samples/GetSample/GetSampleUITests/GetSampleUITestsLaunchTests.swift b/Samples/GetSample/GetSampleUITests/GetSampleUITestsLaunchTests.swift deleted file mode 100644 index 75f3db9b..00000000 --- a/Samples/GetSample/GetSampleUITests/GetSampleUITestsLaunchTests.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// GetSampleUITestsLaunchTests.swift -// GetSampleUITests -// -// Created by Bharti Sagar on 26/06/23. -// - -import XCTest - -final class GetSampleUITestsLaunchTests: XCTestCase { - - override class var runsForEachTargetApplicationUIConfiguration: Bool { - true - } - - override func setUpWithError() throws { - continueAfterFailure = false - } - - func testLaunch() throws { - let app = XCUIApplication() - app.launch() - - // Insert steps here to perform after app launch but before taking a screenshot, - // such as logging into a test account or navigating somewhere in the app - - let attachment = XCTAttachment(screenshot: app.screenshot()) - attachment.name = "Launch Screen" - attachment.lifetime = .keepAlways - add(attachment) - } -} diff --git a/Samples/GetSample/Podfile b/Samples/GetSample/Podfile deleted file mode 100644 index 72b31da4..00000000 --- a/Samples/GetSample/Podfile +++ /dev/null @@ -1,12 +0,0 @@ -# Uncomment the next line to define a global platform for your project -# platform :ios, '9.0' - -source 'https://github.com/CocoaPods/Specs.git' - -target 'GetSample' do - #use_frameworks! - pod 'AEXML', '4.6.1' - # Pods for GetSample - pod 'Skyflow', ">= 1.22.0" - -end diff --git a/Samples/InputFormatting/InputFormatting/ExampleAPICallback.swift b/Samples/InputFormatting/InputFormatting/ExampleAPICallback.swift index 25f84e24..548a8e4c 100644 --- a/Samples/InputFormatting/InputFormatting/ExampleAPICallback.swift +++ b/Samples/InputFormatting/InputFormatting/ExampleAPICallback.swift @@ -1,50 +1,7 @@ /* * Copyright (c) 2022 Skyflow */ -// Created by Bharti Sagar on 26/04/23. -// -import Foundation -import Skyflow - -public class ExampleAPICallback: Skyflow.Callback { - private var updateSuccess: ((SuccessResponse) -> Void)? - private var updateFailure: ((Any) -> Void)? - private var decoder: JSONDecoder? - - internal init(updateSuccess: @escaping (SuccessResponse) -> Void, updateFailure: @escaping (Any) -> Void) { - self.updateSuccess = updateSuccess - self.updateFailure = updateFailure - self.decoder = JSONDecoder() - } - - internal init() { - } - - public func onSuccess(_ responseBody: Any) { - if updateSuccess == nil { - print("success:", responseBody) - return - } - do { - let dataString = String(data: try! JSONSerialization.data(withJSONObject: responseBody), encoding: .utf8) - let responseData = Data(dataString!.utf8) - let jsonResponse = try decoder!.decode(SuccessResponse.self, from: responseData) - - updateSuccess!(jsonResponse) - - print("success:", jsonResponse) - } catch { - print("error deserializing", error) - } - } - - public func onFailure(_ error: Any) { - print(error) - if updateFailure != nil { - print("failure:", error) - return - } - print("onfailure", error) - } -} +// No longer needed - use Skyflow.CollectCallback / Skyflow.RevealCallback directly +// (see ViewController.swift), which already deliver typed responses +// (Skyflow.CollectResponse / Skyflow.RevealResponse) without a custom Callback subclass. diff --git a/Samples/InputFormatting/InputFormatting/ResponseStructs.swift b/Samples/InputFormatting/InputFormatting/ResponseStructs.swift index 2230b92f..67c28811 100644 --- a/Samples/InputFormatting/InputFormatting/ResponseStructs.swift +++ b/Samples/InputFormatting/InputFormatting/ResponseStructs.swift @@ -2,28 +2,6 @@ * Copyright (c) 2022 Skyflow */ -// Created by Bharti Sagar on 26/04/23. -// - -import Foundation - -struct SuccessResponse: Codable { - let records: [Records] -} - -struct Records: Codable { - let fields: Fields - let table: String -} - -struct Fields: Codable { - let cardholder_name: String - let card_number: String - let expiry_month: String - let expiry_year: String - let cvv: String - let ssn: String - let phone_number: String - let license_number: String - let skyflow_id: String -} +// No longer needed - the SDK now provides typed responses directly +// (Skyflow.CollectResponse / Skyflow.RevealResponse), so this sample no longer +// needs to declare its own local response structs. diff --git a/Samples/InputFormatting/InputFormatting/ViewController.swift b/Samples/InputFormatting/InputFormatting/ViewController.swift index fbacc971..fa93680c 100644 --- a/Samples/InputFormatting/InputFormatting/ViewController.swift +++ b/Samples/InputFormatting/InputFormatting/ViewController.swift @@ -201,21 +201,37 @@ class ViewController: UIViewController { } } @objc func revealForm() { - self.revealContainer?.reveal(callback: ExampleAPICallback()) + let revealCallback = Skyflow.RevealCallback( + onSuccess: { response in print("reveal success:", response) }, + onFailure: { error in print("reveal failure:", error) } + ) + self.revealContainer?.reveal(callback: revealCallback) } @objc func submitForm() { - let exampleAPICallback = ExampleAPICallback(updateSuccess: updateSuccess, updateFailure: updateFailure) - container!.collect(callback: exampleAPICallback, options: Skyflow.CollectOptions(tokens: true)) + let collectCallback = Skyflow.CollectCallback(onSuccess: updateSuccess, onFailure: updateFailure) + container!.collect(callback: collectCallback, options: Skyflow.CollectOptions()) } - internal func updateSuccess(_ response: SuccessResponse) { + internal func updateSuccess(_ response: Skyflow.CollectResponse) { print(response) - updateRevealInputs(tokens: response.records[0].fields) + for result in response.records { + if let error = result.error { + print("Record failed:", error, "httpCode:", result.httpCode) + } + } + if let fields = response.records.first?.fields { + updateRevealInputs(tokens: fields) + } print("Successfully got response:", response) } internal func updateFailure(error: Any) { print("Failed Operation", error) } - internal func updateRevealInputs(tokens: Fields) { + // fields is [String: Any] - column name -> array of {"token","tokenGroupName"} dicts. + internal func firstToken(_ fields: [String: Any], column: String) -> (token: String, tokenGroupName: String?) { + guard let entries = fields[column] as? [[String: Any]], let first = entries.first else { return ("", nil) } + return (first["token"] as? String ?? "", first["tokenGroupName"] as? String) + } + internal func updateRevealInputs(tokens: [String: Any]) { let revealBaseStyle = Skyflow.Style( borderColor: UIColor.black, cornerRadius: 20, @@ -232,7 +248,7 @@ class ViewController: UIViewController { self.revealed = true } let revealCardNumberInput = Skyflow.RevealElementInput( - token: tokens.card_number, + token: firstToken(tokens, column: "card_number").token ?? "", inputStyles: revealStyles, label: "Card Number" ) @@ -241,43 +257,43 @@ class ViewController: UIViewController { options: Skyflow.RevealElementOptions(format: "XXXX-XXXX-XXXX-XXXX-XXX", translation: ["X": "[0-9]"]) ) let revealCVVtInput = Skyflow.RevealElementInput( - token: tokens.cvv, + token: firstToken(tokens, column: "cvv").token ?? "", inputStyles: revealStyles, label: "CVV" ) self.revealCVV = self.revealContainer?.create(input: revealCVVtInput) let revealNameInput = Skyflow.RevealElementInput( - token: tokens.cardholder_name, + token: firstToken(tokens, column: "cardholder_name").token ?? "", inputStyles: revealStyles, label: "Card Holder Name" ) self.revealName = self.revealContainer?.create(input: revealNameInput) let revealExpirationMonthInput = Skyflow.RevealElementInput( - token: tokens.expiry_month, + token: firstToken(tokens, column: "expiry_month").token ?? "", inputStyles: revealStyles, label: "Expiration Month" ) self.revealExpirationMonth = self.revealContainer?.create(input: revealExpirationMonthInput) let revealExpirationYearInput = Skyflow.RevealElementInput( - token: tokens.expiry_year, + token: firstToken(tokens, column: "expiry_year").token ?? "", inputStyles: revealStyles, label: "Expiration Year" ) self.revealExpirationYear = self.revealContainer?.create(input: revealExpirationYearInput) let revealSSNInput = Skyflow.RevealElementInput( - token: tokens.ssn, + token: firstToken(tokens, column: "ssn").token ?? "", inputStyles: revealStyles, label: "SSN" ) self.revealSSN = self.revealContainer?.create(input: revealSSNInput, options: Skyflow.RevealElementOptions(format: "XXX XX XXXX", translation: ["X": "[0-9]"])) let revealPhoneNumberInput = Skyflow.RevealElementInput( - token: tokens.phone_number, + token: firstToken(tokens, column: "phone_number").token ?? "", inputStyles: revealStyles, label: "Phone Number" ) self.revealPhoneNumber = self.revealContainer?.create(input: revealPhoneNumberInput, options: Skyflow.RevealElementOptions(format: "+91 XXXX-XX-XXXX", translation: ["X": "[0-9]"])) let revealLicenseNumberInput = Skyflow.RevealElementInput( - token: tokens.phone_number, + token: firstToken(tokens, column: "phone_number").token ?? "", inputStyles: revealStyles, label: "License Number" ) diff --git a/Samples/InputFormatting/Podfile b/Samples/InputFormatting/Podfile index f4ddb57c..354055ad 100644 --- a/Samples/InputFormatting/Podfile +++ b/Samples/InputFormatting/Podfile @@ -3,9 +3,8 @@ target 'InputFormatting' do # Comment the next line if you don't want to use dynamic frameworks use_frameworks! - pod 'AEXML', '4.6.1' - pod 'Skyflow', ">= 1.22.0" + pod 'Skyflow', ">= 1.25.0" # Pods for InputFormatting diff --git a/Samples/InputFormatting/Podfile.lock b/Samples/InputFormatting/Podfile.lock index e9d81271..7592f10a 100644 --- a/Samples/InputFormatting/Podfile.lock +++ b/Samples/InputFormatting/Podfile.lock @@ -1,18 +1,14 @@ PODS: - - AEXML (4.6.1) - Skyflow (1.18.0-beta.1): - - AEXML DEPENDENCIES: - Skyflow (= 1.18.0-beta.1) SPEC REPOS: trunk: - - AEXML - Skyflow SPEC CHECKSUMS: - AEXML: 1e255ecc6597212f97a7454a69ebd3ede64ac1cf Skyflow: d702f8e9beafba05e164a3f74a6f862596f41085 PODFILE CHECKSUM: 4958176a88d5d7c9eb37117625a3937f5da81019 diff --git a/Samples/UpdateDataUsingElements/Podfile b/Samples/UpdateDataUsingElements/Podfile index 288940eb..b8ac5a1c 100644 --- a/Samples/UpdateDataUsingElements/Podfile +++ b/Samples/UpdateDataUsingElements/Podfile @@ -6,20 +6,17 @@ target 'UpdateDataUsingElements' do use_frameworks! # Pods for UpdateDataUsingElements - pod 'AEXML', '4.6.1' - pod 'Skyflow', '1.25.0' + pod 'Skyflow', '>=1.25.0' target 'UpdateDataUsingElementsTests' do inherit! :search_paths # Pods for testing - pod 'AEXML', '4.6.1' - pod 'Skyflow', '1.25.0' + pod 'Skyflow', '>=1.25.0' end target 'UpdateDataUsingElementsUITests' do # Pods for testing - pod 'AEXML', '4.6.1' - pod 'Skyflow', '1.25.0' + pod 'Skyflow', '>=1.25.0' end diff --git a/Samples/UpdateDataUsingElements/UpdateDataUsingElements/ExampleAPICallback.swift b/Samples/UpdateDataUsingElements/UpdateDataUsingElements/ExampleAPICallback.swift index b08d2305..548a8e4c 100644 --- a/Samples/UpdateDataUsingElements/UpdateDataUsingElements/ExampleAPICallback.swift +++ b/Samples/UpdateDataUsingElements/UpdateDataUsingElements/ExampleAPICallback.swift @@ -2,47 +2,6 @@ * Copyright (c) 2022 Skyflow */ -import Foundation -import Skyflow - -public class ExampleAPICallback: Skyflow.Callback { - private var updateSuccess: ((SuccessResponse) -> Void)? - private var updateFailure: ((Any) -> Void)? - private var decoder: JSONDecoder? - - internal init(updateSuccess: @escaping (Any) -> Void, updateFailure: @escaping (Any) -> Void) { - self.updateSuccess = updateSuccess - self.updateFailure = updateFailure - self.decoder = JSONDecoder() - } - - internal init() { - } - - public func onSuccess(_ responseBody: Any) { - if updateSuccess == nil { - print("success:", responseBody) - return - } - do { - let dataString = String(data: try! JSONSerialization.data(withJSONObject: responseBody), encoding: .utf8) - let responseData = Data(dataString!.utf8) - let jsonResponse = try decoder!.decode(SuccessResponse.self, from: responseData) - - updateSuccess!(jsonResponse) - - print("success:", jsonResponse) - } catch { - print("error deserializing", error) - } - } - - public func onFailure(_ error: Any) { - print(error) - if updateFailure != nil { - print("failure:", error) - return - } - print("onfailure", error) - } -} +// No longer needed - use Skyflow.CollectCallback / Skyflow.RevealCallback directly +// (see ViewController.swift), which already deliver typed responses +// (Skyflow.CollectResponse / Skyflow.RevealResponse) without a custom Callback subclass. diff --git a/Samples/UpdateDataUsingElements/UpdateDataUsingElements/ResponseStructs.swift b/Samples/UpdateDataUsingElements/UpdateDataUsingElements/ResponseStructs.swift index 4b948ce6..67c28811 100644 --- a/Samples/UpdateDataUsingElements/UpdateDataUsingElements/ResponseStructs.swift +++ b/Samples/UpdateDataUsingElements/UpdateDataUsingElements/ResponseStructs.swift @@ -2,23 +2,6 @@ * Copyright (c) 2022 Skyflow */ -import Foundation - -struct SuccessResponse: Codable { - let records: [Records] -} - -struct Records: Codable { - let fields: Fields? - let table: String - let skyflow_id: String? -} - -struct Fields: Codable { - let cardholder_name: String? - let card_number: String? - let expiry_month: String? - let expiry_year: String? - let cvv: String? - let skyflow_id: String? -} +// No longer needed - the SDK now provides typed responses directly +// (Skyflow.CollectResponse / Skyflow.RevealResponse), so this sample no longer +// needs to declare its own local response structs. diff --git a/Samples/UpdateDataUsingElements/UpdateDataUsingElements/ViewController.swift b/Samples/UpdateDataUsingElements/UpdateDataUsingElements/ViewController.swift index 6639ec61..cc18b7c8 100644 --- a/Samples/UpdateDataUsingElements/UpdateDataUsingElements/ViewController.swift +++ b/Samples/UpdateDataUsingElements/UpdateDataUsingElements/ViewController.swift @@ -69,7 +69,7 @@ class ViewController: UIViewController { label: "Card Number", placeholder: "4111-1111-1111-1111", type: Skyflow.ElementType.CARD_NUMBER, - skyflowID: "" // replace it with actual skyflowID if you want to test update with elements functionality, otherwise you can remove skyflowID field from input + skyflowId: "" // replace it with actual skyflowId if you want to test update with elements functionality, otherwise you can remove skyflowId field from input ) let collectNameInput = Skyflow.CollectElementInput( @@ -79,7 +79,7 @@ class ViewController: UIViewController { label: "Card Holder Name", placeholder: "John Doe", type: Skyflow.ElementType.CARDHOLDER_NAME, - skyflowID: "" // replace it with actual skyflowID if you want to test update with elements functionality, otherwise you can remove skyflowID field from input + skyflowId: "" // replace it with actual skyflowId if you want to test update with elements functionality, otherwise you can remove skyflowId field from input ) let collectCVVInput = Skyflow.CollectElementInput( table: "credit_cards", @@ -88,7 +88,7 @@ class ViewController: UIViewController { label: "CVV", placeholder: "***", type: .CVV, - skyflowID: "" // replace it with actual skyflowID if you want to test update with elements functionality, otherwise you can remove skyflowID field from input + skyflowId: "" // replace it with actual skyflowId if you want to test update with elements functionality, otherwise you can remove skyflowId field from input ) let collectExpMonthInput = Skyflow.CollectElementInput( table: "credit_cards", @@ -97,7 +97,7 @@ class ViewController: UIViewController { label: "Expiration Month", placeholder: "MM", type: .EXPIRATION_MONTH, - skyflowID: "" // replace it with actual skyflowID if you want to test update with elements functionality, otherwise you can remove skyflowID field from input + skyflowId: "" // replace it with actual skyflowId if you want to test update with elements functionality, otherwise you can remove skyflowId field from input ) let collectExpYearInput = Skyflow.CollectElementInput( @@ -107,7 +107,7 @@ class ViewController: UIViewController { label: "Expiration Year", placeholder: "YYYY", type: .EXPIRATION_YEAR, - skyflowID: "" // replace it with actual skyflowID if you want to test update with elements functionality, otherwise you can remove skyflowID field from input + skyflowId: "" // replace it with actual skyflowId if you want to test update with elements functionality, otherwise you can remove skyflowId field from input ) let requiredOption = Skyflow.CollectElementOptions(required: true) @@ -164,12 +164,17 @@ class ViewController: UIViewController { } @objc func submitForm() { - let exampleAPICallback = ExampleAPICallback(updateSuccess: updateSuccess, updateFailure: updateFailure) - container!.collect(callback: exampleAPICallback, options: Skyflow.CollectOptions(tokens: true)) + let collectCallback = Skyflow.CollectCallback(onSuccess: updateSuccess, onFailure: updateFailure) + container!.collect(callback: collectCallback, options: Skyflow.CollectOptions()) } - internal func updateSuccess(_ response: SuccessResponse) { + internal func updateSuccess(_ response: Skyflow.CollectResponse) { print(response) retryCount = 0 + for result in response.records { + if let error = result.error { + print("Record failed:", error, "httpCode:", result.httpCode) + } + } print("Successfully got response:", response) } internal func updateFailure(error: Any) { diff --git a/Samples/UpsertFeature/UpsertFeature.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Samples/UpsertFeature/UpsertFeature.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 040de638..d02f09c6 100644 --- a/Samples/UpsertFeature/UpsertFeature.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Samples/UpsertFeature/UpsertFeature.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,15 +1,6 @@ { "object": { "pins": [ - { - "package": "AEXML", - "repositoryURL": "https://github.com/tadija/AEXML.git", - "state": { - "branch": null, - "revision": "38f7d00b23ecd891e1ee656fa6aeebd6ba04ecc3", - "version": "4.6.1" - } - }, { "package": "Skyflow", "repositoryURL": "https://github.com/skyflowapi/skyflow-iOS.git", diff --git a/Samples/UpsertFeature/UpsertFeature/ResponseStructs.swift b/Samples/UpsertFeature/UpsertFeature/ResponseStructs.swift index 6ea1a8a6..67c28811 100644 --- a/Samples/UpsertFeature/UpsertFeature/ResponseStructs.swift +++ b/Samples/UpsertFeature/UpsertFeature/ResponseStructs.swift @@ -2,19 +2,6 @@ * Copyright (c) 2022 Skyflow */ -import Foundation - -struct SuccessResponse: Codable { - let records: [Records] -} - -struct Records: Codable { - let fields: Fields - let table: String -} - -struct Fields: Codable { - let cardnumber: String - let cvv: String - let skyflow_id: String -} +// No longer needed - the SDK now provides typed responses directly +// (Skyflow.CollectResponse / Skyflow.RevealResponse), so this sample no longer +// needs to declare its own local response structs. diff --git a/Samples/UpsertFeature/UpsertFeature/ViewController.swift b/Samples/UpsertFeature/UpsertFeature/ViewController.swift index 86f38902..1e82b7cf 100644 --- a/Samples/UpsertFeature/UpsertFeature/ViewController.swift +++ b/Samples/UpsertFeature/UpsertFeature/ViewController.swift @@ -15,6 +15,7 @@ class ViewController: UIViewController { private var revealCvv: Label? private var revealButton: UIButton! private var revealed = false + private var tokenGroupRedactions: [Skyflow.TokenGroupRedaction] = [] override func loadView() { let view = UIView() @@ -40,7 +41,7 @@ class ViewController: UIViewController { textAlignment: .left, textColor: .blue ) - let focusStyle = Skyflow.Style(borderColor: .blue) ̰ + let focusStyle = Skyflow.Style(borderColor: .blue) let completedStyle = Skyflow.Style(borderColor: UIColor.green, textColor: UIColor.green) let invalidStyle = Skyflow.Style(borderColor: UIColor.red, textColor: UIColor.red) let styles = Skyflow.Styles( @@ -150,25 +151,52 @@ class ViewController: UIViewController { } @objc func revealForm() { - self.revealContainer?.reveal(callback: ExampleAPICallback()) + let revealCallback = Skyflow.RevealCallback( + onSuccess: { (response: Skyflow.RevealResponse) in + for record in response.records { + if let error = record.error { + print("reveal failed:", error, "httpCode:", record.httpCode) + } else { + print("revealed:", record.token ?? "", record.tokenGroupName ?? "", record.metadata ?? [:]) + } + } + }, + onFailure: { (error: Skyflow.SkyflowError) in print("reveal failure:", error.httpCode, error.message) } + ) + let revealOptions = Skyflow.RevealOptions(tokenGroupRedactions: self.tokenGroupRedactions.isEmpty ? nil : self.tokenGroupRedactions) + self.revealContainer?.reveal(callback: revealCallback, options: revealOptions) } @objc func submitForm() { - let exampleAPICallback = ExampleAPICallback(updateSuccess: updateSuccess, updateFailure: updateFailure) - let upsertOptions = [["table": "persons", "column": "cardnumber"]] as [[String: Any]] - container!.collect(callback: exampleAPICallback, options: Skyflow.CollectOptions(tokens: true)) + let collectCallback = Skyflow.CollectCallback(onSuccess: updateSuccess, onFailure: updateFailure) + let upsertOptions = [Skyflow.UpsertOption(table: "persons", uniqueColumns: ["cardnumber"], updateType: .UPDATE)] + container!.collect(callback: collectCallback, options: Skyflow.CollectOptions(upsert: upsertOptions)) } - internal func updateSuccess(_ response: SuccessResponse) { - updateRevealInputs(tokens: response.records[0].fields) + internal func updateSuccess(_ response: Skyflow.CollectResponse) { + print(response) + for result in response.records { + if let error = result.error { + print("Record failed:", error, "httpCode:", result.httpCode) + } + } + if let fields = response.records.first?.fields { + updateRevealInputs(tokens: fields) + } print("Successfully got response:", response) } - internal func updateFailure() { - print("Failed Operation") + internal func updateFailure(error: Skyflow.SkyflowError) { + print("Failed Operation", error.httpCode, error.message, error.grpcCode ?? "", error.httpStatus ?? "", error.details ?? "") + } + + // fields is [String: Any] - column name -> array of {"token","tokenGroupName"} dicts. + internal func firstToken(_ fields: [String: Any], column: String) -> (token: String, tokenGroupName: String?) { + guard let entries = fields[column] as? [[String: Any]], let first = entries.first else { return ("", nil) } + return (first["token"] as? String ?? "", first["tokenGroupName"] as? String) } - internal func updateRevealInputs(tokens: Fields) { + internal func updateRevealInputs(tokens: [String: Any]) { let revealBaseStyle = Skyflow.Style( borderColor: UIColor.black, cornerRadius: 20, @@ -184,21 +212,30 @@ class ViewController: UIViewController { } else { self.revealed = true } + // tokenGroupRedactions: request-level redaction per token group, applied when + // reveal() is called (see revealForm()) - not per reveal element. + self.tokenGroupRedactions = [] + func addTokenGroupRedaction(_ redaction: String, forColumn column: String) { + if let tokenGroupName = firstToken(tokens, column: column).tokenGroupName { + self.tokenGroupRedactions.append(Skyflow.TokenGroupRedaction(tokenGroupName: tokenGroupName, redaction: redaction)) + } + } + addTokenGroupRedaction("DEFAULT", forColumn: "cardnumber") + addTokenGroupRedaction("DEFAULT", forColumn: "cvv") + let revealCardNumberInput = Skyflow.RevealElementInput( - token: tokens.cardnumber, + token: firstToken(tokens, column: "cardnumber").token, inputStyles: revealStyles, - label: "Card Number", - redaction: .DEFAULT + label: "Card Number" ) self.revealCardNumber = self.revealContainer?.create( input: revealCardNumberInput, options: Skyflow.RevealElementOptions() ) let revealCvvInput = Skyflow.RevealElementInput( - token: tokens.cvv, + token: firstToken(tokens, column: "cvv").token, inputStyles: revealStyles, - label: "Cvv", - redaction: .DEFAULT + label: "Cvv" ) self.revealCvv = self.revealContainer?.create( input: revealCvvInput, diff --git a/Samples/Validations/Podfile b/Samples/Validations/Podfile index cb757a40..e8fecb94 100644 --- a/Samples/Validations/Podfile +++ b/Samples/Validations/Podfile @@ -5,8 +5,7 @@ source 'https://github.com/CocoaPods/Specs.git' target 'Validations' do #use_frameworks! - pod 'AEXML', '4.6.1' # Pods for skyflow-iOS-sample-app - pod 'Skyflow', ">= 1.22.0" + pod 'Skyflow', ">= 1.25.0" end diff --git a/Sources/Skyflow/collect/AdditionalFields.swift b/Sources/Skyflow/collect/AdditionalFields.swift new file mode 100644 index 00000000..62fd0c99 --- /dev/null +++ b/Sources/Skyflow/collect/AdditionalFields.swift @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// Typed replacement for the old [String: Any] additionalFields shape on CollectOptions - +// non-PCI records to insert/update alongside whatever's collected from mounted elements. + +import Foundation + +public struct AdditionalFieldsRecord { + public let table: String + public let fields: [String: Any] + // Present to update an existing record, absent to insert a new one. + public let skyflowId: String? + + public init(table: String, fields: [String: Any], skyflowId: String? = nil) { + self.table = table + self.fields = fields + self.skyflowId = skyflowId + } +} + +public struct AdditionalFields { + public let records: [AdditionalFieldsRecord] + + public init(records: [AdditionalFieldsRecord]) { + self.records = records + } +} diff --git a/Sources/Skyflow/collect/CollectContainer.swift b/Sources/Skyflow/collect/CollectContainer.swift index 16bb9035..2a5b1d91 100644 --- a/Sources/Skyflow/collect/CollectContainer.swift +++ b/Sources/Skyflow/collect/CollectContainer.swift @@ -22,14 +22,14 @@ public extension Container { return skyflowElement } - func collect(callback: Callback, options: CollectOptions? = CollectOptions()) where T: CollectContainer { + func collect(callback: CollectCallback, options: CollectOptions? = CollectOptions()) where T: CollectContainer { var tempContextOptions = self.skyflow.contextOptions tempContextOptions.interface = .COLLECT_CONTAINER if self.skyflow.vaultID.isEmpty { let errorCode = ErrorCodes.EMPTY_VAULT_ID() return callback.onFailure(errorCode.getErrorObject(contextOptions: tempContextOptions)) } - if self.skyflow.vaultURL == "/v1/vaults/" { + if self.skyflow.vaultURL == "/v2/vaults/" { let errorCode = ErrorCodes.EMPTY_VAULT_URL() return callback.onFailure(errorCode.getErrorObject(contextOptions: tempContextOptions)) } @@ -59,33 +59,23 @@ public extension Container { } } if errors != "" { - callback.onFailure(NSError(domain: "", code: 400, userInfo: [NSLocalizedDescriptionKey: errors])) + callback.onFailure(SkyflowError(domain: "", code: 400, userInfo: [NSLocalizedDescriptionKey: errors])) return } - if options?.additionalFields != nil { - if options?.additionalFields!["records"] == nil { - errorCode = .MISSING_RECORDS_IN_ADDITIONAL_FIELDS() + if let additionalFields = options?.additionalFields { + if additionalFields.records.isEmpty { + errorCode = .EMPTY_RECORDS_OBJECT() return callback.onFailure(errorCode!.getErrorObject(contextOptions: tempContextOptions)) } - if let additionalFieldEntries = options?.additionalFields!["records"] as? [[String: Any]] { - if additionalFieldEntries.isEmpty { - errorCode = .EMPTY_RECORDS_OBJECT() + for (index, record) in additionalFields.records.enumerated() { + errorCode = checkRecord(record: record, index: index) + if errorCode != nil { return callback.onFailure(errorCode!.getErrorObject(contextOptions: tempContextOptions)) } - for (index, record) in additionalFieldEntries.enumerated() { - errorCode = checkRecord(record: record, index: index) - if errorCode != nil { - return callback.onFailure(errorCode!.getErrorObject(contextOptions: tempContextOptions)) - } - } - } else { - errorCode = .INVALID_RECORDS_TYPE() - callback.onFailure(errorCode!.getErrorObject(contextOptions: tempContextOptions)) - return } } - let records = CollectRequestBody.createRequestBody(elements: self.elements, additionalFields: options?.additionalFields, callback: callback, contextOptions: tempContextOptions) - let icOptions = ICOptions(tokens: options!.tokens, additionalFields: options?.additionalFields, upsert: options?.upsert, callback: callback, contextOptions: tempContextOptions) + let records = FlowVaultCollectRequestBody.createRequestBody(elements: self.elements, additionalFields: options?.additionalFields, callback: callback, contextOptions: tempContextOptions) + let icOptions = FlowVaultICOptions(additionalFields: options?.additionalFields, upsert: options?.upsert, callback: callback, contextOptions: tempContextOptions) if options?.upsert != nil { if icOptions.validateUpsert() { return; @@ -117,27 +107,13 @@ public extension Container { return nil } - private func checkRecord(record: [String: Any], index: Int) -> ErrorCodes? { - if record["table"] == nil { - return .TABLE_KEY_ERROR(value: "\(index)") - } - if !(record["table"] is String) { - return .INVALID_TABLE_NAME_TYPE(value: "\(index)") - } - if (record["table"] as? String == "") { + private func checkRecord(record: AdditionalFieldsRecord, index: Int) -> ErrorCodes? { + if record.table.isEmpty { return .EMPTY_TABLE_NAME() } - if record["fields"] == nil { - return .FIELDS_KEY_ERROR(value: "\(index)") - } - if !(record["fields"] is [String: Any]) { - return .INVALID_FIELDS_TYPE(value: "\(index)") - } - let fields = record["fields"] as! [String: Any] - if (fields.isEmpty){ + if record.fields.isEmpty { return .EMPTY_FIELDS_KEY(value: "\(index)") } - return nil } } diff --git a/Sources/Skyflow/collect/CollectOptions.swift b/Sources/Skyflow/collect/CollectOptions.swift index 369961bb..922b1f44 100644 --- a/Sources/Skyflow/collect/CollectOptions.swift +++ b/Sources/Skyflow/collect/CollectOptions.swift @@ -7,11 +7,9 @@ import Foundation public struct CollectOptions { - var tokens: Bool - var additionalFields: [String: Any]? - var upsert: [[String: Any]]? - public init(tokens: Bool = true, additionalFields: [String: Any]? = nil, upsert: [[String: Any]]? = nil) { - self.tokens = tokens + var additionalFields: AdditionalFields? + var upsert: [UpsertOption]? + public init(additionalFields: AdditionalFields? = nil, upsert: [UpsertOption]? = nil) { self.additionalFields = additionalFields self.upsert = upsert } diff --git a/Sources/Skyflow/collect/CollectRequestBody.swift b/Sources/Skyflow/collect/CollectRequestBody.swift index b687f141..6f3d04a3 100644 --- a/Sources/Skyflow/collect/CollectRequestBody.swift +++ b/Sources/Skyflow/collect/CollectRequestBody.swift @@ -73,11 +73,11 @@ internal class CollectRequestBody { let entryDict = entry let tableName = entryDict["table"] as! String let fields = entryDict["fields"] as! [String: Any] - if entryDict["skyflowID"] != nil { + if entryDict["skyflowId"] != nil { // add to update payload - let skyflowID = entryDict["skyflowID"] as! String - if updatePayload[skyflowID] != nil { - let temp = updatePayload[skyflowID] as! [String: Any] + let skyflowId = entryDict["skyflowId"] as! String + if updatePayload[skyflowId] != nil { + let temp = updatePayload[skyflowId] as! [String: Any] // merge existing fields with new field var existingFields = temp["fields"] as! [String: Any] for (key, val) in fields { @@ -86,14 +86,14 @@ internal class CollectRequestBody { } var updatedTemp = temp updatedTemp["fields"] = existingFields - updatePayload[skyflowID] = updatedTemp + updatePayload[skyflowId] = updatedTemp } else { var temp: [String: Any] = [ "table": tableName, "fields": fields, - "skyflowID": skyflowID + "skyflowId": skyflowId ] - updatePayload[skyflowID] = temp + updatePayload[skyflowId] = temp } } else { if tableMap[tableName] != nil { @@ -126,11 +126,11 @@ internal class CollectRequestBody { let tableName = element.tableName! let columnName = element.columnName! let value = element.getValue() - let skyflowID = element.skyflowID // Assumes TextField has this property + let skyflowId = element.skyflowId // Assumes TextField has this property - if let skyflowID = skyflowID, !skyflowID.isEmpty { - if updatePayload[skyflowID] != nil { - var temp = updatePayload[skyflowID] as! [String: Any] + if let skyflowId = skyflowId, !skyflowId.isEmpty { + if updatePayload[skyflowId] != nil { + var temp = updatePayload[skyflowId] as! [String: Any] // merge existing fields with new field var existingFields = temp["fields"] as! [String: Any] if existingFields[columnName] != nil { @@ -151,14 +151,14 @@ internal class CollectRequestBody { existingFields[columnName] = value } temp["fields"] = existingFields - updatePayload[skyflowID] = temp + updatePayload[skyflowId] = temp } else { var temp: [String: Any] = [ "table": tableName, "fields": [columnName: value], - "skyflowID": skyflowID + "skyflowId": skyflowId ] - updatePayload[skyflowID] = temp + updatePayload[skyflowId] = temp } } else { // Only add to payload diff --git a/Sources/Skyflow/collect/CollectResponse.swift b/Sources/Skyflow/collect/CollectResponse.swift new file mode 100644 index 00000000..4aa6ee74 --- /dev/null +++ b/Sources/Skyflow/collect/CollectResponse.swift @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +import Foundation + +public struct CollectResponse { + public let records: [CollectRecord] + + public init?(_ responseBody: Any) { + guard let dict = responseBody as? [String: Any], + let recordDicts = dict["records"] as? [[String: Any]] else { return nil } + self.records = recordDicts.map { CollectRecord($0) } + } +} + +// Each entry in "records" is either a successfully inserted/updated record (error is nil) or a +// failed one (error is non-nil) - the vault returns both together in the same array, each tagged +// with its own httpCode. +public struct CollectRecord { + public let tableName: String? + public let skyflowID: String? + // Keyed by column name, e.g. "card_number": [{"token": "...", "tokenGroupName": "..."}]. + public let fields: [String: Any]? + // Keyed by column name, e.g. "card_number": [{"data": "...", "hashName": "..."}]. + public let hashedData: [String: Any]? + public let httpCode: Int + public let error: String? + + init(_ dict: [String: Any]) { + self.tableName = dict["tableName"] as? String + self.skyflowID = dict["skyflowID"] as? String + self.fields = dict["fields"] as? [String: Any] + self.hashedData = dict["hashedData"] as? [String: Any] + self.httpCode = dict["httpCode"] as? Int ?? 0 + self.error = dict["error"] as? String + } +} + +// A ready-made Skyflow.Callback that unwraps the raw responseBody/error into CollectResponse +// for you. Use it directly with the original callback-based collect(callback:options:). +public class CollectCallback: Callback { + private let successHandler: (CollectResponse) -> Void + private let failureHandler: (SkyflowError) -> Void + + public init(onSuccess: @escaping (CollectResponse) -> Void, onFailure: @escaping (SkyflowError) -> Void) { + self.successHandler = onSuccess + self.failureHandler = onFailure + } + + public func onSuccess(_ responseBody: Any) { + guard let response = CollectResponse(responseBody) else { + failureHandler(SkyflowError.wrap(responseBody)) + return + } + successHandler(response) + } + + // Delivered when the entire request fails (e.g. vault not found, network error) rather + // than a per-record failure inside CollectResponse.records, and for client-side validation + // failures. Always normalized into a Skyflow.SkyflowError - see SkyflowError.wrap. + public func onFailure(_ error: Any) { + failureHandler(SkyflowError.wrap(error)) + } +} diff --git a/Sources/Skyflow/collect/FlowVaultCollectAPICallback.swift b/Sources/Skyflow/collect/FlowVaultCollectAPICallback.swift new file mode 100644 index 00000000..ebfdb0a9 --- /dev/null +++ b/Sources/Skyflow/collect/FlowVaultCollectAPICallback.swift @@ -0,0 +1,292 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// Callback used while API callback for Collect the elements (FlowDB v2) + +import Foundation +import UIKit + +internal class FlowVaultCollectAPICallback: Callback { + var apiClient: APIClient + var records: [String: Any] + var callback: Callback + var options: FlowVaultICOptions + var contextOptions: ContextOptions + + internal init(callback: Callback, apiClient: APIClient, records: [String: Any], options: FlowVaultICOptions, contextOptions: ContextOptions) { + self.records = records + self.apiClient = apiClient + self.callback = callback + self.options = options + self.contextOptions = contextOptions + } + + internal func onSuccess(_ responseBody: Any) { + let insertRecords = records["records"] as? [[String: Any]] ?? [] + let updateRecords = self.flattenUpdates(records["update"] as? [String: Any] ?? [:]) + let hasInsert = !insertRecords.isEmpty + let hasUpdate = !updateRecords.isEmpty + + if !hasInsert && !hasUpdate { + self.callback.onSuccess(["records": []]) + return + } + + // No update records: preserve the original single-call insert behavior exactly + // (including passing a full-request-level failure straight through, unwrapped). + if !hasUpdate { + guard let url = URL(string: self.apiClient.vaultURL + "v2/records/insert") else { + self.callback.onFailure(ErrorCodes.INVALID_URL().getErrorObject(contextOptions: self.contextOptions)) + return + } + do { + let (request, session) = try self.getRequestSession(url: url) + let task = session.dataTask(with: request) { data, response, error in + do { + let response = try self.processResponse(data: data, response: response, error: error) + if response["error"] != nil { + self.callback.onFailure(response) + return + } + self.callback.onSuccess(["records": response["records"] as? [[String: Any]] ?? []]) + } catch { + self.callback.onFailure(error) + } + } + task.resume() + } catch let error { + self.callback.onFailure(error) + } + return + } + + guard URL(string: self.apiClient.vaultURL + "v2/records/insert") != nil else { + self.callback.onFailure(ErrorCodes.INVALID_URL().getErrorObject(contextOptions: self.contextOptions)) + return + } + + let group = DispatchGroup() + var mergedRecords: [[String: Any]] = [] + var mergedErrors: [[String: Any]] = [] + + if hasInsert { + group.enter() + let url = URL(string: self.apiClient.vaultURL + "v2/records/insert")! + do { + let (request, session) = try self.getRequestSession(url: url) + let task = session.dataTask(with: request) { data, response, error in + defer { group.leave() } + do { + let response = try self.processResponse(data: data, response: response, error: error) + if response["error"] != nil { + mergedErrors.append(response) + } else { + mergedRecords.append(contentsOf: response["records"] as? [[String: Any]] ?? []) + } + } catch { + mergedErrors.append(["error": error.localizedDescription]) + } + } + task.resume() + } catch let error { + mergedErrors.append(["error": error.localizedDescription]) + group.leave() + } + } + + if hasUpdate { + group.enter() + let url = URL(string: self.apiClient.vaultURL + "v2/records/update")! + do { + let (request, session) = try self.getUpdateRequestSession(url: url, records: updateRecords) + let task = session.dataTask(with: request) { data, response, error in + defer { group.leave() } + do { + let response = try self.processResponse(data: data, response: response, error: error) + if response["error"] != nil { + mergedErrors.append(response) + } else { + mergedRecords.append(contentsOf: response["records"] as? [[String: Any]] ?? []) + } + } catch { + mergedErrors.append(["error": error.localizedDescription]) + } + } + task.resume() + } catch let error { + mergedErrors.append(["error": error.localizedDescription]) + group.leave() + } + } + + group.notify(queue: .main) { + if mergedErrors.isEmpty { + self.callback.onSuccess(["records": mergedRecords]) + } else { + self.callback.onFailure(["records": mergedRecords, "errors": mergedErrors]) + } + } + } + + internal func onFailure(_ error: Any) { + self.callback.onFailure(error) + } + + internal func flattenUpdates(_ updateDict: [String: Any]) -> [[String: Any]] { + var updateRecords: [[String: Any]] = [] + for (skyflowID, value) in updateDict { + guard let entry = value as? [String: Any], + let tableName = entry["table"] as? String, + let fields = entry["fields"] as? [String: Any] else { continue } + updateRecords.append(["skyflowID": skyflowID, "tableName": tableName, "data": fields]) + } + return updateRecords + } + + internal func buildFieldsDict(dict: [String: Any]) -> [String: Any] { + var temp: [String: Any] = [:] + for (key, val) in dict { + if let v = val as? [String: Any] { + temp[key] = buildFieldsDict(dict: v) + } else { + temp[key] = val + } + } + return temp + } + internal func getRequestSession(url: URL) throws -> (URLRequest, URLSession) { + var jsonString = "" + + do { + let deviceDetails = FetchMetrices().getMetrices() + let jsonData = try JSONSerialization.data(withJSONObject: deviceDetails, options: []) + jsonString = String(data: jsonData, encoding: .utf8) ?? "" + } catch { + jsonString = "" + } + var request = URLRequest(url: url) + request.httpMethod = "POST" + + do { + let data = try JSONSerialization.data(withJSONObject: FlowVaultInsertRequestBody.createRequestBody(vaultID: self.apiClient.vaultID, records: self.records, options: options)) + request.httpBody = data + } + + request.setValue(("Bearer " + self.apiClient.token), forHTTPHeaderField: "Authorization") + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.setValue("application/json", forHTTPHeaderField: "Accept") + request.setValue(jsonString, forHTTPHeaderField: "sky-metadata") + + return (request, URLSession(configuration: .default)) + + } + + internal func getUpdateRequestSession(url: URL, records: [[String: Any]]) throws -> (URLRequest, URLSession) { + var jsonString = "" + + do { + let deviceDetails = FetchMetrices().getMetrices() + let jsonData = try JSONSerialization.data(withJSONObject: deviceDetails, options: []) + jsonString = String(data: jsonData, encoding: .utf8) ?? "" + } catch { + jsonString = "" + } + var request = URLRequest(url: url) + request.httpMethod = "POST" + + do { + let data = try JSONSerialization.data(withJSONObject: FlowVaultUpdateRequestBody.createRequestBody(vaultID: self.apiClient.vaultID, records: records)) + request.httpBody = data + } + + request.setValue(("Bearer " + self.apiClient.token), forHTTPHeaderField: "Authorization") + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.setValue("application/json", forHTTPHeaderField: "Accept") + request.setValue(jsonString, forHTTPHeaderField: "sky-metadata") + + return (request, URLSession(configuration: .default)) + } + + func processResponse(data: Data?, response: URLResponse?, error: Error?) throws -> [String: Any] { + if error != nil || response == nil { + return ["error": ["message": (error)?.localizedDescription ?? "Unknown error"]] + } + + if let httpResponse = response as? HTTPURLResponse { + let range = 400...599 + if range ~= httpResponse.statusCode { + // FlowDB returns a non-2xx status when every record in the batch fails, + // but the body still has the same {"records": [...]} shape as a success/partial + // response (each record carrying its own error/httpCode) - parse it as such. + if let safeData = data, + let jsonObject = try? JSONSerialization.jsonObject(with: safeData, options: .allowFragments) as? [String: Any], + jsonObject["records"] != nil { + return try getCollectResponseBody(data: safeData) + } + var description = "Insert call failed with the following status code " + String(httpResponse.statusCode) + if let safeData = data { + guard let errorResponse = try? JSONSerialization.jsonObject(with: safeData, options: .allowFragments) as? [String: Any] else { + return ["error": ["message": String(data: safeData, encoding: .utf8) ?? "Unknown error", "httpCode": httpResponse.statusCode]] + } + // Pass the vault's structured error object (grpcCode/httpStatus/details) through + // as-is, only patching in the request-id and defaulting httpCode when absent. + if var errorDict = errorResponse["error"] as? [String: Any] { + if let message = errorDict["message"] as? String { + description = message + if let requestId = httpResponse.allHeaderFields["x-request-id"] { + description += " - request-id: \(requestId)" + } + errorDict["message"] = description + } + if errorDict["httpCode"] == nil { errorDict["httpCode"] = httpResponse.statusCode } + return ["error": errorDict] + } + if let requestId = httpResponse.allHeaderFields["x-request-id"] { + description += " - request-id: \(requestId)" + } + } + return ["error": ["message": description, "httpCode": httpResponse.statusCode]] + } + } + + guard let safeData = data else { + return ["records": []] + } + + return try getCollectResponseBody(data: safeData) + + } + + func getCollectResponseBody(data: Data) throws -> [String: Any]{ + let jsonData = (try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String: Any]) ?? [:] + var records: [[String: Any]] = [] + + let responseRecords = jsonData["records"] as? [[String: Any]] ?? [] + for entry in responseRecords { + if let error = entry["error"] as? String { + var errorEntry: [String: Any] = ["error": error] + if let skyflowID = entry["skyflowID"] { errorEntry["skyflowID"] = skyflowID } + if let tableName = entry["tableName"] { errorEntry["tableName"] = tableName } + if let httpCode = entry["httpCode"] { errorEntry["httpCode"] = httpCode } + records.append(errorEntry) + } else { + var successEntry: [String: Any] = [:] + if let skyflowID = entry["skyflowID"] { successEntry["skyflowID"] = skyflowID } + if let tableName = entry["tableName"] { successEntry["tableName"] = tableName } + var fields: [String: Any] = [:] + if let tokens = entry["tokens"] as? [String: Any] { + for (column, tokenValue) in self.buildFieldsDict(dict: tokens) { + fields[column] = tokenValue + } + } + successEntry["fields"] = fields + if let hashedData = entry["hashedData"] as? [String: Any] { successEntry["hashedData"] = self.buildFieldsDict(dict: hashedData) } + if let httpCode = entry["httpCode"] { successEntry["httpCode"] = httpCode } + records.append(successEntry) + } + } + + return ["records": records] + } +} diff --git a/Sources/Skyflow/collect/FlowVaultCollectRequestBody.swift b/Sources/Skyflow/collect/FlowVaultCollectRequestBody.swift new file mode 100644 index 00000000..aa53482f --- /dev/null +++ b/Sources/Skyflow/collect/FlowVaultCollectRequestBody.swift @@ -0,0 +1,195 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// Class for formatting the request body for collecting the data (FlowDB v2) + +import Foundation + +internal class FlowVaultCollectRequestBody { + static var tableSet: Set = Set() + static var callback: Callback? + static var breakFlag = false + static var mergedDict: [String: Any] = [:] + + internal static func addFieldsToTableSet(tableName: String, prefix: String, fields: [String: Any], contextOptions: ContextOptions) { + if !self.breakFlag { + for (key, val) in fields { + if val is [String: Any] { + addFieldsToTableSet(tableName: tableName, prefix: prefix == "" ? key : prefix + "." + key, fields: val as! [String: Any], contextOptions: contextOptions) + } else { + let tableSetEntry = tableName + "-" + (prefix == "" ? key : prefix + "." + key) + if tableSet.contains(tableSetEntry) { + if !self.breakFlag { + self.callback?.onFailure(ErrorCodes.DUPLICATE_ADDITIONAL_FIELD_FOUND(value: key).getErrorObject(contextOptions: contextOptions)) + self.breakFlag = true + return + } + } else { + self.tableSet.insert(tableSetEntry) + } + } + } + } + } + + internal static func mergeFields(tableName: String, prefix: String, dict: [String: Any], contextOptions: ContextOptions) { + for(key, val) in dict { + let keypath = prefix == "" ? key : prefix + "." + key + if val is [String: Any] { + mergeFields(tableName: tableName, prefix: keypath, dict: val as! [String: Any], contextOptions: contextOptions) + } else { + if mergedDict[keyPath: keypath] == nil { + mergedDict[keyPath: keypath] = val + } else { + if !self.breakFlag { + self.callback?.onFailure(ErrorCodes.DUPLICATE_ADDITIONAL_FIELD_FOUND(value: key).getErrorObject(contextOptions: contextOptions)) + self.breakFlag = true + return + } + } + } + } + } + + internal static func createRequestBody( + elements: [TextField], + additionalFields: AdditionalFields? = nil, + callback: Callback, + contextOptions: ContextOptions + ) -> [String: Any]? { + var tableMap: [String: Int] = [:] + var payload: [[String: Any]] = [] + var updatePayload: [String: Any] = [:] + self.callback = callback + self.breakFlag = false + self.tableSet = Set() + var index: Int = 0 + + if let additionalFields = additionalFields { + for entry in additionalFields.records { + let tableName = entry.table + let fields = entry.fields + // Matches the element-based check below: an empty string is treated the same as + // absent, falling back to a plain insert rather than an update targeting "". + if let skyflowId = entry.skyflowId, !skyflowId.isEmpty { + if updatePayload[skyflowId] != nil { + let temp = updatePayload[skyflowId] as! [String: Any] + var existingFields = temp["fields"] as! [String: Any] + for (key, val) in fields { + existingFields[key] = val + } + var updatedTemp = temp + updatedTemp["fields"] = existingFields + updatePayload[skyflowId] = updatedTemp + } else { + let temp: [String: Any] = [ + "table": tableName, + "fields": fields, + "skyflowId": skyflowId + ] + updatePayload[skyflowId] = temp + } + continue + } + if tableMap[tableName] != nil { + let inputEntry = payload[tableMap[tableName]!] + mergedDict = inputEntry["fields"] as! [String: Any] + self.mergeFields(tableName: tableName, prefix: "", dict: fields, contextOptions: contextOptions) + if self.breakFlag { + return nil + } + payload[tableMap[tableName]!]["fields"] = mergedDict + mergedDict = [:] + } else { + tableMap[tableName] = index + let temp: [String: Any] = [ + "table": tableName, + "fields": fields + ] + self.addFieldsToTableSet(tableName: tableName, prefix: "", fields: fields, contextOptions: contextOptions) + if self.breakFlag { + return nil + } + payload.append(temp) + index += 1 + } + } + } + + for element in elements { + let tableName = element.tableName! + let columnName = element.columnName! + let value = element.getValue() + let skyflowId = element.skyflowId // Assumes TextField has this property + + if let skyflowId = skyflowId, !skyflowId.isEmpty { + if updatePayload[skyflowId] != nil { + var temp = updatePayload[skyflowId] as! [String: Any] + var existingFields = temp["fields"] as! [String: Any] + if existingFields[columnName] != nil { + var hasElementValueMatchRule: Bool = false + for validation in element.userValidationRules.rules { + if validation is ElementValueMatchRule { + hasElementValueMatchRule = true + break; + } + } + if(!hasElementValueMatchRule) + { + self.callback?.onFailure(ErrorCodes.DUPLICATE_ELEMENT_FOUND(values: [ element.columnName, element.tableName!]).getErrorObject(contextOptions: contextOptions)) + return nil + } + continue; + } else { + existingFields[columnName] = value + } + temp["fields"] = existingFields + updatePayload[skyflowId] = temp + } else { + let temp: [String: Any] = [ + "table": tableName, + "fields": [columnName: value], + "skyflowId": skyflowId + ] + updatePayload[skyflowId] = temp + } + } else { + // Only add to payload + if tableMap[(element.tableName)!] != nil { + var temp = payload[tableMap[(element.tableName)!]!] + temp[keyPath: "fields." + (element.columnName)!] = element.getValue() + let tableSetEntry = element.tableName! + "-" + element.columnName + if tableSet.contains(tableSetEntry) { + var hasElementValueMatchRule: Bool = false + for validation in element.userValidationRules.rules { + if validation is ElementValueMatchRule { + hasElementValueMatchRule = true + break; + } + } + if(!hasElementValueMatchRule) + { + self.callback?.onFailure(ErrorCodes.DUPLICATE_ELEMENT_FOUND(values: [ element.columnName, element.tableName!]).getErrorObject(contextOptions: contextOptions)) + return nil + } + continue; + } + self.tableSet.insert(tableSetEntry) + payload[tableMap[(element.tableName)!]!] = temp + } else { + tableMap[(element.tableName)!] = index + index += 1 + var temp: [String: Any] = [ + "table": element.tableName!, + "fields": [:] + ] + temp[keyPath: "fields." + element.columnName!] = element.getValue() + self.tableSet.insert(element.tableName! + "-" + element.columnName) + payload.append(temp) + } + } + } + return ["records": payload, "update": updatePayload] + } +} diff --git a/Sources/Skyflow/collect/FlowVaultICOptions.swift b/Sources/Skyflow/collect/FlowVaultICOptions.swift new file mode 100644 index 00000000..b4ba2360 --- /dev/null +++ b/Sources/Skyflow/collect/FlowVaultICOptions.swift @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// Options passed internally for FlowDB v2 collect/insert calls + +import Foundation + +internal struct FlowVaultICOptions { + var additionalFields: AdditionalFields? + var upsert: [UpsertOption]? + var callback: Callback? + var contextOptions: ContextOptions? + + init(additionalFields: AdditionalFields? = nil, upsert: [UpsertOption]? = nil, callback: Callback? = nil, contextOptions: ContextOptions? = nil) { + self.additionalFields = additionalFields + self.upsert = upsert + self.callback = callback + self.contextOptions = contextOptions + } + + public func validateUpsert() -> Bool{ + if self.upsert != nil { + if self.upsert!.count == 0 { + let errorCode = ErrorCodes.UPSERT_OPTION_CANNOT_BE_EMPTY() + self.callback!.onFailure(errorCode.getErrorObject(contextOptions: self.contextOptions!)) + return true + } + + for (index, currUpsertOption) in self.upsert!.enumerated() { + if currUpsertOption.table == "" { + let errorCode = ErrorCodes.TABLE_NAME_IS_EMPTY_FOR_ATLEAST_ONE_UPSERT_OPTION(value: "\(index)") + self.callback!.onFailure(errorCode.getErrorObject(contextOptions: self.contextOptions!)) + return true + } + if currUpsertOption.uniqueColumns.isEmpty { + let errorCode = ErrorCodes.UNIQUE_COLUMNS_EMPTY_FOR_ATLEAST_ONE_UPSERT_OPTION(value: "\(index)") + self.callback!.onFailure(errorCode.getErrorObject(contextOptions: self.contextOptions!)) + return true + } + } + + } + return false; + } +} diff --git a/Sources/Skyflow/collect/FlowVaultInsertAPICallback.swift b/Sources/Skyflow/collect/FlowVaultInsertAPICallback.swift new file mode 100644 index 00000000..22a99ac2 --- /dev/null +++ b/Sources/Skyflow/collect/FlowVaultInsertAPICallback.swift @@ -0,0 +1,295 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// Callback used while API callback for Collect the elements (FlowDB v2) + +import Foundation +import UIKit + +internal class FlowVaultInsertAPICallback: Callback { + // Overridable only for tests (e.g. injecting a URLProtocol mock via protocolClasses) - global + // URLProtocol.registerClass(_:) isn't reliably consulted for manually-created URLSession + // instances in every environment, so tests need a real seam here instead. + internal static var urlSessionConfiguration: URLSessionConfiguration = .default + var apiClient: APIClient + var records: [String: Any] + var callback: Callback + var options: FlowVaultICOptions + var contextOptions: ContextOptions + + internal init(callback: Callback, apiClient: APIClient, records: [String: Any], options: FlowVaultICOptions, contextOptions: ContextOptions) { + self.records = records + self.apiClient = apiClient + self.callback = callback + self.options = options + self.contextOptions = contextOptions + } + internal func onSuccess(_ responseBody: Any) { + let insertRecords = records["records"] as? [[String: Any]] ?? [] + let updateRecords = self.flattenUpdates(records["update"] as? [String: Any] ?? [:]) + let hasInsert = !insertRecords.isEmpty + let hasUpdate = !updateRecords.isEmpty + + if !hasInsert && !hasUpdate { + self.callback.onSuccess(["records": []]) + return + } + + // No update records: preserve the original single-call insert behavior exactly + // (including throwing a full-request-level failure straight to onFailure). + if !hasUpdate { + guard let url = URL(string: self.apiClient.vaultURL + "v2/records/insert") else { + self.callback.onFailure(ErrorCodes.INVALID_URL().getErrorObject(contextOptions: self.contextOptions)) + return + } + do { + let (request, session) = try self.getRequestSession(url: url) + let task = session.dataTask(with: request) { data, response, error in + do { + let response = try self.processResponse(data: data, response: response, error: error) + if response["error"] != nil { + self.callback.onFailure(response) + return + } + self.callback.onSuccess(["records": response["records"] as? [[String: Any]] ?? []]) + } catch { + self.callback.onFailure(error) + } + } + task.resume() + } catch let error { + self.callback.onFailure(error) + } + return + } + + guard URL(string: self.apiClient.vaultURL + "v2/records/insert") != nil else { + self.callback.onFailure(ErrorCodes.INVALID_URL().getErrorObject(contextOptions: self.contextOptions)) + return + } + + let group = DispatchGroup() + var mergedRecords: [[String: Any]] = [] + var mergedErrors: [[String: Any]] = [] + + if hasInsert { + group.enter() + let url = URL(string: self.apiClient.vaultURL + "v2/records/insert")! + do { + let (request, session) = try self.getRequestSession(url: url) + let task = session.dataTask(with: request) { data, response, error in + defer { group.leave() } + do { + let response = try self.processResponse(data: data, response: response, error: error) + if response["error"] != nil { + mergedErrors.append(response) + } else { + mergedRecords.append(contentsOf: response["records"] as? [[String: Any]] ?? []) + } + } catch { + mergedErrors.append(["error": error.localizedDescription]) + } + } + task.resume() + } catch let error { + mergedErrors.append(["error": error.localizedDescription]) + group.leave() + } + } + + if hasUpdate { + group.enter() + let url = URL(string: self.apiClient.vaultURL + "v2/records/update")! + do { + let (request, session) = try self.getUpdateRequestSession(url: url, records: updateRecords) + let task = session.dataTask(with: request) { data, response, error in + defer { group.leave() } + do { + let response = try self.processResponse(data: data, response: response, error: error) + if response["error"] != nil { + mergedErrors.append(response) + } else { + mergedRecords.append(contentsOf: response["records"] as? [[String: Any]] ?? []) + } + } catch { + mergedErrors.append(["error": error.localizedDescription]) + } + } + task.resume() + } catch let error { + mergedErrors.append(["error": error.localizedDescription]) + group.leave() + } + } + + group.notify(queue: .main) { + if mergedErrors.isEmpty { + self.callback.onSuccess(["records": mergedRecords]) + } else { + self.callback.onFailure(["records": mergedRecords, "errors": mergedErrors]) + } + } + } + + internal func onFailure(_ error: Any) { + self.callback.onFailure(error) + } + + internal func flattenUpdates(_ updateDict: [String: Any]) -> [[String: Any]] { + var updateRecords: [[String: Any]] = [] + for (skyflowID, value) in updateDict { + guard let entry = value as? [String: Any], + let tableName = entry["table"] as? String, + let fields = entry["fields"] as? [String: Any] else { continue } + updateRecords.append(["skyflowID": skyflowID, "tableName": tableName, "data": fields]) + } + return updateRecords + } + + internal func buildFieldsDict(dict: [String: Any]) -> [String: Any] { + var temp: [String: Any] = [:] + for (key, val) in dict { + if let v = val as? [String: Any] { + temp[key] = buildFieldsDict(dict: v) + } else { + temp[key] = val + } + } + return temp + } + internal func getRequestSession(url: URL) throws -> (URLRequest, URLSession) { + var jsonString = "" + + do { + let deviceDetails = FetchMetrices().getMetrices() + let jsonData = try JSONSerialization.data(withJSONObject: deviceDetails, options: []) + jsonString = String(data: jsonData, encoding: .utf8) ?? "" + } catch { + jsonString = "" + } + var request = URLRequest(url: url) + request.httpMethod = "POST" + + do { + let data = try JSONSerialization.data(withJSONObject: FlowVaultInsertRequestBody.createRequestBody(vaultID: self.apiClient.vaultID, records: self.records, options: options)) + request.httpBody = data + } + + request.setValue(("Bearer " + self.apiClient.token), forHTTPHeaderField: "Authorization") + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.setValue("application/json", forHTTPHeaderField: "Accept") + request.setValue(jsonString, forHTTPHeaderField: "sky-metadata") + + return (request, URLSession(configuration: Self.urlSessionConfiguration)) + + } + + internal func getUpdateRequestSession(url: URL, records: [[String: Any]]) throws -> (URLRequest, URLSession) { + var jsonString = "" + + do { + let deviceDetails = FetchMetrices().getMetrices() + let jsonData = try JSONSerialization.data(withJSONObject: deviceDetails, options: []) + jsonString = String(data: jsonData, encoding: .utf8) ?? "" + } catch { + jsonString = "" + } + var request = URLRequest(url: url) + request.httpMethod = "POST" + + do { + let data = try JSONSerialization.data(withJSONObject: FlowVaultUpdateRequestBody.createRequestBody(vaultID: self.apiClient.vaultID, records: records)) + request.httpBody = data + } + + request.setValue(("Bearer " + self.apiClient.token), forHTTPHeaderField: "Authorization") + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.setValue("application/json", forHTTPHeaderField: "Accept") + request.setValue(jsonString, forHTTPHeaderField: "sky-metadata") + + return (request, URLSession(configuration: Self.urlSessionConfiguration)) + } + + func processResponse(data: Data?, response: URLResponse?, error: Error?) throws -> [String: Any] { + if error != nil || response == nil { + return ["error": ["message": (error)?.localizedDescription ?? "Unknown error"]] + } + + if let httpResponse = response as? HTTPURLResponse { + let range = 400...599 + if range ~= httpResponse.statusCode { + // FlowDB returns a non-2xx status when every record in the batch fails, + // but the body still has the same {"records": [...]} shape as a success/partial + // response (each record carrying its own error/httpCode) - parse it as such. + if let safeData = data, + let jsonObject = try? JSONSerialization.jsonObject(with: safeData, options: .allowFragments) as? [String: Any], + jsonObject["records"] != nil { + return try getCollectResponseBody(data: safeData) + } + var description = "Insert call failed with the following status code " + String(httpResponse.statusCode) + if let safeData = data { + guard let errorResponse = try? JSONSerialization.jsonObject(with: safeData, options: .allowFragments) as? [String: Any] else { + return ["error": ["message": String(data: safeData, encoding: .utf8) ?? "Unknown error", "httpCode": httpResponse.statusCode]] + } + // Pass the vault's structured error object (grpcCode/httpStatus/details) through + // as-is, only patching in the request-id and defaulting httpCode when absent. + if var errorDict = errorResponse["error"] as? [String: Any] { + if let message = errorDict["message"] as? String { + description = message + if let requestId = httpResponse.allHeaderFields["x-request-id"] { + description += " - request-id: \(requestId)" + } + errorDict["message"] = description + } + if errorDict["httpCode"] == nil { errorDict["httpCode"] = httpResponse.statusCode } + return ["error": errorDict] + } + if let requestId = httpResponse.allHeaderFields["x-request-id"] { + description += " - request-id: \(requestId)" + } + } + return ["error": ["message": description, "httpCode": httpResponse.statusCode]] + } + } + + guard let safeData = data else { + return ["records": []] + } + + return try getCollectResponseBody(data: safeData) + + } + + func getCollectResponseBody(data: Data) throws -> [String: Any]{ + let jsonData = (try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String: Any]) ?? [:] + var records: [[String: Any]] = [] + + let responseRecords = jsonData["records"] as? [[String: Any]] ?? [] + for entry in responseRecords { + if let error = entry["error"] as? String { + var errorEntry: [String: Any] = ["error": error] + if let skyflowID = entry["skyflowID"] { errorEntry["skyflowID"] = skyflowID } + if let tableName = entry["tableName"] { errorEntry["tableName"] = tableName } + if let httpCode = entry["httpCode"] { errorEntry["httpCode"] = httpCode } + records.append(errorEntry) + } else { + var successEntry: [String: Any] = [:] + if let skyflowID = entry["skyflowID"] { successEntry["skyflowID"] = skyflowID } + if let tableName = entry["tableName"] { successEntry["tableName"] = tableName } + var fields: [String: Any] = [:] + if let tokens = entry["tokens"] as? [String: Any] { + for (column, tokenValue) in self.buildFieldsDict(dict: tokens) { + fields[column] = tokenValue + } + } + successEntry["fields"] = fields + if let hashedData = entry["hashedData"] as? [String: Any] { successEntry["hashedData"] = self.buildFieldsDict(dict: hashedData) } + if let httpCode = entry["httpCode"] { successEntry["httpCode"] = httpCode } + records.append(successEntry) + } + } + + return ["records": records] + } +} diff --git a/Sources/Skyflow/collect/FlowVaultInsertRequestBody.swift b/Sources/Skyflow/collect/FlowVaultInsertRequestBody.swift new file mode 100644 index 00000000..e2ef86ff --- /dev/null +++ b/Sources/Skyflow/collect/FlowVaultInsertRequestBody.swift @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// Used for generating request body for FlowDB v2 insert api call + +import Foundation + +internal class FlowVaultInsertRequestBody { + internal static func createRequestBody(vaultID: String, records: [String: Any], options: FlowVaultICOptions) -> [String: Any] { + var recordsPayload: [[String: Any]] = [] + for record in (records["records"] as! [[String: Any]]) { + var temp: [String: Any] = [:] + temp["data"] = record["fields"] + if let tableName = record["table"] as? String { + temp["tableName"] = tableName + if let upsertOptions = options.upsert, let match = upsertOptions.first(where: { $0.table == tableName }) { + var upsertPayload: [String: Any] = ["uniqueColumns": match.uniqueColumns] + if let updateType = match.updateType { + upsertPayload["updateType"] = updateType.rawValue + } + temp["upsert"] = upsertPayload + } + } + recordsPayload.append(temp) + } + return ["vaultID": vaultID, "records": recordsPayload] + } +} diff --git a/Sources/Skyflow/collect/FlowVaultUpdateRequestBody.swift b/Sources/Skyflow/collect/FlowVaultUpdateRequestBody.swift new file mode 100644 index 00000000..54187810 --- /dev/null +++ b/Sources/Skyflow/collect/FlowVaultUpdateRequestBody.swift @@ -0,0 +1,13 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// Used for generating request body for FlowDB v2 update-by-skyflowID api call + +import Foundation + +internal class FlowVaultUpdateRequestBody { + internal static func createRequestBody(vaultID: String, records: [[String: Any]]) -> [String: Any] { + return ["vaultID": vaultID, "records": records] + } +} diff --git a/Sources/Skyflow/collect/InsertOptions.swift b/Sources/Skyflow/collect/InsertOptions.swift index e57737df..9f4f4301 100644 --- a/Sources/Skyflow/collect/InsertOptions.swift +++ b/Sources/Skyflow/collect/InsertOptions.swift @@ -7,10 +7,8 @@ import Foundation public struct InsertOptions { - var tokens: Bool - var upsert: [[String: Any]]? - public init(tokens: Bool = true, upsert: [[String: Any]]? = nil) { - self.tokens = tokens + var upsert: [UpsertOption]? + public init(upsert: [UpsertOption]? = nil) { self.upsert = upsert } } diff --git a/Sources/Skyflow/collect/UpsertOption.swift b/Sources/Skyflow/collect/UpsertOption.swift new file mode 100644 index 00000000..18011787 --- /dev/null +++ b/Sources/Skyflow/collect/UpsertOption.swift @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// Object that describes an upsert configuration for insert/collect calls + +import Foundation + +public enum UpdateType: String { + case UPDATE + case REPLACE +} + +public struct UpsertOption { + public let table: String + public let uniqueColumns: [String] + public let updateType: UpdateType? + + public init(table: String, uniqueColumns: [String], updateType: UpdateType? = nil) { + self.table = table + self.uniqueColumns = uniqueColumns + self.updateType = updateType + } +} diff --git a/Sources/Skyflow/composable/ComposableContainer.swift b/Sources/Skyflow/composable/ComposableContainer.swift index 5da7784c..68f1510b 100644 --- a/Sources/Skyflow/composable/ComposableContainer.swift +++ b/Sources/Skyflow/composable/ComposableContainer.swift @@ -211,7 +211,7 @@ public extension Container { let errorCode = ErrorCodes.EMPTY_VAULT_ID() return callback.onFailure(errorCode.getErrorObject(contextOptions: tempContextOptions)) } - if self.skyflow.vaultURL == "/v1/vaults/" { + if self.skyflow.vaultURL == "/v2/vaults/" { let errorCode = ErrorCodes.EMPTY_VAULT_URL() return callback.onFailure(errorCode.getErrorObject(contextOptions: tempContextOptions)) } @@ -246,30 +246,20 @@ public extension Container { return } - if options?.additionalFields != nil { - if options?.additionalFields!["records"] == nil { - errorCode = .MISSING_RECORDS_IN_ADDITIONAL_FIELDS() + if let additionalFields = options?.additionalFields { + if additionalFields.records.isEmpty { + errorCode = .EMPTY_RECORDS_OBJECT() return callback.onFailure(errorCode!.getErrorObject(contextOptions: tempContextOptions)) } - if let additionalFieldEntries = options?.additionalFields!["records"] as? [[String: Any]] { - if additionalFieldEntries.isEmpty { - errorCode = .EMPTY_RECORDS_OBJECT() + for (index, record) in additionalFields.records.enumerated() { + errorCode = checkRecord(record: record, index: index) + if errorCode != nil { return callback.onFailure(errorCode!.getErrorObject(contextOptions: tempContextOptions)) } - for (index, record) in additionalFieldEntries.enumerated() { - errorCode = checkRecord(record: record, index: index) - if errorCode != nil { - return callback.onFailure(errorCode!.getErrorObject(contextOptions: tempContextOptions)) - } - } - } else { - errorCode = .INVALID_RECORDS_TYPE() - callback.onFailure(errorCode!.getErrorObject(contextOptions: tempContextOptions)) - return } } - let records = CollectRequestBody.createRequestBody(elements: self.elements, additionalFields: options?.additionalFields, callback: callback, contextOptions: tempContextOptions) - let icOptions = ICOptions(tokens: options!.tokens, additionalFields: options?.additionalFields, upsert: options?.upsert, callback: callback, contextOptions: tempContextOptions) + let records = FlowVaultCollectRequestBody.createRequestBody(elements: self.elements, additionalFields: options?.additionalFields, callback: callback, contextOptions: tempContextOptions) + let icOptions = FlowVaultICOptions(additionalFields: options?.additionalFields, upsert: options?.upsert, callback: callback, contextOptions: tempContextOptions) if options?.upsert != nil { if icOptions.validateUpsert() { return; @@ -301,29 +291,14 @@ public extension Container { return nil } - private func checkRecord(record: [String: Any], index: Int) -> ErrorCodes? { - if record["table"] == nil { - return .TABLE_KEY_ERROR(value: "\(index)") - } - if !(record["table"] is String) { - return .INVALID_TABLE_NAME_TYPE(value: "\(index)") - } - if (record["table"] as? String == "") { - return .EMPTY_TABLE_NAME() - } - if record["fields"] == nil { - return .FIELDS_KEY_ERROR(value: "\(index)") - } - if !(record["fields"] is [String: Any]) { - return .INVALID_FIELDS_TYPE(value: "\(index)") - } - let fields = record["fields"] as! [String: Any] - if (fields.isEmpty){ - return .EMPTY_FIELDS_KEY(value: "\(index)") - } - - return nil + private func checkRecord(record: AdditionalFieldsRecord, index: Int) -> ErrorCodes? { + if record.table.isEmpty { + return .EMPTY_TABLE_NAME() } - + if record.fields.isEmpty { + return .EMPTY_FIELDS_KEY(value: "\(index)") + } + return nil + } } diff --git a/Sources/Skyflow/core/APIClient.swift b/Sources/Skyflow/core/APIClient.swift index 3cfd6ef9..f5c19512 100644 --- a/Sources/Skyflow/core/APIClient.swift +++ b/Sources/Skyflow/core/APIClient.swift @@ -59,12 +59,12 @@ internal class APIClient { } } - internal func postAndUpdate(records: [String: Any], callback: Callback, options: ICOptions, contextOptions: ContextOptions) { - let collectApiCallback = CollectAPICallback(callback: callback, apiClient: self, records: records, options: options, contextOptions: contextOptions) + internal func postAndUpdate(records: [String: Any], callback: Callback, options: FlowVaultICOptions, contextOptions: ContextOptions) { + let collectApiCallback = FlowVaultCollectAPICallback(callback: callback, apiClient: self, records: records, options: options, contextOptions: contextOptions) self.getAccessToken(callback: collectApiCallback, contextOptions: contextOptions) } - internal func post(records: [String: Any], callback: Callback, options: ICOptions, contextOptions: ContextOptions) { - let insertApiCallback = InsertAPICallback(callback: callback, apiClient: self, records: records, options: options, contextOptions: contextOptions) + internal func post(records: [String: Any], callback: Callback, options: FlowVaultICOptions, contextOptions: ContextOptions) { + let insertApiCallback = FlowVaultInsertAPICallback(callback: callback, apiClient: self, records: records, options: options, contextOptions: contextOptions) self.getAccessToken(callback: insertApiCallback, contextOptions: contextOptions) } internal func constructUpdateRequestBody(records: [String: Any], options: ICOptions) -> [String: Any] { @@ -85,7 +85,7 @@ internal class APIClient { temp["tableName"] = record["table"] temp["method"] = "POST" temp["quorum"] = true - + if options.tokens { var temp2: [String: Any] = [:] temp2["method"] = "GET" @@ -114,18 +114,18 @@ internal class APIClient { } return uniqueColumn; } - - internal func get(records: [RevealRequestRecord], callback: Callback, contextOptions: ContextOptions) { - let revealApiCallback = RevealAPICallback(callback: callback, apiClient: self, connectionUrl: (vaultURL + vaultID), records: records, contextOptions: contextOptions) + + internal func get(records: [RevealRequestRecord], tokenGroupRedactions: [TokenGroupRedaction]? = nil, callback: Callback, contextOptions: ContextOptions) { + let revealApiCallback = FlowVaultRevealAPICallback(callback: callback, apiClient: self, connectionUrl: (vaultURL + "v2/tokens/detokenize"), records: records, tokenGroupRedactions: tokenGroupRedactions, contextOptions: contextOptions) self.getAccessToken(callback: revealApiCallback, contextOptions: contextOptions) } internal func getById(records: [GetByIdRecord], callback: Callback, contextOptions: ContextOptions) { - let revealByIdApiCallback = RevealByIDAPICallback(callback: callback, apiClient: self, connectionUrl: (vaultURL + vaultID), records: records, contextOptions: contextOptions) + let revealByIdApiCallback = RevealByIDAPICallback(callback: callback, apiClient: self, connectionUrl: (vaultURL + "v1/vaults/" + vaultID), records: records, contextOptions: contextOptions) self.getAccessToken(callback: revealByIdApiCallback, contextOptions: contextOptions) } - internal func getRecord(records: [GetRecord], callback: Callback,getOptions: GetOptions, contextOptions: ContextOptions) { - let getApiCallback = GetAPICallback(callback: callback, apiClient: self, connectionUrl: (vaultURL + vaultID), records: records, getOptions: getOptions,contextOptions: contextOptions) + internal func getRecord(records: [GetRecord], callback: Callback, getOptions: GetOptions, contextOptions: ContextOptions) { + let getApiCallback = GetAPICallback(callback: callback, apiClient: self, connectionUrl: (vaultURL + "v1/vaults/" + vaultID), records: records, getOptions: getOptions, contextOptions: contextOptions) self.getAccessToken(callback: getApiCallback, contextOptions: contextOptions) } } diff --git a/Sources/Skyflow/core/Client.swift b/Sources/Skyflow/core/Client.swift index c9e51761..d956f3cc 100644 --- a/Sources/Skyflow/core/Client.swift +++ b/Sources/Skyflow/core/Client.swift @@ -15,13 +15,14 @@ public class Client { public init(_ skyflowConfig: Configuration) { self.vaultID = skyflowConfig.vaultID - self.vaultURL = skyflowConfig.vaultURL.hasSuffix("/") ? skyflowConfig.vaultURL + "v1/vaults/" : skyflowConfig.vaultURL + "/v1/vaults/" - self.apiClient = APIClient(vaultID: skyflowConfig.vaultID, vaultURL: self.vaultURL, tokenProvider: skyflowConfig.tokenProvider) + let normalizedBaseURL = skyflowConfig.vaultURL.hasSuffix("/") ? skyflowConfig.vaultURL : skyflowConfig.vaultURL + "/" + self.vaultURL = normalizedBaseURL + "v2/vaults/" + self.apiClient = APIClient(vaultID: skyflowConfig.vaultID, vaultURL: normalizedBaseURL, tokenProvider: skyflowConfig.tokenProvider) self.contextOptions = ContextOptions(logLevel: skyflowConfig.options!.logLevel, env: skyflowConfig.options!.env, interface: .CLIENT) Log.info(message: .CLIENT_INITIALIZED, contextOptions: self.contextOptions) } - public func insert(records: [String: Any], options: InsertOptions = InsertOptions(), callback: Callback) { + internal func insert(records: [String: Any], options: InsertOptions = InsertOptions(), callback: Callback) { var tempContextOptions = self.contextOptions tempContextOptions.interface = .INSERT Log.info(message: .INSERT_TRIGGERED, contextOptions: tempContextOptions) @@ -29,11 +30,11 @@ public class Client { let errorCode = ErrorCodes.EMPTY_VAULT_ID() return callback.onFailure(errorCode.getErrorObject(contextOptions: tempContextOptions)) } - if self.vaultURL == "/v1/vaults/" { + if self.vaultURL == "/v2/vaults/" { let errorCode = ErrorCodes.EMPTY_VAULT_URL() return callback.onFailure(errorCode.getErrorObject(contextOptions: tempContextOptions)) } - let icOptions = ICOptions(tokens: options.tokens, upsert: options.upsert, callback: callback, contextOptions: tempContextOptions) + let icOptions = FlowVaultICOptions(upsert: options.upsert, callback: callback, contextOptions: tempContextOptions) var errorCode: ErrorCodes? if records["records"] == nil { @@ -114,15 +115,10 @@ public class Client { return nil } - public func detokenize(records: [String: Any], options: RevealOptions? = RevealOptions(), callback: Callback) { + internal func detokenize(records: [String: Any], options: RevealOptions? = RevealOptions(), callback: Callback) { var tempContextOptions = self.contextOptions tempContextOptions.interface = .DETOKENIZE func checkRecord(token: [String: Any], index: Int) -> ErrorCodes? { - if token["redaction"] != nil { - guard let _ = token["redaction"] as? RedactionType else { - return .INVALID_REDACTION_TYPE() - } - } if token["token"] == nil { return .ID_KEY_ERROR() } else { @@ -138,7 +134,7 @@ public class Client { let errorCode = ErrorCodes.EMPTY_VAULT_ID() return callRevealOnFailure(callback: callback, errorObject: errorCode.getErrorObject(contextOptions: tempContextOptions)) } - if self.vaultURL == "/v1/vaults/" { + if self.vaultURL == "/v2/vaults/" { let errorCode = ErrorCodes.EMPTY_VAULT_URL() return callRevealOnFailure(callback: callback, errorObject: errorCode.getErrorObject(contextOptions: tempContextOptions)) } @@ -156,11 +152,7 @@ public class Client { for (index,token) in tokens.enumerated() { let errorCode = checkRecord(token: token, index: index) if errorCode == nil, let id = token["token"] as? String { - if token["redaction"] == nil{ - list.append(RevealRequestRecord(token: id, redaction: RedactionType.PLAIN_TEXT.rawValue)) - } else if let redaction = token["redaction"] as? RedactionType{ - list.append(RevealRequestRecord(token: id, redaction: redaction.rawValue)) - } + list.append(RevealRequestRecord(token: id)) } else { return callRevealOnFailure(callback: callback, errorObject: errorCode!.getErrorObject(contextOptions: tempContextOptions)) } @@ -172,13 +164,13 @@ public class Client { onFailureHandler: { } ) - self.apiClient.get(records: list, callback: logCallback, contextOptions: tempContextOptions) + self.apiClient.get(records: list, tokenGroupRedactions: options?.tokenGroupRedactions, callback: logCallback, contextOptions: tempContextOptions) } else { callRevealOnFailure(callback: callback, errorObject: ErrorCodes.INVALID_RECORDS_TYPE().getErrorObject(contextOptions: tempContextOptions)) } } - public func getById(records: [String: Any], callback: Callback) { + internal func getById(records: [String: Any], callback: Callback) { var tempContextOptions = self.contextOptions tempContextOptions.interface = .GETBYID Log.info(message: .GET_BY_ID_TRIGGERED, contextOptions: tempContextOptions) @@ -186,7 +178,7 @@ public class Client { let errorCode = ErrorCodes.EMPTY_VAULT_ID() return callRevealOnFailure(callback: callback, errorObject: errorCode.getErrorObject(contextOptions: tempContextOptions)) } - if self.vaultURL == "/v1/vaults/" { + if self.vaultURL == "/v2/vaults/" { let errorCode = ErrorCodes.EMPTY_VAULT_URL() return callRevealOnFailure(callback: callback, errorObject: errorCode.getErrorObject(contextOptions: tempContextOptions)) } @@ -261,7 +253,7 @@ public class Client { callRevealOnFailure(callback: callback, errorObject: ErrorCodes.INVALID_RECORDS_TYPE().getErrorObject(contextOptions: tempContextOptions)) } } - public func get(records: [String: Any], options: GetOptions = GetOptions(), callback: Callback){ + internal func get(records: [String: Any], options: GetOptions = GetOptions(), callback: Callback){ var tempContextOptions = self.contextOptions tempContextOptions.interface = .GET Log.info(message: .GET_TRIGGERED, contextOptions: tempContextOptions) @@ -269,12 +261,12 @@ public class Client { let errorCode = ErrorCodes.EMPTY_VAULT_ID() return callRevealOnFailure(callback: callback, errorObject: errorCode.getErrorObject(contextOptions: tempContextOptions)) } - if self.vaultURL == "/v1/vaults/" { + if self.vaultURL == "/v2/vaults/" { let errorCode = ErrorCodes.EMPTY_VAULT_URL() return callRevealOnFailure(callback: callback, errorObject: errorCode.getErrorObject(contextOptions: tempContextOptions)) } Log.info(message: .VALIDATE_GET_INPUT, contextOptions: tempContextOptions) - + if records["records"] == nil { return callRevealOnFailure(callback: callback, errorObject: ErrorCodes.EMPTY_RECORDS_OBJECT().getErrorObject(contextOptions: tempContextOptions)) } @@ -305,7 +297,7 @@ public class Client { let logCallback = LogCallback(clientCallback: callback, contextOptions: tempContextOptions, onSuccessHandler: { Log.info(message: .GET_SUCCESS, contextOptions: tempContextOptions) }, onFailureHandler: { - + }) self.apiClient.getRecord(records: list, callback: logCallback, getOptions: options, contextOptions: tempContextOptions) } else { @@ -343,7 +335,7 @@ public class Client { if( getOptions.tokens == true ){ if (entry["columnName"] != nil || entry["columnValues"] != nil){ return .TOKENS_GET_COLUMN_NOT_SUPPPORTED() - + } if (entry["redaction"] as? RedactionType) != nil { return .REDACTION_WITH_TOKEN_NOT_SUPPORTED() @@ -355,7 +347,7 @@ public class Client { return .INVALID_REDACTION_TYPE() } } - + if(entry["columnName"] == nil){ if ((entry["ids"] == nil) && (entry["columnValues"] == nil)){ return .MISSING_IDS_OR_COLUMN_VALUES_IN_GET() @@ -389,7 +381,7 @@ public class Client { return .EMPTY_COLUMN_NAME() } } - + return nil } @@ -397,17 +389,6 @@ public class Client { let result = ["errors": [errorObject]] callback.onFailure(result) } - - internal func createDetokenizeRecords(_ IDsToTokens: [String: String]) -> [String: [[String: String]]]{ - var records = [] as [[String : String]] - var index = 0 - for (_, token) in IDsToTokens { - records.append(["token": token]) - index += 1 - } - - return ["records": records] - } } internal class LogCallback: Callback { diff --git a/Sources/Skyflow/core/InterfaceName.swift b/Sources/Skyflow/core/InterfaceName.swift index 54534af7..4afd2dee 100644 --- a/Sources/Skyflow/core/InterfaceName.swift +++ b/Sources/Skyflow/core/InterfaceName.swift @@ -4,7 +4,7 @@ // // File.swift -// +// // // Created by Akhil Anil Mangala on 29/11/21. // @@ -21,7 +21,7 @@ internal enum InterfaceName { case GETBYID case GET case EMPTY - + var description: String { switch self { case .COLLECT_CONTAINER: return "collect container" diff --git a/Sources/Skyflow/core/Message.swift b/Sources/Skyflow/core/Message.swift index e7c7b918..33796934 100644 --- a/Sources/Skyflow/core/Message.swift +++ b/Sources/Skyflow/core/Message.swift @@ -76,7 +76,7 @@ internal enum Message { case .INSERT_TRIGGERED: return "Insert method triggered." case .DETOKENIZE_TRIGGERED: return "Detokenize method triggered." case .GET_BY_ID_TRIGGERED: return "Get by ID triggered." - + //Used in tests case .CLIENT_CONNECTION: return "client connection not established" // A case .CANNOT_CHANGE_ELEMENT: return "Element can't be changed" // A diff --git a/Sources/Skyflow/elements/SkyflowElement.swift b/Sources/Skyflow/elements/SkyflowElement.swift index 0ac78fef..0ac7dd32 100644 --- a/Sources/Skyflow/elements/SkyflowElement.swift +++ b/Sources/Skyflow/elements/SkyflowElement.swift @@ -16,7 +16,7 @@ public class SkyflowElement: UIView { internal var fieldType: ElementType! internal var columnName: String! internal var tableName: String? - internal var skyflowID: String? + internal var skyflowId: String? internal var horizontalConstraints = [NSLayoutConstraint]() internal var verticalConstraint = [NSLayoutConstraint]() internal var collectInput: CollectElementInput! @@ -64,7 +64,7 @@ public class SkyflowElement: UIView { columnName = collectInput.column fieldType = collectInput.type isRequired = options.required - skyflowID = collectInput.skyflowID + skyflowId = collectInput.skyflowId } internal func getOutput() -> String? { diff --git a/Sources/Skyflow/elements/core/CollectElementInput.swift b/Sources/Skyflow/elements/core/CollectElementInput.swift index 419d6b8d..40527522 100644 --- a/Sources/Skyflow/elements/core/CollectElementInput.swift +++ b/Sources/Skyflow/elements/core/CollectElementInput.swift @@ -17,11 +17,11 @@ public struct CollectElementInput { var placeholder: String var type: ElementType? var validations: ValidationSet - var skyflowID: String + var skyflowId: String public init(table: String = "", column: String = "", inputStyles: Styles? = Styles(), labelStyles: Styles? = Styles(), errorTextStyles: Styles? = Styles(), iconStyles: Styles? = Styles(), label: String? = "", - placeholder: String? = "", validations: ValidationSet=ValidationSet(), skyflowID: String? = "") { + placeholder: String? = "", validations: ValidationSet=ValidationSet(), skyflowId: String? = "") { self.table = table self.column = column self.inputStyles = inputStyles! @@ -31,12 +31,12 @@ public struct CollectElementInput { self.label = label! self.placeholder = placeholder! self.validations = validations - self.skyflowID = skyflowID! + self.skyflowId = skyflowId! } public init(table: String = "", column: String = "", inputStyles: Styles? = Styles(), labelStyles: Styles? = Styles(), errorTextStyles: Styles? = Styles(), iconStyles: Styles? = Styles(), label: String? = "", - placeholder: String? = "", type: ElementType?, validations: ValidationSet=ValidationSet(), skyflowID: String? = "") { + placeholder: String? = "", type: ElementType?, validations: ValidationSet=ValidationSet(), skyflowId: String? = "") { self.table = table self.column = column self.inputStyles = inputStyles! @@ -47,13 +47,13 @@ public struct CollectElementInput { self.placeholder = placeholder! self.type = type self.validations = validations - self.skyflowID = skyflowID! + self.skyflowId = skyflowId! } @available(*, deprecated, message: "altText param is deprecated") public init(table: String = "", column: String = "", inputStyles: Styles? = Styles(), labelStyles: Styles? = Styles(), errorTextStyles: Styles? = Styles(), iconStyles: Styles? = Styles(), label: String? = "", - placeholder: String? = "", altText: String? = "", type: ElementType?, validations: ValidationSet=ValidationSet(), skyflowID: String? = "") { + placeholder: String? = "", altText: String? = "", type: ElementType?, validations: ValidationSet=ValidationSet(), skyflowId: String? = "") { self.table = table self.column = column self.inputStyles = inputStyles! @@ -64,6 +64,6 @@ public struct CollectElementInput { self.placeholder = placeholder! self.type = type self.validations = validations - self.skyflowID = skyflowID! + self.skyflowId = skyflowId! } } diff --git a/Sources/Skyflow/errors/ErrorCodes.swift b/Sources/Skyflow/errors/ErrorCodes.swift index d117f4bc..cd489d73 100644 --- a/Sources/Skyflow/errors/ErrorCodes.swift +++ b/Sources/Skyflow/errors/ErrorCodes.swift @@ -30,7 +30,6 @@ internal enum ErrorCodes: CustomStringConvertible { case INVALID_FIELDS_TYPE(code: Int = 400, message: String = "\(LangAndVersion) Validation error.invaid 'fields' key value in record at index . Specify a value of type array for 'fields' key.", value: String) case INVALID_RECORDS_TYPE(code: Int = 400, message: String = "\(LangAndVersion) Validation error. Invalid 'records' key found. Specify a value of type array instead.") case INVALID_BEARER_TOKEN_FORMAT(code: Int = 400, message: String = "\(LangAndVersion) Token generated from 'getBearerToken' callback function is invalid. Make sure the implementation of 'getBearerToken' is correct.") - case MISSING_RECORDS_ARRAY(code: Int = 404, message: String = "\(LangAndVersion) Validation error.'records' key not found in additionalFields. Specify a 'records' key in addtionalFields.") case MISSING_RECORDS_IN_ADDITIONAL_FIELDS(code: Int = 404, message: String = "\(LangAndVersion) Validation error.'records' object cannot be empty within additionalFields. Specify a non-empty value instead.") case EMPTY_RECORDS_OBJECT(code: Int = 404, message: String = "\(LangAndVersion) Validation error. 'records' key cannot be empty. Provide a non-empty value instead.") case MISSING_RECORDS_IN_GETBYID(code: Int = 404, message: String = "\(LangAndVersion) Validation error. 'records' key cannot be empty. Provide a non-empty value instead.") @@ -69,18 +68,19 @@ internal enum ErrorCodes: CustomStringConvertible { case UPSERT_OPTION_CANNOT_BE_EMPTY(code: Int = 400, message: String = "\(LangAndVersion) Validation error. 'upsert' key cannot be an empty array in insert options. Make sure to add atleast one table column object in upsert array.") case MISSING_COLUMN_NAME_IN_USERT_OPTION(code: Int = 400, message: String = "\(LangAndVersion) Validation error. Missing 'column' key in upsert array at index . Provide a valid 'column' key.", value: String) case COLUMN_NAME_IS_EMPTY_FOR_ATLEAST_ONE_UPSERT_OPTION(code: Int = 400, message: String = "\(LangAndVersion) Validation error. Invalid 'table' key in upsert array at index . Specify a value of type string instead.", value: String) + case UNIQUE_COLUMNS_EMPTY_FOR_ATLEAST_ONE_UPSERT_OPTION(code: Int = 400, message: String = "\(LangAndVersion) Validation error. 'uniqueColumns' cannot be empty in upsert option at index . Specify at least one column.", value: String) case TABLE_NAME_IS_EMPTY_FOR_ATLEAST_ONE_UPSERT_OPTION(code: Int = 400, message: String = "\(LangAndVersion) Validation error. Invalid 'table' key in upsert array at index . Specify a value of type string instead.", value: String) case MISMATCH_ELEMENT_COUNT_LAYOUT_SUM(code: Int = 400, message: String = "\(LangAndVersion) Mount failed. Invalid layout array values. Make sure all values in the layout array are positive numbers.") var code: Int { switch self { // No Formatting required // swiftlint:disable:next line_length - case .EMPTY_TABLE_NAME(let code, _), .EMPTY_VAULT_ID(let code, _), .EMPTY_VAULT_URL(let code, _),.RECORDS_KEY_ERROR( let code, _),.EMPTY_TOKEN_ID(let code, _), .ID_KEY_ERROR(let code, _), .INVALID_RECORDS_TYPE(let code, _), .EMPTY_COLUMN_NAME(let code, _), .INVALID_BEARER_TOKEN_FORMAT(let code, _), .MISSING_RECORDS_ARRAY(let code, _), .MISSING_RECORDS_IN_ADDITIONAL_FIELDS(let code, _), .EMPTY_RECORDS_OBJECT(let code, _), .MISSING_RECORDS_IN_GETBYID(let code, _),.INVALID_URL(let code, _), .INVALID_IDS_TYPE(let code, _),.REDACTION_WITH_TOKEN_NOT_SUPPORTED(let code, _), .TOKENS_GET_COLUMN_NOT_SUPPPORTED(let code, _), .MISSING_COLUMN_NAME(let code,_), .UPSERT_OPTION_CANNOT_BE_EMPTY(let code, _),.INVALID_REDACTION_TYPE(let code, _),.EMPTY_COLUMN_NAME_IN_COLLECT(let code, _), .EMPTY_TABLE_NAME_IN_COLLECT(let code, _), .REGEX_MATCH_FAILED(let code, _),.SKYFLOW_IDS_AND_COLUMN_NAME_BOTH_SPECIFIED(let code, _),.MISSING_IDS_OR_COLUMN_VALUES_IN_GET(let code, _), .MISSING_RECORD_COLUMN_VALUE(let code, _), .INVALID_COLUMN_VALUES_IN_GET(let code, _), .EMPTY_COMPOSABLE_LAYOUT_ARRAY(let code, _), .MISSING_COMPOSABLE_LAYOUT_KEY(let code, _), .MISMATCH_ELEMENT_COUNT_LAYOUT_SUM(let code, _),.MISSING_COMPOSABLE_CONTAINER_OPTIONS(code: let code, message: _): + case .EMPTY_TABLE_NAME(let code, _), .EMPTY_VAULT_ID(let code, _), .EMPTY_VAULT_URL(let code, _),.RECORDS_KEY_ERROR( let code, _),.EMPTY_TOKEN_ID(let code, _), .ID_KEY_ERROR(let code, _), .INVALID_RECORDS_TYPE(let code, _), .EMPTY_COLUMN_NAME(let code, _), .INVALID_BEARER_TOKEN_FORMAT(let code, _), .MISSING_RECORDS_IN_ADDITIONAL_FIELDS(let code, _), .EMPTY_RECORDS_OBJECT(let code, _), .MISSING_RECORDS_IN_GETBYID(let code, _),.INVALID_URL(let code, _), .INVALID_IDS_TYPE(let code, _),.REDACTION_WITH_TOKEN_NOT_SUPPORTED(let code, _), .TOKENS_GET_COLUMN_NOT_SUPPPORTED(let code, _), .MISSING_COLUMN_NAME(let code,_), .UPSERT_OPTION_CANNOT_BE_EMPTY(let code, _),.INVALID_REDACTION_TYPE(let code, _),.EMPTY_COLUMN_NAME_IN_COLLECT(let code, _), .EMPTY_TABLE_NAME_IN_COLLECT(let code, _), .REGEX_MATCH_FAILED(let code, _),.SKYFLOW_IDS_AND_COLUMN_NAME_BOTH_SPECIFIED(let code, _),.MISSING_IDS_OR_COLUMN_VALUES_IN_GET(let code, _), .MISSING_RECORD_COLUMN_VALUE(let code, _), .INVALID_COLUMN_VALUES_IN_GET(let code, _), .EMPTY_COMPOSABLE_LAYOUT_ARRAY(let code, _), .MISSING_COMPOSABLE_LAYOUT_KEY(let code, _), .MISMATCH_ELEMENT_COUNT_LAYOUT_SUM(let code, _),.MISSING_COMPOSABLE_CONTAINER_OPTIONS(code: let code, message: _): return code // Single value formatting // swiftlint:disable:next line_length // Multi value formatting - case .EMPTY_IDS(let code, _, _), .EMPTY_ID_VALUE(let code, _, _), .TABLE_KEY_ERROR(let code, _, _), .FIELDS_KEY_ERROR(let code, _, _), .EMPTY_FIELDS_KEY(let code, _, _), .REDACTION_KEY_ERROR(let code, _, _), .MISSING_KEY_IDS(let code, _, _), .INVALID_TABLE_NAME_TYPE(let code, _, _), .INVALID_FIELDS_TYPE(let code, _, _), .INVALID_TOKEN_TYPE(let code, _, _), .MISSING_TABLE_NAME_IN_USERT_OPTION(let code, _, _),.MISSING_COLUMN_NAME_IN_USERT_OPTION(let code, _, _), .COLUMN_NAME_IS_EMPTY_FOR_ATLEAST_ONE_UPSERT_OPTION(let code, _, _), .TABLE_NAME_IS_EMPTY_FOR_ATLEAST_ONE_UPSERT_OPTION(let code, _, _), .APIError(let code, _), .INVALID_COLUMN_NAME(let code, _, _), .UNMOUNTED_COLLECT_ELEMENT(let code, _, _), .UNMOUNTED_REVEAL_ELEMENT(let code, _, _), .ERROR_TRIGGERED(let code, _, _), .DUPLICATE_ADDITIONAL_FIELD_FOUND(let code, _, _), .EMPTY_RECORD_COLUMN_VALUES(code: let code, message: _, value: _), .EMPTY_COLUMN_VALUE(code: let code, message: _, value: _),.DUPLICATE_ELEMENT_FOUND(let code, _, _) : + case .EMPTY_IDS(let code, _, _), .EMPTY_ID_VALUE(let code, _, _), .TABLE_KEY_ERROR(let code, _, _), .FIELDS_KEY_ERROR(let code, _, _), .EMPTY_FIELDS_KEY(let code, _, _), .REDACTION_KEY_ERROR(let code, _, _), .MISSING_KEY_IDS(let code, _, _), .INVALID_TABLE_NAME_TYPE(let code, _, _), .INVALID_FIELDS_TYPE(let code, _, _), .INVALID_TOKEN_TYPE(let code, _, _), .MISSING_TABLE_NAME_IN_USERT_OPTION(let code, _, _), .MISSING_COLUMN_NAME_IN_USERT_OPTION(let code, _, _), .COLUMN_NAME_IS_EMPTY_FOR_ATLEAST_ONE_UPSERT_OPTION(let code, _, _), .UNIQUE_COLUMNS_EMPTY_FOR_ATLEAST_ONE_UPSERT_OPTION(let code, _, _), .TABLE_NAME_IS_EMPTY_FOR_ATLEAST_ONE_UPSERT_OPTION(let code, _, _), .APIError(let code, _), .INVALID_COLUMN_NAME(let code, _, _), .UNMOUNTED_COLLECT_ELEMENT(let code, _, _), .UNMOUNTED_REVEAL_ELEMENT(let code, _, _), .ERROR_TRIGGERED(let code, _, _), .DUPLICATE_ADDITIONAL_FIELD_FOUND(let code, _, _), .EMPTY_RECORD_COLUMN_VALUES(code: let code, message: _, value: _), .EMPTY_COLUMN_VALUE(code: let code, message: _, value: _),.DUPLICATE_ELEMENT_FOUND(let code, _, _) : return code } @@ -90,11 +90,11 @@ internal enum ErrorCodes: CustomStringConvertible { switch self { // No Formatting required // swiftlint:disable:next line_length - case .APIError(_, let message), .INVALID_REDACTION_TYPE( _, let message), .EMPTY_COLUMN_NAME_IN_COLLECT( _, let message), .EMPTY_TABLE_NAME_IN_COLLECT( _, let message), .REGEX_MATCH_FAILED( _, let message), .EMPTY_TABLE_NAME( _, let message), .EMPTY_VAULT_ID( _, let message), .EMPTY_VAULT_URL( _, let message), .RECORDS_KEY_ERROR( _, let message),.INVALID_RECORDS_TYPE( _, let message), .EMPTY_COLUMN_NAME( _, let message), .INVALID_BEARER_TOKEN_FORMAT( _, let message), .MISSING_RECORDS_ARRAY( _, let message), .MISSING_RECORDS_IN_ADDITIONAL_FIELDS( _, let message), .EMPTY_RECORDS_OBJECT( _, let message), .MISSING_RECORDS_IN_GETBYID( _, let message),.INVALID_URL( _, let message),.SKYFLOW_IDS_AND_COLUMN_NAME_BOTH_SPECIFIED( _, let message), .MISSING_IDS_OR_COLUMN_VALUES_IN_GET( _, let message), .MISSING_RECORD_COLUMN_VALUE( _, let message), .UPSERT_OPTION_CANNOT_BE_EMPTY( _, let message),.INVALID_COLUMN_VALUES_IN_GET( _, let message),.MISSING_COMPOSABLE_LAYOUT_KEY(_, let message), .MISMATCH_ELEMENT_COUNT_LAYOUT_SUM(_, let message),.EMPTY_TOKEN_ID( _, let message), .ID_KEY_ERROR( _, let message),.REDACTION_WITH_TOKEN_NOT_SUPPORTED( _, let message), .TOKENS_GET_COLUMN_NOT_SUPPPORTED( _, let message),.MISSING_COLUMN_NAME( _, let message),.EMPTY_COMPOSABLE_LAYOUT_ARRAY(_, let message), .INVALID_IDS_TYPE( _, let message),.MISSING_COMPOSABLE_CONTAINER_OPTIONS(code: _, message: let message): + case .APIError(_, let message), .INVALID_REDACTION_TYPE( _, let message), .EMPTY_COLUMN_NAME_IN_COLLECT( _, let message), .EMPTY_TABLE_NAME_IN_COLLECT( _, let message), .REGEX_MATCH_FAILED( _, let message), .EMPTY_TABLE_NAME( _, let message), .EMPTY_VAULT_ID( _, let message), .EMPTY_VAULT_URL( _, let message), .RECORDS_KEY_ERROR( _, let message),.INVALID_RECORDS_TYPE( _, let message), .EMPTY_COLUMN_NAME( _, let message), .INVALID_BEARER_TOKEN_FORMAT( _, let message), .MISSING_RECORDS_IN_ADDITIONAL_FIELDS( _, let message), .EMPTY_RECORDS_OBJECT( _, let message), .MISSING_RECORDS_IN_GETBYID( _, let message),.INVALID_URL( _, let message),.SKYFLOW_IDS_AND_COLUMN_NAME_BOTH_SPECIFIED( _, let message), .MISSING_IDS_OR_COLUMN_VALUES_IN_GET( _, let message), .MISSING_RECORD_COLUMN_VALUE( _, let message), .UPSERT_OPTION_CANNOT_BE_EMPTY( _, let message),.INVALID_COLUMN_VALUES_IN_GET( _, let message),.MISSING_COMPOSABLE_LAYOUT_KEY(_, let message), .MISMATCH_ELEMENT_COUNT_LAYOUT_SUM(_, let message),.EMPTY_TOKEN_ID( _, let message), .ID_KEY_ERROR( _, let message),.REDACTION_WITH_TOKEN_NOT_SUPPORTED( _, let message), .TOKENS_GET_COLUMN_NOT_SUPPPORTED( _, let message),.MISSING_COLUMN_NAME( _, let message),.EMPTY_COMPOSABLE_LAYOUT_ARRAY(_, let message), .INVALID_IDS_TYPE( _, let message),.MISSING_COMPOSABLE_CONTAINER_OPTIONS(code: _, message: let message): return message // Single value formatting // swiftlint:disable:next line_length - case .UNMOUNTED_COLLECT_ELEMENT( _, let message, let value), .UNMOUNTED_REVEAL_ELEMENT( _, let message, let value), .EMPTY_IDS( _, let message, let value), .EMPTY_ID_VALUE( _, let message, let value), .TABLE_KEY_ERROR( _, let message, let value), .FIELDS_KEY_ERROR( _, let message, let value), .EMPTY_FIELDS_KEY( _, let message, let value), .REDACTION_KEY_ERROR( _, let message, let value), .MISSING_KEY_IDS( _, let message, let value), .INVALID_TABLE_NAME_TYPE( _, let message, let value), .INVALID_FIELDS_TYPE( _, let message, let value), .INVALID_TOKEN_TYPE( _, let message, let value), .MISSING_TABLE_NAME_IN_USERT_OPTION( _, let message, let value), .MISSING_COLUMN_NAME_IN_USERT_OPTION( _, let message, let value), .COLUMN_NAME_IS_EMPTY_FOR_ATLEAST_ONE_UPSERT_OPTION(_, let message, let value), .TABLE_NAME_IS_EMPTY_FOR_ATLEAST_ONE_UPSERT_OPTION( _, let message, let value), .EMPTY_RECORD_COLUMN_VALUES( _, let message, let value), .EMPTY_COLUMN_VALUE( _, let message, let value), .INVALID_COLUMN_NAME( _, let message, let value), .DUPLICATE_ADDITIONAL_FIELD_FOUND( _, let message, let value), .ERROR_TRIGGERED( _, let message, let value): + case .UNMOUNTED_COLLECT_ELEMENT( _, let message, let value), .UNMOUNTED_REVEAL_ELEMENT( _, let message, let value), .EMPTY_IDS( _, let message, let value), .EMPTY_ID_VALUE( _, let message, let value), .TABLE_KEY_ERROR( _, let message, let value), .FIELDS_KEY_ERROR( _, let message, let value), .EMPTY_FIELDS_KEY( _, let message, let value), .REDACTION_KEY_ERROR( _, let message, let value), .MISSING_KEY_IDS( _, let message, let value), .INVALID_TABLE_NAME_TYPE( _, let message, let value), .INVALID_FIELDS_TYPE( _, let message, let value), .INVALID_TOKEN_TYPE( _, let message, let value), .MISSING_TABLE_NAME_IN_USERT_OPTION( _, let message, let value), .MISSING_COLUMN_NAME_IN_USERT_OPTION( _, let message, let value), .COLUMN_NAME_IS_EMPTY_FOR_ATLEAST_ONE_UPSERT_OPTION(_, let message, let value), .UNIQUE_COLUMNS_EMPTY_FOR_ATLEAST_ONE_UPSERT_OPTION(_, let message, let value), .TABLE_NAME_IS_EMPTY_FOR_ATLEAST_ONE_UPSERT_OPTION( _, let message, let value), .EMPTY_RECORD_COLUMN_VALUES( _, let message, let value), .EMPTY_COLUMN_VALUE( _, let message, let value), .INVALID_COLUMN_NAME( _, let message, let value), .DUPLICATE_ADDITIONAL_FIELD_FOUND( _, let message, let value), .ERROR_TRIGGERED( _, let message, let value): return formatMessage(message, [value]) // Multi value formatting case .DUPLICATE_ELEMENT_FOUND( _, let message, let values): @@ -121,7 +121,7 @@ internal enum ErrorCodes: CustomStringConvertible { } else { result += values[valuesIndex] } - + valuesIndex += 1 } else { result += word @@ -134,13 +134,91 @@ internal enum ErrorCodes: CustomStringConvertible { } public class SkyflowError: NSError { - var xml: String = "" - - func setXML(xml: String) { - self.xml = xml + // Only populated for a whole-request API failure (see init(apiError:)) - nil for + // client-side validation failures, which only have httpCode/message. + public let grpcCode: Int? + public let httpStatus: String? + public let details: [Any]? + + // Aliases for NSError's own code/localizedDescription, matching the API error JSON's key names. + public var httpCode: Int { self.code } + public var message: String { self.localizedDescription } + + public init(domain: String, code: Int, userInfo: [String: Any]? = nil, + grpcCode: Int? = nil, httpStatus: String? = nil, details: [Any]? = nil) { + self.grpcCode = grpcCode + self.httpStatus = httpStatus + self.details = details + super.init(domain: domain, code: code, userInfo: userInfo) + } + + public required init?(coder: NSCoder) { + self.grpcCode = coder.decodeObject(forKey: "grpcCode") as? Int + self.httpStatus = coder.decodeObject(forKey: "httpStatus") as? String + self.details = coder.decodeObject(forKey: "details") as? [Any] + super.init(coder: coder) + } + + // Builds a SkyflowError out of a whole-request API failure of the shape + // {"error": {"grpcCode", "httpCode", "message", "httpStatus", "details"}} - returns nil if + // apiError isn't in that shape (e.g. it's already a SkyflowError from client-side validation). + // httpCode maps to NSError's own `code`, and message maps to localizedDescription/NSLocalizedDescriptionKey. + public convenience init?(apiError: Any) { + guard let dict = apiError as? [String: Any], + let detail = dict["error"] as? [String: Any] else { return nil } + self.init( + domain: "", + code: detail["httpCode"] as? Int ?? 0, + userInfo: [NSLocalizedDescriptionKey: detail["message"] as? String ?? ""], + grpcCode: detail["grpcCode"] as? Int, + httpStatus: detail["httpStatus"] as? String, + details: detail["details"] as? [Any] + ) } - - public func getXML() -> String { - return self.xml + + // Normalizes any Callback.onFailure payload into a SkyflowError, so CollectCallback/RevealCallback + // can type onFailure as (SkyflowError) -> Void instead of (Any) -> Void. + internal static func wrap(_ error: Any) -> SkyflowError { + if let skyflowError = error as? SkyflowError { + return skyflowError + } + if let apiError = SkyflowError(apiError: error) { + return apiError + } + if let nsError = error as? NSError { + return SkyflowError(domain: nsError.domain, code: nsError.code, userInfo: nsError.userInfo) + } + // Client.detokenize() bypasses RevealValueCallback entirely, so whole-request failures + // arrive here still wrapped in an "errors" array instead of a raw NSError. Two shapes are + // possible depending on where the failure originated: + // - Client.swift's own pre-network validation (empty vaultID/vaultURL/records key): each + // entry in "errors" IS the NSError directly. + // - FlowVaultRevealAPICallback's post-dispatch failures (invalid bearer token, network + // error, invalid URL, etc.): each entry is {"error": }. + // Recover the underlying NSError instead of stringifying the whole container dict. + // (Container.reveal()'s equivalent failures are already unwrapped earlier, + // by RevealValueCallback.onFailure.) + // + // A third shape reaches here too: FlowVaultInsertAPICallback's insert+update merge + // (group.notify in onSuccess) builds mergedErrors entries as {"error": {"message", + // "httpCode", ...}} - a JSON-decoded API error detail dict, not an NSError - so it's + // handled by trying SkyflowError(apiError:) on the entry itself, which expects exactly + // that {"error": {...}} shape. + if let dict = error as? [String: Any], let errorsArray = dict["errors"] as? [Any] { + for entry in errorsArray { + if let nsError = entry as? NSError { + return SkyflowError(domain: nsError.domain, code: nsError.code, userInfo: nsError.userInfo) + } + if let nested = entry as? [String: Any] { + if let nsError = nested["error"] as? NSError { + return SkyflowError(domain: nsError.domain, code: nsError.code, userInfo: nsError.userInfo) + } + if let apiError = SkyflowError(apiError: entry) { + return apiError + } + } + } + } + return SkyflowError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey: "\(error)"]) } } diff --git a/Sources/Skyflow/reveal/FlowVaultDetokenizeRequestBody.swift b/Sources/Skyflow/reveal/FlowVaultDetokenizeRequestBody.swift new file mode 100644 index 00000000..0a689f8d --- /dev/null +++ b/Sources/Skyflow/reveal/FlowVaultDetokenizeRequestBody.swift @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// Used for generating request body for FlowDB v2 detokenize api call + +import Foundation + +internal class FlowVaultDetokenizeRequestBody { + internal static func createRequestBody(vaultID: String, records: [RevealRequestRecord], tokenGroupRedactions: [TokenGroupRedaction]? = nil) -> [String: Any] { + let tokens = records.map { $0.token } + var body: [String: Any] = ["vaultID": vaultID, "tokens": tokens] + if let tokenGroupRedactions = tokenGroupRedactions, !tokenGroupRedactions.isEmpty { + // Pass through exactly what was collected from the mounted elements, duplicates + // and all - the API validates tokenGroupRedactions, not the SDK. + body["tokenGroupRedactions"] = tokenGroupRedactions.map { ["tokenGroupName": $0.tokenGroupName, "redaction": $0.redaction] } + } + return body + } +} diff --git a/Sources/Skyflow/reveal/FlowVaultRevealApiCallback.swift b/Sources/Skyflow/reveal/FlowVaultRevealApiCallback.swift new file mode 100644 index 00000000..7932295e --- /dev/null +++ b/Sources/Skyflow/reveal/FlowVaultRevealApiCallback.swift @@ -0,0 +1,157 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// Implementation of callback for Reveal api (FlowDB v2) + +import Foundation + +class FlowVaultRevealAPICallback: Callback { + var apiClient: APIClient + var callback: Callback + var connectionUrl: String + var records: [RevealRequestRecord] + var tokenGroupRedactions: [TokenGroupRedaction]? + var contextOptions: ContextOptions + + + internal init(callback: Callback, apiClient: APIClient, connectionUrl: String, + records: [RevealRequestRecord], tokenGroupRedactions: [TokenGroupRedaction]? = nil, contextOptions: ContextOptions) { + self.apiClient = apiClient + self.callback = callback + self.connectionUrl = connectionUrl + self.records = records + self.tokenGroupRedactions = tokenGroupRedactions + self.contextOptions = contextOptions + } + + internal func onSuccess(_ token: Any) { + guard let url = URL(string: connectionUrl) else { + let errorCode = ErrorCodes.INVALID_URL() + self.callRevealOnFailure(callback: self.callback, errorObject: errorCode.getErrorObject(contextOptions: self.contextOptions)) + return + } + + do { + let (request, session) = try getRequestSession(url: url) + let task = session.dataTask(with: request) { data, response, error in + do { + let response = try self.processResponse(data: data, response: response, error: error) + self.callback.onSuccess(response) + } catch { + self.callRevealOnFailure(callback: self.callback, errorObject: error) + } + } + task.resume() + } catch let error { + self.callRevealOnFailure(callback: self.callback, errorObject: error) + } + } + + internal func onFailure(_ error: Any) { + if error is Error { + callRevealOnFailure(callback: self.callback, errorObject: error as! Error) + } else { + self.callback.onFailure(error) + } + } + + private func callRevealOnFailure(callback: Callback, errorObject: Error) { + let result = ["errors": [["error": errorObject]]] + callback.onFailure(result) + } + + internal func getRequestSession(url: URL) throws -> (URLRequest, URLSession) { + var jsonString = "" + + do { + let deviceDetails = FetchMetrices().getMetrices() + let jsonData = try JSONSerialization.data(withJSONObject: deviceDetails, options: []) + jsonString = String(data: jsonData, encoding: .utf8) ?? "" + } catch { + jsonString = "" + } + var request = URLRequest(url: url) + request.httpMethod = "POST" + + do { + let data = try JSONSerialization.data(withJSONObject: FlowVaultDetokenizeRequestBody.createRequestBody(vaultID: self.apiClient.vaultID, records: records, tokenGroupRedactions: tokenGroupRedactions)) + request.httpBody = data + } + + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.setValue("application/json", forHTTPHeaderField: "Accept") + request.setValue(("Bearer " + self.apiClient.token), forHTTPHeaderField: "Authorization") + request.setValue(jsonString, forHTTPHeaderField: "sky-metadata") + + return (request, URLSession(configuration: .default)) + } + + func processResponse(data: Data?, response: URLResponse?, error: Error?) throws -> [String: Any] { + if error != nil || response == nil { + throw error! + } + + if let httpResponse = response as? HTTPURLResponse { + let range = 400...599 + if range ~= httpResponse.statusCode { + // FlowDB returns a non-2xx status when every token in the batch fails to + // detokenize, but the body still has the same {"response": [...]} shape as a + // success/partial response (each entry carrying its own error/httpCode) - parse + // it as such instead of collapsing into a generic top-level error. + if let safeData = data, + let jsonObject = try? JSONSerialization.jsonObject(with: safeData, options: .allowFragments) as? [String: Any], + jsonObject["response"] != nil { + return try getDetokenizeResponseBody(data: safeData) + } + var description = "Detokenize call failed with the following status code " + String(httpResponse.statusCode) + if let safeData = data { + do { + let errorResponse = try JSONSerialization.jsonObject(with: safeData, options: .allowFragments) as! [String: Any] + if let errorDetails = errorResponse["error"] as? [String: Any], + let message = errorDetails["message"] as? String { + description = message + } + if let requestId = httpResponse.allHeaderFields["x-request-id"] { + description += " - request-id: \(requestId)" + } + } catch { + throw ErrorCodes.APIError(code: httpResponse.statusCode, message: String(data: safeData, encoding: .utf8) ?? "Unknown error").getErrorObject(contextOptions: self.contextOptions) + } + } + throw ErrorCodes.APIError(code: httpResponse.statusCode, message: description).getErrorObject(contextOptions: self.contextOptions) + } + } + + guard let safeData = data else { + return ["records": []] + } + + return try getDetokenizeResponseBody(data: safeData) + } + + func getDetokenizeResponseBody(data: Data) throws -> [String: Any] { + let jsonData = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as! [String: Any] + var records: [[String: Any]] = [] + + let responseRecords = jsonData["response"] as? [[String: Any]] ?? [] + for entry in responseRecords { + if let error = entry["error"] as? String { + var errorEntry: [String: Any] = ["error": error] + if let token = entry["token"] { errorEntry["token"] = token } + if let httpCode = entry["httpCode"] { errorEntry["httpCode"] = httpCode } + records.append(errorEntry) + } else { + var successEntry: [String: Any] = [:] + if let token = entry["token"] { successEntry["token"] = token } + if let value = entry["value"] { successEntry["value"] = value } + if let tokenGroupName = entry["tokenGroupName"] { successEntry["tokenGroupName"] = tokenGroupName } + if let httpCode = entry["httpCode"] { successEntry["httpCode"] = httpCode } + if let metadata = entry["metadata"] as? [String: Any] { successEntry["metadata"] = metadata } + records.append(successEntry) + } + } + + return ["records": records] + } +} diff --git a/Sources/Skyflow/reveal/RevealApiCallback.swift b/Sources/Skyflow/reveal/RevealApiCallback.swift index 424ea0f6..ad1ec260 100644 --- a/Sources/Skyflow/reveal/RevealApiCallback.swift +++ b/Sources/Skyflow/reveal/RevealApiCallback.swift @@ -54,10 +54,10 @@ class RevealAPICallback: Callback { defer { revealRequestGroup.leave() } - + do { let (success, failure) = try self.processResponse(record: record, data: data, response: response, error: error) - + if success != nil { list_success.append(success!) } @@ -89,7 +89,7 @@ class RevealAPICallback: Callback { let result = ["errors": [["error": errorObject]]] callback.onFailure(result) } - + internal func getRequestSession() -> (URLRequest, URLSession){ var jsonString = "" @@ -107,29 +107,29 @@ class RevealAPICallback: Callback { request.setValue("application/json", forHTTPHeaderField: "Accept") request.setValue(("Bearer " + self.apiClient.token), forHTTPHeaderField: "Authorization") request.setValue(jsonString, forHTTPHeaderField: "sky-metadata") - + return (request, URLSession(configuration: .default)) } - + internal func getRevealRequestBody(record: RevealRequestRecord) throws -> Data { let bodyObject: [String: Any] = [ "detokenizationParameters": [ [ "token": record.token, - "redaction": record.redaction + "redaction": "PLAIN_TEXT" ] ] ] return try JSONSerialization.data(withJSONObject: bodyObject) } - + internal func processResponse(record: RevealRequestRecord, data: Data?, response: URLResponse?, error: Error?) throws -> (RevealSuccessRecord?, RevealErrorRecord?){ - + if error != nil || response == nil { throw error! } - + if let httpResponse = response as? HTTPURLResponse { let range = 400...599 if range ~= httpResponse.statusCode { @@ -154,13 +154,13 @@ class RevealAPICallback: Callback { let receivedResponseArray: [Any] = (jsonData[keyPath: "records"] as! [Any]) let records: [String: Any] = receivedResponseArray[0] as! [String: Any] let successRecord = RevealSuccessRecord(token_id: records["token"] as! String, value: records["value"] as! String) - + return (successRecord, nil) } - + return (nil, nil) } - + func handleCallbacks(success: [RevealSuccessRecord], failure: [RevealErrorRecord], isSuccess: Bool, errorObject: Error!) { var records: [Any] = [] for record in success { @@ -183,7 +183,7 @@ class RevealAPICallback: Callback { if errors.count != 0 { modifiedResponse["errors"] = errors } - + if isSuccess { if errors.isEmpty { self.callback.onSuccess(modifiedResponse) diff --git a/Sources/Skyflow/reveal/RevealContainer.swift b/Sources/Skyflow/reveal/RevealContainer.swift index 57f97bc9..c3a48380 100644 --- a/Sources/Skyflow/reveal/RevealContainer.swift +++ b/Sources/Skyflow/reveal/RevealContainer.swift @@ -25,14 +25,14 @@ public extension Container { return revealElement } - func reveal(callback: Callback, options: RevealOptions? = RevealOptions()) where T: RevealContainer { + func reveal(callback: RevealCallback, options: RevealOptions? = RevealOptions()) where T: RevealContainer { var tempContextOptions = self.skyflow.contextOptions tempContextOptions.interface = .REVEAL_CONTAINER if self.skyflow.vaultID.isEmpty { let errorCode = ErrorCodes.EMPTY_VAULT_ID() return callback.onFailure(errorCode.getErrorObject(contextOptions: tempContextOptions)) } - if self.skyflow.vaultURL == "/v1/vaults/" { + if self.skyflow.vaultURL == "/v2/vaults/" { let errorCode = ErrorCodes.EMPTY_VAULT_URL() return callback.onFailure(errorCode.getErrorObject(contextOptions: tempContextOptions)) } @@ -61,8 +61,8 @@ public extension Container { if let tokens = records["records"] as? [[String: Any]] { var list: [RevealRequestRecord] = [] for token in tokens { - if let redaction = token["redaction"] as? RedactionType, let id = token["token"] as? String { - list.append(RevealRequestRecord(token: id, redaction: redaction.rawValue)) + if let id = token["token"] as? String { + list.append(RevealRequestRecord(token: id)) } } let logCallback = LogCallback(clientCallback: revealValueCallback, contextOptions: tempContextOptions, @@ -72,7 +72,7 @@ public extension Container { onFailureHandler: { } ) - self.skyflow.apiClient.get(records: list, callback: logCallback, contextOptions: tempContextOptions) + self.skyflow.apiClient.get(records: list, tokenGroupRedactions: options?.tokenGroupRedactions, callback: logCallback, contextOptions: tempContextOptions) } } } diff --git a/Sources/Skyflow/reveal/RevealElementInput.swift b/Sources/Skyflow/reveal/RevealElementInput.swift index d0da3195..3bf4d7f3 100644 --- a/Sources/Skyflow/reveal/RevealElementInput.swift +++ b/Sources/Skyflow/reveal/RevealElementInput.swift @@ -12,26 +12,14 @@ public struct RevealElementInput { internal var labelStyles: Styles? internal var errorTextStyles: Styles? internal var label: String - internal var redaction: RedactionType internal var altText: String - public init(token: String = "", inputStyles: Styles = Styles(), labelStyles: Styles = Styles(), errorTextStyles: Styles = Styles(), label: String, redaction: RedactionType = .PLAIN_TEXT, altText: String = "") { - self.token = token - self.inputStyles = inputStyles - self.labelStyles = labelStyles - self.errorTextStyles = errorTextStyles - self.label = label - self.redaction = redaction - self.altText = altText - } - public init(token: String = "", inputStyles: Styles = Styles(), labelStyles: Styles = Styles(), errorTextStyles: Styles = Styles(), label: String, altText: String = "") { self.token = token self.inputStyles = inputStyles self.labelStyles = labelStyles self.errorTextStyles = errorTextStyles self.label = label - self.redaction = .PLAIN_TEXT self.altText = altText } } diff --git a/Sources/Skyflow/reveal/RevealOptions.swift b/Sources/Skyflow/reveal/RevealOptions.swift index 0857e940..eaf1de74 100644 --- a/Sources/Skyflow/reveal/RevealOptions.swift +++ b/Sources/Skyflow/reveal/RevealOptions.swift @@ -4,7 +4,7 @@ // // File.swift -// +// // // Created by Akhil Anil Mangala on 11/08/21. // @@ -12,5 +12,9 @@ import Foundation public struct RevealOptions { - public init() {} + public let tokenGroupRedactions: [TokenGroupRedaction]? + + public init(tokenGroupRedactions: [TokenGroupRedaction]? = nil) { + self.tokenGroupRedactions = tokenGroupRedactions + } } diff --git a/Sources/Skyflow/reveal/RevealRequestBody.swift b/Sources/Skyflow/reveal/RevealRequestBody.swift index 72d6d919..8b0230e1 100644 --- a/Sources/Skyflow/reveal/RevealRequestBody.swift +++ b/Sources/Skyflow/reveal/RevealRequestBody.swift @@ -12,7 +12,6 @@ internal class RevealRequestBody { for element in elements { var entry: [String: Any] = [:] entry["token"] = element.revealInput.token - entry["redaction"] = element.revealInput.redaction payload.append(entry) } diff --git a/Sources/Skyflow/reveal/RevealRequestRecord.swift b/Sources/Skyflow/reveal/RevealRequestRecord.swift index 6b1b0b01..2652b247 100644 --- a/Sources/Skyflow/reveal/RevealRequestRecord.swift +++ b/Sources/Skyflow/reveal/RevealRequestRecord.swift @@ -8,9 +8,7 @@ import Foundation struct RevealRequestRecord { var token: String - var redaction: String - init(token: String, redaction: String) { + init(token: String) { self.token = token - self.redaction = redaction } } diff --git a/Sources/Skyflow/reveal/RevealResponse.swift b/Sources/Skyflow/reveal/RevealResponse.swift new file mode 100644 index 00000000..2b7b91cd --- /dev/null +++ b/Sources/Skyflow/reveal/RevealResponse.swift @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// Public typed wrappers around the dictionary delivered via Callback.onSuccess for both +// Client.detokenize() and Container.reveal() - both deliver the same merged +// {"records": [...]} shape (success and per-token failures share one array, matching Collect's +// convention). The Callback protocol still delivers raw [String: Any] as before - these types +// don't change that, they just wrap it directly (no JSONDecoder round-trip). Usage: +// +// public func onSuccess(_ responseBody: Any) { +// guard let response = Skyflow.RevealResponse(responseBody) else { return } +// ... +// } + +import Foundation + +public struct RevealResponse { + public let records: [RevealRecord] + + public init?(_ responseBody: Any) { + guard let dict = responseBody as? [String: Any], + let recordDicts = dict["records"] as? [[String: Any]] else { return nil } + self.records = recordDicts.map { RevealRecord($0) } + } +} + +// Each entry in "records" is either a successfully revealed token (error is nil) or a failed one +// (error is non-nil) - the vault returns both together in the same array, each tagged with its +// own httpCode. +public struct RevealRecord { + public let token: String? + public let tokenGroupName: String? + // Keyed by whatever fields the vault includes (e.g. "skyflowID", "tableName"). + public let metadata: [String: Any]? + public let httpCode: Int + public let error: String? + + init(_ dict: [String: Any]) { + self.token = dict["token"] as? String + self.tokenGroupName = dict["tokenGroupName"] as? String + self.metadata = dict["metadata"] as? [String: Any] + self.httpCode = dict["httpCode"] as? Int ?? 0 + self.error = dict["error"] as? String + } +} + +// A ready-made Skyflow.Callback that unwraps the raw responseBody/error into RevealResponse +// for you. Use it directly with the callback-based reveal(callback:options:) - RevealValueCallback +// merges success/failure into one "records" array (matching Collect's convention), so the same +// RevealResponse type used for Client.detokenize() applies here too. +public class RevealCallback: Callback { + private let successHandler: (RevealResponse) -> Void + private let failureHandler: (SkyflowError) -> Void + + public init(onSuccess: @escaping (RevealResponse) -> Void, onFailure: @escaping (SkyflowError) -> Void) { + self.successHandler = onSuccess + self.failureHandler = onFailure + } + + public func onSuccess(_ responseBody: Any) { + guard let response = RevealResponse(responseBody) else { + failureHandler(SkyflowError.wrap(responseBody)) + return + } + successHandler(response) + } + + // Delivered when the entire request fails (e.g. vault not found, network error) rather + // than a per-token failure inside RevealResponse.records, and for client-side validation + // failures (empty vaultID, unmounted element, etc.). Always normalized into a + // Skyflow.SkyflowError - see SkyflowError.wrap. + public func onFailure(_ error: Any) { + failureHandler(SkyflowError.wrap(error)) + } +} diff --git a/Sources/Skyflow/reveal/RevealValueCallback.swift b/Sources/Skyflow/reveal/RevealValueCallback.swift index a32438fc..4c7e1d2b 100644 --- a/Sources/Skyflow/reveal/RevealValueCallback.swift +++ b/Sources/Skyflow/reveal/RevealValueCallback.swift @@ -18,42 +18,31 @@ internal class RevealValueCallback: Callback { func onSuccess(_ responseBody: Any) { var tokens: [String: String] = [:] - - let responseJson = responseBody as! [String: Any] + let responseJson = responseBody as? [String: Any] ?? [:] var response: [String: Any] = [:] - var tempSuccessResponses: [[String: String]] = [] + var records: [[String: Any]] = [] + var errors: [[String: Any]] = [] - if let records = responseJson["records"] as? [Any] { - for record in records { - let dict = record as! [String: Any] - let token = dict["token"] as! String + // Success and per-token failure entries arrive together in one "records" array (matching + // Collect's convention and the JS SDK's shape), each distinguished by an "error" key. + if let responseRecords = responseJson["records"] as? [Any] { + for record in responseRecords { + guard let dict = record as? [String: Any] else { continue } + if dict["error"] != nil { + errors.append(dict) + records.append(dict) + continue + } + guard let token = dict["token"] as? String else { continue } let value = dict["value"] as? String tokens[token] = value ?? token - - var successEntry: [String: String] = [:] - successEntry["token"] = token - tempSuccessResponses.append(successEntry) - } - } - - var successResponses: [[String: String]] = [] - for entry in tempSuccessResponses { - if let token = entry["token"] { - successResponses.append(entry) + + records.append(dict) } } - if successResponses.count != 0 { - response["success"] = successResponses - } - var errors = [] as [[String: Any]] - if let responseErrors = responseJson["errors"] as? [[String: Any]] { - errors = responseErrors - } + response["records"] = records let tokensToErrors = getTokensToErrors(errors) - if errors.count != 0 { - response["errors"] = errors - } DispatchQueue.main.async { for revealElement in self.revealElements { @@ -74,63 +63,75 @@ internal class RevealValueCallback: Callback { self.clientCallback.onSuccess(response) } + // Reserved for whole-request failures: a genuinely opaque error (can't be parsed as a + // dictionary at all), or a network/API-level error not scoped to any specific token (no + // "token" key - e.g. an invalid bearer token, a connection failure). Only PER-TOKEN failures + // (each tagged with the "token" they belong to) are routed through onSuccess instead, matching + // Collect's convention of surfacing per-record issues via the record's own "error" field. func onFailure(_ error: Any) { - var response: [String: Any] = [:] + guard let responseJson = error as? [String: Any] else { + self.clientCallback.onFailure(error) + return + } - if error is [String: Any] { - var tokens: [String: String] = [:] - - let responseJson = error as! [String: Any] - var tempSuccessResponses: [[String: String]] = [] - - if let records = responseJson["records"] as? [Any] { - for record in records { - let dict = record as! [String: Any] - let token = dict["token"] as! String - let value = dict["value"] as? String - tokens[token] = value ?? token - - var successEntry: [String: String] = [:] - successEntry["token"] = token - tempSuccessResponses.append(successEntry) - } - } - - var successResponses: [[String: String]] = [] - for entry in tempSuccessResponses { - if let token = entry["token"] { - successResponses.append(entry) + var errors = [] as [[String: Any]] + if let responseErrors = responseJson["errors"] as? [[String: Any]] { + errors = responseErrors + } + + // Not scoped to any specific reveal element - a genuine network/API-level failure, not a + // per-record issue, so it's delivered via onFailure rather than folded into the records array. + if let networkError = errors.first(where: { $0["token"] == nil }) { + DispatchQueue.main.async { + for revealElement in self.revealElements { + revealElement.hideError() } } + self.clientCallback.onFailure(networkError["error"] ?? error) + return + } - if successResponses.count != 0 { - response["success"] = successResponses - } - var errors = [[:]] as [[String: Any]] - if let responseErrors = responseJson["errors"] as? [[String: Any]] { - errors = responseErrors + var tokens: [String: String] = [:] + var records: [[String: Any]] = [] + + if let responseRecords = responseJson["records"] as? [Any] { + for record in responseRecords { + guard let dict = record as? [String: Any], let token = dict["token"] as? String else { continue } + let value = dict["value"] as? String + tokens[token] = value ?? token + + records.append(dict) } - - let tokensToErrors = getTokensToErrors(errors) - if errors.count != 0 { - response["errors"] = errors + } + + // Each entry's "error" arrives as an NSError here (built internally, not deserialized from + // JSON) - convert to a plain message string so it matches the "records" array's normal + // shape (each entry's "error" is always a String, whether from onSuccess or here). + let stringifiedErrors = errors.map { entry -> [String: Any] in + var updated = entry + if let nsError = entry["error"] as? NSError { + updated["error"] = nsError.localizedDescription } - - DispatchQueue.main.async { - for revealElement in self.revealElements { - if let v = tokens[revealElement.revealInput.token]{ - revealElement.updateVal(value: v) - } - - let inputToken = revealElement.revealInput.token - revealElement.hideError() - if let errorMessage = tokensToErrors[inputToken] { - revealElement.showError(message: errorMessage) - } + return updated + } + records.append(contentsOf: stringifiedErrors) + + let tokensToErrors = getTokensToErrors(errors) + + DispatchQueue.main.async { + for revealElement in self.revealElements { + if let v = tokens[revealElement.revealInput.token]{ + revealElement.updateVal(value: v) + } + + let inputToken = revealElement.revealInput.token + revealElement.hideError() + if let errorMessage = tokensToErrors[inputToken] { + revealElement.showError(message: errorMessage) } } } - self.clientCallback.onFailure(response) + self.clientCallback.onSuccess(["records": records]) } func getTokensToErrors(_ errors: [[String: Any]]?) -> [String: String] { diff --git a/Sources/Skyflow/reveal/TokenGroupRedaction.swift b/Sources/Skyflow/reveal/TokenGroupRedaction.swift new file mode 100644 index 00000000..c5b3cd51 --- /dev/null +++ b/Sources/Skyflow/reveal/TokenGroupRedaction.swift @@ -0,0 +1,17 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// Object that describes a request-level, per-token-group redaction for detokenize + +import Foundation + +public struct TokenGroupRedaction { + public let tokenGroupName: String + public let redaction: String + + public init(tokenGroupName: String, redaction: String) { + self.tokenGroupName = tokenGroupName + self.redaction = redaction + } +} diff --git a/Sources/Skyflow/skyflow_iOS.swift b/Sources/Skyflow/skyflow_iOS.swift deleted file mode 100644 index 226327f6..00000000 --- a/Sources/Skyflow/skyflow_iOS.swift +++ /dev/null @@ -1,7 +0,0 @@ -/* - * Copyright (c) 2022 Skyflow -*/ - -struct skyflow_iOS { - var text = "Hello, World!" -} diff --git a/Tests/skyflow-iOS-collectTests/DemoImpl.swift b/Tests/skyflow-iOS-collectTests/DemoImpl.swift index 7d3e3f85..e89f3a1f 100644 --- a/Tests/skyflow-iOS-collectTests/DemoImpl.swift +++ b/Tests/skyflow-iOS-collectTests/DemoImpl.swift @@ -42,12 +42,24 @@ public class DemoAPICallback: Callback { var receivedResponse: String = "default" var expectation: XCTestExpectation var data: [String: Any] = [:] + var collectResponse: Skyflow.CollectResponse? + var revealResponse: Skyflow.RevealResponse? public init(expectation: XCTestExpectation) { self.expectation = expectation } public func onSuccess(_ responseBody: Any) { + if let response = responseBody as? Skyflow.CollectResponse { + self.collectResponse = response + expectation.fulfill() + return + } + if let response = responseBody as? Skyflow.RevealResponse { + self.revealResponse = response + expectation.fulfill() + return + } do { let dataString = String(data: try JSONSerialization.data(withJSONObject: responseBody), encoding: .utf8) if let unwrapped = dataString { @@ -68,4 +80,15 @@ public class DemoAPICallback: Callback { } expectation.fulfill() } + + // CollectContainer.collect(callback:)/RevealContainer.reveal(callback:) require the + // concrete Skyflow.CollectCallback/RevealCallback types - these wrap self so existing + // DemoAPICallback call sites only need a one-word change (.asCollectCallback/.asRevealCallback). + var asCollectCallback: Skyflow.CollectCallback { + Skyflow.CollectCallback(onSuccess: { self.onSuccess($0) }, onFailure: { self.onFailure($0) }) + } + + var asRevealCallback: Skyflow.RevealCallback { + Skyflow.RevealCallback(onSuccess: { self.onSuccess($0) }, onFailure: { self.onFailure($0) }) + } } diff --git a/Tests/skyflow-iOS-collectTests/skyflow_iOS_collectTests.swift b/Tests/skyflow-iOS-collectTests/skyflow_iOS_collectTests.swift index 78e42f2c..152bedd6 100644 --- a/Tests/skyflow-iOS-collectTests/skyflow_iOS_collectTests.swift +++ b/Tests/skyflow-iOS-collectTests/skyflow_iOS_collectTests.swift @@ -237,7 +237,7 @@ final class skyflow_iOS_collectTests: XCTestCase { let callback = DemoAPICallback(expectation: expectation) - container?.collect(callback: callback) + container?.collect(callback: callback.asCollectCallback) wait(for: [expectation], timeout: 10.0) @@ -277,7 +277,7 @@ final class skyflow_iOS_collectTests: XCTestCase { let callback = DemoAPICallback(expectation: expectation) - container?.collect(callback: callback) + container?.collect(callback: callback.asCollectCallback) wait(for: [expectation], timeout: 10.0) @@ -314,7 +314,7 @@ final class skyflow_iOS_collectTests: XCTestCase { let callback = DemoAPICallback(expectation: expectation) - container?.collect(callback: callback) + container?.collect(callback: callback.asCollectCallback) wait(for: [expectation], timeout: 10.0) @@ -339,7 +339,7 @@ final class skyflow_iOS_collectTests: XCTestCase { let callback = DemoAPICallback(expectation: expectation) - container?.collect(callback: callback) + container?.collect(callback: callback.asCollectCallback) wait(for: [expectation], timeout: 10.0) @@ -386,7 +386,7 @@ final class skyflow_iOS_collectTests: XCTestCase { let expectation = XCTestExpectation(description: "Pure insert with invalid token") let callback = DemoAPICallback(expectation: expectation) - skyflow.insert(records: ["records": records], options: InsertOptions(tokens: true), callback: callback) + skyflow.insert(records: ["records": records], options: InsertOptions(), callback: callback) wait(for: [expectation], timeout: 10.0) @@ -439,7 +439,7 @@ final class skyflow_iOS_collectTests: XCTestCase { textField?.textFieldDidEndEditing(textField!.textField) let expectFailure = XCTestExpectation(description: "Should fail") let myCallback = DemoAPICallback(expectation: expectFailure) - mycontainer?.collect(callback: myCallback) + mycontainer?.collect(callback: myCallback.asCollectCallback) wait(for: [expectFailure], timeout: 10.0) XCTAssertEqual(myCallback.receivedResponse, "for cardnumber INVALID_CARD_NUMBER\n") @@ -462,7 +462,7 @@ final class skyflow_iOS_collectTests: XCTestCase { textField?.textFieldDidEndEditing(textField!.textField) let expectFailure = XCTestExpectation(description: "Should fail") let myCallback = DemoAPICallback(expectation: expectFailure) - mycontainer?.collect(callback: myCallback) + mycontainer?.collect(callback: myCallback.asCollectCallback) wait(for: [expectFailure], timeout: 10.0) XCTAssertEqual(myCallback.receivedResponse, "for cardnumber Regex match failed\n") @@ -513,7 +513,7 @@ final class skyflow_iOS_collectTests: XCTestCase { let expectFailure = XCTestExpectation(description: "Should fail") let myCallback = DemoAPICallback(expectation: expectFailure) - mycontainer?.collect(callback: myCallback) + mycontainer?.collect(callback: myCallback.asCollectCallback) wait(for: [expectFailure], timeout: 10.0) XCTAssertEqual(myCallback.receivedResponse, "for cardnumber triggered error\n") @@ -579,7 +579,7 @@ final class skyflow_iOS_collectTests: XCTestCase { let expectSuccess = XCTestExpectation(description: "Should succeed") let myCallback = DemoAPICallback(expectation: expectSuccess) - let records = CollectRequestBody.createRequestBody(elements: elements, callback: myCallback, contextOptions: ContextOptions()) + let records = FlowVaultCollectRequestBody.createRequestBody(elements: elements, callback: myCallback, contextOptions: ContextOptions()) let recordElement = (records?["records"] as? [[String: Any]])?[0] let fields = recordElement?["fields"] as? [String: Any] @@ -725,251 +725,98 @@ final class skyflow_iOS_collectTests: XCTestCase { let expectation = XCTestExpectation() let callback = DemoAPICallback(expectation: expectation) - container?.collect(callback: callback) + container?.collect(callback: callback.asCollectCallback) wait(for: [expectation], timeout: 20.0) XCTAssertEqual(callback.receivedResponse, ErrorCodes.EMPTY_VAULT_ID().getErrorObject(contextOptions: ContextOptions(interface: InterfaceName.COLLECT_CONTAINER)).localizedDescription) } func testCollectNoVaultURL() { - skyflow.vaultURL = "/v1/vaults/" + skyflow.vaultURL = "/v2/vaults/" let container = skyflow.container(type: ContainerType.COLLECT, options: nil) let expectation = XCTestExpectation() let callback = DemoAPICallback(expectation: expectation) - container?.collect(callback: callback) + container?.collect(callback: callback.asCollectCallback) wait(for: [expectation], timeout: 20.0) XCTAssertEqual(callback.receivedResponse, ErrorCodes.EMPTY_VAULT_URL().getErrorObject(contextOptions: ContextOptions(interface: InterfaceName.COLLECT_CONTAINER)).localizedDescription) } - func testCollectBadTypeAddionalFields() { - let additionalFields = ["records": "records"] - let container = skyflow.container(type: ContainerType.COLLECT) - - let expectation = XCTestExpectation() - let callback = DemoAPICallback(expectation: expectation) - container?.collect(callback: callback, options: CollectOptions(tokens: true, additionalFields: additionalFields)) - - wait(for: [expectation], timeout: 20.0) - - XCTAssertEqual(callback.receivedResponse, ErrorCodes.INVALID_RECORDS_TYPE().getErrorObject(contextOptions: ContextOptions(interface: InterfaceName.COLLECT_CONTAINER)).localizedDescription) - } - - func testCollectNoRecordsInAddionalFields() { - let additionalFields = ["typo": []] - let container = skyflow.container(type: ContainerType.COLLECT) - - let expectation = XCTestExpectation() - let callback = DemoAPICallback(expectation: expectation) - container?.collect(callback: callback, options: CollectOptions(tokens: true, additionalFields: additionalFields)) - - wait(for: [expectation], timeout: 20.0) - - XCTAssertEqual(callback.receivedResponse, - ErrorCodes.MISSING_RECORDS_IN_ADDITIONAL_FIELDS() - .getErrorObject(contextOptions: ContextOptions(interface: InterfaceName.COLLECT_CONTAINER)).localizedDescription) - } - func testCollectEmptyRecordsAddionalFields() { - let additionalFields = ["records": []] + let additionalFields = AdditionalFields(records: []) let container = skyflow.container(type: ContainerType.COLLECT) - + let expectation = XCTestExpectation() let callback = DemoAPICallback(expectation: expectation) - container?.collect(callback: callback, options: CollectOptions(tokens: true, additionalFields: additionalFields)) - + container?.collect(callback: callback.asCollectCallback, options: CollectOptions(additionalFields: additionalFields)) + wait(for: [expectation], timeout: 20.0) - + XCTAssertEqual(callback.receivedResponse, ErrorCodes.EMPTY_RECORDS_OBJECT().getErrorObject(contextOptions: ContextOptions(interface: InterfaceName.COLLECT_CONTAINER)).localizedDescription) } - - func testCollectNoTableKeyAddionalFields() { - let additionalFields = ["records": [[:]]] - let container = skyflow.container(type: ContainerType.COLLECT) - - let expectation = XCTestExpectation() - let callback = DemoAPICallback(expectation: expectation) - container?.collect(callback: callback, options: CollectOptions(tokens: true, additionalFields: additionalFields)) - - wait(for: [expectation], timeout: 20.0) - - XCTAssertEqual(callback.receivedResponse, ErrorCodes.TABLE_KEY_ERROR(value: "0").getErrorObject(contextOptions: ContextOptions(interface: InterfaceName.COLLECT_CONTAINER)).localizedDescription) - } - - func testCollectBadTableKeyAddionalFields() { - let additionalFields = ["records": [["table": []]]] - let container = skyflow.container(type: ContainerType.COLLECT) - - let expectation = XCTestExpectation() - let callback = DemoAPICallback(expectation: expectation) - container?.collect(callback: callback, options: CollectOptions(tokens: true, additionalFields: additionalFields)) - - wait(for: [expectation], timeout: 20.0) - - XCTAssertEqual(callback.receivedResponse, ErrorCodes.INVALID_TABLE_NAME_TYPE(value: "0").getErrorObject(contextOptions: ContextOptions(interface: InterfaceName.COLLECT_CONTAINER)).localizedDescription) - } - + func testCollectEmptyTableAddionalFields() { - let additionalFields = ["records": [["table": ""]]] + let additionalFields = AdditionalFields(records: [AdditionalFieldsRecord(table: "", fields: ["field": "value"])]) let container = skyflow.container(type: ContainerType.COLLECT) - + let expectation = XCTestExpectation() let callback = DemoAPICallback(expectation: expectation) - container?.collect(callback: callback, options: CollectOptions(tokens: true, additionalFields: additionalFields)) - + container?.collect(callback: callback.asCollectCallback, options: CollectOptions(additionalFields: additionalFields)) + wait(for: [expectation], timeout: 20.0) - + XCTAssertEqual(callback.receivedResponse, ErrorCodes.EMPTY_TABLE_NAME().getErrorObject(contextOptions: ContextOptions(interface: InterfaceName.COLLECT_CONTAINER)).localizedDescription) } - - func testCollectNoFieldsKeyAddionalFields() { - let additionalFields = ["records": [["table": "table"]]] - let container = skyflow.container(type: ContainerType.COLLECT) - - let expectation = XCTestExpectation() - let callback = DemoAPICallback(expectation: expectation) - container?.collect(callback: callback, options: CollectOptions(tokens: true, additionalFields: additionalFields)) - - wait(for: [expectation], timeout: 20.0) - - XCTAssertEqual(callback.receivedResponse, ErrorCodes.FIELDS_KEY_ERROR(value: "0").getErrorObject(contextOptions: ContextOptions(interface: InterfaceName.COLLECT_CONTAINER)).localizedDescription) - } - - func testCollectInvalidFieldsAddionalFields() { - let additionalFields = ["records": [["table": "table", "fields": "fields"]]] - let container = skyflow.container(type: ContainerType.COLLECT) - - let expectation = XCTestExpectation() - let callback = DemoAPICallback(expectation: expectation) - container?.collect(callback: callback, options: CollectOptions(tokens: true, additionalFields: additionalFields)) - - wait(for: [expectation], timeout: 20.0) - - XCTAssertEqual(callback.receivedResponse, ErrorCodes.INVALID_FIELDS_TYPE(value: "0").getErrorObject(contextOptions: ContextOptions(interface: InterfaceName.COLLECT_CONTAINER)).localizedDescription) - } - + func testCollectEmptyFieldsAddionalFields() { - let additionalFields = ["records": [["table": "table", "fields": [:]]]] - let container = skyflow.container(type: ContainerType.COLLECT) - - let expectation = XCTestExpectation() - let callback = DemoAPICallback(expectation: expectation) - container?.collect(callback: callback, options: CollectOptions(tokens: true, additionalFields: additionalFields)) - - wait(for: [expectation], timeout: 20.0) - - XCTAssertEqual(callback.receivedResponse, ErrorCodes.EMPTY_FIELDS_KEY(value: "0").getErrorObject(contextOptions: ContextOptions(interface: InterfaceName.COLLECT_CONTAINER)).localizedDescription) - } - - func testCollectEmptyColumnNameForUpsertOption() { - let container = skyflow.container(type: ContainerType.COLLECT) - let upsertOptions = [["table": "card1"]] - let expectation = XCTestExpectation() - let callback = DemoAPICallback(expectation: expectation) - container?.collect(callback: callback, options: CollectOptions(tokens: true, upsert:upsertOptions)) - - wait(for: [expectation], timeout: 20.0) - - XCTAssertEqual(callback.receivedResponse, ErrorCodes.MISSING_COLUMN_NAME_IN_USERT_OPTION(value: "0").getErrorObject(contextOptions: ContextOptions(interface: InterfaceName.COLLECT_CONTAINER)).localizedDescription) - } - - - func testCollectEmptyTableNameForUpsertOption() { + let additionalFields = AdditionalFields(records: [AdditionalFieldsRecord(table: "table", fields: [:])]) let container = skyflow.container(type: ContainerType.COLLECT) - let upsertOptions = [["column": "person"]] + let expectation = XCTestExpectation() let callback = DemoAPICallback(expectation: expectation) - container?.collect(callback: callback, options: CollectOptions(tokens: true, upsert:upsertOptions)) + container?.collect(callback: callback.asCollectCallback, options: CollectOptions(additionalFields: additionalFields)) wait(for: [expectation], timeout: 20.0) - XCTAssertEqual(callback.receivedResponse, ErrorCodes.MISSING_TABLE_NAME_IN_USERT_OPTION(value: "0").getErrorObject(contextOptions: ContextOptions(interface: InterfaceName.COLLECT_CONTAINER)).localizedDescription) + XCTAssertEqual(callback.receivedResponse, ErrorCodes.EMPTY_FIELDS_KEY(value: "0").getErrorObject(contextOptions: ContextOptions(interface: InterfaceName.COLLECT_CONTAINER)).localizedDescription) } - - + func testCollectEmptyUpsertOption() { let container = skyflow.container(type: ContainerType.COLLECT) let expectation = XCTestExpectation() let callback = DemoAPICallback(expectation: expectation) - container?.collect(callback: callback, options: CollectOptions(tokens: true, upsert:[])) + container?.collect(callback: callback.asCollectCallback, options: CollectOptions(upsert:[])) wait(for: [expectation], timeout: 20.0) XCTAssertEqual(callback.receivedResponse, ErrorCodes.UPSERT_OPTION_CANNOT_BE_EMPTY().getErrorObject(contextOptions: ContextOptions(interface: InterfaceName.COLLECT_CONTAINER)).localizedDescription) } - func testCollectEmptyColumnNameUpsertOption() { + func testCollectEmptyUniqueColumnsUpsertOption() { let container = skyflow.container(type: ContainerType.COLLECT) - let upsertOptions = [["table": "card1", "column": ""]] + let upsertOptions = [UpsertOption(table: "card1", uniqueColumns: [])] let expectation = XCTestExpectation() let callback = DemoAPICallback(expectation: expectation) - container?.collect(callback: callback, options: CollectOptions(tokens: true, upsert:upsertOptions)) + container?.collect(callback: callback.asCollectCallback, options: CollectOptions(upsert:upsertOptions)) wait(for: [expectation], timeout: 20.0) - XCTAssertEqual(callback.receivedResponse, ErrorCodes.COLUMN_NAME_IS_EMPTY_FOR_ATLEAST_ONE_UPSERT_OPTION(value: "0").getErrorObject(contextOptions: ContextOptions(interface: InterfaceName.COLLECT_CONTAINER)).localizedDescription) + XCTAssertEqual(callback.receivedResponse, ErrorCodes.UNIQUE_COLUMNS_EMPTY_FOR_ATLEAST_ONE_UPSERT_OPTION(value: "0").getErrorObject(contextOptions: ContextOptions(interface: InterfaceName.COLLECT_CONTAINER)).localizedDescription) } func testCollectEmptyTableNameUpsertOption() { let container = skyflow.container(type: ContainerType.COLLECT) - let upsertOptions = [["table": "", "column": "person"]] + let upsertOptions = [UpsertOption(table: "", uniqueColumns: ["person"])] let expectation = XCTestExpectation() let callback = DemoAPICallback(expectation: expectation) - container?.collect(callback: callback, options: CollectOptions(tokens: true, upsert:upsertOptions)) + container?.collect(callback: callback.asCollectCallback, options: CollectOptions(upsert:upsertOptions)) wait(for: [expectation], timeout: 20.0) XCTAssertEqual(callback.receivedResponse, ErrorCodes.TABLE_NAME_IS_EMPTY_FOR_ATLEAST_ONE_UPSERT_OPTION(value: "0").getErrorObject(contextOptions: ContextOptions(interface: InterfaceName.COLLECT_CONTAINER)).localizedDescription) } - - - - func testInsertEmptyColumnNameForUpsertOption() { - let container = skyflow.container(type: ContainerType.COLLECT) - let upsertOptions = [["table": "card1"]] - let expectation = XCTestExpectation() - let records = [ - "records" : [[ - "table": "card1", - "fields": [ - "person" : "abcfgdyt", - "cvv" : "567" - ] - ]] - ] - let callback = DemoAPICallback(expectation: expectation) - let insertOptions = Skyflow.InsertOptions(tokens: false, upsert: upsertOptions) - self.skyflow?.insert(records: records, options: insertOptions, callback: callback) - wait(for: [expectation], timeout: 20.0) - - XCTAssertEqual(callback.receivedResponse, ErrorCodes.MISSING_COLUMN_NAME_IN_USERT_OPTION(value: "0").getErrorObject(contextOptions: ContextOptions(interface: InterfaceName.INSERT)).localizedDescription) - } - - - func testInsertEmptyTableNameForUpsertOption() { - let container = skyflow.container(type: ContainerType.COLLECT) - let upsertOptions = [["column": "person"]] - let expectation = XCTestExpectation() - let records = [ - "records" : [[ - "table": "card1", - "fields": [ - "person" : "abcfgdyt", - "cvv" : "567" - ] - ]] - ] - let callback = DemoAPICallback(expectation: expectation) - let insertOptions = Skyflow.InsertOptions(tokens: false, upsert: upsertOptions) - self.skyflow?.insert(records: records, options: insertOptions, callback: callback) - wait(for: [expectation], timeout: 20.0) - - XCTAssertEqual(callback.receivedResponse, ErrorCodes.MISSING_TABLE_NAME_IN_USERT_OPTION(value: "0").getErrorObject(contextOptions: ContextOptions(interface: InterfaceName.INSERT)).localizedDescription) - } - - func testInsertEmptyUpsertOption() { let container = skyflow.container(type: ContainerType.COLLECT) let expectation = XCTestExpectation() @@ -983,16 +830,16 @@ final class skyflow_iOS_collectTests: XCTestCase { ]] ] let callback = DemoAPICallback(expectation: expectation) - let insertOptions = Skyflow.InsertOptions(tokens: false, upsert: []) + let insertOptions = Skyflow.InsertOptions(upsert: []) self.skyflow?.insert(records: records, options: insertOptions, callback: callback) wait(for: [expectation], timeout: 20.0) XCTAssertEqual(callback.receivedResponse, ErrorCodes.UPSERT_OPTION_CANNOT_BE_EMPTY().getErrorObject(contextOptions: ContextOptions(interface: InterfaceName.INSERT)).localizedDescription) } - func testInsertEmptyColumnNameUpsertOption() { + func testInsertEmptyUniqueColumnsUpsertOption() { let container = skyflow.container(type: ContainerType.COLLECT) - let upsertOptions = [["table": "card1", "column": ""]] + let upsertOptions = [UpsertOption(table: "card1", uniqueColumns: [])] let expectation = XCTestExpectation() let records = [ "records" : [[ @@ -1004,16 +851,16 @@ final class skyflow_iOS_collectTests: XCTestCase { ]] ] let callback = DemoAPICallback(expectation: expectation) - let insertOptions = Skyflow.InsertOptions(tokens: false, upsert: upsertOptions) + let insertOptions = Skyflow.InsertOptions(upsert: upsertOptions) self.skyflow?.insert(records: records, options: insertOptions, callback: callback) wait(for: [expectation], timeout: 20.0) - XCTAssertEqual(callback.receivedResponse, ErrorCodes.COLUMN_NAME_IS_EMPTY_FOR_ATLEAST_ONE_UPSERT_OPTION(value: "0").getErrorObject(contextOptions: ContextOptions(interface: InterfaceName.INSERT)).localizedDescription) + XCTAssertEqual(callback.receivedResponse, ErrorCodes.UNIQUE_COLUMNS_EMPTY_FOR_ATLEAST_ONE_UPSERT_OPTION(value: "0").getErrorObject(contextOptions: ContextOptions(interface: InterfaceName.INSERT)).localizedDescription) } func testInsertEmptyTableNameUpsertOption() { let container = skyflow.container(type: ContainerType.COLLECT) - let upsertOptions = [["table": "", "column": "person"]] + let upsertOptions = [UpsertOption(table: "", uniqueColumns: ["person"])] let expectation = XCTestExpectation() let records = [ "records" : [[ @@ -1025,13 +872,13 @@ final class skyflow_iOS_collectTests: XCTestCase { ]] ] let callback = DemoAPICallback(expectation: expectation) - let insertOptions = Skyflow.InsertOptions(tokens: false, upsert: upsertOptions) + let insertOptions = Skyflow.InsertOptions(upsert: upsertOptions) self.skyflow?.insert(records: records, options: insertOptions, callback: callback) wait(for: [expectation], timeout: 20.0) XCTAssertEqual(callback.receivedResponse, ErrorCodes.TABLE_NAME_IS_EMPTY_FOR_ATLEAST_ONE_UPSERT_OPTION(value: "0").getErrorObject(contextOptions: ContextOptions(interface: InterfaceName.INSERT)).localizedDescription) } - + func testUnmount() { let container = skyflow.container(type: ContainerType.COLLECT) let date = container?.create(input: CollectElementInput(type: .EXPIRATION_DATE), options: CollectElementOptions(format: "test")) diff --git a/Tests/skyflow-iOS-composableTests/skyflow_iOS_composableEelementsTests.swift b/Tests/skyflow-iOS-composableTests/skyflow_iOS_composableEelementsTests.swift index a94cd297..19d80603 100644 --- a/Tests/skyflow-iOS-composableTests/skyflow_iOS_composableEelementsTests.swift +++ b/Tests/skyflow-iOS-composableTests/skyflow_iOS_composableEelementsTests.swift @@ -455,7 +455,7 @@ final class skyflow_iOS_composableEelementsTests: XCTestCase { } func testInsertEmptyTableNameForUpsertOption() { _ = skyflow.container(type: ContainerType.COMPOSABLE) - let upsertOptions = [["column": "person"]] + let upsertOptions = [UpsertOption(table: "", uniqueColumns: ["person"])] let expectation = XCTestExpectation() let records = [ "records" : [[ @@ -467,23 +467,24 @@ final class skyflow_iOS_composableEelementsTests: XCTestCase { ]] ] let callback = DemoAPICallback(expectation: expectation) - let insertOptions = Skyflow.InsertOptions(tokens: false, upsert: upsertOptions) + let insertOptions = Skyflow.InsertOptions(upsert: upsertOptions) self.skyflow?.insert(records: records, options: insertOptions, callback: callback) wait(for: [expectation], timeout: 20.0) - XCTAssertEqual(callback.receivedResponse, ErrorCodes.MISSING_TABLE_NAME_IN_USERT_OPTION(value: "0").getErrorObject(contextOptions: ContextOptions(interface: InterfaceName.INSERT)).localizedDescription) + XCTAssertEqual(callback.receivedResponse, ErrorCodes.TABLE_NAME_IS_EMPTY_FOR_ATLEAST_ONE_UPSERT_OPTION(value: "0").getErrorObject(contextOptions: ContextOptions(interface: InterfaceName.INSERT)).localizedDescription) } - func testCollectBadTableKeyAddionalFields() { - let additionalFields = ["records": [["table": []]]] - let container = skyflow.container(type: ContainerType.COMPOSABLE) - + + func testComposableCollectEmptyVaultURL() { + let clientWithEmptyURL = Client(Configuration(vaultID: "id", vaultURL: "", tokenProvider: DemoTokenProvider())) + let container = clientWithEmptyURL.container(type: ContainerType.COMPOSABLE) + let expectation = XCTestExpectation() let callback = DemoAPICallback(expectation: expectation) - container?.collect(callback: callback, options: CollectOptions(tokens: true, additionalFields: additionalFields)) - + container?.collect(callback: callback) + wait(for: [expectation], timeout: 20.0) - - XCTAssertEqual(callback.receivedResponse, ErrorCodes.INVALID_TABLE_NAME_TYPE(value: "0").getErrorObject(contextOptions: ContextOptions(interface: InterfaceName.COMPOSABLE_CONTAINER)).localizedDescription) + + XCTAssertEqual(callback.receivedResponse, ErrorCodes.EMPTY_VAULT_URL().getErrorObject(contextOptions: ContextOptions(interface: InterfaceName.COMPOSABLE_CONTAINER)).localizedDescription) } func testCreateRows() { let elements = [1, 2] diff --git a/Tests/skyflow-iOS-elementTests/skyflow_iOS_elementTests.swift b/Tests/skyflow-iOS-elementTests/skyflow_iOS_elementTests.swift index b6e84385..21a86795 100644 --- a/Tests/skyflow-iOS-elementTests/skyflow_iOS_elementTests.swift +++ b/Tests/skyflow-iOS-elementTests/skyflow_iOS_elementTests.swift @@ -26,7 +26,7 @@ class skyflow_iOS_elementTests: XCTestCase { textField = TextField(input: collectInput, options: collectOptions, contextOptions: ContextOptions(), elements: []) - let revealElementInput = RevealElementInput(token: "token", label: "RevealElement", redaction: .DEFAULT) + let revealElementInput = RevealElementInput(token: "token", label: "RevealElement") label = Label(input: revealElementInput, options: RevealElementOptions()) } diff --git a/Tests/skyflow-iOS-errorTests/DemoImpl.swift b/Tests/skyflow-iOS-errorTests/DemoImpl.swift index 71cb4d32..444ad3c3 100644 --- a/Tests/skyflow-iOS-errorTests/DemoImpl.swift +++ b/Tests/skyflow-iOS-errorTests/DemoImpl.swift @@ -41,13 +41,19 @@ public class DemoAPICallback: Callback { var receivedResponse: String = "" var expectation: XCTestExpectation var data: [String: Any] = [:] + var collectResponse: Skyflow.CollectResponse? + var revealResponse: Skyflow.RevealResponse? public init(expectation: XCTestExpectation) { self.expectation = expectation } public func onSuccess(_ responseBody: Any) { - if responseBody is String { + if let response = responseBody as? Skyflow.CollectResponse { + self.collectResponse = response + } else if let response = responseBody as? Skyflow.RevealResponse { + self.revealResponse = response + } else if responseBody is String { self.receivedResponse = responseBody as! String } else { @@ -66,4 +72,15 @@ public class DemoAPICallback: Callback { } expectation.fulfill() } + + // CollectContainer.collect(callback:)/RevealContainer.reveal(callback:) require the + // concrete Skyflow.CollectCallback/RevealCallback types - these wrap self so existing + // DemoAPICallback call sites only need a one-word change (.asCollectCallback/.asRevealCallback). + var asCollectCallback: Skyflow.CollectCallback { + Skyflow.CollectCallback(onSuccess: { self.onSuccess($0) }, onFailure: { self.onFailure($0) }) + } + + var asRevealCallback: Skyflow.RevealCallback { + Skyflow.RevealCallback(onSuccess: { self.onSuccess($0) }, onFailure: { self.onFailure($0) }) + } } diff --git a/Tests/skyflow-iOS-errorTests/Skyflow_iOS_collectErrorTests.swift b/Tests/skyflow-iOS-errorTests/Skyflow_iOS_collectErrorTests.swift index 3df67f16..813668ce 100644 --- a/Tests/skyflow-iOS-errorTests/Skyflow_iOS_collectErrorTests.swift +++ b/Tests/skyflow-iOS-errorTests/Skyflow_iOS_collectErrorTests.swift @@ -60,7 +60,7 @@ final class Skyflow_iOS_collectErrorTests: XCTestCase { let expectation = XCTestExpectation(description: "Pure insert call") let callback = DemoAPICallback(expectation: expectation) - skyflow.insert(records: payload, options: InsertOptions(tokens: true), callback: callback) + skyflow.insert(records: payload, options: InsertOptions(), callback: callback) wait(for: [expectation], timeout: 10.0) let responseData = callback.receivedResponse.utf8 @@ -74,7 +74,7 @@ final class Skyflow_iOS_collectErrorTests: XCTestCase { let expectation = XCTestExpectation(description: "Pure insert call") let callback = DemoAPICallback(expectation: expectation) - skyflow.insert(records: payload, options: InsertOptions(tokens: true), callback: callback) + skyflow.insert(records: payload, options: InsertOptions(), callback: callback) wait(for: [expectation], timeout: 10.0) let responseData = callback.receivedResponse.utf8 @@ -92,7 +92,7 @@ final class Skyflow_iOS_collectErrorTests: XCTestCase { let expectation = XCTestExpectation(description: "Pure insert call") let callback = DemoAPICallback(expectation: expectation) - skyflow.insert(records: payload, options: InsertOptions(tokens: true), callback: callback) + skyflow.insert(records: payload, options: InsertOptions(), callback: callback) wait(for: [expectation], timeout: 10.0) let responseData = callback.receivedResponse.utf8 @@ -112,7 +112,7 @@ final class Skyflow_iOS_collectErrorTests: XCTestCase { let expectation = XCTestExpectation(description: "Pure insert call") let callback = DemoAPICallback(expectation: expectation) - skyflow.insert(records: payload, options: InsertOptions(tokens: true), callback: callback) + skyflow.insert(records: payload, options: InsertOptions(), callback: callback) wait(for: [expectation], timeout: 10.0) let responseData = callback.receivedResponse.utf8 @@ -131,7 +131,7 @@ final class Skyflow_iOS_collectErrorTests: XCTestCase { let expectation = XCTestExpectation(description: "Pure insert call") let callback = DemoAPICallback(expectation: expectation) - skyflow.insert(records: payload, options: InsertOptions(tokens: true), callback: callback) + skyflow.insert(records: payload, options: InsertOptions(), callback: callback) wait(for: [expectation], timeout: 10.0) let responseData = callback.receivedResponse.utf8 @@ -151,7 +151,7 @@ final class Skyflow_iOS_collectErrorTests: XCTestCase { let expectation = XCTestExpectation(description: "Pure insert call") let callback = DemoAPICallback(expectation: expectation) - skyflow.insert(records: payload, options: InsertOptions(tokens: true), callback: callback) + skyflow.insert(records: payload, options: InsertOptions(), callback: callback) wait(for: [expectation], timeout: 10.0) let responseData = callback.receivedResponse.utf8 @@ -184,7 +184,7 @@ final class Skyflow_iOS_collectErrorTests: XCTestCase { let callback = DemoAPICallback(expectation: expectation) - container?.collect(callback: callback) + container?.collect(callback: callback.asCollectCallback) wait(for: [expectation], timeout: 10.0) @@ -219,7 +219,7 @@ final class Skyflow_iOS_collectErrorTests: XCTestCase { let callback = DemoAPICallback(expectation: expectation) - container?.collect(callback: callback) + container?.collect(callback: callback.asCollectCallback) wait(for: [expectation], timeout: 10.0) @@ -244,7 +244,7 @@ final class Skyflow_iOS_collectErrorTests: XCTestCase { let callback = DemoAPICallback(expectation: expectation) - container?.collect(callback: callback) + container?.collect(callback: callback.asCollectCallback) wait(for: [expectation], timeout: 10.0) @@ -267,7 +267,7 @@ final class Skyflow_iOS_collectErrorTests: XCTestCase { window.addSubview(cvv!) let expectation = XCTestExpectation(description: "Container insert call - Duplicate Elements") let callback = DemoAPICallback(expectation: expectation) - CollectRequestBody.createRequestBody(elements: [cardNumber!, cvv!], callback: callback, contextOptions: ContextOptions(interface: .COLLECT_CONTAINER)) + FlowVaultCollectRequestBody.createRequestBody(elements: [cardNumber!, cvv!], callback: callback, contextOptions: ContextOptions(interface: .COLLECT_CONTAINER)) wait(for: [expectation], timeout: 10.0) let responseData = callback.receivedResponse @@ -290,21 +290,16 @@ final class Skyflow_iOS_collectErrorTests: XCTestCase { let expectation = XCTestExpectation(description: "Container insert call - All valid") let callback = DemoAPICallback(expectation: expectation) - let fields: [String: Any] = [ - "records": [[ - "table": "persons", - "fields": [ - "cvv": "123", - "name": "John Doe" - ]] - ]] - CollectRequestBody.createRequestBody(elements: [cardNumber!, cvv!], additionalFields: fields,callback: callback, contextOptions: ContextOptions(interface: .COLLECT_CONTAINER)) + let fields = AdditionalFields(records: [ + AdditionalFieldsRecord(table: "persons", fields: ["cvv": "123", "name": "John Doe"]) + ]) + FlowVaultCollectRequestBody.createRequestBody(elements: [cardNumber!, cvv!], additionalFields: fields,callback: callback, contextOptions: ContextOptions(interface: .COLLECT_CONTAINER)) wait(for: [expectation], timeout: 10.0) - + let responseData = callback.receivedResponse XCTAssertEqual(responseData, ErrorCodes.DUPLICATE_ELEMENT_FOUND(values: ["cvv", "persons"]).description) } - + func testCreateRequestBodyDuplicateInAdditionalFields() { let window = UIWindow() let container = skyflow.container(type: ContainerType.COLLECT, options: nil) @@ -321,22 +316,13 @@ final class Skyflow_iOS_collectErrorTests: XCTestCase { let expectation = XCTestExpectation(description: "Container insert call - All valid") let callback = DemoAPICallback(expectation: expectation) - let fields: [String: Any] = [ - "records": [[ - "table": "persons", - "fields": [ - "duplicate": "123", - "name": "John Doe" - ]], - [ - "table": "persons", - "fields": [ - "duplicate": "123", - ]] - ]] - CollectRequestBody.createRequestBody(elements: [cardNumber!, cvv!], additionalFields: fields,callback: callback, contextOptions: ContextOptions(interface: .COLLECT_CONTAINER)) + let fields = AdditionalFields(records: [ + AdditionalFieldsRecord(table: "persons", fields: ["duplicate": "123", "name": "John Doe"]), + AdditionalFieldsRecord(table: "persons", fields: ["duplicate": "123"]) + ]) + FlowVaultCollectRequestBody.createRequestBody(elements: [cardNumber!, cvv!], additionalFields: fields,callback: callback, contextOptions: ContextOptions(interface: .COLLECT_CONTAINER)) wait(for: [expectation], timeout: 10.0) - + let responseData = callback.receivedResponse XCTAssertEqual(responseData, ErrorCodes.DUPLICATE_ADDITIONAL_FIELD_FOUND(value: "duplicate").description) } @@ -356,25 +342,17 @@ final class Skyflow_iOS_collectErrorTests: XCTestCase { let expectation = XCTestExpectation(description: "Container insert call - All valid") let callback = DemoAPICallback(expectation: expectation) - let fields: [String: Any] = [ - "records": [[ - "table": "persons", - "fields": [ - "cvv": "123", - ]], - [ - "table": "persons", - "fields": [ - "duplicate": "123", - ]] - ]] - CollectRequestBody.createRequestBody(elements: [cardNumber!, cvv!], additionalFields: fields,callback: callback, contextOptions: ContextOptions(interface: .COLLECT_CONTAINER)) + let fields = AdditionalFields(records: [ + AdditionalFieldsRecord(table: "persons", fields: ["cvv": "123"]), + AdditionalFieldsRecord(table: "persons", fields: ["duplicate": "123"]) + ]) + FlowVaultCollectRequestBody.createRequestBody(elements: [cardNumber!, cvv!], additionalFields: fields,callback: callback, contextOptions: ContextOptions(interface: .COLLECT_CONTAINER)) wait(for: [expectation], timeout: 10.0) - + let responseData = callback.receivedResponse XCTAssertEqual(responseData, ErrorCodes.DUPLICATE_ELEMENT_FOUND(values: ["cvv", "persons"]).description) } - + func testCreateRequestBodyWithOnlyInserts() { let window = UIWindow() let container = skyflow.container(type: ContainerType.COLLECT, options: nil) @@ -389,112 +367,323 @@ final class Skyflow_iOS_collectErrorTests: XCTestCase { cvv?.textField.secureText = "211" window.addSubview(cvv!) let callback = DemoAPICallback(expectation: XCTestExpectation(description: "Insert only")) - let requestBody = CollectRequestBody.createRequestBody(elements: [cardNumber!, cvv!], callback: callback, contextOptions: ContextOptions()) + let requestBody = FlowVaultCollectRequestBody.createRequestBody(elements: [cardNumber!, cvv!], callback: callback, contextOptions: ContextOptions()) XCTAssertNotNil(requestBody) XCTAssertNotNil(requestBody?["records"]) XCTAssertEqual(requestBody?["records"] as! [NSDictionary], [ ["table": "persons", "fields": ["card_number": "", "cvv": ""]] ]) - XCTAssertEqual(requestBody?["update"] as? [String: String], [:]) + // Every plain CollectElementInput defaults to skyflowId: "" (not nil) - confirms that + // default doesn't accidentally get treated as "update targeting an empty ID". + XCTAssertTrue((requestBody?["update"] as! [String: Any]).isEmpty) } - func testCreateRequestBodyWithOnlyUpdates() { - let additionalFields: [String: Any] = [ - "records": [ - ["table": "table1", "fields": ["column1": "value1"], "skyflowID": "id1"], - ["table": "table1", "fields": ["column2": "value2"], "skyflowID": "id1"] - ] - ] - let callback = DemoAPICallback(expectation: XCTestExpectation(description: "Update only")) - let requestBody = CollectRequestBody.createRequestBody(elements: [], additionalFields: additionalFields, callback: callback, contextOptions: ContextOptions()) + func testCreateRequestBodyExplicitEmptyStringSkyflowIdOnElementFallsBackToInsert() { + let window = UIWindow() + let container = skyflow.container(type: ContainerType.COLLECT, options: nil) + let options = CollectElementOptions(required: false) + let collectInput = CollectElementInput(table: "persons", column: "card_number", placeholder: "card number", type: .CARD_NUMBER, skyflowId: "") + let cardNumber = container?.create(input: collectInput, options: options) + cardNumber?.textField.secureText = "4111 1111 1111 1111" + window.addSubview(cardNumber!) + + let callback = DemoAPICallback(expectation: XCTestExpectation(description: "Explicit empty-string skyflowId falls back to insert")) + let requestBody = FlowVaultCollectRequestBody.createRequestBody(elements: [cardNumber!], callback: callback, contextOptions: ContextOptions()) + + XCTAssertNotNil(requestBody) + let records = requestBody?["records"] as! [[String: Any]] + XCTAssertEqual(records.count, 1) + XCTAssertEqual(records[0]["table"] as! String, "persons") + + let update = requestBody?["update"] as! [String: Any] + XCTAssertTrue(update.isEmpty) + } + + func testCreateRequestBodyWithSkyflowIDInAdditionalFields() { + let additionalFields = AdditionalFields(records: [ + AdditionalFieldsRecord(table: "table1", fields: ["column1": "value1"], skyflowId: "id1") + ]) + let callback = DemoAPICallback(expectation: XCTestExpectation(description: "Update via additionalFields")) + let requestBody = FlowVaultCollectRequestBody.createRequestBody(elements: [], additionalFields: additionalFields, callback: callback, contextOptions: ContextOptions()) + XCTAssertNotNil(requestBody) + XCTAssertEqual((requestBody?["records"] as! [[String: Any]]).count, 0) + let update = requestBody?["update"] as! [String: Any] + let entry = update["id1"] as! [String: Any] + XCTAssertEqual(entry["table"] as! String, "table1") + XCTAssertEqual(entry["fields"] as! [String: String], ["column1": "value1"]) + } + + func testCreateRequestBodyEmptyStringSkyflowIdOnAdditionalFieldsFallsBackToInsert() { + // Matches element-based skyflowId handling: an empty string is treated the same as + // absent (falls back to a plain insert), rather than creating an update targeting "". + let additionalFields = AdditionalFields(records: [ + AdditionalFieldsRecord(table: "table1", fields: ["column1": "value1"], skyflowId: "") + ]) + let callback = DemoAPICallback(expectation: XCTestExpectation(description: "Empty skyflowId falls back to insert")) + let requestBody = FlowVaultCollectRequestBody.createRequestBody(elements: [], additionalFields: additionalFields, callback: callback, contextOptions: ContextOptions()) + + XCTAssertNotNil(requestBody) + let records = requestBody?["records"] as! [[String: Any]] + XCTAssertEqual(records.count, 1) + XCTAssertEqual(records[0]["table"] as! String, "table1") + + let update = requestBody?["update"] as! [String: Any] + XCTAssertTrue(update.isEmpty) + } + + func testCreateRequestBodyMergesMultipleAdditionalFieldsSharingSkyflowId() { + // Two additionalFields entries that both target the same skyflowId should merge their + // fields into a single update payload entry, not clobber or duplicate it. + let additionalFields = AdditionalFields(records: [ + AdditionalFieldsRecord(table: "persons", fields: ["name": "John"], skyflowId: "id1"), + AdditionalFieldsRecord(table: "persons", fields: ["email": "john@example.com"], skyflowId: "id1") + ]) + let callback = DemoAPICallback(expectation: XCTestExpectation(description: "Merge additionalFields sharing a skyflowId")) + let requestBody = FlowVaultCollectRequestBody.createRequestBody(elements: [], additionalFields: additionalFields, callback: callback, contextOptions: ContextOptions()) + XCTAssertNotNil(requestBody) - XCTAssertNotNil(requestBody?["update"]) - XCTAssertEqual(requestBody?["update"] as? NSDictionary, ["id1": ["table": "table1", "fields": ["column1": "value1", "column2": "value2"], "skyflowID": "id1"]]) - XCTAssertEqual(requestBody?["records"] as? [[String: String]], []) + let update = requestBody?["update"] as! [String: Any] + XCTAssertEqual(update.count, 1) + let entry = update["id1"] as! [String: Any] + XCTAssertEqual(entry["table"] as! String, "persons") + let fields = entry["fields"] as! [String: String] + XCTAssertEqual(fields["name"], "John") + XCTAssertEqual(fields["email"], "john@example.com") } - func testCreateRequestBodyWithOnlyUpdates2() { + func testCreateRequestBodyWithSkyflowIDOnElement() { let window = UIWindow() let container = skyflow.container(type: ContainerType.COLLECT, options: nil) let options = CollectElementOptions(required: false) - let collectInput1 = CollectElementInput(table: "persons", column: "card_number", placeholder: "card number", type: .CARD_NUMBER) + let collectInput1 = CollectElementInput(table: "persons", column: "card_number", placeholder: "card number", type: .CARD_NUMBER, skyflowId: "id1") let cardNumber = container?.create(input: collectInput1, options: options) cardNumber?.textField.secureText = "4111 1111 1111 1111" window.addSubview(cardNumber!) - - let collectInput2 = CollectElementInput(table: "persons", column: "cvv", placeholder: "cvv", type: .CVV) - let cvv = container?.create(input: collectInput2, options: options) - cvv?.textField.secureText = "211" - window.addSubview(cvv!) - - let callback = DemoAPICallback(expectation: XCTestExpectation(description: "Insert only")) - let requestBody = CollectRequestBody.createRequestBody(elements: [cardNumber!, cvv!], callback: callback, contextOptions: ContextOptions()) + + let callback = DemoAPICallback(expectation: XCTestExpectation(description: "Update via element skyflowID")) + let requestBody = FlowVaultCollectRequestBody.createRequestBody(elements: [cardNumber!], callback: callback, contextOptions: ContextOptions()) XCTAssertNotNil(requestBody) - XCTAssertNotNil(requestBody?["records"]) - XCTAssertEqual(requestBody?["records"] as! [NSDictionary], [ - ["table": "persons", "fields": ["card_number": "", "cvv": ""]] + XCTAssertEqual((requestBody?["records"] as! [[String: Any]]).count, 0) + let update = requestBody?["update"] as! [String: Any] + let entry = update["id1"] as! [String: Any] + XCTAssertEqual(entry["table"] as! String, "persons") + XCTAssertEqual(entry["fields"] as! [String: String], ["card_number": ""]) + } + + func testCreateRequestBodyMixedInsertAndUpdateElements() { + // One plain element (no skyflowId) and one update-by-skyflowId element in the same + // collect() call - the plain one should land in "records", the tagged one in "update", + // without either bucket interfering with the other. + let window = UIWindow() + let container = skyflow.container(type: ContainerType.COLLECT, options: nil) + let options = CollectElementOptions(required: false) + + let insertInput = CollectElementInput(table: "cards", column: "card_number", placeholder: "card number", type: .CARD_NUMBER) + let insertElement = container?.create(input: insertInput, options: options) + insertElement?.textField.secureText = "4111 1111 1111 1111" + window.addSubview(insertElement!) + + let updateInput = CollectElementInput(table: "persons", column: "name", placeholder: "name", type: .CARDHOLDER_NAME, skyflowId: "id1") + let updateElement = container?.create(input: updateInput, options: options) + updateElement?.textField.secureText = "John Doe" + window.addSubview(updateElement!) + + let callback = DemoAPICallback(expectation: XCTestExpectation(description: "Mixed insert and update elements")) + let requestBody = FlowVaultCollectRequestBody.createRequestBody(elements: [insertElement!, updateElement!], callback: callback, contextOptions: ContextOptions()) + + XCTAssertNotNil(requestBody) + let records = requestBody?["records"] as! [[String: Any]] + XCTAssertEqual(records.count, 1) + XCTAssertEqual(records[0]["table"] as! String, "cards") + + let update = requestBody?["update"] as! [String: Any] + XCTAssertEqual(update.count, 1) + let updateEntry = update["id1"] as! [String: Any] + XCTAssertEqual(updateEntry["table"] as! String, "persons") + } + + func testCreateRequestBodyPlainInsertViaAdditionalFieldsCombinedWithElement() { + // additionalFields without a skyflowId is a plain insert, same as a mounted element - + // both should end up in "records" together, none in "update". + let window = UIWindow() + let container = skyflow.container(type: ContainerType.COLLECT, options: nil) + let options = CollectElementOptions(required: false) + + let insertInput = CollectElementInput(table: "cards", column: "card_number", placeholder: "card number", type: .CARD_NUMBER) + let insertElement = container?.create(input: insertInput, options: options) + insertElement?.textField.secureText = "4111 1111 1111 1111" + window.addSubview(insertElement!) + + let additionalFields = AdditionalFields(records: [ + AdditionalFieldsRecord(table: "accounts", fields: ["status": "active"]) ]) - XCTAssertEqual(requestBody?["update"] as? [String: String], [:]) + + let callback = DemoAPICallback(expectation: XCTestExpectation(description: "Element insert + additionalFields insert")) + let requestBody = FlowVaultCollectRequestBody.createRequestBody(elements: [insertElement!], additionalFields: additionalFields, callback: callback, contextOptions: ContextOptions()) + + XCTAssertNotNil(requestBody) + let records = requestBody?["records"] as! [[String: Any]] + XCTAssertEqual(records.count, 2) + XCTAssertTrue(records.contains { $0["table"] as? String == "cards" }) + XCTAssertTrue(records.contains { $0["table"] as? String == "accounts" }) + + let update = requestBody?["update"] as! [String: Any] + XCTAssertTrue(update.isEmpty) } - func testCreateRequestBodyWithOnlyUpdates3() { + func testCreateRequestBodyElementUpdateWithAdditionalFieldsInsert() { + // An update-by-skyflowId element combined with a plain-insert additionalFields record - + // "records" should only contain the additionalFields insert, "update" only the element. let window = UIWindow() let container = skyflow.container(type: ContainerType.COLLECT, options: nil) let options = CollectElementOptions(required: false) - let collectInput1 = CollectElementInput(table: "persons", column: "card_number", placeholder: "card number", type: .CARD_NUMBER, skyflowID: "id1") - let cardNumber = container?.create(input: collectInput1, options: options) - cardNumber?.textField.secureText = "4111 1111 1111 1111" - window.addSubview(cardNumber!) - - let collectInput2 = CollectElementInput(table: "persons", column: "cvv", placeholder: "cvv", type: .CVV, skyflowID: "id2") - let cvv = container?.create(input: collectInput2, options: options) - cvv?.textField.secureText = "211" - window.addSubview(cvv!) - - let additionalFields: [String: Any] = [ - "records": [ - ["table": "table1", "fields": ["column1": "value1"], "skyflowID": "id1"], - ["table": "table1", "fields": ["column2": "value2"], "skyflowID": "id1"] - ] - ] - let callback = DemoAPICallback(expectation: XCTestExpectation(description: "Update only")) - let requestBody = CollectRequestBody.createRequestBody(elements: [cardNumber!, cvv!], additionalFields: additionalFields, callback: callback, contextOptions: ContextOptions()) + + let updateInput = CollectElementInput(table: "persons", column: "name", placeholder: "name", type: .CARDHOLDER_NAME, skyflowId: "id1") + let updateElement = container?.create(input: updateInput, options: options) + updateElement?.textField.secureText = "John Doe" + window.addSubview(updateElement!) + + let additionalFields = AdditionalFields(records: [ + AdditionalFieldsRecord(table: "accounts", fields: ["status": "active"]) + ]) + + let callback = DemoAPICallback(expectation: XCTestExpectation(description: "Element update + additionalFields insert")) + let requestBody = FlowVaultCollectRequestBody.createRequestBody(elements: [updateElement!], additionalFields: additionalFields, callback: callback, contextOptions: ContextOptions()) + XCTAssertNotNil(requestBody) - XCTAssertNotNil(requestBody?["update"]) - XCTAssertEqual(requestBody?["update"] as? NSDictionary, ["id1": ["table": "table1", "fields": ["column1": "value1", "column2": "value2", "card_number": ""], "skyflowID": "id1"], "id2": ["table": "persons", "fields": ["cvv": ""], "skyflowID": "id2"]]) - XCTAssertEqual(requestBody?["records"] as? [[String: String]], []) + let records = requestBody?["records"] as! [[String: Any]] + XCTAssertEqual(records.count, 1) + XCTAssertEqual(records[0]["table"] as! String, "accounts") + + let update = requestBody?["update"] as! [String: Any] + XCTAssertEqual(update.count, 1) + XCTAssertEqual((update["id1"] as! [String: Any])["table"] as! String, "persons") } - func testCreateRequestBodyWithMixedInsertsAndUpdates() { + func testFullCombinationCollectUpdateUpsertAndAdditionalFields() { + // The kitchen-sink scenario: an insert element, an update-by-skyflowId element, an + // insert additionalFields record, and an update additionalFields record, all in one + // request - then upsert applied on top. Upsert only ever touches FlowVaultInsertRequestBody's + // "records" bucket (confirmed: FlowVaultUpdateRequestBody never reads options.upsert at + // all), so it should only decorate the matching insert record and leave "update" untouched. let window = UIWindow() let container = skyflow.container(type: ContainerType.COLLECT, options: nil) let options = CollectElementOptions(required: false) - let collectInput1 = CollectElementInput(table: "persons", column: "card_number", placeholder: "card number", type: .CARD_NUMBER) - let cardNumber = container?.create(input: collectInput1, options: options) - cardNumber?.textField.secureText = "4111 1111 1111 1111" - window.addSubview(cardNumber!) - - let collectInput2 = CollectElementInput(table: "persons", column: "cvv", placeholder: "cvv", type: .CVV, skyflowID: "id2") - let cvv = container?.create(input: collectInput2, options: options) - cvv?.textField.secureText = "211" - window.addSubview(cvv!) - - let additionalFields: [String: Any] = [ - "records": [ - ["table": "table1", "fields": ["column3": "value3"], "skyflowID": "id1"] + + let insertInput = CollectElementInput(table: "cards", column: "card_number", placeholder: "card number", type: .CARD_NUMBER) + let insertElement = container?.create(input: insertInput, options: options) + insertElement?.textField.secureText = "4111 1111 1111 1111" + window.addSubview(insertElement!) + + let updateInput = CollectElementInput(table: "persons", column: "name", placeholder: "name", type: .CARDHOLDER_NAME, skyflowId: "id1") + let updateElement = container?.create(input: updateInput, options: options) + updateElement?.textField.secureText = "John Doe" + window.addSubview(updateElement!) + + let additionalFields = AdditionalFields(records: [ + AdditionalFieldsRecord(table: "accounts", fields: ["status": "active"]), + AdditionalFieldsRecord(table: "billing", fields: ["zip": "94105"], skyflowId: "id2") + ]) + + let callback = DemoAPICallback(expectation: XCTestExpectation(description: "Full combination")) + let requestBody = FlowVaultCollectRequestBody.createRequestBody(elements: [insertElement!, updateElement!], additionalFields: additionalFields, callback: callback, contextOptions: ContextOptions()) + XCTAssertNotNil(requestBody) + + let records = requestBody?["records"] as! [[String: Any]] + XCTAssertEqual(records.count, 2) + XCTAssertTrue(records.contains { $0["table"] as? String == "cards" }) + XCTAssertTrue(records.contains { $0["table"] as? String == "accounts" }) + + let update = requestBody?["update"] as! [String: Any] + XCTAssertEqual(update.count, 2) + XCTAssertEqual((update["id1"] as! [String: Any])["table"] as! String, "persons") + XCTAssertEqual((update["id2"] as! [String: Any])["table"] as! String, "billing") + + // Apply upsert on top, matching only the "cards" table. + let upsertOptions = [UpsertOption(table: "cards", uniqueColumns: ["card_number"], updateType: .UPDATE)] + let wireBody = FlowVaultInsertRequestBody.createRequestBody(vaultID: "vault123", records: requestBody!, options: FlowVaultICOptions(upsert: upsertOptions)) + let wireRecords = wireBody["records"] as! [[String: Any]] + + let cardsWireRecord = wireRecords.first { $0["tableName"] as? String == "cards" } + XCTAssertNotNil(cardsWireRecord?["upsert"]) + + let accountsWireRecord = wireRecords.first { $0["tableName"] as? String == "accounts" } + XCTAssertNil(accountsWireRecord?["upsert"]) + + // update bucket is a completely separate path (FlowVaultUpdateRequestBody) - upsert has + // no way to reach or affect it, confirmed structurally since FlowVaultInsertRequestBody + // only ever reads requestBody["records"]. + XCTAssertEqual(update.count, 2) + } + + // Not currently reachable from any documented flow (README only shows ElementValueMatchRule + // used to allow a duplicate plain/insert element, not two update-by-skyflowId elements), but + // the code path exists in FlowVaultCollectRequestBody's update branch too - covering it now + // in case a future update UI (e.g. confirm-password-style re-entry on an editable field) relies on it. + func testCreateRequestBodyElementValueMatchRuleBypassesUpdateDuplicate() { + let window = UIWindow() + let container = skyflow.container(type: ContainerType.COLLECT, options: nil) + let options = CollectElementOptions(required: false) + + let updateInput1 = CollectElementInput(table: "persons", column: "name", placeholder: "name", type: .CARDHOLDER_NAME, skyflowId: "id1") + let updateElement1 = container?.create(input: updateInput1, options: options) + updateElement1?.textField.secureText = "John" + updateElement1?.textFieldDidEndEditing(updateElement1!.textField) + window.addSubview(updateElement1!) + + var vs = ValidationSet() + vs.add(rule: ElementValueMatchRule(element: updateElement1!, error: "ELEMENT NOT MATCHING")) + let updateInput2 = CollectElementInput(table: "persons", column: "name", placeholder: "name", type: .CARDHOLDER_NAME, validations: vs, skyflowId: "id1") + let updateElement2 = container?.create(input: updateInput2, options: options) + updateElement2?.textField.secureText = "Jane" + updateElement2?.textFieldDidEndEditing(updateElement2!.textField) + window.addSubview(updateElement2!) + + let callback = DemoAPICallback(expectation: XCTestExpectation(description: "Should not fail")) + let requestBody = FlowVaultCollectRequestBody.createRequestBody(elements: [updateElement1!, updateElement2!], callback: callback, contextOptions: ContextOptions()) + + XCTAssertNotNil(requestBody) + let update = requestBody?["update"] as! [String: Any] + XCTAssertEqual(update.count, 1) + let entry = update["id1"] as! [String: Any] + // Second element's value is skipped (continue), not merged over the first's. + XCTAssertEqual((entry["fields"] as! [String: String])["name"], "John") + } + + func testInsertEmptyVaultURL() { + let expectation = XCTestExpectation(description: "Insert with empty vaultURL should fail") + let callback = DemoAPICallback(expectation: expectation) + let clientWithEmptyURL = Client(Configuration(vaultID: "id", vaultURL: "", tokenProvider: DemoTokenProvider())) + + clientWithEmptyURL.insert(records: ["records": [["table": "table", "fields": ["field": "value"]]]], callback: callback) + + wait(for: [expectation], timeout: 10.0) + XCTAssertEqual(callback.receivedResponse, ErrorCodes.EMPTY_VAULT_URL().getErrorObject(contextOptions: ContextOptions(interface: .INSERT)).localizedDescription) + } + + func testCollectCallbackOnFailureParsesStructuredAPIError() { + let wholeRequestFailure: [String: Any] = [ + "error": [ + "grpcCode": 13, + "httpCode": 500, + "message": "Skyflow services experienced an internal error. Contact Skyflow support with request ID 2db2c594-b9a5-48db-a220-f936e12a43e9 for more information.", + "httpStatus": "Internal Server Error", + "details": [] ] ] - let callback = DemoAPICallback(expectation: XCTestExpectation(description: "Mixed inserts and updates")) - let requestBody = CollectRequestBody.createRequestBody(elements: [cardNumber!, cvv!], additionalFields: additionalFields, callback: callback, contextOptions: ContextOptions()) - XCTAssertNotNil(requestBody) - XCTAssertNotNil(requestBody?["records"]) - let records = requestBody?["records"] as? [[String: Any]] - XCTAssertEqual(records?.count, 1) - XCTAssertEqual(records?[0] as! NSDictionary as NSDictionary, ["table": "persons", "fields": ["card_number": ""]]) - XCTAssertNotNil(requestBody?["update"]) - let update = requestBody?["update"] as? [String: Any] - let updateRecords = update?["id1"] as! [String: Any] - XCTAssertEqual(updateRecords as NSDictionary, ["table": "table1", "fields": ["column3": "value3"], "skyflowID": "id1"]) + var receivedError: SkyflowError? + let collectCallback = CollectCallback( + onSuccess: { _ in XCTFail("onSuccess should not be called") }, + onFailure: { error in receivedError = error } + ) + + collectCallback.onFailure(wholeRequestFailure) + + XCTAssertEqual(receivedError?.httpCode, 500) + XCTAssertEqual(receivedError?.message, "Skyflow services experienced an internal error. Contact Skyflow support with request ID 2db2c594-b9a5-48db-a220-f936e12a43e9 for more information.") + XCTAssertEqual(receivedError?.grpcCode, 13) + XCTAssertEqual(receivedError?.httpStatus, "Internal Server Error") + XCTAssertEqual(receivedError?.details?.count, 0) } } diff --git a/Tests/skyflow-iOS-errorTests/Skyflow_iOS_generalErrorTests.swift b/Tests/skyflow-iOS-errorTests/Skyflow_iOS_generalErrorTests.swift index c5dfc98d..1c3e52e9 100644 --- a/Tests/skyflow-iOS-errorTests/Skyflow_iOS_generalErrorTests.swift +++ b/Tests/skyflow-iOS-errorTests/Skyflow_iOS_generalErrorTests.swift @@ -69,14 +69,6 @@ class Skyflow_iOS_generalErrorTests: XCTestCase { } - func testGetByIDRecord() { - let record = GetByIdRecord(ids: ["id1", "id2"], table: "table", redaction: "DEFAULT") - - XCTAssertEqual(record.ids, ["id1", "id2"]) - XCTAssertEqual("table", record.table) - XCTAssertEqual(record.redaction, "DEFAULT") - } - func testValidationSet() { let validationSet = ValidationSet( rules: [SkyflowValidateCardNumber( @@ -146,8 +138,63 @@ class Skyflow_iOS_generalErrorTests: XCTestCase { func testIsTokenValid() { let apiClient = APIClient(vaultID: "", vaultURL: "", tokenProvider: DemoTokenProvider()) let expectation = XCTestExpectation(description: "should get token") - + XCTAssertEqual(false, apiClient.isTokenValid()) } + + func testSkyflowErrorWrapReturnsExistingInstanceUnchanged() { + let original = SkyflowError(domain: "TestDomain", code: 123, userInfo: [NSLocalizedDescriptionKey: "original message"]) + let wrapped = SkyflowError.wrap(original) + XCTAssertTrue(wrapped === original) + } + + func testSkyflowErrorWrapPreservesPlainNSError() { + let nsError = NSError(domain: "NSURLErrorDomain", code: -1009, userInfo: [NSLocalizedDescriptionKey: "The Internet connection appears to be offline."]) + let wrapped = SkyflowError.wrap(nsError) + + XCTAssertEqual(wrapped.domain, "NSURLErrorDomain") + XCTAssertEqual(wrapped.httpCode, -1009) + XCTAssertEqual(wrapped.message, "The Internet connection appears to be offline.") + XCTAssertNil(wrapped.grpcCode) + XCTAssertNil(wrapped.httpStatus) + XCTAssertNil(wrapped.details) + } + + func testSkyflowErrorWrapFallsBackForOpaqueValue() { + // Anything that isn't a SkyflowError, the structured {"error": {...}} API shape, an + // NSError, or the internal {"errors": [...]} wrapping still needs to produce *something* + // usable rather than crash. + let wrapped = SkyflowError.wrap("just a plain string, not an error at all") + XCTAssertEqual(wrapped.httpCode, 0) + XCTAssertTrue(wrapped.message.contains("just a plain string")) + } + + func testSkyflowAPIErrorInitMissingOptionalFields() { + let apiError: [String: Any] = [ + "error": ["httpCode": 500, "message": "Internal error"] + ] + let skyflowError = SkyflowError(apiError: apiError) + + XCTAssertEqual(skyflowError?.httpCode, 500) + XCTAssertEqual(skyflowError?.message, "Internal error") + XCTAssertNil(skyflowError?.grpcCode) + XCTAssertNil(skyflowError?.httpStatus) + XCTAssertNil(skyflowError?.details) + } + + func testSkyflowAPIErrorInitReturnsNilForMalformedInput() { + XCTAssertNil(SkyflowError(apiError: "not a dictionary")) + XCTAssertNil(SkyflowError(apiError: ["message": "no nested error key"])) + } + + func testSkyflowErrorWrapRecoversNSErrorFromNestedErrorsArray() { + // Mirrors what FlowVaultRevealAPICallback.callRevealOnFailure produces for a + // whole-request failure reaching Client.detokenize() directly (no RevealValueCallback + // in between to unwrap it first). + let underlying = NSError(domain: "", code: 400, userInfo: [NSLocalizedDescriptionKey: "Invalid bearer token"]) + let wrapped = SkyflowError.wrap(["errors": [["error": underlying]]]) + XCTAssertEqual(wrapped.httpCode, 400) + XCTAssertEqual(wrapped.message, "Invalid bearer token") + } } diff --git a/Tests/skyflow-iOS-errorTests/Skyflow_iOS_revealErrorTests.swift b/Tests/skyflow-iOS-errorTests/Skyflow_iOS_revealErrorTests.swift index 038716e6..74da44b9 100644 --- a/Tests/skyflow-iOS-errorTests/Skyflow_iOS_revealErrorTests.swift +++ b/Tests/skyflow-iOS-errorTests/Skyflow_iOS_revealErrorTests.swift @@ -48,26 +48,8 @@ class Skyflow_iOS_revealErrorTests: XCTestCase { } } - func getByIDFromClientWithExpectation(description: String = "should get records", records: [String: Any]) -> String { - let expectRecords = XCTestExpectation(description: description) - let callback = DemoAPICallback(expectation: expectRecords) - skyflow.getById(records: records, callback: callback) - - wait(for: [expectRecords], timeout: 10.0) - if callback.receivedResponse.isEmpty { - if callback.data["errors"] != nil { - return (callback.data["errors"] as! [NSError])[0].localizedDescription - } else { - return "ok" - } - } else { - return callback.receivedResponse - } - } - - func testDetokenizeNoRecords() { - let records = ["typo": [["token": revealTestId, "redaction": RedactionType.DEFAULT]]] + let records = ["typo": [["token": revealTestId]]] let result = getDataFromClientWithExpectation(records: records) XCTAssertEqual(result, ErrorCodes.RECORDS_KEY_ERROR().description) } @@ -77,7 +59,7 @@ class Skyflow_iOS_revealErrorTests: XCTestCase { let result = getDataFromClientWithExpectation(records: records) XCTAssertEqual(result, ErrorCodes.INVALID_RECORDS_TYPE().description) } - + func testDetokenizeEmptyRecords() { let records = ["records": []] let result = getDataFromClientWithExpectation(records: records) @@ -85,21 +67,16 @@ class Skyflow_iOS_revealErrorTests: XCTestCase { } func testDetokenizeNoTokens() { - let records = ["records": [["redaction": RedactionType.DEFAULT]]] + let records = ["records": [["foo": "bar"]]] let result = getDataFromClientWithExpectation(records: records) XCTAssertEqual(result, ErrorCodes.ID_KEY_ERROR().description) } func testDetokenizeBadTokens() { - let records = ["records": [["token": [], "redaction": RedactionType.DEFAULT]]] + let records = ["records": [["token": []]]] let result = getDataFromClientWithExpectation(records: records) XCTAssertEqual(result, ErrorCodes.INVALID_TOKEN_TYPE(value: "0").description) } - func testDetokenizeInvalidRedaction() { - let records = ["records": [["token": "123233232", "redaction": "123"]]] - let result = getDataFromClientWithExpectation(records: records) - XCTAssertEqual(result, ErrorCodes.INVALID_REDACTION_TYPE().description) - } func testContainerRevealWithUnmountedElements() { @@ -112,26 +89,12 @@ class Skyflow_iOS_revealErrorTests: XCTestCase { let revealElement = revealContainer?.create(input: revealElementInput, options: RevealElementOptions()) let callback = DemoAPICallback(expectation: XCTestExpectation(description: "Should return reveal output")) - revealContainer?.reveal(callback: callback) + revealContainer?.reveal(callback: callback.asRevealCallback) let result = callback.receivedResponse XCTAssertEqual(result, ErrorCodes.UNMOUNTED_REVEAL_ELEMENT(value: revealTestId).description) } - func testContainerRevealWithNoRedaction() { // when redaction is not specify plaintext will be applied - let window = UIWindow() - let revealContainer = skyflow.container(type: ContainerType.REVEAL, options: nil) - - let bstyle = Style(borderColor: UIColor.blue, cornerRadius: 20, padding: UIEdgeInsets(top: 15, left: 12, bottom: 15, right: 5), borderWidth: 2, textColor: UIColor.blue) - let styles = Styles(base: bstyle) - - let revealElementInput = RevealElementInput(token: "232h3j23h2jh",inputStyles: styles, label: "RevealElement") - let revealElement = revealContainer?.create(input: revealElementInput, options: RevealElementOptions()) - - window.addSubview(revealElement!) - XCTAssertEqual(revealElement!.revealInput.redaction, .PLAIN_TEXT) - } - func testContainerRevealWithEmptyToken() { let window = UIWindow() let revealContainer = skyflow.container(type: ContainerType.REVEAL, options: nil) @@ -139,64 +102,44 @@ class Skyflow_iOS_revealErrorTests: XCTestCase { let bstyle = Style(borderColor: UIColor.blue, cornerRadius: 20, padding: UIEdgeInsets(top: 15, left: 12, bottom: 15, right: 5), borderWidth: 2, textColor: UIColor.blue) let styles = Styles(base: bstyle) - let revealElementInput = RevealElementInput(inputStyles: styles, label: "RevealElement", redaction: .DEFAULT) + let revealElementInput = RevealElementInput(inputStyles: styles, label: "RevealElement") let revealElement = revealContainer?.create(input: revealElementInput, options: RevealElementOptions()) window.addSubview(revealElement!) let callback = DemoAPICallback(expectation: XCTestExpectation(description: "Should return reveal output")) - revealContainer?.reveal(callback: callback) + revealContainer?.reveal(callback: callback.asRevealCallback) let result = callback.receivedResponse XCTAssertEqual(result, ErrorCodes.EMPTY_TOKEN_ID().description) } - func testGetByIdNoRecords() { - let records = ["ok": [["redaction": RedactionType.DEFAULT]]] - let result = getByIDFromClientWithExpectation(records: records) - XCTAssertEqual(result, ErrorCodes.EMPTY_RECORDS_OBJECT().description) - } + func testContainerRevealEmptyVaultURL() { + // Unlike Client.detokenize()/getById()/get(), RevealContainer.reveal() calls + // callback.onFailure directly with the raw NSError (no callRevealOnFailure wrapping). + let clientWithEmptyURL = Client(Configuration(vaultID: "id", vaultURL: "", tokenProvider: DemoTokenProvider())) + let revealContainer = clientWithEmptyURL.container(type: ContainerType.REVEAL, options: nil) - func testGetByIdInvalidRecords() { - let records = ["records": 12] - let result = getByIDFromClientWithExpectation(records: records) - XCTAssertEqual(result, ErrorCodes.INVALID_RECORDS_TYPE().description) - } + let callback = DemoAPICallback(expectation: XCTestExpectation(description: "Reveal with empty vaultURL should fail")) + revealContainer?.reveal(callback: callback.asRevealCallback) - func testGetByIdNoIds() { - let records = ["records": [["redaction": RedactionType.DEFAULT]]] - let result = getByIDFromClientWithExpectation(records: records) - XCTAssertEqual(result, ErrorCodes.MISSING_KEY_IDS(value: "0").description) + XCTAssertEqual(callback.receivedResponse, ErrorCodes.EMPTY_VAULT_URL().getErrorObject(contextOptions: ContextOptions(interface: .REVEAL_CONTAINER)).localizedDescription) } - func testGetByIdInvalidIds() { - let records = ["records": [["ids": RedactionType.DEFAULT]]] - let result = getByIDFromClientWithExpectation(records: records) - XCTAssertEqual(result, ErrorCodes.INVALID_IDS_TYPE().description) - } + func testDetokenizeEmptyVaultURL() { + // Client.detokenize()'s vault-level errors route through callRevealOnFailure, which + // wraps the NSError in {"errors": [errorObject]} rather than passing it through raw. + let expectation = XCTestExpectation(description: "Detokenize with empty vaultURL should fail") + let callback = DemoAPICallback(expectation: expectation) + let clientWithEmptyURL = Client(Configuration(vaultID: "id", vaultURL: "", tokenProvider: DemoTokenProvider())) - func testGetByIdNoTable() { - let records = ["records": [["ids": ["abc"]]]] - let result = getByIDFromClientWithExpectation(records: records) - XCTAssertEqual(result, ErrorCodes.TABLE_KEY_ERROR(value: "0").description) - } - - func testGetByIdInvalidTable() { - let records = ["records": [["ids": ["abc"], "table": ["abc"]]]] - let result = getByIDFromClientWithExpectation(records: records) - XCTAssertEqual(result, ErrorCodes.INVALID_TABLE_NAME_TYPE(value: "0").description) - } + clientWithEmptyURL.detokenize(records: ["records": [["token": "sometoken"]]], callback: callback) - func testGetByIdNoRedaction() { - let records = ["records": [["ids": ["abc"], "table": "table"]]] - let result = getByIDFromClientWithExpectation(records: records) - XCTAssertEqual(result, ErrorCodes.REDACTION_KEY_ERROR(value: "0").description) + wait(for: [expectation], timeout: 10.0) + let errors = callback.data["errors"] as! [NSError] + XCTAssertEqual(errors.count, 1) + XCTAssertEqual(errors[0].localizedDescription, ErrorCodes.EMPTY_VAULT_URL().description) } - func testGetByIdInvalidRedaction() { - let records = ["records": [["ids": ["abc"], "table": "table", "redaction": "DEFAULT"]]] - let result = getByIDFromClientWithExpectation(records: records) - XCTAssertEqual(result, ErrorCodes.INVALID_REDACTION_TYPE().description) - } } diff --git a/Tests/skyflow-iOS-getByIdTests/skyflow_iOS_getByIdTests.swift b/Tests/skyflow-iOS-getByIdTests/skyflow_iOS_getByIdTests.swift index 315d8832..00c9fcd9 100644 --- a/Tests/skyflow-iOS-getByIdTests/skyflow_iOS_getByIdTests.swift +++ b/Tests/skyflow-iOS-getByIdTests/skyflow_iOS_getByIdTests.swift @@ -74,5 +74,20 @@ class skyflow_iOS_getByIdTests: XCTestCase { XCTAssertEqual(errorMessage, "TokenProvider error") } - + + func testGetByIdEmptyVaultURL() { + // Client.getById()'s vault-level errors route through callRevealOnFailure, which + // wraps the NSError in {"errors": [errorObject]} rather than passing it through raw. + let expectation = XCTestExpectation(description: "getById with empty vaultURL should fail") + let callback = DemoAPICallback(expectation: expectation) + let clientWithEmptyURL = Client(Configuration(vaultID: "id", vaultURL: "", tokenProvider: DemoTokenProvider())) + + clientWithEmptyURL.getById(records: ["records": [["ids": ["id1"], "table": "persons"]]], callback: callback) + + wait(for: [expectation], timeout: 10.0) + let errors = callback.data["errors"] as! [NSError] + XCTAssertEqual(errors.count, 1) + XCTAssertEqual(errors[0].localizedDescription, ErrorCodes.EMPTY_VAULT_URL().description) + } + } diff --git a/Tests/skyflow-iOS-getTests/skyflow_iOS_getTests.swift b/Tests/skyflow-iOS-getTests/skyflow_iOS_getTests.swift index eabf5a0b..a0413337 100644 --- a/Tests/skyflow-iOS-getTests/skyflow_iOS_getTests.swift +++ b/Tests/skyflow-iOS-getTests/skyflow_iOS_getTests.swift @@ -108,6 +108,20 @@ class skyflow_iOS_getTests: XCTestCase { print(callback.receivedResponse) XCTAssertTrue(callback.data.description.contains(ErrorCodes.REDACTION_WITH_TOKEN_NOT_SUPPORTED().description)) } - - + + func testGetEmptyVaultURL() { + // Client.get()'s vault-level errors route through callRevealOnFailure, which wraps + // the NSError in {"errors": [errorObject]} rather than passing it through raw. + let expectation = XCTestExpectation(description: "get with empty vaultURL should fail") + let callback = DemoAPICallback(expectation: expectation) + let clientWithEmptyURL = Client(Configuration(vaultID: "id", vaultURL: "", tokenProvider: DemoTokenProvider())) + + clientWithEmptyURL.get(records: ["records": [["ids": ["id1"], "table": "persons"]]], callback: callback) + + wait(for: [expectation], timeout: 10.0) + let errors = callback.data["errors"] as! [NSError] + XCTAssertEqual(errors.count, 1) + XCTAssertEqual(errors[0].localizedDescription, ErrorCodes.EMPTY_VAULT_URL().description) + } + } diff --git a/Tests/skyflow-iOS-revealTests/DemoImpl.swift b/Tests/skyflow-iOS-revealTests/DemoImpl.swift index 73208cc0..1454099e 100644 --- a/Tests/skyflow-iOS-revealTests/DemoImpl.swift +++ b/Tests/skyflow-iOS-revealTests/DemoImpl.swift @@ -35,12 +35,24 @@ public class DemoAPICallback: Callback { var receivedResponse: String = "" var expectation: XCTestExpectation var data: [String: Any] = [:] + var collectResponse: Skyflow.CollectResponse? + var revealResponse: Skyflow.RevealResponse? public init(expectation: XCTestExpectation) { self.expectation = expectation } public func onSuccess(_ responseBody: Any) { + if let response = responseBody as? Skyflow.CollectResponse { + self.collectResponse = response + expectation.fulfill() + return + } + if let response = responseBody as? Skyflow.RevealResponse { + self.revealResponse = response + expectation.fulfill() + return + } defer{ expectation.fulfill() } @@ -64,4 +76,15 @@ public class DemoAPICallback: Callback { } expectation.fulfill() } + + // CollectContainer.collect(callback:)/RevealContainer.reveal(callback:) require the + // concrete Skyflow.CollectCallback/RevealCallback types - these wrap self so existing + // DemoAPICallback call sites only need a one-word change (.asCollectCallback/.asRevealCallback). + var asCollectCallback: Skyflow.CollectCallback { + Skyflow.CollectCallback(onSuccess: { self.onSuccess($0) }, onFailure: { self.onFailure($0) }) + } + + var asRevealCallback: Skyflow.RevealCallback { + Skyflow.RevealCallback(onSuccess: { self.onSuccess($0) }, onFailure: { self.onFailure($0) }) + } } diff --git a/Tests/skyflow-iOS-revealTests/skyflow_iOS_revealTests.swift b/Tests/skyflow-iOS-revealTests/skyflow_iOS_revealTests.swift index 101264b8..d981c078 100644 --- a/Tests/skyflow-iOS-revealTests/skyflow_iOS_revealTests.swift +++ b/Tests/skyflow-iOS-revealTests/skyflow_iOS_revealTests.swift @@ -38,15 +38,6 @@ class skyflow_iOS_revealTests: XCTestCase { let istyle = Style(textColor: .red) let styles = Styles(base: bstyle, invalid: istyle) - let revealElementInput = RevealElementInput(token: revealTestId, inputStyles: styles, label: "RevealElement", redaction: .DEFAULT) - - return revealElementInput - } - func getRevealElementInputNoRedaction() -> RevealElementInput { - let bstyle = Style(borderColor: UIColor.blue, cornerRadius: 20, padding: UIEdgeInsets(top: 15, left: 12, bottom: 15, right: 5), borderWidth: 2, textColor: UIColor.blue) - let istyle = Style(textColor: .red) - let styles = Styles(base: bstyle, invalid: istyle) - let revealElementInput = RevealElementInput(token: revealTestId, inputStyles: styles, label: "RevealElement") return revealElementInput @@ -65,10 +56,23 @@ class skyflow_iOS_revealTests: XCTestCase { let revealElementInput = getRevealElementInput() XCTAssertEqual(revealElementInput.token, revealTestId) - XCTAssertEqual(revealElementInput.redaction, .DEFAULT) XCTAssertEqual(revealElementInput.label, "RevealElement") } + func testRevealOptionsCarriesTokenGroupRedactions() { + let options = RevealOptions(tokenGroupRedactions: [TokenGroupRedaction(tokenGroupName: "deterministic_string", redaction: "MASKED")]) + + XCTAssertEqual(options.tokenGroupRedactions?.count, 1) + XCTAssertEqual(options.tokenGroupRedactions?.first?.tokenGroupName, "deterministic_string") + XCTAssertEqual(options.tokenGroupRedactions?.first?.redaction, "MASKED") + } + + func testRevealOptionsDefaultsToNilTokenGroupRedactions() { + let options = RevealOptions() + + XCTAssertNil(options.tokenGroupRedactions) + } + func testCreateSkyflowRevealContainer() { let revealContainer = skyflow.container(type: ContainerType.REVEAL, options: nil) let revealElementInput = getRevealElementInput() @@ -104,22 +108,11 @@ class skyflow_iOS_revealTests: XCTestCase { let revealContainer = skyflow.container(type: ContainerType.REVEAL, options: nil) let revealElementInput = getRevealElementInput() let revealElement = revealContainer?.create(input: revealElementInput, options: RevealElementOptions()) - - let requestBody = RevealRequestBody.createRequestBody(elements: [revealElement!]) as! [String: [[String: Any]]] - - let result: [String: [[String: Any]]] = ["records": [["token": revealTestId, "redaction": RedactionType.DEFAULT]]] - - XCTAssertTrue(compareDictionaries(dict1: result, dict2: requestBody)) - } - func testCreateRevealRequestBodyWithNoRedaction() { - let revealContainer = skyflow.container(type: ContainerType.REVEAL, options: nil) - let revealElementInput = getRevealElementInputNoRedaction() - let revealElement = revealContainer?.create(input: revealElementInput, options: RevealElementOptions()) - + let requestBody = RevealRequestBody.createRequestBody(elements: [revealElement!]) as! [String: [[String: Any]]] - - let result: [String: [[String: Any]]] = ["records": [["token": revealTestId, "redaction": RedactionType.PLAIN_TEXT]]] - + + let result: [String: [[String: Any]]] = ["records": [["token": revealTestId]]] + XCTAssertTrue(compareDictionaries(dict1: result, dict2: requestBody)) } @@ -182,7 +175,7 @@ class skyflow_iOS_revealTests: XCTestCase { window.addSubview(revealElement!) let callback = DemoAPICallback(expectation: expectFailure) - revealContainer?.reveal(callback: callback) + revealContainer?.reveal(callback: callback.asRevealCallback) wait(for: [expectFailure], timeout: 10.0) diff --git a/Tests/skyflow-iOS-scenarioTests/ClientScenario.swift b/Tests/skyflow-iOS-scenarioTests/ClientScenario.swift index a0189fdb..33f86654 100644 --- a/Tests/skyflow-iOS-scenarioTests/ClientScenario.swift +++ b/Tests/skyflow-iOS-scenarioTests/ClientScenario.swift @@ -47,7 +47,7 @@ class ClientScenario { func getById(ids: [String: Any], callback: Callback) { Client(self.config).getById(records: ids, callback: callback) } - + private func detokenizeRequestBody(_ tokens: [String]) -> [String: [[String: String]]]{ var records = [] as [[String: String]] diff --git a/Tests/skyflow-iOS-scenarioTests/DemoImpl.swift b/Tests/skyflow-iOS-scenarioTests/DemoImpl.swift index 29e985e0..b5b8c05c 100644 --- a/Tests/skyflow-iOS-scenarioTests/DemoImpl.swift +++ b/Tests/skyflow-iOS-scenarioTests/DemoImpl.swift @@ -33,7 +33,6 @@ public class DemoTokenProvider: TokenProvider { public class DemoAPICallback: Callback { var receivedResponse: String = "" - var xml: String = "" var expectation: XCTestExpectation var data: [String: Any] = [:] var error: NSError? = nil @@ -54,9 +53,6 @@ public class DemoAPICallback: Callback { } else if error is [String: Any] { self.data = (error as! [String: Any]) } - if error is SkyflowError { - self.xml = (error as! SkyflowError).getXML() - } expectation.fulfill() } diff --git a/Tests/skyflow-iOS-utilTests/DemoImpl.swift b/Tests/skyflow-iOS-utilTests/DemoImpl.swift index e93c1169..f7315771 100644 --- a/Tests/skyflow-iOS-utilTests/DemoImpl.swift +++ b/Tests/skyflow-iOS-utilTests/DemoImpl.swift @@ -16,6 +16,8 @@ public class DemoAPICallback: Callback { var receivedResponse: String = "" var expectation: XCTestExpectation var data: [String: Any] = [:] + var collectResponse: Skyflow.CollectResponse? + var revealResponse: Skyflow.RevealResponse? public init(expectation: XCTestExpectation) { self.expectation = expectation @@ -23,7 +25,11 @@ public class DemoAPICallback: Callback { public func onSuccess(_ responseBody: Any) { print("success") - if let response = responseBody as? String { + if let response = responseBody as? Skyflow.CollectResponse { + self.collectResponse = response + } else if let response = responseBody as? Skyflow.RevealResponse { + self.revealResponse = response + } else if let response = responseBody as? String { self.receivedResponse = response } else { @@ -44,4 +50,15 @@ public class DemoAPICallback: Callback { expectation.fulfill() } + + // CollectContainer.collect(callback:)/RevealContainer.reveal(callback:) require the + // concrete Skyflow.CollectCallback/RevealCallback types - these wrap self so existing + // DemoAPICallback call sites only need a one-word change (.asCollectCallback/.asRevealCallback). + var asCollectCallback: Skyflow.CollectCallback { + Skyflow.CollectCallback(onSuccess: { self.onSuccess($0) }, onFailure: { self.onFailure($0) }) + } + + var asRevealCallback: Skyflow.RevealCallback { + Skyflow.RevealCallback(onSuccess: { self.onSuccess($0) }, onFailure: { self.onFailure($0) }) + } } diff --git a/Tests/skyflow-iOS-utilTests/MockURLProtocol.swift b/Tests/skyflow-iOS-utilTests/MockURLProtocol.swift new file mode 100644 index 00000000..64a125f9 --- /dev/null +++ b/Tests/skyflow-iOS-utilTests/MockURLProtocol.swift @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// Minimal URLProtocol-based network mock. Register via URLProtocol.registerClass(MockURLProtocol.self) +// before the request fires (URLSession(configuration: .default) consults globally registered +// protocol classes), and unregister afterward. Used for tests that need to exercise real +// URLSession.dataTask call sites (e.g. FlowVaultInsertAPICallback's insert+update merge), where +// bypassing the network layer entirely (as most other tests do via processResponse(data:response:error:)) +// wouldn't exercise the merge logic itself. + +import Foundation + +final class MockURLProtocol: URLProtocol { + static var requestHandler: ((URLRequest) throws -> (HTTPURLResponse, Data))? + + override class func canInit(with request: URLRequest) -> Bool { + return true + } + + override class func canonicalRequest(for request: URLRequest) -> URLRequest { + return request + } + + override func startLoading() { + guard let handler = MockURLProtocol.requestHandler else { + client?.urlProtocol(self, didFailWithError: NSError(domain: "MockURLProtocol", code: -1, userInfo: [NSLocalizedDescriptionKey: "No requestHandler configured"])) + return + } + do { + let (response, data) = try handler(request) + client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) + client?.urlProtocol(self, didLoad: data) + client?.urlProtocolDidFinishLoading(self) + } catch { + client?.urlProtocol(self, didFailWithError: error) + } + } + + override func stopLoading() {} +} diff --git a/Tests/skyflow-iOS-utilTests/skyflow_iOS_collectUtilTests.swift b/Tests/skyflow-iOS-utilTests/skyflow_iOS_collectUtilTests.swift index 40bbe132..7f3662e3 100644 --- a/Tests/skyflow-iOS-utilTests/skyflow_iOS_collectUtilTests.swift +++ b/Tests/skyflow-iOS-utilTests/skyflow_iOS_collectUtilTests.swift @@ -7,130 +7,169 @@ import XCTest final class skyflow_iOS_collectUtilTests: XCTestCase { - var collectCallback: CollectAPICallback! = nil + var collectCallback: FlowVaultCollectAPICallback! = nil var defaultRecord: [String: Any] = ["records": [["table": "table", "fields": ["field": "value"]]]] - + override func setUp() { - self.collectCallback = CollectAPICallback(callback: DemoAPICallback(expectation: XCTestExpectation()), + self.collectCallback = FlowVaultCollectAPICallback(callback: DemoAPICallback(expectation: XCTestExpectation()), apiClient: APIClient(vaultID: "", vaultURL: "", tokenProvider: DemoTokenProvider()), records: defaultRecord, - options: ICOptions(tokens: false, additionalFields: nil), + options: FlowVaultICOptions(additionalFields: nil), contextOptions: ContextOptions()) } - + func testBuildFieldsDict() { let dict = ["key": "value", "nested": ["key": "value"]] as [String: Any] let result = self.collectCallback.buildFieldsDict(dict: dict) XCTAssertEqual(dict["key"] as! String, result["key"] as! String) XCTAssertEqual(dict["nested"] as! [String: String], result["nested"] as! [String: String]) } - + func testOnSuccessInvalidUrl() { let expectation = XCTestExpectation(description: "Invalid URL should trigger failure") let callback = DemoAPICallback(expectation: expectation) self.collectCallback.apiClient.vaultURL = "Invalid url" self.collectCallback.callback = callback - + self.collectCallback.onSuccess("string") wait(for: [expectation], timeout: 20.0) - - let result = callback.data["errors"] as! [[String: Any]] - let errorObject = result[0]["error"] as! [String: Any] + + let errorObject = callback.data["error"] as! [String: Any] let msg = errorObject["message"] as! String XCTAssert(msg.contains("unsupported URL")) } - + func testGetRequestSession() { let url = URL(string: "https://example.org")! do { let (request, session) = try self.collectCallback.getRequestSession(url: url) - + XCTAssertEqual(request.allHTTPHeaderFields!["Authorization"], "Bearer ") // From DemoTokenProvider() + XCTAssertEqual(request.allHTTPHeaderFields!["Content-Type"], "application/json") + XCTAssertEqual(request.allHTTPHeaderFields!["Accept"], "application/json") let body = try JSONSerialization.jsonObject(with: request.httpBody!, options: .allowFragments) as! [String: Any] let records = body["records"] as! [[String: Any]] XCTAssertEqual(records.count, 1) - XCTAssertEqual(records[0]["fields"] as! [String: String], ["field": "value"]) - XCTAssertEqual(records[0]["method"] as! String, "POST") + XCTAssertEqual(records[0]["data"] as! [String: String], ["field": "value"]) XCTAssertEqual(records[0]["tableName"] as! String, "table") - + } catch { XCTFail(error.localizedDescription) } } - + func testGetCollectResponse() { - let response = ["responses": [["records": [["skyflow_id": "SID"]]]]] - + let response = ["records": [["skyflowID": "SID", "tableName": "table"]]] + do { let data = try JSONSerialization.data(withJSONObject: response, options: .fragmentsAllowed) let result = try self.collectCallback.getCollectResponseBody(data: data) - - XCTAssertEqual(result as! [String: [[String: String]]], ["records": [ - [ - "table": "table", - "skyflow_id": "SID" - ] - ]]) + let records = result["records"] as! [[String: Any]] + + XCTAssertEqual(records.count, 1) + XCTAssertEqual(records[0]["tableName"] as! String, "table") + XCTAssertEqual(records[0]["skyflowID"] as! String, "SID") + let fields = records[0]["fields"] as! [String: Any] + XCTAssertNil(fields["tokens"]) } catch { XCTFail(error.localizedDescription) } } - + func testGetCollectResponseWithTokens() { - let response = ["responses": [["records": [["skyflow_id": "SID"]]], ["fields": ["field": "value"]]]] - self.collectCallback.options = ICOptions() - + let response = ["records": [["skyflowID": "SID", "tableName": "table", "tokens": ["field": [["token": "tok", "tokenGroupName": "group"]]]]]] as [String: Any] + self.collectCallback.options = FlowVaultICOptions() + + do { + let data = try JSONSerialization.data(withJSONObject: response, options: .fragmentsAllowed) + let result = try self.collectCallback.getCollectResponseBody(data: data) + let records = result["records"] as! [[String: Any]] + + XCTAssertEqual(records.count, 1) + XCTAssertEqual(records[0]["tableName"] as! String, "table") + XCTAssertEqual(records[0]["skyflowID"] as! String, "SID") + let fields = records[0]["fields"] as! [String: Any] + let fieldTokens = fields["field"] as! [[String: Any]] + XCTAssertEqual(fieldTokens[0]["token"] as? String, "tok") + } catch { + XCTFail(error.localizedDescription) + } + } + + func testGetCollectResponseWithHashedData() { + let response: [String: Any] = ["records": [[ + "skyflowID": "SID", + "tableName": "table", + "httpCode": 200, + "data": ["field": "value"], + "hashedData": ["field": "hashed-value"] + ]]] + self.collectCallback.options = FlowVaultICOptions() + do { let data = try JSONSerialization.data(withJSONObject: response, options: .fragmentsAllowed) let result = try self.collectCallback.getCollectResponseBody(data: data) let records = result["records"] as! [[String: Any]] - - let expected = ["records": [["table": "table", "fields": ["field": "value", "skyflow_id": "SID"]]]] - - XCTAssertEqual(records.count, expected["records"]!.count) - XCTAssertEqual(records[0]["table"] as! String, expected["records"]![0]["table"] as! String) - XCTAssertEqual(records[0]["fields"] as! [String: String], expected["records"]![0]["fields"] as! [String: String]) - - + + XCTAssertEqual(records.count, 1) + XCTAssertEqual(records[0]["httpCode"] as? Int, 200) + let fields = records[0]["fields"] as! [String: Any] + XCTAssertNil(fields["data"]) + XCTAssertEqual(records[0]["hashedData"] as! [String: String], ["field": "hashed-value"]) } catch { XCTFail(error.localizedDescription) } } - + func testProcessResponse() { - let response = ["responses": [["records": [["skyflow_id": "SID"]]], ["fields": ["field": "value"]]]] - + let response = ["records": [["skyflowID": "SID", "tableName": "table"]]] + do { let data = try JSONSerialization.data(withJSONObject: response, options: .fragmentsAllowed) let response = HTTPURLResponse(url: URL(string: "https://example.org")!, statusCode: 200, httpVersion: "1.1", headerFields: nil) - - let processedData = try self.collectCallback.processResponse(data: data, response: response, error: nil) as! [String: [[String: String]]] - XCTAssertEqual(processedData, ["records": [["skyflow_id": "SID", "table": "table"]]]) - + + let processedData = try self.collectCallback.processResponse(data: data, response: response, error: nil) + let records = processedData["records"] as! [[String: Any]] + XCTAssertEqual(records[0]["tableName"] as! String, "table") + XCTAssertEqual(records[0]["skyflowID"] as! String, "SID") + } catch { XCTFail(error.localizedDescription) } } - + func testProcessResponseError() { - let response = ["responses": [["records": [["skyflow_id": "SID"]]], ["fields": ["field": "value"]]]] - + // A genuine connection/network-level error (URLSession error, no HTTP response reached) + // should not throw - it returns a top-level {"error": {...}} dict directly. + let networkError = NSError(domain: "NSURLErrorDomain", code: -1009, userInfo: [NSLocalizedDescriptionKey: "The Internet connection appears to be offline."]) + do { - let data = try JSONSerialization.data(withJSONObject: response, options: .fragmentsAllowed) - let response = HTTPURLResponse(url: URL(string: "https://example.org")!, statusCode: 200, httpVersion: "1.1", headerFields: nil) - - let processedData = try self.collectCallback.processResponse(data: data, response: response, error: NSError(domain: "", code: 400, userInfo: nil)) - as! [String: [String: String]] + let processedData = try self.collectCallback.processResponse(data: nil, response: nil, error: networkError) + let errorDict = processedData["error"] as! [String: Any] + XCTAssertEqual(errorDict["message"] as? String, "The Internet connection appears to be offline.") + } catch { + XCTFail("Should not throw for a connection-level error: \(error)") + } + } + + func testGetCollectResponseBodyWithMalformedTopLevelJSON() { + // A response body that parses as valid JSON but isn't a top-level object (e.g. a bare + // array or scalar) must not crash - it should fall back to an empty result. + do { + let arrayData = try JSONSerialization.data(withJSONObject: ["not", "an", "object"], options: .fragmentsAllowed) + let result = try self.collectCallback.getCollectResponseBody(data: arrayData) + XCTAssertEqual((result["records"] as? [[String: Any]])?.count, 0) } catch { + XCTFail("Malformed top-level JSON should not throw or crash: \(error)") } } - + func testProcessResponseFailure() { let response = ["error": ["message": "Internal Server Error"]] do { let data = try JSONSerialization.data(withJSONObject: response, options: .fragmentsAllowed) let response = HTTPURLResponse(url: URL(string: "https://example.org")!, statusCode: 500, httpVersion: "1.1", headerFields: ["x-request-id": "RID"]) - + // XCTFail("Not throwing on Api Error") do { var res = try self.collectCallback.processResponse(data: data, response: response, error: nil) @@ -143,7 +182,7 @@ final class skyflow_iOS_collectUtilTests: XCTestCase { XCTAssertEqual(error.localizedDescription, "Internal Server Error - request-id: RID") } } - + func testCollectInvalidBearerToken() { let expectation = XCTestExpectation() let callback = DemoAPICallback(expectation: expectation) @@ -151,11 +190,11 @@ final class skyflow_iOS_collectUtilTests: XCTestCase { let container = client.container(type: ContainerType.COLLECT) let input = CollectElementInput(table: "table", column: "column", type: .EXPIRATION_YEAR) let element = container?.create(input: input) - + UIWindow().addSubview(element!) - - container?.collect(callback: callback) - + + container?.collect(callback: callback.asCollectCallback) + wait(for: [expectation], timeout: 20.0) XCTAssertTrue(callback.receivedResponse.contains("Token generated from 'getBearerToken' callback function is invalid")) } @@ -167,68 +206,34 @@ final class skyflow_iOS_collectUtilTests: XCTestCase { XCTAssertEqual(device.systemName + "@" + device.systemVersion, deviceInfo["sdk_client_os_details"] as! String) } func testDeviceDetails() { - + let deviceDetails = FetchMetrices().getDeviceDetails() - + XCTAssertNotNil(deviceDetails["device"]) XCTAssertNotNil(deviceDetails["os_details"]) XCTAssertNotNil(deviceDetails["sdk_name_version"]) - + if let device = deviceDetails["device"] as? String { XCTAssertFalse(device.isEmpty) } else { XCTAssertTrue(deviceDetails["device"] as! String == "") } - + if let osDetails = deviceDetails["os_details"] as? String { XCTAssertFalse(osDetails.isEmpty) } else { XCTAssertTrue(deviceDetails["os_details"] as! String == "") } - + if let sdkNameVersion = deviceDetails["sdk_name_version"] as? String { XCTAssertFalse(sdkNameVersion.isEmpty) } else { XCTAssertTrue(deviceDetails["sdk_name_version"] as! String == "") } } - func testUpdateNewFlow() { - let expectation = XCTestExpectation(description: "Update new flow should succeed and merge response") - let callback = DemoAPICallback(expectation: expectation) - let updateRecord: [String: Any] = [ - "table": "table", - "skyflowID": "id1", - "fields": ["field": "newValue"] - ] - let collectCallback = CollectAPICallback( - callback: callback, - apiClient: APIClient(vaultID: "vault", vaultURL: "https://example.org/", tokenProvider: DemoTokenProvider()), - records: ["update": ["id1": updateRecord]], - options: ICOptions(tokens: true, additionalFields: nil), - contextOptions: ContextOptions() - ) - // Simulate a successful update response - let responseDict: [String: Any] = [ - "skyflow_id": "id1", - "tokens": ["field": "newValue"] - ] - let responseData = try! JSONSerialization.data(withJSONObject: responseDict, options: .fragmentsAllowed) - let urlResponse = HTTPURLResponse(url: URL(string: "https://example.org/table/id1")!, statusCode: 200, httpVersion: "1.1", headerFields: nil) - do { - let processed = try collectCallback.processUpdateResponse(data: responseData, response: urlResponse, error: nil, table: "table") - let records = processed["records"] as! [[String: Any]] - print("records after processing update response", records) - XCTAssertEqual(records.count, 1) - XCTAssertEqual(records[0]["table"] as? String, "table") - let fields = records[0]["fields"] as? [String: String] - XCTAssertEqual((fields?["skyflow_id"] ?? "") as String, "id1") - XCTAssertEqual(fields?["field"] as? String, "newValue") - } catch { - XCTFail("Update flow failed: \(error)") - } - } - func testPartialInsertAndUpdateScenario() { - let expectation = XCTestExpectation(description: "Partial insert and update scenario should succeed") + + func testOnlyInsertSuccess() { + let expectation = XCTestExpectation(description: "Only insert records should succeed") let callback = DemoAPICallback(expectation: expectation) let insertRecord: [String: Any] = [ @@ -236,343 +241,404 @@ final class skyflow_iOS_collectUtilTests: XCTestCase { "fields": ["field": "value"] ] - let updateRecord: [String: Any] = [ - "table": "table", - "skyflowID": "id1", - "fields": ["field": "newValue"] - ] - - let collectCallback = CollectAPICallback( + let collectCallback = FlowVaultCollectAPICallback( callback: callback, apiClient: APIClient(vaultID: "vault", vaultURL: "https://example.org/", tokenProvider: DemoTokenProvider()), - records: ["records": [insertRecord], "update": ["id1": updateRecord]], - options: ICOptions(tokens: true, additionalFields: nil), + records: ["records": [insertRecord]], + options: FlowVaultICOptions(additionalFields: nil), contextOptions: ContextOptions() ) // Simulate a successful insert response - let insertResponseDict = ["responses": [["records": [["skyflow_id": "SID"]]], ["fields": ["field": "value"]]]] - let insertResponseData = try! JSONSerialization.data(withJSONObject: insertResponseDict, options: .fragmentsAllowed) - let insertUrlResponse = HTTPURLResponse(url: URL(string: "https://example.org/vault")!, statusCode: 200, httpVersion: "1.1", headerFields: nil) + let insertResponseDict: [String: Any] = ["records": [["skyflowID": "SID", "tableName": "table", "tokens": ["field": "value"]]]] - // Simulate a successful update response - let updateResponseDict: [String: Any] = [ - "skyflow_id": "id1", - "tokens": ["field": "newValue"] - ] - let updateResponseData = try! JSONSerialization.data(withJSONObject: updateResponseDict, options: .fragmentsAllowed) - let updateUrlResponse = HTTPURLResponse(url: URL(string: "https://example.org/table/id1")!, statusCode: 200, httpVersion: "1.1", headerFields: nil) + let insertResponseData = try! JSONSerialization.data(withJSONObject: insertResponseDict, options: .fragmentsAllowed) + let insertUrlResponse = HTTPURLResponse(url: URL(string: "https://example.org/v2/records/insert")!, statusCode: 200, httpVersion: "1.1", headerFields: nil) do { let processedInsert = try collectCallback.processResponse(data: insertResponseData, response: insertUrlResponse, error: nil) - let processedUpdate = try collectCallback.processUpdateResponse(data: updateResponseData, response: updateUrlResponse, error: nil, table: "table") - let insertRecords = processedInsert["records"] as! [[String: Any]] - let updateRecords = processedUpdate["records"] as! [[String: Any]] XCTAssertEqual(insertRecords.count, 1) - let ifields = insertRecords[0]["fields"] as? [String: String] - XCTAssertEqual(ifields?["skyflow_id"] as? String, "SID") - - XCTAssertEqual(updateRecords.count, 1) - XCTAssertEqual(updateRecords[0]["table"] as? String, "table") - let fields = updateRecords[0]["fields"] as? [String: String] - XCTAssertEqual(fields?["skyflow_id"], "id1") - XCTAssertEqual(fields?["field"], "newValue") + XCTAssertEqual(insertRecords[0]["tableName"] as? String, "table") + XCTAssertEqual(insertRecords[0]["skyflowID"] as? String, "SID") + let fields = insertRecords[0]["fields"] as! [String: Any] + XCTAssertEqual(fields["field"] as? String, "value") } catch { - XCTFail("Partial insert and update scenario failed: \(error)") + XCTFail("Insert scenario failed: \(error)") } } - func testOnlyInsertSuccess() { - let expectation = XCTestExpectation(description: "Only insert records should succeed") + + func testInsertPartialFailure() { + let expectation = XCTestExpectation(description: "Partial insert failure should populate errors array") let callback = DemoAPICallback(expectation: expectation) - let insertRecord: [String: Any] = [ - "table": "table", - "fields": ["field": "value"] + let insertRecords: [[String: Any]] = [ + ["table": "table", "fields": ["field": "value"]], + ["table": "table", "fields": ["field": "value2"]] ] - let collectCallback = CollectAPICallback( + let collectCallback = FlowVaultCollectAPICallback( callback: callback, apiClient: APIClient(vaultID: "vault", vaultURL: "https://example.org/", tokenProvider: DemoTokenProvider()), - records: ["records": [insertRecord]], - options: ICOptions(tokens: true, additionalFields: nil), + records: ["records": insertRecords], + options: FlowVaultICOptions(additionalFields: nil), contextOptions: ContextOptions() ) - // Simulate a successful insert response - let insertResponseDict = ["responses": [["records": [["skyflow_id": "SID"]]], ["fields": ["field": "value"]]]] - - let insertResponseData = try! JSONSerialization.data(withJSONObject: insertResponseDict, options: .fragmentsAllowed) - let insertUrlResponse = HTTPURLResponse(url: URL(string: "https://example.org/vault")!, statusCode: 200, httpVersion: "1.1", headerFields: nil) + let responseDict: [String: Any] = ["records": [ + ["skyflowID": "SID", "tableName": "table", "tokens": ["field": "value"]], + ["tableName": "table", "error": "insert failed", "httpCode": 400] + ]] + let responseData = try! JSONSerialization.data(withJSONObject: responseDict, options: .fragmentsAllowed) + let urlResponse = HTTPURLResponse(url: URL(string: "https://example.org/v2/records/insert")!, statusCode: 200, httpVersion: "1.1", headerFields: nil) do { - let processedInsert = try collectCallback.processResponse(data: insertResponseData, response: insertUrlResponse, error: nil) - let insertRecords = processedInsert["records"] as! [[String: Any]] + let processed = try collectCallback.processResponse(data: responseData, response: urlResponse, error: nil) + let records = processed["records"] as! [[String: Any]] - XCTAssertEqual(insertRecords.count, 1) - let fields = insertRecords[0]["fields"] as? [String: String] - XCTAssertEqual(fields?["skyflow_id"] as? String, "SID") + XCTAssertEqual(records.count, 2) + XCTAssertNil(records[0]["error"]) + XCTAssertEqual(records[1]["error"] as? String, "insert failed") + XCTAssertEqual(records[1]["httpCode"] as? Int, 400) } catch { - XCTFail("Insert scenario failed: \(error)") + XCTFail("Partial insert failure scenario failed: \(error)") } } - func testOnlyUpdateSuccess() { - let expectation = XCTestExpectation(description: "Only update records should succeed") + func testInsertFullFailureWithNon2xxOuterStatus() { + let expectation = XCTestExpectation(description: "Full batch failure with 400 outer status should still parse per-record errors") let callback = DemoAPICallback(expectation: expectation) - let updateRecord: [String: Any] = [ - "table": "table", - "skyflowID": "id1", - "fields": ["field": "newValue"] + let insertRecords: [[String: Any]] = [ + ["table": "table", "fields": ["field": "value"]], + ["table": "table", "fields": ["field": "value2"]] ] - let collectCallback = CollectAPICallback( + let collectCallback = FlowVaultCollectAPICallback( callback: callback, apiClient: APIClient(vaultID: "vault", vaultURL: "https://example.org/", tokenProvider: DemoTokenProvider()), - records: ["update": ["id1": updateRecord]], - options: ICOptions(tokens: true, additionalFields: nil), + records: ["records": insertRecords], + options: FlowVaultICOptions(additionalFields: nil), contextOptions: ContextOptions() ) - // Simulate a successful update response - let updateResponseDict: [String: Any] = [ - "skyflow_id": "id1", - "tokens": ["field": "newValue"] - ] - let updateResponseData = try! JSONSerialization.data(withJSONObject: updateResponseDict, options: .fragmentsAllowed) - let updateUrlResponse = HTTPURLResponse(url: URL(string: "https://example.org/table/id1")!, statusCode: 200, httpVersion: "1.1", headerFields: nil) + let responseDict: [String: Any] = ["records": [ + ["skyflowID": NSNull(), "tokens": NSNull(), "data": NSNull(), "hashedData": NSNull(), + "error": "Invalid request. Table name table not present for record. Specify a valid table name.", + "httpCode": 400, "tableName": ""], + ["skyflowID": NSNull(), "tokens": NSNull(), "data": NSNull(), "hashedData": NSNull(), + "error": "Invalid request. Table name table not present for record. Specify a valid table name.", + "httpCode": 400, "tableName": ""] + ]] + let responseData = try! JSONSerialization.data(withJSONObject: responseDict, options: .fragmentsAllowed) + let urlResponse = HTTPURLResponse(url: URL(string: "https://example.org/v2/records/insert")!, statusCode: 400, httpVersion: "1.1", headerFields: nil) do { - let processedUpdate = try collectCallback.processUpdateResponse(data: updateResponseData, response: updateUrlResponse, error: nil, table: "table") - let updateRecords = processedUpdate["records"] as! [[String: Any]] - - XCTAssertEqual(updateRecords.count, 1) - XCTAssertEqual(updateRecords[0]["table"] as? String, "table") - let fields = updateRecords[0]["fields"] as? [String: String] - XCTAssertEqual(fields?["skyflow_id"], "id1") - XCTAssertEqual(fields?["field"], "newValue") + let processed = try collectCallback.processResponse(data: responseData, response: urlResponse, error: nil) + XCTAssertNil(processed["error"], "Should not collapse into a generic top-level error when the body has a records array") + let records = processed["records"] as! [[String: Any]] + + XCTAssertEqual(records.count, 2) + XCTAssertEqual(records[0]["error"] as? String, "Invalid request. Table name table not present for record. Specify a valid table name.") + XCTAssertEqual(records[0]["httpCode"] as? Int, 400) } catch { - XCTFail("Update scenario failed: \(error)") + XCTFail("Full failure scenario should not throw: \(error)") } } - func testInsertAndUpdateSuccess() { - let expectation = XCTestExpectation(description: "Insert and update records should succeed") + func testInsertPartialFailureWith207OuterStatus() { + let expectation = XCTestExpectation(description: "Partial failure with 207 outer status should parse per-record errors") let callback = DemoAPICallback(expectation: expectation) - let insertRecord: [String: Any] = [ - "table": "table", - "fields": ["field": "value"] - ] - - let updateRecord: [String: Any] = [ - "table": "table", - "skyflowID": "id1", - "fields": ["field": "newValue"] + let insertRecords: [[String: Any]] = [ + ["table": "table2", "fields": ["address": "dedede", "gender": "sagar"]], + ["table": "table", "fields": ["field": "value"]] ] - let collectCallback = CollectAPICallback( + let collectCallback = FlowVaultCollectAPICallback( callback: callback, apiClient: APIClient(vaultID: "vault", vaultURL: "https://example.org/", tokenProvider: DemoTokenProvider()), - records: ["records": [insertRecord], "update": ["id1": updateRecord]], - options: ICOptions(tokens: true, additionalFields: nil), + records: ["records": insertRecords], + options: FlowVaultICOptions(additionalFields: nil), contextOptions: ContextOptions() ) - // Simulate a successful insert response -// let insertResponseDict: [String: Any] = [ -// "records": [["skyflow_id": "inserted_id"]] -// ] - let insertResponseDict = ["responses": [["records": [["skyflow_id": "inserted_id"]]], ["fields": ["field": "value"]]]] - - let insertResponseData = try! JSONSerialization.data(withJSONObject: insertResponseDict, options: .fragmentsAllowed) - let insertUrlResponse = HTTPURLResponse(url: URL(string: "https://example.org/vault")!, statusCode: 200, httpVersion: "1.1", headerFields: nil) - - // Simulate a successful update response - let updateResponseDict: [String: Any] = [ - "skyflow_id": "id1", - "tokens": ["field": "newValue"] - ] - let updateResponseData = try! JSONSerialization.data(withJSONObject: updateResponseDict, options: .fragmentsAllowed) - let updateUrlResponse = HTTPURLResponse(url: URL(string: "https://example.org/table/id1")!, statusCode: 200, httpVersion: "1.1", headerFields: nil) + let responseDict: [String: Any] = ["records": [ + ["skyflowID": "b187b5b6-28b4-4881-93fa-4ef10e30b20e", "tokens": ["address": [["token": "dedwim", "tokenGroupName": "deterministic_string"]]], + "data": ["address": "dedede", "gender": "sagar"], "hashedData": [:], "error": NSNull(), "httpCode": 200, "tableName": "table2"], + ["skyflowID": NSNull(), "tokens": NSNull(), "data": NSNull(), "hashedData": NSNull(), + "error": "Invalid request. Table name table not present for record. Specify a valid table name.", + "httpCode": 400, "tableName": ""] + ]] + let responseData = try! JSONSerialization.data(withJSONObject: responseDict, options: .fragmentsAllowed) + let urlResponse = HTTPURLResponse(url: URL(string: "https://example.org/v2/records/insert")!, statusCode: 207, httpVersion: "1.1", headerFields: nil) do { - let processedInsert = try collectCallback.processResponse(data: insertResponseData, response: insertUrlResponse, error: nil) - let processedUpdate = try collectCallback.processUpdateResponse(data: updateResponseData, response: updateUrlResponse, error: nil, table: "table") - - let insertRecords = processedInsert["records"] as! [[String: Any]] - let updateRecords = processedUpdate["records"] as! [[String: Any]] + let processed = try collectCallback.processResponse(data: responseData, response: urlResponse, error: nil) + let records = processed["records"] as! [[String: Any]] - XCTAssertEqual(insertRecords.count, 1) - var ifields = insertRecords[0]["fields"] as? [String: String] - XCTAssertEqual(ifields?["skyflow_id"] as? String, "inserted_id") - - XCTAssertEqual(updateRecords.count, 1) - XCTAssertEqual(updateRecords[0]["table"] as? String, "table") - let fields = updateRecords[0]["fields"] as? [String: String] - XCTAssertEqual(fields?["skyflow_id"], "id1") - XCTAssertEqual(fields?["field"], "newValue") + XCTAssertEqual(records.count, 2) + XCTAssertEqual(records[0]["skyflowID"] as? String, "b187b5b6-28b4-4881-93fa-4ef10e30b20e") + XCTAssertEqual(records[1]["httpCode"] as? Int, 400) } catch { - XCTFail("Insert and update scenario failed: \(error)") + XCTFail("Partial (207) scenario should not throw: \(error)") } } - func testInsertSuccessAndUpdateFailure() { - let expectation = XCTestExpectation(description: "Insert success and update failure") - let callback = DemoAPICallback(expectation: expectation) + func testUpdateSuccessResponseLiteral() { + let json = """ + { + "records": [ + { + "skyflowID": "f30c8ccf-7e86-46b4-be74-0b2db44e4b87", + "tokens": { + "email": [{"token": "a@ehmw.aqk", "tokenGroupName": "nondeterministic_string"}], + "name": [{"token": "sagwm", "tokenGroupName": "deterministic_string"}], + "passport": [{"token": "21284054160", "tokenGroupName": "deterministic_string"}] + }, + "data": {"email": "b@demo.com", "name": "sagar", "passport": "21212121212"}, + "hashedData": {}, + "error": null, + "httpCode": 200, + "tableName": "table1" + } + ] + } + """ + let responseData = json.data(using: .utf8)! + let urlResponse = HTTPURLResponse(url: URL(string: "https://example.org/v2/records/update")!, statusCode: 200, httpVersion: "1.1", headerFields: nil) + self.collectCallback.options = FlowVaultICOptions() - let insertRecord: [String: Any] = [ - "table": "table", - "fields": ["field": "value"] - ] + do { + let processed = try self.collectCallback.processResponse(data: responseData, response: urlResponse, error: nil) + let records = processed["records"] as! [[String: Any]] - let updateRecord: [String: Any] = [ - "table": "table", - "skyflowID": "id1", - "fields": ["field": "newValue"] - ] + XCTAssertEqual(records.count, 1) + XCTAssertEqual(records[0]["tableName"] as? String, "table1") + XCTAssertEqual(records[0]["skyflowID"] as? String, "f30c8ccf-7e86-46b4-be74-0b2db44e4b87") + let fields = records[0]["fields"] as! [String: Any] + XCTAssertNil(fields["data"]) + let nameTokens = fields["name"] as! [[String: Any]] + XCTAssertEqual(nameTokens[0]["token"] as? String, "sagwm") + } catch { + XCTFail("Update success response should not throw: \(error)") + } + } - let collectCallback = CollectAPICallback( - callback: callback, - apiClient: APIClient(vaultID: "vault", vaultURL: "https://example.org/", tokenProvider: DemoTokenProvider()), - records: ["records": [insertRecord], "update": ["id1": updateRecord]], - options: ICOptions(tokens: true, additionalFields: nil), - contextOptions: ContextOptions() - ) + func testUpdateFullFailureResponseLiteral() { + let json = """ + { + "records": [ + { + "skyflowID": null, "tokens": null, "data": null, "hashedData": null, + "error": "Invalid request. Table name table not present for record. Specify a valid table name.", + "httpCode": 400, "tableName": "" + }, + { + "skyflowID": null, "tokens": null, "data": null, "hashedData": null, + "error": "Invalid request. Table name table not present for record. Specify a valid table name.", + "httpCode": 400, "tableName": "" + } + ] + } + """ + let responseData = json.data(using: .utf8)! + let urlResponse = HTTPURLResponse(url: URL(string: "https://example.org/v2/records/update")!, statusCode: 400, httpVersion: "1.1", headerFields: nil) - // Simulate a successful insert response - let insertResponseDict = ["responses": [["records": [["skyflow_id": "inserted_id"]]], ["fields": ["field": "value"]]]] - let insertResponseData = try! JSONSerialization.data(withJSONObject: insertResponseDict, options: .fragmentsAllowed) - let insertUrlResponse = HTTPURLResponse(url: URL(string: "https://example.org/vault")!, statusCode: 200, httpVersion: "1.1", headerFields: nil) + do { + let processed = try self.collectCallback.processResponse(data: responseData, response: urlResponse, error: nil) + XCTAssertNil(processed["error"], "Should not collapse into a generic top-level error when the body has a records array") + let records = processed["records"] as! [[String: Any]] - // Simulate a failed update response - let updateError = NSError(domain: "", code: 400, userInfo: [NSLocalizedDescriptionKey: "Update failed"]) + XCTAssertEqual(records.count, 2) + XCTAssertEqual(records[0]["error"] as? String, "Invalid request. Table name table not present for record. Specify a valid table name.") + } catch { + XCTFail("Update full failure response should not throw: \(error)") + } + } - do { - let processedInsert = try collectCallback.processResponse(data: insertResponseData, response: insertUrlResponse, error: nil) - let insertRecords = processedInsert["records"] as! [[String: Any]] + func testUpdatePartialFailureResponseLiteral() { + let json = """ + { + "records": [ + { + "skyflowID": "b187b5b6-28b4-4881-93fa-4ef10e30b20e", + "tokens": { + "address": [{"token": "dedwim", "tokenGroupName": "deterministic_string"}], + "gender": [{"token": "sagwm", "tokenGroupName": "deterministic_string"}] + }, + "data": {"address": "dedede", "gender": "sagar"}, + "hashedData": {}, + "error": null, + "httpCode": 200, + "tableName": "table2" + }, + { + "skyflowID": null, "tokens": null, "data": null, "hashedData": null, + "error": "Invalid request. Table name table not present for record. Specify a valid table name.", + "httpCode": 400, "tableName": "" + } + ] + } + """ + let responseData = json.data(using: .utf8)! + let urlResponse = HTTPURLResponse(url: URL(string: "https://example.org/v2/records/update")!, statusCode: 207, httpVersion: "1.1", headerFields: nil) + self.collectCallback.options = FlowVaultICOptions() - XCTAssertEqual(insertRecords.count, 1) - var fields = insertRecords[0]["fields"] as? [String: Any] - XCTAssertEqual(fields?["skyflow_id"] as? String, "inserted_id") + do { + let processed = try self.collectCallback.processResponse(data: responseData, response: urlResponse, error: nil) + let records = processed["records"] as! [[String: Any]] - let insertResponse = try collectCallback.processUpdateResponse(data: nil, response: nil, error: updateError, table: "table") as! [String: [String: String]] - XCTAssertEqual(insertResponse, ["error": ["message": "Update failed"]]) + XCTAssertEqual(records.count, 2) + XCTAssertEqual(records[0]["skyflowID"] as? String, "b187b5b6-28b4-4881-93fa-4ef10e30b20e") + XCTAssertEqual(records[1]["httpCode"] as? Int, 400) } catch { - XCTFail("Insert success and update failure scenario failed: \(error)") + XCTFail("Update partial response should not throw: \(error)") } } - func testInsertFailureAndUpdateSuccess() { - let expectation = XCTestExpectation(description: "Insert failure and update success") - let callback = DemoAPICallback(expectation: expectation) + // The tests above all call processResponse(data:response:error:) directly, which only + // exercises parsing a single HTTP response body. When a request has BOTH inserts and + // updates, FlowVaultInsertAPICallback.onSuccess fires two REAL network calls (v2/records/insert + // and v2/records/update) and merges both results via a DispatchGroup - that merge logic can + // only be exercised by going through onSuccess with real URLSession dispatch, hence the + // URLProtocol mock here. + func testInsertAndUpdateResponsesMergeIntoOneCollectResponse() { + let mockConfiguration = URLSessionConfiguration.ephemeral + mockConfiguration.protocolClasses = [MockURLProtocol.self] + let originalConfiguration = FlowVaultInsertAPICallback.urlSessionConfiguration + FlowVaultInsertAPICallback.urlSessionConfiguration = mockConfiguration + defer { FlowVaultInsertAPICallback.urlSessionConfiguration = originalConfiguration } + + MockURLProtocol.requestHandler = { request in + let url = request.url!.absoluteString + let body: [String: Any] + if url.contains("v2/records/insert") { + body = ["records": [["skyflowID": "insertedId", "tableName": "cards", "tokens": ["card_number": "value"], "httpCode": 200]]] + } else if url.contains("v2/records/update") { + body = ["records": [["skyflowID": "id1", "tableName": "persons", "tokens": [:], "httpCode": 200]]] + } else { + XCTFail("Unexpected URL: \(url)") + body = ["records": []] + } + let data = try! JSONSerialization.data(withJSONObject: body) + let response = HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: "1.1", headerFields: nil)! + return (response, data) + } - let insertRecord: [String: Any] = [ - "table": "table", - "fields": ["field": "value"] - ] + let expectation = XCTestExpectation(description: "Merged insert+update response") + let callback = DemoAPICallback(expectation: expectation) - let updateRecord: [String: Any] = [ - "table": "table", - "skyflowID": "id1", - "fields": ["field": "newValue"] + let records: [String: Any] = [ + "records": [["table": "cards", "fields": ["card_number": "4111"]]], + "update": ["id1": ["table": "persons", "fields": ["name": "John"]]] ] - let collectCallback = CollectAPICallback( - callback: callback, + let insertApiCallback = FlowVaultInsertAPICallback( + callback: callback.asCollectCallback, apiClient: APIClient(vaultID: "vault", vaultURL: "https://example.org/", tokenProvider: DemoTokenProvider()), - records: ["records": [insertRecord], "update": ["id1": updateRecord]], - options: ICOptions(tokens: true, additionalFields: nil), + records: records, + options: FlowVaultICOptions(), contextOptions: ContextOptions() ) + // onSuccess's parameter here is the bearer token from TokenAPICallback - unused by this + // method's own logic (it reads self.records instead), so any value triggers the real flow. + insertApiCallback.onSuccess("dummy-token") - // Simulate a failed insert response - let insertErrorResponseDict: [String: Any] = [ - "error": ["message": "Insert failed"] - ] - let insertErrorResponseData = try! JSONSerialization.data(withJSONObject: insertErrorResponseDict, options: .fragmentsAllowed) - let insertErrorUrlResponse = HTTPURLResponse(url: URL(string: "https://example.org/vault")!, statusCode: 404, httpVersion: "1.1", headerFields: nil) - - // Simulate a successful update response - let updateResponseDict: [String: Any] = [ - "skyflow_id": "id1", - "tokens": ["field": "newValue"] - ] - let updateResponseData = try! JSONSerialization.data(withJSONObject: updateResponseDict, options: .fragmentsAllowed) - let updateUrlResponse = HTTPURLResponse(url: URL(string: "https://example.org/table/id1")!, statusCode: 200, httpVersion: "1.1", headerFields: nil) + wait(for: [expectation], timeout: 10.0) - do { - let insertProccessed = try collectCallback.processResponse(data: insertErrorResponseData, response: insertErrorUrlResponse, error: nil) as! [String: [String: Any]] - XCTAssertEqual(insertProccessed["error"]?["message"] as? String, "Insert failed") - XCTAssertEqual(insertProccessed["error"]?["code"] as? Int, 404) - - let processedUpdate = try collectCallback.processUpdateResponse(data: updateResponseData, response: updateUrlResponse, error: nil, table: "table") - let updateRecords = processedUpdate["records"] as! [[String: Any]] - - XCTAssertEqual(updateRecords.count, 1) - XCTAssertEqual(updateRecords[0]["table"] as? String, "table") - let fields = updateRecords[0]["fields"] as? [String: String] - XCTAssertEqual(fields?["skyflow_id"], "id1") - XCTAssertEqual(fields?["field"], "newValue") - } catch { - XCTFail("Insert failure and update success scenario failed: \(error)") + guard let collectResponse = callback.collectResponse else { + XCTFail("Expected a merged CollectResponse, got: receivedResponse=\(callback.receivedResponse) data=\(callback.data)") + return } + XCTAssertEqual(collectResponse.records.count, 2) + XCTAssertTrue(collectResponse.records.contains { $0.tableName == "cards" && $0.skyflowID == "insertedId" }) + XCTAssertTrue(collectResponse.records.contains { $0.tableName == "persons" && $0.skyflowID == "id1" }) } - func testInsertFailureAndUpdateFailure() { - let expectation = XCTestExpectation(description: "Insert failure and update failure") - let callback = DemoAPICallback(expectation: expectation) - let insertRecord: [String: Any] = [ - "table": "table", - "fields": ["field": "value"] - ] + func testInsertSucceedsButUpdateFailsSurfacesAsSkyflowError() { + // One side of the merge fails outright (not a per-record error, a whole-side failure) - + // the merge should surface a proper Skyflow.SkyflowError with the real message, not a + // stringified dump of the internal {"records", "errors"} merge container. + let mockConfiguration = URLSessionConfiguration.ephemeral + mockConfiguration.protocolClasses = [MockURLProtocol.self] + let originalConfiguration = FlowVaultInsertAPICallback.urlSessionConfiguration + FlowVaultInsertAPICallback.urlSessionConfiguration = mockConfiguration + defer { FlowVaultInsertAPICallback.urlSessionConfiguration = originalConfiguration } + + MockURLProtocol.requestHandler = { request in + let url = request.url!.absoluteString + if url.contains("v2/records/insert") { + let body: [String: Any] = ["records": [["skyflowID": "insertedId", "tableName": "cards", "tokens": ["card_number": "value"], "httpCode": 200]]] + let data = try! JSONSerialization.data(withJSONObject: body) + let response = HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: "1.1", headerFields: nil)! + return (response, data) + } else if url.contains("v2/records/update") { + let body: [String: Any] = ["error": ["message": "Update service unavailable", "httpCode": 503]] + let data = try! JSONSerialization.data(withJSONObject: body) + let response = HTTPURLResponse(url: request.url!, statusCode: 503, httpVersion: "1.1", headerFields: nil)! + return (response, data) + } + XCTFail("Unexpected URL: \(url)") + return (HTTPURLResponse(url: request.url!, statusCode: 500, httpVersion: "1.1", headerFields: nil)!, Data()) + } - let updateRecord: [String: Any] = [ - "table": "table", - "skyflowID": "id1", - "fields": ["field": "newValue"] + let expectation = XCTestExpectation(description: "Update-side failure surfaces as SkyflowError") + let callback = DemoAPICallback(expectation: expectation) + + let records: [String: Any] = [ + "records": [["table": "cards", "fields": ["card_number": "4111"]]], + "update": ["id1": ["table": "persons", "fields": ["name": "John"]]] ] - let collectCallback = CollectAPICallback( - callback: callback, + let insertApiCallback = FlowVaultInsertAPICallback( + callback: callback.asCollectCallback, apiClient: APIClient(vaultID: "vault", vaultURL: "https://example.org/", tokenProvider: DemoTokenProvider()), - records: ["records": [insertRecord], "update": ["id1": updateRecord]], - options: ICOptions(tokens: true, additionalFields: nil), + records: records, + options: FlowVaultICOptions(), contextOptions: ContextOptions() ) + insertApiCallback.onSuccess("dummy-token") - // Simulate a failed insert response - let insertErrorResponseDict: [String: Any] = [ - "error": ["message": "Insert failed"] - ] - let insertErrorResponseData = try! JSONSerialization.data(withJSONObject: insertErrorResponseDict, options: .fragmentsAllowed) - let insertErrorUrlResponse = HTTPURLResponse(url: URL(string: "https://example.org/vault")!, statusCode: 404, httpVersion: "1.1", headerFields: nil) + wait(for: [expectation], timeout: 10.0) - // Simulate a successful update response - let updateResponseDict: [String: Any] = [ - "skyflow_id": "id1", - "tokens": ["field": "newValue"] - ] - let updateErrorResponseDict: [String: Any] = [ - "error": ["message": "Update failed"] - ] - let updateResponseData = try! JSONSerialization.data(withJSONObject: updateErrorResponseDict, options: .fragmentsAllowed) - let updateUrlResponse = HTTPURLResponse(url: URL(string: "https://example.org/table/id1")!, statusCode: 404, httpVersion: "1.1", headerFields: nil) + XCTAssertEqual(callback.receivedResponse, "Update service unavailable") + } - do { - let insertProccessed = try collectCallback.processResponse(data: insertErrorResponseData, response: insertErrorUrlResponse, error: nil) as! [String: [String: Any]] - XCTAssertEqual(insertProccessed["error"]?["message"] as? String, "Insert failed") - XCTAssertEqual(insertProccessed["error"]?["code"] as? Int, 404) + func testCollectRecordHttpCodeDefaultsToZeroWhenMissing() { + // httpCode is declared non-optional (Int, not Int?) - verify a dict lacking the key + // doesn't crash and just defaults to 0 instead. + let record = CollectRecord(["tableName": "persons", "skyflowID": "id1"]) + XCTAssertEqual(record.httpCode, 0) + XCTAssertNil(record.error) + XCTAssertEqual(record.tableName, "persons") + } - let processedUpdate = try collectCallback.processUpdateResponse(data: updateResponseData, response: updateUrlResponse, error: nil, table: "table") as! [String: [String: Any]] + func testCollectRecordErrorDiscriminatesSuccessFromFailure() { + let success = CollectRecord(["tableName": "persons", "httpCode": 200]) + let failure = CollectRecord(["error": "Invalid request", "httpCode": 400]) - XCTAssertEqual(processedUpdate["error"]?["message"] as? String, "Update failed") - XCTAssertEqual(processedUpdate["error"]?["code"] as? Int, 404) + XCTAssertNil(success.error) + XCTAssertEqual(failure.error, "Invalid request") + XCTAssertEqual(failure.httpCode, 400) + } - } catch { - XCTFail("Insert failure and update success scenario failed: \(error)") - } + func testCollectResponseInitReturnsNilForMalformedBody() { + XCTAssertNil(CollectResponse("not a dictionary")) + XCTAssertNil(CollectResponse(["typo": []])) + XCTAssertNil(CollectResponse(["records": "not an array"])) + } + + func testCollectResponseInitParsesValidBody() { + let response = CollectResponse([ + "records": [ + ["tableName": "persons", "skyflowID": "id1", "httpCode": 200], + ["error": "failed", "httpCode": 400] + ] + ]) + XCTAssertEqual(response?.records.count, 2) + XCTAssertNil(response?.records[0].error) + XCTAssertEqual(response?.records[1].error, "failed") } } diff --git a/Tests/skyflow-iOS-utilTests/skyflow_iOS_dormantCollectApiCallbackTests.swift b/Tests/skyflow-iOS-utilTests/skyflow_iOS_dormantCollectApiCallbackTests.swift new file mode 100644 index 00000000..9da2f33e --- /dev/null +++ b/Tests/skyflow-iOS-utilTests/skyflow_iOS_dormantCollectApiCallbackTests.swift @@ -0,0 +1,249 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// Unit tests for the dormant v1 PDB CollectAPICallback/InsertAPICallback (kept for potential +// future PDB reuse). + +import XCTest +@testable import Skyflow + +final class skyflow_iOS_dormantCollectApiCallbackTests: XCTestCase { + var collectCallback: CollectAPICallback! + var defaultRecord: [String: Any] = ["records": [["table": "table", "fields": ["field": "value"]]]] + + override func setUp() { + self.collectCallback = CollectAPICallback( + callback: DemoAPICallback(expectation: XCTestExpectation()), + apiClient: APIClient(vaultID: "vault", vaultURL: "https://example.org/", tokenProvider: DemoTokenProvider()), + records: defaultRecord, + options: ICOptions(tokens: false), + contextOptions: ContextOptions() + ) + } + + func testBuildFieldsDict() { + let dict = ["key": "value", "nested": ["key": "value"]] as [String: Any] + let result = self.collectCallback.buildFieldsDict(dict: dict) + XCTAssertEqual(dict["key"] as! String, result["key"] as! String) + XCTAssertEqual(dict["nested"] as! [String: String], result["nested"] as! [String: String]) + } + + func testOnSuccessWithNoInsertAndNoUpdateRecords() { + // No network call should be made at all when there's nothing to insert or update. + let expectation = XCTestExpectation(description: "Empty batch should succeed with an empty response") + let callback = DemoAPICallback(expectation: expectation) + let emptyCallback = CollectAPICallback( + callback: callback, + apiClient: APIClient(vaultID: "vault", vaultURL: "https://example.org/", tokenProvider: DemoTokenProvider()), + records: ["records": []], + options: ICOptions(tokens: false), + contextOptions: ContextOptions() + ) + + emptyCallback.onSuccess("token") + wait(for: [expectation], timeout: 10.0) + + XCTAssertTrue(callback.data.isEmpty) + } + + func testGetRequestSession() { + let url = URL(string: "https://example.org")! + do { + let (request, session) = try self.collectCallback.getRequestSession(url: url) + XCTAssertEqual(request.httpMethod, "POST") + XCTAssertEqual(request.allHTTPHeaderFields?["Authorization"], "Bearer ") + let body = try JSONSerialization.jsonObject(with: request.httpBody!, options: .allowFragments) as! [String: Any] + let records = body["records"] as! [[String: Any]] + XCTAssertEqual(records.count, 1) + XCTAssertEqual(records[0]["tableName"] as? String, "table") + XCTAssertNotNil(session) + } catch { + XCTFail(error.localizedDescription) + } + } + + func testProcessResponseNetworkError() { + let networkError = NSError(domain: "NSURLErrorDomain", code: -1009, userInfo: [NSLocalizedDescriptionKey: "offline"]) + do { + let result = try self.collectCallback.processResponse(data: nil, response: nil, error: networkError) + let errorDict = result["error"] as! [String: Any] + XCTAssertEqual(errorDict["message"] as? String, "offline") + } catch { + XCTFail("Should not throw for a connection-level error: \(error)") + } + } + + func testProcessResponseBadStatusCode() { + let responseDict = ["error": ["message": "Internal Server Error"]] + do { + let data = try JSONSerialization.data(withJSONObject: responseDict, options: .fragmentsAllowed) + let httpResponse = HTTPURLResponse(url: URL(string: "https://example.org")!, statusCode: 500, httpVersion: "1.1", headerFields: ["x-request-id": "RID"]) + let result = try self.collectCallback.processResponse(data: data, response: httpResponse, error: nil) + let errorDict = result["error"] as! [String: Any] + XCTAssertEqual(errorDict["message"] as? String, "Internal Server Error - request-id: RID") + } catch { + XCTFail(error.localizedDescription) + } + } + + func testGetCollectResponseBody() { + // Old v1 gateway-batch response shape: {"responses": [{"records": [{"skyflow_id": ...}]}]} + // getCollectResponseBody walks self.records["records"] (the INPUT records), reading the + // matching entry out of the responses array at the same index - so the input record count + // must match, not the responses array's own length. + let twoRecordCallback = CollectAPICallback( + callback: DemoAPICallback(expectation: XCTestExpectation()), + apiClient: APIClient(vaultID: "vault", vaultURL: "https://example.org/", tokenProvider: DemoTokenProvider()), + records: ["records": [["table": "table1", "fields": ["field": "value1"]], ["table": "table2", "fields": ["field": "value2"]]]], + options: ICOptions(tokens: false), + contextOptions: ContextOptions() + ) + let responseDict: [String: Any] = [ + "responses": [ + ["records": [["skyflow_id": "SID1"]]], + ["records": [["skyflow_id": "SID2"]]] + ] + ] + do { + let data = try JSONSerialization.data(withJSONObject: responseDict, options: .fragmentsAllowed) + let result = try twoRecordCallback.getCollectResponseBody(data: data) + let records = result["records"] as! [[String: Any]] + XCTAssertEqual(records.count, 2) + XCTAssertEqual(records[0]["table"] as? String, "table1") + XCTAssertEqual(records[0]["skyflow_id"] as? String, "SID1") + XCTAssertEqual(records[1]["table"] as? String, "table2") + XCTAssertEqual(records[1]["skyflow_id"] as? String, "SID2") + } catch { + XCTFail(error.localizedDescription) + } + } + + func testProcessUpdateResponseSuccess() { + let responseDict: [String: Any] = ["skyflow_id": "SID1"] + do { + let data = try JSONSerialization.data(withJSONObject: responseDict, options: .fragmentsAllowed) + let httpResponse = HTTPURLResponse(url: URL(string: "https://example.org")!, statusCode: 200, httpVersion: "1.1", headerFields: nil) + let result = try self.collectCallback.processUpdateResponse(data: data, response: httpResponse, error: nil, table: "table") + let records = result["records"] as! [[String: Any]] + XCTAssertEqual(records.count, 1) + XCTAssertEqual(records[0]["skyflow_id"] as? String, "SID1") + XCTAssertEqual(records[0]["table"] as? String, "table") + } catch { + XCTFail(error.localizedDescription) + } + } + + func testProcessUpdateResponseBadStatusCode() { + let responseDict = ["error": ["message": "Record not found"]] + do { + let data = try JSONSerialization.data(withJSONObject: responseDict, options: .fragmentsAllowed) + let httpResponse = HTTPURLResponse(url: URL(string: "https://example.org")!, statusCode: 404, httpVersion: "1.1", headerFields: nil) + let result = try self.collectCallback.processUpdateResponse(data: data, response: httpResponse, error: nil, table: "table") + let errorDict = result["error"] as! [String: Any] + XCTAssertEqual(errorDict["message"] as? String, "Record not found") + } catch { + XCTFail(error.localizedDescription) + } + } + + func testOnFailure() { + let expectation = XCTestExpectation() + let callback = DemoAPICallback(expectation: expectation) + self.collectCallback.callback = callback + + self.collectCallback.onFailure(["errors": [["error": "boom"]]]) + wait(for: [expectation], timeout: 10.0) + + XCTAssertNotNil(callback.data["errors"]) + } +} + +final class skyflow_iOS_dormantInsertApiCallbackTests: XCTestCase { + var insertCallback: InsertAPICallback! + var defaultRecord: [String: Any] = ["records": [["table": "table", "fields": ["field": "value"]]]] + + override func setUp() { + self.insertCallback = InsertAPICallback( + callback: DemoAPICallback(expectation: XCTestExpectation()), + apiClient: APIClient(vaultID: "vault", vaultURL: "https://example.org/", tokenProvider: DemoTokenProvider()), + records: defaultRecord, + options: ICOptions(tokens: false), + contextOptions: ContextOptions() + ) + } + + func testBuildFieldsDict() { + let dict = ["key": "value", "nested": ["key": "value"]] as [String: Any] + let result = self.insertCallback.buildFieldsDict(dict: dict) + XCTAssertEqual(dict["key"] as! String, result["key"] as! String) + XCTAssertEqual(dict["nested"] as! [String: String], result["nested"] as! [String: String]) + } + + func testGetRequestSession() { + let url = URL(string: "https://example.org")! + do { + let (request, session) = try self.insertCallback.getRequestSession(url: url) + XCTAssertEqual(request.httpMethod, "POST") + XCTAssertEqual(request.allHTTPHeaderFields?["Authorization"], "Bearer ") + let body = try JSONSerialization.jsonObject(with: request.httpBody!, options: .allowFragments) as! [String: Any] + let records = body["records"] as! [[String: Any]] + XCTAssertEqual(records.count, 1) + XCTAssertEqual(records[0]["tableName"] as? String, "table") + XCTAssertNotNil(session) + } catch { + XCTFail(error.localizedDescription) + } + } + + func testProcessResponseNetworkErrorThrows() { + let networkError = NSError(domain: "NSURLErrorDomain", code: -1009, userInfo: [NSLocalizedDescriptionKey: "offline"]) + do { + _ = try self.insertCallback.processResponse(data: nil, response: nil, error: networkError) + XCTFail("Should throw on a connection-level error") + } catch { + XCTAssertEqual((error as NSError).code, -1009) + } + } + + func testProcessResponseBadStatusCodeThrows() { + let responseDict = ["error": ["message": "Internal Server Error"]] + do { + let data = try JSONSerialization.data(withJSONObject: responseDict, options: .fragmentsAllowed) + let httpResponse = HTTPURLResponse(url: URL(string: "https://example.org")!, statusCode: 500, httpVersion: "1.1", headerFields: ["x-request-id": "RID"]) + _ = try self.insertCallback.processResponse(data: data, response: httpResponse, error: nil) + XCTFail("Should throw on a bad status code") + } catch { + XCTAssertEqual(error.localizedDescription, "Internal Server Error - request-id: RID") + } + } + + func testGetCollectResponseBody() { + let responseDict: [String: Any] = [ + "responses": [ + ["records": [["skyflow_id": "SID1"]]] + ] + ] + do { + let data = try JSONSerialization.data(withJSONObject: responseDict, options: .fragmentsAllowed) + let result = try self.insertCallback.getCollectResponseBody(data: data) + let records = result["records"] as! [[String: Any]] + XCTAssertEqual(records.count, 1) + XCTAssertEqual(records[0]["table"] as? String, "table") + XCTAssertEqual(records[0]["skyflow_id"] as? String, "SID1") + } catch { + XCTFail(error.localizedDescription) + } + } + + func testOnFailure() { + let expectation = XCTestExpectation() + let callback = DemoAPICallback(expectation: expectation) + self.insertCallback.callback = callback + + self.insertCallback.onFailure(["errors": [["error": "boom"]]]) + wait(for: [expectation], timeout: 10.0) + + XCTAssertNotNil(callback.data["errors"]) + } +} diff --git a/Tests/skyflow-iOS-utilTests/skyflow_iOS_dormantCollectRequestBodyTests.swift b/Tests/skyflow-iOS-utilTests/skyflow_iOS_dormantCollectRequestBodyTests.swift new file mode 100644 index 00000000..5f119654 --- /dev/null +++ b/Tests/skyflow-iOS-utilTests/skyflow_iOS_dormantCollectRequestBodyTests.swift @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// Unit tests for the dormant v1 PDB CollectRequestBody (kept for potential future PDB reuse). +// Distinct from FlowVaultCollectRequestBody, which is the live class covering this same logic. + +import XCTest +@testable import Skyflow + +final class skyflow_iOS_dormantCollectRequestBodyTests: XCTestCase { + var skyflow: Client! + + override func setUp() { + self.skyflow = Client(Configuration(vaultID: "id", vaultURL: "http://demo.com", tokenProvider: DemoTokenProvider(), options: Options(logLevel: .DEBUG))) + } + + func testCreateRequestBodySimpleInsert() { + let window = UIWindow() + let container = skyflow.container(type: ContainerType.COLLECT, options: nil) + let options = CollectElementOptions(required: false) + + let cardNumberInput = CollectElementInput(table: "persons", column: "card_number", type: .CARD_NUMBER) + let cardNumber = container?.create(input: cardNumberInput, options: options) + cardNumber?.textField.secureText = "4111 1111 1111 1111" + window.addSubview(cardNumber!) + + let callback = DemoAPICallback(expectation: XCTestExpectation(description: "Pure insert")) + let requestBody = CollectRequestBody.createRequestBody(elements: [cardNumber!], callback: callback, contextOptions: ContextOptions()) + + XCTAssertNotNil(requestBody) + let records = requestBody?["records"] as! [[String: Any]] + XCTAssertEqual(records.count, 1) + XCTAssertEqual(records[0]["table"] as? String, "persons") + let update = requestBody?["update"] as! [String: Any] + XCTAssertTrue(update.isEmpty) + } + + func testCreateRequestBodyWithSkyflowIDInAdditionalFields() { + let additionalFields: [String: Any] = [ + "records": [ + ["table": "table1", "fields": ["column1": "value1"], "skyflowId": "id1"] + ] + ] + let callback = DemoAPICallback(expectation: XCTestExpectation(description: "Update via additionalFields")) + let requestBody = CollectRequestBody.createRequestBody(elements: [], additionalFields: additionalFields, callback: callback, contextOptions: ContextOptions()) + + XCTAssertNotNil(requestBody) + XCTAssertEqual((requestBody?["records"] as! [[String: Any]]).count, 0) + let update = requestBody?["update"] as! [String: Any] + let entry = update["id1"] as! [String: Any] + XCTAssertEqual(entry["table"] as! String, "table1") + XCTAssertEqual(entry["fields"] as! [String: String], ["column1": "value1"]) + } + + func testCreateRequestBodyWithSkyflowIDOnElement() { + let window = UIWindow() + let container = skyflow.container(type: ContainerType.COLLECT, options: nil) + let options = CollectElementOptions(required: false) + + let cardNumberInput = CollectElementInput(table: "persons", column: "card_number", type: .CARD_NUMBER, skyflowId: "id1") + let cardNumber = container?.create(input: cardNumberInput, options: options) + cardNumber?.textField.secureText = "4111 1111 1111 1111" + window.addSubview(cardNumber!) + + let callback = DemoAPICallback(expectation: XCTestExpectation(description: "Update via element skyflowID")) + let requestBody = CollectRequestBody.createRequestBody(elements: [cardNumber!], callback: callback, contextOptions: ContextOptions()) + + XCTAssertNotNil(requestBody) + XCTAssertEqual((requestBody?["records"] as! [[String: Any]]).count, 0) + let update = requestBody?["update"] as! [String: Any] + let entry = update["id1"] as! [String: Any] + XCTAssertEqual(entry["table"] as! String, "persons") + XCTAssertEqual(entry["fields"] as! [String: String], ["card_number": ""]) + } + + func testCreateRequestBodyDuplicateAdditionalFieldError() { + let additionalFields: [String: Any] = [ + "records": [ + ["table": "table1", "fields": ["column1": "value1"]], + ["table": "table1", "fields": ["column1": "value2"]] + ] + ] + let expectation = XCTestExpectation(description: "Duplicate additional field should fail") + let callback = DemoAPICallback(expectation: expectation) + let requestBody = CollectRequestBody.createRequestBody(elements: [], additionalFields: additionalFields, callback: callback, contextOptions: ContextOptions()) + + XCTAssertNil(requestBody) + XCTAssertEqual(callback.receivedResponse, ErrorCodes.DUPLICATE_ADDITIONAL_FIELD_FOUND(value: "column1").getErrorObject(contextOptions: ContextOptions()).localizedDescription) + } + + func testGetUniqueColumn() { + let upsert: [[String: Any]] = [ + ["table": "table1", "column": "email"], + ["table": "table2", "column": "ssn"] + ] + + XCTAssertEqual(CollectRequestBody.getUniqueColumn(tableName: "table1", upsert: upsert), "email") + XCTAssertEqual(CollectRequestBody.getUniqueColumn(tableName: "table2", upsert: upsert), "ssn") + XCTAssertEqual(CollectRequestBody.getUniqueColumn(tableName: "table3", upsert: upsert), "") + } +} diff --git a/Tests/skyflow-iOS-utilTests/skyflow_iOS_insertUtilTests 2.swift b/Tests/skyflow-iOS-utilTests/skyflow_iOS_insertUtilTests 2.swift index e3becb93..8bab8c4f 100644 --- a/Tests/skyflow-iOS-utilTests/skyflow_iOS_insertUtilTests 2.swift +++ b/Tests/skyflow-iOS-utilTests/skyflow_iOS_insertUtilTests 2.swift @@ -7,137 +7,175 @@ import XCTest final class skyflow_iOS_insertUtilTests: XCTestCase { - var collectCallback: InsertAPICallback! = nil + var collectCallback: FlowVaultInsertAPICallback! = nil var defaultRecord: [String: Any] = ["records": [["table": "table", "fields": ["field": "value"]]]] - + override func setUp() { - self.collectCallback = InsertAPICallback(callback: DemoAPICallback(expectation: XCTestExpectation()), + self.collectCallback = FlowVaultInsertAPICallback(callback: DemoAPICallback(expectation: XCTestExpectation()), apiClient: APIClient(vaultID: "", vaultURL: "", tokenProvider: DemoTokenProvider()), records: defaultRecord, - options: ICOptions(tokens: false, additionalFields: nil), + options: FlowVaultICOptions(additionalFields: nil), contextOptions: ContextOptions()) } - + func testBuildFieldsDict() { let dict = ["key": "value", "nested": ["key": "value"]] as [String: Any] let result = self.collectCallback.buildFieldsDict(dict: dict) XCTAssertEqual(dict["key"] as! String, result["key"] as! String) XCTAssertEqual(dict["nested"] as! [String: String], result["nested"] as! [String: String]) } - + func testOnSuccessInvalidUrl() { let expectation = XCTestExpectation(description: "Invalid URL should trigger failure") let callback = DemoAPICallback(expectation: expectation) self.collectCallback.apiClient.vaultURL = "Invalid url" self.collectCallback.callback = callback - + self.collectCallback.onSuccess("string") wait(for: [expectation], timeout: 20.0) - - let result = callback.receivedResponse - XCTAssert(result.contains("unsupported URL")) + + let errorObject = callback.data["error"] as! [String: Any] + let msg = errorObject["message"] as! String + XCTAssert(msg.contains("unsupported URL")) } - + func testGetRequestSession() { let url = URL(string: "https://example.org")! do { let (request, session) = try self.collectCallback.getRequestSession(url: url) - + XCTAssertEqual(request.allHTTPHeaderFields!["Authorization"], "Bearer ") // From DemoTokenProvider() + XCTAssertEqual(request.allHTTPHeaderFields!["Content-Type"], "application/json") + XCTAssertEqual(request.allHTTPHeaderFields!["Accept"], "application/json") let body = try JSONSerialization.jsonObject(with: request.httpBody!, options: .allowFragments) as! [String: Any] let records = body["records"] as! [[String: Any]] XCTAssertEqual(records.count, 1) - XCTAssertEqual(records[0]["fields"] as! [String: String], ["field": "value"]) - XCTAssertEqual(records[0]["method"] as! String, "POST") + XCTAssertEqual(records[0]["data"] as! [String: String], ["field": "value"]) XCTAssertEqual(records[0]["tableName"] as! String, "table") - + } catch { XCTFail(error.localizedDescription) } } - + func testGetCollectResponse() { - let response = ["responses": [["records": [["skyflow_id": "SID"]]]]] - + let response = ["records": [["skyflowID": "SID", "tableName": "table"]]] + do { let data = try JSONSerialization.data(withJSONObject: response, options: .fragmentsAllowed) let result = try self.collectCallback.getCollectResponseBody(data: data) - - XCTAssertEqual(result as! [String: [[String: String]]], ["records": [ - [ - "table": "table", - "skyflow_id": "SID" - ] - ]]) + let records = result["records"] as! [[String: Any]] + + XCTAssertEqual(records.count, 1) + XCTAssertEqual(records[0]["tableName"] as! String, "table") + XCTAssertEqual(records[0]["skyflowID"] as! String, "SID") } catch { XCTFail(error.localizedDescription) } } - + func testGetCollectResponseWithTokens() { - let response = ["responses": [["records": [["skyflow_id": "SID"]]], ["fields": ["field": "value"]]]] - self.collectCallback.options = ICOptions() - + let response = ["records": [["skyflowID": "SID", "tableName": "table", "tokens": ["field": [["token": "tok", "tokenGroupName": "group"]]]]]] as [String: Any] + self.collectCallback.options = FlowVaultICOptions() + do { let data = try JSONSerialization.data(withJSONObject: response, options: .fragmentsAllowed) let result = try self.collectCallback.getCollectResponseBody(data: data) let records = result["records"] as! [[String: Any]] - - let expected = ["records": [["table": "table", "fields": ["field": "value", "skyflow_id": "SID"]]]] - - XCTAssertEqual(records.count, expected["records"]!.count) - XCTAssertEqual(records[0]["table"] as! String, expected["records"]![0]["table"] as! String) - XCTAssertEqual(records[0]["fields"] as! [String: String], expected["records"]![0]["fields"] as! [String: String]) - - + + XCTAssertEqual(records.count, 1) + XCTAssertEqual(records[0]["tableName"] as! String, "table") + XCTAssertEqual(records[0]["skyflowID"] as! String, "SID") + let fields = records[0]["fields"] as! [String: Any] + let fieldTokens = fields["field"] as! [[String: Any]] + XCTAssertEqual(fieldTokens[0]["token"] as? String, "tok") } catch { XCTFail(error.localizedDescription) } } - + + func testGetCollectResponseWithHashedData() { + let response: [String: Any] = ["records": [[ + "skyflowID": "SID", + "tableName": "table", + "httpCode": 200, + "data": ["field": "value"], + "hashedData": ["field": "hashed-value"] + ]]] + self.collectCallback.options = FlowVaultICOptions() + + do { + let data = try JSONSerialization.data(withJSONObject: response, options: .fragmentsAllowed) + let result = try self.collectCallback.getCollectResponseBody(data: data) + let records = result["records"] as! [[String: Any]] + + XCTAssertEqual(records.count, 1) + XCTAssertEqual(records[0]["httpCode"] as? Int, 200) + let fields = records[0]["fields"] as! [String: Any] + XCTAssertNil(fields["data"]) + XCTAssertEqual(records[0]["hashedData"] as! [String: String], ["field": "hashed-value"]) + } catch { + XCTFail(error.localizedDescription) + } + } + func testProcessResponse() { - let response = ["responses": [["records": [["skyflow_id": "SID"]]], ["fields": ["field": "value"]]]] - + let response = ["records": [["skyflowID": "SID", "tableName": "table"]]] + do { let data = try JSONSerialization.data(withJSONObject: response, options: .fragmentsAllowed) let response = HTTPURLResponse(url: URL(string: "https://example.org")!, statusCode: 200, httpVersion: "1.1", headerFields: nil) - - let processedData = try self.collectCallback.processResponse(data: data, response: response, error: nil) as! [String: [[String: String]]] - XCTAssertEqual(processedData, ["records": [["skyflow_id": "SID", "table": "table"]]]) - + + let processedData = try self.collectCallback.processResponse(data: data, response: response, error: nil) + let records = processedData["records"] as! [[String: Any]] + XCTAssertEqual(records[0]["tableName"] as! String, "table") + XCTAssertEqual(records[0]["skyflowID"] as! String, "SID") + } catch { XCTFail(error.localizedDescription) } } - + func testProcessResponseError() { - let response = ["responses": [["records": [["skyflow_id": "SID"]]], ["fields": ["field": "value"]]]] - + // A genuine connection/network-level error (URLSession error, no HTTP response reached) + // should not throw - it returns a top-level {"error": {...}} dict directly. + let networkError = NSError(domain: "NSURLErrorDomain", code: -1009, userInfo: [NSLocalizedDescriptionKey: "The Internet connection appears to be offline."]) + do { - let data = try JSONSerialization.data(withJSONObject: response, options: .fragmentsAllowed) - let response = HTTPURLResponse(url: URL(string: "https://example.org")!, statusCode: 200, httpVersion: "1.1", headerFields: nil) - - let processedData = try self.collectCallback.processResponse(data: data, response: response, error: NSError(domain: "", code: 400, userInfo: nil)) as! [String: [[String: String]]] - XCTFail("Should have thrown error") - + let processedData = try self.collectCallback.processResponse(data: nil, response: nil, error: networkError) + let errorDict = processedData["error"] as! [String: Any] + XCTAssertEqual(errorDict["message"] as? String, "The Internet connection appears to be offline.") + } catch { + XCTFail("Should not throw for a connection-level error: \(error)") + } + } + + func testGetCollectResponseBodyWithMalformedTopLevelJSON() { + // A response body that parses as valid JSON but isn't a top-level object (e.g. a bare + // array or scalar) must not crash - it should fall back to an empty result. + do { + let arrayData = try JSONSerialization.data(withJSONObject: ["not", "an", "object"], options: .fragmentsAllowed) + let result = try self.collectCallback.getCollectResponseBody(data: arrayData) + XCTAssertEqual((result["records"] as? [[String: Any]])?.count, 0) } catch { + XCTFail("Malformed top-level JSON should not throw or crash: \(error)") } } - + func testProcessResponseFailure() { let response = ["error": ["message": "Internal Server Error"]] do { let data = try JSONSerialization.data(withJSONObject: response, options: .fragmentsAllowed) let response = HTTPURLResponse(url: URL(string: "https://example.org")!, statusCode: 500, httpVersion: "1.1", headerFields: ["x-request-id": "RID"]) - - try self.collectCallback.processResponse(data: data, response: response, error: nil) - XCTFail("Not throwing on Api Error") - + + let res = try self.collectCallback.processResponse(data: data, response: response, error: nil) + let message = (res["error"] as! [String: Any])["message"] as! String + XCTAssertEqual(message, "Internal Server Error - request-id: RID") } catch { - XCTAssertEqual(error.localizedDescription, "Internal Server Error - request-id: RID") + XCTFail("Should not throw on Api Error: \(error)") } } - + func testCollectInvalidBearerToken() { let expectation = XCTestExpectation() let callback = DemoAPICallback(expectation: expectation) @@ -145,11 +183,11 @@ final class skyflow_iOS_insertUtilTests: XCTestCase { let container = client.container(type: ContainerType.COLLECT) let input = CollectElementInput(table: "table", column: "column", type: .EXPIRATION_YEAR) let element = container?.create(input: input) - + UIWindow().addSubview(element!) - - container?.collect(callback: callback) - + + container?.collect(callback: callback.asCollectCallback) + wait(for: [expectation], timeout: 20.0) XCTAssertTrue(callback.receivedResponse.contains("Token generated from 'getBearerToken' callback function is invalid")) } @@ -161,25 +199,25 @@ final class skyflow_iOS_insertUtilTests: XCTestCase { XCTAssertEqual(device.systemName + "@" + device.systemVersion, deviceInfo["sdk_client_os_details"] as! String) } func testDeviceDetails() { - + let deviceDetails = FetchMetrices().getDeviceDetails() - + XCTAssertNotNil(deviceDetails["device"]) XCTAssertNotNil(deviceDetails["os_details"]) XCTAssertNotNil(deviceDetails["sdk_name_version"]) - + if let device = deviceDetails["device"] as? String { XCTAssertFalse(device.isEmpty) } else { XCTAssertTrue(deviceDetails["device"] as! String == "") } - + if let osDetails = deviceDetails["os_details"] as? String { XCTAssertFalse(osDetails.isEmpty) } else { XCTAssertTrue(deviceDetails["os_details"] as! String == "") } - + if let sdkNameVersion = deviceDetails["sdk_name_version"] as? String { XCTAssertFalse(sdkNameVersion.isEmpty) } else { @@ -196,30 +234,70 @@ final class skyflow_iOS_insertUtilTests: XCTestCase { "fields": ["field": "value"] ] - let collectCallback = InsertAPICallback( + let collectCallback = FlowVaultInsertAPICallback( callback: callback, apiClient: APIClient(vaultID: "vault", vaultURL: "https://example.org/", tokenProvider: DemoTokenProvider()), records: ["records": [insertRecord]], - options: ICOptions(tokens: true, additionalFields: nil), + options: FlowVaultICOptions(additionalFields: nil), contextOptions: ContextOptions() ) // Simulate a successful insert response - let insertResponseDict = ["responses": [["records": [["skyflow_id": "SID"]]], ["fields": ["field": "value"]]]] + let insertResponseDict: [String: Any] = ["records": [["skyflowID": "SID", "tableName": "table", "tokens": ["field": "value"]]]] let insertResponseData = try! JSONSerialization.data(withJSONObject: insertResponseDict, options: .fragmentsAllowed) - let insertUrlResponse = HTTPURLResponse(url: URL(string: "https://example.org/vault")!, statusCode: 200, httpVersion: "1.1", headerFields: nil) + let insertUrlResponse = HTTPURLResponse(url: URL(string: "https://example.org/v2/records/insert")!, statusCode: 200, httpVersion: "1.1", headerFields: nil) do { let processedInsert = try collectCallback.processResponse(data: insertResponseData, response: insertUrlResponse, error: nil) let insertRecords = processedInsert["records"] as! [[String: Any]] XCTAssertEqual(insertRecords.count, 1) - let fields = insertRecords[0]["fields"] as? [String: String] - XCTAssertEqual(fields?["skyflow_id"] as? String, "SID") + XCTAssertEqual(insertRecords[0]["tableName"] as? String, "table") + XCTAssertEqual(insertRecords[0]["skyflowID"] as? String, "SID") + let fields = insertRecords[0]["fields"] as! [String: Any] + XCTAssertEqual(fields["field"] as? String, "value") } catch { XCTFail("Insert scenario failed: \(error)") } } + func testInsertFullFailureWithNon2xxOuterStatus() { + let expectation = XCTestExpectation(description: "Full batch failure with 400 outer status should still parse per-record errors") + let callback = DemoAPICallback(expectation: expectation) + + let insertRecord: [String: Any] = [ + "table": "table", + "fields": ["field": "value"] + ] + + let collectCallback = FlowVaultInsertAPICallback( + callback: callback, + apiClient: APIClient(vaultID: "vault", vaultURL: "https://example.org/", tokenProvider: DemoTokenProvider()), + records: ["records": [insertRecord]], + options: FlowVaultICOptions(additionalFields: nil), + contextOptions: ContextOptions() + ) + + let responseDict: [String: Any] = ["records": [ + ["skyflowID": NSNull(), "tokens": NSNull(), "data": NSNull(), "hashedData": NSNull(), + "error": "Invalid request. Table name table not present for record. Specify a valid table name.", + "httpCode": 400, "tableName": ""] + ]] + let responseData = try! JSONSerialization.data(withJSONObject: responseDict, options: .fragmentsAllowed) + let urlResponse = HTTPURLResponse(url: URL(string: "https://example.org/v2/records/insert")!, statusCode: 400, httpVersion: "1.1", headerFields: nil) + + do { + let processed = try collectCallback.processResponse(data: responseData, response: urlResponse, error: nil) + XCTAssertNil(processed["error"], "Should not collapse into a generic top-level error when the body has a records array") + let records = processed["records"] as! [[String: Any]] + + XCTAssertEqual(records.count, 1) + XCTAssertEqual(records[0]["error"] as? String, "Invalid request. Table name table not present for record. Specify a valid table name.") + XCTAssertEqual(records[0]["httpCode"] as? Int, 400) + } catch { + XCTFail("Full failure scenario should not throw: \(error)") + } + } + } diff --git a/Tests/skyflow-iOS-utilTests/skyflow_iOS_revealApiCallbackTests.swift b/Tests/skyflow-iOS-utilTests/skyflow_iOS_revealApiCallbackTests.swift new file mode 100644 index 00000000..c18a05e5 --- /dev/null +++ b/Tests/skyflow-iOS-utilTests/skyflow_iOS_revealApiCallbackTests.swift @@ -0,0 +1,178 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// Unit tests for the dormant v1 PDB RevealAPICallback (kept for potential future PDB reuse). + +import XCTest +@testable import Skyflow + +final class skyflow_iOS_revealApiCallbackTests: XCTestCase { + var revealApiCallback: RevealAPICallback! + var expectation: XCTestExpectation! + var callback: DemoAPICallback! + + override func setUp() { + self.expectation = XCTestExpectation() + self.callback = DemoAPICallback(expectation: self.expectation) + self.revealApiCallback = RevealAPICallback( + callback: self.callback, + apiClient: APIClient(vaultID: "vault", vaultURL: "https://example.org/", tokenProvider: DemoTokenProvider()), + connectionUrl: "https://example.org/v1/vaults/vault", + records: [RevealRequestRecord(token: "token1")], + contextOptions: ContextOptions() + ) + } + + func testGetRequestSession() { + let (request, _) = self.revealApiCallback.getRequestSession() + + XCTAssertEqual(request.url?.absoluteString, "https://example.org/v1/vaults/vault/detokenize") + XCTAssertEqual(request.httpMethod, "POST") + XCTAssertEqual(request.allHTTPHeaderFields?["Content-Type"], "application/json; utf-8") + XCTAssertEqual(request.allHTTPHeaderFields?["Accept"], "application/json") + XCTAssertEqual(request.allHTTPHeaderFields?["Authorization"], "Bearer ") + } + + func testGetRevealRequestBody() { + do { + let record = RevealRequestRecord(token: "abc123") + let data = try self.revealApiCallback.getRevealRequestBody(record: record) + let json = try JSONSerialization.jsonObject(with: data) as! [String: Any] + let params = json["detokenizationParameters"] as! [[String: Any]] + + XCTAssertEqual(params.count, 1) + XCTAssertEqual(params[0]["token"] as? String, "abc123") + XCTAssertEqual(params[0]["redaction"] as? String, "PLAIN_TEXT") + } catch { + XCTFail(error.localizedDescription) + } + } + + func testProcessResponseSuccess() { + do { + let record = RevealRequestRecord(token: "abc123") + let responseDict = ["records": [["token": "abc123", "value": "revealed-value"]]] + let data = try JSONSerialization.data(withJSONObject: responseDict) + let httpResponse = HTTPURLResponse(url: URL(string: "https://example.org")!, statusCode: 200, httpVersion: "1.1", headerFields: nil) + + let (success, failure) = try self.revealApiCallback.processResponse(record: record, data: data, response: httpResponse, error: nil) + + XCTAssertNil(failure) + XCTAssertEqual(success?.token_id, "abc123") + XCTAssertEqual(success?.value, "revealed-value") + } catch { + XCTFail(error.localizedDescription) + } + } + + func testProcessResponseBadStatusCode() { + do { + let record = RevealRequestRecord(token: "badtoken") + let responseDict = ["error": ["message": "Invalid Token"]] + let data = try JSONSerialization.data(withJSONObject: responseDict) + let httpResponse = HTTPURLResponse(url: URL(string: "https://example.org")!, statusCode: 404, httpVersion: "1.1", headerFields: ["x-request-id": "RID"]) + + let (success, failure) = try self.revealApiCallback.processResponse(record: record, data: data, response: httpResponse, error: nil) + + XCTAssertNil(success) + XCTAssertEqual(failure?.id, "badtoken") + XCTAssertEqual(failure?.error.localizedDescription, "Invalid Token - request-id: RID") + } catch { + XCTFail(error.localizedDescription) + } + } + + func testProcessResponseNetworkError() { + let record = RevealRequestRecord(token: "abc123") + let networkError = NSError(domain: "NSURLErrorDomain", code: -1009, userInfo: [NSLocalizedDescriptionKey: "The Internet connection appears to be offline."]) + + do { + _ = try self.revealApiCallback.processResponse(record: record, data: nil, response: nil, error: networkError) + XCTFail("Should throw on a connection-level error") + } catch { + XCTAssertEqual((error as NSError).code, -1009) + } + } + + func testHandleCallbacksAllSuccess() { + let success = [RevealSuccessRecord(token_id: "t1", value: "v1")] + + self.revealApiCallback.handleCallbacks(success: success, failure: [], isSuccess: true, errorObject: nil) + wait(for: [self.expectation], timeout: 10.0) + + let records = self.callback.data["records"] as! [[String: Any]] + XCTAssertEqual(records.count, 1) + XCTAssertEqual(records[0]["token"] as? String, "t1") + XCTAssertEqual(records[0]["value"] as? String, "v1") + XCTAssertNil(self.callback.data["errors"]) + } + + func testHandleCallbacksPartialFailure() { + let success = [RevealSuccessRecord(token_id: "t1", value: "v1")] + let failure = [RevealErrorRecord(id: "t2", error: NSError(domain: "", code: 404, userInfo: [NSLocalizedDescriptionKey: "Invalid Token"]))] + + self.revealApiCallback.handleCallbacks(success: success, failure: failure, isSuccess: true, errorObject: nil) + wait(for: [self.expectation], timeout: 10.0) + + let records = self.callback.data["records"] as! [[String: Any]] + let errors = self.callback.data["errors"] as! [[String: Any]] + XCTAssertEqual(records.count, 1) + XCTAssertEqual(errors.count, 1) + XCTAssertEqual(errors[0]["token"] as? String, "t2") + } + + func testHandleCallbacksAllFailure() { + let failure = [RevealErrorRecord(id: "t1", error: NSError(domain: "", code: 404, userInfo: [NSLocalizedDescriptionKey: "Invalid Token"]))] + + self.revealApiCallback.handleCallbacks(success: [], failure: failure, isSuccess: true, errorObject: nil) + wait(for: [self.expectation], timeout: 10.0) + + XCTAssertNil(self.callback.data["records"]) + let errors = self.callback.data["errors"] as! [[String: Any]] + XCTAssertEqual(errors.count, 1) + } + + func testHandleCallbacksTransportFailure() { + // isSuccess = false represents a request-level (network) failure somewhere in the batch, + // as opposed to a per-token error - it should route through callRevealOnFailure instead. + let networkError = NSError(domain: "", code: -1009, userInfo: [NSLocalizedDescriptionKey: "offline"]) + + self.revealApiCallback.handleCallbacks(success: [], failure: [], isSuccess: false, errorObject: networkError) + wait(for: [self.expectation], timeout: 10.0) + + let errors = self.callback.data["errors"] as! [[String: NSError]] + XCTAssertEqual(errors.count, 1) + XCTAssertEqual(errors[0]["error"]?.localizedDescription, "offline") + } + + func testOnSuccessInvalidUrl() { + self.revealApiCallback.connectionUrl = "invalid url" + self.revealApiCallback.onSuccess("token") + wait(for: [self.expectation], timeout: 20.0) + + let errors = self.callback.data["errors"] as! [[String: NSError]] + XCTAssertEqual(errors.count, 1) + XCTAssertEqual(errors[0]["error"]?.localizedDescription, "unsupported URL") + } + + func testOnFailureWrapsSwiftError() { + let error = NSError(domain: "", code: 500, userInfo: [NSLocalizedDescriptionKey: "boom"]) + + self.revealApiCallback.onFailure(error) + wait(for: [self.expectation], timeout: 10.0) + + let errors = self.callback.data["errors"] as! [[String: NSError]] + XCTAssertEqual(errors.count, 1) + XCTAssertEqual(errors[0]["error"]?.localizedDescription, "boom") + } + + func testOnFailurePassesThroughNonErrorValues() { + let dict: [String: Any] = ["errors": [["error": "already a dict"]]] + + self.revealApiCallback.onFailure(dict) + wait(for: [self.expectation], timeout: 10.0) + + XCTAssertNotNil(self.callback.data["errors"]) + } +} diff --git a/Tests/skyflow-iOS-utilTests/skyflow_iOS_revealUtilTests.swift b/Tests/skyflow-iOS-utilTests/skyflow_iOS_revealUtilTests.swift index f09f9108..66730bb7 100644 --- a/Tests/skyflow-iOS-utilTests/skyflow_iOS_revealUtilTests.swift +++ b/Tests/skyflow-iOS-utilTests/skyflow_iOS_revealUtilTests.swift @@ -9,7 +9,7 @@ import XCTest // swiftlint:disable:next type_body_length final class skyflow_iOS_revealUtilTests: XCTestCase { - var revealApiCallback: RevealAPICallback! = nil + var revealApiCallback: FlowVaultRevealAPICallback! = nil var revealValueCallback: RevealValueCallback! = nil var expectation: XCTestExpectation! = nil var callback: DemoAPICallback! = nil @@ -20,7 +20,7 @@ final class skyflow_iOS_revealUtilTests: XCTestCase { override func setUp() { self.expectation = XCTestExpectation() self.callback = DemoAPICallback(expectation: self.expectation) - self.revealApiCallback = RevealAPICallback(callback: self.callback, + self.revealApiCallback = FlowVaultRevealAPICallback(callback: self.callback, apiClient: APIClient(vaultID: "", vaultURL: "", tokenProvider: DemoTokenProvider()), connectionUrl: "", records: [], @@ -40,7 +40,7 @@ final class skyflow_iOS_revealUtilTests: XCTestCase { func testOnSuccessInvalidUrl() { self.revealApiCallback.connectionUrl = "invalid url" - let record = RevealRequestRecord(token: "token", redaction: RedactionType.PLAIN_TEXT.rawValue) + let record = RevealRequestRecord(token: "token") self.revealApiCallback.records = [record] self.revealApiCallback.onSuccess("token") wait(for: [self.expectation], timeout: 10.0) @@ -48,111 +48,178 @@ final class skyflow_iOS_revealUtilTests: XCTestCase { XCTAssertEqual(errors.count, 1) XCTAssertEqual(errors[0]["error"]?.localizedDescription, "unsupported URL") } - + func testGetRequestSession() { - self.revealApiCallback.connectionUrl = "https://www.example.org" - - let (request, session) = self.revealApiCallback.getRequestSession() - XCTAssertEqual(request.url?.absoluteString, "https://www.example.org/detokenize") - XCTAssertEqual(request.allHTTPHeaderFields!["Content-Type"], "application/json; utf-8") - XCTAssertEqual(request.allHTTPHeaderFields!["Accept"], "application/json") - XCTAssertEqual(request.allHTTPHeaderFields!["Authorization"], "Bearer ") - } - - func testRevealRequestBody() { - let record = RevealRequestRecord(token: "token", redaction: RedactionType.PLAIN_TEXT.rawValue) + let url = URL(string: "https://www.example.org")! + do { - let result = try self.revealApiCallback.getRevealRequestBody(record: record) - - if let jsonObject = try? JSONSerialization.jsonObject(with: result, options: []) as? [String: Any] { - let key = jsonObject.keys - let values = jsonObject.values - XCTAssertTrue(key.contains("detokenizationParameters")) - } else { - print("Failed to convert JSON data into a JSON object.") - } - XCTAssertNotNil(result) + let (request, session) = try self.revealApiCallback.getRequestSession(url: url) + XCTAssertEqual(request.url?.absoluteString, "https://www.example.org") + XCTAssertEqual(request.allHTTPHeaderFields!["Content-Type"], "application/json") + XCTAssertEqual(request.allHTTPHeaderFields!["Accept"], "application/json") + XCTAssertEqual(request.allHTTPHeaderFields!["Authorization"], "Bearer ") } catch { XCTFail(error.localizedDescription) } } - + + func testConstructV2DetokenizeRequestBody() { + self.revealApiCallback.records = [RevealRequestRecord(token: "token1"), RevealRequestRecord(token: "token2")] + let result = FlowVaultDetokenizeRequestBody.createRequestBody(vaultID: "vault123", records: self.revealApiCallback.records) + + XCTAssertEqual(result["vaultID"] as! String, "vault123") + XCTAssertEqual(result["tokens"] as! [String], ["token1", "token2"]) + XCTAssertNil(result["tokenGroupRedactions"]) + } + + func testConstructV2DetokenizeRequestBodyWithTokenGroupRedactions() { + let records = [RevealRequestRecord(token: "token1")] + let tokenGroupRedactions = [TokenGroupRedaction(tokenGroupName: "group1", redaction: "MASKED")] + let result = FlowVaultDetokenizeRequestBody.createRequestBody(vaultID: "vault123", records: records, tokenGroupRedactions: tokenGroupRedactions) + + let redactions = result["tokenGroupRedactions"] as! [[String: Any]] + XCTAssertEqual(redactions.count, 1) + XCTAssertEqual(redactions[0]["tokenGroupName"] as? String, "group1") + XCTAssertEqual(redactions[0]["redaction"] as? String, "MASKED") + } + func testProcessResponseError() { let revealedResponse = ["key": "value"] let serverError = NSError(domain: "", code: 500, userInfo: [NSLocalizedDescriptionKey: "Internal Server Error"]) do { let responseData = try JSONSerialization.data(withJSONObject: revealedResponse, options: .fragmentsAllowed) - try self.revealApiCallback.processResponse(record: RevealRequestRecord(token: "token", redaction: RedactionType.PLAIN_TEXT.rawValue), data: responseData, response: nil, error: serverError) + _ = try self.revealApiCallback.processResponse(data: responseData, response: nil, error: serverError) XCTFail("Not throwing on http error") } catch { XCTAssertEqual(error.localizedDescription, serverError.localizedDescription) } } - + func testProcessResponseBadCode() { let revealedResponse = ["error": ["message": "Internal Server Error"]] let httpResponse = HTTPURLResponse(url: URL(string: "https://www.example.org")!, statusCode: 500, httpVersion: "1.1", headerFields: ["x-request-id": "RID"]) do { let responseData = try JSONSerialization.data(withJSONObject: revealedResponse, options: .fragmentsAllowed) - let (success, failure) = try self.revealApiCallback.processResponse(record: RevealRequestRecord(token: "token", redaction: RedactionType.PLAIN_TEXT.rawValue), data: responseData, response: httpResponse, error: nil) - XCTAssertNil(success) - XCTAssertNotNil(failure) - XCTAssertEqual(failure?.error.localizedDescription, "Internal Server Error - request-id: RID") + _ = try self.revealApiCallback.processResponse(data: responseData, response: httpResponse, error: nil) + XCTFail("Not throwing on http error") } catch { - XCTFail(error.localizedDescription) + XCTAssertEqual(error.localizedDescription, "Internal Server Error - request-id: RID") } } - + func testProcessResponseSuccess() { - let revealedResponse = ["records": [["token": "token", "value": "value"]]] + let revealedResponse = ["response": [["token": "token", "value": "value"]]] let httpResponse = HTTPURLResponse(url: URL(string: "https://www.example.org")!, statusCode: 200, httpVersion: "1.1", headerFields: ["x-request-id": "RID"]) do { let responseData = try JSONSerialization.data(withJSONObject: revealedResponse, options: .fragmentsAllowed) - let (success, failure) = try self.revealApiCallback.processResponse(record: RevealRequestRecord(token: "token", redaction: RedactionType.PLAIN_TEXT.rawValue), data: responseData, response: httpResponse, error: nil) - XCTAssertNil(failure) - XCTAssertNotNil(success) - XCTAssertEqual(success?.token_id, "token") - XCTAssertEqual(success?.value, "value") + let response = try self.revealApiCallback.processResponse(data: responseData, response: httpResponse, error: nil) + let records = response["records"] as! [[String: Any]] + XCTAssertEqual(records.count, 1) + XCTAssertEqual(records[0]["token"] as? String, "token") + XCTAssertEqual(records[0]["value"] as? String, "value") + XCTAssertNil(records[0]["error"]) } catch { XCTFail(error.localizedDescription) } } - - func testHandleCallbacksSuccess() { - let errorObject = NSError(domain: "", code: 200, userInfo: nil) - let success = [RevealSuccessRecord(token_id: "token", value: "value")] - - self.revealApiCallback.handleCallbacks(success: success, failure: [], isSuccess: true, errorObject: nil) - - wait(for: [self.expectation], timeout: 20.0) - let records = self.callback.data["records"] as! [[String: String]] - - XCTAssertEqual(records.count, 1) - XCTAssertEqual(records[0]["value"], "value") - XCTAssertEqual(records[0]["token"], "token") + + func testProcessResponseWithMetadataAndHttpCode() { + let revealedResponse: [String: Any] = ["response": [ + ["token": "token", "value": "value", "httpCode": 200, "metadata": ["tableName": "table", "skyflowID": "SID"]] + ]] + let httpResponse = HTTPURLResponse(url: URL(string: "https://www.example.org")!, statusCode: 200, httpVersion: "1.1", headerFields: nil) + do { + let responseData = try JSONSerialization.data(withJSONObject: revealedResponse, options: .fragmentsAllowed) + let response = try self.revealApiCallback.processResponse(data: responseData, response: httpResponse, error: nil) + let records = response["records"] as! [[String: Any]] + + XCTAssertEqual(records.count, 1) + XCTAssertEqual(records[0]["httpCode"] as? Int, 200) + let metadata = records[0]["metadata"] as! [String: Any] + XCTAssertEqual(metadata["tableName"] as? String, "table") + XCTAssertEqual(metadata["skyflowID"] as? String, "SID") + } catch { + XCTFail(error.localizedDescription) + } } - - func testHandleCallbacksFailure() { - let failure = RevealErrorRecord(id: "token", error: NSError(domain: "", code: 500, userInfo: [NSLocalizedDescriptionKey: "Invalid Token"])) - self.revealApiCallback.handleCallbacks(success: [], failure: [failure], isSuccess: true, errorObject: nil) - - wait(for: [self.expectation], timeout: 20.0) - let errors = self.callback.data["errors"] as! [[String: Any]] - XCTAssertEqual(errors.count, 1) - XCTAssertEqual(errors[0]["error"] as! NSError, failure.error) - XCTAssertEqual(errors[0]["token"] as! String, "token") + + func testProcessResponsePartialFailure() { + let revealedResponse: [String: Any] = ["response": [ + ["token": "token1", "value": "value1"], + ["token": "token2", "error": "Invalid Token", "httpCode": 400] + ]] + let httpResponse = HTTPURLResponse(url: URL(string: "https://www.example.org")!, statusCode: 200, httpVersion: "1.1", headerFields: nil) + do { + let responseData = try JSONSerialization.data(withJSONObject: revealedResponse, options: .fragmentsAllowed) + let response = try self.revealApiCallback.processResponse(data: responseData, response: httpResponse, error: nil) + let records = response["records"] as! [[String: Any]] + XCTAssertEqual(records.count, 2) + XCTAssertEqual(records[0]["token"] as? String, "token1") + XCTAssertNil(records[0]["error"]) + XCTAssertEqual(records[1]["token"] as? String, "token2") + XCTAssertEqual(records[1]["error"] as? String, "Invalid Token") + } catch { + XCTFail(error.localizedDescription) + } } - - func testHandleCallbacksError() { - let error = NSError(domain: "", code: 500, userInfo: [NSLocalizedDescriptionKey: "Invalid Token"]) - self.revealApiCallback.handleCallbacks(success: [], failure: [], isSuccess: false, errorObject: error) - - wait(for: [self.expectation], timeout: 20.0) - let errors = self.callback.data["errors"] as! [[String: Any]] - XCTAssertEqual(errors.count, 1) - XCTAssertEqual(errors[0]["error"] as! NSError, error) + + func testProcessResponseFullFailureWithNon2xxOuterStatus() { + let revealedResponse: [String: Any] = ["response": [ + ["token": "dedwimm", "value": NSNull(), "tokenGroupName": NSNull(), + "error": "Detokenize failed. Token dedwimm is invalid. Specify a valid token.", + "httpCode": 404, "metadata": NSNull()], + ["token": "femwdmm", "value": NSNull(), "tokenGroupName": NSNull(), + "error": "Detokenize failed. Token femwdmm is invalid. Specify a valid token.", + "httpCode": 404, "metadata": NSNull()] + ]] + let httpResponse = HTTPURLResponse(url: URL(string: "https://www.example.org")!, statusCode: 404, httpVersion: "1.1", headerFields: nil) + do { + let responseData = try JSONSerialization.data(withJSONObject: revealedResponse, options: .fragmentsAllowed) + let response = try self.revealApiCallback.processResponse(data: responseData, response: httpResponse, error: nil) + let records = response["records"] as! [[String: Any]] + + XCTAssertEqual(records.count, 2) + XCTAssertEqual(records[0]["error"] as? String, "Detokenize failed. Token dedwimm is invalid. Specify a valid token.") + XCTAssertEqual(records[0]["httpCode"] as? Int, 404) + } catch { + XCTFail("Full failure with 404 outer status should not throw: \(error)") + } } - + + func testProcessResponseInvalidTokenGroupError() { + let revealedResponse: [String: Any] = ["error": [ + "grpc_code": 3, + "http_code": 400, + "message": "Detokenize failed. Token group no is invalid. Specify a valid token group.", + "http_status": "Bad Request", + "details": [] + ]] + let httpResponse = HTTPURLResponse(url: URL(string: "https://www.example.org")!, statusCode: 400, httpVersion: "1.1", headerFields: nil) + do { + let responseData = try JSONSerialization.data(withJSONObject: revealedResponse, options: .fragmentsAllowed) + _ = try self.revealApiCallback.processResponse(data: responseData, response: httpResponse, error: nil) + XCTFail("Should throw on genuine top-level error (invalid token group)") + } catch { + XCTAssertEqual(error.localizedDescription, "Detokenize failed. Token group no is invalid. Specify a valid token group.") + } + } + + func testConstructV2DetokenizeRequestBodyPassesThroughDuplicateTokenGroupRedactions() { + let records = [RevealRequestRecord(token: "token1")] + let tokenGroupRedactions = [ + TokenGroupRedaction(tokenGroupName: "group1", redaction: "MASKED"), + TokenGroupRedaction(tokenGroupName: "group1", redaction: "PLAIN_TEXT") + ] + let result = FlowVaultDetokenizeRequestBody.createRequestBody(vaultID: "vault123", records: records, tokenGroupRedactions: tokenGroupRedactions) + + let redactions = result["tokenGroupRedactions"] as! [[String: Any]] + XCTAssertEqual(redactions.count, 2) + XCTAssertEqual(redactions[0]["tokenGroupName"] as? String, "group1") + XCTAssertEqual(redactions[0]["redaction"] as? String, "MASKED") + XCTAssertEqual(redactions[1]["tokenGroupName"] as? String, "group1") + XCTAssertEqual(redactions[1]["redaction"] as? String, "PLAIN_TEXT") + } + func testGetTokensToErrors() { let errors = [["token": "1234"], ["token": "4321"]] @@ -162,6 +229,148 @@ final class skyflow_iOS_revealUtilTests: XCTestCase { XCTAssertEqual(result["4321"], "Invalid Token") } + func testRevealValueOnSuccessPureSuccess() { + // Every token revealed, zero errors - a genuine full-success scenario + // (as opposed to the mixed success+error fixtures used elsewhere in this file). + let token1 = "123" + let token2 = "456" + let response: [String: Any] = [ + "records": [ + ["token": token1, "value": "John"], + ["token": token2, "value": "Doe"] + ], + "errors": [] + ] + + let element1 = self.container.create(input: RevealElementInput(token: token1, label: "first"), options: RevealElementOptions()) + let element2 = self.container.create(input: RevealElementInput(token: token2, label: "second"), options: RevealElementOptions()) + + self.revealValueCallback.revealElements = [element1, element2] + self.revealValueCallback.onSuccess(response) + wait(for: [self.expectation], timeout: 20.0) + waitForUIUpdates() + + let records = self.callback.data["records"] as! [[String: Any]] + XCTAssertEqual(records.count, 2) + XCTAssertTrue(records.allSatisfy { $0["error"] == nil }) + XCTAssertEqual(element1.actualValue, "John") + XCTAssertEqual(element2.actualValue, "Doe") + XCTAssertEqual(element1.errorMessage.text, nil) + XCTAssertEqual(element2.errorMessage.text, nil) + } + + func testRevealValueOnFailurePureFailure() { + // Every token invalid, zero successes - a genuine full-failure scenario. + let token1 = "123" + let token2 = "456" + let response: [String: Any] = [ + "records": [], + "errors": [ + ["token": token1, "error": "Invalid Token"], + ["token": token2, "error": "Invalid Token"] + ] + ] + + let element1 = self.container.create(input: RevealElementInput(token: token1, label: "first")) + let element2 = self.container.create(input: RevealElementInput(token: token2, label: "second")) + + self.revealValueCallback.revealElements = [element1, element2] + self.revealValueCallback.onFailure(response) + wait(for: [self.expectation], timeout: 20.0) + waitForUIUpdates() + + let errors = self.callback.data["records"] as! [[String: Any]] + XCTAssertEqual(errors.count, 2) + XCTAssertTrue(errors.allSatisfy { $0["error"] != nil }) + XCTAssertEqual(element1.actualValue, nil) + XCTAssertEqual(element2.actualValue, nil) + XCTAssertEqual(element1.errorMessage.text, "Invalid Token") + XCTAssertEqual(element2.errorMessage.text, "Invalid Token") + } + + func testRevealValueOnFailureNetworkErrorHasNoPerTokenErrors() { + // Simulates what FlowVaultRevealAPICallback.callRevealOnFailure produces for a genuine + // network/connection-level failure: no "records", and the error entry has no "token" + // (since the failure isn't scoped to any specific token). Elements should not crash and + // should not show a per-element error message (there's no token to match against), but + // the client callback should still receive the error. + let token1 = "123" + let networkError = NSError(domain: "NSURLErrorDomain", code: -1009, userInfo: [NSLocalizedDescriptionKey: "The Internet connection appears to be offline."]) + let response: [String: Any] = [ + "errors": [["error": networkError]] + ] + + let element1 = self.container.create(input: RevealElementInput(token: token1, label: "first")) + self.revealValueCallback.revealElements = [element1] + + self.revealValueCallback.onFailure(response) + wait(for: [self.expectation], timeout: 20.0) + waitForUIUpdates() + + // Not scoped to any specific token, so it's delivered via onFailure (not folded into the + // records array) rather than a per-record error. + XCTAssertNil(self.callback.data["records"]) + XCTAssertEqual(self.callback.receivedResponse, "The Internet connection appears to be offline.") + // No token to match this error against, so the element shows no inline error. + XCTAssertEqual(element1.errorMessage.text, nil) + XCTAssertEqual(element1.actualValue, nil) + } + + func testRevealValueOnFailureWithNonDictionaryError() { + // onFailure(_ error: Any) can in principle be called with something that isn't a + // dictionary at all (the Callback protocol accepts Any). Must not crash, and should + // report an empty response rather than propagating garbage. + let element1 = self.container.create(input: RevealElementInput(token: "123", label: "first")) + self.revealValueCallback.revealElements = [element1] + + self.revealValueCallback.onFailure("not a dictionary") + wait(for: [self.expectation], timeout: 20.0) + + XCTAssertNil(self.callback.data["success"]) + XCTAssertNil(self.callback.data["errors"]) + } + + func testRevealValueOnSuccessWithNonDictionaryResponseBody() { + // Same defensive case for onSuccess: a non-dictionary responseBody must not crash. + let element1 = self.container.create(input: RevealElementInput(token: "123", label: "first")) + self.revealValueCallback.revealElements = [element1] + + self.revealValueCallback.onSuccess(42) + wait(for: [self.expectation], timeout: 20.0) + waitForUIUpdates() + + XCTAssertNil(self.callback.data["success"]) + XCTAssertNil(self.callback.data["errors"]) + XCTAssertEqual(element1.actualValue, nil) + } + + func testRevealValueOnSuccessSkipsMalformedRecords() { + // A record missing "token" (or not a dictionary at all) must be skipped, not crash + // the whole reveal for every other token in the same batch. + let goodToken = "123" + let response: [String: Any] = [ + "records": [ + ["token": goodToken, "value": "John"], + ["value": "no token here"], + "not even a dictionary", + ["token": 42, "value": "token is not a String"] + ], + "errors": [] + ] + + let goodElement = self.container.create(input: RevealElementInput(token: goodToken, label: "good"), options: RevealElementOptions()) + self.revealValueCallback.revealElements = [goodElement] + + self.revealValueCallback.onSuccess(response) + wait(for: [self.expectation], timeout: 20.0) + waitForUIUpdates() + + let records = self.callback.data["records"] as! [[String: Any]] + XCTAssertEqual(records.count, 1) + XCTAssertEqual(records[0]["token"] as? String, goodToken) + XCTAssertEqual(goodElement.actualValue, "John") + } + func testRevealValueOnFailure() { let successToken = "123" let failureToken = "1234" @@ -179,8 +388,9 @@ final class skyflow_iOS_revealUtilTests: XCTestCase { wait(for: [self.expectation], timeout: 20.0) waitForUIUpdates() - let errors = self.callback.data["errors"] as! [[String: String]] - let records = self.callback.data["success"] as! [[String: String]] + let allRecords = self.callback.data["records"] as! [[String: String]] + let errors = allRecords.filter { $0["error"] != nil } + let records = allRecords.filter { $0["error"] == nil } XCTAssertEqual(errors.count, 1) XCTAssertEqual(errors[0]["token"], failureToken) @@ -198,8 +408,7 @@ final class skyflow_iOS_revealUtilTests: XCTestCase { let successToken = "123" let failureToken = "1234" let response = [ - "records": [["token": successToken, "value": "John"]], - "errors": [["token": failureToken, "error": "Invalid Token"]] + "records": [["token": successToken, "value": "John"], ["token": failureToken, "error": "Invalid Token"]] ] let successElement = self.container.create(input: RevealElementInput(token: successToken, label: "name"), options: RevealElementOptions()) @@ -211,8 +420,9 @@ final class skyflow_iOS_revealUtilTests: XCTestCase { wait(for: [self.expectation], timeout: 20.0) waitForUIUpdates() - let errors = self.callback.data["errors"] as! [[String: String]] - let records = self.callback.data["success"] as! [[String: String]] + let allRecords = self.callback.data["records"] as! [[String: String]] + let errors = allRecords.filter { $0["error"] != nil } + let records = allRecords.filter { $0["error"] == nil } XCTAssertEqual(errors.count, 1) XCTAssertEqual(errors[0]["token"], failureToken) @@ -229,8 +439,7 @@ final class skyflow_iOS_revealUtilTests: XCTestCase { let successToken = "123" let failureToken = "1234" let response = [ - "records": [["token": successToken, "value": "4567890"]], - "errors": [["token": failureToken, "error": "Invalid Token"]] + "records": [["token": successToken, "value": "4567890"], ["token": failureToken, "error": "Invalid Token"]] ] let successElement = self.container.create(input: RevealElementInput(token: successToken, label: "name"), options: RevealElementOptions(format: "XXX-XXX-X", translation: ["X": "[0-9]"])) @@ -242,8 +451,9 @@ final class skyflow_iOS_revealUtilTests: XCTestCase { wait(for: [self.expectation], timeout: 20.0) waitForUIUpdates() - let errors = self.callback.data["errors"] as! [[String: String]] - let records = self.callback.data["success"] as! [[String: String]] + let allRecords = self.callback.data["records"] as! [[String: String]] + let errors = allRecords.filter { $0["error"] != nil } + let records = allRecords.filter { $0["error"] == nil } XCTAssertEqual(errors.count, 1) XCTAssertEqual(errors[0]["token"], failureToken) @@ -262,8 +472,7 @@ final class skyflow_iOS_revealUtilTests: XCTestCase { let successToken = "123" let failureToken = "1234" let response = [ - "records": [["token": successToken, "value": "12345678"]], - "errors": [["token": failureToken, "error": "Invalid Token"]] + "records": [["token": successToken, "value": "12345678"], ["token": failureToken, "error": "Invalid Token"]] ] let successElement = self.container.create(input: RevealElementInput(token: successToken, label: "name"), options: RevealElementOptions(format: "XXX-XXX-XXX", translation: ["X": "[0-9]"])) @@ -275,8 +484,9 @@ final class skyflow_iOS_revealUtilTests: XCTestCase { wait(for: [self.expectation], timeout: 20.0) waitForUIUpdates() - let errors = self.callback.data["errors"] as! [[String: String]] - let records = self.callback.data["success"] as! [[String: String]] + let allRecords = self.callback.data["records"] as! [[String: String]] + let errors = allRecords.filter { $0["error"] != nil } + let records = allRecords.filter { $0["error"] == nil } XCTAssertEqual(errors.count, 1) XCTAssertEqual(errors[0]["token"], failureToken) @@ -295,8 +505,7 @@ final class skyflow_iOS_revealUtilTests: XCTestCase { let successToken = "123" let failureToken = "1234" let response = [ - "records": [["token": successToken, "value": "12345678"]], - "errors": [["token": failureToken, "error": "Invalid Token"]] + "records": [["token": successToken, "value": "12345678"], ["token": failureToken, "error": "Invalid Token"]] ] let successElement = self.container.create(input: RevealElementInput(token: successToken, label: "name"), options: RevealElementOptions(format: "XXX-XXX", translation: ["X": "[0-9]"])) @@ -308,8 +517,9 @@ final class skyflow_iOS_revealUtilTests: XCTestCase { wait(for: [self.expectation], timeout: 20.0) waitForUIUpdates() - let errors = self.callback.data["errors"] as! [[String: String]] - let records = self.callback.data["success"] as! [[String: String]] + let allRecords = self.callback.data["records"] as! [[String: String]] + let errors = allRecords.filter { $0["error"] != nil } + let records = allRecords.filter { $0["error"] == nil } XCTAssertEqual(errors.count, 1) XCTAssertEqual(errors[0]["token"], failureToken) @@ -329,8 +539,7 @@ final class skyflow_iOS_revealUtilTests: XCTestCase { let successToken = "123" let failureToken = "1234" let response = [ - "records": [["token": successToken, "value": "12345678"]], - "errors": [["token": failureToken, "error": "Invalid Token"]] + "records": [["token": successToken, "value": "12345678"], ["token": failureToken, "error": "Invalid Token"]] ] let successElement = self.container.create(input: RevealElementInput(token: successToken, label: "name"), options: RevealElementOptions(format: "+91 XXX-XXX", translation: ["X": "[0-9]"])) @@ -342,8 +551,9 @@ final class skyflow_iOS_revealUtilTests: XCTestCase { wait(for: [self.expectation], timeout: 20.0) waitForUIUpdates() - let errors = self.callback.data["errors"] as! [[String: String]] - let records = self.callback.data["success"] as! [[String: String]] + let allRecords = self.callback.data["records"] as! [[String: String]] + let errors = allRecords.filter { $0["error"] != nil } + let records = allRecords.filter { $0["error"] == nil } XCTAssertEqual(errors.count, 1) XCTAssertEqual(errors[0]["token"], failureToken) @@ -362,8 +572,7 @@ final class skyflow_iOS_revealUtilTests: XCTestCase { let successToken = "123" let failureToken = "1234" let response = [ - "records": [["token": successToken, "value": "name"]], - "errors": [["token": failureToken, "error": "Invalid Token"]] + "records": [["token": successToken, "value": "name"], ["token": failureToken, "error": "Invalid Token"]] ] let successElement = self.container.create(input: RevealElementInput(token: successToken, label: "name"), options: RevealElementOptions(format: "+91 XXX-XXX", translation: ["X": "[0-9]"])) @@ -375,8 +584,9 @@ final class skyflow_iOS_revealUtilTests: XCTestCase { wait(for: [self.expectation], timeout: 20.0) waitForUIUpdates() - let errors = self.callback.data["errors"] as! [[String: String]] - let records = self.callback.data["success"] as! [[String: String]] + let allRecords = self.callback.data["records"] as! [[String: String]] + let errors = allRecords.filter { $0["error"] != nil } + let records = allRecords.filter { $0["error"] == nil } XCTAssertEqual(errors.count, 1) XCTAssertEqual(errors[0]["token"], failureToken) @@ -395,8 +605,7 @@ final class skyflow_iOS_revealUtilTests: XCTestCase { let successToken = "123" let failureToken = "1234" let response = [ - "records": [["token": successToken, "value": "name"]], - "errors": [["token": failureToken, "error": "Invalid Token"]] + "records": [["token": successToken, "value": "name"], ["token": failureToken, "error": "Invalid Token"]] ] let successElement = self.container.create(input: RevealElementInput(token: successToken, label: "name"), options: RevealElementOptions(format: "+91 XXX-XXX", translation: ["Y": "[0-9]"])) @@ -408,8 +617,9 @@ final class skyflow_iOS_revealUtilTests: XCTestCase { wait(for: [self.expectation], timeout: 20.0) waitForUIUpdates() - let errors = self.callback.data["errors"] as! [[String: String]] - let records = self.callback.data["success"] as! [[String: String]] + let allRecords = self.callback.data["records"] as! [[String: String]] + let errors = allRecords.filter { $0["error"] != nil } + let records = allRecords.filter { $0["error"] == nil } XCTAssertEqual(errors.count, 1) XCTAssertEqual(errors[0]["token"], failureToken) @@ -436,12 +646,85 @@ final class skyflow_iOS_revealUtilTests: XCTestCase { UIWindow().addSubview(element!) - container?.reveal(callback: callback) + container?.reveal(callback: callback.asRevealCallback) wait(for: [expectation], timeout: 20.0) - print(callback.data) - let errors = callback.data["errors"] as! [[String: NSError]] - XCTAssertTrue(errors[0]["error"]!.localizedDescription.contains("Token generated from 'getBearerToken' callback function is invalid")) + // Invalid bearer token isn't scoped to any specific token, so RevealValueCallback routes it + // through onFailure as a Skyflow.SkyflowError rather than folding it into the records array. + XCTAssertTrue(callback.receivedResponse.contains("Token generated from 'getBearerToken' callback function is invalid")) } - + + func testRevealValueOnFailureMixedTokenScopedAndUnscopedErrorsRoutesToOnFailure() { + // Edge case: if a genuine network/API-level error (no "token") arrives in the same batch + // as per-token errors, the whole thing is treated as a whole-request failure - the + // per-token entries are discarded rather than partially surfaced via onSuccess. This + // documents that behavior explicitly, since it's not obvious from reading either branch + // in isolation. + let scopedToken = "123" + let networkError = NSError(domain: "NSURLErrorDomain", code: -1009, userInfo: [NSLocalizedDescriptionKey: "network down"]) + let response: [String: Any] = [ + "errors": [ + ["token": scopedToken, "error": "Invalid Token"], + ["error": networkError] + ] + ] + + let element1 = self.container.create(input: RevealElementInput(token: scopedToken, label: "first")) + self.revealValueCallback.revealElements = [element1] + + self.revealValueCallback.onFailure(response) + wait(for: [self.expectation], timeout: 20.0) + waitForUIUpdates() + + XCTAssertNil(self.callback.data["records"]) + XCTAssertEqual(self.callback.receivedResponse, "network down") + XCTAssertEqual(element1.errorMessage.text, nil) + } + + func testRevealRecordHttpCodeDefaultsToZeroWhenMissing() { + let record = RevealRecord(["token": "abc"]) + XCTAssertEqual(record.httpCode, 0) + XCTAssertNil(record.error) + } + + func testRevealRecordErrorDiscriminatesSuccessFromFailure() { + let success = RevealRecord(["token": "abc", "value": "1234", "httpCode": 200]) + let failure = RevealRecord(["token": "xyz", "error": "Tokens not found", "httpCode": 404]) + + XCTAssertNil(success.error) + XCTAssertEqual(failure.error, "Tokens not found") + XCTAssertEqual(failure.httpCode, 404) + } + + func testRevealResponseInitReturnsNilForMalformedBody() { + XCTAssertNil(RevealResponse("not a dictionary")) + XCTAssertNil(RevealResponse(["typo": []])) + XCTAssertNil(RevealResponse(["records": "not an array"])) + } + + // Verifies the real end-to-end wiring for a whole-request failure through + // FlowVaultRevealAPICallback -> callRevealOnFailure -> LogCallback -> RevealCallback -> + // SkyflowError.wrap, using a token provider that fails immediately (no network mocking + // needed - TokenAPICallback's failure reaches the exact same callRevealOnFailure wrapping + // as a real dispatch failure would). This is the plumbing that had a real bug (SkyflowError.wrap + // losing the message entirely) until it was found and fixed earlier via a unit test on + // SkyflowError.wrap directly - this test instead confirms the full real call chain, not just + // the isolated function. + func testDetokenizeTokenProviderFailureSurfacesAsSkyflowErrorThroughRealCallChain() { + class FailingTokenProvider: TokenProvider { + func getBearerToken(_ apiCallback: Callback) { + apiCallback.onFailure(NSError(domain: "", code: 500, userInfo: [NSLocalizedDescriptionKey: "TokenProvider error"])) + } + } + let client = Client(Configuration(vaultID: "vault", vaultURL: "https://example.org/", tokenProvider: FailingTokenProvider())) + let expectation = XCTestExpectation(description: "TokenProvider failure surfaces as SkyflowError") + let callback = DemoAPICallback(expectation: expectation) + + client.detokenize(records: ["records": [["token": "tok1"]]], callback: callback.asRevealCallback) + + wait(for: [expectation], timeout: 10.0) + + XCTAssertEqual(callback.receivedResponse, "TokenProvider error") + } + } diff --git a/Tests/skyflow-iOS-utilTests/skyflow_iOS_utilTests.swift b/Tests/skyflow-iOS-utilTests/skyflow_iOS_utilTests.swift index 6e872245..28acb539 100644 --- a/Tests/skyflow-iOS-utilTests/skyflow_iOS_utilTests.swift +++ b/Tests/skyflow-iOS-utilTests/skyflow_iOS_utilTests.swift @@ -61,30 +61,171 @@ final class skyflow_iOS_utilTests: XCTestCase { textField.addAndFormatText(str) XCTAssertEqual(textField.secureText, "4111 1111 1111 1111") } - func testConstructRequestWithTokens() { - let apiClient = APIClient(vaultID: "", vaultURL: "", tokenProvider: DemoTokenProvider()) - let result = apiClient.constructBatchRequestBody(records: ["records": [["table": "table", "fields": ["field1": "value1"]]]], options: ICOptions(tokens: true)) - let record = result["records"] as! [[String: Any]] - let id = record[1]["ID"] as! String - let tokenization = record[1]["tokenization"] as! Bool - XCTAssertEqual(id, "$responses.0.records.0.skyflow_id") - XCTAssertTrue(tokenization) + func testConstructV2RequestBody() { + let result = FlowVaultInsertRequestBody.createRequestBody(vaultID: "vault123", records: ["records": [["table": "table", "fields": ["field1": "value1"]]]], options: FlowVaultICOptions()) + XCTAssertEqual(result["vaultID"] as! String, "vault123") + let records = result["records"] as! [[String: Any]] + XCTAssertEqual(records[0]["tableName"] as! String, "table") + XCTAssertEqual(records[0]["data"] as! [String: String], ["field1": "value1"]) } - - func testConstructUpdateRequestBody() { - let apiClient = APIClient(vaultID: "vault123", vaultURL: "https://example.com", tokenProvider: DemoTokenProvider()) - - let updateRecords: [String: Any] = [ - "update": [ - ["table": "users", "fields": ["name": "John Doe", "email": "john.doe@example.com"], "skyflowID": "id123"], - ["table": "users", "fields": ["name": "Jane Doe", "email": "jane.doe@example.com"], "skyflowID": "id124"] - ] + + func testConstructV2RequestBodyWithUpsert() { + let upsert = [UpsertOption(table: "table", uniqueColumns: ["field1"], updateType: .REPLACE)] + let result = FlowVaultInsertRequestBody.createRequestBody(vaultID: "vault123", records: ["records": [["table": "table", "fields": ["field1": "value1"]]]], options: FlowVaultICOptions(upsert: upsert)) + let records = result["records"] as! [[String: Any]] + let upsertPayload = records[0]["upsert"] as! [String: Any] + XCTAssertEqual(upsertPayload["uniqueColumns"] as! [String], ["field1"]) + XCTAssertEqual(upsertPayload["updateType"] as! String, "REPLACE") + } + + // Not exercised by any current call site (UpsertOption always supplies a concrete + // updateType today), but the nil default is part of the public API - confirms + // "updateType" is omitted from the wire payload rather than serialized as null. + func testConstructV2RequestBodyWithUpsertAndNilUpdateType() { + let upsert = [UpsertOption(table: "table", uniqueColumns: ["field1"])] + let result = FlowVaultInsertRequestBody.createRequestBody(vaultID: "vault123", records: ["records": [["table": "table", "fields": ["field1": "value1"]]]], options: FlowVaultICOptions(upsert: upsert)) + let records = result["records"] as! [[String: Any]] + let upsertPayload = records[0]["upsert"] as! [String: Any] + XCTAssertEqual(upsertPayload["uniqueColumns"] as! [String], ["field1"]) + XCTAssertNil(upsertPayload["updateType"]) + } + + // Dormant v1 PDB helpers on APIClient (untyped upsert), kept for potential future PDB reuse. + + func testGetUniqueColumn() { + let apiClient = APIClient(vaultID: "vault", vaultURL: "https://example.org/", tokenProvider: DemoTokenProvider()) + let upsert: [[String: Any]] = [ + ["table": "table1", "column": "email"], + ["table": "table2", "column": "ssn"] ] - let singleUpdateRecord = ["table": "users", "fields": ["name": "John Doe", "email": "john.doe@example.com"], "skyflowID": "id123"] as [String : Any] - let result = apiClient.constructUpdateRequestBody(records: singleUpdateRecord, options: ICOptions(tokens: false)) - let records = result["record"] as! [String: Any] - let tokenization = result["tokenization"] as! Bool - XCTAssertEqual(records as NSDictionary, ["fields": ["name": "John Doe", "email": "john.doe@example.com"]]) - XCTAssertFalse(tokenization) + + XCTAssertEqual(apiClient.getUniqueColumn(tableName: "table1", upsert: upsert), "email") + XCTAssertEqual(apiClient.getUniqueColumn(tableName: "table2", upsert: upsert), "ssn") + XCTAssertEqual(apiClient.getUniqueColumn(tableName: "table3", upsert: upsert), "") + } + + func testConstructBatchRequestBodyWithUpsert() { + let apiClient = APIClient(vaultID: "vault", vaultURL: "https://example.org/", tokenProvider: DemoTokenProvider()) + let records: [String: Any] = ["records": [["table": "table1", "fields": ["email": "a@b.com"]]]] + let upsert: [[String: Any]] = [["table": "table1", "column": "email"]] + let options = ICOptions(tokens: false, upsert: upsert) + + let result = apiClient.constructBatchRequestBody(records: records, options: options) + let postPayload = result["records"] as! [[String: Any]] + + XCTAssertEqual(postPayload.count, 1) + XCTAssertEqual(postPayload[0]["upsert"] as? String, "email") + XCTAssertEqual(postPayload[0]["tableName"] as? String, "table1") + XCTAssertEqual(postPayload[0]["method"] as? String, "POST") + XCTAssertEqual(postPayload[0]["quorum"] as? Bool, true) + } + + func testConstructBatchRequestBodyWithTokensAddsGetStep() { + let apiClient = APIClient(vaultID: "vault", vaultURL: "https://example.org/", tokenProvider: DemoTokenProvider()) + let records: [String: Any] = ["records": [["table": "table1", "fields": ["email": "a@b.com"]]]] + let options = ICOptions(tokens: true) + + let result = apiClient.constructBatchRequestBody(records: records, options: options) + let postPayload = result["records"] as! [[String: Any]] + + // One POST insert entry, plus one GET tokenization entry appended after it. + XCTAssertEqual(postPayload.count, 2) + XCTAssertEqual(postPayload[0]["method"] as? String, "POST") + XCTAssertEqual(postPayload[1]["method"] as? String, "GET") + XCTAssertEqual(postPayload[1]["tokenization"] as? Bool, true) + XCTAssertEqual(postPayload[1]["ID"] as? String, "$responses.0.records.0.skyflow_id") + } + + func testConstructBatchRequestBodyNoUpsertNoTokens() { + let apiClient = APIClient(vaultID: "vault", vaultURL: "https://example.org/", tokenProvider: DemoTokenProvider()) + let records: [String: Any] = ["records": [["table": "table1", "fields": ["email": "a@b.com"]]]] + let options = ICOptions(tokens: false) + + let result = apiClient.constructBatchRequestBody(records: records, options: options) + let postPayload = result["records"] as! [[String: Any]] + + XCTAssertEqual(postPayload.count, 1) + XCTAssertNil(postPayload[0]["upsert"]) + } + + // Dormant v1 ICOptions.validateUpsert() (untyped upsert dicts), kept for potential future + // PDB reuse. Distinct from FlowVaultICOptions.validateUpsert(), which covers the live path. + + func testICOptionsValidateUpsertValid() { + let callback = DemoAPICallback(expectation: XCTestExpectation()) + let upsert: [[String: Any]] = [["table": "table1", "column": "email"]] + let options = ICOptions(tokens: false, upsert: upsert, callback: callback, contextOptions: ContextOptions()) + + XCTAssertFalse(options.validateUpsert()) + } + + func testICOptionsValidateUpsertEmptyArray() { + let expectation = XCTestExpectation() + let callback = DemoAPICallback(expectation: expectation) + let options = ICOptions(tokens: false, upsert: [], callback: callback, contextOptions: ContextOptions()) + + XCTAssertTrue(options.validateUpsert()) + wait(for: [expectation], timeout: 10.0) + XCTAssertEqual(callback.receivedResponse, ErrorCodes.UPSERT_OPTION_CANNOT_BE_EMPTY().getErrorObject(contextOptions: ContextOptions()).localizedDescription) + } + + func testICOptionsValidateUpsertMissingTableKey() { + let expectation = XCTestExpectation() + let callback = DemoAPICallback(expectation: expectation) + let upsert: [[String: Any]] = [["column": "email"]] + let options = ICOptions(tokens: false, upsert: upsert, callback: callback, contextOptions: ContextOptions()) + + XCTAssertTrue(options.validateUpsert()) + wait(for: [expectation], timeout: 10.0) + XCTAssertEqual(callback.receivedResponse, ErrorCodes.MISSING_TABLE_NAME_IN_USERT_OPTION(value: "0").getErrorObject(contextOptions: ContextOptions()).localizedDescription) + } + + func testICOptionsValidateUpsertMissingColumnKey() { + let expectation = XCTestExpectation() + let callback = DemoAPICallback(expectation: expectation) + let upsert: [[String: Any]] = [["table": "table1"]] + let options = ICOptions(tokens: false, upsert: upsert, callback: callback, contextOptions: ContextOptions()) + + XCTAssertTrue(options.validateUpsert()) + wait(for: [expectation], timeout: 10.0) + XCTAssertEqual(callback.receivedResponse, ErrorCodes.MISSING_COLUMN_NAME_IN_USERT_OPTION(value: "0").getErrorObject(contextOptions: ContextOptions()).localizedDescription) + } + + func testICOptionsValidateUpsertEmptyTableValue() { + let expectation = XCTestExpectation() + let callback = DemoAPICallback(expectation: expectation) + let upsert: [[String: Any]] = [["table": "", "column": "email"]] + let options = ICOptions(tokens: false, upsert: upsert, callback: callback, contextOptions: ContextOptions()) + + XCTAssertTrue(options.validateUpsert()) + wait(for: [expectation], timeout: 10.0) + XCTAssertEqual(callback.receivedResponse, ErrorCodes.TABLE_NAME_IS_EMPTY_FOR_ATLEAST_ONE_UPSERT_OPTION(value: "0").getErrorObject(contextOptions: ContextOptions()).localizedDescription) + } + + func testICOptionsValidateUpsertEmptyColumnValue() { + let expectation = XCTestExpectation() + let callback = DemoAPICallback(expectation: expectation) + let upsert: [[String: Any]] = [["table": "table1", "column": ""]] + let options = ICOptions(tokens: false, upsert: upsert, callback: callback, contextOptions: ContextOptions()) + + XCTAssertTrue(options.validateUpsert()) + wait(for: [expectation], timeout: 10.0) + XCTAssertEqual(callback.receivedResponse, ErrorCodes.COLUMN_NAME_IS_EMPTY_FOR_ATLEAST_ONE_UPSERT_OPTION(value: "0").getErrorObject(contextOptions: ContextOptions()).localizedDescription) + } + + func testFlowVaultUpdateRequestBody() { + let records: [[String: Any]] = [ + ["skyflowID": "id1", "tableName": "table1", "data": ["email": "a@b.com"]], + ["skyflowID": "id2", "tableName": "table2", "data": ["name": "chiku"]] + ] + + let result = FlowVaultUpdateRequestBody.createRequestBody(vaultID: "vault123", records: records) + + XCTAssertEqual(result["vaultID"] as? String, "vault123") + let resultRecords = result["records"] as! [[String: Any]] + XCTAssertEqual(resultRecords.count, 2) + XCTAssertEqual(resultRecords[0]["skyflowID"] as? String, "id1") + XCTAssertEqual(resultRecords[0]["tableName"] as? String, "table1") + XCTAssertEqual(resultRecords[1]["tableName"] as? String, "table2") } }