Repository: skyflowapi/skyflow-python (language: python)

Issue:
Add detokenize method



Discussion / hints:

Fix patch:
diff --git a/.gitignore b/.gitignore
index b6e4761..a1ac457 100644
--- a/.gitignore
+++ b/.gitignore
@@ -127,3 +127,7 @@ dmypy.json
 
 # Pyre type checker
 .pyre/
+
+# Misc
+test.py
+credentials.json
\ No newline at end of file
diff --git a/skyflow/Errors/_skyflowErrors.py b/skyflow/Errors/_skyflowErrors.py
index 55315b9..626b19d 100644
--- a/skyflow/Errors/_skyflowErrors.py
+++ b/skyflow/Errors/_skyflowErrors.py
@@ -3,6 +3,7 @@
 class SkyflowErrorCodes(Enum):
     INVALID_INPUT = 400
     SERVER_ERROR = 500
+    PARTIAL_SUCCESS = 200
 
 class SkyflowErrorMessages(Enum):
     FILE_NOT_FOUND = "File at %s not found"
@@ -18,9 +19,29 @@ class SkyflowErrorMessages(Enum):
     MISSING_ACCESS_TOKEN = "accessToken not present in response"
     MISSING_TOKEN_TYPE = "tokenType not present in response"
 
+    # vault
+    RECORDS_KEY_ERROR = "Records key is missing from payload"
+    FIELDS_KEY_ERROR = "Fields key is missing from payload"
+    TABLE_KEY_ERROR = "Table key is missing from payload"
+    TOKEN_KEY_ERROR = "Token key is missing from payload"
+    INVALID_JSON = "Given %s is invalid JSON"
+    INVALID_RECORDS_TYPE = "Records key has value of type %s, expected list"
+    INVALID_FIELDS_TYPE = "Fields key has value of type %s, expected string"
+    INVALID_TABLE_TYPE = "Table key has value of type %s, expected string"
+
+    INVALID_REQUEST_BODY = "Given request body is not valid"
+    INVALID_HEADERS = "Given Request Headers is not valid"
+    INVALID_PATH_PARAMS = "Given path params are not valid"
+    INVALID_QUERY_PARAMS = "Given query params are not valid"
+    INVALID_PATH_PARAM_TYPE = "Path params (key, value) must be of type 'str' given type - (%s, %s)"
+    INVALID_QUERY_PARAM_TYPE = "Query params (key, value) must be of type 'str' given type - (%s, %s)"
+
+
+    INVALID_TOKEN_TYPE = "Token key has value of type %s, expected string"
+    PARTIAL_SUCCESS = "Partial success has occurred"
 
 class SkyflowError(Exception):
-    def __init__(self, code, message="An Error occured") -> None:
+    def __init__(self, code, message="An Error occured", data={}) -> None:
         if type(code) is SkyflowErrorCodes:
             self.code = code.value
         else:
@@ -29,5 +50,6 @@ def __init__(self, code, message="An Error occured") -> None:
             self.message = message.value
         else:
             self.message = message
+        self.data = data
         super().__init__(self.message)
 
diff --git a/skyflow/Vault/__init__.py b/skyflow/Vault/__init__.py
new file mode 100644
index 0000000..fa6e98d
--- /dev/null
+++ b/skyflow/Vault/__init__.py
@@ -0,0 +1,2 @@
+from ._client import Client
+from ._config import * 
\ No newline at end of file
diff --git a/skyflow/Vault/_client.py b/skyflow/Vault/_client.py
new file mode 100644
index 0000000..013e352
--- /dev/null
+++ b/skyflow/Vault/_client.py
@@ -0,0 +1,47 @@
+import requests
+from ._insert import getInsertRequestBody, processResponse, convertResponse
+from ._config import SkyflowConfiguration
+from ._config import InsertOptions, GatewayConfig
+from ._gateway import createRequest
+from ._detokenize import sendDetokenizeRequests, createDetokenizeResponseBody
+import asyncio
+from skyflow.Errors._skyflowErrors import SkyflowError, SkyflowErrorCodes, SkyflowErrorMessages   
+
+class Client:
+    def __init__(self, config: SkyflowConfiguration):
+        self.vaultID = config.vaultID
+        self.vaultURL = config.vaultURL.rstrip('/')
+        self.tokenProvider = config.tokenProvider
+
+    def insert(self, data: dict, options: InsertOptions = InsertOptions()):
+        jsonBody = getInsertRequestBody(data, options.tokens)
+        requestURL = self.vaultURL + "/v1/vaults/" + self.vaultID
+        token = self.tokenProvider()
+        headers = {
+            "Authorization": "Bearer " + token
+        }
+        response = requests.post(requestURL, data=jsonBody, headers=headers)
+        processedResponse = processResponse(response)
+        return convertResponse(data, processedResponse, options.tokens)
+
+    def invokeGateway(self, config: GatewayConfig):
+        session = requests.Session()
+        token = self.tokenProvider()
+        request = createRequest(config)
+        request.headers['X-Skyflow-Authorization'] = token
+        response = session.send(request)
+        session.close()
+        return processResponse(response)
+
+    def detokenize(self, data):
+        token = self.tokenProvider()
+        url = self.vaultURL + "/v1/vaults/" + self.vaultID + "/detokenize"
+        responses = asyncio.run(sendDetokenizeRequests(data, url, token))
+        result, partial = createDetokenizeResponseBody(responses)
+        if partial:
+            raise SkyflowError(SkyflowErrorCodes.PARTIAL_SUCCESS ,SkyflowErrorMessages.PARTIAL_SUCCESS, result)
+        else:
+            return result
+        
+
+
diff --git a/skyflow/Vault/_config.py b/skyflow/Vault/_config.py
new file mode 100644
index 0000000..117c9c8
--- /dev/null
+++ b/skyflow/Vault/_config.py
@@ -0,0 +1,29 @@
+from enum import Enum
+
+
+class SkyflowConfiguration:
+    def __init__(self, vaultID: str, vaultURL: str, tokenProvider):
+        self.vaultID = vaultID
+        self.vaultURL = vaultURL
+        self.tokenProvider = tokenProvider
+
+class InsertOptions:
+    def __init__(self, tokens: bool=True):
+        self.tokens = tokens
+
+class RequestMethod(Enum):
+    GET = 'GET'
+    POST = 'POST'
+    PUT = 'PUT'
+    PATCH = 'PATCH'
+    DELETE = 'DELETE'
+class GatewayConfig:
+    def __init__(self, gatewayURL: str, methodName: RequestMethod, 
+    pathParams: dict={}, queryParams: dict={}, requestHeader: dict={}, requestBody: dict={}, responseBody: dict={}):
+        self.gatewayURL = gatewayURL.rstrip("/")
+        self.methodName = methodName
+        self.pathParams = pathParams
+        self.queryParams = queryParams
+        self.requestHeader = requestHeader
+        self.requestBody = requestBody
+        self.responseBody = responseBody
diff --git a/skyflow/Vault/_detokenize.py b/skyflow/Vault/_detokenize.py
new file mode 100644
index 0000000..b857cef
--- /dev/null
+++ b/skyflow/Vault/_detokenize.py
@@ -0,0 +1,74 @@
+from skyflow.Errors._skyflowErrors import SkyflowError, SkyflowErrorCodes, SkyflowErrorMessages
+import asyncio
+from aiohttp import ClientSession
+import json
+
+def getDetokenizeRequestBody(data):
+    try:
+        token = data["token"]
+    except KeyError:
+        raise SkyflowError(SkyflowErrorCodes.INVALID_INPUT, SkyflowErrorMessages.TOKEN_KEY_ERROR)
+    if not isinstance(token, str):
+        tokenType = str(type(token))
+        raise SkyflowError(SkyflowErrorCodes.INVALID_INPUT, SkyflowErrorMessages.INVALID_TOKEN_TYPE.value%(tokenType))
+    requestBody = {"detokenizationParameters": []}
+    requestBody["detokenizationParameters"].append({
+        "token": token})
+    return requestBody
+
+async def sendDetokenizeRequests(data, url, token):
+    
+    tasks = []
+
+    try:
+        records = data["records"]
+    except KeyError:
+        raise SkyflowError(SkyflowErrorCodes.INVALID_INPUT, SkyflowErrorMessages.RECORDS_KEY_ERROR)
+    if not isinstance(records, list):
+        recordsType = str(type(records))
+        raise SkyflowError(SkyflowErrorCodes.INVALID_INPUT, SkyflowErrorMessages.INVALID_RECORDS_TYPE.value%(recordsType))
+        
+    validatedRecords = []
+    for record in records:
+        requestBody = getDetokenizeRequestBody(record)
+        jsonBody = json.dumps(requestBody)
+        validatedRecords.append(jsonBody)
+    async with ClientSession() as session:
+        for record in validatedRecords:
+            headers = {
+                "Authorization": "Bearer " + token
+            }
+            task = asyncio.ensure_future(post(url, record, headers, session))
+            tasks.append(task)
+        await asyncio.gather(*tasks)
+        await session.close()
+    return tasks
+
+
+async def post(url, data, headers, session):
+    async with session.post(url, data=data, headers=headers, ssl=False) as response:
+        return (await response.read(), response.status)
+
+def createDetokenizeResponseBody(responses):
+    result = {
+        "success" : [],
+        "errors" : []
+    }
+    for response in responses:
+        partial = False
+        r = response.result()
+        jsonRes = json.loads(r[0].decode('utf-8'))
+        status = r[1]
+
+        if status == 200:
+            temp = {}
+            temp["token"] = jsonRes["records"][0]["token"]
+            temp["value"] = jsonRes["records"][0]["value"]
+            result["success"].append(temp)
+        else:
+            temp = {"error": {}}
+            temp["error"]["code"] = jsonRes["error"]["http_code"]
+            temp["error"]["description"] = jsonRes["error"]["message"]
+            result["errors"].append(temp)
+            partial = True
+    return result, partial
diff --git a/skyflow/Vault/_gateway.py b/skyflow/Vault/_gateway.py
new file mode 100644
index 0000000..c4616cc
--- /dev/null
+++ b/skyflow/Vault/_gateway.py
@@ -0,0 +1,60 @@
+from requests.sessions import PreparedRequest
+from ._config import GatewayConfig
+from skyflow.Errors._skyflowErrors import *
+import requests
+import json
+
+def createRequest(config: GatewayConfig) -> PreparedRequest:
+    url = parsePathParams(config.gatewayURL.rstrip('/'), config.pathParams)
+
+    try:
+        if isinstance(config.requestHeader, dict):
+            header = json.loads(json.dumps(config.requestHeader))
+        else:
+            raise Exception()
+    except Exception:
+        raise SkyflowError(SkyflowErrorCodes.INVALID_INPUT, SkyflowErrorMessages.INVALID_HEADERS)
+
+    try:
+        if isinstance(config.requestBody, dict):
+            jsonData = json.dumps(config.requestBody)
+        else:
+            raise Exception()
+    except Exception:
+        raise SkyflowError(SkyflowErrorCodes.INVALID_INPUT, SkyflowErrorMessages.INVALID_REQUEST_BODY)
+
+    verifyParams(config.queryParams, config.pathParams)
+
+    try:
+        return requests.Request(
+        method=config.methodName.value,
+        url=url,
+        data=jsonData,
+        headers=header,
+        params=config.queryParams
+    ).prepare()
+    except requests.exceptions.InvalidURL:
+        raise SkyflowError(SkyflowErrorCodes.INVALID_INPUT, SkyflowErrorMessages.INVALID_URL.value%(config.gatewayURL))
+
+def parsePathParams(url, pathParams):
+    result = url
+    for param, value in pathParams.items():
+        result = result.replace('{' + param + '}', value)
+        
+    return result
+
+def verifyParams(queryParams, pathParams):
+    if not isinstance(pathParams, dict):
+        raise SkyflowError(SkyflowErrorCodes.INVALID_INPUT, SkyflowErrorMessages.INVALID_PATH_PARAMS)
+    if not isinstance(queryParams, dict):
+        raise SkyflowError(SkyflowErrorCodes.INVALID_INPUT, SkyflowErrorMessages.INVALID_QUERY_PARAMS)
+
+    for param, value in pathParams.items():
+        if not(isinstance(param, str) and isinstance(value, str)):
+            raise SkyflowError(SkyflowErrorCodes.INVALID_INPUT, SkyflowErrorMessages.INVALID_PATH_PARAM_TYPE.value%(str(type(param)), str(type(value))))
+    
+    for param, value in queryParams.items():
+        if not(isinstance(param, str) and isinstance(value, str)):
+            raise SkyflowError(SkyflowErrorCodes.INVALID_INPUT, SkyflowErrorMessages.INVALID_QUERY_PARAM_TYPE.value%(str(type(param)), str(type(value))))
+
+
diff --git a/skyflow/Vault/_insert.py b/skyflow/Vault/_insert.py
new file mode 100644
index 0000000..2f10b29
--- /dev/null
+++ b/skyflow/Vault/_insert.py
@@ -0,0 +1,86 @@
+import json
+
+import requests
+from requests.models import HTTPError
+from skyflow.Errors._skyflowErrors import SkyflowError, SkyflowErrorCodes, SkyflowErrorMessages
+
+
+def getInsertRequestBody(data, tokens: bool):
+    try:
+        records = data["records"]
+    except KeyError:
+        raise SkyflowError(SkyflowErrorCodes.INVALID_INPUT, SkyflowErrorMessages.RECORDS_KEY_ERROR)
+
+    if not isinstance(records, list):
+        recordsType = str(type(records))
+        raise SkyflowError(SkyflowErrorCodes.INVALID_INPUT, SkyflowErrorMessages.INVALID_RECORDS_TYPE.value%(recordsType))
+        
+    requestPayload = []
+    insertTokenPayload = []
+    for index, record in enumerate(records):
+        tableName, fields = getTableAndFields(record)
+        requestPayload.append({
+                "tableName": tableName,
+                "fields": fields,
+                "method": "POST",
+                "quorum": True})
+        if tokens:
+            insertTokenPayload.append({
+                "method": "GET",
+                "tableName": tableName,
+                "ID": "$responses." + str(index) + ".records.0.skyflow_id",
+                "tokenization": True
+            })
+    requestBody = {"records": requestPayload + insertTokenPayload}
+    try:
+        jsonBody = json.dumps(requestBody)
+    except Exception as e:
+        raise SkyflowError(SkyflowErrorCodes.INVALID_INPUT ,SkyflowErrorMessages.INVALID_JSON.value%('insert payload'))
+
+    return jsonBody
+
+def getTableAndFields(record):
+    try:
+        table = record["table"]
+    except KeyError:
+        raise SkyflowError(SkyflowErrorCodes.INVALID_INPUT, SkyflowErrorMessages.TABLE_KEY_ERROR)
+    
+    if not isinstance(table, str):
+        tableType = str(type(table))
+        raise SkyflowError(SkyflowErrorCodes.INVALID_INPUT, SkyflowErrorMessages.INVALID_TABLE_TYPE.value%(tableType))
+
+    try:
+        fields = record["fields"]
+    except KeyError:
+        raise SkyflowError(SkyflowErrorCodes.INVALID_INPUT, SkyflowErrorMessages.FIELDS_KEY_ERROR)
+
+    if not isinstance(fields, dict):
+        fieldsType = str(type(table))
+        raise SkyflowError(SkyflowErrorCodes.INVALID_INPUT, SkyflowErrorMessages.INVALID_FIELDS_TYPE.value%(fieldsType))
+
+    return (table, fields)
+
+def processResponse(response: requests.Response):
+    statusCode = response.status_code
+    strcontent = response.content.decode('utf-8')
+    try:
+        response.raise_for_status()
+        return json.loads(strcontent)
+    except HTTPError:
+        raise SkyflowError(statusCode, strcontent)
+
+def convertResponse(request: dict, response: dict, tokens: bool):
+    responseArray = response['responses']
+    records = request['records']
+    recordsSize = len(records)
+    result = []
+    for id, _ in enumerate(request):
+        table = records[id]['table']
+        skyflow_id = responseArray[0]['records'][id]['skyflow_id']
+        if tokens:
+            fieldsDict = responseArray[recordsSize + id]['fields']
+            fieldsDict['skyflow_id'] = skyflow_id
+            result.append({'table': table, 'fields': fieldsDict})
+        else:
+            result.append({'table': table, 'skyflow_id': skyflow_id})
+    return {'records': result}
\ No newline at end of file


Test patch:
diff --git a/tests/ServiceAccount/testGenerateToken.py b/tests/ServiceAccount/testGenerateToken.py
index 366545e..318d6f8 100644
--- a/tests/ServiceAccount/testGenerateToken.py
+++ b/tests/ServiceAccount/testGenerateToken.py
@@ -1,5 +1,6 @@
 import unittest
 import os
+import sys
 from skyflow.ServiceAccount import GenerateToken
 from skyflow.Errors._skyflowErrors import *
 
@@ -61,4 +62,5 @@ def testInvalidCreds(self):
              GenerateToken(self.getDataPath('invalidPrivateKey'))
         except SkyflowError as se:
             self.assertEqual(se.code, SkyflowErrorCodes.INVALID_INPUT.value)
-            self.assertEqual(se.message, SkyflowErrorMessages.JWT_INVALID_FORMAT.value)
\ No newline at end of file
+            self.assertEqual(se.message, SkyflowErrorMessages.JWT_INVALID_FORMAT.value)
+
diff --git a/tests/Vault/__init__.py b/tests/Vault/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/Vault/testConfig.py b/tests/Vault/testConfig.py
new file mode 100644
index 0000000..4d97837
--- /dev/null
+++ b/tests/Vault/testConfig.py
@@ -0,0 +1,27 @@
+import unittest
+
+from skyflow.Vault._config import *
+
+class TestConfig(unittest.TestCase):
+    def testInsertOptions(self):
+        defaultOptions = InsertOptions()
+        noTokensOption = InsertOptions(tokens=False)
+
+        self.assertEqual(defaultOptions.tokens, True)
+        self.assertEqual(noTokensOption.tokens, False)
+
+    def testSkyflowConfig(self):
+        myconfig = SkyflowConfiguration("vaultID", "https://vaults.skyflow.com", lambda: "token")
+        self.assertEqual(myconfig.vaultID, "vaultID")
+        self.assertEqual(myconfig.vaultURL, "https://vaults.skyflow.com")
+        self.assertEqual(myconfig.tokenProvider(), "token")
+
+    def testGatewayConfigDefaults(self):
+        gatewayConfig = GatewayConfig('https://skyflow.com', methodName=RequestMethod.GET)
+        self.assertEqual(gatewayConfig.gatewayURL, 'https://skyflow.com')
+        self.assertEqual(gatewayConfig.methodName, RequestMethod.GET)
+        self.assertDictEqual(gatewayConfig.pathParams, {})
+        self.assertDictEqual(gatewayConfig.queryParams, {})
+        self.assertDictEqual(gatewayConfig.requestHeader, {})
+        self.assertDictEqual(gatewayConfig.requestBody, {})
+        self.assertDictEqual(gatewayConfig.responseBody, {})
\ No newline at end of file
diff --git a/tests/Vault/testDetokenize.py b/tests/Vault/testDetokenize.py
new file mode 100644
index 0000000..b7a0221
--- /dev/null
+++ b/tests/Vault/testDetokenize.py
@@ -0,0 +1,98 @@
+import unittest
+import os
+from skyflow.Vault._detokenize import getDetokenizeRequestBody
+from skyflow.Errors._skyflowErrors import SkyflowError, SkyflowErrorCodes, SkyflowErrorMessages
+from skyflow.Vault._client import Client, SkyflowConfiguration
+from skyflow.ServiceAccount import GenerateToken
+from dotenv import dotenv_values
+
+class TestDetokenize(unittest.TestCase):
+
+    def setUp(self) -> None:
+        self.envValues = dotenv_values(".env")
+        self.dataPath = os.path.join(os.getcwd(), 'tests/Vault/data/')
+        self.testToken = self.envValues["DETOKENIZE_TEST_TOKEN"]
+        self.tokenField = {
+            "token": self.envValues["DETOKENIZE_TEST_TOKEN"]
+        }
+        self.data = {"records": [self.tokenField]}
+        def tokenProvider():
+            token, type = GenerateToken(self.envValues["CREDENTIALS_FILE_PATH"])
+            return token
+
+        config = SkyflowConfiguration(self.envValues["VAULT_ID"], self.envValues["VAULT_URL"], tokenProvider)
+        self.client = Client(config)
+        return super().setUp()
+
+
+    def getDataPath(self, file):
+        return self.dataPath + file + '.json'
+
+    def testGetDetokenizeRequestBodyWithValidBody(self):
+        body = getDetokenizeRequestBody(self.tokenField)
+        expectedOutput = {
+            "detokenizationParameters": [{
+                "token": self.testToken
+            }]
+        }
+
+        self.assertEqual(body, expectedOutput)
+
+    def testDetokenizeNoRecords(self):
+        invalidData = {"invalidKey": self.tokenField}
+        try:
+            self.client.detokenize(invalidData)
+            self.fail('Should have thrown an error')
+        except SkyflowError as e:
+            self.assertEqual(e.code, SkyflowErrorCodes.INVALID_INPUT.value)
+            self.assertEqual(e.message, SkyflowErrorMessages.RECORDS_KEY_ERROR.value)
+
+    def testDetokenizeRecordsInvalidType(self):
+        invalidData = {"records": "invalid"}
+        try:
+            self.client.detokenize(invalidData)
+            self.fail('Should have thrown an error')
+        except SkyflowError as e:
+            self.assertEqual(e.code, SkyflowErrorCodes.INVALID_INPUT.value)
+            self.assertEqual(e.message, SkyflowErrorMessages.INVALID_RECORDS_TYPE.value%(str))
+    
+    def testDetokenizeNoToken(self):
+        invalidData = {"records": [{"invalid": "invalid"}]}
+        try:
+            self.client.detokenize(invalidData)
+            self.fail('Should have thrown an error')
+        except SkyflowError as e:
+            self.assertEqual(e.code, SkyflowErrorCodes.INVALID_INPUT.value)
+            self.assertEqual(e.message, SkyflowErrorMessages.TOKEN_KEY_ERROR.value)
+
+    def testDetokenizeTokenInvalidType(self):
+        invalidData = {"records": [{"token": ["invalid"]}]}
+        try:
+            self.client.detokenize(invalidData)
+            self.fail('Should have thrown an error')
+        except SkyflowError as e:
+            self.assertEqual(e.code, SkyflowErrorCodes.INVALID_INPUT.value)
+            self.assertEqual(e.message, SkyflowErrorMessages.INVALID_TOKEN_TYPE.value%(list))
+
+    def testDetokenizeSuccess(self):
+        invalidData = {"records": [{"token": self.envValues["DETOKENIZE_TEST_TOKEN"]}]}
+        try:
+            response = self.client.detokenize(invalidData)
+            self.assertEqual(response["success"][0]["token"], self.envValues["DETOKENIZE_TEST_TOKEN"])
+            self.assertEqual(response["success"][0]["value"], self.envValues["DETOKENIZE_TEST_VALUE"])
+        except SkyflowError as e:
+            self.fail('Should not throw an error')
+        
+    def testDetokenizePartialSuccess(self):
+        invalidData = {"records": [{"token": self.envValues["DETOKENIZE_TEST_TOKEN"]}, {"token": "invalid-token"}]}
+        try:
+            self.client.detokenize(invalidData)
+            self.fail('Should have thrown an error')
+        except SkyflowError as e:
+            self.assertEqual(e.code, SkyflowErrorCodes.PARTIAL_SUCCESS.value)
+            self.assertEqual(e.message, SkyflowErrorMessages.PARTIAL_SUCCESS.value)
+            self.assertEqual(e.data["success"][0]["token"], self.envValues["DETOKENIZE_TEST_TOKEN"])
+            self.assertEqual(e.data["success"][0]["value"], self.envValues["DETOKENIZE_TEST_VALUE"])
+            self.assertEqual(e.data["errors"][0]["error"]["code"], 404)
+            self.assertEqual(e.data["errors"][0]["error"]["description"], "Token not found for invalid-token")
+
diff --git a/tests/Vault/testGateway.py b/tests/Vault/testGateway.py
new file mode 100644
index 0000000..95f3f15
--- /dev/null
+++ b/tests/Vault/testGateway.py
@@ -0,0 +1,151 @@
+import unittest
+
+from skyflow.ServiceAccount._token import GenerateToken
+from skyflow.Vault._gateway import *
+from skyflow.Vault._client import *
+from skyflow.Vault._config import *
+from skyflow.Errors._skyflowErrors import *
+from dotenv import dotenv_values
+
+class testGateway(unittest.TestCase):
+    def testCreateRequestDefault(self):
+        config = GatewayConfig('https://skyflow.com/', RequestMethod.GET)
+        try:
+            req = createRequest(config)
+            body, url, method = req.body, req.url, req.method
+            self.assertEqual(url, 'https://skyflow.com/')
+            self.assertEqual(body, '{}')
+            self.assertEqual(method, RequestMethod.GET.value)
+        except SkyflowError:
+            self.fail()
+
+    def testCreateRequestInvalidJSONBody(self):
+        invalidJsonBody = {'somekey': unittest}
+        config = GatewayConfig('https://skyflow.com/', RequestMethod.GET, requestBody=invalidJsonBody)
+        try:
+            createRequest(config)
+            self.fail()
+        except SkyflowError as e:
+            self.assertEqual(e.code, SkyflowErrorCodes.INVALID_INPUT.value)
+            self.assertEqual(e.message, SkyflowErrorMessages.INVALID_REQUEST_BODY.value)
+
+    def testCreateRequestInvalidBodyType(self):
+        nonDictBody = 'body'
+        config = GatewayConfig('https://skyflow.com/', RequestMethod.GET, requestBody=nonDictBody)
+        try:
+            createRequest(config)
+            self.fail()
+        except SkyflowError as e:
+            self.assertEqual(e.code, SkyflowErrorCodes.INVALID_INPUT.value)
+            self.assertEqual(e.message, SkyflowErrorMessages.INVALID_REQUEST_BODY.value)
+
+    def testCreateRequestBodyInvalidHeadersJson(self):
+        invalidJsonHeaders = {'somekey': unittest}
+        config = GatewayConfig('https://skyflow.com/', RequestMethod.GET, requestHeader=invalidJsonHeaders)
+        try:
+            createRequest(config)
+            self.fail()
+        except SkyflowError as e:
+            self.assertEqual(e.code, SkyflowErrorCodes.INVALID_INPUT.value)
+            self.assertEqual(e.message, SkyflowErrorMessages.INVALID_HEADERS.value)
+
+    
+    def testCreateRequestBodyHeadersNotDict(self):
+        invalidJsonHeaders = 'invalidheaderstype'
+        config = GatewayConfig('https://skyflow.com/', RequestMethod.GET, requestHeader=invalidJsonHeaders)
+        try:
+            createRequest(config)
+            self.fail()
+        except SkyflowError as e:
+            self.assertEqual(e.code, SkyflowErrorCodes.INVALID_INPUT.value)
+            self.assertEqual(e.message, SkyflowErrorMessages.INVALID_HEADERS.value)
+
+    def testCreateRequestInvalidURL(self):
+        invalidUrl = 'https::///skyflow.com'
+        config = GatewayConfig(invalidUrl, RequestMethod.GET)
+        try:
+            createRequest(config)
+            self.fail()
+        except SkyflowError as e:
+            self.assertEqual(e.code, SkyflowErrorCodes.INVALID_INPUT.value)
+            self.assertEqual(e.message, SkyflowErrorMessages.INVALID_URL.value%(invalidUrl))
+
+
+    def testPathParams(self):
+        try:
+            url = parsePathParams(url='https://skyflow.com/{name}/{department}/content/{action}', 
+            pathParams={'name': 'john', 'department': 'test', 'action': 'download'})
+
+            expectedURL = 'https://skyflow.com/john/test/content/download'
+
+            self.assertEqual(url, expectedURL)
+        except SkyflowError as e:
+            self.fail()
+
+    def testVerifyParamsPathParamsNotDict(self):
+        pathParams = {'name': 'john', 'department': ['test'], 'action': 1}
+        try:
+            verifyParams({}, pathParams)
+            self.fail()
+        except SkyflowError as e:
+            self.assertEqual(e.code, SkyflowErrorCodes.INVALID_INPUT.value)
+            self.assertEqual(e.message, SkyflowErrorMessages.INVALID_PATH_PARAM_TYPE.value%(str(type('department')), str(type(['str']))))
+    def testVerifyParamsQueryParamsNotDict(self):
+        queryParams = {'name': 'john', 'department': ['test'], 'action': 1}
+        try:
+            verifyParams(queryParams, {})
+            self.fail()
+        except SkyflowError as e:
+            self.assertEqual(e.code, SkyflowErrorCodes.INVALID_INPUT.value)
+            self.assertEqual(e.message, SkyflowErrorMessages.INVALID_QUERY_PARAM_TYPE.value%(str(type('name')), str(type(['str']))))
+    
+    def testVerifyParamsInvalidPathParams(self):
+        pathParams = 'string'
+        try:
+            verifyParams({}, pathParams)
+            self.fail()
+        except SkyflowError as e:
+            self.assertEqual(e.code, SkyflowErrorCodes.INVALID_INPUT.value)
+            self.assertEqual(e.message, SkyflowErrorMessages.INVALID_PATH_PARAMS.value)
+    def testVerifyParamsInvalidQueryParams(self):
+        queryParams = 'string'
+        try:
+            verifyParams(queryParams, {})
+            self.fail()
+        except SkyflowError as e:
+            self.assertEqual(e.code, SkyflowErrorCodes.INVALID_INPUT.value)
+            self.assertEqual(e.message, SkyflowErrorMessages.INVALID_QUERY_PARAMS.value)
+
+    def testInvokeGatewayCvvGenSuccess(self):
+        env_values = dotenv_values('.env')
+        gatewayUrl = env_values['CVV_GEN_GATEWAY_URL']
+
+        def tokenProvider():
+            token, _ = GenerateToken(env_values['CREDENTIALS_FILE_PATH'])
+            return token
+
+        config = SkyflowConfiguration(env_values['VAULT_ID'], env_values['VAULT_URL'], tokenProvider)
+        gatewayConfig = GatewayConfig(gatewayUrl, RequestMethod.POST,
+        requestHeader={
+                    'Content-Type': 'application/json',
+                    'Authorization': env_values['VISA_GATEWAY_BASIC_AUTH']
+        },
+        requestBody=
+        {
+            "expirationDate": {
+                "mm": "12",
+                "yy": "22"
+            }
+        },
+        pathParams={'cardID': env_values['DETOKENIZE_TEST_TOKEN']})
+        client = Client(config)
+
+
+        try:
+            resp = client.invokeGateway(gatewayConfig)
+            self.assertIsNotNone(resp['resource']['cvv2'])
+            self.assertIsNotNone(resp['processingTimeinMs'])
+            self.assertIsNotNone(resp['receivedTimestamp'])
+        except SkyflowError as e:
+            print(e)
+            self.fail()
\ No newline at end of file
diff --git a/tests/Vault/testInsert.py b/tests/Vault/testInsert.py
new file mode 100644
index 0000000..6c044a4
--- /dev/null
+++ b/tests/Vault/testInsert.py
@@ -0,0 +1,233 @@
+import json
+import unittest
+import os
+from requests.models import Response
+from dotenv import dotenv_values
+from skyflow.Vault._insert import getInsertRequestBody, processResponse
+from skyflow.Errors._skyflowErrors import SkyflowError, SkyflowErrorCodes, SkyflowErrorMessages
+from skyflow.ServiceAccount import GenerateToken
+from skyflow.Vault._client import Client
+from skyflow.Vault._config import SkyflowConfiguration, InsertOptions
+
+class TestInsert(unittest.TestCase):
+
+    def setUp(self) -> None:
+        self.dataPath = os.path.join(os.getcwd(), 'tests/Vault/data/')
+        field = {
+            "table": "persons",
+            "fields": {
+                "cardNumber": "4111-1111-1111-1111",
+                "cvv": "234"
+            }
+        }
+        self.data = {"records": [field]}
+        return super().setUp()
+
+
+    def getDataPath(self, file):
+        return self.dataPath + file + '.json'
+
+    def testGetInsertRequestBodyWithValidBody(self):
+        body = json.loads(getInsertRequestBody(self.data, True))
+        expectedOutput = {
+            "tableName": "persons",
+            "fields": {
+                "cardNumber": "4111-1111-1111-1111",
+                "cvv": "234"
+            },
+            "method": 'POST',
+            "quorum": True
+        }
+        self.assertEqual(body["records"][0], expectedOutput)
+
+    def testGetInsertRequestBodyNoRecords(self):
+        invalidData = {"invalidKey": self.data["records"]}
+        try:
+            getInsertRequestBody(invalidData, True)
+            self.fail('Should have thrown an error')
+        except SkyflowError as e:
+            self.assertEqual(e.code, SkyflowErrorCodes.INVALID_INPUT.value)
+            self.assertEqual(e.message, SkyflowErrorMessages.RECORDS_KEY_ERROR.value)
+
+    def testGetInsertRequestBodyRecordsInvalidType(self):
+        invalidData = {"records": 'records'}
+        try:
+            getInsertRequestBody(invalidData, True)
+            self.fail('Should have thrown an error')
+        except SkyflowError as e:
+            self.assertEqual(e.code, SkyflowErrorCodes.INVALID_INPUT.value)
+            self.assertEqual(e.message, SkyflowErrorMessages.INVALID_RECORDS_TYPE.value % (str(type('str'))))
+
+    def testGetInsertRequestBodyNoFields(self):
+        invalidData = {"records": [{
+            "table": "table",
+            "fields": {
+                "card_number": "4111-1111"
+            }
+        },
+        {
+            "table": "table",
+            "invalid": {}
+        }
+        ]}
+        try:
+            getInsertRequestBody(invalidData, True)
+            self.fail('Should have thrown an error')
+        except SkyflowError as e:
+            self.assertEqual(e.code, SkyflowErrorCodes.INVALID_INPUT.value)
+            self.assertEqual(e.message, SkyflowErrorMessages.FIELDS_KEY_ERROR.value)
+
+    def testGetInsertRequestBodyInvalidFieldsType(self):
+        invalidData = {"records": [{
+            "table": "table",
+            "fields": 'fields'
+        }
+        ]}
+        try:
+            getInsertRequestBody(invalidData, True)
+            self.fail('Should have thrown an error')
+        except SkyflowError as e:
+            self.assertEqual(e.code, SkyflowErrorCodes.INVALID_INPUT.value)
+            self.assertEqual(e.message, SkyflowErrorMessages.INVALID_FIELDS_TYPE.value % (str(type('str'))))
+
+
+    def testGetInsertRequestBodyNoTable(self):
+        invalidData = {"records": [{
+            "noTable": "tableshouldbehere",
+            "fields": {
+                "card_number": "4111-1111"
+            }
+        },
+        {
+            "table": "table",
+            "invalid": {}
+        }
+        ]}
+        try:
+            getInsertRequestBody(invalidData, True)
+            self.fail('Should have thrown an error')
+        except SkyflowError as e:
+            self.assertEqual(e.code, SkyflowErrorCodes.INVALID_INPUT.value)
+            self.assertEqual(e.message, SkyflowErrorMessages.TABLE_KEY_ERROR.value)
+
+    def testGetInsertRequestBodyInvalidTableType(self):
+        invalidData = {"records": [{
+            "table": {'invalidtype': 'thisisinvalid'},
+            "fields": {
+                "card_number": "4111-1111"
+            }
+        }
+        ]}
+        try:
+            getInsertRequestBody(invalidData, True)
+            self.fail('Should have thrown an error')
+        except SkyflowError as e:
+            self.assertEqual(e.code, SkyflowErrorCodes.INVALID_INPUT.value)
+            self.assertEqual(e.message, SkyflowErrorMessages.INVALID_TABLE_TYPE.value % (str(type({'a': 'b'}))))
+
+    def testInsertInvalidJson(self):
+        invalidjson = {
+            "records": [{
+                "table": "table",
+                "fields": {
+                    "invalid": json
+                }
+            }]
+        }
+
+        try:
+            getInsertRequestBody(invalidjson, True)
+            self.fail('Should have thrown an error')
+        except SkyflowError as e:
+            self.assertEqual(e.code, SkyflowErrorCodes.INVALID_INPUT.value)
+            self.assertEqual(e.message, SkyflowErrorMessages.INVALID_JSON.value%('insert payload'))
+
+    def testProcessInvalidResponse(self):
+        response = Response()
+        response.status_code = 500
+        response._content = b"Invalid Request"
+        try:
+            processResponse(response)
+            self.fail()
+        except SkyflowError as e:
+            self.assertEqual(e.code, 500)
+            self.assertEqual(e.message, response.content.decode('utf-8'))
+
+    def testProcessValidResponse(self):
+        response = Response()
+        response.status_code = 200
+        response._content = b'{"key": "value"}'
+        try:
+            responseDict = processResponse(response)
+            self.assertDictEqual(responseDict, {'key': 'value'})
+        except SkyflowError as e:
+            self.fail()
+
+    def testClientInit(self):
+        config = SkyflowConfiguration('bdc271aee8584eed88253877019657b3', 'https://sb.area51.vault.skyflowapis.dev', lambda: 'test')
+        client = Client(config)
+        self.assertEqual(client.vaultURL, config.vaultURL)
+        self.assertEqual(client.vaultID, config.vaultID)
+        self.assertEqual(client.tokenProvider, config.tokenProvider)
+
+    def testClientInsert(self):
+        env_values = dotenv_values('.env')
+
+        def tokenProvider():
+            token, _ = GenerateToken(env_values['CREDENTIALS_FILE_PATH'])
+            return token
+
+        config = SkyflowConfiguration(env_values['VAULT_ID'], env_values['VAULT_URL'], tokenProvider)
+        client = Client(config)
+
+        options = InsertOptions(False)
+
+        data = {
+            "records": [
+                {
+                    "table": "persons",
+                    "fields": {
+                        "cvv": "122",
+                        "card_expiration": "1221",
+                        "card_number": "4111111111111111",
+                        "name": {"first_name": "Bob"}
+                    }
+                }
+            ]
+        }
+        try:
+            response = client.insert(data, options=options)
+            self.assertEqual(len(response['records']), 1)
+        except SkyflowError as e:
+            self.fail()
+
+    def testClientInsertWithTokens(self):
+        env_values = dotenv_values('.env')
+
+        def tokenProvider():
+            token, _ = GenerateToken(env_values['CREDENTIALS_FILE_PATH'])
+            return token
+
+        config = SkyflowConfiguration(env_values['VAULT_ID'], env_values['VAULT_URL'], tokenProvider)
+        client = Client(config)
+
+        options = InsertOptions(True)
+
+        data = {
+            "records": [
+                {
+                    "table": "persons",
+                    "fields": {
+                        "cvv": "122",
+                        "card_expiration": "1221",
+                        "card_number": "4111111111111111",
+                        "name": {"first_name": "Bob"}
+                    }
+                }
+            ]
+        }
+        try:
+            response = client.insert(data, options=options)
+            self.assertEqual(len(response['records']), 1)
+        except SkyflowError as e:
+            self.fail()