diff --git a/.gitignore b/.gitignore
index 6e408509a217113a28e975536bb60e2a6e9e57e6..345414100643f71ae1a17b45ad34413f10ac85da 100644
--- a/.gitignore
+++ b/.gitignore
@@ -37,6 +37,7 @@ build/
 
 ### Integration tests ###
 .gradle
+**/allure-results/
 
 ### Environment Configuration ###
 *.env
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index a57fda27b072fa10653eb9818967b8fdb4f0de1b..b25beafe41df2996ee999643f6c0ec0190e4abf6 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -34,6 +34,7 @@ variables:
   IBM_HELM_DEPLOY_PATH: devops/ibm/ibm-storage-deploy
 
   CORE_BUILD_SUBDIR: storage-core
+  ACCEPTANCE_TEST_DIR: "storage-acceptance-test"
   CORE_COVERAGE_THRESHOLD: "80"
 include:
   - project: "osdu/platform/ci-cd-pipelines"
@@ -247,6 +248,94 @@ azure_test:
 aws-test-java:
   image: $CI_REGISTRY/osdu/platform/deployment-and-operations/base-containers-aws/aws-maven/aws-maven:v2.0
 
+core-acceptance-test:
+  stage: acceptance
+  extends: core-test
+  needs: ["core-test"]
+  variables:
+    OPA_INTEGRATION_ENABLED: "true"
+    ROOT_USER_OPENID_PROVIDER_CLIENT_ID: $DATA_ROOT_OPENID_PROVIDER_CLIENT_ID
+    ROOT_USER_OPENID_PROVIDER_CLIENT_SECRET: $DATA_ROOT_OPENID_PROVIDER_CLIENT_SECRET
+    NO_ACCESS_USER_OPENID_PROVIDER_CLIENT_ID: $TEST_NO_ACCESS_OPENID_PROVIDER_CLIENT_ID
+    NO_ACCESS_USER_OPENID_PROVIDER_CLIENT_SECRET: $TEST_NO_ACCESS_OPENID_PROVIDER_CLIENT_SECRET
+    PRIVILEGED_USER_OPENID_PROVIDER_CLIENT_ID: $TEST_OPENID_PROVIDER_CLIENT_ID
+    PRIVILEGED_USER_OPENID_PROVIDER_CLIENT_SECRET: $TEST_OPENID_PROVIDER_CLIENT_SECRET
+    ENTITLEMENTS_DOMAIN: $GROUP_ID
+  script:
+    - >
+      $MAVEN_BUILD . test-results.log
+      verify -DdisableXmlReport=true
+      --quiet
+      --file $ACCEPTANCE_TEST_DIR/pom.xml
+      --update-snapshots
+
+aws-acceptance-test:
+  extends:
+    - .maven
+    - .aws
+    - .aws_common_variables
+    - .aws_variables
+  stage: acceptance
+  image: $CI_REGISTRY/osdu/platform/deployment-and-operations/base-containers-aws/aws-maven/aws-maven:v2.1
+  needs: [{ job: 'aws-update-tf', optional: true }]
+  before_script:
+    - !reference [.maven, before_script]
+    - !reference [.aws, before_script]
+    - !reference [.aws_variables, before_script]
+  variables:
+    OPA_INTEGRATION_ENABLED: "true"
+  script:
+    - export GROUP_ID=$DOMAIN
+    - export COGNITO_AUTH_TOKEN_URI=$(aws ssm get-parameter --name "/osdu/cognito/${COGNITO_NAME}/oauth/token-uri" --query Parameter.Value --output text --region $AWS_REGION)
+    - export COGNITO_ALLOWED_SCOPES=$(aws ssm get-parameter --name "/osdu/cognito/${COGNITO_NAME}/oauth/allowed-scopes" --query Parameter.Value --output text --region $AWS_REGION)
+    - export AWS_CLIENT_CREDENTIALS_CLIENT_ID=$(aws ssm get-parameter --name "/osdu/cognito/${COGNITO_NAME}/client/client-credentials/id" --query Parameter.Value --output text --region $AWS_REGION)
+    - export AWS_CLIENT_CREDENTIALS_CLIENT_SECRET=$(aws secretsmanager get-secret-value --secret-id /osdu/cognito/${COGNITO_NAME}/client-credentials-secret --query SecretString --output json --region $AWS_REGION | sed -e 's/\\\"/\"/g' -e 's/^.//g' -e 's/.$//g' | jq -r '.client_credentials_client_secret')
+    - export AWS_SERVICE_PRINCIPAL_AUTHORIZATION=$(echo -n "${AWS_CLIENT_CREDENTIALS_CLIENT_ID}:${AWS_CLIENT_CREDENTIALS_CLIENT_SECRET}" | base64)
+    - export PRIVILEGED_USER_TOKEN=$(aws cognito-idp initiate-auth --region ${AWS_REGION} --auth-flow ${AWS_COGNITO_AUTH_FLOW} --client-id ${AWS_COGNITO_CLIENT_ID} --auth-parameters USERNAME=${AWS_COGNITO_AUTH_PARAMS_USER},PASSWORD=${AWS_COGNITO_AUTH_PARAMS_PASSWORD} --query AuthenticationResult.AccessToken --output text)
+
+    - export ROOT_USER_TOKEN=$(curl --location ${COGNITO_AUTH_TOKEN_URI} --header "Content-Type:application/x-www-form-urlencoded" --header "Authorization:Basic ${AWS_SERVICE_PRINCIPAL_AUTHORIZATION}" --data-urlencode "grant_type=client_credentials" --data-urlencode ${COGNITO_ALLOWED_SCOPES}  --http1.1 | jq -r '.access_token')
+    - export NO_ACCESS_USER_TOKEN=$(aws cognito-idp initiate-auth --region ${AWS_REGION} --auth-flow ${AWS_COGNITO_AUTH_FLOW} --client-id ${AWS_COGNITO_CLIENT_ID} --auth-parameters USERNAME=${AWS_COGNITO_AUTH_PARAMS_USER_NO_ACCESS},PASSWORD=${AWS_COGNITO_AUTH_PARAMS_PASSWORD} --query AuthenticationResult.AccessToken --output text)
+    - >
+      $MAVEN_BUILD . test-results.log
+      verify -DdisableXmlReport=true
+      --quiet
+      --file $ACCEPTANCE_TEST_DIR/pom.xml
+      --update-snapshots
+  allow_failure: true
+  only:
+    variables:
+      - $AWS == '1'
+
+azure-acceptance-test:
+  stage: acceptance
+  script:
+    - echo "This job is expected to fail"
+    - exit 1
+  allow_failure: true
+  only:
+    variables:
+      - $AZURE == '1'
+
+gc-acceptance-test:
+  stage: acceptance
+  script:
+    - echo "This job is expected to fail"
+    - exit 1
+  allow_failure: true
+  only:
+    variables:
+      - $GC == '1'
+
+ibm-acceptance-test:
+  stage: acceptance
+  script:
+    - echo "This job is expected to fail"
+    - exit 1
+  allow_failure: true
+  only:
+    variables:
+      - $IBM == '1'
+
 fossa-analyze:
   image: $CI_REGISTRY/divido/fossa-with-cache:v0.9-jdk17
 fossa-check-notice:
diff --git a/storage-acceptance-test/docs/README.md b/storage-acceptance-test/docs/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..0efa62efcb2e8fcc0054d4eedb819cb4a6aea0f7
--- /dev/null
+++ b/storage-acceptance-test/docs/README.md
@@ -0,0 +1,84 @@
+### Running E2E Tests
+
+You will need to have the following environment variables defined.
+
+| name                  | value                                          | description                                    | sensitive? | source |
+|-----------------------|------------------------------------------------|------------------------------------------------|------------|--------|
+| `ENTITLEMENTS_DOMAIN` | ex`opendes-gc.projects.com`                    | OSDU R2 entitlements domain to run tests under | no         | -      |
+| `LEGAL_URL`           | ex`http://localhost:8080/api/legal/v1/`        | Legal API endpoint                             | no         | -      |
+| `STORAGE_URL`         | ex`http://localhost:8080/api/storage/v2/`      | Endpoint of storage service                    | no         | -      |
+| `TENANT_NAME`         | ex `opendes`                                   | OSDU tenant used for testing                   | no         | --     |
+| `ENTITLEMENTS_URL`    | ex`http://localhost:8080/api/entitlements/v2/` | Endpoint of entitlements service               | no         | -      |
+
+Authentication can be provided as OIDC config:
+
+| name                                            | value                                   | description                   | sensitive? | source |
+|-------------------------------------------------|-----------------------------------------|-------------------------------|------------|--------|
+| `ROOT_USER_OPENID_PROVIDER_CLIENT_ID`           | `********`                              | ROOT_USER Client Id           | yes        | -      |
+| `ROOT_USER_OPENID_PROVIDER_CLIENT_SECRET`       | `********`                              | ROOT_USER Client secret       | yes        | -      |
+| `NO_ACCESS_USER_OPENID_PROVIDER_CLIENT_ID`      | `********`                              | NO_ACCESS_USER Client Id      | yes        | -      |
+| `NO_ACCESS_USER_OPENID_PROVIDER_CLIENT_SECRET`  | `********`                              | NO_ACCESS_USER Client secret  | yes        | -      |
+| `PRIVILEGED_USER_OPENID_PROVIDER_CLIENT_ID`     | `********`                              | PRIVILEGED_USER Client Id     | yes        | -      |
+| `PRIVILEGED_USER_OPENID_PROVIDER_CLIENT_SECRET` | `********`                              | PRIVILEGED_USER Client secret | yes        | -      |
+| `TEST_OPENID_PROVIDER_URL`                      | `https://keycloak.com/auth/realms/osdu` | OpenID provider url           | yes        | -      |
+
+Or tokens can be used directly from env variables:
+
+| name                    | value      | description           | sensitive? | source |
+|-------------------------|------------|-----------------------|------------|--------|
+| `PRIVILEGED_USER_TOKEN` | `********` | PRIVILEGED_USER Token | yes        | -      |
+| `NO_ACCESS_USER_TOKEN`  | `********` | NO_ACCESS_USER Token  | yes        | -      |
+| `ROOT_USER_TOKEN`       | `********` | ROOT_USER Token       | yes        | -      |
+
+
+Feature testing is controlled with the following environment variables:
+
+| name                      | value             | description                                                               |
+|---------------------------|-------------------|---------------------------------------------------------------------------|
+| `TEST_REPLAY_ENABLED`     | `true` OR `false` | Controls Replay API tests.                                                |
+| `COLLABORATION_ENABLED`   | `true` OR `false` | Controls collaboration feature tests.                                     |
+| `OPA_INTEGRATION_ENABLED` | `true` OR `false` | Used to adjust assertions if integration with OPA\Policy enabled\disabled |
+
+
+
+**Entitlements configuration for integration accounts**
+
+| PRIVILEGED_USER            | NO_ACCESS_USER            | ROOT_USER                 |
+|----------------------------|---------------------------|---------------------------|
+| users                      | users                     | users                     |
+| service.entitlements.user  | service.entitlements.user | users.data.root           |
+| service.entitlements.admin | service.storage.admin     | service.entitlements.user |
+| service.storage.admin      |                           | service.storage.viewer    |
+| service.storage.creator    |                           |                           |
+| service.storage.viewer     |                           |                           |
+| service.legal.admin        |                           |                           |
+| service.legal.editor       |                           |                           |
+| data.test1                 |                           |                           |
+| data.integration.test      |                           |                           |
+
+Execute following command to build code and run all the integration tests:
+
+ ```bash
+ # Note: this assumes that the environment variables for integration tests as outlined
+ #       above are already exported in your environment.
+ # build + install integration test core
+ $ (cd storage-acceptance-test && mvn clean test)
+ ```
+
+## License
+
+Copyright © Google LLC
+
+Copyright © EPAM Systems
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+[http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0)
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
diff --git a/storage-acceptance-test/pom.xml b/storage-acceptance-test/pom.xml
new file mode 100644
index 0000000000000000000000000000000000000000..997268ec154eae79c7c1220dbcda19b2f891057b
--- /dev/null
+++ b/storage-acceptance-test/pom.xml
@@ -0,0 +1,177 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Copyright 2017-2019, Schlumberger
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+    <modelVersion>4.0.0</modelVersion>
+    <groupId>org.opengroup.osdu.storage</groupId>
+    <artifactId>storage-acceptance-test</artifactId>
+    <version>0.28.0-SNAPSHOT</version>
+    <packaging>jar</packaging>
+
+    <properties>
+        <maven.compiler.target>17</maven.compiler.target>
+        <maven.compiler.source>17</maven.compiler.source>
+        <java.version>17</java.version>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <project.main.basedir>${project.basedir}</project.main.basedir>
+        <allure.version>2.29.0</allure.version>
+        <aspectj.version>1.9.22</aspectj.version>
+        <log4j.version>2.23.0</log4j.version>
+        <jackson.version>2.16.1</jackson.version>
+        <jackson-databind.version>2.16.1</jackson-databind.version>
+        <os-core-common.version>1.0.0</os-core-common.version>
+        <maven-surefire-plugin.version>3.2.5</maven-surefire-plugin.version>
+        <argLine>
+            --add-opens java.base/java.lang=ALL-UNNAMED
+        </argLine>
+    </properties>
+
+    <dependencyManagement>
+        <dependencies>
+            <dependency>
+                <groupId>io.qameta.allure</groupId>
+                <artifactId>allure-bom</artifactId>
+                <version>${allure.version}</version>
+                <type>pom</type>
+                <scope>import</scope>
+            </dependency>
+        </dependencies>
+    </dependencyManagement>
+
+    <dependencies>
+        <dependency>
+            <groupId>com.sun.jersey</groupId>
+            <artifactId>jersey-client</artifactId>
+            <version>1.19.4</version>
+        </dependency>
+        <dependency>
+            <groupId>io.jsonwebtoken</groupId>
+            <artifactId>jjwt</artifactId>
+            <version>0.9.1</version>
+            <exclusions>
+                <exclusion>
+                    <groupId>com.fasterxml.jackson.core</groupId>
+                    <artifactId>jackson-databind</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+            <version>1.18.30</version>
+        </dependency>
+        <dependency>
+            <groupId>io.qameta.allure</groupId>
+            <artifactId>allure-junit5</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>io.qameta.allure</groupId>
+            <artifactId>allure-generator</artifactId>
+            <version>2.30.0</version>
+        </dependency>
+        <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter-engine</artifactId>
+            <version>5.10.4</version>
+        </dependency>
+        <dependency>
+            <groupId>org.junit.platform</groupId>
+            <artifactId>junit-platform-console</artifactId>
+            <version>1.10.4</version>
+        </dependency>
+        <dependency>
+            <groupId>org.opengroup.osdu</groupId>
+            <artifactId>os-core-common</artifactId>
+            <version>${os-core-common.version}</version>
+            <scope>compile</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-lang3</artifactId>
+            <version>3.14.0</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.httpcomponents.client5</groupId>
+            <artifactId>httpclient5</artifactId>
+            <version>5.2.3</version>
+        </dependency>
+        <dependency>
+            <groupId>com.google.code.gson</groupId>
+            <artifactId>gson</artifactId>
+            <version>2.9.1</version>
+        </dependency>
+        <dependency>
+            <groupId>jakarta.ws.rs</groupId>
+            <artifactId>jakarta.ws.rs-api</artifactId>
+            <version>3.1.0</version>
+        </dependency>
+        <dependency>
+            <groupId>com.nimbusds</groupId>
+            <artifactId>oauth2-oidc-sdk</artifactId>
+            <version>9.15</version>
+        </dependency>
+    </dependencies>
+
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-surefire-plugin</artifactId>
+                <version>${maven-surefire-plugin.version}</version>
+            </plugin>
+        </plugins>
+    </build>
+
+    <repositories>
+        <repository>
+            <id>${repo.releases.id}</id>
+            <url>${repo.releases.url}</url>
+        </repository>
+    </repositories>
+
+    <distributionManagement>
+        <repository>
+            <id>${publish.releases.id}</id>
+            <url>${publish.releases.url}</url>
+        </repository>
+        <snapshotRepository>
+            <id>${publish.snapshots.id}</id>
+            <url>${publish.snapshots.url}</url>
+        </snapshotRepository>
+    </distributionManagement>
+
+    <profiles>
+        <profile>
+            <id>Default</id>
+            <activation>
+                <property>
+                    <name>!repo.releases.id</name>
+                </property>
+            </activation>
+            <properties>
+                <repo.releases.id>community-maven-repo</repo.releases.id>
+                <publish.snapshots.id>community-maven-via-job-token</publish.snapshots.id>
+                <publish.releases.id>community-maven-via-job-token</publish.releases.id>
+                <repo.releases.url>https://community.opengroup.org/api/v4/groups/17/-/packages/maven</repo.releases.url>
+                <publish.snapshots.url>https://community.opengroup.org/api/v4/projects/44/packages/maven</publish.snapshots.url>
+                <publish.releases.url>https://community.opengroup.org/api/v4/projects/44/packages/maven</publish.releases.url>
+            </properties>
+        </profile>
+    </profiles>
+</project>
diff --git a/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/api/HealthCheckApiIntegrationTest.java b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/api/HealthCheckApiIntegrationTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..bae65090176418ed1ee5d605eb8bd524a5de2838
--- /dev/null
+++ b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/api/HealthCheckApiIntegrationTest.java
@@ -0,0 +1,58 @@
+/*
+ *  Copyright 2020-2024 Google LLC
+ *  Copyright 2020-2024 EPAM Systems, Inc
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+package org.opengroup.osdu.storage.api;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
+import org.apache.http.HttpStatus;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.opengroup.osdu.storage.util.HeaderUtils;
+import org.opengroup.osdu.storage.util.TenantUtils;
+import org.opengroup.osdu.storage.util.TestBase;
+import org.opengroup.osdu.storage.util.TestUtils;
+import org.opengroup.osdu.storage.util.TokenTestUtils;
+
+public final class HealthCheckApiIntegrationTest extends TestBase {
+
+  @BeforeEach
+  @Override
+  public void setup() throws Exception {
+    this.testUtils = new TokenTestUtils();
+  }
+
+  @AfterEach
+  @Override
+  public void tearDown() throws Exception {
+    this.testUtils = null;
+  }
+
+  @Test
+  public void should_returnOk() throws Exception {
+    CloseableHttpResponse response =
+        TestUtils.send(
+            "liveness_check",
+            "GET",
+            HeaderUtils.getHeaders(TenantUtils.getTenantName(), null),
+            "",
+            "");
+    assertEquals(HttpStatus.SC_OK, response.getCode());
+  }
+}
diff --git a/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/headervalidations/ValidateRequiredHeaders.java b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/headervalidations/ValidateRequiredHeaders.java
new file mode 100644
index 0000000000000000000000000000000000000000..b024973980e04247eabd58bffd7727d368285ad8
--- /dev/null
+++ b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/headervalidations/ValidateRequiredHeaders.java
@@ -0,0 +1,116 @@
+/*
+ *  Copyright 2020-2024 Google LLC
+ *  Copyright 2020-2024 EPAM Systems, Inc
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+package org.opengroup.osdu.storage.headervalidations;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import jakarta.ws.rs.HttpMethod;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
+import org.apache.http.HttpStatus;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.opengroup.osdu.core.common.model.http.AppError;
+import org.opengroup.osdu.storage.util.HeaderUtils;
+import org.opengroup.osdu.storage.util.LegalTagUtils;
+import org.opengroup.osdu.storage.util.RecordUtil;
+import org.opengroup.osdu.storage.util.TenantUtils;
+import org.opengroup.osdu.storage.util.TestBase;
+import org.opengroup.osdu.storage.util.TestUtils;
+import org.opengroup.osdu.storage.util.TokenTestUtils;
+
+public final class ValidateRequiredHeaders extends TestBase {
+    private static final String RECORDS = "records";
+    private static final String KIND_ONE = TenantUtils.getTenantName() + ":test:endtoend:1.1."
+            + System.currentTimeMillis();
+    private static final String KIND_ID_ONE = TenantUtils.getTenantName() + ":endtoend:1.1."
+            + System.currentTimeMillis();
+    private static final String KIND_VERSION_ID = TenantUtils.getTenantName() + ":endtoend:1.2."
+            + System.currentTimeMillis();
+    private static final String LEGAL_TAG_NAME = LegalTagUtils.createRandomName();
+
+    private static final TokenTestUtils TOKEN_TEST_UTILS = new TokenTestUtils();
+
+    @BeforeAll
+    public static void classSetup() throws Exception {
+        ValidateRequiredHeaders.classSetup(TOKEN_TEST_UTILS.getToken());
+    }
+
+    @AfterAll
+    public static void classTearDown() throws Exception {
+        ValidateRequiredHeaders.classTearDown(TOKEN_TEST_UTILS.getToken());
+    }
+
+    @BeforeEach
+    @Override
+    public void setup() throws Exception {
+        this.testUtils = new TokenTestUtils();
+    }
+
+    @AfterEach
+    @Override
+    public void tearDown() throws Exception {
+        this.testUtils = null;
+    }
+
+    public static void classSetup(String token) throws Exception {
+        LegalTagUtils.create(LEGAL_TAG_NAME, token);
+    }
+
+    public static void classTearDown(String token) throws Exception {
+        TestUtils.send(
+            RECORDS + "/" + KIND_ID_ONE, HttpMethod.DELETE, HeaderUtils.getHeaders(TenantUtils.getTenantName(), token), "", "");
+        TestUtils.send(RECORDS + "/" + KIND_VERSION_ID, HttpMethod.DELETE, HeaderUtils.getHeaders(TenantUtils.getTenantName(), token), "", "");
+
+        LegalTagUtils.delete(LEGAL_TAG_NAME, token);
+    }
+
+    private CloseableHttpResponse createTestRecordWithoutAuth(String kind, String id, String legalName) throws Exception {
+        String jsonInputRecord = RecordUtil.createDefaultJsonRecord(id, kind, legalName);
+        return TestUtils.send(
+            RECORDS, HttpMethod.PUT, HeaderUtils.getHeadersWithoutAuth(TenantUtils.getTenantName(), testUtils.getToken()), jsonInputRecord, "");
+    }
+
+    private CloseableHttpResponse createTestRecordWithoutDataPartitionID(String kind, String id, String legalName) throws Exception {
+        String jsonInputRecord = RecordUtil.createDefaultJsonRecord(id, kind, legalName);
+        return TestUtils.send(
+            RECORDS, HttpMethod.PUT, HeaderUtils.getHeadersWithoutDataPartitionId(TenantUtils.getTenantName(), testUtils.getToken()), jsonInputRecord, "");
+    }
+
+    @Test
+    public void ValidateMissingAuthHeaderReturnsUnauthorizedError() throws Exception {
+        CloseableHttpResponse recordResponse = createTestRecordWithoutAuth(KIND_ONE, KIND_ID_ONE, LEGAL_TAG_NAME);
+        //validate that the error code is either 401/403 since for some its 403 I guess at some
+        //other level like istio etc.
+        assertTrue(recordResponse.getCode() == HttpStatus.SC_UNAUTHORIZED || recordResponse.getCode() == HttpStatus.SC_FORBIDDEN);
+    }
+
+    @Test
+    public void ValidateMissingDataPartitionHeaderReturnsBadRequestError() throws Exception {
+        CloseableHttpResponse recordResponse = createTestRecordWithoutDataPartitionID(KIND_ONE, KIND_ID_ONE, LEGAL_TAG_NAME);
+        AppError recordResult = TestUtils.getResult(recordResponse, HttpStatus.SC_BAD_REQUEST,
+                AppError.class);
+
+        AppError expectedError = new AppError(HttpStatus.SC_BAD_REQUEST, "Bad Request", "data-partition-id header is missing");
+
+        assertEquals(recordResult, expectedError);
+    }
+}
diff --git a/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/legal/PopulateLegalInfoFromParentRecordsTests.java b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/legal/PopulateLegalInfoFromParentRecordsTests.java
new file mode 100644
index 0000000000000000000000000000000000000000..7175788531b49bfd50293fa5a27c6a7d5a750672
--- /dev/null
+++ b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/legal/PopulateLegalInfoFromParentRecordsTests.java
@@ -0,0 +1,263 @@
+// Copyright 2017-2019, Schlumberger
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package org.opengroup.osdu.storage.legal;
+
+import static org.apache.http.HttpStatus.SC_BAD_REQUEST;
+import static org.apache.http.HttpStatus.SC_CREATED;
+import static org.apache.http.HttpStatus.SC_OK;
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.opengroup.osdu.storage.util.LegalTagUtils.createRandomName;
+
+import com.google.common.collect.Lists;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonObject;
+import io.jsonwebtoken.lang.Collections;
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.commons.lang3.ArrayUtils;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
+import org.apache.hc.core5.http.io.entity.EntityUtils;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.opengroup.osdu.storage.util.DummyRecordsHelper.CreateRecordResponse;
+import org.opengroup.osdu.storage.util.DummyRecordsHelper.RecordResultMock;
+import org.opengroup.osdu.storage.util.HeaderUtils;
+import org.opengroup.osdu.storage.util.LegalTagUtils;
+import org.opengroup.osdu.storage.util.TenantUtils;
+import org.opengroup.osdu.storage.util.TestBase;
+import org.opengroup.osdu.storage.util.TestUtils;
+import org.opengroup.osdu.storage.util.TokenTestUtils;
+
+public final class PopulateLegalInfoFromParentRecordsTests extends TestBase {
+
+	private static final String KIND = TenantUtils.getTenantName() + ":parent:inttest:1.0."
+			+ System.currentTimeMillis();
+	private static String LEGAL_TAG_PARENT_ONE;
+	private static String LEGAL_TAG_PARENT_TWO;
+	private static String LEGAL_TAG_CHILD;
+	private static String LEGAL_TAG_CHILD_THAT_WILL_NOT_BE_CREATED;
+	private static String PARENT_ID_ONE;
+	private static String PARENT_ID_TWO;
+	private static String CHILD_ID;
+	private static String CHILD_ID_THAT_IS_NOT_CREATED;
+
+	private static final TokenTestUtils TOKEN_TEST_UTILS = new TokenTestUtils();
+
+	@BeforeAll
+	public static void classSetup() throws Exception {
+		PopulateLegalInfoFromParentRecordsTests.classSetup(TOKEN_TEST_UTILS.getToken());
+	}
+
+	@AfterAll
+	public static void classTearDown() throws Exception {
+		PopulateLegalInfoFromParentRecordsTests.classTearDown(TOKEN_TEST_UTILS.getToken());
+	}
+
+	@BeforeEach
+	@Override
+	public void setup() throws Exception {
+		this.testUtils = new TokenTestUtils();
+	}
+
+	@AfterEach
+	@Override
+	public void tearDown() throws Exception {
+		this.testUtils = null;
+	}
+
+	public static void classSetup(String token) throws Exception {
+		LEGAL_TAG_PARENT_ONE = createRandomName() + "parent";
+		Thread.sleep(1);
+		LEGAL_TAG_PARENT_TWO = createRandomName() + "parent";
+		LEGAL_TAG_CHILD = createRandomName() + "child";
+		Thread.sleep(1);
+		LEGAL_TAG_CHILD_THAT_WILL_NOT_BE_CREATED = createRandomName() + "child";
+		PARENT_ID_ONE = TenantUtils.getTenantName() + ":inttest:" + System.currentTimeMillis();
+		Thread.sleep(1);
+		PARENT_ID_TWO = TenantUtils.getTenantName() + ":inttest:" + System.currentTimeMillis();
+		CHILD_ID = TenantUtils.getTenantName() + ":inttest:" + System.currentTimeMillis();
+		Thread.sleep(1);
+		CHILD_ID_THAT_IS_NOT_CREATED = TenantUtils.getTenantName() + ":inttest:" + System.currentTimeMillis();
+		LegalTagUtils.create(LEGAL_TAG_PARENT_ONE, token);
+		LegalTagUtils.create(LEGAL_TAG_PARENT_TWO, token);
+		LegalTagUtils.create(LEGAL_TAG_CHILD, token);
+
+		createAndAssertRecord(PARENT_ID_ONE, LEGAL_TAG_PARENT_ONE, "parent1", Lists.newArrayList("BR", "IT"), null, token);
+		createAndAssertRecord(PARENT_ID_TWO, LEGAL_TAG_PARENT_TWO, "parent2", Lists.newArrayList("DE", "DK"), null, token);
+	}
+
+	public static void classTearDown(String token) throws Exception {
+		purgeRecord(PARENT_ID_ONE, token);
+		purgeRecord(PARENT_ID_TWO, token);
+		purgeRecord(CHILD_ID, token);
+
+		LegalTagUtils.delete(LEGAL_TAG_PARENT_ONE, token);
+		LegalTagUtils.delete(LEGAL_TAG_PARENT_TWO, token);
+		LegalTagUtils.delete(LEGAL_TAG_CHILD, token);
+	}
+
+	@Test
+	public void should_appendOrdcAndLegalTagsWithParents_when_creatingRecordWithParentsSupplied() throws Exception {
+		RecordResultMock parentRecord1 = this.retrieveRecord(PARENT_ID_ONE);
+		RecordResultMock parentRecord2 = this.retrieveRecord(PARENT_ID_TWO);
+
+		createAndAssertRecord(CHILD_ID, LEGAL_TAG_CHILD, "chiiiiiild", Lists.newArrayList("FR", "US", "CA"),
+				Lists.newArrayList(PARENT_ID_ONE + ":" + parentRecord1.version,
+						PARENT_ID_TWO + ":" + parentRecord2.version), testUtils.getToken());
+		RecordResultMock record = this.retrieveRecord(CHILD_ID);
+
+		assertEquals(CHILD_ID, record.id);
+		assertEquals(1, record.data.size());
+		assertEquals("chiiiiiild", record.data.get("name"));
+		assertNotNull(record.version);
+		assertEquals(KIND, record.kind);
+		assertArrayEquals(new String[] { TestUtils.getAcl() }, record.acl.viewers);
+		assertArrayEquals(new String[] { TestUtils.getAcl() }, record.acl.owners);
+		assertEquals(3, record.legal.legaltags.length);
+		assertTrue(ArrayUtils.contains(record.legal.legaltags, LEGAL_TAG_CHILD));
+		assertTrue(ArrayUtils.contains(record.legal.legaltags, LEGAL_TAG_PARENT_ONE));
+		assertTrue(ArrayUtils.contains(record.legal.legaltags, LEGAL_TAG_PARENT_TWO));
+		assertTrue(ArrayUtils.contains(record.legal.otherRelevantDataCountries, "BR"));
+		assertTrue(ArrayUtils.contains(record.legal.otherRelevantDataCountries, "IT"));
+		assertTrue(ArrayUtils.contains(record.legal.otherRelevantDataCountries, "FR"));
+		assertTrue(ArrayUtils.contains(record.legal.otherRelevantDataCountries, "US"));
+		assertTrue(ArrayUtils.contains(record.legal.otherRelevantDataCountries, "CA"));
+		assertTrue(ArrayUtils.contains(record.legal.otherRelevantDataCountries, "DE"));
+		assertTrue(ArrayUtils.contains(record.legal.otherRelevantDataCountries, "DK"));
+		assertEquals(2, record.ancestry.parents.length);
+		assertTrue(ArrayUtils.contains(record.ancestry.parents, PARENT_ID_ONE + ":" + parentRecord1.version));
+		assertTrue(ArrayUtils.contains(record.ancestry.parents, PARENT_ID_TWO + ":" + parentRecord2.version));
+	}
+
+	@Test
+	public void should_returnErrorCode400_when_anInvalidChildLegalTagProvided() throws Exception {
+		String childBody = createBody(CHILD_ID_THAT_IS_NOT_CREATED, "childname",
+				Lists.newArrayList(LEGAL_TAG_CHILD_THAT_WILL_NOT_BE_CREATED), Lists.newArrayList("FR", "US", "CA"),
+				null);
+
+		CloseableHttpResponse response = TestUtils.send("records", "PUT",
+				HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), childBody, "");
+		assertEquals(SC_BAD_REQUEST, response.getCode());
+	}
+
+	@Test
+	public void should_return400_when_noParentRecordAndNoChildLegalTagsProvided() throws Exception {
+		String body = createBody(CHILD_ID_THAT_IS_NOT_CREATED, "childname", null, Lists.newArrayList("FR", "US"), null);
+		CloseableHttpResponse response = TestUtils.send("records", "PUT",
+				HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), body, "");
+		assertEquals(SC_BAD_REQUEST, response.getCode());
+	}
+
+	@Test
+	public void should_returnErrorCode400_when_noParentRecordAndNoORDCValuesProvided() throws Exception {
+		String body = createBody(CHILD_ID_THAT_IS_NOT_CREATED, "childname", Lists.newArrayList(LEGAL_TAG_PARENT_ONE),
+				null, null);
+		CloseableHttpResponse response = TestUtils.send("records", "PUT",
+				HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), body, "");
+		assertEquals(SC_BAD_REQUEST, response.getCode());
+	}
+
+	protected RecordResultMock retrieveRecord(String recordId) throws Exception {
+		System.out.println("Retrieving record=" + recordId);
+		CloseableHttpResponse response = TestUtils.send("records/" + recordId, "GET",
+				HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "", "");
+		String responseBody = EntityUtils.toString(response.getEntity());
+		System.out.println(" responseBody=" + responseBody);
+		assertEquals(SC_OK, response.getCode());
+
+		return GSON.fromJson(responseBody, RecordResultMock.class);
+	}
+
+	protected static String createBody(String id, String dataValue, List<String> legalTags, List<String> ordc,
+			List<String> parents) {
+		JsonObject data = new JsonObject();
+		data.addProperty("name", dataValue);
+
+		JsonObject acl = new JsonObject();
+		JsonArray acls = new JsonArray();
+		acls.add(TestUtils.getAcl());
+		acl.add("viewers", acls);
+		acl.add("owners", acls);
+
+		JsonArray tags = new JsonArray();
+		if (legalTags != null) {
+			legalTags.forEach(t -> tags.add(t));
+		}
+
+		JsonArray ordcJson = new JsonArray();
+		if (ordc != null) {
+			ordc.forEach(o -> ordcJson.add(o));
+		}
+
+		JsonObject legal = new JsonObject();
+		if (legalTags != null) {
+			legal.add("legaltags", tags);
+		}
+		legal.add("otherRelevantDataCountries", ordcJson);
+
+		JsonObject record = new JsonObject();
+		record.addProperty("id", id);
+		record.addProperty("kind", KIND);
+		record.add("acl", acl);
+		record.add("legal", legal);
+		record.add("data", data);
+
+		if (!Collections.isEmpty(parents)) {
+			JsonArray parentsJson = new JsonArray();
+			parents.forEach(p -> parentsJson.add(p));
+
+			JsonObject ancestry = new JsonObject();
+			ancestry.add("parents", parentsJson);
+
+			record.add("ancestry", ancestry);
+		}
+
+		JsonArray records = new JsonArray();
+		records.add(record);
+
+		return records.toString();
+	}
+
+	protected static void createAndAssertRecord(String parentId, String legalTagForParent, String dataValue,
+			ArrayList<String> ordc, List<String> parents, String token) throws Exception {
+		String parentBody = createBody(parentId, dataValue, Lists.newArrayList(legalTagForParent), ordc, parents);
+		System.out.println("createAndAssertRecord");
+		System.out.println("parentBody=" + parentId + " " + parentBody);
+		CloseableHttpResponse response = TestUtils.send("records", "PUT",
+				HeaderUtils.getHeaders(TenantUtils.getTenantName(), token), parentBody, "");
+
+		String responseBody = EntityUtils.toString(response.getEntity());
+		System.out.println("responseBody=" + parentId + " " + responseBody);
+		assertEquals(SC_CREATED, response.getCode());
+		assertTrue(response.getEntity().getContentType().contains("application/json"));
+
+		CreateRecordResponse result = GSON.fromJson(responseBody, CreateRecordResponse.class);
+
+		assertEquals(1, result.recordCount);
+		assertEquals(1, result.recordIds.length);
+		assertEquals(1, result.recordIdVersions.length);
+		assertEquals(parentId, result.recordIds[0]);
+	}
+
+	protected static void purgeRecord(String recordId, String token) throws Exception {
+		TestUtils.send("records/" + recordId, "DELETE", HeaderUtils.getHeaders(TenantUtils.getTenantName(), token), "", "");
+	}
+}
diff --git a/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/misc/StorageCorsTests.java b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/misc/StorageCorsTests.java
new file mode 100644
index 0000000000000000000000000000000000000000..788c46fe03831adf0e620b2a3a6f63a9671347cc
--- /dev/null
+++ b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/misc/StorageCorsTests.java
@@ -0,0 +1,70 @@
+// Copyright 2017-2019, Schlumberger
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package org.opengroup.osdu.storage.misc;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
+import org.apache.http.HttpStatus;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+import org.opengroup.osdu.storage.util.HeaderUtils;
+import org.opengroup.osdu.storage.util.TenantUtils;
+import org.opengroup.osdu.storage.util.TestBase;
+import org.opengroup.osdu.storage.util.TestUtils;
+import org.opengroup.osdu.storage.util.TokenTestUtils;
+
+public final class StorageCorsTests extends TestBase {
+
+    @BeforeEach
+    @Override
+    public void setup() throws Exception {
+        this.testUtils = new TokenTestUtils();
+    }
+
+    @AfterEach
+    @Override
+    public void tearDown() throws Exception {
+        this.testUtils = null;
+    }
+
+    @Test
+    @Disabled
+    public void should_returnProperStatusCodeAndResponseHeaders_when_sendingPreflightOptionsRequest() throws Exception {
+        CloseableHttpResponse response = TestUtils.send("query/kinds", "OPTIONS", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "", "?limit=1");
+        assertEquals(HttpStatus.SC_OK, response.getCode());
+
+        assertEquals("*", response.getHeaders("Access-Control-Allow-Origin")[0]);
+        assertEquals(
+                "origin, content-type, accept, authorization, data-partition-id, correlation-id, appkey",
+                response.getHeaders("Access-Control-Allow-Headers")[0]);
+        assertEquals("GET, POST, PUT, DELETE, OPTIONS, HEAD, PATCH",
+                response.getHeaders("Access-Control-Allow-Methods")[0]);
+        assertEquals("true", response.getHeaders("Access-Control-Allow-Credentials")[0]);
+        assertEquals("DENY", response.getHeaders("X-Frame-Options")[0]);
+        assertEquals("1; mode=block", response.getHeaders("X-XSS-Protection")[0]);
+        assertEquals("nosniff", response.getHeaders("X-Content-Type-Options")[0]);
+        assertEquals("no-cache, no-store, must-revalidate", response.getHeaders("Cache-Control")[0]);
+        assertEquals("default-src 'self'", response.getHeaders("Content-Security-Policy")[0]);
+        assertTrue(response.getHeaders("Strict-Transport-Security")[0].getValue().contains("max-age=31536000"));
+        assertTrue(response.getHeaders("Strict-Transport-Security")[0].getValue().contains("includeSubDomains"));
+        assertEquals("0", response.getHeaders("Expires")[0]);
+        assertNotNull(response.getHeaders("correlation-id")[0]);
+    }
+}
diff --git a/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/misc/StressTests.java b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/misc/StressTests.java
new file mode 100644
index 0000000000000000000000000000000000000000..bd40cde37b0d04f759ff2cacf6751ff859e844fc
--- /dev/null
+++ b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/misc/StressTests.java
@@ -0,0 +1,154 @@
+// Copyright 2017-2019, Schlumberger
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package org.opengroup.osdu.storage.misc;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.google.gson.Gson;
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
+import org.apache.hc.core5.http.io.entity.EntityUtils;
+import org.apache.http.HttpStatus;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.opengroup.osdu.storage.records.RecordsApiAcceptanceTests;
+import org.opengroup.osdu.storage.util.DummyRecordsHelper;
+import org.opengroup.osdu.storage.util.HeaderUtils;
+import org.opengroup.osdu.storage.util.LegalTagUtils;
+import org.opengroup.osdu.storage.util.TenantUtils;
+import org.opengroup.osdu.storage.util.TestBase;
+import org.opengroup.osdu.storage.util.TestUtils;
+import org.opengroup.osdu.storage.util.TokenTestUtils;
+
+public final class StressTests extends TestBase {
+
+	private static final String RECORD_ID = TenantUtils.getTenantName()
+			+ ":WG-Multi-Client:flatten-full-seismic-2d-shape_survey_2d_0623_Survey2D_Angola_Lower_Congo_2D_Repro_AWG98_26_1";
+
+	private static String LEGAL_TAG_NAME = LegalTagUtils.createRandomName();
+
+	private static final String KIND = TenantUtils.getTenantName() + ":ds:inttest:1.0."
+			+ System.currentTimeMillis();
+
+	private static final TokenTestUtils TOKEN_TEST_UTILS = new TokenTestUtils();
+
+	@BeforeAll
+	public static void classSetup() throws Exception {
+		StressTests.classSetup(TOKEN_TEST_UTILS.getToken());
+	}
+
+	@AfterAll
+	public static void classTearDown() throws Exception {
+		StressTests.classTearDown(TOKEN_TEST_UTILS.getToken());
+	}
+
+	@BeforeEach
+	@Override
+	public void setup() throws Exception {
+		this.testUtils = new TokenTestUtils();
+	}
+
+	@AfterEach
+	@Override
+	public void tearDown() throws Exception {
+		this.testUtils = null;
+	}
+
+	public static void classSetup(String token) throws Exception {
+		LegalTagUtils.create(LEGAL_TAG_NAME, token);
+	}
+
+	public static void classTearDown(String token) throws Exception {
+		TestUtils.send("records/", "DELETE", HeaderUtils.getHeaders(TenantUtils.getTenantName(), token), "", RECORD_ID);
+		LegalTagUtils.delete(LEGAL_TAG_NAME, token);
+	}
+
+	@Test
+	public void should_create100Records_when_givenValidRecord() throws Exception {
+		this.performanceTestCreateAndUpdateRecord(100);
+	}
+
+	@Test
+	public void should_create10Records_when_givenValidRecord() throws Exception {
+		this.performanceTestCreateAndUpdateRecord(10);
+	}
+
+	@Test
+	public void should_create1Records_when_givenValidRecord() throws Exception {
+		this.performanceTestCreateAndUpdateRecord(1);
+	}
+
+	protected void performanceTestCreateAndUpdateRecord(int capacity) throws Exception {
+		String json = "";
+		List<String> ids = new ArrayList<>(capacity);
+		for (int i = 0; i < capacity; i++) {
+			String id1 = TenantUtils.getTenantName() + ":inttest:" + System.currentTimeMillis() + i;
+			json += RecordsApiAcceptanceTests.singleEntityBody(id1, "ash ketchum", KIND, LEGAL_TAG_NAME);
+			if (i != capacity - 1) {
+				json += ",";
+			}
+			ids.add(id1);
+		}
+
+		json = "[" + json + "]";
+
+		long startMillis = System.currentTimeMillis();
+		CloseableHttpResponse response = TestUtils.send("records", "PUT", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), json, "");
+		long totalMillis = System.currentTimeMillis() - startMillis;
+		System.out.println(String.format("Took %s milliseconds to Create %s 1KB records", totalMillis, ids.size()));
+
+		String responseJson = EntityUtils.toString(response.getEntity());
+		System.out.println(responseJson);
+		assertEquals(HttpStatus.SC_CREATED, response.getCode());
+		assertTrue(response.getEntity().getContentType().toString().contains("application/json"));
+		Gson gson = new Gson();
+		DummyRecordsHelper.CreateRecordResponse result = gson.fromJson(responseJson,
+				DummyRecordsHelper.CreateRecordResponse.class);
+		assertEquals(capacity, result.recordCount);
+		assertEquals(capacity, result.recordIds.length);
+		assertEquals(capacity, result.recordIdVersions.length);
+
+		startMillis = System.currentTimeMillis();
+		response = TestUtils.send("records", "PUT", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), json, "?skipdupes=false");
+		totalMillis = System.currentTimeMillis() - startMillis;
+		assertEquals(HttpStatus.SC_CREATED, response.getCode());
+		System.out.println(String.format("Took %s milliseconds to Update %s 1KB records", totalMillis, ids.size()));
+
+		startMillis = System.currentTimeMillis();
+		response = TestUtils.send("records", "PUT", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), json, "?skipdupes=false");
+		totalMillis = System.currentTimeMillis() - startMillis;
+		assertEquals(HttpStatus.SC_CREATED, response.getCode());
+		System.out.println(String.format("Took %s milliseconds to Update %s 1KB records when when skipdupes is true",
+				totalMillis, ids.size()));
+
+		startMillis = System.currentTimeMillis();
+		response = TestUtils.send("records/" + ids.get(0), "GET", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "", "");
+		totalMillis = System.currentTimeMillis() - startMillis;
+		assertEquals(HttpStatus.SC_OK, response.getCode());
+		System.out.println(String.format("Took %s milliseconds to GET 1 1KB record", totalMillis));
+
+		ids.parallelStream().forEach((id) -> {
+			try {
+				TestUtils.send("records/" + id, "DELETE", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "", "");
+			} catch (Exception e) {
+			}
+		});
+	}
+}
diff --git a/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/misc/SwaggerIntegrationTests.java b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/misc/SwaggerIntegrationTests.java
new file mode 100644
index 0000000000000000000000000000000000000000..6deb49caa9e3ec208a771651617c7a59b1395427
--- /dev/null
+++ b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/misc/SwaggerIntegrationTests.java
@@ -0,0 +1,58 @@
+/*
+ *  Copyright 2020-2024 Google LLC
+ *  Copyright 2020-2024 EPAM Systems, Inc
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+package org.opengroup.osdu.storage.misc;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.util.HashMap;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
+import org.apache.http.HttpStatus;
+import org.junit.jupiter.api.Test;
+import org.opengroup.osdu.storage.util.TestBase;
+import org.opengroup.osdu.storage.util.TestUtils;
+
+public final class SwaggerIntegrationTests extends TestBase {
+
+    public static final String SWAGGER_API_PATH = "swagger-ui/index.html";
+    public static final String SWAGGER_API_DOCS_PATH = "api-docs";
+
+    @Override
+    public void setup() throws Exception {
+        // the test suite does not require pre-run configuration
+    }
+
+    @Override
+    public void tearDown() throws Exception {
+        // the test suite does not require post-run tear-down procedures
+    }
+
+    @Test
+    public void shouldReturn200_whenSwaggerApiIsCalled() throws Exception {
+        CloseableHttpResponse response = TestUtils
+                .send(SWAGGER_API_PATH, "GET", new HashMap<>(), "", "");
+        assertEquals(HttpStatus.SC_OK, response.getCode());
+    }
+
+    @Test
+    public void shouldReturn200_whenSwaggerApiDocsIsCalled() throws Exception {
+        CloseableHttpResponse response = TestUtils
+                .send(SWAGGER_API_DOCS_PATH, "GET", new HashMap<>(), "", "");
+        assertEquals(HttpStatus.SC_OK, response.getCode());
+    }
+
+}
diff --git a/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/model/CreatedRecordInStorage.java b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/model/CreatedRecordInStorage.java
new file mode 100644
index 0000000000000000000000000000000000000000..64539824356e5b830a545f27fd20cdf727ef02d8
--- /dev/null
+++ b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/model/CreatedRecordInStorage.java
@@ -0,0 +1,58 @@
+// Copyright 2017-2019, Schlumberger
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package org.opengroup.osdu.storage.model;
+
+import java.util.Arrays;
+import java.util.Map;
+import lombok.Data;
+
+@Data
+public class CreatedRecordInStorage {
+    public int recordCount;
+    public String[] recordIds;
+    public String[] skippedRecordIds;
+
+    @Override
+    public String toString() {
+        return "CreatedRecordInStorage{" +
+                "recordCount=" + recordCount +
+                ", recordIds=" + Arrays.toString(recordIds) +
+                ", skippedRecordIds=" + Arrays.toString(skippedRecordIds) +
+                '}';
+    }
+    public class RecordResult {
+        public String id;
+        public String version;
+        public String kind;
+        public RecordAcl acl;
+        public Map<String, Object> data;
+        public RecordLegal legal;
+        public RecordAncestry ancestry;
+    }
+
+    public class RecordAcl {
+        public String[] viewers;
+        public String[] owners;
+    }
+
+    public class RecordLegal {
+        public String[] legaltags;
+        public String[] otherRelevantDataCountries;
+    }
+
+    public class RecordAncestry {
+        public String[] parents;
+    }
+}
diff --git a/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/model/FetchRecordResponse.java b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/model/FetchRecordResponse.java
new file mode 100644
index 0000000000000000000000000000000000000000..7dea558f1f636c2fef0e3616df2dea0609e7d52e
--- /dev/null
+++ b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/model/FetchRecordResponse.java
@@ -0,0 +1,24 @@
+// Copyright 2017-2019, Schlumberger
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package org.opengroup.osdu.storage.model;
+
+import lombok.Data;
+
+@Data
+public class FetchRecordResponse {
+    public String[] records;
+    public String[] invalidRecords;
+    public String[] retryRecords;
+}
diff --git a/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/model/ForbiddenNotFoundResponse.java b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/model/ForbiddenNotFoundResponse.java
new file mode 100644
index 0000000000000000000000000000000000000000..457e19b2f5e43f1bacd574c0b6d2eef96dfa3995
--- /dev/null
+++ b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/model/ForbiddenNotFoundResponse.java
@@ -0,0 +1,24 @@
+// Copyright 2017-2019, Schlumberger
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package org.opengroup.osdu.storage.model;
+
+import lombok.Data;
+
+@Data
+public class ForbiddenNotFoundResponse {
+    public int code;
+    public String reason;
+    public String message;
+}
diff --git a/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/model/GetCursorValue.java b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/model/GetCursorValue.java
new file mode 100644
index 0000000000000000000000000000000000000000..c9ec5040f346f14a1cfef00d7b4da504ab5a6535
--- /dev/null
+++ b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/model/GetCursorValue.java
@@ -0,0 +1,23 @@
+// Copyright 2017-2019, Schlumberger
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package org.opengroup.osdu.storage.model;
+
+import lombok.Data;
+
+@Data
+public class GetCursorValue {
+    public String cursor;
+    public String[] results;
+}
diff --git a/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/model/ReplayFilter.java b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/model/ReplayFilter.java
new file mode 100644
index 0000000000000000000000000000000000000000..a202289fe98e8ac5aa5f11f7a1f6e7e7fd037ce2
--- /dev/null
+++ b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/model/ReplayFilter.java
@@ -0,0 +1,31 @@
+// Copyright © Microsoft Corporation
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package org.opengroup.osdu.storage.model;
+
+
+import java.util.List;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class ReplayFilter {
+
+    private List<String> kinds;
+}
diff --git a/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/model/ReplayStatus.java b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/model/ReplayStatus.java
new file mode 100644
index 0000000000000000000000000000000000000000..3331d8d6041e38a0c10270d28c3f9c5ac5b833a0
--- /dev/null
+++ b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/model/ReplayStatus.java
@@ -0,0 +1,40 @@
+// Copyright © Microsoft Corporation
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package org.opengroup.osdu.storage.model;
+
+import java.util.Date;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class ReplayStatus {
+
+    private String kind;
+
+    private Long totalRecords;
+
+    private Long processedRecords;
+
+    private String state;
+
+    private Date startedAt;
+
+    private String elapsedTime;
+}
diff --git a/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/model/ReplayStatusResponseHelper.java b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/model/ReplayStatusResponseHelper.java
new file mode 100644
index 0000000000000000000000000000000000000000..615c847776ca1bb2f0c23e301bfa23fb823f1397
--- /dev/null
+++ b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/model/ReplayStatusResponseHelper.java
@@ -0,0 +1,47 @@
+// Copyright © Microsoft Corporation
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package org.opengroup.osdu.storage.model;
+
+import java.util.Date;
+import java.util.List;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class ReplayStatusResponseHelper {
+
+    private String replayId;
+
+    private String operation;
+
+    private Long totalRecords;
+
+    private Date startedAt;
+
+    private String elapsedTime;
+
+    private Long processedRecords;
+
+    private String overallState;
+
+    private ReplayFilter filter;
+
+    private List<ReplayStatus> status;
+}
diff --git a/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/pubsubendpoint/PubsubEndpointTest.java b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/pubsubendpoint/PubsubEndpointTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..68e22a87e39df0895e3e771f0ea1f928c45fdff7
--- /dev/null
+++ b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/pubsubendpoint/PubsubEndpointTest.java
@@ -0,0 +1,162 @@
+// Copyright 2017-2019, Schlumberger
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package org.opengroup.osdu.storage.pubsubendpoint;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
+import org.apache.hc.core5.http.ParseException;
+import org.apache.hc.core5.http.io.entity.EntityUtils;
+import org.apache.http.HttpStatus;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.opengroup.osdu.storage.util.HeaderUtils;
+import org.opengroup.osdu.storage.util.LegalTagUtils;
+import org.opengroup.osdu.storage.util.RecordUtil;
+import org.opengroup.osdu.storage.util.TenantUtils;
+import org.opengroup.osdu.storage.util.TestBase;
+import org.opengroup.osdu.storage.util.TestUtils;
+import org.opengroup.osdu.storage.util.TokenTestUtils;
+
+public final class PubsubEndpointTest extends TestBase {
+	private static final long NOW = System.currentTimeMillis();
+	private static final long FIVE_SECOND_LATER = NOW + 5000L;
+	private static final String LEGAL_TAG_1 = LegalTagUtils.createRandomName();
+	private static final String LEGAL_TAG_2 = LEGAL_TAG_1 + "random2";
+
+	private static final String KIND = TenantUtils.getTenantName() + ":test:endtoend:1.1." + NOW;
+	private static final String RECORD_ID = TenantUtils.getTenantName() + ":endtoend:1.1." + NOW;
+	private static final String RECORD_ID_2 = TenantUtils.getTenantName() + ":endtoend:1.1."
+			+ FIVE_SECOND_LATER;
+
+	private static final TokenTestUtils TOKEN_TEST_UTILS = new TokenTestUtils();
+
+	@BeforeAll
+	public static void classSetup() throws Exception {
+		PubsubEndpointTest.classSetup(TOKEN_TEST_UTILS.getToken());
+	}
+
+	@AfterAll
+	public static void classTearDown() throws Exception {
+		PubsubEndpointTest.classTearDown(TOKEN_TEST_UTILS.getToken());
+	}
+
+	@BeforeEach
+	@Override
+	public void setup() throws Exception {
+		this.testUtils = new TokenTestUtils();
+	}
+
+	@AfterEach
+	@Override
+	public void tearDown() throws Exception {
+		this.testUtils = null;
+	}
+
+	public static void classSetup(String token) throws Exception {
+		LegalTagUtils.create(LEGAL_TAG_1, token);
+		String record1 = RecordUtil.createDefaultJsonRecord(RECORD_ID, KIND, LEGAL_TAG_1);
+		CloseableHttpResponse responseValid = TestUtils.send("records", "PUT", HeaderUtils.getHeaders(TenantUtils.getTenantName(), token), record1, "");
+		assertEquals(HttpStatus.SC_CREATED, responseValid.getCode());
+
+		LegalTagUtils.create(LEGAL_TAG_2, token);
+		String record2 = RecordUtil.createDefaultJsonRecord(RECORD_ID_2, KIND, LEGAL_TAG_2);
+		CloseableHttpResponse responseValid2 = TestUtils.send("records", "PUT", HeaderUtils.getHeaders(TenantUtils.getTenantName(), token), record2, "");
+		assertEquals(HttpStatus.SC_CREATED, responseValid2.getCode());
+	}
+
+	public static void classTearDown(String token) throws Exception {
+		TestUtils.send("records/" + RECORD_ID, "DELETE", HeaderUtils.getHeaders(TenantUtils.getTenantName(), token), "", "");
+		TestUtils.send("records/" + RECORD_ID_2, "DELETE", HeaderUtils.getHeaders(TenantUtils.getTenantName(), token), "", "");
+
+		LegalTagUtils.delete(LEGAL_TAG_1, token);
+		LegalTagUtils.delete(LEGAL_TAG_2, token);
+	}
+
+	@Test
+	public void should_deleteIncompliantLegaltagAndInvalidateRecordsAndNotIngestAgain_whenIncompliantMessageSentToEndpoint()
+			throws Exception {
+		LegalTagUtils.delete(LEGAL_TAG_1, testUtils.getToken());
+		// wait until cache of opa will be rebuild
+		Thread.sleep(100000);
+
+		List<String> legalTagNames = new ArrayList<>();
+		legalTagNames.add(LEGAL_TAG_1);
+		legalTagNames.add(LEGAL_TAG_2);
+
+		CloseableHttpResponse responseRecordQuery =
+				TestUtils.send(
+						"records/" + RECORD_ID,
+						"GET",
+						HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()),
+						"",
+						"");
+		assertEquals(HttpStatus.SC_NOT_FOUND, responseRecordQuery.getCode());
+
+		long now = System.currentTimeMillis();
+		long later = now + 2000L;
+		String recordIdTemp1 = TenantUtils.getTenantName() + ":endtoend:1.1." + now;
+		String kindTemp = TenantUtils.getTenantName() + ":test:endtoend:1.1." + now;
+		String recordTemp1 = RecordUtil.createDefaultJsonRecord(recordIdTemp1, kindTemp, LEGAL_TAG_1);
+		String recordIdTemp2 = TenantUtils.getTenantName() + ":endtoend:1.1." + later;
+		String recordTemp2 = RecordUtil.createDefaultJsonRecord(recordIdTemp2, kindTemp, LEGAL_TAG_2);
+
+		CloseableHttpResponse responseInvalid =
+				TestUtils.send(
+						"records",
+						"PUT",
+						HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()),
+						recordTemp1,
+						"");
+		assertEquals(HttpStatus.SC_BAD_REQUEST, responseInvalid.getCode());
+		assertEquals(
+				"Invalid legal tags", this.getResponseReasonFromRecordIngestResponse(responseInvalid));
+		CloseableHttpResponse responseValid3 =
+				TestUtils.send(
+						"records",
+						"PUT",
+						HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()),
+						recordTemp2,
+						"");
+		assertEquals(HttpStatus.SC_CREATED, responseValid3.getCode());
+		TestUtils.send(
+				"records/" + recordIdTemp2,
+				"DELETE",
+				HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()),
+				"",
+				"");
+	}
+
+	protected String getResponseReasonFromRecordIngestResponse(CloseableHttpResponse response) {
+		JsonObject json = null;
+		try {
+			json = new JsonParser().parse(EntityUtils.toString(response.getEntity())).getAsJsonObject();
+		} catch (IOException e) {
+			throw new RuntimeException(e);
+		} catch (ParseException e) {
+			throw new RuntimeException(e);
+		}
+		return json.get("reason").getAsString();
+	}
+
+}
diff --git a/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/query/GetQueryInfoIntegrationTest.java b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/query/GetQueryInfoIntegrationTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..d0a2da46d8b099d96cd0ec6b6da4e044adb00d6a
--- /dev/null
+++ b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/query/GetQueryInfoIntegrationTest.java
@@ -0,0 +1,82 @@
+/*
+ *  Copyright 2020-2024 Google LLC
+ *  Copyright 2020-2024 EPAM Systems, Inc
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+package org.opengroup.osdu.storage.query;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
+import org.apache.http.HttpStatus;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.opengroup.osdu.storage.util.HeaderUtils;
+import org.opengroup.osdu.storage.util.TenantUtils;
+import org.opengroup.osdu.storage.util.TestBase;
+import org.opengroup.osdu.storage.util.TestUtils;
+import org.opengroup.osdu.storage.util.TokenTestUtils;
+import org.opengroup.osdu.storage.util.VersionInfoUtils;
+
+
+public final class GetQueryInfoIntegrationTest extends TestBase {
+
+  private static final VersionInfoUtils VERSION_INFO_UTILS = new VersionInfoUtils();
+  private static final TokenTestUtils TOKEN_TEST_UTILS = new TokenTestUtils();
+
+  @BeforeAll
+  public static void classSetup() throws Exception {
+    GetQueryRecordsIntegrationTest.classSetup(TOKEN_TEST_UTILS.getToken());
+  }
+
+  @AfterAll
+  public static void classTearDown() throws Exception {
+    GetQueryRecordsIntegrationTest.classTearDown(TOKEN_TEST_UTILS.getToken());
+  }
+
+  @BeforeEach
+  @Override
+  public void setup() throws Exception {
+    this.testUtils = new TokenTestUtils();
+  }
+
+  @AfterEach
+  @Override
+  public void tearDown() throws Exception {
+    this.testUtils = null;
+  }
+
+  @Test
+  public void should_returnInfo() throws Exception {
+    CloseableHttpResponse response = TestUtils
+        .send("info", "GET", HeaderUtils.getHeaders(TenantUtils.getTenantName(),
+            testUtils.getToken()), "", "");
+    assertEquals(HttpStatus.SC_OK, response.getCode());
+
+    VersionInfoUtils.VersionInfo responseObject = VERSION_INFO_UTILS.getVersionInfoFromResponse(response);
+
+    assertNotNull(responseObject.groupId);
+    assertNotNull(responseObject.artifactId);
+    assertNotNull(responseObject.version);
+    assertNotNull(responseObject.buildTime);
+    assertNotNull(responseObject.branch);
+    assertNotNull(responseObject.commitId);
+    assertNotNull(responseObject.commitMessage);
+  }
+}
diff --git a/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/query/GetQueryKindsIntegrationTests.java b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/query/GetQueryKindsIntegrationTests.java
new file mode 100644
index 0000000000000000000000000000000000000000..a2cd320c73f5babb3ae47e0ecd053c489461641c
--- /dev/null
+++ b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/query/GetQueryKindsIntegrationTests.java
@@ -0,0 +1,86 @@
+// Copyright 2017-2019, Schlumberger
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package org.opengroup.osdu.storage.query;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
+import org.apache.hc.core5.http.io.entity.EntityUtils;
+import org.apache.http.HttpStatus;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.opengroup.osdu.storage.util.DummyRecordsHelper;
+import org.opengroup.osdu.storage.util.HeaderUtils;
+import org.opengroup.osdu.storage.util.TenantUtils;
+import org.opengroup.osdu.storage.util.TestBase;
+import org.opengroup.osdu.storage.util.TestUtils;
+import org.opengroup.osdu.storage.util.TokenTestUtils;
+
+
+public final class GetQueryKindsIntegrationTests extends TestBase {
+
+	private static final DummyRecordsHelper RECORD_HELPER = new DummyRecordsHelper();
+
+	@BeforeEach
+	@Override
+	public void setup() throws Exception {
+		this.testUtils = new TokenTestUtils();
+	}
+
+	@AfterEach
+	@Override
+	public void tearDown() throws Exception {
+		this.testUtils = null;
+	}
+
+	@Test
+	public void should_returnMax1000Results_when_settingLimitToAValueLessThan1() throws Exception {
+		if (configUtils != null && configUtils.getIsSchemaEndpointsEnabled()) {
+			CloseableHttpResponse response = TestUtils.send("query/kinds", "GET", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "", "?limit=0");
+			assertEquals(HttpStatus.SC_OK, response.getCode());
+
+			DummyRecordsHelper.QueryResultMock responseObject = RECORD_HELPER.getQueryResultMockFromResponse(response);
+
+			assertTrue(responseObject.results.length > 1 && responseObject.results.length <= 1000);
+		}
+	}
+
+	@Test
+	public void should_return400ErrorResult_when_givingAnInvalidCursorParameter() throws Exception {
+		if (configUtils != null && configUtils.getIsSchemaEndpointsEnabled()) {
+			CloseableHttpResponse response = TestUtils.send("query/kinds", "GET", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "",
+					"?cursor=badCursorString");
+			assertEquals(HttpStatus.SC_BAD_REQUEST, response.getCode());
+
+      assertEquals(
+          "{\"code\":400,\"reason\":\"Cursor invalid\",\"message\":\"The requested cursor does not exist or is invalid\"}",
+          EntityUtils.toString(response.getEntity()));
+		}
+	}
+
+	@Test
+	public void should_return2Results_when_requesting2Items() throws Exception {
+			if (configUtils != null && configUtils.getIsSchemaEndpointsEnabled()) {
+			CloseableHttpResponse response = TestUtils.send("query/kinds", "GET", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "", "?limit=2");
+			assertEquals(HttpStatus.SC_OK, response.getCode());
+
+			DummyRecordsHelper.QueryResultMock responseObject = RECORD_HELPER.getQueryResultMockFromResponse(response);
+
+			assertEquals(2, responseObject.results.length);
+		}
+	}
+}
diff --git a/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/query/GetQueryRecordsIntegrationTest.java b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/query/GetQueryRecordsIntegrationTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..76e54a2dce0cbde1115386e93c9b010833685899
--- /dev/null
+++ b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/query/GetQueryRecordsIntegrationTest.java
@@ -0,0 +1,202 @@
+// Copyright 2017-2019, Schlumberger
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package org.opengroup.osdu.storage.query;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import java.io.IOException;
+import java.util.HashSet;
+import java.util.Set;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
+import org.apache.hc.core5.http.ParseException;
+import org.apache.hc.core5.http.io.entity.EntityUtils;
+import org.apache.http.HttpStatus;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.opengroup.osdu.storage.util.DummyRecordsHelper;
+import org.opengroup.osdu.storage.util.HeaderUtils;
+import org.opengroup.osdu.storage.util.LegalTagUtils;
+import org.opengroup.osdu.storage.util.RecordUtil;
+import org.opengroup.osdu.storage.util.TenantUtils;
+import org.opengroup.osdu.storage.util.TestBase;
+import org.opengroup.osdu.storage.util.TestUtils;
+import org.opengroup.osdu.storage.util.TokenTestUtils;
+
+public final class GetQueryRecordsIntegrationTest extends TestBase {
+
+	private static final long NOW = System.currentTimeMillis();
+
+	private static final String RECORD_ID = TenantUtils.getTenantName() + ":query:" + NOW;
+	private static final String KIND = TenantUtils.getTenantName() + ":ds:query:1.0." + NOW;
+	private static final String LEGAL_TAG = LegalTagUtils.createRandomName();
+	private static final DummyRecordsHelper RECORDS_HELPER = new DummyRecordsHelper();
+	private static final TokenTestUtils TOKEN_TEST_UTILS = new TokenTestUtils();
+
+	@BeforeAll
+	public static void classSetup() throws Exception {
+		GetQueryRecordsIntegrationTest.classSetup(TOKEN_TEST_UTILS.getToken());
+	}
+
+	@AfterAll
+	public static void classTearDown() throws Exception {
+		GetQueryRecordsIntegrationTest.classTearDown(TOKEN_TEST_UTILS.getToken());
+	}
+
+	@BeforeEach
+	@Override
+	public void setup() throws Exception {
+		this.testUtils = new TokenTestUtils();
+	}
+
+	@AfterEach
+	@Override
+	public void tearDown() throws Exception {
+		this.testUtils = null;
+	}
+
+	public static void classSetup(String token) throws Exception {
+		LegalTagUtils.create(LEGAL_TAG, token);
+		String jsonInput = RecordUtil.createDefaultJsonRecords(5, RECORD_ID, KIND, LEGAL_TAG);
+
+		CloseableHttpResponse response = TestUtils.send("records", "PUT", HeaderUtils.getHeaders(TenantUtils.getTenantName(), token), jsonInput, "");
+		assertEquals(HttpStatus.SC_CREATED, response.getCode());
+	}
+
+	public static void classTearDown(String token) throws Exception {
+		TestUtils.send("records/" + RECORD_ID + 0, "DELETE", HeaderUtils.getHeaders(TenantUtils.getTenantName(), token), "", "");
+		TestUtils.send("records/" + RECORD_ID + 1, "DELETE", HeaderUtils.getHeaders(TenantUtils.getTenantName(), token), "", "");
+		TestUtils.send("records/" + RECORD_ID + 2, "DELETE", HeaderUtils.getHeaders(TenantUtils.getTenantName(), token), "", "");
+		TestUtils.send("records/" + RECORD_ID + 3, "DELETE", HeaderUtils.getHeaders(TenantUtils.getTenantName(), token), "", "");
+		TestUtils.send("records/" + RECORD_ID + 4, "DELETE", HeaderUtils.getHeaders(TenantUtils.getTenantName(), token), "", "");
+
+		LegalTagUtils.delete(LEGAL_TAG, token);
+	}
+
+	@Test
+	public void should_return5Ids_when_requestingKindThatHas5Entries() throws Exception {
+		CloseableHttpResponse response = TestUtils.send("query/records", "GET", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "", "?kind=" + KIND);
+		assertEquals(HttpStatus.SC_OK, response.getCode());
+
+		DummyRecordsHelper.QueryResultMock responseObject = RECORDS_HELPER.getQueryResultMockFromResponse(response);
+		assertEquals(5, responseObject.results.length);
+	}
+
+	@Test
+	public void should_incrementThroughIds_when_requestingKindThatHasMoreThanOneEntry_and_limitIsSetTo2_and_usingPreviousCursorPos()
+			throws Exception {
+
+		Set<String> result = new HashSet<>();
+
+		// first call
+		CloseableHttpResponse response = TestUtils.send("query/records", "GET", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "", "?limit=2&kind=" + KIND);
+		if (response.getCode() != HttpStatus.SC_OK) {
+			fail(formResponseCheckingMessage(response));
+		}
+		DummyRecordsHelper.QueryResultMock responseObject = RECORDS_HELPER.getQueryResultMockFromResponse(response);
+		assertEquals(2, responseObject.results.length);
+		assertFalse(StringUtils.isEmpty(responseObject.cursor));
+
+		result.add(responseObject.results[0]);
+		result.add(responseObject.results[1]);
+
+		String cursor = responseObject.cursor;
+
+		// second call
+		response = TestUtils.send("query/records", "GET", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "",
+				"?limit=2&cursor=" + cursor + "&kind=" + KIND);
+		if (response.getCode() != HttpStatus.SC_OK) {
+			fail(formResponseCheckingMessage(response));
+		}
+		responseObject = RECORDS_HELPER.getQueryResultMockFromResponse(response);
+		assertEquals(2, responseObject.results.length);
+		assertFalse(StringUtils.isEmpty(responseObject.cursor));
+
+		result.add(responseObject.results[0]);
+		result.add(responseObject.results[1]);
+
+		cursor = responseObject.cursor;
+
+		// third call
+		response = TestUtils.send("query/records", "GET", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "",
+				"?limit=2&cursor=" + cursor + "&kind=" + KIND);
+		if (response.getCode() != HttpStatus.SC_OK) {
+			fail(formResponseCheckingMessage(response));
+		}
+		responseObject = RECORDS_HELPER.getQueryResultMockFromResponse(response);
+		assertEquals(1, responseObject.results.length);
+
+		result.add(responseObject.results[0]);
+
+		assertEquals(5, result.size());
+	}
+
+	@Test
+	public void should_returnError400_when_usingKindThatHasBadFormat() throws Exception {
+		CloseableHttpResponse response = TestUtils.send("query/records", "GET", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "", "?limit=1&kind=bad:kind");
+		assertEquals(HttpStatus.SC_BAD_REQUEST, response.getCode());
+	}
+
+	@Test
+	public void should_returnNoResults_when_usingKindThatDoesNotExist() throws Exception {
+		CloseableHttpResponse response = TestUtils.send("query/records", "GET", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "",
+				"?limit=1&kind=nonexisting:kind:formatted:1.0.0");
+		assertEquals(HttpStatus.SC_OK, response.getCode());
+		DummyRecordsHelper.QueryResultMock responseObject = RECORDS_HELPER.getQueryResultMockFromResponse(response);
+
+		assertEquals(0, responseObject.results.length);
+	}
+
+	@Test
+	public void should_returnError400_when_usingInvalidCursorParameter() throws Exception {
+		CloseableHttpResponse response = TestUtils.send("query/records", "GET", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "",
+				"?limit=1&cursor=MY_BAD_CURSOR&kind=" + RECORDS_HELPER.KIND);
+		assertEquals(HttpStatus.SC_BAD_REQUEST, response.getCode());
+	}
+
+	@Test
+	public void should_returnError400_when_notProvidingKindParameter() throws Exception {
+		CloseableHttpResponse response = TestUtils.send("query/records", "GET", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "", "?limit=1&kind=");
+		assertEquals(HttpStatus.SC_BAD_REQUEST, response.getCode());
+
+		response = TestUtils.send("query/records", "GET", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "", "?limit=1");
+		assertEquals(HttpStatus.SC_BAD_REQUEST, response.getCode());
+	}
+
+	protected String formResponseCheckingMessage(CloseableHttpResponse response) {
+		JsonObject json;
+		try {
+			json = JsonParser.parseString(EntityUtils.toString(response.getEntity())).getAsJsonObject();
+		} catch (IOException e) {
+			throw new RuntimeException(e);
+		} catch (ParseException e) {
+			throw new RuntimeException(e);
+		}
+		StringBuilder output = new StringBuilder();
+		output.append("API is not acting properly, responde code is: ")
+				.append(String.valueOf(response.getCode()))
+				.append(". And the reason is: ")
+				.append(json.get("reason").getAsString())
+		        .append(response.getHeaders("correlation-id")[0]);
+		return  output.toString();
+	}
+}
diff --git a/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/query/PostFetchRecordsIntegrationTests.java b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/query/PostFetchRecordsIntegrationTests.java
new file mode 100644
index 0000000000000000000000000000000000000000..56c47c1b054f5b31d43a415bfa2d4522cd0125dd
--- /dev/null
+++ b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/query/PostFetchRecordsIntegrationTests.java
@@ -0,0 +1,894 @@
+// Copyright 2017-2019, Schlumberger
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package org.opengroup.osdu.storage.query;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonObject;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
+import org.apache.http.HttpStatus;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.opengroup.osdu.storage.util.DummyRecordsHelper;
+import org.opengroup.osdu.storage.util.HeaderUtils;
+import org.opengroup.osdu.storage.util.LegalTagUtils;
+import org.opengroup.osdu.storage.util.RecordUtil;
+import org.opengroup.osdu.storage.util.TenantUtils;
+import org.opengroup.osdu.storage.util.TestBase;
+import org.opengroup.osdu.storage.util.TestUtils;
+import org.opengroup.osdu.storage.util.TokenTestUtils;
+
+
+public final class PostFetchRecordsIntegrationTests extends TestBase {
+    private static final long NOW = System.currentTimeMillis();
+
+    private static final String RECORD_ID_PREFIX = TenantUtils.getFirstTenantName() + ":query:";
+    private static final String KIND = TenantUtils.getTenantName() + ":ds:query:1.0." + NOW;
+    private static final String LEGAL_TAG = LegalTagUtils.createRandomName();
+    private static final DummyRecordsHelper RECORDS_HELPER = new DummyRecordsHelper();
+
+    private static final String PERSISTABLE_REFERENCE = "%7B%22LB_CRS%22%3A%22%257B%2522WKT%2522%253A%2522PROJCS%255B%255C%2522British_National_Grid%255C%2522%252CGEOGCS%255B%255C%2522GCS_OSGB_1936%255C%2522%252CDATUM%255B%255C%2522D_OSGB_1936%255C%2522%252CSPHEROID%255B%255C%2522Airy_1830%255C%2522%252C6377563.396%252C299.3249646%255D%255D%252CPRIMEM%255B%255C%2522Greenwich%255C%2522%252C0.0%255D%252CUNIT%255B%255C%2522Degree%255C%2522%252C0.0174532925199433%255D%255D%252CPROJECTION%255B%255C%2522Transverse_Mercator%255C%2522%255D%252CPARAMETER%255B%255C%2522False_Easting%255C%2522%252C400000.0%255D%252CPARAMETER%255B%255C%2522False_Northing%255C%2522%252C-100000.0%255D%252CPARAMETER%255B%255C%2522Central_Meridian%255C%2522%252C-2.0%255D%252CPARAMETER%255B%255C%2522Scale_Factor%255C%2522%252C0.9996012717%255D%252CPARAMETER%255B%255C%2522Latitude_Of_Origin%255C%2522%252C49.0%255D%252CUNIT%255B%255C%2522Meter%255C%2522%252C1.0%255D%252CAUTHORITY%255B%255C%2522EPSG%255C%2522%252C27700%255D%255D%2522%252C%2522Type%2522%253A%2522LBCRS%2522%252C%2522EngineVersion%2522%253A%2522PE_10_3_1%2522%252C%2522AuthorityCode%2522%253A%257B%2522Authority%2522%253A%2522EPSG%2522%252C%2522Code%2522%253A%252227700%2522%257D%252C%2522Name%2522%253A%2522British_National_Grid%2522%257D%22%2C%22TRF%22%3A%22%257B%2522WKT%2522%253A%2522GEOGTRAN%255B%255C%2522OSGB_1936_To_WGS_1984_Petroleum%255C%2522%252CGEOGCS%255B%255C%2522GCS_OSGB_1936%255C%2522%252CDATUM%255B%255C%2522D_OSGB_1936%255C%2522%252CSPHEROID%255B%255C%2522Airy_1830%255C%2522%252C6377563.396%252C299.3249646%255D%255D%252CPRIMEM%255B%255C%2522Greenwich%255C%2522%252C0.0%255D%252CUNIT%255B%255C%2522Degree%255C%2522%252C0.0174532925199433%255D%255D%252CGEOGCS%255B%255C%2522GCS_WGS_1984%255C%2522%252CDATUM%255B%255C%2522D_WGS_1984%255C%2522%252CSPHEROID%255B%255C%2522WGS_1984%255C%2522%252C6378137.0%252C298.257223563%255D%255D%252CPRIMEM%255B%255C%2522Greenwich%255C%2522%252C0.0%255D%252CUNIT%255B%255C%2522Degree%255C%2522%252C0.0174532925199433%255D%255D%252CMETHOD%255B%255C%2522Position_Vector%255C%2522%255D%252CPARAMETER%255B%255C%2522X_Axis_Translation%255C%2522%252C446.448%255D%252CPARAMETER%255B%255C%2522Y_Axis_Translation%255C%2522%252C-125.157%255D%252CPARAMETER%255B%255C%2522Z_Axis_Translation%255C%2522%252C542.06%255D%252CPARAMETER%255B%255C%2522X_Axis_Rotation%255C%2522%252C0.15%255D%252CPARAMETER%255B%255C%2522Y_Axis_Rotation%255C%2522%252C0.247%255D%252CPARAMETER%255B%255C%2522Z_Axis_Rotation%255C%2522%252C0.842%255D%252CPARAMETER%255B%255C%2522Scale_Difference%255C%2522%252C-20.489%255D%252CAUTHORITY%255B%255C%2522EPSG%255C%2522%252C1314%255D%255D%2522%252C%2522Type%2522%253A%2522STRF%2522%252C%2522EngineVersion%2522%253A%2522PE_10_3_1%2522%252C%2522AuthorityCode%2522%253A%257B%2522Authority%2522%253A%2522EPSG%2522%252C%2522Code%2522%253A%25221314%2522%257D%252C%2522Name%2522%253A%2522OSGB_1936_To_WGS_1984_Petroleum%2522%257D%22%2C%22Type%22%3A%22EBCRS%22%2C%22EngineVersion%22%3A%22PE_10_3_1%22%2C%22Name%22%3A%22OSGB+1936+*+UKOOA-Pet+%2F+British+National+Grid+%5B27700%2C1314%5D%22%2C%22AuthorityCode%22%3A%7B%22Authority%22%3A%22MyCompany%22%2C%22Code%22%3A%2227700006%22%7D%7D";
+    private static final String PERSISTABLE_REFERENCE_CRS = "{\"lateBoundCRS\":{\"wkt\":\"PROJCS[\\\"ED_1950_UTM_Zone_32N\\\",GEOGCS[\\\"GCS_European_1950\\\",DATUM[\\\"D_European_1950\\\",SPHEROID[\\\"International_1924\\\",6378388.0,297.0]],PRIMEM[\\\"Greenwich\\\",0.0],UNIT[\\\"Degree\\\",0.0174532925199433]],PROJECTION[\\\"Transverse_Mercator\\\"],PARAMETER[\\\"False_Easting\\\",500000.0],PARAMETER[\\\"False_Northing\\\",0.0],PARAMETER[\\\"Central_Meridian\\\",9.0],PARAMETER[\\\"Scale_Factor\\\",0.9996],PARAMETER[\\\"Latitude_Of_Origin\\\",0.0],UNIT[\\\"Meter\\\",1.0],AUTHORITY[\\\"EPSG\\\",23032]]\",\"ver\":\"PE_10_3_1\",\"name\":\"ED_1950_UTM_Zone_32N\",\"authCode\":{\"auth\":\"EPSG\",\"code\":\"23032\"},\"type\":\"LBC\"},\"singleCT\":{\"wkt\":\"GEOGTRAN[\\\"ED_1950_To_WGS_1984_23\\\",GEOGCS[\\\"GCS_European_1950\\\",DATUM[\\\"D_European_1950\\\",SPHEROID[\\\"International_1924\\\",6378388.0,297.0]],PRIMEM[\\\"Greenwich\\\",0.0],UNIT[\\\"Degree\\\",0.0174532925199433]],GEOGCS[\\\"GCS_WGS_1984\\\",DATUM[\\\"D_WGS_1984\\\",SPHEROID[\\\"WGS_1984\\\",6378137.0,298.257223563]],PRIMEM[\\\"Greenwich\\\",0.0],UNIT[\\\"Degree\\\",0.0174532925199433]],METHOD[\\\"Position_Vector\\\"],PARAMETER[\\\"X_Axis_Translation\\\",-116.641],PARAMETER[\\\"Y_Axis_Translation\\\",-56.931],PARAMETER[\\\"Z_Axis_Translation\\\",-110.559],PARAMETER[\\\"X_Axis_Rotation\\\",0.893],PARAMETER[\\\"Y_Axis_Rotation\\\",0.921],PARAMETER[\\\"Z_Axis_Rotation\\\",-0.917],PARAMETER[\\\"Scale_Difference\\\",-3.52],AUTHORITY[\\\"EPSG\\\",1612]]\",\"ver\":\"PE_10_3_1\",\"name\":\"ED_1950_To_WGS_1984_23\",\"authCode\":{\"auth\":\"EPSG\",\"code\":\"1612\"},\"type\":\"ST\"},\"ver\":\"PE_10_3_1\",\"name\":\"ED50 * EPSG-Nor N62 2001 / UTM zone 32N [23032,1612]\",\"authCode\":{\"auth\":\"SLB\",\"code\":\"23032023\"},\"type\":\"EBC\"}";
+    private static final String PERSISTABLE_REFERENCE_UNIT_Z = "{\"baseMeasurement\":{\"ancestry\":\"Length\",\"type\":\"UM\"},\"scaleOffset\":{\"offset\":0.0,\"scale\":0.3048},\"symbol\":\"ft\",\"type\":\"USO\"}";
+    private static final String DATETIME_PERSISTABLE_REFERENCE = "{\"type\":\"DAT\",\"format\":\"YYYY-MM-DD\"}";
+    private static final String UNIT_PERSISTABLE_REFERENCE = "{\"abcd\":{\"a\":0.0,\"b\":0.3048,\"c\":1.0,\"d\":0.0},\"symbol\":\"ft\",\"baseMeasurement\":{\"ancestry\":\"L\",\"type\":\"UM\"},\"type\":\"UAD\"}";
+    private static final String UNIT_OF_MEASURE_ID = String.format("%s:reference-data--UnitOfMeasure:ft:", TenantUtils.getTenantName());
+
+    private static final TokenTestUtils TOKEN_TEST_UTILS = new TokenTestUtils();
+
+    @BeforeAll
+    public static void classSetup() throws Exception {
+        PostFetchRecordsIntegrationTests.classSetup(TOKEN_TEST_UTILS.getToken());
+    }
+
+    @AfterAll
+    public static void classTearDown() throws Exception {
+        PostFetchRecordsIntegrationTests.classTearDown(TOKEN_TEST_UTILS.getToken());
+    }
+
+    @BeforeEach
+    @Override
+    public void setup() throws Exception {
+        this.testUtils = new TokenTestUtils();
+    }
+
+    @AfterEach
+    @Override
+    public void tearDown() throws Exception {
+        this.testUtils = null;
+    }
+
+    public static void classSetup(String token) throws Exception {
+        LegalTagUtils.create(LEGAL_TAG, token);
+    }
+
+    public static void classTearDown(String token) throws Exception {
+        LegalTagUtils.delete(LEGAL_TAG, token);
+    }
+    
+    @Test
+    public void should_returnSingleRecordMatching_when_noConversionRequired() throws Exception {
+        String recordId = RECORD_ID_PREFIX + UUID.randomUUID().toString();
+        String jsonInput = RecordUtil.createJsonRecordWithReference(1, recordId, KIND, LEGAL_TAG, PERSISTABLE_REFERENCE, "CRS");
+        CloseableHttpResponse createResponse = TestUtils.send("records", "PUT", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), jsonInput, "");
+        assertEquals(HttpStatus.SC_CREATED, createResponse.getCode());
+
+        JsonArray records = new JsonArray();
+        records.add(recordId + 0);
+
+        JsonObject body = new JsonObject();
+        body.add("records", records);
+
+        Map<String, String> headers = HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken());
+        headers.put("frame-of-reference", "none");
+        CloseableHttpResponse response = TestUtils.send("query/records:batch", "POST", headers, body.toString(),
+                "");
+        assertEquals(HttpStatus.SC_OK, response.getCode());
+
+        DummyRecordsHelper.ConvertedRecordsMock responseObject = RECORDS_HELPER.getConvertedRecordsMockFromResponse(response);
+
+        assertEquals(1, responseObject.records.length);
+        assertEquals(0, responseObject.notFound.length);
+        assertEquals(0, responseObject.conversionStatuses.size());
+
+        assertEquals(TestUtils.getAcl(), responseObject.records[0].acl.viewers[0]);
+        assertEquals(recordId + 0, responseObject.records[0].id);
+        assertEquals(3, responseObject.records[0].data.size());
+        assertEquals(KIND, responseObject.records[0].kind);
+        assertTrue(responseObject.records[0].version != null && !responseObject.records[0].version.isEmpty());
+
+        CloseableHttpResponse deleteResponse = TestUtils.send("records/" + recordId + 0, "DELETE", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "", "");
+        assertEquals(HttpStatus.SC_NO_CONTENT, deleteResponse.getCode());
+    }
+
+    @Test
+    public void should_returnRecordMatchingAndRecordNotFound_when_noConversionRequired() throws Exception {
+        String recordId = RECORD_ID_PREFIX + UUID.randomUUID().toString();
+        String jsonInput = RecordUtil.createJsonRecordWithReference(1, recordId, KIND, LEGAL_TAG, PERSISTABLE_REFERENCE, "CRS");
+        CloseableHttpResponse createResponse = TestUtils.send("records", "PUT", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), jsonInput, "");
+        assertEquals(HttpStatus.SC_CREATED, createResponse.getCode());
+
+        JsonArray records = new JsonArray();
+        records.add(recordId + 0);
+        records.add("nonexisting:id");
+
+        JsonObject body = new JsonObject();
+        body.add("records", records);
+
+        Map<String, String> headers = HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken());
+        headers.put("frame-of-reference", "none");
+        CloseableHttpResponse response = TestUtils.send("query/records:batch", "POST", headers, body.toString(),
+                "");
+        assertEquals(HttpStatus.SC_OK, response.getCode());
+
+        DummyRecordsHelper.ConvertedRecordsMock responseObject = RECORDS_HELPER.getConvertedRecordsMockFromResponse(response);
+
+        assertEquals(1, responseObject.records.length);
+        assertEquals(1, responseObject.notFound.length);
+        assertEquals(0, responseObject.conversionStatuses.size());
+
+        assertEquals("nonexisting:id", responseObject.notFound[0]);
+        assertEquals(TestUtils.getAcl(), responseObject.records[0].acl.viewers[0]);
+        assertEquals(recordId + 0, responseObject.records[0].id);
+        assertEquals(3, responseObject.records[0].data.size());
+        assertEquals(KIND, responseObject.records[0].kind);
+        assertTrue(responseObject.records[0].version != null && !responseObject.records[0].version.isEmpty());
+
+        CloseableHttpResponse deleteResponse = TestUtils.send("records/" + recordId + 0, "DELETE", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "", "");
+        assertEquals(HttpStatus.SC_NO_CONTENT, deleteResponse.getCode());
+    }
+
+    @Test
+    public void should_return400BadRequest_when_moreThan20RecordsRequiredAndNoConversionRequired() throws Exception {
+        String recordId = RECORD_ID_PREFIX + UUID.randomUUID().toString();
+
+        JsonArray records = new JsonArray();
+        for (int i = 0; i < 21; i++) {
+            records.add(recordId + String.valueOf(i));
+        }
+
+        JsonObject body = new JsonObject();
+        body.add("records", records);
+
+        Map<String, String> headers = HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken());
+        headers.put("frame-of-reference", "none");
+        CloseableHttpResponse response = TestUtils.send("query/records:batch", "POST", headers, body.toString(),
+                "");
+        assertEquals(HttpStatus.SC_BAD_REQUEST, response.getCode());
+    }
+
+    @Test
+   // @Ignore // Ignoring the test for now, once we have CRS converter we should enable this test
+    public void should_returnConvertedRecords_whenConversionRequiredAndNoError() throws Exception {
+        String recordId = RECORD_ID_PREFIX + UUID.randomUUID().toString();
+        String jsonInput = RecordUtil.createJsonRecordWithReference(2, recordId, KIND, LEGAL_TAG, PERSISTABLE_REFERENCE, "CRS");
+        CloseableHttpResponse createResponse = TestUtils.send("records", "PUT", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), jsonInput, "");
+        assertEquals(HttpStatus.SC_CREATED, createResponse.getCode());
+
+        JsonArray records = new JsonArray();
+        records.add(recordId + 0);
+        records.add(recordId + 1);
+
+        JsonObject body = new JsonObject();
+        body.add("records", records);
+
+        Map<String, String> headers = HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken());
+        headers.put("frame-of-reference", "units=SI;crs=wgs84;elevation=msl;azimuth=true north;dates=utc;");
+        CloseableHttpResponse response = TestUtils.send("query/records:batch", "POST", headers, body.toString(),
+                "");
+        assertEquals(HttpStatus.SC_OK, response.getCode());
+
+        DummyRecordsHelper.ConvertedRecordsMock responseObject = RECORDS_HELPER.getConvertedRecordsMockFromResponse(response);
+        assertEquals(2, responseObject.records.length);
+        assertEquals(0, responseObject.notFound.length);
+        assertEquals(2, responseObject.conversionStatuses.size());
+
+        assertEquals(KIND, responseObject.records[0].kind);
+        assertTrue(responseObject.records[0].version != null && !responseObject.records[0].version.isEmpty());
+        assertEquals(3, responseObject.records[0].data.size());
+
+        CloseableHttpResponse deleteResponse1 = TestUtils.send("records/" + recordId + 0, "DELETE", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "", "");
+        assertEquals(HttpStatus.SC_NO_CONTENT, deleteResponse1.getCode());
+        CloseableHttpResponse deleteResponse2 = TestUtils.send("records/" + recordId + 1, "DELETE", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "", "");
+        assertEquals(HttpStatus.SC_NO_CONTENT, deleteResponse2.getCode());
+
+    }
+
+   // @Ignore // Ignoring the test for now, once we have CRS converter we should enable this test
+    @Test
+    public void should_returnConvertedRecords_whenConversionRequiredAndNoErrorWithMultiplePairOfCoordinates() throws Exception {
+        String recordId = RECORD_ID_PREFIX + UUID.randomUUID().toString();
+        String jsonInput = RecordUtil.createJsonRecordWithMultiplePairOfCoordinates(2, recordId, KIND, LEGAL_TAG, PERSISTABLE_REFERENCE, "CRS");
+        CloseableHttpResponse createResponse = TestUtils.send("records", "PUT", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), jsonInput, "");
+        assertEquals(HttpStatus.SC_CREATED, createResponse.getCode());
+
+        JsonArray records = new JsonArray();
+        records.add(recordId + 0);
+        records.add(recordId + 1);
+
+        JsonObject body = new JsonObject();
+        body.add("records", records);
+
+        Map<String, String> headers = HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken());
+        headers.put("frame-of-reference", "units=SI;crs=wgs84;elevation=msl;azimuth=true north;dates=utc;");
+        CloseableHttpResponse response = TestUtils.send("query/records:batch", "POST", headers, body.toString(),
+                "");
+        assertEquals(HttpStatus.SC_OK, response.getCode());
+
+        DummyRecordsHelper.ConvertedRecordsMock responseObject = RECORDS_HELPER.getConvertedRecordsMockFromResponse(response);
+        assertEquals(2, responseObject.records.length);
+        assertEquals(0, responseObject.notFound.length);
+
+        assertEquals(KIND, responseObject.records[0].kind);
+        assertTrue(responseObject.records[0].version != null && !responseObject.records[0].version.isEmpty());
+        assertEquals(5, responseObject.records[0].data.size());
+
+        CloseableHttpResponse deleteResponse1 = TestUtils.send("records/" + recordId + 0, "DELETE", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "", "");
+        assertEquals(HttpStatus.SC_NO_CONTENT, deleteResponse1.getCode());
+        CloseableHttpResponse deleteResponse2 = TestUtils.send("records/" + recordId + 1, "DELETE", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "", "");
+        assertEquals(HttpStatus.SC_NO_CONTENT, deleteResponse2.getCode());
+
+    }
+
+   // @Ignore // Ignoring the test for now, once we have CRS converter we should enable this test
+    @Test
+    public void should_returnOriginalRecordsAndConversionStatusAsNoMeta_whenConversionRequiredAndNoMetaBlockInRecord() throws Exception{
+        String recordId = RECORD_ID_PREFIX + UUID.randomUUID().toString();
+        String jsonInput = RecordUtil.createJsonRecordNoMetaBlock(2, recordId, KIND, LEGAL_TAG);
+        CloseableHttpResponse createResponse = TestUtils.send("records", "PUT", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), jsonInput, "");
+        assertEquals(HttpStatus.SC_CREATED, createResponse.getCode());
+
+        JsonArray records = new JsonArray();
+        records.add(recordId + 0);
+        records.add(recordId + 1);
+        records.add("nonexisting:id");
+
+        JsonObject body = new JsonObject();
+        body.add("records", records);
+
+        Map<String, String> headers = HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken());
+        headers.put("frame-of-reference", "units=SI;crs=wgs84;elevation=msl;azimuth=true north;dates=utc;");
+        CloseableHttpResponse response = TestUtils.send("query/records:batch", "POST", headers, body.toString(),
+                "");
+        assertEquals(HttpStatus.SC_OK, response.getCode());
+
+        DummyRecordsHelper.ConvertedRecordsMock responseObject = RECORDS_HELPER.getConvertedRecordsMockFromResponse(response);
+        assertEquals(2, responseObject.records.length);
+        assertEquals(1, responseObject.notFound.length);
+        assertEquals(2, responseObject.conversionStatuses.size());
+        assertEquals("nonexisting:id", responseObject.notFound[0]);
+        assertEquals(KIND, responseObject.records[0].kind);
+        assertTrue(responseObject.records[0].version != null && !responseObject.records[0].version.isEmpty());
+        assertEquals(3, responseObject.records[0].data.size());
+        List<DummyRecordsHelper.RecordStatusMock> conversionStatuses = responseObject.conversionStatuses;
+        assertEquals("CRS Conversion: Meta Block is missing or empty in this record, no conversion applied.", conversionStatuses.get(0).errors.get(0));
+        CloseableHttpResponse deleteResponse1 = TestUtils.send("records/" + recordId + 0, "DELETE", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "", "");
+        assertEquals(HttpStatus.SC_NO_CONTENT, deleteResponse1.getCode());
+        CloseableHttpResponse deleteResponse2 = TestUtils.send("records/" + recordId + 1, "DELETE", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "", "");
+        assertEquals(HttpStatus.SC_NO_CONTENT, deleteResponse2.getCode());
+    }
+
+    @Test
+    public void should_returnRecordsAndConversionStatus_whenConversionRequiredAndConversionErrorExists() throws Exception {
+        String recordId = RECORD_ID_PREFIX + UUID.randomUUID().toString();
+        String jsonInput = RecordUtil.createJsonRecordMissingValue(2, recordId, KIND, LEGAL_TAG, PERSISTABLE_REFERENCE, "CRS");
+        CloseableHttpResponse createResponse = TestUtils.send("records", "PUT", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), jsonInput, "");
+        assertEquals(HttpStatus.SC_CREATED, createResponse.getCode());
+
+        JsonArray records = new JsonArray();
+        records.add(recordId + 0);
+        records.add(recordId + 1);
+
+        JsonObject body = new JsonObject();
+        body.add("records", records);
+
+        Map<String, String> headers = HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken());
+        headers.put("frame-of-reference", "units=SI;crs=wgs84;elevation=msl;azimuth=true north;dates=utc;");
+        CloseableHttpResponse response = TestUtils.send("query/records:batch", "POST", headers, body.toString(),
+                "");
+        assertEquals(HttpStatus.SC_OK, response.getCode());
+
+        DummyRecordsHelper.ConvertedRecordsMock responseObject = RECORDS_HELPER.getConvertedRecordsMockFromResponse(response);
+        assertEquals(2, responseObject.records.length);
+        assertEquals(0, responseObject.notFound.length);
+        assertEquals(2, responseObject.conversionStatuses.size());
+        assertEquals(TestUtils.getAcl(), responseObject.records[0].acl.viewers[0]);
+        assertEquals(KIND, responseObject.records[0].kind);
+        assertTrue(responseObject.records[0].version != null && !responseObject.records[0].version.isEmpty());
+        assertEquals(2, responseObject.records[0].data.size());
+        List<DummyRecordsHelper.RecordStatusMock> conversionStatuses = responseObject.conversionStatuses;
+        assertEquals("CRS conversion: Unknown coordinate pair 'z'.", conversionStatuses.get(0).errors.get(1));
+        assertEquals("CRS conversion: property 'Y' is missing in datablock, no conversion applied to this property and its corresponding pairing property.", conversionStatuses.get(0).errors.get(0));
+        CloseableHttpResponse deleteResponse1 = TestUtils.send("records/" + recordId + 0, "DELETE", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "", "");
+        assertEquals(HttpStatus.SC_NO_CONTENT, deleteResponse1.getCode());
+        CloseableHttpResponse deleteResponse2 = TestUtils.send("records/" + recordId + 1, "DELETE", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "", "");
+        assertEquals(HttpStatus.SC_NO_CONTENT, deleteResponse2.getCode());
+    }
+
+    @Test
+   // @Ignore // Ignoring the test for now, once we have CRS converter we should enable this test
+    public void should_returnRecordsAndConversionStatus_whenConversionRequiredAndNestedPropertyProvidedInMetaBlock() throws Exception {
+        String recordId = RECORD_ID_PREFIX + UUID.randomUUID().toString();
+        String jsonInput = RecordUtil.createJsonRecordWithNestedProperty(1, recordId, KIND, LEGAL_TAG, PERSISTABLE_REFERENCE, "CRS");
+        CloseableHttpResponse createResponse = TestUtils.send("records", "PUT", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), jsonInput, "");
+        assertEquals(HttpStatus.SC_CREATED, createResponse.getCode());
+
+        JsonArray records = new JsonArray();
+        records.add(recordId + 0);
+
+        JsonObject body = new JsonObject();
+        body.add("records", records);
+
+        Map<String, String> headers = HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken());
+        headers.put("frame-of-reference", "units=SI;crs=wgs84;elevation=msl;azimuth=true north;dates=utc;");
+        CloseableHttpResponse response = TestUtils.send("query/records:batch", "POST", headers, body.toString(),
+                "");
+        assertEquals(HttpStatus.SC_OK, response.getCode());
+
+        DummyRecordsHelper.ConvertedRecordsMock responseObject = RECORDS_HELPER.getConvertedRecordsMockFromResponse(response);
+        assertEquals(1, responseObject.records.length);
+        assertEquals(0, responseObject.notFound.length);
+        assertEquals(1, responseObject.conversionStatuses.size());
+        assertEquals(TestUtils.getAcl(), responseObject.records[0].acl.viewers[0]);
+        assertEquals(KIND, responseObject.records[0].kind);
+        assertTrue(responseObject.records[0].version != null && !responseObject.records[0].version.isEmpty());
+
+        CloseableHttpResponse deleteResponse = TestUtils.send("records/" + recordId + 0, "DELETE", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "", "");
+        assertEquals(HttpStatus.SC_NO_CONTENT, deleteResponse.getCode());
+    }
+    //@Ignore // Ignoring the test for now, once we have CRS converter we should enable this test
+    @Test
+    public void should_returnRecordsAndConversionStatus_whenConversionRequiredAndNestedPropertyProvidedInMetaBlock1() throws Exception {
+        String recordId = RECORD_ID_PREFIX + UUID.randomUUID().toString();
+        String jsonInput = RecordUtil.createJsonRecordWithNestedProperty(1, recordId, KIND, LEGAL_TAG, PERSISTABLE_REFERENCE, "CRS");
+        CloseableHttpResponse createResponse = TestUtils.send("records", "PUT", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), jsonInput, "");
+        assertEquals(HttpStatus.SC_CREATED, createResponse.getCode());
+
+        JsonArray records = new JsonArray();
+        records.add(recordId + 0);
+
+        JsonObject body = new JsonObject();
+        body.add("records", records);
+
+        Map<String, String> headers = HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken());
+        headers.put("frame-of-reference", "units=SI;crs=wgs84;elevation=msl;azimuth=true north;dates=utc;");
+        CloseableHttpResponse response = TestUtils.send("query/records:batch", "POST", headers, body.toString(),
+                "");
+        assertEquals(HttpStatus.SC_OK, response.getCode());
+
+        DummyRecordsHelper.ConvertedRecordsMock responseObject = RECORDS_HELPER.getConvertedRecordsMockFromResponse(response);
+        assertEquals(1, responseObject.records.length);
+        assertEquals(0, responseObject.notFound.length);
+        assertEquals(TestUtils.getAcl(), responseObject.records[0].acl.viewers[0]);
+        assertEquals(KIND, responseObject.records[0].kind);
+        assertTrue(responseObject.records[0].version != null && !responseObject.records[0].version.isEmpty());
+
+        CloseableHttpResponse deleteResponse = TestUtils.send("records/" + recordId + 0, "DELETE", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "", "");
+        assertEquals(HttpStatus.SC_NO_CONTENT, deleteResponse.getCode());
+    }
+
+    @Test
+    public void should_returnRecordsAndConversionStatus_whenDateAndFormatProvidedInMetaBlock() throws Exception {
+        String recordId = RECORD_ID_PREFIX + UUID.randomUUID().toString();
+        String jsonInput = RecordUtil.createJsonRecordsWithDateFormat(1, recordId, KIND, LEGAL_TAG, "yyyy-MM-dd", "creationDate", "2019-08-03", DATETIME_PERSISTABLE_REFERENCE);
+        CloseableHttpResponse createResponse = TestUtils.send("records", "PUT", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), jsonInput, "");
+        assertEquals(HttpStatus.SC_CREATED, createResponse.getCode());
+
+        JsonArray records = new JsonArray();
+        records.add(recordId + 0);
+
+        JsonObject body = new JsonObject();
+        body.add("records", records);
+
+        Map<String, String> headers = HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken());
+        headers.put("frame-of-reference", "units=SI;crs=wgs84;elevation=msl;azimuth=true north;dates=utc;");
+        CloseableHttpResponse response = TestUtils.send("query/records:batch", "POST", headers, body.toString(),"");
+        assertEquals(HttpStatus.SC_OK, response.getCode());
+
+        DummyRecordsHelper.ConvertedRecordsMock responseObject = RECORDS_HELPER.getConvertedRecordsMockFromResponse(response);
+        assertEquals(1, responseObject.records.length);
+        assertEquals(0, responseObject.notFound.length);
+
+        assertEquals(TestUtils.getAcl(), responseObject.records[0].acl.viewers[0]);
+        assertEquals(KIND, responseObject.records[0].kind);
+        assertTrue(responseObject.records[0].version != null && !responseObject.records[0].version.isEmpty());
+
+        CloseableHttpResponse deleteResponse = TestUtils.send("records/" + recordId + 0, "DELETE", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "", "");
+        assertEquals(HttpStatus.SC_NO_CONTENT, deleteResponse.getCode());
+    }
+
+   // @Ignore // Ignoring the test for now, once we have CRS converter we should enable this test
+    @Test
+    public void should_returnRecordsAndConversionStatus_whenNestedArrayOfPropertiesProvidedWithoutError() throws Exception {
+        String recordId = RECORD_ID_PREFIX + UUID.randomUUID().toString();
+
+        String jsonInput = RecordUtil.createJsonRecordWithNestedArrayOfProperties(1, recordId, KIND, LEGAL_TAG, UNIT_PERSISTABLE_REFERENCE, "Unit",  UNIT_OF_MEASURE_ID);
+        CloseableHttpResponse createResponse = TestUtils.send("records", "PUT", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), jsonInput, "");
+        assertEquals(HttpStatus.SC_CREATED, createResponse.getCode());
+
+        JsonArray records = new JsonArray();
+        records.add(recordId + 12);
+
+        JsonObject body = new JsonObject();
+        body.add("records", records);
+
+        Map<String, String> headers = HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken());
+        headers.put("frame-of-reference", "units=SI;crs=wgs84;elevation=msl;azimuth=true north;dates=utc;");
+        CloseableHttpResponse response = TestUtils.send("query/records:batch", "POST", headers, body.toString(),"");
+        assertEquals(HttpStatus.SC_OK, response.getCode());
+
+        DummyRecordsHelper.ConvertedRecordsMock responseObject = RECORDS_HELPER.getConvertedRecordsMockFromResponse(response);
+        assertNotNull(responseObject.records);
+        assertEquals(1, responseObject.records.length);
+        assertNotNull(responseObject.notFound);
+        assertEquals(0, responseObject.notFound.length);
+        assertNotNull(responseObject.conversionStatuses);
+        assertEquals(1, responseObject.conversionStatuses.size());
+        assertEquals(TestUtils.getAcl(), responseObject.records[0].acl.viewers[0]);
+        assertEquals(KIND, responseObject.records[0].kind);
+        assertTrue(responseObject.records[0].version != null && !responseObject.records[0].version.isEmpty());
+        assertEquals(2, responseObject.records[0].data.size());
+        List<DummyRecordsHelper.RecordStatusMock> conversionStatuses = responseObject.conversionStatuses;
+        assertEquals("SUCCESS", conversionStatuses.get(0).status);
+
+        CloseableHttpResponse deleteResponse = TestUtils.send("records/" + recordId + 12, "DELETE", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "", "");
+        assertEquals(HttpStatus.SC_NO_CONTENT, deleteResponse.getCode());
+    }
+
+  //  @Ignore // Ignoring the test for now, once we have CRS converter we should enable this test
+    @Test
+    public void should_returnRecordsAndConversionStatus_whenNestedArrayOfPropertiesProvidedWithInvalidValues() throws Exception {
+        String recordId = RECORD_ID_PREFIX + UUID.randomUUID().toString();
+        String jsonInput = RecordUtil.createJsonRecordWithNestedArrayOfPropertiesAndInvalidValues(1, recordId, KIND, LEGAL_TAG, UNIT_PERSISTABLE_REFERENCE, "Unit", UNIT_OF_MEASURE_ID);
+        CloseableHttpResponse createResponse = TestUtils.send("records", "PUT", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), jsonInput, "");
+        assertEquals(HttpStatus.SC_CREATED, createResponse.getCode());
+
+        JsonArray records = new JsonArray();
+        records.add(recordId + 12);
+
+        JsonObject body = new JsonObject();
+        body.add("records", records);
+
+        Map<String, String> headers = HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken());
+        headers.put("frame-of-reference", "units=SI;crs=wgs84;elevation=msl;azimuth=true north;dates=utc;");
+        CloseableHttpResponse response = TestUtils.send("query/records:batch", "POST", headers, body.toString(),"");
+        assertEquals(HttpStatus.SC_OK, response.getCode());
+
+        DummyRecordsHelper.ConvertedRecordsMock responseObject = RECORDS_HELPER.getConvertedRecordsMockFromResponse(response);
+        assertEquals(1, responseObject.records.length);
+        assertEquals(0, responseObject.notFound.length);
+        assertEquals(1, responseObject.conversionStatuses.size());
+        assertEquals(TestUtils.getAcl(), responseObject.records[0].acl.viewers[0]);
+        assertEquals(KIND, responseObject.records[0].kind);
+        assertTrue(responseObject.records[0].version != null && !responseObject.records[0].version.isEmpty());
+        assertEquals(2, responseObject.records[0].data.size());
+        List<DummyRecordsHelper.RecordStatusMock> conversionStatuses = responseObject.conversionStatuses;
+        assertEquals("ERROR", conversionStatuses.get(0).status);
+        assertEquals("Unit conversion: illegal value for property markers[1].measuredDepth", conversionStatuses.get(0).errors.get(0));
+
+        CloseableHttpResponse deleteResponse = TestUtils.send("records/" + recordId + 12, "DELETE", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "", "");
+        assertEquals(HttpStatus.SC_NO_CONTENT, deleteResponse.getCode());
+    }
+
+   // @Ignore // Ignoring the test for now, once we have CRS converter we should enable this test
+    @Test
+    public void should_returnRecordsAndConversionStatus_whenInhomogeneousNestedArrayOfPropertiesProvidedWithoutError() throws Exception {
+        String recordId = RECORD_ID_PREFIX + UUID.randomUUID().toString();
+        String jsonInput = RecordUtil.createJsonRecordWithInhomogeneousNestedArrayOfProperties(1, recordId, KIND, LEGAL_TAG, UNIT_PERSISTABLE_REFERENCE, "Unit", UNIT_OF_MEASURE_ID);
+        CloseableHttpResponse createResponse = TestUtils.send("records", "PUT", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), jsonInput, "");
+        assertEquals(HttpStatus.SC_CREATED, createResponse.getCode());
+
+        JsonArray records = new JsonArray();
+        records.add(recordId + 13);
+
+        JsonObject body = new JsonObject();
+        body.add("records", records);
+
+        Map<String, String> headers = HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken());
+        headers.put("frame-of-reference", "units=SI;crs=wgs84;elevation=msl;azimuth=true north;dates=utc;");
+        CloseableHttpResponse response = TestUtils.send("query/records:batch", "POST", headers, body.toString(),"");
+        assertEquals(HttpStatus.SC_OK, response.getCode());
+
+        DummyRecordsHelper.ConvertedRecordsMock responseObject = RECORDS_HELPER.getConvertedRecordsMockFromResponse(response);
+        assertEquals(1, responseObject.records.length);
+        assertEquals(0, responseObject.notFound.length);
+        assertEquals(1, responseObject.conversionStatuses.size());
+        assertEquals(TestUtils.getAcl(), responseObject.records[0].acl.viewers[0]);
+        assertEquals(KIND, responseObject.records[0].kind);
+        assertTrue(responseObject.records[0].version != null && !responseObject.records[0].version.isEmpty());
+        assertEquals(2, responseObject.records[0].data.size());
+        List<DummyRecordsHelper.RecordStatusMock> conversionStatuses = responseObject.conversionStatuses;
+        assertEquals("SUCCESS", conversionStatuses.get(0).status);
+
+        CloseableHttpResponse deleteResponse = TestUtils.send("records/" + recordId + 13, "DELETE", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "", "");
+        assertEquals(HttpStatus.SC_NO_CONTENT, deleteResponse.getCode());
+    }
+
+   // @Ignore // Ignoring the test for now, once we have CRS converter we should enable this test
+    @Test
+    public void should_returnRecordsAndConversionStatus_whenInhomogeneousNestedArrayOfPropertiesProvidedWithInvalidValues() throws Exception {
+        String recordId = RECORD_ID_PREFIX + UUID.randomUUID().toString();
+        String jsonInput = RecordUtil.createJsonRecordWithInhomogeneousNestedArrayOfPropertiesAndInvalidValues(1, recordId, KIND, LEGAL_TAG, UNIT_PERSISTABLE_REFERENCE, "Unit", UNIT_OF_MEASURE_ID);
+        CloseableHttpResponse createResponse = TestUtils.send("records", "PUT", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), jsonInput, "");
+        assertEquals(HttpStatus.SC_CREATED, createResponse.getCode());
+
+        JsonArray records = new JsonArray();
+        records.add(recordId + 13);
+
+        JsonObject body = new JsonObject();
+        body.add("records", records);
+
+        Map<String, String> headers = HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken());
+        headers.put("frame-of-reference", "units=SI;crs=wgs84;elevation=msl;azimuth=true north;dates=utc;");
+        CloseableHttpResponse response = TestUtils.send("query/records:batch", "POST", headers, body.toString(),"");
+        assertEquals(HttpStatus.SC_OK, response.getCode());
+
+        DummyRecordsHelper.ConvertedRecordsMock responseObject = RECORDS_HELPER.getConvertedRecordsMockFromResponse(response);
+        assertEquals(1, responseObject.records.length);
+        assertEquals(0, responseObject.notFound.length);
+        assertEquals(1, responseObject.conversionStatuses.size());
+        assertEquals(TestUtils.getAcl(), responseObject.records[0].acl.viewers[0]);
+        assertEquals(KIND, responseObject.records[0].kind);
+        assertTrue(responseObject.records[0].version != null && !responseObject.records[0].version.isEmpty());
+        assertEquals(2, responseObject.records[0].data.size());
+        List<DummyRecordsHelper.RecordStatusMock> conversionStatuses = responseObject.conversionStatuses;
+        assertEquals("ERROR", conversionStatuses.get(0).status);
+        assertEquals("Unit conversion: illegal value for property markers[1].measuredDepth", conversionStatuses.get(0).errors.get(0));
+
+        CloseableHttpResponse deleteResponse = TestUtils.send("records/" + recordId + 13, "DELETE", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "", "");
+        assertEquals(HttpStatus.SC_NO_CONTENT, deleteResponse.getCode());
+    }
+
+   // @Ignore // Ignoring the test for now, once we have CRS converter we should enable this test
+    @Test
+    public void should_returnRecordsAndConversionStatus_whenInhomogeneousNestedArrayOfPropertiesProvidedWithIndexOutOfBoundary() throws Exception {
+        String recordId = RECORD_ID_PREFIX + UUID.randomUUID().toString();
+        String jsonInput = RecordUtil.createJsonRecordWithInhomogeneousNestedArrayOfPropertiesAndIndexOutOfBoundary(1, recordId, KIND, LEGAL_TAG, UNIT_PERSISTABLE_REFERENCE, "Unit", UNIT_OF_MEASURE_ID);
+        CloseableHttpResponse createResponse = TestUtils.send("records", "PUT", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), jsonInput, "");
+        assertEquals(HttpStatus.SC_CREATED, createResponse.getCode());
+
+        JsonArray records = new JsonArray();
+        records.add(recordId + 13);
+
+        JsonObject body = new JsonObject();
+        body.add("records", records);
+
+        Map<String, String> headers = HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken());
+        headers.put("frame-of-reference", "units=SI;crs=wgs84;elevation=msl;azimuth=true north;dates=utc;");
+        CloseableHttpResponse response = TestUtils.send("query/records:batch", "POST", headers, body.toString(),"");
+        assertEquals(HttpStatus.SC_OK, response.getCode());
+
+        DummyRecordsHelper.ConvertedRecordsMock responseObject = RECORDS_HELPER.getConvertedRecordsMockFromResponse(response);
+        assertEquals(1, responseObject.records.length);
+        assertEquals(0, responseObject.notFound.length);
+        assertEquals(1, responseObject.conversionStatuses.size());
+        assertEquals(TestUtils.getAcl(), responseObject.records[0].acl.viewers[0]);
+        assertEquals(KIND, responseObject.records[0].kind);
+        assertTrue(responseObject.records[0].version != null && !responseObject.records[0].version.isEmpty());
+        assertEquals(2, responseObject.records[0].data.size());
+        List<DummyRecordsHelper.RecordStatusMock> conversionStatuses = responseObject.conversionStatuses;
+        assertEquals("SUCCESS", conversionStatuses.get(0).status);
+        assertEquals("Unit conversion: property markers[2].measuredDepth missing", conversionStatuses.get(0).errors.get(0));
+
+        CloseableHttpResponse deleteResponse = TestUtils.send("records/" + recordId + 13, "DELETE", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "", "");
+        assertEquals(HttpStatus.SC_NO_CONTENT, deleteResponse.getCode());
+    }
+
+   // @Ignore // Ignoring the test for now, once we have CRS converter we should enable this test
+    @Test
+    public void should_returnRecordsAfterCrsConversion__whenProvidedRecordWithAsIngestedCoordinatesBlockTypePoint() throws Exception {
+        String recordId = RECORD_ID_PREFIX + UUID.randomUUID().toString();
+        String jsonInput = RecordUtil.createJsonRecordWithAsIngestedCoordinates(1, recordId, KIND, LEGAL_TAG, PERSISTABLE_REFERENCE_CRS, PERSISTABLE_REFERENCE_UNIT_Z, "AnyCrsPoint", "SpatialLocation");
+        CloseableHttpResponse createResponse = TestUtils.send("records", "PUT", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), jsonInput, "");
+        assertEquals(HttpStatus.SC_CREATED, createResponse.getCode());
+
+        JsonArray records = new JsonArray();
+        records.add(recordId + 0);
+
+        JsonObject body = new JsonObject();
+        body.add("records", records);
+
+        Map<String, String> headers = HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken());
+        headers.put("frame-of-reference", "units=SI;crs=wgs84;elevation=msl;azimuth=true north;dates=utc;");
+        CloseableHttpResponse response = TestUtils.send("query/records:batch", "POST", headers, body.toString(),"");
+        assertEquals(HttpStatus.SC_OK, response.getCode());
+
+        DummyRecordsHelper.ConvertedRecordsMock responseObject = RECORDS_HELPER.getConvertedRecordsMockFromResponse(response);
+        assertEquals(1, responseObject.records.length);
+        assertEquals(0, responseObject.notFound.length);
+        assertEquals(1, responseObject.conversionStatuses.size());
+        assertEquals("SUCCESS", responseObject.conversionStatuses.get(0).status);
+
+        assertEquals(KIND, responseObject.records[0].kind);
+        assertTrue(responseObject.records[0].version != null && !responseObject.records[0].version.isEmpty());
+        assertEquals(1, responseObject.records[0].data.size());
+
+        CloseableHttpResponse deleteResponse1 = TestUtils.send("records/" + recordId + 0, "DELETE", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "", "");
+        assertEquals(HttpStatus.SC_NO_CONTENT, deleteResponse1.getCode());
+    }
+
+   // @Ignore // Ignoring the test for now, once we have CRS converter we should enable this test
+    @Test
+    public void should_returnRecordsAfterCrsConversion__whenProvidedRecordWithAsIngestedCoordinatesBlockTypeMultiPoint() throws Exception {
+        String recordId = RECORD_ID_PREFIX + UUID.randomUUID().toString();
+        String jsonInput = RecordUtil.createJsonRecordWithAsIngestedCoordinates(1, recordId, KIND, LEGAL_TAG, PERSISTABLE_REFERENCE_CRS, PERSISTABLE_REFERENCE_UNIT_Z, "AnyCrsMultiPoint", "SpatialLocation");
+        CloseableHttpResponse createResponse = TestUtils.send("records", "PUT", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), jsonInput, "");
+        assertEquals(HttpStatus.SC_CREATED, createResponse.getCode());
+
+        JsonArray records = new JsonArray();
+        records.add(recordId + 0);
+
+        JsonObject body = new JsonObject();
+        body.add("records", records);
+
+        Map<String, String> headers = HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken());
+        headers.put("frame-of-reference", "units=SI;crs=wgs84;elevation=msl;azimuth=true north;dates=utc;");
+        CloseableHttpResponse response = TestUtils.send("query/records:batch", "POST", headers, body.toString(),"");
+        assertEquals(HttpStatus.SC_OK, response.getCode());
+
+        DummyRecordsHelper.ConvertedRecordsMock responseObject = RECORDS_HELPER.getConvertedRecordsMockFromResponse(response);
+        assertEquals(1, responseObject.records.length);
+        assertEquals(0, responseObject.notFound.length);
+        assertEquals(1, responseObject.conversionStatuses.size());
+        assertEquals("SUCCESS", responseObject.conversionStatuses.get(0).status);
+
+        assertEquals(KIND, responseObject.records[0].kind);
+        assertTrue(responseObject.records[0].version != null && !responseObject.records[0].version.isEmpty());
+        assertEquals(1, responseObject.records[0].data.size());
+
+        CloseableHttpResponse deleteResponse1 = TestUtils.send("records/" + recordId + 0, "DELETE", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "", "");
+        assertEquals(HttpStatus.SC_NO_CONTENT, deleteResponse1.getCode());
+    }
+
+   // @Ignore // Ignoring the test for now, once we have CRS converter we should enable this test
+    @Test
+    public void should_returnRecordsAfterCrsConversion__whenProvidedRecordWithAsIngestedCoordinatesBlockTypePolygon() throws Exception {
+        String recordId = RECORD_ID_PREFIX + UUID.randomUUID().toString();
+        String jsonInput = RecordUtil.createJsonRecordWithAsIngestedCoordinates(1, recordId, KIND, LEGAL_TAG, PERSISTABLE_REFERENCE_CRS, PERSISTABLE_REFERENCE_UNIT_Z, "AnyCrsPolygon", "SpatialLocation");
+        CloseableHttpResponse createResponse = TestUtils.send("records", "PUT", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), jsonInput, "");
+        assertEquals(HttpStatus.SC_CREATED, createResponse.getCode());
+
+        JsonArray records = new JsonArray();
+        records.add(recordId + 0);
+
+        JsonObject body = new JsonObject();
+        body.add("records", records);
+
+        Map<String, String> headers = HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken());
+        headers.put("frame-of-reference", "units=SI;crs=wgs84;elevation=msl;azimuth=true north;dates=utc;");
+        CloseableHttpResponse response = TestUtils.send("query/records:batch", "POST", headers, body.toString(),"");
+        assertEquals(HttpStatus.SC_OK, response.getCode());
+
+        DummyRecordsHelper.ConvertedRecordsMock responseObject = RECORDS_HELPER.getConvertedRecordsMockFromResponse(response);
+        assertEquals(1, responseObject.records.length);
+        assertEquals(0, responseObject.notFound.length);
+        assertEquals(1, responseObject.conversionStatuses.size());
+        assertEquals("SUCCESS", responseObject.conversionStatuses.get(0).status);
+
+        assertEquals(KIND, responseObject.records[0].kind);
+        assertTrue(responseObject.records[0].version != null && !responseObject.records[0].version.isEmpty());
+        assertEquals(1, responseObject.records[0].data.size());
+
+        CloseableHttpResponse deleteResponse1 = TestUtils.send("records/" + recordId + 0, "DELETE", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "", "");
+        assertEquals(HttpStatus.SC_NO_CONTENT, deleteResponse1.getCode());
+    }
+
+   // @Ignore // Ignoring the test for now, once we have CRS converter we should enable this test
+    @Test
+    public void should_returnRecordsAfterCrsConversion__whenProvidedRecordWithAsIngestedCoordinatesBlockTypeMultiPolygon() throws Exception {
+        String recordId = RECORD_ID_PREFIX + UUID.randomUUID().toString();
+        String jsonInput = RecordUtil.createJsonRecordWithAsIngestedCoordinates(1, recordId, KIND, LEGAL_TAG, PERSISTABLE_REFERENCE_CRS, PERSISTABLE_REFERENCE_UNIT_Z, "AnyCrsMultiPolygon", "SpatialLocation");
+        CloseableHttpResponse createResponse = TestUtils.send("records", "PUT", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), jsonInput, "");
+        assertEquals(HttpStatus.SC_CREATED, createResponse.getCode());
+
+        JsonArray records = new JsonArray();
+        records.add(recordId + 0);
+
+        JsonObject body = new JsonObject();
+        body.add("records", records);
+
+        Map<String, String> headers = HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken());
+        headers.put("frame-of-reference", "units=SI;crs=wgs84;elevation=msl;azimuth=true north;dates=utc;");
+        CloseableHttpResponse response = TestUtils.send("query/records:batch", "POST", headers, body.toString(),"");
+        assertEquals(HttpStatus.SC_OK, response.getCode());
+
+        DummyRecordsHelper.ConvertedRecordsMock responseObject = RECORDS_HELPER.getConvertedRecordsMockFromResponse(response);
+        assertEquals(1, responseObject.records.length);
+        assertEquals(0, responseObject.notFound.length);
+        assertEquals(1, responseObject.conversionStatuses.size());
+        assertEquals("SUCCESS", responseObject.conversionStatuses.get(0).status);
+
+        assertEquals(KIND, responseObject.records[0].kind);
+        assertTrue(responseObject.records[0].version != null && !responseObject.records[0].version.isEmpty());
+        assertEquals(1, responseObject.records[0].data.size());
+
+        CloseableHttpResponse deleteResponse1 = TestUtils.send("records/" + recordId + 0, "DELETE", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "", "");
+        assertEquals(HttpStatus.SC_NO_CONTENT, deleteResponse1.getCode());
+    }
+
+   // @Ignore // Ignoring the test for now, once we have CRS converter we should enable this test
+    @Test
+    public void should_returnRecordsAfterCrsConversion__whenProvidedRecordWithAsIngestedCoordinatesBlockTypeLineString() throws Exception {
+        String recordId = RECORD_ID_PREFIX + UUID.randomUUID().toString();
+        String jsonInput = RecordUtil.createJsonRecordWithAsIngestedCoordinates(1, recordId, KIND, LEGAL_TAG, PERSISTABLE_REFERENCE_CRS, PERSISTABLE_REFERENCE_UNIT_Z, "AnyCrsLineString", "SpatialLocation");
+        CloseableHttpResponse createResponse = TestUtils.send("records", "PUT", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), jsonInput, "");
+        assertEquals(HttpStatus.SC_CREATED, createResponse.getCode());
+
+        JsonArray records = new JsonArray();
+        records.add(recordId + 0);
+
+        JsonObject body = new JsonObject();
+        body.add("records", records);
+
+        Map<String, String> headers = HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken());
+        headers.put("frame-of-reference", "units=SI;crs=wgs84;elevation=msl;azimuth=true north;dates=utc;");
+        CloseableHttpResponse response = TestUtils.send("query/records:batch", "POST", headers, body.toString(),"");
+        assertEquals(HttpStatus.SC_OK, response.getCode());
+
+        DummyRecordsHelper.ConvertedRecordsMock responseObject = RECORDS_HELPER.getConvertedRecordsMockFromResponse(response);
+        assertEquals(1, responseObject.records.length);
+        assertEquals(0, responseObject.notFound.length);
+        assertEquals(1, responseObject.conversionStatuses.size());
+        assertEquals("SUCCESS", responseObject.conversionStatuses.get(0).status);
+
+        assertEquals(KIND, responseObject.records[0].kind);
+        assertTrue(responseObject.records[0].version != null && !responseObject.records[0].version.isEmpty());
+        assertEquals(1, responseObject.records[0].data.size());
+
+        CloseableHttpResponse deleteResponse1 = TestUtils.send("records/" + recordId + 0, "DELETE", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "", "");
+        assertEquals(HttpStatus.SC_NO_CONTENT, deleteResponse1.getCode());
+    }
+
+   // @Ignore // Ignoring the test for now, once we have CRS converter we should enable this test
+    @Test
+    public void should_returnRecordsAfterCrsConversion__whenProvidedRecordWithAsIngestedCoordinatesBlockTypeMultiLineString() throws Exception {
+        String recordId = RECORD_ID_PREFIX + UUID.randomUUID().toString();
+        String jsonInput = RecordUtil.createJsonRecordWithAsIngestedCoordinates(1, recordId, KIND, LEGAL_TAG, PERSISTABLE_REFERENCE_CRS, PERSISTABLE_REFERENCE_UNIT_Z, "AnyCrsMultiLineString", "SpatialLocation");
+        CloseableHttpResponse createResponse = TestUtils.send("records", "PUT", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), jsonInput, "");
+        assertEquals(HttpStatus.SC_CREATED, createResponse.getCode());
+
+        JsonArray records = new JsonArray();
+        records.add(recordId + 0);
+
+        JsonObject body = new JsonObject();
+        body.add("records", records);
+
+        Map<String, String> headers = HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken());
+        headers.put("frame-of-reference", "units=SI;crs=wgs84;elevation=msl;azimuth=true north;dates=utc;");
+        CloseableHttpResponse response = TestUtils.send("query/records:batch", "POST", headers, body.toString(),"");
+        assertEquals(HttpStatus.SC_OK, response.getCode());
+
+        DummyRecordsHelper.ConvertedRecordsMock responseObject = RECORDS_HELPER.getConvertedRecordsMockFromResponse(response);
+        assertEquals(1, responseObject.records.length);
+        assertEquals(0, responseObject.notFound.length);
+        assertEquals(1, responseObject.conversionStatuses.size());
+        assertEquals("SUCCESS", responseObject.conversionStatuses.get(0).status);
+
+        assertEquals(KIND, responseObject.records[0].kind);
+        assertTrue(responseObject.records[0].version != null && !responseObject.records[0].version.isEmpty());
+        assertEquals(1, responseObject.records[0].data.size());
+
+        CloseableHttpResponse deleteResponse1 = TestUtils.send("records/" + recordId + 0, "DELETE", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "", "");
+        assertEquals(HttpStatus.SC_NO_CONTENT, deleteResponse1.getCode());
+    }
+
+   // @Ignore // Ignoring the test for now, once we have CRS converter we should enable this test
+    @Test
+    public void should_returnRecordsAfterCrsConversion__whenProvidedRecordWithAsIngestedCoordinatesBlockTypeGeometryCollection() throws Exception {
+        String recordId = RECORD_ID_PREFIX + UUID.randomUUID().toString();
+        String jsonInput = RecordUtil.createJsonRecordWithAsIngestedCoordinates(1, recordId, KIND, LEGAL_TAG, PERSISTABLE_REFERENCE_CRS, PERSISTABLE_REFERENCE_UNIT_Z, "AnyCrsGeometryCollection", "SpatialLocation");
+        CloseableHttpResponse createResponse = TestUtils.send("records", "PUT", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), jsonInput, "");
+        assertEquals(HttpStatus.SC_CREATED, createResponse.getCode());
+
+        JsonArray records = new JsonArray();
+        records.add(recordId + 0);
+
+        JsonObject body = new JsonObject();
+        body.add("records", records);
+
+        Map<String, String> headers = HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken());
+        headers.put("frame-of-reference", "units=SI;crs=wgs84;elevation=msl;azimuth=true north;dates=utc;");
+        CloseableHttpResponse response = TestUtils.send("query/records:batch", "POST", headers, body.toString(),"");
+        assertEquals(HttpStatus.SC_OK, response.getCode());
+
+        DummyRecordsHelper.ConvertedRecordsMock responseObject = RECORDS_HELPER.getConvertedRecordsMockFromResponse(response);
+        assertEquals(1, responseObject.records.length);
+        assertEquals(0, responseObject.notFound.length);
+        assertEquals(1, responseObject.conversionStatuses.size());
+        assertEquals("SUCCESS", responseObject.conversionStatuses.get(0).status);
+
+        assertEquals(KIND, responseObject.records[0].kind);
+        assertTrue(responseObject.records[0].version != null && !responseObject.records[0].version.isEmpty());
+        assertEquals(1, responseObject.records[0].data.size());
+
+        CloseableHttpResponse deleteResponse1 = TestUtils.send("records/" + recordId + 0, "DELETE", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "", "");
+        assertEquals(HttpStatus.SC_NO_CONTENT, deleteResponse1.getCode());
+    }
+
+   // @Ignore // Ignoring the test for now, once we have CRS converter we should enable this test
+    @Test
+    public void should_returnConvertedRecords_whenConversionRequiredWithAsIngestedCoordinatesBlockWithError() throws Exception {
+        String recordId = RECORD_ID_PREFIX + UUID.randomUUID().toString();
+        String jsonInput = RecordUtil.createJsonRecordWithInvalidAsIngestedCoordinates(1, recordId, KIND, LEGAL_TAG, PERSISTABLE_REFERENCE_CRS, PERSISTABLE_REFERENCE_UNIT_Z, "AnyCrsPoint", "SpatialLocation");
+        CloseableHttpResponse createResponse = TestUtils.send("records", "PUT", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), jsonInput, "");
+        assertEquals(HttpStatus.SC_CREATED, createResponse.getCode());
+
+        JsonArray records = new JsonArray();
+        records.add(recordId + 0);
+
+        JsonObject body = new JsonObject();
+        body.add("records", records);
+
+        Map<String, String> headers = HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken());
+        headers.put("frame-of-reference", "units=SI;crs=wgs84;elevation=msl;azimuth=true north;dates=utc;");
+        CloseableHttpResponse response = TestUtils.send("query/records:batch", "POST", headers, body.toString(),"");
+        assertEquals(HttpStatus.SC_OK, response.getCode());
+
+        DummyRecordsHelper.ConvertedRecordsMock responseObject = RECORDS_HELPER.getConvertedRecordsMockFromResponse(response);
+        assertEquals(1, responseObject.records.length);
+        assertEquals(0, responseObject.notFound.length);
+        assertEquals(1, responseObject.conversionStatuses.size());
+        assertEquals("CRS conversion: 'features' missing, no conversion applied.", responseObject.conversionStatuses.get(0).errors.get(0));
+
+        assertEquals(KIND, responseObject.records[0].kind);
+        assertTrue(responseObject.records[0].version != null && !responseObject.records[0].version.isEmpty());
+        assertEquals(1, responseObject.records[0].data.size());
+
+        CloseableHttpResponse deleteResponse1 = TestUtils.send("records/" + recordId + 0, "DELETE", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "", "");
+        assertEquals(HttpStatus.SC_NO_CONTENT, deleteResponse1.getCode());
+    }
+
+   // @Ignore // Ignoring the test for now, once we have CRS converter we should enable this test
+    @Test
+    public void should_returnConvertedRecords_whenConversionNotRequiredWithAsIngestedCoordinatesAndWgs84CoordinatesBlocks() throws Exception {
+        String recordId = RECORD_ID_PREFIX + UUID.randomUUID().toString();
+        String jsonInput = RecordUtil.createJsonRecordWithWGS84Coordinates(1, recordId, KIND, LEGAL_TAG, PERSISTABLE_REFERENCE_CRS, PERSISTABLE_REFERENCE_UNIT_Z, "AnyCrsPoint", "SpatialLocation");
+        CloseableHttpResponse createResponse = TestUtils.send("records", "PUT", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), jsonInput, "");
+        assertEquals(HttpStatus.SC_CREATED, createResponse.getCode());
+
+        JsonArray records = new JsonArray();
+        records.add(recordId + 0);
+
+        JsonObject body = new JsonObject();
+        body.add("records", records);
+
+        Map<String, String> headers = HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken());
+        headers.put("frame-of-reference", "units=SI;crs=wgs84;elevation=msl;azimuth=true north;dates=utc;");
+        CloseableHttpResponse response = TestUtils.send("query/records:batch", "POST", headers, body.toString(),"");
+        assertEquals(HttpStatus.SC_OK, response.getCode());
+
+        DummyRecordsHelper.ConvertedRecordsMock responseObject = RECORDS_HELPER.getConvertedRecordsMockFromResponse(response);
+        assertEquals(1, responseObject.records.length);
+        assertEquals(0, responseObject.notFound.length);
+        assertEquals(1, responseObject.conversionStatuses.size());
+        assertEquals("CRS conversion: 'Wgs84Coordinates' block exists, no conversion applied.", responseObject.conversionStatuses.get(0).errors.get(0));
+
+        assertEquals(KIND, responseObject.records[0].kind);
+        assertTrue(responseObject.records[0].version != null && !responseObject.records[0].version.isEmpty());
+        assertEquals(1, responseObject.records[0].data.size());
+
+        CloseableHttpResponse deleteResponse1 = TestUtils.send("records/" + recordId + 0, "DELETE", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "", "");
+        assertEquals(HttpStatus.SC_NO_CONTENT, deleteResponse1.getCode());
+    }
+}
diff --git a/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/query/PostQueryRecordsIntegrationTests.java b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/query/PostQueryRecordsIntegrationTests.java
new file mode 100644
index 0000000000000000000000000000000000000000..eabc47d653a096d6c7b301822cbc15319a33fda4
--- /dev/null
+++ b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/query/PostQueryRecordsIntegrationTests.java
@@ -0,0 +1,191 @@
+// Copyright 2017-2019, Schlumberger
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package org.opengroup.osdu.storage.query;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonObject;
+import java.util.Arrays;
+import org.apache.commons.lang3.ArrayUtils;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
+import org.apache.http.HttpStatus;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.opengroup.osdu.storage.util.DummyRecordsHelper;
+import org.opengroup.osdu.storage.util.HeaderUtils;
+import org.opengroup.osdu.storage.util.LegalTagUtils;
+import org.opengroup.osdu.storage.util.RecordUtil;
+import org.opengroup.osdu.storage.util.TenantUtils;
+import org.opengroup.osdu.storage.util.TestBase;
+import org.opengroup.osdu.storage.util.TestUtils;
+import org.opengroup.osdu.storage.util.TokenTestUtils;
+
+
+public final class PostQueryRecordsIntegrationTests extends TestBase {
+
+	private static final long NOW = System.currentTimeMillis();
+
+	private static final String RECORD_ID = TenantUtils.getTenantName() + ":query:" + NOW;
+	private static final String KIND = TenantUtils.getTenantName() + ":ds:query:1.0." + NOW;
+	private static final String LEGAL_TAG = LegalTagUtils.createRandomName();
+	private static final DummyRecordsHelper RECORDS_HELPER = new DummyRecordsHelper();
+	private static final TokenTestUtils TOKEN_TEST_UTILS = new TokenTestUtils();
+
+	@BeforeAll
+	public static void classSetup() throws Exception {
+		PostQueryRecordsIntegrationTests.classSetup(TOKEN_TEST_UTILS.getToken());
+	}
+
+	@AfterAll
+	public static void classTearDown() throws Exception {
+		PostQueryRecordsIntegrationTests.classTearDown(TOKEN_TEST_UTILS.getToken());
+	}
+
+	@BeforeEach
+	@Override
+	public void setup() throws Exception {
+		this.testUtils = new TokenTestUtils();
+	}
+
+	@AfterEach
+	@Override
+	public void tearDown() throws Exception {
+		this.testUtils = null;
+	}
+
+	public static void classSetup(String token) throws Exception {
+		LegalTagUtils.create(LEGAL_TAG, token);
+		String jsonInput = RecordUtil.createDefaultJsonRecords(3, RECORD_ID, KIND, LEGAL_TAG);
+
+		CloseableHttpResponse response = TestUtils.send("records", "PUT", HeaderUtils.getHeaders(TenantUtils.getTenantName(), token), jsonInput, "");
+		CloseableHttpResponse modifyRecordsResponse = TestUtils.send("records", "PUT", HeaderUtils.getHeaders(TenantUtils.getTenantName(), token), jsonInput, "");
+		assertEquals(HttpStatus.SC_CREATED, response.getCode());
+		assertEquals(HttpStatus.SC_CREATED, modifyRecordsResponse.getCode());
+	}
+
+	public static void classTearDown(String token) throws Exception {
+		TestUtils.send("records/" + RECORD_ID + 0, "DELETE", HeaderUtils.getHeaders(TenantUtils.getTenantName(), token), "", "");
+		TestUtils.send("records/" + RECORD_ID + 1, "DELETE", HeaderUtils.getHeaders(TenantUtils.getTenantName(), token), "", "");
+		TestUtils.send("records/" + RECORD_ID + 2, "DELETE", HeaderUtils.getHeaders(TenantUtils.getTenantName(), token), "", "");
+
+		LegalTagUtils.delete(LEGAL_TAG, token);
+	}
+
+	@Test
+	public void should_returnSingleRecordMatching_when_givenIdAndNoAttributes() throws Exception {
+		JsonArray attributes = new JsonArray();
+		JsonArray records = new JsonArray();
+		records.add(RECORD_ID + 0);
+
+		JsonObject body = new JsonObject();
+		body.add("records", records);
+		body.add("attributes", attributes);
+
+		CloseableHttpResponse response = TestUtils.send("query/records", "POST", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), body.toString(),
+				"");
+		assertEquals(HttpStatus.SC_OK, response.getCode());
+
+		DummyRecordsHelper.RecordsMock responseObject = RECORDS_HELPER.getRecordsMockFromResponse(response);
+
+		assertEquals(1, responseObject.records.length);
+		assertEquals(0, responseObject.invalidRecords.length);
+		assertEquals(0, responseObject.retryRecords.length);
+
+		assertEquals(TestUtils.getAcl(), responseObject.records[0].acl.viewers[0]);
+		assertEquals(RECORD_ID + 0, responseObject.records[0].id);
+		assertEquals(KIND, responseObject.records[0].kind);
+		assertTrue(responseObject.records[0].createUser != null && responseObject.records[0].createTime != null);
+		assertTrue(responseObject.records[0].modifyUser != null && responseObject.records[0].modifyTime != null);
+		assertTrue(responseObject.records[0].version != null && !responseObject.records[0].version.isEmpty());
+		assertEquals(3, responseObject.records[0].data.size());
+	}
+
+	@Test
+	public void should_returnOnlyRequestedDataProperties_when_specificAttributesAreGiven() throws Exception {
+		JsonArray attributes = new JsonArray();
+		attributes.add("data.count");
+		JsonArray records = new JsonArray();
+		records.add(RECORD_ID + 1);
+
+		JsonObject body = new JsonObject();
+		body.add("records", records);
+		body.add("attributes", attributes);
+
+		CloseableHttpResponse response = TestUtils.send("query/records", "POST", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), body.toString(),
+				"");
+		assertEquals(HttpStatus.SC_OK, response.getCode());
+
+		DummyRecordsHelper.RecordsMock responseObject = RECORDS_HELPER.getRecordsMockFromResponse(response);
+
+		assertEquals(1, responseObject.records[0].data.size());
+		assertEquals("1.23456789E8", responseObject.records[0].data.get("count").toString());
+	}
+
+	@Test
+	public void should_returnMultipleRecordsMatchingGivenIds_when_noAttributesAreGiven() throws Exception {
+		JsonArray attributes = new JsonArray();
+		JsonArray records = new JsonArray();
+		records.add(RECORD_ID + 0);
+		records.add(RECORD_ID + 1);
+		records.add(RECORD_ID + 2);
+
+		JsonObject body = new JsonObject();
+		body.add("records", records);
+		body.add("attributes", attributes);
+
+		CloseableHttpResponse response = TestUtils.send("query/records", "POST", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), body.toString(),
+				"");
+		assertEquals(HttpStatus.SC_OK, response.getCode());
+
+		DummyRecordsHelper.RecordsMock responseObject = RECORDS_HELPER.getRecordsMockFromResponse(response);
+
+		assertEquals(3, responseObject.records.length);
+		assertEquals(0, responseObject.invalidRecords.length);
+		assertEquals(0, responseObject.retryRecords.length);
+
+		String[] ids = ArrayUtils.addAll(new String[] { responseObject.records[0].id }, responseObject.records[1].id,
+				responseObject.records[2].id);
+		assertTrue(Arrays.asList(ids).contains(RECORD_ID + 0));
+		assertTrue(Arrays.asList(ids).contains(RECORD_ID + 1));
+		assertTrue(Arrays.asList(ids).contains(RECORD_ID + 2));
+	}
+
+	@Test
+	public void should_returnInvalidRecord_when_nonExistingIDGiven() throws Exception {
+		JsonArray attributes = new JsonArray();
+		JsonArray records = new JsonArray();
+		records.add("nonexisting:id");
+
+		JsonObject body = new JsonObject();
+		body.add("records", records);
+		body.add("attributes", attributes);
+
+		CloseableHttpResponse response = TestUtils.send("query/records", "POST", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), body.toString(),
+				"");
+		assertEquals(HttpStatus.SC_OK, response.getCode());
+
+		DummyRecordsHelper.RecordsMock responseObject = RECORDS_HELPER.getRecordsMockFromResponse(response);
+
+		assertEquals(0, responseObject.records.length);
+		assertEquals(1, responseObject.invalidRecords.length);
+		assertEquals("nonexisting:id", responseObject.invalidRecords[0]);
+		assertEquals(0, responseObject.retryRecords.length);
+	}
+}
diff --git a/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/query/StorageQuerySuccessfulTest.java b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/query/StorageQuerySuccessfulTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..63725e18c23dd9141eeedcf332411e4735084c44
--- /dev/null
+++ b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/query/StorageQuerySuccessfulTest.java
@@ -0,0 +1,150 @@
+// Copyright 2017-2019, Schlumberger
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package org.opengroup.osdu.storage.query;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonObject;
+import jakarta.ws.rs.HttpMethod;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
+import org.apache.http.HttpStatus;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.opengroup.osdu.storage.model.CreatedRecordInStorage;
+import org.opengroup.osdu.storage.model.GetCursorValue;
+import org.opengroup.osdu.storage.util.HeaderUtils;
+import org.opengroup.osdu.storage.util.LegalTagUtils;
+import org.opengroup.osdu.storage.util.RecordUtil;
+import org.opengroup.osdu.storage.util.TenantUtils;
+import org.opengroup.osdu.storage.util.TestBase;
+import org.opengroup.osdu.storage.util.TestUtils;
+import org.opengroup.osdu.storage.util.TokenTestUtils;
+
+
+public final class StorageQuerySuccessfulTest extends TestBase {
+
+	private static final String RECORD = "records";
+	private static final String KIND_ONE = TenantUtils.getTenantName() + ":test:endtoend:1.1."
+			+ System.currentTimeMillis();
+	private static final String KIND_ID_ONE = TenantUtils.getTenantName() + ":endtoend:1.1."
+			+ System.currentTimeMillis();
+	private static final String KIND_VERSION_ID = TenantUtils.getTenantName() + ":endtoend:1.2."
+			+ System.currentTimeMillis();
+	private static final String LEGAL_TAG_NAME = LegalTagUtils.createRandomName();
+
+	private static final TokenTestUtils TOKEN_TEST_UTILS = new TokenTestUtils();
+
+	@BeforeAll
+	public static void classSetup() throws Exception {
+		StorageQuerySuccessfulTest.classSetup(TOKEN_TEST_UTILS.getToken());
+	}
+
+	@AfterAll
+	public static void classTearDown() throws Exception {
+		StorageQuerySuccessfulTest.classTearDown(TOKEN_TEST_UTILS.getToken());
+	}
+
+	@BeforeEach
+	@Override
+	public void setup() throws Exception {
+		this.testUtils = new TokenTestUtils();
+	}
+
+	@AfterEach
+	@Override
+	public void tearDown() throws Exception {
+		this.testUtils = null;
+	}
+
+    public static void classSetup(String token) throws Exception {
+        LegalTagUtils.create(LEGAL_TAG_NAME, token);
+    }
+
+    public static void classTearDown(String token) throws Exception {
+        TestUtils.send(RECORD + "/" + KIND_ID_ONE, HttpMethod.DELETE, HeaderUtils.getHeaders(TenantUtils.getTenantName(), token), "", "");
+        TestUtils.send(RECORD + "/" + KIND_VERSION_ID, HttpMethod.DELETE, HeaderUtils.getHeaders(TenantUtils.getTenantName(), token), "", "");
+
+    	LegalTagUtils.delete(LEGAL_TAG_NAME, token);
+    }
+	
+	@Test
+	public void should_retrieveAllKinds_when_toCursorIdIsGiven() throws Exception {
+		if (configUtils != null && configUtils.getIsSchemaEndpointsEnabled()) {
+			CloseableHttpResponse recordResponse = TestUtils.send("query/kinds?limit=10", HttpMethod.GET, HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()),
+					"", "");
+			GetCursorValue getCursorValue = TestUtils.getResult(recordResponse, HttpStatus.SC_OK, GetCursorValue.class);
+			String cursorValue = getCursorValue.getCursor();
+			assertEquals(HttpStatus.SC_OK, recordResponse.getCode());
+			assertEquals(cursorValue, getCursorValue.getCursor());
+			CloseableHttpResponse recordResponseWithCursorValue = TestUtils.send("query/kinds?cursor=" + cursorValue + "&limit=10",
+					HttpMethod.GET, HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "", "");
+			assertEquals(HttpStatus.SC_OK, recordResponseWithCursorValue.getCode());
+		}
+	}
+
+	@Test
+	public void should_retrieveAllRecords_when_kindIsGiven() throws Exception {
+		CloseableHttpResponse recordResponse = createTestRecord(KIND_ONE, KIND_ID_ONE, LEGAL_TAG_NAME);
+		assertEquals(HttpStatus.SC_CREATED, recordResponse.getCode());
+		CloseableHttpResponse recordResponseGet = TestUtils.send("query/records?kind=" + KIND_ONE, HttpMethod.GET,
+				HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "", "");
+		assertEquals(HttpStatus.SC_OK, recordResponseGet.getCode());
+	}
+
+	@Test
+	public void should_queryToFetchMultipleRecords_when_recordIsGiven() throws Exception {
+		CloseableHttpResponse recordResponse = createTestRecord(KIND_ONE, KIND_ID_ONE, LEGAL_TAG_NAME);
+		CreatedRecordInStorage recordResult = TestUtils.getResult(recordResponse, HttpStatus.SC_CREATED,
+				CreatedRecordInStorage.class);
+		JsonArray recordIDS = new JsonArray();
+		recordIDS.add(recordResult.recordIds[0]);
+		JsonArray attribute = new JsonArray();
+		attribute.add("");
+		JsonObject createSearchRecordPayload = new JsonObject();
+		createSearchRecordPayload.add("records", recordIDS);
+		createSearchRecordPayload.add("attributes", attribute);
+		String path = "query/records";
+		CloseableHttpResponse recordResponsePost = TestUtils.send(path, HttpMethod.POST, HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()),
+				createSearchRecordPayload.toString(), "");
+		assertEquals(HttpStatus.SC_OK, recordResponsePost.getCode());
+	}
+
+	@Test
+	public void should_queryToFetchMultipleRecords_when_recordIsGiven_and_trailingSlash() throws Exception {
+		CloseableHttpResponse recordResponse = createTestRecord(KIND_ONE, KIND_ID_ONE, LEGAL_TAG_NAME);
+		CreatedRecordInStorage recordResult = TestUtils.getResult(recordResponse, HttpStatus.SC_CREATED,
+				CreatedRecordInStorage.class);
+		JsonArray recordIDS = new JsonArray();
+		recordIDS.add(recordResult.recordIds[0]);
+		JsonArray attribute = new JsonArray();
+		attribute.add("");
+		JsonObject createSearchRecordPayload = new JsonObject();
+		createSearchRecordPayload.add("records", recordIDS);
+		createSearchRecordPayload.add("attributes", attribute);
+		String path = "query/records/";
+		CloseableHttpResponse recordResponsePost = TestUtils.send(path, HttpMethod.POST, HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()),
+				createSearchRecordPayload.toString(), "");
+		assertEquals(HttpStatus.SC_OK, recordResponsePost.getCode());
+	}
+
+	private CloseableHttpResponse createTestRecord(String kind, String id, String legalName) throws Exception {
+		String jsonInputRecord = RecordUtil.createDefaultJsonRecord(id, kind, legalName);
+		return TestUtils.send(RECORD, HttpMethod.PUT, HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), jsonInputRecord, "");
+	}
+}
diff --git a/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/records/CollaborationRecordsPurgeTest.java b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/records/CollaborationRecordsPurgeTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..f13b11762971c572ab8c736255c3ef0cec28279d
--- /dev/null
+++ b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/records/CollaborationRecordsPurgeTest.java
@@ -0,0 +1,137 @@
+/*
+ *  Copyright 2020-2024 Google LLC
+ *  Copyright 2020-2024 EPAM Systems, Inc
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+package org.opengroup.osdu.storage.records;
+
+import static org.apache.http.HttpStatus.SC_CREATED;
+import static org.apache.http.HttpStatus.SC_NOT_FOUND;
+import static org.apache.http.HttpStatus.SC_NO_CONTENT;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.google.api.client.util.Strings;
+import java.io.IOException;
+import java.util.Map;
+import java.util.UUID;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
+import org.apache.hc.core5.http.ParseException;
+import org.apache.hc.core5.http.io.entity.EntityUtils;
+import org.apache.http.HttpStatus;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+import org.opengroup.osdu.storage.util.ConfigUtils;
+import org.opengroup.osdu.storage.util.DummyRecordsHelper;
+import org.opengroup.osdu.storage.util.HeaderUtils;
+import org.opengroup.osdu.storage.util.LegalTagUtils;
+import org.opengroup.osdu.storage.util.RecordUtil;
+import org.opengroup.osdu.storage.util.TenantUtils;
+import org.opengroup.osdu.storage.util.TestBase;
+import org.opengroup.osdu.storage.util.TestUtils;
+import org.opengroup.osdu.storage.util.TokenTestUtils;
+
+public final class CollaborationRecordsPurgeTest extends TestBase {
+
+    static final String COLLABORATION1_ID = UUID.randomUUID().toString();
+
+    private static boolean isCollaborationEnabled = false;
+    private static final String COLLABORATION_HEADER = "x-collaboration";
+    private static final String APPLICATION_NAME = "storage service integration test";
+    private static final String TENANT_NAME = TenantUtils.getTenantName();
+    private static final long CURRENT_TIME_MILLIS = System.currentTimeMillis();
+    private static final String RECORD_PURGE_ID = TENANT_NAME + ":inttestpurge:1" + CURRENT_TIME_MILLIS;
+    private static final String COLLABORATION2_ID = UUID.randomUUID().toString();
+    private static final String KIND1 = TENANT_NAME + ":ds:inttest:1" + CURRENT_TIME_MILLIS;
+    private static Long RECORD_PURGE_V1;
+    private static Long RECORD_PURGE_V2;
+    private static Long RECORD_PURGE_V3;
+    private static String LEGAL_TAG_NAME_A;
+
+    @Override
+    public void setup() throws Exception {
+        this.testUtils = new TokenTestUtils();
+        this.configUtils = new ConfigUtils("test.properties");
+
+        if (configUtils != null && !configUtils.getIsCollaborationEnabled()) {
+            return;
+        }
+        isCollaborationEnabled = true;
+        LEGAL_TAG_NAME_A = LegalTagUtils.createRandomName();
+        LegalTagUtils.create(LEGAL_TAG_NAME_A, testUtils.getToken());
+
+        RECORD_PURGE_V1 = createRecord(RECORD_PURGE_ID, COLLABORATION1_ID, KIND1, testUtils.getToken());
+        RECORD_PURGE_V2 = createRecord(RECORD_PURGE_ID, COLLABORATION1_ID, KIND1, testUtils.getToken());
+        RECORD_PURGE_V3 = createRecord(RECORD_PURGE_ID, COLLABORATION2_ID, KIND1, testUtils.getToken());
+    }
+
+    @AfterEach
+    public void tearDown() throws Exception {
+        if (!isCollaborationEnabled) return;
+        TestUtils.send("records/" + RECORD_PURGE_ID, "DELETE", getHeadersWithxCollaboration(COLLABORATION1_ID, testUtils.getToken()), "", "");
+        TestUtils.send("records/" + RECORD_PURGE_ID, "DELETE", getHeadersWithxCollaboration(COLLABORATION2_ID, testUtils.getToken()), "", "");
+        LegalTagUtils.delete(LEGAL_TAG_NAME_A, testUtils.getToken());
+
+        this.testUtils = null;
+        this.configUtils = null;
+    }
+
+    @Test
+    public void should_purgeAllRecordVersionsOnlyInCollaborationContext() throws Exception {
+        if (!isCollaborationEnabled) return;
+        CloseableHttpResponse response = TestUtils.send("records/" + RECORD_PURGE_ID, "DELETE", getHeadersWithxCollaboration(COLLABORATION1_ID, testUtils.getToken()), "", "");
+        assertEquals(SC_NO_CONTENT, response.getCode());
+        response = TestUtils.send("records/" + RECORD_PURGE_ID, "GET", getHeadersWithxCollaboration(COLLABORATION1_ID, testUtils.getToken()), "", "");
+        assertEquals(SC_NOT_FOUND, response.getCode());
+        response = TestUtils.send("records/" + RECORD_PURGE_ID, "GET", getHeadersWithxCollaboration(COLLABORATION2_ID, testUtils.getToken()), "", "");
+        assertRecordVersion(response, RECORD_PURGE_V3);
+    }
+
+    private static Long createRecord(String recordId, String collaborationId, String kind, String token) throws Exception {
+        String jsonInput = RecordUtil.createDefaultJsonRecord(recordId, kind, LEGAL_TAG_NAME_A);
+
+        CloseableHttpResponse response = TestUtils.send("records", "PUT", getHeadersWithxCollaboration(collaborationId, token), jsonInput, "");
+        assertEquals(SC_CREATED, response.getCode());
+        assertTrue(response.getEntity().getContentType().contains("application/json"));
+
+        String responseBody = EntityUtils.toString(response.getEntity());
+        DummyRecordsHelper.CreateRecordResponse result = GSON.fromJson(responseBody, DummyRecordsHelper.CreateRecordResponse.class);
+
+        return Long.parseLong(result.recordIdVersions[0].split(":")[3]);
+    }
+
+    private static Map<String, String> getHeadersWithxCollaboration(String collaborationId, String token) {
+        Map<String, String> headers = HeaderUtils.getHeaders(TENANT_NAME, token);
+        if (!Strings.isNullOrEmpty(collaborationId)) {
+            headers.put(COLLABORATION_HEADER, "id=" + collaborationId + ",application=" + APPLICATION_NAME);
+        }
+        return headers;
+    }
+
+    private static void assertRecordVersion(CloseableHttpResponse response, Long expectedVersion) {
+        assertEquals(HttpStatus.SC_OK, response.getCode());
+
+        String responseBody = null;
+        try {
+            responseBody = EntityUtils.toString(response.getEntity());
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        } catch (ParseException e) {
+            throw new RuntimeException(e);
+        }
+        DummyRecordsHelper.RecordResultMock result = GSON.fromJson(responseBody, DummyRecordsHelper.RecordResultMock.class);
+        assertEquals(expectedVersion.longValue(), Long.parseLong(result.version));
+    }
+}
diff --git a/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/records/CollaborationRecordsRetrieveTest.java b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/records/CollaborationRecordsRetrieveTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..faaa079bc2669af630653e5ce06838a4a7860618
--- /dev/null
+++ b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/records/CollaborationRecordsRetrieveTest.java
@@ -0,0 +1,263 @@
+// Copyright 2017-2019, Schlumberger
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package org.opengroup.osdu.storage.records;
+
+import static org.apache.http.HttpStatus.SC_OK;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+import static org.opengroup.osdu.storage.util.HeaderUtils.getHeadersWithxCollaboration;
+import static org.opengroup.osdu.storage.util.TestUtils.assertRecordVersion;
+import static org.opengroup.osdu.storage.util.TestUtils.createRecordInCollaborationContext_AndReturnVersion;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonObject;
+import java.util.Arrays;
+import java.util.List;
+import java.util.UUID;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
+import org.apache.http.HttpStatus;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+import org.opengroup.osdu.storage.util.ConfigUtils;
+import org.opengroup.osdu.storage.util.DummyRecordsHelper;
+import org.opengroup.osdu.storage.util.LegalTagUtils;
+import org.opengroup.osdu.storage.util.TenantUtils;
+import org.opengroup.osdu.storage.util.TestBase;
+import org.opengroup.osdu.storage.util.TestUtils;
+import org.opengroup.osdu.storage.util.TokenTestUtils;
+
+public final class CollaborationRecordsRetrieveTest extends TestBase {
+
+    private static boolean isCollaborationEnabled = false;
+    private static final DummyRecordsHelper RECORDS_HELPER = new DummyRecordsHelper();
+    private static final String APPLICATION_NAME = "storage service integration test";
+    private static final String TENANT_NAME = TenantUtils.getTenantName();
+    private static final long CURRENT_TIME_MILLIS = System.currentTimeMillis();
+    private static final String COLLABORATION1_ID = UUID.randomUUID().toString();
+    private static final String COLLABORATION2_ID = UUID.randomUUID().toString();
+    private static final String RECORD_ID_1 = TENANT_NAME + ":inttest:1" + CURRENT_TIME_MILLIS;
+    private static final String RECORD_ID_2 = TENANT_NAME + ":inttest:2" + CURRENT_TIME_MILLIS;
+    private static final String RECORD_ID_3 = TENANT_NAME + ":inttest:3" + CURRENT_TIME_MILLIS;
+    private static final String KIND1 = TENANT_NAME + ":ds:inttest:1" + CURRENT_TIME_MILLIS;
+    private static final String KIND2 = TENANT_NAME + ":ds:inttest:2" + CURRENT_TIME_MILLIS;
+    private static final String KIND3 = TENANT_NAME + ":ds:inttest:3" + CURRENT_TIME_MILLIS;
+    private static Long RECORD1_V1;
+    private static Long RECORD1_V2;
+    private static Long RECORD1_V3;
+    private static Long RECORD1_V4;
+    private static Long RECORD2_V1;
+    private static Long RECORD2_V2;
+    private static Long RECORD3_V1;
+    private static Long RECORD3_V2;
+    private static String LEGAL_TAG_NAME_A;
+
+    @Override
+    public void setup() throws Exception {
+        this.testUtils = new TokenTestUtils();
+        this.configUtils = new ConfigUtils("test.properties");
+
+        if (configUtils != null && !configUtils.getIsCollaborationEnabled()) {
+            return;
+        }
+        isCollaborationEnabled = true;
+        LEGAL_TAG_NAME_A = LegalTagUtils.createRandomName();
+        LegalTagUtils.create(LEGAL_TAG_NAME_A, testUtils.getToken());
+
+        RECORD1_V1 = createRecordInCollaborationContext_AndReturnVersion(RECORD_ID_1, KIND1, LEGAL_TAG_NAME_A, null, APPLICATION_NAME, TENANT_NAME, testUtils.getToken());
+        RECORD1_V2 = createRecordInCollaborationContext_AndReturnVersion(RECORD_ID_1, KIND1, LEGAL_TAG_NAME_A, COLLABORATION1_ID, APPLICATION_NAME, TENANT_NAME, testUtils.getToken());
+        RECORD1_V3 = createRecordInCollaborationContext_AndReturnVersion(RECORD_ID_1, KIND1, LEGAL_TAG_NAME_A, COLLABORATION1_ID, APPLICATION_NAME, TENANT_NAME, testUtils.getToken());
+        RECORD1_V4 = createRecordInCollaborationContext_AndReturnVersion(RECORD_ID_1, KIND1, LEGAL_TAG_NAME_A, COLLABORATION2_ID, APPLICATION_NAME, TENANT_NAME, testUtils.getToken());
+
+        RECORD2_V1 = createRecordInCollaborationContext_AndReturnVersion(RECORD_ID_2, KIND1, LEGAL_TAG_NAME_A, null, APPLICATION_NAME, TENANT_NAME, testUtils.getToken());
+        RECORD2_V2 = createRecordInCollaborationContext_AndReturnVersion(RECORD_ID_2, KIND1, LEGAL_TAG_NAME_A, COLLABORATION2_ID, APPLICATION_NAME, TENANT_NAME, testUtils.getToken());
+
+        RECORD3_V1 = createRecordInCollaborationContext_AndReturnVersion(RECORD_ID_3, KIND2, LEGAL_TAG_NAME_A, COLLABORATION1_ID, APPLICATION_NAME, TENANT_NAME, testUtils.getToken());
+        RECORD3_V2 = createRecordInCollaborationContext_AndReturnVersion(RECORD_ID_3, KIND2, LEGAL_TAG_NAME_A, COLLABORATION2_ID, APPLICATION_NAME, TENANT_NAME, testUtils.getToken());
+    }
+
+    @AfterEach
+    public void tearDown() throws Exception {
+        if (!isCollaborationEnabled) return;
+        TestUtils.send("records/" + RECORD_ID_1, "DELETE", getHeadersWithxCollaboration(null, APPLICATION_NAME, TENANT_NAME, testUtils.getToken()), "", "");
+        TestUtils.send("records/" + RECORD_ID_1, "DELETE", getHeadersWithxCollaboration(COLLABORATION1_ID, APPLICATION_NAME, TENANT_NAME, testUtils.getToken()), "", "");
+        TestUtils.send("records/" + RECORD_ID_1, "DELETE", getHeadersWithxCollaboration(COLLABORATION2_ID, APPLICATION_NAME, TENANT_NAME, testUtils.getToken()), "", "");
+        TestUtils.send("records/" + RECORD_ID_2, "DELETE", getHeadersWithxCollaboration(null, APPLICATION_NAME, TENANT_NAME, testUtils.getToken()), "", "");
+        TestUtils.send("records/" + RECORD_ID_2, "DELETE", getHeadersWithxCollaboration(COLLABORATION2_ID, APPLICATION_NAME, TENANT_NAME, testUtils.getToken()), "", "");
+        TestUtils.send("records/" + RECORD_ID_3, "DELETE", getHeadersWithxCollaboration(COLLABORATION1_ID, APPLICATION_NAME, TENANT_NAME, testUtils.getToken()), "", "");
+        TestUtils.send("records/" + RECORD_ID_3, "DELETE", getHeadersWithxCollaboration(COLLABORATION2_ID, APPLICATION_NAME, TENANT_NAME, testUtils.getToken()), "", "");
+        LegalTagUtils.delete(LEGAL_TAG_NAME_A, testUtils.getToken());
+
+        this.testUtils = null;
+        this.configUtils = null;
+    }
+
+    @Test
+    public void should_getLatestVersion_when_validRecordIdAndCollaborationIdAreProvided() throws Exception {
+        if (!isCollaborationEnabled) return;
+        //get record1 --> v1
+        CloseableHttpResponse response = TestUtils.send("records/" + RECORD_ID_1, "GET", getHeadersWithxCollaboration(null, APPLICATION_NAME, TENANT_NAME, testUtils.getToken()), "", "");
+        assertRecordVersion(response, RECORD1_V1);
+        //get record1 with guid1 --> v3
+        response = TestUtils.send("records/" + RECORD_ID_1, "GET", getHeadersWithxCollaboration(COLLABORATION1_ID, APPLICATION_NAME, TENANT_NAME, testUtils.getToken()), "", "");
+        assertRecordVersion(response, RECORD1_V3);
+        //get record1 with guid2 --> v4
+        response = TestUtils.send("records/" + RECORD_ID_1, "GET", getHeadersWithxCollaboration(COLLABORATION2_ID, APPLICATION_NAME, TENANT_NAME, testUtils.getToken()), "", "");
+        assertRecordVersion(response, RECORD1_V4);
+        //get record2 with guid1 --> 404
+        response = TestUtils.send("records/" + RECORD_ID_2, "GET", getHeadersWithxCollaboration(COLLABORATION1_ID, APPLICATION_NAME, TENANT_NAME, testUtils.getToken()), "", "");
+        assertEquals(HttpStatus.SC_NOT_FOUND, response.getCode());
+    }
+
+    @Test
+    public void should_getCorrectRecordVersion_when_validRecordIdAndCollaborationIdAndRecordVersionAreProvided() throws Exception {
+        if (!isCollaborationEnabled) return;
+        //get record1 with v2 with context guid1
+        CloseableHttpResponse response = TestUtils.send("records/" + RECORD_ID_1 + "/" + RECORD1_V2, "GET", getHeadersWithxCollaboration(COLLABORATION1_ID, APPLICATION_NAME, TENANT_NAME, testUtils.getToken()), "", "");
+        assertRecordVersion(response, RECORD1_V2);
+        //get 404 for record1 with v2 with context guid2
+        response = TestUtils.send("records/" + RECORD_ID_1 + "/" + RECORD1_V2, "GET", getHeadersWithxCollaboration(COLLABORATION2_ID, APPLICATION_NAME, TENANT_NAME, testUtils.getToken()), "", "");
+        assertEquals(HttpStatus.SC_NOT_FOUND, response.getCode());
+    }
+
+    @Test
+    public void should_getAllRecordVersions_when_validRecordIdAndCollaborationIdAreProvided() throws Exception {
+        if (!isCollaborationEnabled) return;
+        //I will get only v1 for record1 with no context
+        CloseableHttpResponse response = TestUtils.send("records/versions/" + RECORD_ID_1, "GET", getHeadersWithxCollaboration(null, APPLICATION_NAME, TENANT_NAME, testUtils.getToken()), "", "");
+        RecordsApiAcceptanceTests.GetVersionsResponse versionsResponse = TestUtils.getResult(response, HttpStatus.SC_OK, RecordsApiAcceptanceTests.GetVersionsResponse.class);
+        assertEquals(1, versionsResponse.versions.length);
+        assertEquals(RECORD1_V1, versionsResponse.versions[0]);
+
+        //I will get v2 and v3 for record1 with context guid1
+        response = TestUtils.send("records/versions/" + RECORD_ID_1, "GET", getHeadersWithxCollaboration(COLLABORATION1_ID, APPLICATION_NAME, TENANT_NAME, testUtils.getToken()), "", "");
+        versionsResponse = TestUtils.getResult(response, HttpStatus.SC_OK, RecordsApiAcceptanceTests.GetVersionsResponse.class);
+        assertEquals(2, versionsResponse.versions.length);
+        List<Long> versions = Arrays.asList(versionsResponse.versions);
+        assertTrue(versions.contains(RECORD1_V2));
+        assertTrue(versions.contains(RECORD1_V3));
+    }
+
+    @Test
+    public void should_getRecordsOnlyInCollaborationContext_whenQueryByKind() throws Exception {
+        if (!isCollaborationEnabled) return;
+        CloseableHttpResponse response = TestUtils.send("query/records", "GET", getHeadersWithxCollaboration(COLLABORATION2_ID, APPLICATION_NAME, TENANT_NAME, testUtils.getToken()), "", "?kind=" + KIND1);
+        assertEquals(SC_OK, response.getCode());
+        DummyRecordsHelper.QueryResultMock responseObject = RECORDS_HELPER.getQueryResultMockFromResponse(response);
+        assertEquals(2, responseObject.results.length);
+        assertTrue(Arrays.stream(responseObject.results).anyMatch(RECORD_ID_1::equals));
+        assertTrue(Arrays.stream(responseObject.results).anyMatch(RECORD_ID_2::equals));
+
+        response = TestUtils.send("query/records", "GET", getHeadersWithxCollaboration(COLLABORATION1_ID, APPLICATION_NAME, TENANT_NAME, testUtils.getToken()), "", "?kind=" + KIND1);
+        assertEquals(SC_OK, response.getCode());
+        responseObject = RECORDS_HELPER.getQueryResultMockFromResponse(response);
+        assertEquals(1, responseObject.results.length);
+        assertTrue(Arrays.stream(responseObject.results).anyMatch(RECORD_ID_1::equals));
+
+        response = TestUtils.send("query/records", "GET", getHeadersWithxCollaboration(COLLABORATION1_ID, APPLICATION_NAME, TENANT_NAME, testUtils.getToken()), "", "?kind=" + KIND3);
+        assertEquals(SC_OK, response.getCode());
+        responseObject = RECORDS_HELPER.getQueryResultMockFromResponse(response);
+        assertEquals(0, responseObject.results.length);
+    }
+
+    @Test
+    public void should_getEmptyRecordsInNoCollaborationContext_whenQueryByKind() throws Exception {
+        if (!isCollaborationEnabled) return;
+        CloseableHttpResponse response = TestUtils.send("query/records", "GET", getHeadersWithxCollaboration(null, null, TENANT_NAME, testUtils.getToken()), "", "?kind=" + KIND2);
+        assertEquals(SC_OK, response.getCode());
+        DummyRecordsHelper.QueryResultMock responseObject = RECORDS_HELPER.getQueryResultMockFromResponse(response);
+        assertEquals(0, responseObject.results.length);
+    }
+
+    @Test
+    public void should_fetchCorrectRecords_when_validRecordIdsAndCollaborationIdAreProvided() throws Exception {
+        if (!isCollaborationEnabled) return;
+        //If I fetch records 1, 2,and 3 in context guid1,I should get a 200 with records 1 and 3
+        JsonArray records = new JsonArray();
+        records.add(RECORD_ID_1);
+        records.add(RECORD_ID_2);
+        records.add(RECORD_ID_3);
+        JsonObject body = new JsonObject();
+        body.add("records", records);
+        CloseableHttpResponse response = TestUtils.send("query/records:batch", "POST", getHeadersWithxCollaboration(COLLABORATION1_ID, APPLICATION_NAME, TENANT_NAME, testUtils.getToken()), body.toString(), "");
+        assertEquals(HttpStatus.SC_OK, response.getCode());
+
+        DummyRecordsHelper.ConvertedRecordsMock responseObject = RECORDS_HELPER.getConvertedRecordsMockFromResponse(response);
+        assertEquals(2, responseObject.records.length);
+        assertEquals(1, responseObject.notFound.length);
+        assertEquals(0, responseObject.conversionStatuses.size());
+        for (DummyRecordsHelper.RecordResultMock record : responseObject.records) {
+            if (record.id.equals(RECORD_ID_1)) assertEquals(RECORD1_V3, Long.valueOf(record.version));
+            else if (record.id.equals(RECORD_ID_2)) fail("should not contain record 2: " + RECORD_ID_2);
+            else if (record.id.equals(RECORD_ID_3)) assertEquals(RECORD3_V1, Long.valueOf(record.version));
+            else fail(String.format("should only contain record 1 %s, and record 3 %s", RECORD_ID_1, RECORD_ID_3));
+        }
+
+        // If I fetch records 1, 2, and 3 in no context, I should get a 200 with records 1 and 2
+        response = TestUtils.send("query/records:batch", "POST", getHeadersWithxCollaboration(null, APPLICATION_NAME, TENANT_NAME, testUtils.getToken()), body.toString(), "");
+        assertEquals(HttpStatus.SC_OK, response.getCode());
+
+        responseObject = RECORDS_HELPER.getConvertedRecordsMockFromResponse(response);
+        assertEquals(2, responseObject.records.length);
+        assertEquals(1, responseObject.notFound.length);
+        assertEquals(0, responseObject.conversionStatuses.size());
+        for (DummyRecordsHelper.RecordResultMock record : responseObject.records) {
+            if (record.id.equals(RECORD_ID_1)) assertEquals(RECORD1_V1, Long.valueOf(record.version));
+            else if (record.id.equals(RECORD_ID_2)) assertEquals(RECORD2_V1, Long.valueOf(record.version));
+            else if (record.id.equals(RECORD_ID_3)) fail("should not contain record 3: " + RECORD_ID_3);
+            else fail(String.format("should only contain record 1 %s, and record 2 %s", RECORD_ID_1, RECORD_ID_2));
+        }
+    }
+
+    @Test
+    public void should_queryAllRecords_when_validRecordIdsAndCollaborationIdAreProvided() throws Exception {
+        if (!isCollaborationEnabled) return;
+        // If I query records 1,2 and 3 in context guid2, I should get 200 with records 1,2 and 3
+        JsonArray records = new JsonArray();
+        records.add(RECORD_ID_1);
+        records.add(RECORD_ID_2);
+        records.add(RECORD_ID_3);
+        JsonObject body = new JsonObject();
+        body.add("records", records);
+        CloseableHttpResponse response = TestUtils.send("query/records", "POST", getHeadersWithxCollaboration(COLLABORATION2_ID, APPLICATION_NAME, TENANT_NAME, testUtils.getToken()), body.toString(), "");
+        assertEquals(HttpStatus.SC_OK, response.getCode());
+
+        DummyRecordsHelper.RecordsMock responseObject = RECORDS_HELPER.getRecordsMockFromResponse(response);
+        assertEquals(3, responseObject.records.length);
+        assertEquals(0, responseObject.invalidRecords.length);
+        assertEquals(0, responseObject.retryRecords.length);
+        for (DummyRecordsHelper.RecordResultMock record : responseObject.records) {
+            if (record.id.equals(RECORD_ID_1)) assertEquals(RECORD1_V4, Long.valueOf(record.version));
+            else if (record.id.equals(RECORD_ID_2)) assertEquals(RECORD2_V2, Long.valueOf(record.version));
+            else if (record.id.equals(RECORD_ID_3)) assertEquals(RECORD3_V2, Long.valueOf(record.version));
+            else fail(String.format("should only contain record 1 %s, 2 %s and record 3 %s", RECORD_ID_1, RECORD_ID_2, RECORD_ID_3));
+        }
+
+        // If I query records 1, 2 and 3 in context guid1, I should get 2xx with records 1 and 3
+        response = TestUtils.send("query/records", "POST", getHeadersWithxCollaboration(COLLABORATION1_ID, APPLICATION_NAME, TENANT_NAME, testUtils.getToken()), body.toString(), "");
+        assertEquals(HttpStatus.SC_OK, response.getCode());
+
+        responseObject = RECORDS_HELPER.getRecordsMockFromResponse(response);
+        assertEquals(2, responseObject.records.length);
+        assertEquals(1, responseObject.invalidRecords.length);
+        assertEquals(0, responseObject.retryRecords.length);
+        for (DummyRecordsHelper.RecordResultMock record : responseObject.records) {
+            if (record.id.equals(RECORD_ID_1)) assertEquals(RECORD1_V3, Long.valueOf(record.version));
+            else if (record.id.equals(RECORD_ID_2)) fail("should not contain record 2: " + RECORD_ID_2);
+            else if (record.id.equals(RECORD_ID_3)) assertEquals(RECORD3_V1, Long.valueOf(record.version));
+            else fail(String.format("should only contain record 1 %s, and record 3 %s", RECORD_ID_1, RECORD_ID_3));
+        }
+    }
+}
diff --git a/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/records/CollaborationRecordsSoftDeleteTest.java b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/records/CollaborationRecordsSoftDeleteTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..0208bc239b903a291f70f772146e300233ea41a7
--- /dev/null
+++ b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/records/CollaborationRecordsSoftDeleteTest.java
@@ -0,0 +1,136 @@
+/*
+ *  Copyright 2020-2024 Google LLC
+ *  Copyright 2020-2024 EPAM Systems, Inc
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+package org.opengroup.osdu.storage.records;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.opengroup.osdu.storage.util.HeaderUtils.getHeadersWithxCollaboration;
+import static org.opengroup.osdu.storage.util.TestUtils.assertRecordVersion;
+import static org.opengroup.osdu.storage.util.TestUtils.createRecordInCollaborationContext_AndReturnVersion;
+
+import com.google.gson.JsonArray;
+import java.util.UUID;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
+import org.apache.http.HttpStatus;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+import org.opengroup.osdu.storage.util.ConfigUtils;
+import org.opengroup.osdu.storage.util.LegalTagUtils;
+import org.opengroup.osdu.storage.util.TenantUtils;
+import org.opengroup.osdu.storage.util.TestBase;
+import org.opengroup.osdu.storage.util.TestUtils;
+import org.opengroup.osdu.storage.util.TokenTestUtils;
+
+public final class CollaborationRecordsSoftDeleteTest extends TestBase {
+    private static boolean isCollaborationEnabled = false;
+    private static final String APPLICATION_NAME = "storage service integration test for soft delete";
+    private static final String TENANT_NAME = TenantUtils.getTenantName();
+    private static final long CURRENT_TIME_MILLIS = System.currentTimeMillis();
+    private static final String COLLABORATION1_ID = UUID.randomUUID().toString();
+    private static final String COLLABORATION2_ID = UUID.randomUUID().toString();
+    private static final String RECORD_ID_1 = TENANT_NAME + ":inttest:1" + CURRENT_TIME_MILLIS;
+    private static final String RECORD_ID_2 = TENANT_NAME + ":inttest:2" + CURRENT_TIME_MILLIS;
+    private static final String RECORD_ID_3 = TENANT_NAME + ":inttest:3" + CURRENT_TIME_MILLIS;
+    private static final String KIND = TENANT_NAME + ":ds:inttest:" + CURRENT_TIME_MILLIS;
+    private static Long RECORD1_V1;
+    private static Long RECORD1_V2;
+    private static Long RECORD1_V3;
+    private static Long RECORD1_V4;
+    private static Long RECORD2_V1;
+    private static Long RECORD2_V2;
+    private static Long RECORD3_V1;
+    private static Long RECORD3_V2;
+    private static String LEGAL_TAG_NAME;
+
+    @Override
+    public void setup() throws Exception {
+        this.testUtils = new TokenTestUtils();
+        this.configUtils = new ConfigUtils("test.properties");
+
+        if (configUtils != null && !configUtils.getIsCollaborationEnabled()) {
+            return;
+        }
+        isCollaborationEnabled = true;
+        LEGAL_TAG_NAME = LegalTagUtils.createRandomName();
+        LegalTagUtils.create(LEGAL_TAG_NAME, testUtils.getToken());
+
+        RECORD1_V1 = createRecordInCollaborationContext_AndReturnVersion(RECORD_ID_1, KIND, LEGAL_TAG_NAME, COLLABORATION1_ID, APPLICATION_NAME, TENANT_NAME, testUtils.getToken());
+        RECORD1_V2 = createRecordInCollaborationContext_AndReturnVersion(RECORD_ID_1, KIND, LEGAL_TAG_NAME, COLLABORATION1_ID, APPLICATION_NAME, TENANT_NAME, testUtils.getToken());
+        RECORD1_V3 = createRecordInCollaborationContext_AndReturnVersion(RECORD_ID_1, KIND, LEGAL_TAG_NAME, null, APPLICATION_NAME, TENANT_NAME, testUtils.getToken());
+        RECORD1_V4 = createRecordInCollaborationContext_AndReturnVersion(RECORD_ID_1, KIND, LEGAL_TAG_NAME,COLLABORATION2_ID, APPLICATION_NAME, TENANT_NAME, testUtils.getToken());
+
+        RECORD2_V1 = createRecordInCollaborationContext_AndReturnVersion(RECORD_ID_2, KIND, LEGAL_TAG_NAME, COLLABORATION1_ID, APPLICATION_NAME, TENANT_NAME, testUtils.getToken());
+        RECORD2_V2 = createRecordInCollaborationContext_AndReturnVersion(RECORD_ID_2, KIND, LEGAL_TAG_NAME, COLLABORATION2_ID, APPLICATION_NAME, TENANT_NAME, testUtils.getToken());
+
+        RECORD3_V1 = createRecordInCollaborationContext_AndReturnVersion(RECORD_ID_3, KIND, LEGAL_TAG_NAME, COLLABORATION1_ID, APPLICATION_NAME, TENANT_NAME, testUtils.getToken());
+        RECORD3_V2 = createRecordInCollaborationContext_AndReturnVersion(RECORD_ID_3, KIND, LEGAL_TAG_NAME, COLLABORATION2_ID, APPLICATION_NAME, TENANT_NAME, testUtils.getToken());
+    }
+
+    @AfterEach
+    public void tearDown() throws Exception {
+        if (!isCollaborationEnabled) return;
+        TestUtils.send("records/" + RECORD_ID_1, "DELETE", getHeadersWithxCollaboration(COLLABORATION1_ID, APPLICATION_NAME, TENANT_NAME, testUtils.getToken()), "", "");
+        TestUtils.send("records/" + RECORD_ID_1, "DELETE", getHeadersWithxCollaboration(null, APPLICATION_NAME, TENANT_NAME, testUtils.getToken()), "", "");
+        TestUtils.send("records/" + RECORD_ID_1, "DELETE", getHeadersWithxCollaboration(COLLABORATION2_ID, APPLICATION_NAME, TENANT_NAME, testUtils.getToken()), "", "");
+        TestUtils.send("records/" + RECORD_ID_2, "DELETE", getHeadersWithxCollaboration(COLLABORATION1_ID, APPLICATION_NAME, TENANT_NAME, testUtils.getToken()), "", "");
+        TestUtils.send("records/" + RECORD_ID_2, "DELETE", getHeadersWithxCollaboration(COLLABORATION2_ID, APPLICATION_NAME, TENANT_NAME, testUtils.getToken()), "", "");
+        TestUtils.send("records/" + RECORD_ID_3, "DELETE", getHeadersWithxCollaboration(COLLABORATION1_ID, APPLICATION_NAME, TENANT_NAME, testUtils.getToken()), "", "");
+        TestUtils.send("records/" + RECORD_ID_3, "DELETE", getHeadersWithxCollaboration(COLLABORATION2_ID, APPLICATION_NAME, TENANT_NAME, testUtils.getToken()), "", "");
+        LegalTagUtils.delete(LEGAL_TAG_NAME, testUtils.getToken());
+
+        this.testUtils = null;
+        this.configUtils = null;
+    }
+
+    @Test
+    public void should_softDeleteSingleRecordWithinCollaborationContext_when_validRecordIdsAndCollaborationIdAreProvided() throws Exception {
+        if (!isCollaborationEnabled) return;
+        CloseableHttpResponse response = TestUtils.send("records/" + RECORD_ID_1 + ":delete", "POST", getHeadersWithxCollaboration(COLLABORATION1_ID, APPLICATION_NAME, TENANT_NAME, testUtils.getToken()), "", "");
+        assertEquals(HttpStatus.SC_NO_CONTENT, response.getCode());
+
+        response = TestUtils.send("records/" + RECORD_ID_1, "GET", getHeadersWithxCollaboration(COLLABORATION1_ID, APPLICATION_NAME, TENANT_NAME, testUtils.getToken()), "", "");
+        assertEquals(HttpStatus.SC_NOT_FOUND, response.getCode());
+
+        response = TestUtils.send("records/" + RECORD_ID_1, "GET", getHeadersWithxCollaboration(null, APPLICATION_NAME, TENANT_NAME, testUtils.getToken()), "", "");
+        assertRecordVersion(response, RECORD1_V3);
+
+        response = TestUtils.send("records/" + RECORD_ID_1, "GET", getHeadersWithxCollaboration(COLLABORATION2_ID, APPLICATION_NAME, TENANT_NAME, testUtils.getToken()), "", "");
+        assertRecordVersion(response, RECORD1_V4);
+    }
+
+    @Test
+    public void should_bulkSoftDeleteWithinCollaborationContext_when_validRecordIdsAndCollaborationIdAreProvided() throws Exception {
+        if (!isCollaborationEnabled) return;
+        JsonArray body = new JsonArray();
+        body.add(RECORD_ID_2);
+        body.add(RECORD_ID_3);
+        CloseableHttpResponse response = TestUtils.send("records/delete", "POST", getHeadersWithxCollaboration(COLLABORATION1_ID, APPLICATION_NAME, TENANT_NAME, testUtils.getToken()), body.toString(), "");
+        assertEquals(HttpStatus.SC_NO_CONTENT, response.getCode());
+
+        response = TestUtils.send("records/" + RECORD_ID_2, "GET", getHeadersWithxCollaboration(COLLABORATION1_ID, APPLICATION_NAME, TENANT_NAME, testUtils.getToken()), "", "");
+        assertEquals(HttpStatus.SC_NOT_FOUND, response.getCode());
+
+        response = TestUtils.send("records/" + RECORD_ID_3, "GET", getHeadersWithxCollaboration(COLLABORATION1_ID, APPLICATION_NAME, TENANT_NAME, testUtils.getToken()), "", "");
+        assertEquals(HttpStatus.SC_NOT_FOUND, response.getCode());
+
+        response = TestUtils.send("records/" + RECORD_ID_2, "GET", getHeadersWithxCollaboration(COLLABORATION2_ID, APPLICATION_NAME, TENANT_NAME, testUtils.getToken()), "", "");
+        assertRecordVersion(response, RECORD2_V2);
+
+        response = TestUtils.send("records/" + RECORD_ID_3, "GET", getHeadersWithxCollaboration(COLLABORATION2_ID, APPLICATION_NAME, TENANT_NAME, testUtils.getToken()), "", "");
+        assertRecordVersion(response, RECORD3_V2);
+    }
+}
diff --git a/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/records/CollaborationUpdateRecordsMetadataTest.java b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/records/CollaborationUpdateRecordsMetadataTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..aeb3c8132aff51d8942ff818b3b43c854d284f26
--- /dev/null
+++ b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/records/CollaborationUpdateRecordsMetadataTest.java
@@ -0,0 +1,107 @@
+/*
+ *  Copyright 2020-2024 Google LLC
+ *  Copyright 2020-2024 EPAM Systems, Inc
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+package org.opengroup.osdu.storage.records;
+
+import static org.apache.http.HttpStatus.SC_OK;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.opengroup.osdu.storage.records.CollaborationRecordsPurgeTest.COLLABORATION1_ID;
+import static org.opengroup.osdu.storage.records.UpdateRecordsMetadataTest.TAG_KEY;
+import static org.opengroup.osdu.storage.records.UpdateRecordsMetadataTest.TAG_VALUE1;
+import static org.opengroup.osdu.storage.util.HeaderUtils.getHeadersWithxCollaboration;
+import static org.opengroup.osdu.storage.util.TestUtils.assertRecordVersionAndReturnResponseBody;
+import static org.opengroup.osdu.storage.util.TestUtils.createRecordInCollaborationContext_AndReturnVersion;
+
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+import org.opengroup.osdu.storage.util.ConfigUtils;
+import org.opengroup.osdu.storage.util.LegalTagUtils;
+import org.opengroup.osdu.storage.util.RecordUtil;
+import org.opengroup.osdu.storage.util.TenantUtils;
+import org.opengroup.osdu.storage.util.TestBase;
+import org.opengroup.osdu.storage.util.TestUtils;
+import org.opengroup.osdu.storage.util.TokenTestUtils;
+
+public final class CollaborationUpdateRecordsMetadataTest extends TestBase {
+    private static boolean isCollaborationEnabled = false;
+    private static final String APPLICATION_NAME = "storage service integration test for update records metadata";
+    private static final String TENANT_NAME = TenantUtils.getTenantName();
+    private static final long CURRENT_TIME_MILLIS = System.currentTimeMillis();
+    private static String LEGAL_TAG_NAME;
+    private static final String KIND = TENANT_NAME + ":ds:patchtest:1" + CURRENT_TIME_MILLIS;
+    private static final String RECORD_PATCH_ID = TENANT_NAME + ":patchtest:1" + CURRENT_TIME_MILLIS;
+    private static Long RECORD_PATCH_V1;
+    private static Long RECORD_PATCH_V2;
+
+    @Override
+    public void setup() throws Exception {
+        this.testUtils = new TokenTestUtils();
+        this.configUtils = new ConfigUtils("test.properties");
+
+        if (configUtils != null && !configUtils.getIsCollaborationEnabled()) {
+            return;
+        }
+        isCollaborationEnabled = true;
+        LEGAL_TAG_NAME = LegalTagUtils.createRandomName();
+        LegalTagUtils.create(LEGAL_TAG_NAME, testUtils.getToken());
+
+        //create records in different collaboration context
+        RECORD_PATCH_V1 = createRecordInCollaborationContext_AndReturnVersion(RECORD_PATCH_ID, KIND, LEGAL_TAG_NAME, null, APPLICATION_NAME, TENANT_NAME, testUtils.getToken());
+        RECORD_PATCH_V2 = createRecordInCollaborationContext_AndReturnVersion(RECORD_PATCH_ID, KIND, LEGAL_TAG_NAME, COLLABORATION1_ID, APPLICATION_NAME, TENANT_NAME, testUtils.getToken());
+
+        //update record with tags in one collaboration context
+        JsonObject updateBody = RecordUtil.buildUpdateTagBody(RECORD_PATCH_ID, "add", TAG_KEY + ":" + TAG_VALUE1);
+        CloseableHttpResponse patchResponse = TestUtils.send("records", "PATCH", getHeadersWithxCollaboration(COLLABORATION1_ID, APPLICATION_NAME, TenantUtils.getTenantName(), testUtils.getToken()), updateBody.toString(), "");
+        assertEquals(SC_OK, patchResponse.getCode());
+    }
+
+    @Test
+    public void shouldMaintainAndUpdateRecordInRespctiveCollaborationContext() throws Exception {
+        if (!isCollaborationEnabled) return;
+        //assert record with no collaboration context
+        CloseableHttpResponse getResponse = TestUtils.send("records/" + RECORD_PATCH_ID, "GET", getHeadersWithxCollaboration(null, APPLICATION_NAME, TENANT_NAME, testUtils.getToken()), "", "");
+        String responseBody = assertRecordVersionAndReturnResponseBody(getResponse, RECORD_PATCH_V1);
+        JsonObject resultObject = bodyToJsonObject(responseBody);
+        assertNull(resultObject.get("tags"));
+        //assert record with collaboration context
+        getResponse = TestUtils.send("records/" + RECORD_PATCH_ID, "GET", getHeadersWithxCollaboration(COLLABORATION1_ID, APPLICATION_NAME, TENANT_NAME, testUtils.getToken()), "", "");
+        responseBody = assertRecordVersionAndReturnResponseBody(getResponse, RECORD_PATCH_V2);
+        resultObject = bodyToJsonObject(responseBody);
+        assertTrue(resultObject.get("tags").getAsJsonObject().has(TAG_KEY));
+        assertEquals(TAG_VALUE1, resultObject.get("tags").getAsJsonObject().get(TAG_KEY).getAsString());
+    }
+
+    @AfterEach
+    public void tearDown() throws Exception {
+        if (!isCollaborationEnabled) return;
+        TestUtils.send("records/" + RECORD_PATCH_ID, "DELETE", getHeadersWithxCollaboration(null, APPLICATION_NAME, TENANT_NAME, testUtils.getToken()), "", "");
+        TestUtils.send("records/" + RECORD_PATCH_ID, "DELETE", getHeadersWithxCollaboration(COLLABORATION1_ID, APPLICATION_NAME, TENANT_NAME, testUtils.getToken()), "", "");
+        LegalTagUtils.delete(LEGAL_TAG_NAME, testUtils.getToken());
+
+        this.testUtils = null;
+        this.configUtils = null;
+    }
+
+    private static JsonObject bodyToJsonObject(String json) {
+        return JsonParser.parseString(json).getAsJsonObject();
+    }
+}
diff --git a/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/records/CopyRecordReferencesTest.java b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/records/CopyRecordReferencesTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..1fa7442eabf3269a8affe1446e58d027a6324333
--- /dev/null
+++ b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/records/CopyRecordReferencesTest.java
@@ -0,0 +1,465 @@
+/*
+ *  Copyright 2020-2024 Google LLC
+ *  Copyright 2020-2024 EPAM Systems, Inc
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+package org.opengroup.osdu.storage.records;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assumptions.assumeTrue;
+import static org.opengroup.osdu.storage.util.HeaderUtils.getHeadersWithxCollaborationWithoutId;
+import static org.opengroup.osdu.storage.util.TestUtils.getCopyRecordRequest;
+
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import java.util.UUID;
+import lombok.extern.java.Log;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
+import org.apache.hc.core5.http.io.entity.EntityUtils;
+import org.apache.http.HttpStatus;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.opengroup.osdu.storage.util.ConfigUtils;
+import org.opengroup.osdu.storage.util.HeaderUtils;
+import org.opengroup.osdu.storage.util.LegalTagUtils;
+import org.opengroup.osdu.storage.util.RecordUtil;
+import org.opengroup.osdu.storage.util.TenantUtils;
+import org.opengroup.osdu.storage.util.TestBase;
+import org.opengroup.osdu.storage.util.TestUtils;
+import org.opengroup.osdu.storage.util.TokenTestUtils;
+
+@Log
+public final class CopyRecordReferencesTest extends TestBase {
+
+  private static final String APPLICATION_NAME = "storage service integration test";
+
+  private static final String COLLABORATION_ID = "25c25830-8588-4b12-a0be-7263f2e43a09";
+  private static final String COLLABORATION_ID_WIP_TO_WIP = "cfa0c1b0-421a-4f51-a2ac-84ff8a968736";
+
+  private static final String RECORD_ID_SOR_TO_WIP =
+      TenantUtils.getTenantName() + ":getrecord:" + UUID.randomUUID();
+
+  private static final String RECORD_ID_WIP_TO_SOR =
+      TenantUtils.getTenantName() + ":getrecord:" + UUID.randomUUID();
+
+  private static final String RECORD_ID_WIP_TO_WIP =
+      TenantUtils.getTenantName() + ":getrecord:" + UUID.randomUUID();
+
+  private static final String RECORD_ID_EXIST_IN_TARGET =
+      TenantUtils.getTenantName() + ":getrecord:" + UUID.randomUUID();
+
+  private static final String KIND = TenantUtils.getTenantName() + ":ds:getrecord:1.0."
+      + System.currentTimeMillis();
+  private static String LEGAL_TAG_NAME_A;
+
+  private static final TokenTestUtils TOKEN_TEST_UTILS = new TokenTestUtils();
+
+  @BeforeAll
+  public static void classSetup() throws Exception {
+    ConfigUtils configUtils = new ConfigUtils("test.properties");
+    assumeTrue(configUtils.getIsCollaborationEnabled());
+    CopyRecordReferencesTest.classSetup(TOKEN_TEST_UTILS.getToken());
+  }
+
+  @AfterAll
+  public static void classTearDown() throws Exception {
+    CopyRecordReferencesTest.classTearDown(TOKEN_TEST_UTILS.getToken());
+  }
+
+  @BeforeEach
+  @Override
+  public void setup() throws Exception {
+    this.testUtils = new TokenTestUtils();
+  }
+
+  @AfterEach
+  @Override
+  public void tearDown() throws Exception {
+    this.testUtils = null;
+  }
+
+  public static void classSetup(String token) throws Exception {
+    System.out.println(String.format("Test Ids: %s, %s, %s, %s", RECORD_ID_SOR_TO_WIP, RECORD_ID_WIP_TO_SOR, RECORD_ID_WIP_TO_WIP, RECORD_ID_EXIST_IN_TARGET));
+    LEGAL_TAG_NAME_A = LegalTagUtils.createRandomName();
+    LegalTagUtils.create(LEGAL_TAG_NAME_A, token);
+    Thread.sleep(100);
+
+    String jsonInputSorToWip = RecordUtil.createDefaultJsonRecord(RECORD_ID_SOR_TO_WIP, KIND,
+        LEGAL_TAG_NAME_A);
+
+    CloseableHttpResponse responseSorToWip = TestUtils.send("records", "PUT",
+        HeaderUtils.getHeaders(TenantUtils.getTenantName(), token), jsonInputSorToWip, "");
+
+    JsonObject jsonResponseSorToWip = JsonParser.parseString(
+            EntityUtils.toString(responseSorToWip.getEntity()))
+        .getAsJsonObject();
+
+    assertEquals(
+        HttpStatus.SC_CREATED,
+        responseSorToWip.getCode(),
+        "Creating record for copy from SOR to WIP"
+    );
+
+    assertTrue(
+        responseSorToWip.getEntity().getContentType().contains("application/json"),
+        "Creating record for copy from SOR to WIP"
+    );
+
+    assertEquals(
+        RECORD_ID_SOR_TO_WIP,
+        jsonResponseSorToWip.get("recordIds").getAsString(),
+        "Creating record for copy from SOR to WIP"
+    );
+
+    String jsonInputWipToSor = RecordUtil.createDefaultJsonRecord(RECORD_ID_WIP_TO_SOR, KIND,
+        LEGAL_TAG_NAME_A);
+
+    CloseableHttpResponse responseWipToSor = TestUtils.send("records", "PUT",
+        HeaderUtils.getHeadersWithxCollaborationWithoutId(COLLABORATION_ID, APPLICATION_NAME,
+            TenantUtils.getTenantName(), token), jsonInputWipToSor, "");
+    JsonObject jsonResponseWipToSor = JsonParser.parseString(
+            EntityUtils.toString(responseWipToSor.getEntity()))
+        .getAsJsonObject();
+
+    assertEquals(
+        HttpStatus.SC_CREATED,
+        responseWipToSor.getCode(),
+        "Creating record for copy from WIP to SOR"
+    );
+
+    assertTrue(
+        responseWipToSor.getEntity().getContentType().contains("application/json"),
+        "Creating record for copy from WIP to SOR"
+    );
+
+    assertEquals(
+        RECORD_ID_WIP_TO_SOR,
+        jsonResponseWipToSor.get("recordIds").getAsString(),
+        "Creating record for copy from WIP to SOR"
+    );
+
+    String jsonInputWipToWip = RecordUtil.createDefaultJsonRecord(RECORD_ID_WIP_TO_WIP, KIND,
+        LEGAL_TAG_NAME_A);
+
+    CloseableHttpResponse responseWipToWip = TestUtils.send("records", "PUT",
+        HeaderUtils.getHeadersWithxCollaborationWithoutId(COLLABORATION_ID, APPLICATION_NAME,
+            TenantUtils.getTenantName(), token), jsonInputWipToWip, "");
+    JsonObject jsonResponseWipToWip = JsonParser.parseString(
+            EntityUtils.toString(responseWipToWip.getEntity()))
+        .getAsJsonObject();
+
+    assertEquals(
+        HttpStatus.SC_CREATED,
+        responseWipToWip.getCode(),
+        "Creating record for copy from WIP to WIP"
+    );
+
+    assertTrue(
+        responseWipToWip.getEntity().getContentType().contains("application/json"),
+        "Creating record for copy from WIP to WIP"
+    );
+
+    assertEquals(
+        RECORD_ID_WIP_TO_WIP,
+        jsonResponseWipToWip.get("recordIds").getAsString(),
+        "Creating record for copy from WIP to WIP"
+    );
+
+    String jsonInputExistInTarget = RecordUtil.createDefaultJsonRecord(RECORD_ID_EXIST_IN_TARGET,
+        KIND,
+        LEGAL_TAG_NAME_A);
+
+    CloseableHttpResponse responseExistInTarget = TestUtils.send("records", "PUT",
+        HeaderUtils.getHeadersWithxCollaborationWithoutId(COLLABORATION_ID, APPLICATION_NAME,
+            TenantUtils.getTenantName(), token), jsonInputExistInTarget, "");
+    JsonObject jsonResponseExistInTarget = JsonParser.parseString(
+            EntityUtils.toString(responseExistInTarget.getEntity()))
+        .getAsJsonObject();
+
+    assertEquals(
+        HttpStatus.SC_CREATED,
+        responseExistInTarget.getCode(),
+        "Creating record for check existing in target"
+    );
+
+    assertTrue(
+        responseExistInTarget.getEntity().getContentType().contains("application/json"),
+        "Creating record for check existing in target"
+    );
+
+    assertEquals(
+        RECORD_ID_EXIST_IN_TARGET,
+        jsonResponseExistInTarget.get("recordIds").getAsString(),
+        "Creating record for check existing in target"
+    );
+  }
+
+  public static void classTearDown(String token) throws Exception {
+    TestUtils.send("records/" + RECORD_ID_SOR_TO_WIP, "DELETE",
+        HeaderUtils.getHeaders(TenantUtils.getTenantName(), token), "", "");
+    TestUtils.send("records/" + RECORD_ID_WIP_TO_SOR, "DELETE",
+        HeaderUtils.getHeadersWithxCollaborationWithoutId(COLLABORATION_ID, APPLICATION_NAME,
+            TenantUtils.getTenantName(), token), "", "");
+    TestUtils.send("records/" + RECORD_ID_WIP_TO_WIP, "DELETE",
+        HeaderUtils.getHeadersWithxCollaborationWithoutId(COLLABORATION_ID, APPLICATION_NAME,
+            TenantUtils.getTenantName(), token), "", "");
+    Thread.sleep(100);
+    LegalTagUtils.delete(LEGAL_TAG_NAME_A, token);
+  }
+
+  @Test
+  public void should_copyRecord_from_sor_to_wip() throws Exception {
+    // check namespace before copy
+    CloseableHttpResponse responseGet = TestUtils.send("records/" + RECORD_ID_SOR_TO_WIP, "GET",
+        getHeadersWithxCollaborationWithoutId(COLLABORATION_ID, APPLICATION_NAME,
+            TenantUtils.getTenantName(), testUtils.getToken()), "", "");
+
+    assertEquals(
+        HttpStatus.SC_NOT_FOUND,
+        responseGet.getCode(),
+        "Check that record absent in target when copy from SOR to WIP"
+    );
+
+    // copy checks
+    JsonObject copyBody = getCopyRecordRequest(COLLABORATION_ID, RECORD_ID_SOR_TO_WIP);
+    String body = copyBody.toString();
+
+    CloseableHttpResponse responseCopy = TestUtils.send("records/copy", "PUT",
+        getHeadersWithxCollaborationWithoutId("", APPLICATION_NAME, TenantUtils.getTenantName(),
+            testUtils.getToken()), body, "");
+
+    assertEquals(
+        HttpStatus.SC_OK,
+        responseCopy.getCode(),
+        "Check response after copy SOR to WIP"
+    );
+
+    // check namespace after copy
+    CloseableHttpResponse responseGetCopy = TestUtils.send("records/" + RECORD_ID_SOR_TO_WIP, "GET",
+        getHeadersWithxCollaborationWithoutId(COLLABORATION_ID, APPLICATION_NAME,
+            TenantUtils.getTenantName(), testUtils.getToken()), "", "");
+    JsonObject jsonCopy = JsonParser.parseString(EntityUtils.toString(responseGetCopy.getEntity()))
+        .getAsJsonObject();
+
+    assertEquals(
+        HttpStatus.SC_OK,
+        responseGetCopy.getCode(),
+        "Get copied record from WIP when copy SOR to WIP"
+    );
+
+    assertEquals(
+        RECORD_ID_SOR_TO_WIP,
+        jsonCopy.get("id").getAsString(),
+        "Get copied record from WIP when copy SOR to WIP"
+    );
+
+    // delete from WIP
+    CloseableHttpResponse responseDelete = TestUtils.send("records/" + RECORD_ID_SOR_TO_WIP,
+        "DELETE", getHeadersWithxCollaborationWithoutId(COLLABORATION_ID, APPLICATION_NAME,
+            TenantUtils.getTenantName(), testUtils.getToken()), body, "");
+
+    assertEquals(
+        HttpStatus.SC_NO_CONTENT,
+        responseDelete.getCode(),
+        "Check that record deleted when copy SOR to WIP"
+    );
+
+  }
+
+  @Test
+  public void should_copyRecord_from_wip_to_sor() throws Exception {
+    // check namespace before copy
+    CloseableHttpResponse responseGet = TestUtils.send("records/" + RECORD_ID_WIP_TO_SOR, "GET",
+        HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "", "");
+
+    assertEquals(
+        HttpStatus.SC_NOT_FOUND,
+        responseGet.getCode(),
+        "Check that record absent in target when copy WIP to SOR"
+    );
+
+    // copy checks
+    JsonObject copyBody = getCopyRecordRequest("", RECORD_ID_WIP_TO_SOR);
+    String body = copyBody.toString();
+
+    CloseableHttpResponse responseCopy = TestUtils.send("records/copy", "PUT",
+        getHeadersWithxCollaborationWithoutId(COLLABORATION_ID, APPLICATION_NAME,
+            TenantUtils.getTenantName(),
+            testUtils.getToken()), body, "");
+
+    assertEquals(
+        HttpStatus.SC_OK,
+        responseCopy.getCode(),
+        "Check response after copy WIP to SOR"
+    );
+
+    // check namespace after copy
+    CloseableHttpResponse responseGetCopy = TestUtils.send("records/" + RECORD_ID_WIP_TO_SOR, "GET",
+        HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "", "");
+    JsonObject jsonCopy = JsonParser.parseString(EntityUtils.toString(responseGetCopy.getEntity()))
+        .getAsJsonObject();
+
+    assertEquals(
+        HttpStatus.SC_OK,
+        responseGetCopy.getCode(),
+        "Get copied record from WIP when copy WIP to SOR"
+    );
+
+    assertEquals(
+        RECORD_ID_WIP_TO_SOR,
+        jsonCopy.get("id").getAsString(),
+        "Get copied record from WIP when copy WIP to SOR"
+    );
+
+    // delete from SOR
+    CloseableHttpResponse responseDelete = TestUtils.send("records/" + RECORD_ID_WIP_TO_SOR,
+        "DELETE", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()),
+        body, "");
+
+    assertEquals(
+        HttpStatus.SC_NO_CONTENT,
+        responseDelete.getCode(),
+        "Check that record deleted when copy WIP to SOR"
+    );
+  }
+
+  @Test
+  public void should_copyRecord_from_wip_to_wip() throws Exception {
+    // check namespace before copy
+    CloseableHttpResponse responseGet = TestUtils.send("records/" + RECORD_ID_WIP_TO_WIP, "GET",
+        getHeadersWithxCollaborationWithoutId(COLLABORATION_ID_WIP_TO_WIP, APPLICATION_NAME,
+            TenantUtils.getTenantName(), testUtils.getToken()), "", "");
+
+    assertEquals(
+        HttpStatus.SC_NOT_FOUND,
+        responseGet.getCode(),
+        "Check that record absent in target copy WIP to WIP"
+    );
+
+    // copy checks
+    JsonObject copyBody = getCopyRecordRequest(COLLABORATION_ID_WIP_TO_WIP, RECORD_ID_WIP_TO_WIP);
+    String body = copyBody.toString();
+
+    CloseableHttpResponse responseCopy = TestUtils.send("records/copy", "PUT",
+        getHeadersWithxCollaborationWithoutId(COLLABORATION_ID, APPLICATION_NAME,
+            TenantUtils.getTenantName(),
+            testUtils.getToken()), body, "");
+
+    assertEquals(
+        HttpStatus.SC_OK,
+        responseCopy.getCode(),
+        "Check response after copy WIP to WIP"
+    );
+
+    // check namespace after copy
+    CloseableHttpResponse responseGetCopy = TestUtils.send("records/" + RECORD_ID_WIP_TO_WIP, "GET",
+        getHeadersWithxCollaborationWithoutId(COLLABORATION_ID_WIP_TO_WIP, APPLICATION_NAME,
+            TenantUtils.getTenantName(), testUtils.getToken()), "", "");
+    JsonObject jsonCopy = JsonParser.parseString(EntityUtils.toString(responseGetCopy.getEntity()))
+        .getAsJsonObject();
+
+    assertEquals(
+        HttpStatus.SC_OK,
+        responseGetCopy.getCode(),
+        "Get copied record from WIP when copy WIP to WIP"
+    );
+
+    assertEquals(
+        RECORD_ID_WIP_TO_WIP,
+        jsonCopy.get("id").getAsString(),
+        "Get copied record from WIP when copy WIP to WIP"
+    );
+
+    // delete from WIP
+    CloseableHttpResponse responseDelete = TestUtils.send("records/" + RECORD_ID_WIP_TO_WIP,
+        "DELETE",
+        getHeadersWithxCollaborationWithoutId(COLLABORATION_ID_WIP_TO_WIP, APPLICATION_NAME,
+            TenantUtils.getTenantName(), testUtils.getToken()), body, "");
+
+    assertEquals(
+        HttpStatus.SC_NO_CONTENT,
+        responseDelete.getCode(),
+        "Check that record deleted when copy WIP to WIP"
+    );
+  }
+
+  @Test
+  public void should_return409_when_try_to_copy_sor_to_sor() throws Exception {
+    JsonObject copyBody = getCopyRecordRequest("", RECORD_ID_WIP_TO_WIP);
+    String body = copyBody.toString();
+
+    CloseableHttpResponse responseCopy = TestUtils.send("records/copy", "PUT",
+        getHeadersWithxCollaborationWithoutId("", APPLICATION_NAME, TenantUtils.getTenantName(),
+            testUtils.getToken()), body, "");
+
+    assertEquals(HttpStatus.SC_CONFLICT, responseCopy.getCode());
+  }
+
+  @Test
+  public void should_return404_when_record_absent_in_source() throws Exception {
+    JsonObject copyBody = getCopyRecordRequest(COLLABORATION_ID, RECORD_ID_WIP_TO_SOR);
+    String body = copyBody.toString();
+
+    CloseableHttpResponse responseCopy = TestUtils.send("records/copy", "PUT",
+        getHeadersWithxCollaborationWithoutId("", APPLICATION_NAME, TenantUtils.getTenantName(),
+            testUtils.getToken()), body, "");
+    assertEquals(HttpStatus.SC_NOT_FOUND, responseCopy.getCode());
+  }
+
+  @Test
+  public void should_return409_when_record_exist_in_target() throws Exception {
+    JsonObject copyBody = getCopyRecordRequest("", RECORD_ID_EXIST_IN_TARGET);
+    String body = copyBody.toString();
+
+    CloseableHttpResponse responseCopy = TestUtils.send("records/copy", "PUT",
+        getHeadersWithxCollaborationWithoutId(COLLABORATION_ID, APPLICATION_NAME,
+            TenantUtils.getTenantName(),
+            testUtils.getToken()), body, "");
+
+    assertEquals(
+        HttpStatus.SC_OK,
+        responseCopy.getCode(),
+        "Check that record created in WIP when check exception about existing in target"
+    );
+
+    JsonObject copyBodyTest = getCopyRecordRequest("", RECORD_ID_EXIST_IN_TARGET);
+    String bodyTest = copyBodyTest.toString();
+
+    CloseableHttpResponse responseCopyTest = TestUtils.send("records/copy", "PUT",
+        getHeadersWithxCollaborationWithoutId(COLLABORATION_ID, APPLICATION_NAME,
+            TenantUtils.getTenantName(),
+            testUtils.getToken()), bodyTest, "");
+
+    assertEquals(
+        HttpStatus.SC_CONFLICT,
+        responseCopyTest.getCode(),
+        "The already exists when check exception about existing in target"
+    );
+
+    // delete from SOR
+    CloseableHttpResponse responseDelete = TestUtils.send("records/" + RECORD_ID_EXIST_IN_TARGET,
+        "DELETE", getHeadersWithxCollaborationWithoutId(COLLABORATION_ID, APPLICATION_NAME,
+            TenantUtils.getTenantName(), testUtils.getToken()), body, "");
+    assertEquals(
+        HttpStatus.SC_NO_CONTENT,
+        responseDelete.getCode(),
+        "Check that record deleted when check exception about existing in target"
+    );
+  }
+}
diff --git a/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/records/DataRootAccessTest.java b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/records/DataRootAccessTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..244b641d99c9a3ba5fa71e299135e7d4a1e41393
--- /dev/null
+++ b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/records/DataRootAccessTest.java
@@ -0,0 +1,156 @@
+/*
+ *  Copyright 2020-2023 Google LLC
+ *  Copyright 2020-2023 EPAM Systems, Inc
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+package org.opengroup.osdu.storage.records;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonObject;
+import java.util.Map;
+import java.util.stream.Stream;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
+import org.apache.hc.core5.http.io.entity.EntityUtils;
+import org.apache.http.HttpStatus;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.opengroup.osdu.storage.util.ConfigUtils;
+import org.opengroup.osdu.storage.util.DummyRecordsHelper;
+import org.opengroup.osdu.storage.util.EntitlementsUtil;
+import org.opengroup.osdu.storage.util.HeaderUtils;
+import org.opengroup.osdu.storage.util.LegalTagUtils;
+import org.opengroup.osdu.storage.util.RecordUtil;
+import org.opengroup.osdu.storage.util.TenantUtils;
+import org.opengroup.osdu.storage.util.TestBase;
+import org.opengroup.osdu.storage.util.TestUtils;
+import org.opengroup.osdu.storage.util.TokenTestUtils;
+
+public final class DataRootAccessTest extends TestBase {
+
+  private static long NOW = System.currentTimeMillis();
+  private static String LEGAL_TAG = LegalTagUtils.createRandomName();
+  private static String KIND = TenantUtils.getTenantName() + ":data-root-test:no:1.1." + NOW;
+  private static String RECORD_ID = TenantUtils.getTenantName() + ":data-root-test:1.1." + NOW;
+  private static String DATA_GROUP_ID = "data.test-users-data-root." + NOW;
+  private static String GROUP_DESCRIPTION = "Used in ACL, to test that users.data.root have access to any data group.";
+  private static String GROUP_EMAIL;
+
+  private static final TokenTestUtils TOKEN_TEST_UTILS = new TokenTestUtils();
+
+  @BeforeAll
+  public static void classSetup() throws Exception {
+    DataRootAccessTest.classSetup(TOKEN_TEST_UTILS.getToken());
+  }
+
+  @AfterAll
+  public static void classTearDown() throws Exception {
+    DataRootAccessTest.classTearDown(TOKEN_TEST_UTILS.getToken());
+  }
+
+  @BeforeEach
+  @Override
+  public void setup() throws Exception {
+    this.testUtils = new TokenTestUtils();
+    this.configUtils = new ConfigUtils("test.properties");
+  }
+
+  @AfterEach
+  @Override
+  public void tearDown() throws Exception {
+    this.testUtils = null;
+  }
+
+  public static void classSetup(String token) throws Exception {
+    LegalTagUtils.create(LEGAL_TAG, token);
+    Map<String, String> headers = HeaderUtils.getHeaders(TenantUtils.getTenantName(), token);
+    GROUP_EMAIL = createDataGroup(headers);
+    String createRecordBody = RecordUtil.createJsonRecordWithCustomAcl(RECORD_ID, KIND, LEGAL_TAG,
+        GROUP_EMAIL);
+    CloseableHttpResponse response = TestUtils.send(
+        "records",
+        "PUT",
+        headers,
+        createRecordBody,
+        ""
+    );
+    assertEquals(HttpStatus.SC_CREATED, response.getCode());
+  }
+
+  public static void classTearDown(String token) throws Exception {
+    Map<String, String> headers = HeaderUtils.getHeaders(TenantUtils.getTenantName(), token);
+    TestUtils.send(
+        "records/" + RECORD_ID, "DELETE",
+        HeaderUtils.getHeaders(TenantUtils.getTenantName(), token),
+        "",
+        ""
+    );
+    deleteDataGroup(headers, GROUP_EMAIL);
+    LegalTagUtils.delete(LEGAL_TAG, token);
+  }
+
+  @Test
+  public void shouldHaveAccessToNewlyCreatedDataGroupWhenBelongsToUsersDataRoot() throws Exception {
+    JsonArray records = new JsonArray();
+    records.add(RECORD_ID);
+
+    JsonObject body = new JsonObject();
+    body.add("records", records);
+
+    Map<String, String> headersWithUsersDataRootAccess = HeaderUtils.getHeaders(
+        TenantUtils.getTenantName(),
+        testUtils.getDataRootUserToken());
+
+    CloseableHttpResponse queryResponse = TestUtils.send(
+        "query/records",
+        "POST",
+        headersWithUsersDataRootAccess,
+        body.toString(),
+        ""
+    );
+
+    DummyRecordsHelper.RecordsMock responseObject = new DummyRecordsHelper().getRecordsMockFromResponse(
+        queryResponse);
+
+    assertEquals(HttpStatus.SC_OK, queryResponse.getCode());
+    assertEquals(1, responseObject.records.length);
+    assertEquals(RECORD_ID, Stream.of(responseObject.records).findFirst().get().id);
+  }
+
+  protected static String createDataGroup(Map<String, String> headersWithValidAccessToken)
+      throws Exception {
+    CloseableHttpResponse entitlementsGroup = EntitlementsUtil.createEntitlementsGroup(
+        headersWithValidAccessToken,
+        DATA_GROUP_ID,
+        GROUP_DESCRIPTION
+    );
+    assertTrue(entitlementsGroup.getEntity().getContentType().contains("application/json"));
+    String json = EntityUtils.toString(entitlementsGroup.getEntity());
+    Gson gson = new Gson();
+    JsonObject groupEntity = gson.fromJson(json, JsonObject.class);
+    return groupEntity.get("email").getAsString();
+  }
+
+  protected static void deleteDataGroup(Map<String, String> headersWithValidAccessToken,
+      String groupEmail) throws Exception {
+    EntitlementsUtil.deleteEntitlementsGroup(headersWithValidAccessToken, groupEmail);
+  }
+}
diff --git a/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/records/DeleteRecordLogicallyAndItsVersionsTest.java b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/records/DeleteRecordLogicallyAndItsVersionsTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..b225c8049b2000b51b19a332a0579f7344b3acee
--- /dev/null
+++ b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/records/DeleteRecordLogicallyAndItsVersionsTest.java
@@ -0,0 +1,90 @@
+// Copyright 2017-2019, Schlumberger
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package org.opengroup.osdu.storage.records;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
+import org.apache.http.HttpStatus;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.opengroup.osdu.storage.util.HeaderUtils;
+import org.opengroup.osdu.storage.util.LegalTagUtils;
+import org.opengroup.osdu.storage.util.RecordUtil;
+import org.opengroup.osdu.storage.util.TenantUtils;
+import org.opengroup.osdu.storage.util.TestBase;
+import org.opengroup.osdu.storage.util.TestUtils;
+import org.opengroup.osdu.storage.util.TokenTestUtils;
+
+public final class DeleteRecordLogicallyAndItsVersionsTest extends TestBase {
+
+	private static final Long NOW = System.currentTimeMillis();
+	private static final String LEGAL_TAG = LegalTagUtils.createRandomName();
+
+	private static final String KIND = TenantUtils.getTenantName() + ":test:endtoend:1.1."
+			+ NOW;
+	private static final String RECORD_ID = TenantUtils.getTenantName() + ":endtoend:1.1."
+			+ NOW;
+
+	@BeforeEach
+	public void setup() throws Exception {
+		this.testUtils = new TokenTestUtils();
+		LegalTagUtils.create(LEGAL_TAG, testUtils.getToken());
+		CloseableHttpResponse response = TestUtils.send("records", "PUT", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()),
+				RecordUtil.createJsonRecordWithData(RECORD_ID, KIND, LEGAL_TAG, "v1"), "");
+		assertEquals(HttpStatus.SC_CREATED, response.getCode());
+	}
+
+	@AfterEach
+	public void tearDown() throws Exception {
+		TestUtils.send("records/" + RECORD_ID, "DELETE", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "", "");
+		LegalTagUtils.delete(LEGAL_TAG, testUtils.getToken());
+		this.testUtils = null;
+	}
+
+	@Test
+	public void should_deleteRecordAndAllVersionsLogically_when_userIsAuthorized() throws Exception {
+
+		CloseableHttpResponse response = TestUtils.send("records", "PUT", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()),
+				RecordUtil.createJsonRecordWithData(RECORD_ID, KIND, LEGAL_TAG, "v2"), "");
+		assertEquals(HttpStatus.SC_CREATED, response.getCode());
+
+		CloseableHttpResponse versionResponse = TestUtils.send("records/versions/" + RECORD_ID, "GET",
+				HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "", "");
+		assertEquals(HttpStatus.SC_OK, versionResponse.getCode());
+
+		String versions = TestUtils.getResult(versionResponse, HttpStatus.SC_OK, String.class);
+		JsonObject content = new JsonParser().parse(versions).getAsJsonObject();
+		JsonArray versionArray = content.get("versions").getAsJsonArray();
+
+		String versionOne = versionArray.get(0).toString();
+		String versionTwo = versionArray.get(1).toString();
+
+		CloseableHttpResponse deleteResponse = TestUtils.send("records/", "POST", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()),
+				"{'anything':'anything'}", RECORD_ID + ":delete");
+
+		assertEquals(HttpStatus.SC_NO_CONTENT, deleteResponse.getCode());
+
+		response = TestUtils.send("records/" + RECORD_ID + "/" + versionOne, "GET", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "", "");
+		assertEquals(HttpStatus.SC_NOT_FOUND, response.getCode());
+
+		response = TestUtils.send("records/" + RECORD_ID + "/" + versionTwo, "GET", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "", "");
+		assertEquals(HttpStatus.SC_NOT_FOUND, response.getCode());
+	}
+}
diff --git a/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/records/GetRecordsIntegrationTest.java b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/records/GetRecordsIntegrationTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..9b297384570495471d6ce8008eb89406d672482d
--- /dev/null
+++ b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/records/GetRecordsIntegrationTest.java
@@ -0,0 +1,197 @@
+// Copyright 2017-2019, Schlumberger
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package org.opengroup.osdu.storage.records;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
+import org.apache.hc.core5.http.io.entity.EntityUtils;
+import org.apache.http.HttpStatus;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.opengroup.osdu.storage.util.HeaderUtils;
+import org.opengroup.osdu.storage.util.LegalTagUtils;
+import org.opengroup.osdu.storage.util.RecordUtil;
+import org.opengroup.osdu.storage.util.TenantUtils;
+import org.opengroup.osdu.storage.util.TestBase;
+import org.opengroup.osdu.storage.util.TestUtils;
+import org.opengroup.osdu.storage.util.TokenTestUtils;
+
+public final class GetRecordsIntegrationTest extends TestBase {
+	private static final String RECORD_ID = TenantUtils.getTenantName() + ":getrecord:" + System.currentTimeMillis();
+	private static final String ANOTHER_RECORD_ID = TenantUtils.getTenantName() + ":getrecordnodup:" + System.currentTimeMillis();
+
+	private static final String KIND = TenantUtils.getTenantName() + ":ds:getrecord:1.0."
+			+ System.currentTimeMillis();
+
+	private static String LEGAL_TAG_NAME_A;
+	private static String LEGAL_TAG_NAME_B;
+
+	private static final TokenTestUtils TOKEN_TEST_UTILS = new TokenTestUtils();
+
+	@BeforeAll
+	public static void classSetup() throws Exception {
+		GetRecordsIntegrationTest.classSetup(TOKEN_TEST_UTILS.getToken());
+	}
+
+	@AfterAll
+	public static void classTearDown() throws Exception {
+		GetRecordsIntegrationTest.classTearDown(TOKEN_TEST_UTILS.getToken());
+	}
+
+	@BeforeEach
+	@Override
+	public void setup() throws Exception {
+		this.testUtils = new TokenTestUtils();
+	}
+
+	@AfterEach
+	@Override
+	public void tearDown() throws Exception {
+		this.testUtils = null;
+	}
+
+	public static void classSetup(String token) throws Exception {
+        LEGAL_TAG_NAME_A = LegalTagUtils.createRandomName();
+        Thread.sleep(100);
+        LEGAL_TAG_NAME_B = LegalTagUtils.createRandomName();
+
+        LegalTagUtils.create(LEGAL_TAG_NAME_A, token);
+        LegalTagUtils.create(LEGAL_TAG_NAME_B, token);
+
+		String jsonInput = RecordUtil.createDefaultJsonRecord(RECORD_ID, KIND, LEGAL_TAG_NAME_A);
+
+		CloseableHttpResponse response = TestUtils.send("records", "PUT", HeaderUtils.getHeaders(TenantUtils.getTenantName(), token), jsonInput, "");
+		assertEquals(HttpStatus.SC_CREATED, response.getCode());
+		assertTrue(response.getEntity().getContentType().contains("application/json"));
+	}
+
+	public static void classTearDown(String token) throws Exception {
+		TestUtils.send("records/" + RECORD_ID, "DELETE", HeaderUtils.getHeaders(TenantUtils.getTenantName(), token), "", "");
+        LegalTagUtils.delete(LEGAL_TAG_NAME_A, token);
+        Thread.sleep(100);
+        LegalTagUtils.delete(LEGAL_TAG_NAME_B, token);
+	}
+
+	@Test
+	public void should_getRecord_when_validRecordIdIsProvided() throws Exception {
+		CloseableHttpResponse response = TestUtils.send("records/" + RECORD_ID, "GET", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "", "");
+		assertEquals(HttpStatus.SC_OK, response.getCode());
+
+		JsonObject json = JsonParser.parseString(EntityUtils.toString(response.getEntity())).getAsJsonObject();
+		JsonObject dataJson = json.get("data").getAsJsonObject();
+		JsonObject acl = json.get("acl").getAsJsonObject();
+
+		assertEquals(RECORD_ID, json.get("id").getAsString());
+		assertEquals(KIND, json.get("kind").getAsString());
+		assertEquals(TestUtils.getAcl(), acl.get("owners").getAsString());
+		assertEquals(TestUtils.getAcl(), acl.get("viewers").getAsString());
+
+		assertEquals("58377304471659395", dataJson.get("int-tag").getAsJsonObject().get("score-int").toString());
+		assertEquals("5.837730447165939E7",
+				dataJson.get("double-tag").getAsJsonObject().get("score-double").toString());
+		assertEquals("123456789", dataJson.get("count").toString());
+	}
+
+	@Test
+	public void should_getRecord_withoutDuplicates_when_duplicateAclAndLegaltagsAreProvided() throws Exception {
+		String jsonInputWithDuplicates = RecordUtil.createRecordWithDuplicateAclAndLegaltags(ANOTHER_RECORD_ID, KIND, LEGAL_TAG_NAME_A);
+		CloseableHttpResponse putResponse = TestUtils.send("records", "PUT", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), jsonInputWithDuplicates, "");
+		assertEquals(HttpStatus.SC_CREATED, putResponse.getCode());
+		assertTrue(putResponse.getEntity().getContentType().contains("application/json"));
+
+		CloseableHttpResponse response = TestUtils.send("records/" + ANOTHER_RECORD_ID, "GET", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "", "");
+		assertEquals(HttpStatus.SC_OK, response.getCode());
+
+		JsonObject json = JsonParser.parseString(EntityUtils.toString(response.getEntity())).getAsJsonObject();
+		JsonObject acl = json.get("acl").getAsJsonObject();
+		JsonObject legal = json.get("legal").getAsJsonObject();
+
+		assertEquals(ANOTHER_RECORD_ID, json.get("id").getAsString());
+		assertEquals(KIND, json.get("kind").getAsString());
+		assertEquals(LEGAL_TAG_NAME_A, legal.get("legaltags").getAsString());
+		assertEquals(TestUtils.getAcl(), acl.get("owners").getAsString());
+		assertEquals(TestUtils.getAcl(), acl.get("viewers").getAsString());
+	}
+
+	@Test
+	public void should_getOnlyTheCertainDataFields_when_attributesAreProvided() throws Exception {
+		CloseableHttpResponse response = TestUtils.send("records/" + RECORD_ID, "GET", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "",
+				"?attribute=data.count&attribute=data.int-tag.score-int");
+		assertEquals(HttpStatus.SC_OK, response.getCode());
+
+		JsonObject json = JsonParser.parseString(EntityUtils.toString(response.getEntity())).getAsJsonObject();
+		JsonObject dataJson = json.get("data").getAsJsonObject();
+		JsonObject acl = json.get("acl").getAsJsonObject();
+
+		assertEquals(RECORD_ID, json.get("id").getAsString());
+		assertEquals(KIND, json.get("kind").getAsString());
+		assertEquals(TestUtils.getAcl(), acl.get("owners").getAsString());
+		assertEquals(TestUtils.getAcl(), acl.get("viewers").getAsString());
+
+		assertEquals("58377304471659395", dataJson.get("int-tag.score-int").getAsString());
+		assertNull(dataJson.get("double-tag"));
+		assertEquals("123456789", dataJson.get("count").toString());
+	}
+
+	@Test
+	public void should_notReturnFieldsAlreadyInDatastore_when_returningRecord() throws Exception {
+		CloseableHttpResponse response = TestUtils.send("records/" + RECORD_ID, "GET", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "", "");
+		assertEquals(HttpStatus.SC_OK, response.getCode());
+
+		JsonObject json = JsonParser.parseString(EntityUtils.toString(response.getEntity())).getAsJsonObject();
+
+		assertNotNull(json.get("id"));
+		assertNotNull(json.get("kind"));
+		assertNotNull(json.get("acl"));
+		assertNotNull(json.get("version"));
+		assertNotNull(json.get("data"));
+		assertNotNull(json.get("createTime"));
+
+		assertNull(json.get("bucket"));
+		assertNull(json.get("status"));
+		assertNull(json.get("modifyUser"));
+		assertNull(json.get("modifyTime"));
+	}
+
+    @Test
+    public void should_legaltagChange_when_updateRecordWithLegaltag() throws Exception {
+		String newJsonInput = RecordUtil.createDefaultJsonRecord(RECORD_ID, KIND, LEGAL_TAG_NAME_B);
+        CloseableHttpResponse response = TestUtils.send("records", "PUT", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), newJsonInput, "?skipdupes=false");
+        assertEquals(HttpStatus.SC_CREATED, response.getCode());
+
+        response = TestUtils.send("records/" + RECORD_ID, "GET", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "", "");
+        assertEquals(HttpStatus.SC_OK, response.getCode());
+
+        JsonObject json = JsonParser.parseString(EntityUtils.toString(response.getEntity())).getAsJsonObject();
+
+        assertEquals(RECORD_ID, json.get("id").getAsString());
+        assertEquals(KIND, json.get("kind").getAsString());
+
+        JsonArray legaltags = json.get("legal").getAsJsonObject().get("legaltags").getAsJsonArray();
+        String updatedLegaltag = legaltags.get(0).getAsString();
+        assertEquals(1, legaltags.size());
+        assertEquals(LEGAL_TAG_NAME_B, updatedLegaltag);
+    }
+}
diff --git a/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/records/IngestRecordNotFoundTest.java b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/records/IngestRecordNotFoundTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..c4c24b3d550317524a35cfcc8327f9f7cd2bf0b4
--- /dev/null
+++ b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/records/IngestRecordNotFoundTest.java
@@ -0,0 +1,91 @@
+// Copyright 2017-2019, Schlumberger
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package org.opengroup.osdu.storage.records;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
+import org.apache.http.HttpStatus;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.opengroup.osdu.storage.util.HeaderUtils;
+import org.opengroup.osdu.storage.util.LegalTagUtils;
+import org.opengroup.osdu.storage.util.RecordUtil;
+import org.opengroup.osdu.storage.util.TenantUtils;
+import org.opengroup.osdu.storage.util.TestBase;
+import org.opengroup.osdu.storage.util.TestUtils;
+import org.opengroup.osdu.storage.util.TokenTestUtils;
+
+public final class IngestRecordNotFoundTest extends TestBase {
+
+	private static final long NOW = System.currentTimeMillis();
+	private static final String LEGAL_TAG = LegalTagUtils.createRandomName();
+
+	private static final String KIND = TenantUtils.getTenantName() + ":test:endtoend:1.1." + NOW;
+	private static final String RECORD_ID = TenantUtils.getTenantName() + ":endtoend:1.1." + NOW;
+
+	public static void classSetup(String token) throws Exception {
+		LegalTagUtils.create(LEGAL_TAG, token);
+	}
+
+	public static void classTearDown(String token) throws Exception {
+		LegalTagUtils.delete(LEGAL_TAG, token);
+	}
+
+	private static final TokenTestUtils TOKEN_TEST_UTILS = new TokenTestUtils();
+
+	@BeforeAll
+	public static void classSetup() throws Exception {
+		IngestRecordNotFoundTest.classSetup(TOKEN_TEST_UTILS.getToken());
+	}
+
+	@AfterAll
+	public static void classTearDown() throws Exception {
+		IngestRecordNotFoundTest.classTearDown(TOKEN_TEST_UTILS.getToken());
+	}
+
+	@BeforeEach
+	@Override
+	public void setup() throws Exception {
+		this.testUtils = new TokenTestUtils();
+	}
+
+	@AfterEach
+	@Override
+	public void tearDown() throws Exception {
+		this.testUtils = null;
+	}
+
+	@Test
+	public void should_returnBadRequest_when_userGroupDoesNotExist() throws Exception {
+
+		String group = String.format("data.thisDataGrpDoesNotExsist@%s", TestUtils.getAclSuffix());
+
+		String record = RecordUtil.createDefaultJsonRecord(RECORD_ID, KIND, LEGAL_TAG).replace(TestUtils.getAcl(), group);
+
+		CloseableHttpResponse response = TestUtils.send("records", "PUT", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), record, "");
+
+		String result = TestUtils.getResult(response, HttpStatus.SC_BAD_REQUEST, String.class);
+		JsonObject jsonResponse = JsonParser.parseString(result).getAsJsonObject();
+		assertEquals("Error on writing record", jsonResponse.get("reason").getAsString());
+		assertEquals("Could not find group \"" + group + "\".",
+				jsonResponse.get("message").getAsString());
+	}
+}
diff --git a/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/records/LogicalBatchRecordsDeleteTests.java b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/records/LogicalBatchRecordsDeleteTests.java
new file mode 100644
index 0000000000000000000000000000000000000000..c74ef227e102b27d41319d656bc596fa429cf46f
--- /dev/null
+++ b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/records/LogicalBatchRecordsDeleteTests.java
@@ -0,0 +1,165 @@
+// Copyright 2017-2021, Schlumberger
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package org.opengroup.osdu.storage.records;
+
+import static org.apache.commons.lang3.StringUtils.EMPTY;
+import static org.apache.http.HttpStatus.SC_MULTI_STATUS;
+import static org.apache.http.HttpStatus.SC_NOT_FOUND;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import com.google.common.collect.Lists;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import java.util.List;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
+import org.apache.hc.core5.http.io.entity.EntityUtils;
+import org.apache.http.HttpStatus;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.opengroup.osdu.storage.util.HeaderUtils;
+import org.opengroup.osdu.storage.util.LegalTagUtils;
+import org.opengroup.osdu.storage.util.TenantUtils;
+import org.opengroup.osdu.storage.util.TestBase;
+import org.opengroup.osdu.storage.util.TestUtils;
+import org.opengroup.osdu.storage.util.TokenTestUtils;
+
+public final class LogicalBatchRecordsDeleteTests extends TestBase {
+
+    private static final long NOW = System.currentTimeMillis();
+    private static final String KIND = TenantUtils.getTenantName() + ":delete:inttest:1.0." + NOW;
+    private static final String LEGAL_TAG = LegalTagUtils.createRandomName();
+    private static final String RECORD_ID_1 = TenantUtils.getTenantName() + ":testint:" + NOW;
+    private static final String RECORD_ID_2 = TenantUtils.getTenantName() + ":testint:" + NOW;
+    private static final String NOT_EXISTED_RECORD_ID = TenantUtils.getFirstTenantName() + ":notexisted:" + NOW;
+
+    private static final TokenTestUtils TOKEN_TEST_UTILS = new TokenTestUtils();
+
+    @BeforeEach
+    @Override
+    public void setup() throws Exception {
+        this.testUtils = new TokenTestUtils();
+        this.setup(TOKEN_TEST_UTILS.getToken());
+    }
+
+    @AfterEach
+    @Override
+    public void tearDown() throws Exception {
+        this.tearDown(TOKEN_TEST_UTILS.getToken());
+        this.testUtils = null;
+    }
+
+    @Test
+    public void should_deleteRecordsLogically_successfully() throws Exception {
+        String requestBody = String.format("[\"%s\",\"%s\"]", RECORD_ID_1, RECORD_ID_2);
+
+        CloseableHttpResponse response = TestUtils.send("records/delete", "POST",
+                HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), requestBody, EMPTY);
+        assertEquals(HttpStatus.SC_NO_CONTENT, response.getCode());
+
+        response = TestUtils.send("records/" + RECORD_ID_1, "GET",
+                HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "", "");
+        assertEquals(HttpStatus.SC_NOT_FOUND, response.getCode());
+
+        response = TestUtils.send("records/" + RECORD_ID_2, "GET",
+                HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "", "");
+        assertEquals(HttpStatus.SC_NOT_FOUND, response.getCode());
+    }
+
+    @Test
+    public void should_deleteRecordsLogically_withPartialSuccess_whenOneRecordNotFound() throws Exception {
+        String requestBody = String.format("[\"%s\",\"%s\",\"%s\"]", RECORD_ID_1, RECORD_ID_2, NOT_EXISTED_RECORD_ID);
+
+        CloseableHttpResponse deleteResponse = TestUtils.send("records/delete", "POST", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()),
+                requestBody, EMPTY);
+
+        assertEquals(SC_MULTI_STATUS, deleteResponse.getCode());
+
+        JsonArray jsonBody = JsonParser.parseString(EntityUtils.toString(deleteResponse.getEntity())).getAsJsonArray();
+
+        assertEquals(1, jsonBody.size());
+        assertEquals(getValueFromDeleteResponseJsonArray(jsonBody, "notDeletedRecordId"), NOT_EXISTED_RECORD_ID);
+        assertEquals(getValueFromDeleteResponseJsonArray(jsonBody, "message"), "Record with id '" + NOT_EXISTED_RECORD_ID + "' not found");
+
+        CloseableHttpResponse response = TestUtils.send("records/" + RECORD_ID_1, "GET", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "", "");
+        assertEquals(SC_NOT_FOUND, response.getCode());
+
+        response = TestUtils.send("records/" + RECORD_ID_2, "GET",HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "", "");
+        assertEquals(SC_NOT_FOUND, response.getCode());
+    }
+
+    public void setup(String token) throws Exception {
+        LegalTagUtils.create(LEGAL_TAG, token);
+
+        String firstBody = createBody(RECORD_ID_1, "anything", Lists.newArrayList(LEGAL_TAG), Lists.newArrayList("BR", "IT"));
+        String secondBody = createBody(RECORD_ID_2, "anything", Lists.newArrayList(LEGAL_TAG), Lists.newArrayList("BR", "IT"));
+
+        CloseableHttpResponse firstResponse = TestUtils.send("records", "PUT",
+                HeaderUtils.getHeaders(TenantUtils.getTenantName(), token), firstBody, "");
+        CloseableHttpResponse secondResponse = TestUtils.send("records", "PUT",
+                HeaderUtils.getHeaders(TenantUtils.getTenantName(), token), secondBody, "");
+
+        assertEquals(HttpStatus.SC_CREATED, firstResponse.getCode());
+        assertEquals(HttpStatus.SC_CREATED, secondResponse.getCode());
+    }
+
+    public void tearDown(String token) throws Exception {
+        TestUtils.send("records/" + RECORD_ID_1, "DELETE", HeaderUtils.getHeaders(TenantUtils.getTenantName(), token), "", "");
+        TestUtils.send("records/" + RECORD_ID_2, "DELETE", HeaderUtils.getHeaders(TenantUtils.getTenantName(), token), "", "");
+
+        LegalTagUtils.delete(LEGAL_TAG, token);
+    }
+
+    protected static String createBody(String id, String dataValue, List<String> legalTags, List<String> ordc) {
+        JsonObject data = new JsonObject();
+        data.addProperty("name", dataValue);
+
+        JsonObject acl = new JsonObject();
+        JsonArray acls = new JsonArray();
+        acls.add(TestUtils.getAcl());
+        acl.add("viewers", acls);
+        acl.add("owners", acls);
+
+        JsonArray tags = new JsonArray();
+        legalTags.forEach(t -> tags.add(t));
+
+        JsonArray ordcJson = new JsonArray();
+        ordc.forEach(o -> ordcJson.add(o));
+
+        JsonObject legal = new JsonObject();
+        legal.add("legaltags", tags);
+        legal.add("otherRelevantDataCountries", ordcJson);
+
+        JsonObject record = new JsonObject();
+        record.addProperty("id", id);
+        record.addProperty("kind", KIND);
+        record.add("acl", acl);
+        record.add("legal", legal);
+        record.add("data", data);
+
+        JsonArray records = new JsonArray();
+        records.add(record);
+
+        return records.toString();
+    }
+
+    private String getValueFromDeleteResponseJsonArray(JsonArray jsonBody, String propertyName) {
+        if(jsonBody.isEmpty()){
+            throw new RuntimeException("Not able to fetch property: %s from response body. Response body is empty.".formatted(propertyName));
+        }
+        return jsonBody.get(0).getAsJsonObject().get(propertyName).getAsString();
+    }
+}
diff --git a/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/records/LogicalRecordDeleteTests.java b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/records/LogicalRecordDeleteTests.java
new file mode 100644
index 0000000000000000000000000000000000000000..0075fd559aeec9562deb2c054450f99377eca36e
--- /dev/null
+++ b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/records/LogicalRecordDeleteTests.java
@@ -0,0 +1,151 @@
+// Copyright 2017-2019, Schlumberger
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package org.opengroup.osdu.storage.records;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.google.common.collect.Lists;
+import com.google.gson.Gson;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonObject;
+import java.util.List;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
+import org.apache.hc.core5.http.io.entity.EntityUtils;
+import org.apache.http.HttpStatus;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.opengroup.osdu.storage.util.DummyRecordsHelper.CreateRecordResponse;
+import org.opengroup.osdu.storage.util.HeaderUtils;
+import org.opengroup.osdu.storage.util.LegalTagUtils;
+import org.opengroup.osdu.storage.util.TenantUtils;
+import org.opengroup.osdu.storage.util.TestBase;
+import org.opengroup.osdu.storage.util.TestUtils;
+import org.opengroup.osdu.storage.util.TokenTestUtils;
+
+public final class LogicalRecordDeleteTests extends TestBase {
+
+	private static final long NOW = System.currentTimeMillis();
+	private static final String KIND = TenantUtils.getTenantName() + ":delete:inttest:1.0." + NOW;
+	private static final String LEGAL_TAG = LegalTagUtils.createRandomName();
+	private static final String RECORD_ID = TenantUtils.getTenantName() + ":inttest:" + NOW;
+
+	private static final TokenTestUtils TOKEN_TEST_UTILS = new TokenTestUtils();
+
+	@BeforeAll
+	public static void classSetup() throws Exception {
+		LogicalRecordDeleteTests.classSetup(TOKEN_TEST_UTILS.getToken());
+	}
+
+	@AfterAll
+	public static void classTearDown() throws Exception {
+		LogicalRecordDeleteTests.classTearDown(TOKEN_TEST_UTILS.getToken());
+	}
+
+	@BeforeEach
+	@Override
+	public void setup() throws Exception {
+		this.testUtils = new TokenTestUtils();
+	}
+
+	@AfterEach
+	@Override
+	public void tearDown() throws Exception {
+		this.testUtils = null;
+	}
+
+	public static void classSetup(String token) throws Exception {
+		LegalTagUtils.create(LEGAL_TAG, token);
+
+		String body = createBody(RECORD_ID, "anything", Lists.newArrayList(LEGAL_TAG), Lists.newArrayList("BR", "IT"));
+
+		CloseableHttpResponse response = TestUtils.send("records", "PUT",
+				HeaderUtils.getHeaders(TenantUtils.getTenantName(), token), body, "");
+
+		String responseBody = EntityUtils.toString(response.getEntity());
+		assertEquals(HttpStatus.SC_CREATED, response.getCode());
+		assertTrue(response.getEntity().getContentType().contains("application/json"));
+
+		Gson gson = new Gson();
+		CreateRecordResponse result = gson.fromJson(responseBody, CreateRecordResponse.class);
+
+		assertEquals(1, result.recordCount);
+		assertEquals(1, result.recordIds.length);
+		assertEquals(1, result.recordIdVersions.length);
+		assertEquals(RECORD_ID, result.recordIds[0]);
+	}
+
+	public static void classTearDown(String token) throws Exception {
+		TestUtils.send("records/" + RECORD_ID, "DELETE", HeaderUtils.getHeaders(TenantUtils.getTenantName(), token), "", "");
+
+		LegalTagUtils.delete(LEGAL_TAG, token);
+	}
+
+	@Test
+	public void should_notRetrieveRecord_and_notDeleteRecordAgain_when_deletingItLogically() throws Exception {
+		String queryParam = String.format("records/%s:delete", RECORD_ID);
+
+		// deleting
+		CloseableHttpResponse response = TestUtils.send(queryParam, "POST",
+				HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "{'anything':'teste'}", "");
+		assertEquals(HttpStatus.SC_NO_CONTENT, response.getCode());
+
+		// trying to get
+		response = TestUtils.send("records/" + RECORD_ID, "GET",
+				HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "", "");
+		assertEquals(HttpStatus.SC_NOT_FOUND, response.getCode());
+
+		// trying to delete again
+		response = TestUtils.send(queryParam, "POST",
+				HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "{'anything':'teste'}", "");
+		assertEquals(HttpStatus.SC_NOT_FOUND, response.getCode());
+	}
+
+	protected static String createBody(String id, String dataValue, List<String> legalTags, List<String> ordc) {
+		JsonObject data = new JsonObject();
+		data.addProperty("name", dataValue);
+
+		JsonObject acl = new JsonObject();
+		JsonArray acls = new JsonArray();
+		acls.add(TestUtils.getAcl());
+		acl.add("viewers", acls);
+		acl.add("owners", acls);
+
+		JsonArray tags = new JsonArray();
+		legalTags.forEach(t -> tags.add(t));
+
+		JsonArray ordcJson = new JsonArray();
+		ordc.forEach(o -> ordcJson.add(o));
+
+		JsonObject legal = new JsonObject();
+		legal.add("legaltags", tags);
+		legal.add("otherRelevantDataCountries", ordcJson);
+
+		JsonObject record = new JsonObject();
+		record.addProperty("id", id);
+		record.addProperty("kind", KIND);
+		record.add("acl", acl);
+		record.add("legal", legal);
+		record.add("data", data);
+
+		JsonArray records = new JsonArray();
+		records.add(record);
+
+		return records.toString();
+	}
+}
diff --git a/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/records/ParentRecordValidationTest.java b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/records/ParentRecordValidationTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..95ddb5cdfba7e2c0ec1755f0bc8cbe60ddf42954
--- /dev/null
+++ b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/records/ParentRecordValidationTest.java
@@ -0,0 +1,105 @@
+// Copyright 2017-2019, Schlumberger
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package org.opengroup.osdu.storage.records;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import com.google.gson.JsonParser;
+import java.util.Arrays;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
+import org.apache.hc.core5.http.io.entity.EntityUtils;
+import org.apache.http.HttpStatus;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.opengroup.osdu.storage.util.HeaderUtils;
+import org.opengroup.osdu.storage.util.LegalTagUtils;
+import org.opengroup.osdu.storage.util.RecordUtil;
+import org.opengroup.osdu.storage.util.TenantUtils;
+import org.opengroup.osdu.storage.util.TestBase;
+import org.opengroup.osdu.storage.util.TestUtils;
+import org.opengroup.osdu.storage.util.TokenTestUtils;
+
+public final class ParentRecordValidationTest extends TestBase {
+
+    private static long NOW = System.currentTimeMillis();
+    private static String LEGAL_TAG = LegalTagUtils.createRandomName();
+    private static String KIND = TenantUtils.getFirstTenantName() + ":bulkupdate:test:1.1." + NOW;
+    private static String RECORD_ID = TenantUtils.getFirstTenantName() + ":test:1.1." + NOW;
+    private static String RECORD_ID_2 = TenantUtils.getFirstTenantName() + ":test:1.2." + NOW;
+    private static String RECORD_ID_3 = TenantUtils.getFirstTenantName() + ":test:1.3." + NOW;
+
+    @BeforeEach
+    public void setup() throws Exception {
+        this.testUtils = new TokenTestUtils();
+        LegalTagUtils.create(LEGAL_TAG, testUtils.getToken());
+    }
+
+    @AfterEach
+    public void tearDown() throws Exception {
+        LegalTagUtils.delete(LEGAL_TAG, testUtils.getToken());
+        for (String record_id : Arrays.asList(RECORD_ID, RECORD_ID_2, RECORD_ID_3))
+        {
+            TestUtils.send(
+                "records/" + record_id,
+                "DELETE",
+                HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()),
+                "",
+                ""
+            );
+        }
+        this.testUtils = null;
+    }
+
+    @Test
+    public void shouldReturn200_whenRecordContainsValidAncestry() throws Exception {
+        CloseableHttpResponse response = TestUtils.send("records", "PUT", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()),
+                RecordUtil.createDefaultJsonRecord(RECORD_ID, KIND, LEGAL_TAG), "");
+
+        String responseString = EntityUtils.toString(response.getEntity());
+        String parentIdWithVersion = JsonParser
+                .parseString(responseString)
+                .getAsJsonObject()
+                .get("recordIdVersions")
+                .getAsJsonArray()
+                .get(0).getAsString();
+
+        CloseableHttpResponse response2 = TestUtils.send("records", "PUT", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()),
+                RecordUtil.createDefaultJsonRecordWithParentId(RECORD_ID_2, KIND, LEGAL_TAG, parentIdWithVersion), "");
+
+        assertEquals(HttpStatus.SC_CREATED, response.getCode());
+        assertEquals(HttpStatus.SC_CREATED, response2.getCode());
+    }
+
+    @Test
+    public void shouldReturn404_whenRecordAncestryNotExisted() throws Exception {
+
+        String parentIdWithVersion = "opendes:test:1.1.1000000000000:1000000000000000";
+        CloseableHttpResponse response = TestUtils.send("records", "PUT", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()),
+                RecordUtil.createDefaultJsonRecordWithParentId(RECORD_ID_3, KIND, LEGAL_TAG, parentIdWithVersion), "");
+
+
+        String expectedErrorMessage = "The record 'RecordIdWithVersion(recordId=opendes:test:1.1.1000000000000, recordVersion=1000000000000000)' was not found";
+        String actualErrorMessage = JsonParser
+            .parseString(EntityUtils.toString(response.getEntity()))
+            .getAsJsonObject()
+            .get("message")
+            .getAsString();
+
+        assertEquals(HttpStatus.SC_NOT_FOUND, response.getCode());
+        assertEquals(expectedErrorMessage, actualErrorMessage);
+    }
+}
+
diff --git a/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/records/PatchRecordsTest.java b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/records/PatchRecordsTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..293ff6b273a904f5fef56ca68a2f8436f14ba892
--- /dev/null
+++ b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/records/PatchRecordsTest.java
@@ -0,0 +1,330 @@
+// Copyright 2017-2023, Schlumberger
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package org.opengroup.osdu.storage.records;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonObject;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
+import org.apache.http.HttpStatus;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.opengroup.osdu.storage.util.DummyRecordsHelper;
+import org.opengroup.osdu.storage.util.HeaderUtils;
+import org.opengroup.osdu.storage.util.LegalTagUtils;
+import org.opengroup.osdu.storage.util.RecordUtil;
+import org.opengroup.osdu.storage.util.TenantUtils;
+import org.opengroup.osdu.storage.util.TestBase;
+import org.opengroup.osdu.storage.util.TestUtils;
+import org.opengroup.osdu.storage.util.TokenTestUtils;
+
+public final class PatchRecordsTest extends TestBase {
+
+    private static long NOW = System.currentTimeMillis();
+    private static String LEGAL_TAG = LegalTagUtils.createRandomName();
+    private static String LEGAL_TAG_TO_BE_PATCHED = LegalTagUtils.createRandomName() + "1";
+    private static String KIND = TenantUtils.getFirstTenantName() + ":bulkupdate:test:1.1." + NOW;
+    private static String KIND_TO_BE_PATCHED = TenantUtils.getFirstTenantName() + ":bulkupdate:test:1.2." + NOW;
+    private static String RECORD_ID1 = TenantUtils.getFirstTenantName() + ":test:1.1." + NOW;
+    private static String RECORD_ID2 = TenantUtils.getFirstTenantName() + ":test:1.2." + NOW;
+    private static final int MAX_OP_NUMBER = 100;
+
+    private static final DummyRecordsHelper RECORDS_HELPER = new DummyRecordsHelper();
+
+    @BeforeEach
+    public void setup() throws Exception {
+        this.testUtils = new TokenTestUtils();
+        LegalTagUtils.create(LEGAL_TAG, testUtils.getToken());
+        LegalTagUtils.create(LEGAL_TAG_TO_BE_PATCHED, testUtils.getToken());
+
+        CloseableHttpResponse response = TestUtils.send("records", "PUT", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()),
+                RecordUtil.createDefaultJsonRecord(RECORD_ID1, KIND, LEGAL_TAG), "");
+        assertEquals(HttpStatus.SC_CREATED, response.getCode());
+
+        response = TestUtils.send("records", "PUT", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()),
+                RecordUtil.createDefaultJsonRecord(RECORD_ID2, KIND, LEGAL_TAG), "");
+        assertEquals(HttpStatus.SC_CREATED, response.getCode());
+    }
+
+    @AfterEach
+    public void tearDown() throws Exception {
+        LegalTagUtils.delete(LEGAL_TAG, testUtils.getToken());
+        LegalTagUtils.delete(LEGAL_TAG_TO_BE_PATCHED, testUtils.getToken());
+        TestUtils.send("records/" + RECORD_ID1, "DELETE", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "", "");
+        TestUtils.send("records/" + RECORD_ID2, "DELETE", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "", "");
+        this.testUtils = null;
+    }
+
+    @Test
+    public void should_updateOnlyMetadata_whenOnlyMetadataIsPatched() throws Exception {
+        List<String> records = new ArrayList<>();
+        records.add(RECORD_ID1);
+        records.add(RECORD_ID2);
+        CloseableHttpResponse queryResponse = queryRecordsResponse(records);
+        assertEquals(HttpStatus.SC_OK, queryResponse.getCode());
+
+        DummyRecordsHelper.ConvertedRecordsMock queryResponseObject = RECORDS_HELPER.getConvertedRecordsMockFromResponse(queryResponse);
+        assertQueryResponse(queryResponseObject, 2);
+        String currentVersionRecord1 = queryResponseObject.records[0].version;
+        String currentVersionRecord2 = queryResponseObject.records[1].version;
+        assertEquals(null, queryResponseObject.records[0].modifyTime);
+        assertEquals(null, queryResponseObject.records[0].modifyUser);
+
+        CloseableHttpResponse patchResponse = TestUtils.sendWithCustomMediaType("records", "PATCH", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "application/json-patch+json", getPatchPayload(records, true, false), "");
+        assertEquals(HttpStatus.SC_OK, patchResponse.getCode());
+
+        queryResponse = queryRecordsResponse(records);
+        assertEquals(HttpStatus.SC_OK, queryResponse.getCode());
+
+        queryResponseObject = RECORDS_HELPER.getConvertedRecordsMockFromResponse(queryResponse);
+        //modifyUser and modifyTime are not reflected appropriately, please refer to this issue https://community.opengroup.org/osdu/platform/system/storage/-/issues/171
+        assertEquals(currentVersionRecord1, queryResponseObject.records[0].version);
+        assertEquals(currentVersionRecord2, queryResponseObject.records[1].version);
+        assertEquals(2, queryResponseObject.records.length);
+        assertEquals(KIND_TO_BE_PATCHED, queryResponseObject.records[0].kind);
+        assertEquals(KIND_TO_BE_PATCHED, queryResponseObject.records[1].kind);
+        assertEquals(TestUtils.getAcl(), queryResponseObject.records[0].acl.viewers[0]);
+        assertEquals(TestUtils.getAcl(), queryResponseObject.records[1].acl.viewers[0]);
+        assertEquals(TestUtils.getIntegrationTesterAcl(), queryResponseObject.records[0].acl.owners[0]);
+        assertEquals(TestUtils.getIntegrationTesterAcl(), queryResponseObject.records[1].acl.owners[0]);
+        assertTrue(Arrays.stream(queryResponseObject.records[0].legal.legaltags).anyMatch(LEGAL_TAG::equals));
+        assertTrue(Arrays.stream(queryResponseObject.records[1].legal.legaltags).anyMatch(LEGAL_TAG::equals));
+        assertTrue(Arrays.stream(queryResponseObject.records[0].legal.legaltags).anyMatch(LEGAL_TAG_TO_BE_PATCHED::equals));
+        assertTrue(Arrays.stream(queryResponseObject.records[1].legal.legaltags).anyMatch(LEGAL_TAG_TO_BE_PATCHED::equals));
+        Map<String, String> tags = queryResponseObject.records[0].tags;
+        assertTrue(tags.containsKey("tag1"));
+        assertTrue(tags.containsKey("tag2"));
+        assertEquals("value1", tags.get("tag1"));
+        assertEquals("value2", tags.get("tag2"));
+        tags = queryResponseObject.records[1].tags;
+        assertTrue(tags.containsKey("tag1"));
+        assertTrue(tags.containsKey("tag2"));
+        assertEquals("value1", tags.get("tag1"));
+        assertEquals("value2", tags.get("tag2"));
+    }
+
+    @Test
+    public void should_updateDataAndMetadataVersion_whenOnlyDataIsPatched() throws Exception {
+        List<String> records = new ArrayList<>();
+        records.add(RECORD_ID1);
+        CloseableHttpResponse queryResponse = queryRecordsResponse(records);
+        assertEquals(HttpStatus.SC_OK, queryResponse.getCode());
+
+        DummyRecordsHelper.ConvertedRecordsMock queryResponseObject = RECORDS_HELPER.getConvertedRecordsMockFromResponse(queryResponse);
+        assertQueryResponse(queryResponseObject, 1);
+        String currentVersionRecord1 = queryResponseObject.records[0].version;
+
+        CloseableHttpResponse patchResponse = TestUtils.sendWithCustomMediaType("records", "PATCH", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "application/json-patch+json", getPatchPayload(records, false, true), "");
+        assertEquals(HttpStatus.SC_OK, patchResponse.getCode());
+
+        queryResponse = queryRecordsResponse(records);
+        assertEquals(HttpStatus.SC_OK, queryResponse.getCode());
+
+        queryResponseObject = RECORDS_HELPER.getConvertedRecordsMockFromResponse(queryResponse);
+        assertNotEquals(currentVersionRecord1, queryResponseObject.records[0].version);
+        assertEquals(KIND, queryResponseObject.records[0].kind);
+        assertTrue(queryResponseObject.records[0].data.containsKey("data"));
+        assertTrue(queryResponseObject.records[0].data.get("data").toString().equals("{message=test data}"));
+        assertQueryResponse(queryResponseObject, 1);
+    }
+
+    @Test
+    public void should_updateBothMetadataAndData_whenDataAndMetadataArePatched() throws Exception {
+        List<String> records = new ArrayList<>();
+        records.add(RECORD_ID1);
+        CloseableHttpResponse queryResponse = queryRecordsResponse(records);
+        assertEquals(HttpStatus.SC_OK, queryResponse.getCode());
+
+        DummyRecordsHelper.ConvertedRecordsMock queryResponseObject = RECORDS_HELPER.getConvertedRecordsMockFromResponse(queryResponse);
+        assertQueryResponse(queryResponseObject, 1);
+        String currentVersionRecord = queryResponseObject.records[0].version;
+
+        CloseableHttpResponse patchResponse = TestUtils.sendWithCustomMediaType("records", "PATCH", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "application/json-patch+json", getPatchPayload(records, true, true), "");
+        assertEquals(HttpStatus.SC_OK, patchResponse.getCode());
+
+        queryResponse = queryRecordsResponse(records);
+        assertEquals(HttpStatus.SC_OK, queryResponse.getCode());
+
+        queryResponseObject = RECORDS_HELPER.getConvertedRecordsMockFromResponse(queryResponse);
+        assertEquals(1, queryResponseObject.records.length);
+        assertNotEquals(currentVersionRecord, queryResponseObject.records[0].version);
+        assertEquals(KIND_TO_BE_PATCHED, queryResponseObject.records[0].kind);
+        assertEquals(TestUtils.getAcl(), queryResponseObject.records[0].acl.viewers[0]);
+        assertEquals(TestUtils.getIntegrationTesterAcl(), queryResponseObject.records[0].acl.owners[0]);
+        assertTrue(Arrays.stream(queryResponseObject.records[0].legal.legaltags).anyMatch(LEGAL_TAG::equals));
+        assertTrue(Arrays.stream(queryResponseObject.records[0].legal.legaltags).anyMatch(LEGAL_TAG_TO_BE_PATCHED::equals));
+        Map<String, String> tags = queryResponseObject.records[0].tags;
+        assertTrue(tags.containsKey("tag1"));
+        assertTrue(tags.containsKey("tag2"));
+        assertEquals("value1", tags.get("tag1"));
+        assertEquals("value2", tags.get("tag2"));
+        assertTrue(queryResponseObject.records[0].data.containsKey("data"));
+        assertTrue(queryResponseObject.records[0].data.get("data").toString().equals("{message=test data}"));
+
+    }
+
+    @Test
+    public void should_update_whenNumberOfPatchOperationsIsMaximum() throws Exception {
+        List<String> records = new ArrayList<>();
+        records.add(RECORD_ID1);
+        CloseableHttpResponse queryResponse = queryRecordsResponse(records);
+        assertEquals(HttpStatus.SC_OK, queryResponse.getCode());
+
+        DummyRecordsHelper.ConvertedRecordsMock queryResponseObject = RECORDS_HELPER.getConvertedRecordsMockFromResponse(queryResponse);
+        assertQueryResponse(queryResponseObject, 1);
+        String currentVersionRecord = queryResponseObject.records[0].version;
+
+        CloseableHttpResponse patchResponse = TestUtils.sendWithCustomMediaType("records", "PATCH", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "application/json-patch+json", getMaximumPatchOperationsPayload(records), "");
+        assertEquals(HttpStatus.SC_OK, patchResponse.getCode());
+
+        queryResponse = queryRecordsResponse(records);
+        assertEquals(HttpStatus.SC_OK, queryResponse.getCode());
+
+        queryResponseObject = RECORDS_HELPER.getConvertedRecordsMockFromResponse(queryResponse);
+        assertEquals(1, queryResponseObject.records.length);
+        assertEquals(currentVersionRecord, queryResponseObject.records[0].version);
+        Map<String, String> tags = queryResponseObject.records[0].tags;
+        assertTrue(tags.containsKey("testTag0"));
+        assertTrue(tags.containsKey("testTag99"));
+        assertEquals("value0", tags.get("testTag0"));
+        assertEquals("value99", tags.get("testTag99"));
+    }
+
+    //TODO: add a test to validate same 'op' and 'path' and assert expected behavior
+
+    private CloseableHttpResponse queryRecordsResponse(List<String> recordIds) throws Exception {
+        JsonArray records = new JsonArray();
+        for (String recordId : recordIds) {
+            records.add(recordId);
+        }
+        JsonObject queryBody = new JsonObject();
+        queryBody.add("records", records);
+
+        Map<String, String> queryHeader = HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken());
+        queryHeader.put("frame-of-reference", "none");
+        return TestUtils.send("query/records:batch", "POST", queryHeader, queryBody.toString(), "");
+    }
+
+    private void assertQueryResponse(DummyRecordsHelper.ConvertedRecordsMock queryResponse, int expectedRecordCount) {
+        assertEquals(expectedRecordCount, queryResponse.records.length);
+        assertEquals(TestUtils.getAcl(), queryResponse.records[0].acl.viewers[0]);
+        assertEquals(TestUtils.getAcl(), queryResponse.records[0].acl.owners[0]);
+    }
+
+    private String getPatchPayload(List<String> records, boolean isMetaUpdate, boolean isDataUpdate) {
+        JsonArray recordsJson = new JsonArray();
+        for (String record : records) {
+            recordsJson.add(record);
+        }
+
+        JsonArray ops = new JsonArray();
+        if (isMetaUpdate) {
+            ops.add(getAddTagsPatchOp());
+            ops.add(getReplaceAclOwnersPatchOp());
+            ops.add(getAddLegaltagsPatchOp());
+            ops.add(getReplaceKindPatchOp());
+        }
+        if (isDataUpdate) {
+            ops.add(getReplaceDataPatchOp());
+        }
+
+        return getPatchrequestBody(recordsJson, ops);
+    }
+
+    private JsonObject getAddTagsPatchOp() {
+        JsonObject tagsValue = new JsonObject();
+        tagsValue.addProperty("tag1", "value1");
+        tagsValue.addProperty("tag2", "value2");
+        JsonObject addTagsPatch = new JsonObject();
+        addTagsPatch.addProperty("op", "add");
+        addTagsPatch.addProperty("path", "/tags");
+        addTagsPatch.add("value", tagsValue);
+        return addTagsPatch;
+    }
+
+    private JsonObject getReplaceAclOwnersPatchOp() {
+        JsonArray newAclValue = new JsonArray();
+        newAclValue.add(TestUtils.getIntegrationTesterAcl());
+        JsonObject replaceAclPatch = new JsonObject();
+        replaceAclPatch.addProperty("op", "replace");
+        replaceAclPatch.addProperty("path", "/acl/owners");
+        replaceAclPatch.add("value", newAclValue);
+        return replaceAclPatch;
+    }
+
+    private JsonObject getAddLegaltagsPatchOp() {
+        JsonObject replaceAclPatch = new JsonObject();
+        replaceAclPatch.addProperty("op", "add");
+        replaceAclPatch.addProperty("path", "/legal/legaltags/-");
+        replaceAclPatch.addProperty("value", LEGAL_TAG_TO_BE_PATCHED);
+        return replaceAclPatch;
+    }
+
+    private JsonObject getReplaceKindPatchOp() {
+        JsonObject replaeKindPatch = new JsonObject();
+        replaeKindPatch.addProperty("op", "replace");
+        replaeKindPatch.addProperty("path", "/kind");
+        replaeKindPatch.addProperty("value", KIND_TO_BE_PATCHED);
+        return replaeKindPatch;
+    }
+
+    private JsonObject getReplaceDataPatchOp() {
+        JsonObject newDataValue = new JsonObject();
+        JsonObject innerDataValue = new JsonObject();
+        innerDataValue.addProperty("message", "test data");
+        newDataValue.add("data", innerDataValue);
+        JsonObject replaceDataPatch = new JsonObject();
+        replaceDataPatch.addProperty("op", "replace");
+        replaceDataPatch.addProperty("path", "/data");
+        replaceDataPatch.add("value", newDataValue);
+        return replaceDataPatch;
+    }
+
+    private String getMaximumPatchOperationsPayload(List<String> records) {
+        JsonArray recordsJson = new JsonArray();
+        for (String record : records) {
+            recordsJson.add(record);
+        }
+        JsonArray ops = new JsonArray();
+        for (int i = 0; i < MAX_OP_NUMBER; i++) {
+            JsonObject addTagOperation = new JsonObject();
+            addTagOperation.addProperty("op", "add");
+            addTagOperation.addProperty("path", "/tags/testTag" + i);
+            addTagOperation.addProperty("value", "value" + i);
+            ops.add(addTagOperation);
+        }
+        return getPatchrequestBody(recordsJson, ops);
+    }
+
+    private String getPatchrequestBody(JsonArray recordsJson, JsonArray ops) {
+        JsonObject query = new JsonObject();
+        query.add("ids", recordsJson);
+
+        JsonObject updateBody = new JsonObject();
+        updateBody.add("query", query);
+        updateBody.add("ops", ops);
+
+        return updateBody.toString();
+    }
+
+}
diff --git a/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/records/PurgeRecordsIntegrationTest.java b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/records/PurgeRecordsIntegrationTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..569507eef6b6ccf4138252458986ee3322d1c589
--- /dev/null
+++ b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/records/PurgeRecordsIntegrationTest.java
@@ -0,0 +1,253 @@
+// Copyright 2017-2019, Schlumberger
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package org.opengroup.osdu.storage.records;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
+import org.apache.hc.core5.http.io.entity.EntityUtils;
+import org.apache.http.HttpStatus;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.opengroup.osdu.storage.util.HeaderUtils;
+import org.opengroup.osdu.storage.util.LegalTagUtils;
+import org.opengroup.osdu.storage.util.RecordUtil;
+import org.opengroup.osdu.storage.util.TenantUtils;
+import org.opengroup.osdu.storage.util.TestBase;
+import org.opengroup.osdu.storage.util.TestUtils;
+import org.opengroup.osdu.storage.util.TokenTestUtils;
+
+public final class PurgeRecordsIntegrationTest extends TestBase {
+
+	private static final long NOW = System.currentTimeMillis();
+	private static final String RECORD_ID = TenantUtils.getTenantName() + ":getrecord:" + NOW;
+	private static final String RECORD_ID1 = TenantUtils.getTenantName() + ":getrecord1:" + NOW;
+	private static final String RECORD_ID2 = TenantUtils.getTenantName() + ":getrecord2:" + NOW;
+	private static final String RECORD_ID3 = TenantUtils.getTenantName() + ":getrecord3:" + NOW;
+	private static final String RECORD_ID4 = TenantUtils.getTenantName() + ":getrecord4:" + NOW;
+	private static final String KIND = TenantUtils.getTenantName() + ":ds:getrecord:1.0." + NOW;
+	private static final String LEGAL_TAG = LegalTagUtils.createRandomName();
+	public static final String HTTP_DELETE = "DELETE";
+
+	private static final TokenTestUtils TOKEN_TEST_UTILS = new TokenTestUtils();
+
+	@BeforeAll
+	public static void classSetup() throws Exception {
+		PurgeRecordsIntegrationTest.classSetup(TOKEN_TEST_UTILS.getToken());
+	}
+
+	@AfterAll
+	public static void classTearDown() throws Exception {
+		PurgeRecordsIntegrationTest.classTearDown(TOKEN_TEST_UTILS.getToken());
+	}
+
+	@BeforeEach
+	@Override
+	public void setup() throws Exception {
+		this.testUtils = new TokenTestUtils();
+	}
+
+	@AfterEach
+	@Override
+	public void tearDown() throws Exception {
+		this.testUtils = null;
+	}
+
+	public static void classSetup(String token) throws Exception {
+		LegalTagUtils.create(LEGAL_TAG, token);
+		String jsonInput = RecordUtil.createDefaultJsonRecord(RECORD_ID, KIND, LEGAL_TAG);
+		String jsonInput1 = RecordUtil.createDefaultJsonRecord(RECORD_ID1, KIND, LEGAL_TAG);
+		String jsonInput2 = RecordUtil.createDefaultJsonRecord(RECORD_ID2, KIND, LEGAL_TAG);
+		String jsonInput3 = RecordUtil.createDefaultJsonRecord(RECORD_ID3, KIND, LEGAL_TAG);
+		String jsonInput4 = RecordUtil.createDefaultJsonRecord(RECORD_ID4, KIND, LEGAL_TAG);
+
+		CloseableHttpResponse response = TestUtils.send("records", "PUT", HeaderUtils.getHeaders(TenantUtils.getTenantName(), token), jsonInput, "");
+		assertEquals(HttpStatus.SC_CREATED, response.getCode());
+		assertTrue(response.getEntity().getContentType().contains("application/json"));
+
+		for(int i=0; i < 4; i++) {
+			// Create 4 record versions - for limit scenario
+			CloseableHttpResponse response1 = TestUtils.send("records", "PUT", HeaderUtils.getHeaders(TenantUtils.getTenantName(), token), jsonInput1, "");
+			assertEquals(HttpStatus.SC_CREATED, response1.getCode());
+			assertTrue(response1.getEntity().getContentType().contains("application/json"));
+
+			// Create 4 record versions - for versionIds scenario
+			CloseableHttpResponse response2 = TestUtils.send("records", "PUT", HeaderUtils.getHeaders(TenantUtils.getTenantName(), token), jsonInput2, "");
+			assertEquals(HttpStatus.SC_CREATED, response2.getCode());
+			assertTrue(response2.getEntity().getContentType().contains("application/json"));
+
+			// Create 4 record versions - for fromVersion scenario
+			CloseableHttpResponse response3 = TestUtils.send("records", "PUT", HeaderUtils.getHeaders(TenantUtils.getTenantName(), token), jsonInput3, "");
+			assertEquals(HttpStatus.SC_CREATED, response3.getCode());
+			assertTrue(response3.getEntity().getContentType().contains("application/json"));
+
+			// Create 4 record versions - for bad requests scenario
+			CloseableHttpResponse response4 = TestUtils.send("records", "PUT", HeaderUtils.getHeaders(TenantUtils.getTenantName(), token), jsonInput4, "");
+			assertEquals(HttpStatus.SC_CREATED, response4.getCode());
+			assertTrue(response4.getEntity().getContentType().contains("application/json"));
+
+		}
+	}
+
+	public static void classTearDown(String token) throws Exception {
+		TestUtils.send("records/" + RECORD_ID1, HTTP_DELETE, HeaderUtils.getHeaders(TenantUtils.getTenantName(), token), "", "");
+		TestUtils.send("records/" + RECORD_ID2, HTTP_DELETE, HeaderUtils.getHeaders(TenantUtils.getTenantName(), token), "", "");
+		TestUtils.send("records/" + RECORD_ID3, HTTP_DELETE, HeaderUtils.getHeaders(TenantUtils.getTenantName(), token), "", "");
+		TestUtils.send("records/" + RECORD_ID4, HTTP_DELETE, HeaderUtils.getHeaders(TenantUtils.getTenantName(), token), "", "");
+
+		LegalTagUtils.delete(LEGAL_TAG, token);
+	}
+
+	@Test
+	public void should_ReturnHttp204_when_purgingRecordSuccessfully() throws Exception {
+		CloseableHttpResponse response = TestUtils.send("records/" + RECORD_ID, HTTP_DELETE, HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "", "");
+		assertEquals(HttpStatus.SC_NO_CONTENT, response.getCode());
+
+		response = TestUtils.send("records/" + RECORD_ID, "GET", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "", "");
+		assertEquals(HttpStatus.SC_NOT_FOUND, response.getCode());
+	}
+
+	@Test
+	public void shouldReturnHttp204_whenPurgeRecordVersions_byLimit_isSuccess() throws Exception {
+		String queryParams= "?limit=2";
+		CloseableHttpResponse response = TestUtils.send("records/" + RECORD_ID1 + "/versions", HTTP_DELETE, HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "", queryParams);
+		assertEquals(HttpStatus.SC_NO_CONTENT, response.getCode());
+
+		CloseableHttpResponse getVersionResponse = TestUtils.send("records/versions/" + RECORD_ID1, "GET", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "", "");
+		JsonObject json = JsonParser.parseString(EntityUtils.toString(getVersionResponse.getEntity())).getAsJsonObject();
+		JsonArray versions = json.get("versions").getAsJsonArray();
+		// Record version : 4, limit: 2, Deleted version paths: 2
+		assertEquals(HttpStatus.SC_OK, getVersionResponse.getCode());
+		assertEquals(RECORD_ID1, json.get("recordId").getAsString());
+		assertEquals(2, versions.size());
+	}
+
+	@Test
+	public void shouldReturnHttp204_whenPurgeRecordVersions_byVersionIds_isSuccess() throws Exception {
+		CloseableHttpResponse getVersionResponse = TestUtils.send("records/versions/" + RECORD_ID2, "GET", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "", "");
+		JsonObject json = JsonParser.parseString(EntityUtils.toString(getVersionResponse.getEntity())).getAsJsonObject();
+		JsonArray versions = json.get("versions").getAsJsonArray();
+		String versionIds = versions.get(0).getAsString() + "," + versions.get(1).getAsString();
+
+		String queryParams = "?limit=2&versionIds="+versionIds;
+		CloseableHttpResponse response = TestUtils.send("records/" + RECORD_ID2 + "/versions", HTTP_DELETE, HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "", queryParams);
+		assertEquals(HttpStatus.SC_NO_CONTENT, response.getCode());
+
+		CloseableHttpResponse getVersionResponse1 = TestUtils.send("records/versions/" + RECORD_ID2, "GET", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "", "");
+		JsonObject json1 = JsonParser.parseString(EntityUtils.toString(getVersionResponse1.getEntity())).getAsJsonObject();
+		JsonArray versions1 = json1.get("versions").getAsJsonArray();
+		// Record version : 4, versionIds to delete: 2, Deleted version paths: 2
+		assertEquals(HttpStatus.SC_OK, getVersionResponse1.getCode());
+		assertEquals(RECORD_ID2, json1.get("recordId").getAsString());
+		assertEquals(2, versions1.size());
+	}
+
+	@Test
+	public void shouldReturnHttp204_whenPurgeRecordVersions_byFromVersion_isSuccess() throws Exception {
+		CloseableHttpResponse getVersionResponse = TestUtils.send("records/versions/" + RECORD_ID3, "GET", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "", "");
+		JsonObject json = JsonParser.parseString(EntityUtils.toString(getVersionResponse.getEntity())).getAsJsonObject();
+		JsonArray versions = json.get("versions").getAsJsonArray();
+		String fromVersion =  versions.get(1).getAsString();
+
+		String queryParams = "?from="+fromVersion;
+		CloseableHttpResponse response = TestUtils.send("records/" + RECORD_ID3 + "/versions", HTTP_DELETE, HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "", queryParams);
+		assertEquals(HttpStatus.SC_NO_CONTENT, response.getCode());
+
+		CloseableHttpResponse getVersionResponse1 = TestUtils.send("records/versions/" + RECORD_ID3, "GET", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "", "");
+		JsonObject json1 = JsonParser.parseString(EntityUtils.toString(getVersionResponse1.getEntity())).getAsJsonObject();
+		JsonArray versions1 = json1.get("versions").getAsJsonArray();
+		// Record version : 4, versionIds to delete: 2, Deleted version paths: 2
+		assertEquals(HttpStatus.SC_OK, getVersionResponse1.getCode());
+		assertEquals(RECORD_ID3, json1.get("recordId").getAsString());
+		assertEquals(2, versions1.size());
+	}
+
+	@Test
+	public void shouldReturnHttp400BadRequest_whenPurgeRecordVersions_forInvalidVersionIds() throws Exception {
+		Long versionId1 =  404L;
+		Long versionId2 =  405L;
+		String invalidVersionIds = versionId1+","+versionId2;
+		String queryParams= "?versionIds="+invalidVersionIds;
+
+		CloseableHttpResponse response = TestUtils.send("records/" + RECORD_ID4 + "/versions", HTTP_DELETE, HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "", queryParams);
+		JsonObject jsonObject = JsonParser.parseString(EntityUtils.toString(response.getEntity())).getAsJsonObject();
+		String errorMessage = String.format("Invalid Version Ids. The versionIds contains non existing version(s) '%s'", invalidVersionIds);
+
+		assertEquals(HttpStatus.SC_BAD_REQUEST, response.getCode());
+		assertEquals(HttpStatus.SC_BAD_REQUEST, jsonObject.get("code").getAsInt());
+		assertEquals(errorMessage, jsonObject.get("message").getAsString());
+	}
+
+	@Test
+	public void shouldReturnHttp400BadRequest_whenPurgeRecordVersions_forLimitExceedsRecordVersions() throws Exception {
+		int totalVersions = 4;
+		int limitValue = 5;
+		String queryParams= "?limit="+limitValue;
+
+		CloseableHttpResponse response = TestUtils.send("records/" + RECORD_ID4 + "/versions", HTTP_DELETE, HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "", queryParams);
+		JsonObject jsonObject = JsonParser.parseString(EntityUtils.toString(response.getEntity())).getAsJsonObject();
+		String errorMessage = String.format("The record '%s' version count (excluding latest version) is : %d , which is less than limit value : %d ", RECORD_ID4, totalVersions - 1, limitValue);
+
+		assertEquals(HttpStatus.SC_BAD_REQUEST, response.getCode());
+		assertEquals(HttpStatus.SC_BAD_REQUEST, jsonObject.get("code").getAsInt());
+		assertEquals("Invalid limit.", jsonObject.get("reason").getAsString());
+		assertEquals(errorMessage, jsonObject.get("message").getAsString());
+	}
+
+	@Test
+	public void shouldReturnHttp400BadRequest_whenPurgeRecordVersions_forInvalidFromVersion() throws Exception {
+		Long fromVersion =  404L;
+		String queryParams= "?from="+fromVersion;
+
+		CloseableHttpResponse response = TestUtils.send("records/" + RECORD_ID4 + "/versions", HTTP_DELETE, HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "", queryParams);
+		JsonObject jsonObject = JsonParser.parseString(EntityUtils.toString(response.getEntity())).getAsJsonObject();
+		String errorMessage = String.format("Invalid 'from' version. The record version does not contains specified from version '%d'", fromVersion);
+
+		assertEquals(HttpStatus.SC_BAD_REQUEST, response.getCode());
+		assertEquals(HttpStatus.SC_BAD_REQUEST, jsonObject.get("code").getAsInt());
+		assertEquals("Invalid 'from' version.", jsonObject.get("reason").getAsString());
+		assertEquals(errorMessage, jsonObject.get("message").getAsString());
+	}
+
+	@Test
+	public void shouldReturnHttp400BadRequest_whenPurgeRecordVersions_forInvalidLimitAndValidFromVersion() throws Exception {
+
+		CloseableHttpResponse getVersionResponse = TestUtils.send("records/versions/" + RECORD_ID4, "GET", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "", "");
+		JsonObject json = JsonParser.parseString(EntityUtils.toString(getVersionResponse.getEntity())).getAsJsonObject();
+		JsonArray versions = json.get("versions").getAsJsonArray();
+		Long fromVersion =  versions.get(1).getAsLong();
+
+		int limitValue = 3;
+		String queryParams= "?limit="+limitValue+"&from="+fromVersion;
+
+		CloseableHttpResponse response = TestUtils.send("records/" + RECORD_ID4 + "/versions", HTTP_DELETE, HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "", queryParams);
+		JsonObject jsonObject = JsonParser.parseString(EntityUtils.toString(response.getEntity())).getAsJsonObject();
+		String errorMessage = String.format("Invalid limit. Given limit count %d, exceeds the record versions count specified by the given 'from' version '%d'", limitValue, fromVersion);
+
+		assertEquals(HttpStatus.SC_BAD_REQUEST, response.getCode());
+		assertEquals(HttpStatus.SC_BAD_REQUEST, jsonObject.get("code").getAsInt());
+		assertEquals("Invalid limit.", jsonObject.get("reason").getAsString());
+		assertEquals(errorMessage, jsonObject.get("message").getAsString());
+	}
+
+
+}
diff --git a/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/records/RecordAccessAuthorizationTests.java b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/records/RecordAccessAuthorizationTests.java
new file mode 100644
index 0000000000000000000000000000000000000000..0fc37ad74fdcd92f1ef4886c80b7699d3e540a81
--- /dev/null
+++ b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/records/RecordAccessAuthorizationTests.java
@@ -0,0 +1,214 @@
+// Copyright 2017-2019, Schlumberger
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package org.opengroup.osdu.storage.records;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import java.io.IOException;
+import java.util.Map;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
+import org.apache.hc.core5.http.ParseException;
+import org.apache.hc.core5.http.io.entity.EntityUtils;
+import org.apache.http.HttpStatus;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.opengroup.osdu.storage.util.DummyRecordsHelper;
+import org.opengroup.osdu.storage.util.HeaderUtils;
+import org.opengroup.osdu.storage.util.LegalTagUtils;
+import org.opengroup.osdu.storage.util.RecordUtil;
+import org.opengroup.osdu.storage.util.TenantUtils;
+import org.opengroup.osdu.storage.util.TestBase;
+import org.opengroup.osdu.storage.util.TestUtils;
+import org.opengroup.osdu.storage.util.TokenTestUtils;
+
+
+public final class RecordAccessAuthorizationTests extends TestBase {
+
+	private static long NOW = System.currentTimeMillis();
+	private static String LEGAL_TAG = LegalTagUtils.createRandomName();
+	private static String KIND = TenantUtils.getTenantName() + ":dataaccess:no:1.1." + NOW;
+	private static String RECORD_ID = TenantUtils.getTenantName() + ":no:1.1." + NOW;
+
+	private static final TokenTestUtils TOKEN_TEST_UTILS = new TokenTestUtils();
+
+	@BeforeAll
+	public static void classSetup() throws Exception {
+		RecordAccessAuthorizationTests.classSetup(TOKEN_TEST_UTILS.getToken());
+	}
+
+	@AfterAll
+	public static void classTearDown() throws Exception {
+		RecordAccessAuthorizationTests.classTearDown(TOKEN_TEST_UTILS.getToken());
+	}
+
+	@BeforeEach
+	@Override
+	public void setup() throws Exception {
+		this.testUtils = new TokenTestUtils();
+	}
+
+	@AfterEach
+	@Override
+	public void tearDown() throws Exception {
+		this.testUtils = null;
+	}
+
+	public static void classSetup(String token) throws Exception {
+		LegalTagUtils.create(LEGAL_TAG, token);
+
+		CloseableHttpResponse response = TestUtils.send("records", "PUT", HeaderUtils.getHeaders(TenantUtils.getTenantName(), token),
+				RecordUtil.createDefaultJsonRecord(RECORD_ID, KIND, LEGAL_TAG), "");
+
+		assertEquals(HttpStatus.SC_CREATED, response.getCode());
+	}
+
+	public static void classTearDown(String token) throws Exception {
+		LegalTagUtils.delete(LEGAL_TAG, token);
+
+		TestUtils.send("records/" + RECORD_ID, "DELETE", HeaderUtils.getHeaders(TenantUtils.getTenantName(), token), "", "");
+	}
+
+	@Test
+	public void should_receiveHttp403_when_userIsNotAuthorizedToGetLatestVersionOfARecord() throws Exception {
+		Map<String, String> headers = HeaderUtils.getHeaders(TenantUtils.getTenantName(),
+				testUtils.getNoDataAccessToken());
+
+		CloseableHttpResponse response = TestUtils.send("records/" + RECORD_ID, "GET", headers, "", "");
+
+		this.assertNotAuthorized(response);
+	}
+
+	@Test
+	public void should_receiveHttp403_when_userIsNotAuthorizedToListVersionsOfARecord() throws Exception {
+		Map<String, String> headers = HeaderUtils.getHeaders(TenantUtils.getTenantName(),
+				testUtils.getNoDataAccessToken());
+
+		CloseableHttpResponse response = TestUtils.send("records/versions/" + RECORD_ID, "GET", headers, "", "");
+
+		this.assertNotAuthorized(response);
+	}
+
+	@Test
+	public void should_receiveHttp403_when_userIsNotAuthorizedToGetSpecificVersionOfARecord() throws Exception {
+		Map<String, String> withDataAccessHeader = HeaderUtils.getHeaders(TenantUtils.getTenantName(),
+				testUtils.getToken());
+
+		CloseableHttpResponse response = TestUtils.send("records/versions/" + RECORD_ID, "GET", withDataAccessHeader, "", "");
+		JsonObject json = JsonParser.parseString(EntityUtils.toString(response.getEntity())).getAsJsonObject();
+		String version = json.get("versions").getAsJsonArray().get(0).toString();
+
+		Map<String, String> withoutDataAccessHeader = HeaderUtils.getHeaders(TenantUtils.getTenantName(),
+				testUtils.getNoDataAccessToken());
+
+		response = TestUtils.send("records/" + RECORD_ID + "/" + version, "GET", withoutDataAccessHeader, "", "");
+
+		this.assertNotAuthorized(response);
+	}
+
+	@Test
+	public void should_receiveHttp403_when_userIsNotAuthorizedToDeleteRecord() throws Exception {
+		Map<String, String> headers = HeaderUtils.getHeaders(TenantUtils.getTenantName(),
+				testUtils.getNoDataAccessToken());
+
+		CloseableHttpResponse response = TestUtils.send("records/", "POST", headers, "{'anything':'anything'}",
+				RECORD_ID + ":delete");
+
+		this.assertNotAuthorized(response);
+	}
+
+	@Test
+	public void should_receiveHttp403_when_userIsNotAuthorizedToPurgeRecord() throws Exception {
+		Map<String, String> headers = HeaderUtils.getHeaders(TenantUtils.getTenantName(),
+				testUtils.getNoDataAccessToken());
+
+		CloseableHttpResponse response = TestUtils.send("records/" + RECORD_ID, "DELETE", headers, "", "");
+
+        assertEquals(HttpStatus.SC_FORBIDDEN, response.getCode());
+        JsonObject json = JsonParser.parseString(EntityUtils.toString(response.getEntity())).getAsJsonObject();
+        assertEquals(HttpStatus.SC_FORBIDDEN, json.get("code").getAsInt());
+        assertEquals("Access denied", json.get("reason").getAsString());
+    }
+
+	@Test
+	public void should_receiveHttp403_when_userIsNotAuthorizedToUpdateARecord() throws Exception {
+		Map<String, String> headers = HeaderUtils.getHeaders(TenantUtils.getTenantName(),
+				testUtils.getNoDataAccessToken());
+
+		CloseableHttpResponse response = TestUtils.send("records", "PUT", headers,
+				RecordUtil.createDefaultJsonRecord(RECORD_ID, KIND, LEGAL_TAG), "");
+
+		this.assertNotAuthorized(response);
+	}
+
+	@Test
+	public void should_NoneRecords_when_fetchingMultipleRecords_and_notAuthorizedToRecords()
+			throws Exception {
+
+		// Creates a new record
+		String newRecordId = TenantUtils.getTenantName() + ":no:2.2." + NOW;
+
+		Map<String, String> headersWithValidAccessToken = HeaderUtils.getHeaders(TenantUtils.getTenantName(),
+				testUtils.getToken());
+
+		CloseableHttpResponse response = TestUtils.send("records", "PUT", headersWithValidAccessToken,
+				RecordUtil.createDefaultJsonRecord(newRecordId, KIND, LEGAL_TAG), "");
+
+		assertEquals(HttpStatus.SC_CREATED, response.getCode());
+
+		// Query for original record (no access) and recently created record (with
+		// access)
+		Map<String, String> headersWithNoDataAccessToken = HeaderUtils.getHeaders(TenantUtils.getTenantName(),
+				testUtils.getNoDataAccessToken());
+
+		JsonArray records = new JsonArray();
+		records.add(RECORD_ID);
+		records.add(newRecordId);
+
+		JsonObject body = new JsonObject();
+		body.add("records", records);
+
+		response = TestUtils.send("query/records", "POST", headersWithNoDataAccessToken, body.toString(), "");
+		assertEquals(HttpStatus.SC_OK, response.getCode());
+
+		DummyRecordsHelper.RecordsMock responseObject = new DummyRecordsHelper().getRecordsMockFromResponse(response);
+
+		assertEquals(0, responseObject.records.length);
+		assertEquals(0, responseObject.invalidRecords.length);
+		assertEquals(0, responseObject.retryRecords.length);
+
+		TestUtils.send("records/" + newRecordId, "DELETE", headersWithNoDataAccessToken, "", "");
+	}
+
+	protected void assertNotAuthorized(CloseableHttpResponse response) {
+		assertEquals(HttpStatus.SC_FORBIDDEN, response.getCode());
+		JsonObject json = null;
+		try {
+			json = JsonParser.parseString(EntityUtils.toString(response.getEntity())).getAsJsonObject();
+		} catch (IOException e) {
+			throw new RuntimeException(e);
+		} catch (ParseException e) {
+			throw new RuntimeException(e);
+		}
+		assertEquals(HttpStatus.SC_FORBIDDEN, json.get("code").getAsInt());
+		assertEquals("Access denied", json.get("reason").getAsString());
+		assertEquals("The user is not authorized to perform this action", json.get("message").getAsString());
+	}
+}
diff --git a/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/records/RecordWithEntV2OnlyAclTest.java b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/records/RecordWithEntV2OnlyAclTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..dbc7282df058397cfd04b66027da84c62ed3afaa
--- /dev/null
+++ b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/records/RecordWithEntV2OnlyAclTest.java
@@ -0,0 +1,77 @@
+/*
+ *  Copyright 2020-2024 Google LLC
+ *  Copyright 2020-2024 EPAM Systems, Inc
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+package org.opengroup.osdu.storage.records;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.opengroup.osdu.storage.util.TestUtils.STORAGE_TEST_GROUP_ENT_V_2;
+import static org.opengroup.osdu.storage.util.TestUtils.STORAGE_TEST_GROUP_ENT_V_2_DESCRIPTION;
+
+import java.util.Map;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
+import org.apache.http.HttpStatus;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.opengroup.osdu.storage.util.EntitlementsUtil;
+import org.opengroup.osdu.storage.util.HeaderUtils;
+import org.opengroup.osdu.storage.util.LegalTagUtils;
+import org.opengroup.osdu.storage.util.RecordUtil;
+import org.opengroup.osdu.storage.util.TenantUtils;
+import org.opengroup.osdu.storage.util.TestBase;
+import org.opengroup.osdu.storage.util.TestUtils;
+import org.opengroup.osdu.storage.util.TokenTestUtils;
+
+public final class RecordWithEntV2OnlyAclTest extends TestBase {
+
+    private static final long NOW = System.currentTimeMillis();
+    private static final String LEGAL_TAG = LegalTagUtils.createRandomName();
+    private static final String KIND = TenantUtils.getTenantName() + ":test:inttest:1.1." + NOW;
+    private static final String RECORD_ID = TenantUtils.getTenantName() + ":inttest:" + NOW;
+
+
+    @BeforeEach
+    public void setup() throws Exception {
+        this.testUtils = new TokenTestUtils();
+        LegalTagUtils.create(LEGAL_TAG, testUtils.getToken());
+        Map<String, String> headers = HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken());
+        CloseableHttpResponse createGroupResponse = EntitlementsUtil.createEntitlementsGroup(
+            headers,
+            STORAGE_TEST_GROUP_ENT_V_2,
+            STORAGE_TEST_GROUP_ENT_V_2_DESCRIPTION
+        );
+        int responseCode = createGroupResponse.getCode();
+        assertTrue(responseCode == HttpStatus.SC_CREATED || responseCode == HttpStatus.SC_CONFLICT);
+    }
+
+    @AfterEach
+    public void tearDown() throws Exception {
+        LegalTagUtils.delete(LEGAL_TAG, testUtils.getToken());
+        TestUtils.send("records/" + RECORD_ID, "DELETE", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "", "");
+        this.testUtils = null;
+    }
+
+    @Test
+    public void should_allow_recordWithAclThatExistsOnlyInEntV2() throws Exception{
+        //create record
+        CloseableHttpResponse response = TestUtils.send("records", "PUT", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()),
+                RecordUtil.createJsonRecordWithEntV2OnlyAcl(RECORD_ID, KIND, LEGAL_TAG, RECORD_ID), "");
+        assertEquals(HttpStatus.SC_CREATED, response.getCode());
+    }
+
+}
diff --git a/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/records/RecordWithNullFieldTest.java b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/records/RecordWithNullFieldTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..ad4ad4fa55cb42527c4bb71f81567dafaa851c15
--- /dev/null
+++ b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/records/RecordWithNullFieldTest.java
@@ -0,0 +1,131 @@
+// Copyright 2017-2019, Schlumberger
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package org.opengroup.osdu.storage.records;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonNull;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
+import org.apache.hc.core5.http.io.entity.EntityUtils;
+import org.apache.http.HttpStatus;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.opengroup.osdu.storage.util.DummyRecordsHelper;
+import org.opengroup.osdu.storage.util.DummyRecordsHelper.RecordResultMock;
+import org.opengroup.osdu.storage.util.HeaderUtils;
+import org.opengroup.osdu.storage.util.LegalTagUtils;
+import org.opengroup.osdu.storage.util.RecordUtil;
+import org.opengroup.osdu.storage.util.TenantUtils;
+import org.opengroup.osdu.storage.util.TestBase;
+import org.opengroup.osdu.storage.util.TestUtils;
+import org.opengroup.osdu.storage.util.TokenTestUtils;
+
+public final class RecordWithNullFieldTest extends TestBase {
+
+	private static final Long NOW = System.currentTimeMillis();
+	private static final String LEGAL_TAG = LegalTagUtils.createRandomName();
+
+	private static final String KIND = TenantUtils.getTenantName() + ":test:endtoend:1.1."
+			+ NOW;
+	private static final String RECORD_ID = TenantUtils.getTenantName() + ":endtoend:1.1."
+			+ NOW;
+
+	@BeforeEach
+	public void setup() throws Exception {
+		this.testUtils = new TokenTestUtils();
+		LegalTagUtils.create(LEGAL_TAG, testUtils.getToken());
+	}
+
+	@AfterEach
+	public void tearDown() throws Exception {
+		LegalTagUtils.delete(LEGAL_TAG, testUtils.getToken());
+		TestUtils.send(
+				"records/" + RECORD_ID,
+				"DELETE",
+				HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()),
+				"",
+				""
+		);
+		this.testUtils = null;
+	}
+
+	@Test
+	public void should_returnRecordWithoutNullFields_when_recordIsIngestedWithNullFields() throws Exception {
+
+		// create record with null field
+		CloseableHttpResponse response = TestUtils.send("records", "PUT", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()),
+				RecordUtil.createJsonRecordWithData(RECORD_ID, KIND, LEGAL_TAG, null), "");
+		assertEquals(HttpStatus.SC_CREATED, response.getCode());
+
+		// get record
+		response = TestUtils.send("records/" + RECORD_ID, "GET", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "", "");
+		assertEquals(HttpStatus.SC_OK, response.getCode());
+
+		JsonObject json = JsonParser.parseString(EntityUtils.toString(response.getEntity())).getAsJsonObject();
+		JsonObject dataJson = json.get("data").getAsJsonObject();
+
+		assertEquals("58377304471659395", dataJson.get("score-int").toString());
+		assertEquals("5.837730447165939E7", dataJson.get("score-double").toString());
+		assertEquals(JsonNull.INSTANCE, dataJson.get("custom"));
+
+		// query records without attribute
+		JsonArray attributes = new JsonArray();
+		JsonArray records = new JsonArray();
+		records.add(RECORD_ID);
+
+		JsonObject body = new JsonObject();
+		body.add("records", records);
+		body.add("attributes", attributes);
+
+		response = TestUtils.send("query/records", "POST", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), body.toString(), "");
+		assertEquals(HttpStatus.SC_OK, response.getCode());
+
+		DummyRecordsHelper.RecordsMock responseObject = new DummyRecordsHelper().getRecordsMockFromResponse(response);
+		assertEquals(1, responseObject.records.length);
+		assertEquals(0, responseObject.invalidRecords.length);
+		assertEquals(0, responseObject.retryRecords.length);
+
+		RecordResultMock result = responseObject.records[0];
+
+		assertEquals(5.8377304471659392E16, result.data.get("score-int"));
+		assertEquals("5.837730447165939E7", result.data.get("score-double").toString());
+		assertTrue(result.data.containsKey("custom"));
+		assertEquals(null, result.data.get("custom"));
+
+		// query records with attribute
+		attributes.add("data.custom");
+
+		response = TestUtils.send("query/records", "POST", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), body.toString(), "");
+		assertEquals(HttpStatus.SC_OK, response.getCode());
+
+		responseObject = new DummyRecordsHelper().getRecordsMockFromResponse(response);
+		assertEquals(1, responseObject.records.length);
+		assertEquals(0, responseObject.invalidRecords.length);
+		assertEquals(0, responseObject.retryRecords.length);
+
+		result = responseObject.records[0];
+
+		assertFalse(result.data.containsKey("score-int"));
+		assertFalse(result.data.containsKey("score-double"));
+		assertTrue(result.data.containsKey("custom"));
+		assertEquals(null, result.data.get("custom"));
+	}
+}
diff --git a/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/records/RecordsApiAcceptanceTests.java b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/records/RecordsApiAcceptanceTests.java
new file mode 100644
index 0000000000000000000000000000000000000000..0ddbca60e3e3fa7fa74936ad4a3c95d21d13b452
--- /dev/null
+++ b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/records/RecordsApiAcceptanceTests.java
@@ -0,0 +1,484 @@
+// Copyright 2017-2019, Schlumberger
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package org.opengroup.osdu.storage.records;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.google.common.base.Strings;
+import com.google.gson.Gson;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
+import org.apache.hc.core5.http.io.entity.EntityUtils;
+import org.apache.http.HttpStatus;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.opengroup.osdu.storage.util.ConfigUtils;
+import org.opengroup.osdu.storage.util.DummyRecordsHelper;
+import org.opengroup.osdu.storage.util.HeaderUtils;
+import org.opengroup.osdu.storage.util.LegalTagUtils;
+import org.opengroup.osdu.storage.util.TenantUtils;
+import org.opengroup.osdu.storage.util.TestBase;
+import org.opengroup.osdu.storage.util.TestUtils;
+import org.opengroup.osdu.storage.util.TokenTestUtils;
+
+public final class RecordsApiAcceptanceTests extends TestBase {
+
+	private static final String RECORD_ID = TenantUtils.getTenantName() + ":inttest:" + System.currentTimeMillis();
+	private static final String RECORD_NEW_ID = TenantUtils.getTenantName() + ":inttest:"
+			+ System.currentTimeMillis();
+	
+	private static final String RECORD_ID_3 = TenantUtils.getTenantName() + ":inttest:testModifyTimeUser-" + System.currentTimeMillis();
+
+	static final String KIND = TenantUtils.getTenantName() + ":ds:inttest:1.0."
+			+ System.currentTimeMillis();
+	private static final String KIND_WITH_OTHER_TENANT = "tenant1" + ":ds:inttest:1.0."
+			+ System.currentTimeMillis();
+
+	static String LEGAL_TAG = LegalTagUtils.createRandomName();
+
+	private static final TokenTestUtils TOKEN_TEST_UTILS = new TokenTestUtils();
+
+	@BeforeAll
+	public static void classSetup() throws Exception {
+		RecordsApiAcceptanceTests.classSetup(TOKEN_TEST_UTILS.getToken());
+	}
+
+	@AfterAll
+	public static void classTearDown() throws Exception {
+		RecordsApiAcceptanceTests.classTearDown(TOKEN_TEST_UTILS.getToken());
+	}
+
+	@BeforeEach
+	@Override
+	public void setup() throws Exception {
+		this.testUtils = new TokenTestUtils();
+		this.configUtils = new ConfigUtils("test.properties");
+	}
+
+	@AfterEach
+	@Override
+	public void tearDown() throws Exception {
+		this.testUtils = null;
+	}
+
+	public static void classSetup(String token) throws Exception {
+		LegalTagUtils.create(LEGAL_TAG, token);
+		String jsonInput = createJsonBody(RECORD_ID, "tian");
+
+		TestUtils.send("records", "PUT", HeaderUtils.getHeaders(TenantUtils.getTenantName(), token), jsonInput, "");
+	}
+
+	public static void classTearDown(String token) throws Exception {
+		// attempt to cleanup both records used during tests no matter what state they
+		// are in
+		TestUtils.send("records/" + RECORD_ID, "DELETE", HeaderUtils.getHeaders(TenantUtils.getTenantName(), token), "", "");
+		TestUtils.send("records/" + RECORD_NEW_ID, "DELETE", HeaderUtils.getHeaders(TenantUtils.getTenantName(), token), "", "");
+		TestUtils.send("records/" + RECORD_ID_3, "DELETE", HeaderUtils.getHeaders(TenantUtils.getTenantName(), token), "", "");
+		LegalTagUtils.delete(LEGAL_TAG, token);
+	}
+
+	@Test
+	public void should_createNewRecord_when_givenValidRecord_and_verifyNoAncestry() throws Exception {
+		String jsonInput = createJsonBody(RECORD_NEW_ID, "Flor�");
+
+		CloseableHttpResponse response = TestUtils.send("records", "PUT", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), jsonInput, "");
+		String json = EntityUtils.toString(response.getEntity());
+		assertEquals(HttpStatus.SC_CREATED, response.getCode());
+		assertTrue(response.getEntity().getContentType().contains("application/json"));
+
+		Gson gson = new Gson();
+		DummyRecordsHelper.CreateRecordResponse result = gson.fromJson(json,
+				DummyRecordsHelper.CreateRecordResponse.class);
+
+		assertEquals(1, result.recordCount);
+		assertEquals(1, result.recordIds.length);
+		assertEquals(1, result.recordIdVersions.length);
+		assertEquals(RECORD_NEW_ID, result.recordIds[0]);
+
+		response = TestUtils.send("records/" + RECORD_NEW_ID, "GET", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "", "");
+		GetRecordResponse recordResult = TestUtils.getResult(response, HttpStatus.SC_OK, GetRecordResponse.class);
+		assertEquals("Flor?", recordResult.data.get("name"));
+		assertEquals(null, recordResult.data.get("ancestry"));
+	}
+
+	@Test
+	public void should_updateRecordsWithSameData_when_skipDupesIsFalse() throws Exception {
+
+		CloseableHttpResponse response = TestUtils.send("records/" + RECORD_ID, "GET", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "", "");
+		GetRecordResponse recordResult = TestUtils.getResult(response, HttpStatus.SC_OK, GetRecordResponse.class);
+
+		String jsonInput = createJsonBody(RECORD_ID, "tianNew");
+
+		// make update with different name
+		response = TestUtils.send("records", "PUT", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), jsonInput, "?skipdupes=true");
+		DummyRecordsHelper.CreateRecordResponse result = TestUtils.getResult(response, HttpStatus.SC_CREATED,
+				DummyRecordsHelper.CreateRecordResponse.class);
+		assertNotNull(result);
+		assertEquals(1, result.recordCount);
+		assertEquals(1, result.recordIds.length);
+		assertEquals(1, result.recordIdVersions.length);
+		assertEquals(0, result.skippedRecordIds.length);
+		assertEquals(RECORD_ID, result.recordIds[0]);
+
+		response = TestUtils.send("records/" + RECORD_ID, "GET", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "", "");
+		GetRecordResponse recordResult2 = TestUtils.getResult(response, HttpStatus.SC_OK, GetRecordResponse.class);
+		assertNotEquals(recordResult.version, recordResult2.version);
+		assertEquals("tianNew", recordResult2.data.get("name"));
+
+		// use skip dupes to skip update
+		response = TestUtils.send("records", "PUT", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), jsonInput, "?skipdupes=true");
+		result = TestUtils.getResult(response, HttpStatus.SC_CREATED, DummyRecordsHelper.CreateRecordResponse.class);
+		assertNotNull(result);
+		assertEquals(1, result.recordCount);
+		assertNull(result.recordIds);
+		assertNull(result.recordIdVersions);
+		assertEquals(
+				1,
+				result.skippedRecordIds.length,
+				"Expected to skip the update when the data was the same as previous update and skipdupes is true"
+		);
+		assertEquals(RECORD_ID, result.skippedRecordIds[0]);
+
+		response = TestUtils.send("records/" + RECORD_ID, "GET", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "", "");
+		GetRecordResponse recordResult3 = TestUtils.getResult(response, HttpStatus.SC_OK, GetRecordResponse.class);
+		assertEquals(recordResult2.version, recordResult3.version);
+		assertEquals("tianNew", recordResult3.data.get("name"));
+
+		// set skip dupes to false to make the update with same data
+		response = TestUtils.send("records", "PUT", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), jsonInput, "?skipdupes=false");
+		result = TestUtils.getResult(response, HttpStatus.SC_CREATED, DummyRecordsHelper.CreateRecordResponse.class);
+		assertNotNull(result);
+		assertEquals(1, result.recordCount);
+		assertEquals(1, result.recordIds.length);
+		assertEquals(1, result.recordIdVersions.length);
+		assertEquals(
+				0,
+				result.skippedRecordIds.length,
+				"Expected to NOT skip the update when data is the same but skipdupes is false"
+		);
+		assertEquals(RECORD_ID, result.recordIds[0]);
+
+		response = TestUtils.send("records/" + RECORD_ID, "GET", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "", "");
+		recordResult3 = TestUtils.getResult(response, HttpStatus.SC_OK, GetRecordResponse.class);
+		assertNotEquals(recordResult2.version, recordResult3.version);
+		assertEquals("tianNew", recordResult3.data.get("name"));
+	}
+
+	@Test
+	public void should_getAnOlderVersion_and_theMostRecentVersion_and_retrieveAllVersions()
+			throws Exception {
+
+		CloseableHttpResponse response = TestUtils.send("records/" + RECORD_ID, "GET", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "", "");
+		GetRecordResponse originalRecordResult = TestUtils.getResult(response, HttpStatus.SC_OK, GetRecordResponse.class);
+
+		String jsonInput = createJsonBody(RECORD_ID, "tianNew2");
+
+		// add an extra version
+		response = TestUtils.send("records", "PUT", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), jsonInput, "");
+		TestUtils.getResult(response, HttpStatus.SC_CREATED, DummyRecordsHelper.CreateRecordResponse.class);
+
+		// get a specific older version and validate it is the same
+		response = TestUtils.send("records/" + RECORD_ID + "/" + originalRecordResult.version, "GET",
+				HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "", "");
+		GetRecordResponse recordResultVersion = TestUtils.getResult(response, HttpStatus.SC_OK, GetRecordResponse.class);
+		assertEquals(originalRecordResult.id, recordResultVersion.id);
+		assertEquals(originalRecordResult.version, recordResultVersion.version);
+		assertEquals(originalRecordResult.data.get("name"), recordResultVersion.data.get("name"));
+
+		// get the latest version by using id and validate it has the latest data
+		response = TestUtils.send("records/" + RECORD_ID, "GET", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "", "");
+		GetRecordResponse newRecordResult = TestUtils.getResult(response, HttpStatus.SC_OK, GetRecordResponse.class);
+		assertEquals(originalRecordResult.id, newRecordResult.id);
+		assertNotEquals(originalRecordResult.version, newRecordResult.version);
+		assertEquals("tianNew2", newRecordResult.data.get("name"));
+
+		// older version and new version should be found
+		response = TestUtils.send("records/versions/" + RECORD_ID, "GET", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "", "");
+		GetVersionsResponse versionsResponse = TestUtils.getResult(response, HttpStatus.SC_OK, GetVersionsResponse.class);
+		assertEquals(RECORD_ID, versionsResponse.recordId);
+		List<Long> versions = Arrays.asList(versionsResponse.versions);
+		assertTrue(versions.contains(originalRecordResult.version));
+		assertTrue(versions.contains(newRecordResult.version));
+	}
+
+	@Test
+	public void should_deleteAllVersionsOfARecord_when_deletingARecordById() throws Exception {
+		String idToDelete = RECORD_ID + 1;
+
+		String jsonInput = createJsonBody(idToDelete, "tianNew2");
+
+		// add an extra version
+		CloseableHttpResponse response = TestUtils.send("records", "PUT", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), jsonInput, "");
+		TestUtils.getResult(response, HttpStatus.SC_CREATED, DummyRecordsHelper.CreateRecordResponse.class);
+
+		response = TestUtils.send("records/" + idToDelete, "DELETE", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "", "");
+		assertEquals(HttpStatus.SC_NO_CONTENT, response.getCode());
+
+		response = TestUtils.send("records/" + idToDelete, "GET", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "", "");
+		String notFoundResponse = TestUtils.getResult(response, 404, String.class);
+		assertEquals("{\"code\":404,\"reason\":\"Record not found\",\"message\":\"The record" + " '" + idToDelete + "' "
+				+ "was not found\"}", notFoundResponse);
+	}
+
+	@Test
+	public void should_ingestRecord_when_noRecordIdIsProvided() throws Exception {
+		String body = createJsonBody(null, "Foo");
+
+		CloseableHttpResponse response = TestUtils.send("records", "PUT", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), body, "");
+		String responseString = TestUtils.getResult(response, HttpStatus.SC_CREATED, String.class);
+		JsonObject responseJson = new JsonParser().parse(responseString).getAsJsonObject();
+
+		assertEquals(1, responseJson.get("recordCount").getAsInt());
+		assertEquals(1, responseJson.get("recordIds").getAsJsonArray().size());
+		assertTrue(responseJson.get("recordIds").getAsJsonArray().get(0).getAsString()
+				.startsWith(TenantUtils.getTenantName() + ":"));
+	}
+
+	@Test
+	public void should_returnWholeRecord_when_recordIsIngestedWithAllFields() throws Exception {
+		final String RECORD_ID = TenantUtils.getTenantName() + ":inttest:wholerecord-" + System.currentTimeMillis();
+
+		String body = createJsonBody(RECORD_ID, "Foo");
+
+		// injesting record
+		CloseableHttpResponse response = TestUtils.send("records", "PUT", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), body, "");
+		TestUtils.getResult(response, HttpStatus.SC_CREATED, String.class);
+
+		// getting record
+		response = TestUtils.send("records/" + RECORD_ID, "GET", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "", "");
+		String responseString = TestUtils.getResult(response, HttpStatus.SC_OK, String.class);
+
+		JsonObject responseJson = JsonParser.parseString(responseString).getAsJsonObject();
+
+		assertEquals(RECORD_ID, responseJson.get("id").getAsString());
+
+		assertEquals(KIND, responseJson.get("kind").getAsString());
+
+		JsonObject acl = responseJson.get("acl").getAsJsonObject();
+		assertEquals(TestUtils.getAcl(), acl.get("owners").getAsString());
+		assertEquals(TestUtils.getAcl(), acl.get("viewers").getAsString());
+
+		assertEquals("Foo", responseJson.getAsJsonObject("data").get("name").getAsString());
+	}
+
+	@Test
+	public void should_returnWholeRecord_when_recordIsIngestedWithOtherTenantInKind() throws Exception {
+		final String RECORD_ID = TenantUtils.getTenantName() + ":inttest:wholerecord-" + System.currentTimeMillis();
+		String body = createJsonBody(RECORD_ID, "Foo", KIND_WITH_OTHER_TENANT);
+		CloseableHttpResponse response = TestUtils.send("records", "PUT", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), body, "");
+		TestUtils.getResult(response, HttpStatus.SC_CREATED, String.class);
+		response = TestUtils.send("records/" + RECORD_ID, "GET", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "", "");
+		String responseString = TestUtils.getResult(response, HttpStatus.SC_OK, String.class);
+		JsonObject responseJson = JsonParser.parseString(responseString).getAsJsonObject();
+		assertEquals(RECORD_ID, responseJson.get("id").getAsString());
+		assertEquals(KIND_WITH_OTHER_TENANT, responseJson.get("kind").getAsString());
+		JsonObject acl = responseJson.get("acl").getAsJsonObject();
+		assertEquals(TestUtils.getAcl(), acl.get("owners").getAsString());
+		assertEquals(TestUtils.getAcl(), acl.get("viewers").getAsString());
+		assertEquals("Foo", responseJson.getAsJsonObject("data").get("name").getAsString());
+	}
+
+	@Test
+	public void should_insertNewRecord_when_skipDupesIsTrue() throws Exception {
+		final String RECORD_ID = TenantUtils.getTenantName() + ":inttest:wholerecord-" + System.currentTimeMillis();
+		String body = createJsonBody(RECORD_ID, "Foo");
+		CloseableHttpResponse response = TestUtils.send("records", "PUT", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), body, "?skipdupes=true");
+		DummyRecordsHelper.CreateRecordResponse result = TestUtils.getResult(response, HttpStatus.SC_CREATED, DummyRecordsHelper.CreateRecordResponse.class);
+		assertNotNull(result);
+		assertEquals(1, result.recordCount);
+		assertEquals(
+				1,
+				result.recordIds.length,
+				"Expected to insert the new record when skipdupes is true"
+		);
+		assertEquals(1, result.recordIdVersions.length);
+		assertEquals(RECORD_ID, result.recordIds[0]);
+		response = TestUtils.send("records/" + RECORD_ID, "DELETE", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "", "");
+		assertEquals(HttpStatus.SC_NO_CONTENT, response.getCode());
+	}
+
+	@Test
+	public void should_createNewRecord_withSpecialCharacter_ifEnabled() throws Exception {
+		final long currentTimeMillis = System.currentTimeMillis();
+		final String RECORD_ID = TenantUtils.getTenantName() + ":inttest:testSpecialChars%abc%2Ffoobar-" + currentTimeMillis;
+		final String ENCODED_RECORD_ID = TenantUtils.getTenantName() + ":inttest:testSpecialChars%25abc%252Ffoobar-" + currentTimeMillis;
+
+		String jsonInput = createJsonBody(RECORD_ID, "TestSpecialCharacters");
+
+		CloseableHttpResponse response = TestUtils.send("records", "PUT", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), jsonInput, "");
+		String json = EntityUtils.toString(response.getEntity());
+		assertEquals(HttpStatus.SC_CREATED, response.getCode());
+		assertTrue(response.getEntity().getContentType().contains("application/json"));
+
+		Gson gson = new Gson();
+		DummyRecordsHelper.CreateRecordResponse result = gson.fromJson(json,
+				DummyRecordsHelper.CreateRecordResponse.class);
+
+		assertEquals(1, result.recordCount);
+		assertEquals(1, result.recordIds.length);
+		assertEquals(1, result.recordIdVersions.length);
+		assertEquals(RECORD_ID, result.recordIds[0]);
+
+		response = TestUtils.send("records/" + ENCODED_RECORD_ID, "GET", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "", "");
+
+		// If encoded percent is true, the request should go through and should be able to get a successful response.
+		if (configUtils != null && configUtils.getBooleanProperty("enableEncodedSpecialCharactersInURL", "false")) {
+			GetRecordResponse recordResult = TestUtils.getResult(response, HttpStatus.SC_OK, GetRecordResponse.class);
+			assertEquals("TestSpecialCharacters", recordResult.data.get("name"));
+		} else {
+			// Service does not allow URLs with suspicious characters, Which is the default setting.
+			// Different CSPs are responding with different status code for this error when a special character like %25 is present in the URL.
+			// Hence the Assert Statement is marked not to be 200.
+			// More details - https://community.opengroup.org/osdu/platform/system/storage/-/issues/61
+			assertNotEquals(HttpStatus.SC_OK, response.getCode());
+		}
+
+	}
+
+	@Test
+	public void should_updateModifyTimeWithRecordUpdate() throws Exception {
+
+		String jsonInput = createJsonBody(RECORD_ID_3, "tianNew");
+
+		CloseableHttpResponse response = TestUtils.send("records", "PUT", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), jsonInput, "?skipdupes=false");
+		DummyRecordsHelper.CreateRecordResponse result = TestUtils.getResult(response, HttpStatus.SC_CREATED,
+				DummyRecordsHelper.CreateRecordResponse.class);
+		assertNotNull(result);
+		assertEquals(1, result.recordCount);
+		assertEquals(1, result.recordIds.length);
+		assertEquals(1, result.recordIdVersions.length);
+		assertEquals(0, result.skippedRecordIds.length);
+		assertEquals(RECORD_ID_3, result.recordIds[0]);
+		String firstVersionNumber = StringUtils.substringAfterLast(result.recordIdVersions[0],":");
+
+		response = TestUtils.send("records/" + RECORD_ID_3, "GET", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "", "");
+		GetRecordResponse recordResult1 = TestUtils.getResult(response, HttpStatus.SC_OK, GetRecordResponse.class);
+
+		//No modify user and time in 1st version of record
+		assertNull(recordResult1.modifyTime);
+		assertNull(recordResult1.modifyUser);
+
+		// make update-1
+		response = TestUtils.send("records", "PUT", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), jsonInput, "?skipdupes=false");
+		DummyRecordsHelper.CreateRecordResponse result2 = TestUtils.getResult(response, HttpStatus.SC_CREATED,
+				DummyRecordsHelper.CreateRecordResponse.class);
+		assertNotNull(result2);
+		assertEquals(1, result2.recordCount);
+		assertEquals(1, result2.recordIds.length);
+		assertEquals(1, result2.recordIdVersions.length);
+		assertEquals(0, result2.skippedRecordIds.length);
+		assertEquals(RECORD_ID_3, result2.recordIds[0]);
+		String secondVersionNumber = StringUtils.substringAfterLast(result2.recordIdVersions[0],":");
+
+		// make update-2
+		response = TestUtils.send("records", "PUT", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), jsonInput, "?skipdupes=false");
+		DummyRecordsHelper.CreateRecordResponse result3 = TestUtils.getResult(response, HttpStatus.SC_CREATED, DummyRecordsHelper.CreateRecordResponse.class);
+		assertNotNull(result3);
+		assertEquals(1, result3.recordCount);
+		assertEquals(1, result3.recordIds.length);
+		assertEquals(1, result3.recordIdVersions.length);
+		assertEquals(0, result3.skippedRecordIds.length);
+		assertEquals(RECORD_ID_3, result3.recordIds[0]);
+
+		String thirdLastVersionNumber = StringUtils.substringAfterLast(result3.recordIdVersions[0],":");
+		response = TestUtils.send("records/" + RECORD_ID_3+"/"+firstVersionNumber, "GET", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "", "");
+		GetRecordResponse recordResult2 = TestUtils.getResult(response, HttpStatus.SC_OK, GetRecordResponse.class);
+
+		//No modify user and time in 1st version of record
+		assertNull(recordResult2.modifyTime);
+		assertNull(recordResult2.modifyUser);
+
+		response = TestUtils.send("records/" + RECORD_ID_3+"/"+secondVersionNumber, "GET", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "", "");
+		GetRecordResponse recordResult3 = TestUtils.getResult(response, HttpStatus.SC_OK, GetRecordResponse.class);
+
+		response = TestUtils.send("records/" + RECORD_ID_3+"/"+thirdLastVersionNumber, "GET", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "", "");
+		GetRecordResponse recordResult4 = TestUtils.getResult(response, HttpStatus.SC_OK, GetRecordResponse.class);
+
+		//modify time is different for each version of record
+		assertNotEquals(recordResult4.modifyTime, recordResult3.modifyTime);
+
+
+	}
+
+	protected static String createJsonBody(String id, String name) {
+		return "[" + singleEntityBody(id, name, KIND, LEGAL_TAG) + "]";
+	}
+
+	protected static String createJsonBody(String id, String name, String kind) {
+		return "[" + singleEntityBody(id, name, kind, LEGAL_TAG) + "]";
+	}
+
+	public class RecordAncestry {
+		public String[] parents;
+	}
+
+	protected class GetVersionsResponse {
+		String recordId;
+		Long versions[];
+	}
+
+	protected class GetRecordResponse {
+		String id;
+		long version;
+		Map<String, Object> data;
+		String modifyTime;
+		String modifyUser;
+	}
+
+	public static String singleEntityBody(String id, String name, String kind, String legalTagName) {
+
+		JsonObject data = new JsonObject();
+		data.addProperty("name", name);
+
+		JsonObject acl = new JsonObject();
+		JsonArray acls = new JsonArray();
+		acls.add(TestUtils.getAcl());
+		acl.add("viewers", acls);
+		acl.add("owners", acls);
+
+		JsonObject legal = new JsonObject();
+		JsonArray legals = new JsonArray();
+		legals.add(legalTagName);
+		legal.add("legaltags", legals);
+		JsonArray ordc = new JsonArray();
+		ordc.add("BR");
+		legal.add("otherRelevantDataCountries", ordc);
+
+		JsonObject record = new JsonObject();
+		if (!Strings.isNullOrEmpty(id)) {
+			record.addProperty("id", id);
+		}
+
+		record.addProperty("kind", kind);
+		record.add("acl", acl);
+		record.add("legal", legal);
+		record.add("data", data);
+
+		return record.toString();
+	}
+}
diff --git a/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/records/UpdateRecordsMetadataTest.java b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/records/UpdateRecordsMetadataTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..ca0dd11a9056981902c6096641d2ed02d6153ddf
--- /dev/null
+++ b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/records/UpdateRecordsMetadataTest.java
@@ -0,0 +1,462 @@
+// Copyright 2017-2019, Schlumberger
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package org.opengroup.osdu.storage.records;
+
+import static org.apache.commons.lang3.StringUtils.EMPTY;
+import static org.apache.http.HttpStatus.SC_BAD_REQUEST;
+import static org.apache.http.HttpStatus.SC_OK;
+import static org.apache.http.HttpStatus.SC_PARTIAL_CONTENT;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import java.util.Map;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
+import org.apache.hc.core5.http.io.entity.EntityUtils;
+import org.apache.http.HttpStatus;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.opengroup.osdu.storage.util.DummyRecordsHelper;
+import org.opengroup.osdu.storage.util.HeaderUtils;
+import org.opengroup.osdu.storage.util.LegalTagUtils;
+import org.opengroup.osdu.storage.util.RecordUtil;
+import org.opengroup.osdu.storage.util.TenantUtils;
+import org.opengroup.osdu.storage.util.TestBase;
+import org.opengroup.osdu.storage.util.TestUtils;
+import org.opengroup.osdu.storage.util.TokenTestUtils;
+
+public final class UpdateRecordsMetadataTest extends TestBase {
+    static final String TAG_KEY = "tagkey1";
+    static final String TAG_VALUE1 = "tagvalue1";
+    private static final String TAG_VALUE2 = "tagvalue2";
+
+    private static long NOW;
+    private static String ACL = TestUtils.getIntegrationTesterAcl();
+    private static String ACL_2 = TestUtils.getAcl();
+    private static String LEGAL_TAG;
+    private static String LEGAL_TAG_2;
+    private static String LEGAL_TAG_3;
+    private static String LEGAL_TAG_4;
+    private static String KIND;
+    private static String RECORD_ID;
+    private static String RECORD_ID_2;
+    private static String RECORD_ID_3;
+    private static String RECORD_ID_4;
+    private static String NOT_EXISTED_RECORD_ID;
+    private static final DummyRecordsHelper RECORDS_HELPER = new DummyRecordsHelper();
+
+    @BeforeEach
+    public void setup() throws Exception {
+        this.testUtils = new TokenTestUtils();
+
+        LEGAL_TAG = LegalTagUtils.createRandomName();
+        LEGAL_TAG_2 = LegalTagUtils.createRandomName() + "2";
+        LEGAL_TAG_3 = LegalTagUtils.createRandomName() + "3";
+        LEGAL_TAG_4 = LegalTagUtils.createRandomName() + "4";
+        NOW = System.currentTimeMillis();
+        KIND = TenantUtils.getFirstTenantName() + ":bulkupdate:test:1.1." + NOW;
+        RECORD_ID = TenantUtils.getFirstTenantName() + ":test:1.1." + NOW;
+        RECORD_ID_2 = TenantUtils.getFirstTenantName() + ":test:1.2." + NOW;
+        RECORD_ID_3 = TenantUtils.getFirstTenantName() + ":test:1.3." + NOW;
+        RECORD_ID_4 = TenantUtils.getFirstTenantName() + ":test:1.4." + NOW;
+        NOT_EXISTED_RECORD_ID = TenantUtils.getFirstTenantName() + ":bulkupdate:1.6." + NOW;
+        LegalTagUtils.create(LEGAL_TAG, testUtils.getToken());
+        LegalTagUtils.create(LEGAL_TAG_2, testUtils.getToken());
+        LegalTagUtils.create(LEGAL_TAG_3, testUtils.getToken());
+        LegalTagUtils.create(LEGAL_TAG_4, testUtils.getToken());
+        CloseableHttpResponse response = TestUtils.send("records", "PUT", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()),
+                RecordUtil.createDefaultJsonRecord(RECORD_ID, KIND, LEGAL_TAG), "");
+        CloseableHttpResponse response2 = TestUtils.send("records", "PUT", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()),
+                RecordUtil.createDefaultJsonRecord(RECORD_ID_2, KIND, LEGAL_TAG_2), "");
+        CloseableHttpResponse response3 = TestUtils.send("records", "PUT", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()),
+                RecordUtil.createDefaultJsonRecord(RECORD_ID_3, KIND, LEGAL_TAG_3), "");
+        CloseableHttpResponse response4 = TestUtils.send("records", "PUT", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()),
+                RecordUtil.createDefaultJsonRecord(RECORD_ID_4, KIND, LEGAL_TAG_4), "");
+        assertEquals(HttpStatus.SC_CREATED, response.getCode());
+        assertEquals(HttpStatus.SC_CREATED, response2.getCode());
+        assertEquals(HttpStatus.SC_CREATED, response3.getCode());
+        assertEquals(HttpStatus.SC_CREATED, response4.getCode());
+
+    }
+
+    @AfterEach
+    public void tearDown() throws Exception {
+        LegalTagUtils.delete(LEGAL_TAG, testUtils.getToken());
+        LegalTagUtils.delete(LEGAL_TAG_2, testUtils.getToken());
+        LegalTagUtils.delete(LEGAL_TAG_3, testUtils.getToken());
+        LegalTagUtils.delete(LEGAL_TAG_4, testUtils.getToken());
+        TestUtils.send("records/" + RECORD_ID, "DELETE", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "", "");
+        TestUtils.send("records/" + RECORD_ID_2, "DELETE", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "", "");
+        TestUtils.send("records/" + RECORD_ID_3, "DELETE", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "", "");
+        TestUtils.send("records/" + RECORD_ID_4, "DELETE", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "", "");
+
+        this.testUtils = null;
+    }
+
+    @Test
+    public void should_return200andUpdateMetadata_whenValidRecordsProvided() throws Exception {
+        //1. query 2 records, check the acls
+        JsonArray records = new JsonArray();
+        records.add(RECORD_ID);
+        records.add(RECORD_ID_2);
+
+        JsonObject queryBody = new JsonObject();
+        queryBody.add("records", records);
+
+        Map<String, String> queryHeader = HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken());
+        queryHeader.put("frame-of-reference", "none");
+        CloseableHttpResponse queryResponse1 = TestUtils.send("query/records:batch", "POST", queryHeader, queryBody.toString(),
+                "");
+        assertEquals(HttpStatus.SC_OK, queryResponse1.getCode());
+
+        DummyRecordsHelper.ConvertedRecordsMock queryResponseObject1 = RECORDS_HELPER.getConvertedRecordsMockFromResponse(queryResponse1);
+        assertEquals(2, queryResponseObject1.records.length);
+        assertEquals(TestUtils.getAcl(), queryResponseObject1.records[0].acl.viewers[0]);
+        assertEquals(TestUtils.getAcl(), queryResponseObject1.records[0].acl.owners[0]);
+
+        //2. bulk update requests, change acls
+        JsonArray value = new JsonArray();
+        value.add(TestUtils.getIntegrationTesterAcl());
+        JsonObject op1 = new JsonObject();
+        op1.addProperty("op", "replace");
+        op1.addProperty("path", "/acl/viewers");
+        op1.add("value", value);
+        JsonObject op2 = new JsonObject();
+        op2.addProperty("op", "replace");
+        op2.addProperty("path", "/acl/owners");
+        op2.add("value", value);
+        JsonArray ops = new JsonArray();
+        ops.add(op1);
+        ops.add(op2);
+
+        JsonObject query = new JsonObject();
+        query.add("ids", records);
+
+        JsonObject updateBody = new JsonObject();
+        updateBody.add("query", query);
+        updateBody.add("ops", ops);
+
+        CloseableHttpResponse bulkUpdateResponse = TestUtils.send("records", "PATCH", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), updateBody.toString(),
+                "");
+        assertEquals(HttpStatus.SC_OK, bulkUpdateResponse.getCode());
+
+        //3. query 2 records again, check acls
+        CloseableHttpResponse queryResponse2 = TestUtils.send("query/records:batch", "POST", queryHeader, queryBody.toString(),
+                "");
+        assertEquals(HttpStatus.SC_OK, queryResponse2.getCode());
+
+        DummyRecordsHelper.ConvertedRecordsMock queryResponseObject2 = RECORDS_HELPER.getConvertedRecordsMockFromResponse(queryResponse2);
+        assertEquals(2, queryResponseObject2.records.length);
+        assertEquals(TestUtils.getIntegrationTesterAcl(), queryResponseObject2.records[0].acl.viewers[0]);
+        assertEquals(TestUtils.getIntegrationTesterAcl(), queryResponseObject2.records[0].acl.owners[0]);
+    }
+
+    @Test
+    public void should_return206andUpdateMetadata_whenOneRecordProvided() throws Exception {
+        //1. query 2 records, check the acls
+        JsonArray records = new JsonArray();
+        records.add(RECORD_ID_3);
+        records.add(RECORD_ID_4);
+
+        JsonObject queryBody = new JsonObject();
+        queryBody.add("records", records);
+
+        Map<String, String> queryHeader = HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken());
+        queryHeader.put("slb-frame-of-reference", "none");
+        CloseableHttpResponse queryResponse1 = TestUtils.send("query/records:batch", "POST", queryHeader, queryBody.toString(),
+                "");
+        assertEquals(HttpStatus.SC_OK, queryResponse1.getCode());
+
+        DummyRecordsHelper.ConvertedRecordsMock queryResponseObject1 = RECORDS_HELPER.getConvertedRecordsMockFromResponse(queryResponse1);
+        assertEquals(2, queryResponseObject1.records.length);
+        assertEquals(TestUtils.getAcl(), queryResponseObject1.records[0].acl.viewers[0]);
+        assertEquals(TestUtils.getAcl(), queryResponseObject1.records[0].acl.owners[0]);
+
+        //2. bulk update requests, change acls
+        JsonArray value = new JsonArray();
+        value.add(TestUtils.getIntegrationTesterAcl());
+        JsonObject op1 = new JsonObject();
+        op1.addProperty("op", "replace");
+        op1.addProperty("path", "/acl/viewers");
+        op1.add("value", value);
+        JsonObject op2 = new JsonObject();
+        op2.addProperty("op", "replace");
+        op2.addProperty("path", "/acl/owners");
+        op2.add("value", value);
+        JsonArray ops = new JsonArray();
+        ops.add(op1);
+        ops.add(op2);
+
+        JsonArray records2 = new JsonArray();
+        records2.add(RECORD_ID_3);
+        records2.add(RECORD_ID_4);
+        records2.add(TenantUtils.getFirstTenantName() + ":not:found");
+        JsonObject query = new JsonObject();
+        query.add("ids", records2);
+
+        JsonObject updateBody = new JsonObject();
+        updateBody.add("query", query);
+        updateBody.add("ops", ops);
+
+        CloseableHttpResponse bulkUpdateResponse = TestUtils.send("records", "PATCH", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), updateBody.toString(),
+                "");
+        assertEquals(HttpStatus.SC_PARTIAL_CONTENT, bulkUpdateResponse.getCode());
+
+        //3. query 2 records again, check acls
+        CloseableHttpResponse queryResponse2 = TestUtils.send("query/records:batch", "POST", queryHeader, queryBody.toString(),
+                "");
+        assertEquals(HttpStatus.SC_OK, queryResponse2.getCode());
+
+        DummyRecordsHelper.ConvertedRecordsMock queryResponseObject2 = RECORDS_HELPER.getConvertedRecordsMockFromResponse(queryResponse2);
+        assertEquals(2, queryResponseObject2.records.length);
+        assertEquals(TestUtils.getIntegrationTesterAcl(), queryResponseObject2.records[0].acl.viewers[0]);
+        assertEquals(TestUtils.getIntegrationTesterAcl(), queryResponseObject2.records[0].acl.owners[0]);
+    }
+
+    @Test
+    public void should_return200AndUpdateTagsMetadata_whenValidRecordsProvided() throws Exception {
+        //add operation
+        JsonObject updateBody = RecordUtil.buildUpdateTagBody(RECORD_ID, "add", TAG_KEY + ":" + TAG_VALUE1);
+
+        CloseableHttpResponse updateResponse = sendRequest("PATCH", "records", toJson(updateBody), testUtils.getToken());
+        CloseableHttpResponse recordResponse = sendRequest("GET", "records/" + RECORD_ID, EMPTY, testUtils.getToken());
+
+        assertEquals(SC_OK, updateResponse.getCode());
+        assertEquals(SC_OK, recordResponse.getCode());
+
+        JsonObject resultObject = bodyToJsonObject(EntityUtils.toString(updateResponse.getEntity()));
+        assertEquals(RECORD_ID, resultObject.get("recordIds").getAsJsonArray().get(0).getAsString());
+
+        resultObject = bodyToJsonObject(EntityUtils.toString(recordResponse.getEntity()));
+        assertEquals(TAG_VALUE1, resultObject.get("tags").getAsJsonObject().get(TAG_KEY).getAsString());
+
+        //replace operation
+        updateBody = RecordUtil.buildUpdateTagBody(RECORD_ID, "replace", TAG_KEY + ":" + TAG_VALUE2);
+        sendRequest("PATCH", "records", toJson(updateBody), testUtils.getToken());
+        recordResponse = sendRequest("GET", "records/" + RECORD_ID, EMPTY, testUtils.getToken());
+
+        resultObject = bodyToJsonObject(EntityUtils.toString(recordResponse.getEntity()));
+        assertEquals(TAG_VALUE2, resultObject.get("tags").getAsJsonObject().get(TAG_KEY).getAsString());
+
+        //remove operation
+        updateBody = RecordUtil.buildUpdateTagBody(RECORD_ID,"remove", TAG_KEY);
+        sendRequest("PATCH", "records", toJson(updateBody), testUtils.getToken());
+        recordResponse = sendRequest("GET", "records/" + RECORD_ID, EMPTY, testUtils.getToken());
+
+        resultObject = JsonParser.parseString(EntityUtils.toString(recordResponse.getEntity())).getAsJsonObject();
+        assertNull(resultObject.get("tags"));
+    }
+
+    @Test
+    public void should_return206andUpdateTagsMetadata_whenNotExistedRecordProvided() throws Exception {
+        JsonObject updateBody = RecordUtil.buildUpdateTagBody(NOT_EXISTED_RECORD_ID, "replace", TAG_KEY + ":" + TAG_VALUE1);
+
+        CloseableHttpResponse updateResponse = sendRequest("PATCH", "records", toJson(updateBody), testUtils.getToken());
+
+        assertEquals(SC_PARTIAL_CONTENT, updateResponse.getCode());
+        JsonObject resultObject = bodyToJsonObject(EntityUtils.toString(updateResponse.getEntity()));
+
+        System.out.println(resultObject.toString());
+        assertEquals(NOT_EXISTED_RECORD_ID, resultObject.get("notFoundRecordIds").getAsJsonArray().getAsString());
+    }
+
+    private static CloseableHttpResponse sendRequest(String method, String path, String body, String token) throws Exception {
+        return TestUtils
+            .send(path, method, HeaderUtils.getHeaders(TenantUtils.getTenantName(), token), body, "");
+    }
+
+    private String toJson(Object object) {
+        return new Gson().toJson(object);
+    }
+
+    private JsonObject bodyToJsonObject(String json) {
+        return JsonParser.parseString(json).getAsJsonObject();
+    }
+
+    @Test
+    public void should_return200AndUpdateLegalMetadataOr400ForRemoveRestriction_whenValidRecordsProvided() throws Exception {
+        //add operation
+        JsonObject updateBody = buildUpdateLegalBody(RECORD_ID, "add", LEGAL_TAG);
+
+        CloseableHttpResponse updateResponse = sendRequest("PATCH", "records", toJson(updateBody), testUtils.getToken());
+        CloseableHttpResponse recordResponse = sendRequest("GET", "records/" + RECORD_ID, EMPTY, testUtils.getToken());
+
+        assertEquals(SC_OK, updateResponse.getCode());
+        assertEquals(SC_OK, recordResponse.getCode());
+
+        JsonObject resultObject = bodyToJsonObject(EntityUtils.toString(updateResponse.getEntity()));
+        assertEquals(RECORD_ID, resultObject.get("recordIds").getAsJsonArray().get(0).getAsString());
+
+        resultObject = bodyToJsonObject(EntityUtils.toString(recordResponse.getEntity()));
+        assertEquals(LEGAL_TAG, resultObject.get("legal").getAsJsonObject().get("legaltags").getAsString());
+
+        //replace operation
+        updateBody = buildUpdateLegalBody(RECORD_ID, "replace", LEGAL_TAG_2);
+        sendRequest("PATCH", "records", toJson(updateBody), testUtils.getToken());
+        recordResponse = sendRequest("GET", "records/" + RECORD_ID, EMPTY, testUtils.getToken());
+
+        resultObject = bodyToJsonObject(EntityUtils.toString(recordResponse.getEntity()));
+        assertEquals(LEGAL_TAG_2, resultObject.get("legal").getAsJsonObject().get("legaltags").getAsString());
+
+        //remove operation
+        updateBody = buildUpdateLegalBody(RECORD_ID,"remove", LEGAL_TAG_2);
+        updateResponse= sendRequest("PATCH", "records", toJson(updateBody), testUtils.getToken());
+
+        assertEquals(SC_BAD_REQUEST,updateResponse.getCode());
+    }
+
+    @Test
+    public void should_return200AndUpdateAclViewersMetadataOr400ForRemoveRestriction_whenValidRecordsProvided() throws Exception {
+        //add operation
+        JsonObject updateBody = buildUpdateAclBody(RECORD_ID, "add","/acl/viewers", ACL);
+
+        CloseableHttpResponse updateResponse = sendRequest("PATCH", "records", toJson(updateBody), testUtils.getToken());
+        CloseableHttpResponse recordResponse = sendRequest("GET", "records/" + RECORD_ID, EMPTY, testUtils.getToken());
+
+        assertEquals(SC_OK, updateResponse.getCode());
+        assertEquals(SC_OK, recordResponse.getCode());
+
+        JsonObject resultObject = bodyToJsonObject(EntityUtils.toString(updateResponse.getEntity()));
+        assertEquals(RECORD_ID, resultObject.get("recordIds").getAsJsonArray().get(0).getAsString());
+
+        resultObject = bodyToJsonObject(EntityUtils.toString(recordResponse.getEntity()));
+        assertEquals(ACL, resultObject.get("acl").getAsJsonObject().get("viewers").getAsJsonArray().get(0).getAsString());
+
+        //replace operation
+        updateBody = buildUpdateAclBody(RECORD_ID, "replace","/acl/viewers", ACL_2);
+        sendRequest("PATCH", "records", toJson(updateBody), testUtils.getToken());
+        recordResponse = sendRequest("GET", "records/" + RECORD_ID, EMPTY, testUtils.getToken());
+
+        resultObject = bodyToJsonObject(EntityUtils.toString(recordResponse.getEntity()));
+        assertEquals(ACL_2, resultObject.get("acl").getAsJsonObject().get("viewers").getAsJsonArray().get(0).getAsString());
+
+        //remove operation
+        updateBody = buildUpdateAclBody(RECORD_ID,"remove","/acl/viewers", ACL_2);
+        updateResponse = sendRequest("PATCH", "records", toJson(updateBody), testUtils.getToken());
+
+        assertEquals(SC_BAD_REQUEST,updateResponse.getCode());
+    }
+
+    @Test
+    public void should_return200AndUpdateAclOwnersMetadataOr400ForRemoveRestriction_whenValidRecordsProvided() throws Exception {
+        //add operation
+        JsonObject updateBody = buildUpdateAclBody(RECORD_ID, "add","/acl/owners", ACL);
+
+        CloseableHttpResponse updateResponse = sendRequest("PATCH", "records", toJson(updateBody), testUtils.getToken());
+        CloseableHttpResponse recordResponse = sendRequest("GET", "records/" + RECORD_ID, EMPTY, testUtils.getToken());
+
+        assertEquals(SC_OK, updateResponse.getCode());
+        assertEquals(SC_OK, recordResponse.getCode());
+
+        JsonObject resultObject = bodyToJsonObject(EntityUtils.toString(updateResponse.getEntity()));
+        assertEquals(RECORD_ID, resultObject.get("recordIds").getAsJsonArray().get(0).getAsString());
+
+        resultObject = bodyToJsonObject(EntityUtils.toString(recordResponse.getEntity()));
+        assertEquals(ACL, resultObject.get("acl").getAsJsonObject().get("owners").getAsJsonArray().get(0).getAsString());
+
+        //remove operation
+        updateBody = buildUpdateAclBody(RECORD_ID,"remove","/acl/owners", ACL_2);
+        sendRequest("PATCH", "records", toJson(updateBody), testUtils.getToken());
+
+        recordResponse = sendRequest("GET", "records/" + RECORD_ID, EMPTY, testUtils.getToken());
+        resultObject = JsonParser.parseString(EntityUtils.toString(recordResponse.getEntity())).getAsJsonObject();
+
+        assertEquals(ACL, resultObject.get("acl").getAsJsonObject().get("owners").getAsJsonArray().get(0).getAsString());
+
+        //replace operation
+        updateBody = buildUpdateAclBody(RECORD_ID, "replace","/acl/owners ", ACL);
+        sendRequest("PATCH", "records", toJson(updateBody), testUtils.getToken());
+        recordResponse = sendRequest("GET", "records/" + RECORD_ID, EMPTY, testUtils.getToken());
+
+        resultObject = bodyToJsonObject(EntityUtils.toString(recordResponse.getEntity()));
+        assertEquals(ACL, resultObject.get("acl").getAsJsonObject().get("owners").getAsJsonArray().get(0).getAsString());
+    }
+
+    @Test
+    public void should_return206andUpdateLegalMetadata_whenNotExistedRecordProvided() throws Exception {
+        JsonObject updateBody = buildUpdateLegalBody(NOT_EXISTED_RECORD_ID, "replace", LEGAL_TAG);
+
+        CloseableHttpResponse updateResponse = sendRequest("PATCH", "records", toJson(updateBody), testUtils.getToken());
+
+        assertEquals(SC_PARTIAL_CONTENT, updateResponse.getCode());
+        JsonObject resultObject = bodyToJsonObject(EntityUtils.toString(updateResponse.getEntity()));
+
+        System.out.println(resultObject.toString());
+        assertEquals(NOT_EXISTED_RECORD_ID, resultObject.get("notFoundRecordIds").getAsJsonArray().getAsString());
+    }
+
+    @Test
+    public void should_return206andUpdateAclMetadata_whenNotExistedRecordProvided() throws Exception {
+        JsonObject updateBody = buildUpdateAclBody(NOT_EXISTED_RECORD_ID, "replace","/acl/viewers", ACL);
+
+        CloseableHttpResponse updateResponse = sendRequest("PATCH", "records", toJson(updateBody), testUtils.getToken());
+
+        assertEquals(SC_PARTIAL_CONTENT, updateResponse.getCode());
+        JsonObject resultObject = bodyToJsonObject(EntityUtils.toString(updateResponse.getEntity()));
+
+        System.out.println(resultObject.toString());
+        assertEquals(NOT_EXISTED_RECORD_ID, resultObject.get("notFoundRecordIds").getAsJsonArray().getAsString());
+    }
+
+    private JsonObject buildUpdateLegalBody(String id, String op, String val) {
+        JsonArray records = new JsonArray();
+        records.add(id);
+
+        JsonArray value = new JsonArray();
+        value.add(val);
+        JsonObject operation = new JsonObject();
+        operation.addProperty("op", op);
+        operation.addProperty("path", "/legal/legaltags");
+        operation.add("value", value);
+        JsonArray ops = new JsonArray();
+        ops.add(operation);
+
+        JsonObject query = new JsonObject();
+        query.add("ids", records);
+
+        JsonObject updateBody = new JsonObject();
+        updateBody.add("query", query);
+        updateBody.add("ops", ops);
+
+        return updateBody;
+    }
+
+    private JsonObject buildUpdateAclBody(String id, String op, String path, String val) {
+        JsonArray records = new JsonArray();
+        records.add(id);
+
+        JsonArray value = new JsonArray();
+        value.add(val);
+        JsonObject operation = new JsonObject();
+        operation.addProperty("op", op);
+        operation.addProperty("path", path);
+        operation.add("value", value);
+        JsonArray ops = new JsonArray();
+        ops.add(operation);
+
+        JsonObject query = new JsonObject();
+        query.add("ids", records);
+
+        JsonObject updateBody = new JsonObject();
+        updateBody.add("query", query);
+        updateBody.add("ops", ops);
+
+        return updateBody;
+    }
+}
diff --git a/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/replay/ReplayEndpointsTests.java b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/replay/ReplayEndpointsTests.java
new file mode 100644
index 0000000000000000000000000000000000000000..010e4d1e57b8fb2a97e3ce24f92504d6a56885b4
--- /dev/null
+++ b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/replay/ReplayEndpointsTests.java
@@ -0,0 +1,367 @@
+// Copyright © Microsoft Corporation
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package org.opengroup.osdu.storage.replay;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.fail;
+import static org.junit.jupiter.api.Assumptions.assumeTrue;
+
+import com.google.gson.Gson;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
+import org.apache.hc.core5.http.io.entity.EntityUtils;
+import org.apache.http.HttpStatus;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.Timeout;
+import org.opengroup.osdu.storage.model.ReplayStatusResponseHelper;
+import org.opengroup.osdu.storage.records.RecordsApiAcceptanceTests;
+import org.opengroup.osdu.storage.util.ConfigUtils;
+import org.opengroup.osdu.storage.util.DummyRecordsHelper;
+import org.opengroup.osdu.storage.util.HeaderUtils;
+import org.opengroup.osdu.storage.util.LegalTagUtils;
+import org.opengroup.osdu.storage.util.ReplayUtils;
+import org.opengroup.osdu.storage.util.TenantUtils;
+import org.opengroup.osdu.storage.util.TestBase;
+import org.opengroup.osdu.storage.util.TestUtils;
+import org.opengroup.osdu.storage.util.TokenTestUtils;
+
+public final class ReplayEndpointsTests extends TestBase {
+    private static String LEGAL_TAG_NAME = LegalTagUtils.createRandomName();
+
+    private static final String INVALID_KIND = TenantUtils.getTenantName() + ":ds:1.0."
+            + System.currentTimeMillis();
+
+    private static final TokenTestUtils TOKEN_TEST_UTILS = new TokenTestUtils();
+
+    @BeforeAll
+    public static void classSetup() throws Exception {
+        ReplayEndpointsTests.classSetup(TOKEN_TEST_UTILS.getToken());
+    }
+
+    @AfterAll
+    public static void classTearDown() throws Exception {
+        ReplayEndpointsTests.classTearDown(TOKEN_TEST_UTILS.getToken());
+    }
+
+    @BeforeEach
+    @Override
+    public void setup() throws Exception {
+        this.testUtils = new TokenTestUtils();
+        this.configUtils = new ConfigUtils("test.properties");
+        assumeTrue(configUtils.isTestReplayEnabled());
+    }
+
+    @AfterEach
+    @Override
+    public void tearDown() throws Exception {
+        this.testUtils = null;
+    }
+
+    public static void classSetup(String token) throws Exception {
+        LegalTagUtils.create(LEGAL_TAG_NAME, token);
+    }
+
+    public static void classTearDown(String token) throws Exception {
+        LegalTagUtils.delete(LEGAL_TAG_NAME, token);
+    }
+
+    @Test
+    public void should_return_400_when_givenNoOperationNameIsNotInRequest() throws Exception {
+
+        String requestBody = ReplayUtils.createJsonEmpty();
+        CloseableHttpResponse response = TestUtils.send("replay", "POST", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), requestBody, "");
+        assertEquals(HttpStatus.SC_BAD_REQUEST, response.getCode());
+        String actualErrorMessage = ReplayUtils.getFieldFromResponse(response, "message");
+        assertEquals(HttpStatus.SC_BAD_REQUEST, response.getCode());
+        assertEquals("Operation field is required. The valid operations are: 'replay', 'reindex'.", actualErrorMessage);
+    }
+
+    @Test
+    public void should_return_400_when_givenKindIsEmpty() throws Exception {
+
+        String requestBody = ReplayUtils.createJsonWithKind("reindex", new ArrayList<>());
+        CloseableHttpResponse response = TestUtils.send("replay", "POST", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), requestBody, "");
+        String actualErrorMessage = ReplayUtils.getFieldFromResponse(response, "message");
+        assertEquals(HttpStatus.SC_BAD_REQUEST, response.getCode());
+        assertEquals("Currently restricted to a single valid kind.", actualErrorMessage);
+    }
+
+    @Test
+    public void should_return_400_when_givenKindSizeIsGreaterDenOne() throws Exception {
+
+        List<String> kindList = new ArrayList<>();
+        kindList.add(getKind());
+        kindList.add(getKind());
+
+        String requestBody = ReplayUtils.createJsonWithKind("reindex", kindList);
+        CloseableHttpResponse response = TestUtils.send("replay", "POST", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), requestBody, "");
+        String actualErrorMessage = ReplayUtils.getFieldFromResponse(response, "message");
+        assertEquals(HttpStatus.SC_BAD_REQUEST, response.getCode());
+        assertEquals("Currently restricted to a single valid kind.", actualErrorMessage);
+    }
+
+    @Test
+    public void Should_return_400_when_givenInvalidKind() throws Exception {
+
+        List<String> kindList = new ArrayList<>();
+        kindList.add(INVALID_KIND);
+        String requestBody = ReplayUtils.createJsonWithKind("reindex", kindList);
+        CloseableHttpResponse response = TestUtils.send("replay", "POST", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), requestBody, "");
+        String actualErrorMessage = ReplayUtils.getFieldFromResponse(response, "message");
+        assertEquals(HttpStatus.SC_BAD_REQUEST, response.getCode());
+        assertEquals("The requested kind does not exist.", actualErrorMessage);
+    }
+
+    @Test
+    public void Should_return_400_when_givenInvalidOperationName() throws Exception {
+
+        String requestBody = ReplayUtils.createJsonWithOperationName("invalidOperation");
+        CloseableHttpResponse response = TestUtils.send("replay", "POST", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), requestBody, "");
+        String actualErrorMessage = ReplayUtils.getFieldFromResponse(response, "message");
+        assertEquals(HttpStatus.SC_BAD_REQUEST, response.getCode());
+        assertEquals("Not a valid operation. The valid operations are: [reindex, replay]", actualErrorMessage);
+    }
+
+    @Test
+    public void should_return_200_GivenReplayAll() throws Exception {
+
+        if (configUtils != null && configUtils.getIsTestReplayAllEnabled()) {
+
+            String kind_1 = getKind();
+            String kind_2 = getKind();
+            List<String> givenKindList = Arrays.asList(kind_1, kind_2);
+            List<String> totalRecordIds = this.createTestRecordForGivenCapacityAndKinds(500, 100, givenKindList);
+
+            DummyRecordsHelper dummyRecordsHelper = new DummyRecordsHelper();
+
+            CloseableHttpResponse response = TestUtils.send("query/kinds", "GET", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "", "?limit=10");
+            assertEquals(HttpStatus.SC_OK, response.getCode());
+            DummyRecordsHelper.QueryResultMock responseObject = dummyRecordsHelper.getQueryResultMockFromResponse(response);
+            List<String> kindList = new ArrayList<>(Arrays.asList(responseObject.results));
+            kindList.add(kind_1);
+            kindList.add(kind_2);
+
+            performValidationBeforeOrAfterReplay(kindList, givenKindList, "*:*:*:*", 2000);
+            String requestBody = ReplayUtils.createJsonWithOperationName("reindex");
+            ReplayStatusResponseHelper replayStatusResponseHelper = this.performReplay(requestBody);
+            assertEquals("reindex", replayStatusResponseHelper.getOperation());
+
+            assertNull(replayStatusResponseHelper.getFilter());
+            assertEquals(1, replayStatusResponseHelper.getStatus().size());
+            performValidationBeforeOrAfterReplay(kindList, givenKindList, "*:*:*:*", 2000);
+        }
+    }
+
+    @Test
+    @Timeout(2)
+    public void should_return_200_givenSingleKind() throws Exception {
+
+        if (configUtils != null && configUtils.getIsTestReplayAllEnabled()) {
+
+            String kind_1 = getKind();
+            List<String> kindList = new ArrayList<>();
+            kindList.add(kind_1);
+            List<String> ids = this.createTestRecordForGivenCapacityAndKinds(1, 1, kindList);
+
+            performValidationBeforeOrAfterReplay(kindList, kindList, kind_1, 1);
+            String requestBody = ReplayUtils.createJsonWithKind("reindex", kindList);
+            ReplayStatusResponseHelper replayStatusResponseHelper = performReplay(requestBody);
+
+            assertEquals("reindex", replayStatusResponseHelper.getOperation());
+            assertEquals(1, replayStatusResponseHelper.getFilter().getKinds().size());
+            assertEquals(1, replayStatusResponseHelper.getStatus().size());
+            assertEquals(kind_1, replayStatusResponseHelper.getFilter().getKinds().get(0));
+
+            performValidationBeforeOrAfterReplay(kindList, kindList, kind_1, 1);
+            deleteRecords(ids);
+        }
+    }
+
+    public List<String> createTestRecordForGivenCapacityAndKinds(int n, int factor, List<String> kinds) throws Exception {
+
+        int totalRecordCount = n * kinds.size();
+        long startTime = System.currentTimeMillis();
+        List<String> totalIds = new ArrayList<>();
+        for (String kind : kinds) {
+            int counter = n;
+            while (counter > 0) {
+                List<String> listIds = create_N_TestRecordForGivenKind(factor, kind);
+                totalIds = Stream.concat(totalIds.stream(), listIds.stream()).collect(Collectors.toList());
+                counter -= factor;
+                Thread.sleep(1000);
+            }
+
+        }
+
+        Thread.sleep(40000);
+        return totalIds;
+    }
+
+    @Test
+    public void should_return_400_when_givenEmptyJSONIsSent() throws Exception {
+
+        String requestBody = ReplayUtils.createJsonEmpty();
+        CloseableHttpResponse response = TestUtils.send("replay", "POST", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), requestBody, "");
+        assertEquals(HttpStatus.SC_BAD_REQUEST, response.getCode());
+    }
+
+    protected List<String> create_N_TestRecordForGivenKind(int n, String kind) throws Exception {
+
+        String json = "";
+        List<String> ids = new ArrayList<>(n);
+        for (int i = 0; i < n; i++) {
+            String id1 = TenantUtils.getTenantName() + ":inttest:" + System.currentTimeMillis() + i;
+            json += RecordsApiAcceptanceTests.singleEntityBody(id1, "ash ketchum", kind, LEGAL_TAG_NAME);
+            if (i != n - 1) {
+                json += ",";
+            }
+            ids.add(id1);
+        }
+
+        json = "[" + json + "]";
+
+        CloseableHttpResponse response = TestUtils.send("records", "PUT", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), json, "");
+
+        String responseJson = EntityUtils.toString(response.getEntity());
+        assertEquals(HttpStatus.SC_CREATED, response.getCode());
+        Gson gson = new Gson();
+        DummyRecordsHelper.CreateRecordResponse result = gson.fromJson(
+                responseJson,
+                DummyRecordsHelper.CreateRecordResponse.class
+                                                                      );
+        assertEquals(n, result.recordCount);
+        assertEquals(n, result.recordIds.length);
+        assertEquals(n, result.recordIdVersions.length);
+
+        return ids;
+    }
+
+    private void performValidationBeforeOrAfterReplay(List<String> kinds, List<String> givenKindList, String kindType, int totalReplayAllRecord) throws Exception {
+
+        long startTime = System.currentTimeMillis();
+        CloseableHttpResponse response = null;
+
+        int initialRecordCount = 0;
+        int countNoOfAPICalls = 0;
+        while ((initialRecordCount = getIndexedRecordCount(givenKindList)) != totalReplayAllRecord) {
+            if (countNoOfAPICalls > 10)
+                fail();
+
+            Thread.sleep(configUtils.getTimeoutForReplay());
+            countNoOfAPICalls++;
+        }
+
+        assertEquals(totalReplayAllRecord, initialRecordCount);
+
+        System.out.println("Total count for Kind " + kindType + " is " + initialRecordCount);
+
+        for (String kind : kinds) {
+            response = TestUtils.send(ReplayUtils.getIndexerUrl(), "index?kind=" + kind, "DELETE", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "", "");
+            Thread.sleep(1000);
+        }
+
+        int countOfRecord = getIndexedRecordCount(givenKindList);
+        System.out.println("Total count for Kind " + kindType + " after deletion is " + countOfRecord);
+        assertEquals(0, countOfRecord);
+
+        System.out.println("The end time for performValidationBeforeOrAfterReplay for KindType " + kindType + "is " + (System.currentTimeMillis() - startTime));
+    }
+
+    private ReplayStatusResponseHelper performReplay(String requestBody) throws Exception {
+
+        CloseableHttpResponse response = TestUtils.send("replay", "POST", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), requestBody, "");
+
+        if (response.getCode() == HttpStatus.SC_INTERNAL_SERVER_ERROR)
+            System.out.println("Error in replay call  " + ReplayUtils.getFieldFromResponse(response, "message"));
+
+        assertEquals(202, response.getCode());
+
+        String replayId = ReplayUtils.getFieldFromResponse(response, "replayId");
+        response = TestUtils.send("replay/status/", "GET", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "", replayId);
+
+        ReplayStatusResponseHelper replayStatusResponseHelper = ReplayUtils.getConvertedReplayStatusResponseFromResponse(response);
+        System.out.println("Total Number of Record to be Processed for Replay " + replayStatusResponseHelper.getTotalRecords());
+
+        int countNoOfAPICalls = 0;
+
+        while (!replayStatusResponseHelper.getOverallState().equals("COMPLETED")) {
+            assertNotEquals("FAILED", replayStatusResponseHelper.getOverallState());
+            response = TestUtils.send("replay/status/", "GET", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "", replayId);
+            replayStatusResponseHelper = ReplayUtils.getConvertedReplayStatusResponseFromResponse(response);
+            if (replayStatusResponseHelper.getStatus() != null && !replayStatusResponseHelper.getStatus().isEmpty())
+                System.out.println("Number of Record to be Processed for Replay  " + replayStatusResponseHelper.getStatus().get(0).getProcessedRecords());
+
+            if (countNoOfAPICalls > 10)
+                 fail();
+
+            Thread.sleep(configUtils.getTimeoutForReplay());
+            countNoOfAPICalls++;
+        }
+
+        assertEquals(replayId, replayStatusResponseHelper.getReplayId());
+        return replayStatusResponseHelper;
+    }
+
+    protected void deleteRecords(List<String> ids) {
+
+        long startTime = System.currentTimeMillis();
+
+        ids.parallelStream().forEach((id) -> {
+            try {
+                TestUtils.send("records/" + id, "DELETE", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "", "");
+            } catch (Exception e) {
+            }
+        });
+
+        System.out.println("The totalTime for delete Records for size " + ids.size() + "is " + (System.currentTimeMillis() - startTime));
+    }
+
+    private int getIndexedRecordCount(List<String> kinds) throws Exception {
+
+        int recordCountIndexed = 0;
+        for (String kind : kinds) {
+            String requestBody = ReplayUtils.getSearchCountQueryForKind(kind);
+            CloseableHttpResponse response = TestUtils.send(ReplayUtils.getSearchUrl(), "query", "POST", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), requestBody, "");
+            recordCountIndexed += Integer.parseInt(ReplayUtils.getFieldFromResponse(response, "totalCount"));
+
+        }
+        return recordCountIndexed;
+    }
+
+    @Test
+    public void should_return_404_when_givenInvalidReplayID() throws Exception {
+
+        CloseableHttpResponse response = TestUtils.send("replay/status/", "GET", HeaderUtils.getHeaders(TenantUtils.getTenantName(), testUtils.getToken()), "", "1234");
+        String actualErrorMessage = ReplayUtils.getFieldFromResponse(response, "message");
+        assertEquals("The replay ID 1234 is invalid.", actualErrorMessage);
+        assertEquals(404, response.getCode());
+    }
+
+    public static String getKind() throws InterruptedException {
+
+        Thread.sleep(1);
+        return TenantUtils.getTenantName() + ":ds:inttest:1.0." + System.nanoTime();
+    }
+}
diff --git a/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/util/ConfigUtils.java b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/util/ConfigUtils.java
new file mode 100644
index 0000000000000000000000000000000000000000..673183446e2756bba4e4fb09091e22c582ee7a86
--- /dev/null
+++ b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/util/ConfigUtils.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2020  Microsoft Corporation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.opengroup.osdu.storage.util;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Properties;
+
+public class ConfigUtils {
+
+    private final Properties properties;
+
+    public ConfigUtils(String propertiesFileName) throws IOException {
+        ClassLoader loader = Thread.currentThread().getContextClassLoader();
+        try (InputStream input = loader.getResourceAsStream(propertiesFileName)) {
+            properties = new Properties();
+            // load a properties file
+            properties.load(input);
+        }
+    }
+
+    public boolean getBooleanProperty(String propertyName, String defaultValue) {
+        String propValue = properties.getProperty(propertyName, defaultValue);
+        String envValue = getEnvValue(propertyName);
+        return (envValue == null || envValue.isEmpty()) ? Boolean.parseBoolean(propValue)
+            : Boolean.parseBoolean(envValue);
+    }
+
+    public long getLongProperty(String propertyName, String defaultValue) {
+        String propValue = properties.getProperty(propertyName, defaultValue);
+        String envValue = getEnvValue(propertyName);
+        return (envValue == null || envValue.isEmpty()) ? Long.parseLong(propValue)
+            : Long.parseLong(envValue);
+    }
+
+    public boolean getIsSchemaEndpointsEnabled() {
+        return !getBooleanProperty("schema.endpoints.disabled", "true");
+    }
+    public boolean getIsCollaborationEnabled() {
+        return getBooleanProperty("collaboration.enabled", "false");
+    }
+
+    public boolean isTestReplayEnabled(){ return  getBooleanProperty("test.replay.enabled", "false");}
+
+    public boolean getIsTestReplayAllEnabled() { return  getBooleanProperty("test.replayAll.enabled", "false");}
+
+    public long getTimeoutForReplay() { return  getLongProperty("test.replayAll.timeout", "60");}
+
+    private static String getEnvValue(String propertyName) {
+        return System.getenv(propertyName.toUpperCase().replaceAll("\\.", "_"));
+    }
+
+}
diff --git a/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/util/CustomHttpClientResponseHandler.java b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/util/CustomHttpClientResponseHandler.java
new file mode 100644
index 0000000000000000000000000000000000000000..ac8478e6a6f13d2a52bbdaa949875c09c817280f
--- /dev/null
+++ b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/util/CustomHttpClientResponseHandler.java
@@ -0,0 +1,50 @@
+/*
+ *  Copyright 2020-2024 Google LLC
+ *  Copyright 2020-2024 EPAM Systems, Inc
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+package org.opengroup.osdu.storage.util;
+
+import java.io.IOException;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
+import org.apache.hc.core5.http.ClassicHttpResponse;
+import org.apache.hc.core5.http.ContentType;
+import org.apache.hc.core5.http.HttpEntity;
+import org.apache.hc.core5.http.HttpStatus;
+import org.apache.hc.core5.http.ParseException;
+import org.apache.hc.core5.http.io.HttpClientResponseHandler;
+import org.apache.hc.core5.http.io.entity.EntityUtils;
+import org.apache.hc.core5.http.io.entity.StringEntity;
+
+@Slf4j
+public class CustomHttpClientResponseHandler implements HttpClientResponseHandler<CloseableHttpResponse> {
+
+    @Override
+    public CloseableHttpResponse handleResponse(ClassicHttpResponse classicHttpResponse) {
+        HttpEntity entity = classicHttpResponse.getEntity();
+        if(classicHttpResponse.getCode() != HttpStatus.SC_NO_CONTENT) {
+            String body = "";
+            try {
+                body = EntityUtils.toString(entity);
+            } catch (IOException | ParseException e) {
+                log.error("unable to parse response");
+            }
+            HttpEntity newEntity = new StringEntity(body, ContentType.parse(entity.getContentType()));
+            classicHttpResponse.setEntity(newEntity);
+        }
+        return (CloseableHttpResponse) classicHttpResponse;
+    }
+}
diff --git a/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/util/DummyRecordsHelper.java b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/util/DummyRecordsHelper.java
new file mode 100644
index 0000000000000000000000000000000000000000..e1116aefc394069088eca61e9aee2dd292ba09ec
--- /dev/null
+++ b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/util/DummyRecordsHelper.java
@@ -0,0 +1,133 @@
+// Copyright 2017-2019, Schlumberger
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package org.opengroup.osdu.storage.util;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.google.gson.Gson;
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
+import org.apache.hc.core5.http.ParseException;
+import org.apache.hc.core5.http.io.entity.EntityUtils;
+
+public class DummyRecordsHelper {
+
+    protected static final long NOW = System.currentTimeMillis();
+
+    public final String KIND = TenantUtils.getTenantName() + ":storage:inttest:1.0.0" + NOW;
+
+    public QueryResultMock getQueryResultMockFromResponse(CloseableHttpResponse response) {
+        assertTrue(response.getEntity().getContentType().contains("application/json"));
+        String json;
+        try {
+            json = EntityUtils.toString(response.getEntity());
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        } catch (ParseException e) {
+            throw new RuntimeException(e);
+        }
+        Gson gson = new Gson();
+        return gson.fromJson(json, QueryResultMock.class);
+    }
+
+    public RecordsMock getRecordsMockFromResponse(CloseableHttpResponse response) {
+        assertTrue(response.getEntity().getContentType().contains("application/json"));
+        String json = null;
+        try {
+            json = EntityUtils.toString(response.getEntity());
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        } catch (ParseException e) {
+            throw new RuntimeException(e);
+        }
+        Gson gson = new Gson();
+        return gson.fromJson(json, RecordsMock.class);
+    }
+
+    public ConvertedRecordsMock getConvertedRecordsMockFromResponse(CloseableHttpResponse response) {
+        assertTrue(response.getEntity().getContentType().contains("application/json"));
+        String json = null;
+        try {
+            json = EntityUtils.toString(response.getEntity());
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        } catch (ParseException e) {
+            throw new RuntimeException(e);
+        }
+        Gson gson = new Gson();
+        return gson.fromJson(json, ConvertedRecordsMock.class);
+    }
+
+    public class QueryResultMock {
+        public String cursor;
+        public String[] results;
+    }
+
+    public class RecordsMock {
+        public RecordResultMock[] records;
+        public String[] invalidRecords;
+        public String[] retryRecords;
+    }
+
+    public class ConvertedRecordsMock {
+        public RecordResultMock[] records;
+        public String[] notFound;
+        public List<RecordStatusMock> conversionStatuses;
+    }
+
+    public class RecordStatusMock {
+        public String id;
+        public String status;
+        public List<String> errors;
+    }
+
+    public class RecordResultMock {
+        public String id;
+        public String version;
+        public String kind;
+        public RecordAclMock acl;
+        public Map<String, Object> data;
+        public RecordLegalMock legal;
+        public RecordAncestryMock ancestry;
+        public Map<String, String> tags;
+        public String modifyTime;
+        public String modifyUser;
+        public String createTime;
+        public String createUser;
+    }
+
+    public class RecordAclMock {
+        public String[] viewers;
+        public String[] owners;
+    }
+
+    public class RecordLegalMock {
+        public String[] legaltags;
+        public String[] otherRelevantDataCountries;
+    }
+
+    public class RecordAncestryMock {
+        public String[] parents;
+    }
+
+    public class CreateRecordResponse {
+        public int recordCount;
+        public String[] recordIds;
+        public String[] skippedRecordIds;
+        public String[] recordIdVersions;
+    }
+}
diff --git a/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/util/EntitlementsUtil.java b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/util/EntitlementsUtil.java
new file mode 100644
index 0000000000000000000000000000000000000000..a79a1e529e9365fb5bc690595b6d8877fabbec5d
--- /dev/null
+++ b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/util/EntitlementsUtil.java
@@ -0,0 +1,68 @@
+/*
+ *  Copyright 2020-2023 Google LLC
+ *  Copyright 2020-2023 EPAM Systems, Inc
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+package org.opengroup.osdu.storage.util;
+
+import com.google.gson.JsonObject;
+import java.util.Map;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
+import org.opengroup.osdu.core.common.http.HttpRequest;
+
+public class EntitlementsUtil {
+
+  public static final String GROUPS_ENDPOINT = "groups";
+
+  public static CloseableHttpResponse createEntitlementsGroup(Map<String, String> headers,
+                                                              String groupName,
+                                                              String groupDescription) throws Exception {
+    String body = getCreateGroupBody(groupName, groupDescription);
+    CloseableHttpResponse response = TestUtils.send(
+        getEntitlementsUrl(),
+        GROUPS_ENDPOINT,
+        HttpRequest.POST,
+        headers,
+        body,
+        ""
+    );
+    return response;
+  }
+
+  public static CloseableHttpResponse deleteEntitlementsGroup(Map<String, String> headers,
+                                                              String groupEmail)
+      throws Exception {
+    CloseableHttpResponse response = TestUtils.send(
+        getEntitlementsUrl(),
+        GROUPS_ENDPOINT,
+        HttpRequest.DELETE,
+        headers,
+        "",
+        groupEmail
+    );
+    return response;
+  }
+
+  protected static String getCreateGroupBody(String name, String groupDescription) {
+    JsonObject groupBody = new JsonObject();
+    groupBody.addProperty("name", name);
+    groupBody.addProperty("description", groupDescription);
+    return groupBody.toString();
+  }
+
+  protected static String getEntitlementsUrl() {
+    return System.getProperty("ENTITLEMENTS_URL", System.getenv("ENTITLEMENTS_URL"));
+  }
+}
diff --git a/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/util/HeaderUtils.java b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/util/HeaderUtils.java
new file mode 100644
index 0000000000000000000000000000000000000000..60d38f2d510c79038f2328b5774d865c600a8555
--- /dev/null
+++ b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/util/HeaderUtils.java
@@ -0,0 +1,91 @@
+// Copyright 2017-2019, Schlumberger
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package org.opengroup.osdu.storage.util;
+
+import com.google.api.client.util.Strings;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+
+public class HeaderUtils {
+
+  protected static final String COLLABORATION_HEADER = "x-collaboration";
+
+  public static Map<String, String> getHeaders(String tenantName, String token) {
+    Map<String, String> headers = new HashMap<>();
+    if (tenantName == null || tenantName.isEmpty()) {
+      tenantName = TenantUtils.getTenantName();
+    }
+    headers.put("Data-Partition-Id", tenantName);
+    headers.put("Authorization", token);
+
+    final String correlationId = UUID.randomUUID().toString();
+    System.out.printf("Using correlation-id for the request: %s \n", correlationId);
+    headers.put("correlation-id", correlationId);
+
+    return headers;
+  }
+
+  public static Map<String, String> getHeadersWithxCollaboration(String collaborationId,
+      String applicationName, String tenantName, String token) {
+    Map<String, String> headers = getHeaders(tenantName, token);
+    if (!Strings.isNullOrEmpty(collaborationId)) {
+      headers.put(COLLABORATION_HEADER,
+          "id=" + collaborationId + ",application=" + applicationName);
+    }
+    return headers;
+  }
+
+  public static Map<String, String> getHeadersWithxCollaborationWithoutId(String collaborationId,
+      String applicationName, String tenantName, String token) {
+    Map<String, String> headers = getHeaders(tenantName, token);
+    if (Strings.isNullOrEmpty(collaborationId)) {
+      headers.put(COLLABORATION_HEADER, "application=" + applicationName);
+    }else {
+      headers.put(COLLABORATION_HEADER, "id=" + collaborationId + ",application=" + applicationName);
+    }
+    return headers;
+  }
+
+  public static Map<String, String> getHeadersWithoutAuth(String tenantName, String token) {
+    Map<String, String> headers = new HashMap<>();
+    if (tenantName == null || tenantName.isEmpty()) {
+      tenantName = TenantUtils.getTenantName();
+    }
+    headers.put("Data-Partition-Id", tenantName);
+
+    final String correlationId = UUID.randomUUID().toString();
+    System.out.printf("Using correlation-id for the request: %s \n", correlationId);
+    headers.put("correlation-id", correlationId);
+
+    return headers;
+  }
+
+  public static Map<String, String> getHeadersWithoutDataPartitionId(String tenantName,
+      String token) {
+    Map<String, String> headers = new HashMap<>();
+    if (tenantName == null || tenantName.isEmpty()) {
+      tenantName = TenantUtils.getTenantName();
+    }
+    headers.put("Authorization", token);
+
+    final String correlationId = UUID.randomUUID().toString();
+    System.out.printf("Using correlation-id for the request: %s \n", correlationId);
+    headers.put("correlation-id", correlationId);
+
+    return headers;
+  }
+
+}
diff --git a/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/util/LegalTagUtils.java b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/util/LegalTagUtils.java
new file mode 100644
index 0000000000000000000000000000000000000000..f24a1691cb5306b83af24f457811662f24b4f9a0
--- /dev/null
+++ b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/util/LegalTagUtils.java
@@ -0,0 +1,78 @@
+// Copyright 2017-2019, Schlumberger
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package org.opengroup.osdu.storage.util;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonObject;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
+import org.apache.http.HttpStatus;
+
+public class LegalTagUtils {
+	public static String createRandomName() {
+		return TenantUtils.getTenantName() + "-storage-" + System.currentTimeMillis();
+	}
+
+	public static CloseableHttpResponse create(String legalTagName, String token) throws Exception {
+		return create("US", legalTagName, "2099-01-25", "Public Domain Data", token);
+	}
+
+	protected static CloseableHttpResponse create(String countryOfOrigin, String name, String expDate, String dataType, String token)
+			throws Exception {
+		String body = getBody(countryOfOrigin, name, expDate, dataType);
+		CloseableHttpResponse response = TestUtils.send(getLegalUrl(), "legaltags", "POST", HeaderUtils.getHeaders(TenantUtils.getTenantName(), token), body,
+				"");
+
+		assertEquals(HttpStatus.SC_CREATED, response.getCode());
+		Thread.sleep(100);
+		return response;
+	}
+
+	public static CloseableHttpResponse delete(String legalTagName, String token) throws Exception {
+		return TestUtils.send(getLegalUrl(), "legaltags/" + legalTagName, "DELETE", HeaderUtils.getHeaders(TenantUtils.getTenantName(), token), "", "");
+	}
+
+	protected static String getLegalUrl() {
+		String legalUrl = System.getProperty("LEGAL_URL", System.getenv("LEGAL_URL"));
+		if (legalUrl == null || legalUrl.contains("-null")) {
+			legalUrl = "https://os-legal-dot-opendes.appspot.com/api/legal/v1/";
+		}
+		return legalUrl;
+	}
+
+	protected static String getBody(String countryOfOrigin, String name, String expDate, String dataType) {
+
+		JsonArray coo = new JsonArray();
+		coo.add(countryOfOrigin);
+
+		JsonObject properties = new JsonObject();
+		properties.add("countryOfOrigin", coo);
+		properties.addProperty("contractId", "A1234");
+		properties.addProperty("expirationDate", expDate);
+		properties.addProperty("dataType", dataType);
+		properties.addProperty("originator", "MyCompany");
+		properties.addProperty("securityClassification", "Public");
+		properties.addProperty("exportClassification", "EAR99");
+		properties.addProperty("personalData", "No Personal Data");
+
+		JsonObject tag = new JsonObject();
+		tag.addProperty("name", name);
+		tag.addProperty("description", "test for " + name);
+		tag.add("properties", properties);
+
+		return tag.toString();
+	}
+}
diff --git a/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/util/OpenIDTokenProvider.java b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/util/OpenIDTokenProvider.java
new file mode 100644
index 0000000000000000000000000000000000000000..d87386ad095ea136e37baf33b089b5e25c461f84
--- /dev/null
+++ b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/util/OpenIDTokenProvider.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright 2020-2023 Google LLC
+ * Copyright 2020-2023 EPAM Systems, Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.opengroup.osdu.storage.util;
+
+import com.nimbusds.oauth2.sdk.AuthorizationGrant;
+import com.nimbusds.oauth2.sdk.ClientCredentialsGrant;
+import com.nimbusds.oauth2.sdk.ParseException;
+import com.nimbusds.oauth2.sdk.Scope;
+import com.nimbusds.oauth2.sdk.TokenRequest;
+import com.nimbusds.oauth2.sdk.TokenResponse;
+import com.nimbusds.oauth2.sdk.auth.ClientAuthentication;
+import com.nimbusds.oauth2.sdk.auth.ClientSecretBasic;
+import com.nimbusds.oauth2.sdk.auth.Secret;
+import com.nimbusds.oauth2.sdk.id.ClientID;
+import com.nimbusds.openid.connect.sdk.OIDCTokenResponseParser;
+import java.io.IOException;
+import java.net.URI;
+import java.util.Objects;
+import net.minidev.json.JSONObject;
+import org.opengroup.osdu.storage.util.conf.OpenIDProviderConfig;
+
+public class OpenIDTokenProvider {
+
+  private static final OpenIDProviderConfig openIDProviderConfig = OpenIDProviderConfig.Instance();
+  private static final String ID_TOKEN = "id_token";
+  private final AuthorizationGrant clientGrant = new ClientCredentialsGrant();
+  private final URI tokenEndpointURI;
+  private final Scope scope;
+  private final ClientAuthentication clientAuthentication;
+  private final ClientAuthentication noAccessClientAuthentication;
+  private final ClientAuthentication dataRootAuthentication;
+
+  public OpenIDTokenProvider() {
+    this.tokenEndpointURI = openIDProviderConfig.getProviderMetadata().getTokenEndpointURI();
+    this.scope = new Scope(openIDProviderConfig.getScopes());
+    this.clientAuthentication =
+        new ClientSecretBasic(
+            new ClientID(openIDProviderConfig.getClientId()),
+            new Secret(openIDProviderConfig.getClientSecret()));
+    this.noAccessClientAuthentication =
+        new ClientSecretBasic(
+            new ClientID(openIDProviderConfig.getNoAccessClientId()),
+            new Secret(openIDProviderConfig.getNoAccessClientSecret()));
+    this.dataRootAuthentication =
+        new ClientSecretBasic(
+            new ClientID(openIDProviderConfig.getDataRootClientId()),
+            new Secret(openIDProviderConfig.getDataRootClientSecret()));
+  }
+
+  public String getToken() {
+    try {
+      TokenRequest request =
+          new TokenRequest(
+              this.tokenEndpointURI, this.clientAuthentication, this.clientGrant, this.scope);
+      return requestToken(request);
+    } catch (ParseException | IOException e) {
+      throw new RuntimeException("Unable get credentials from INTEGRATION_TESTER variables", e);
+    }
+  }
+
+  public String getNoAccessToken() {
+    try {
+      TokenRequest request =
+          new TokenRequest(
+              this.tokenEndpointURI,
+              this.noAccessClientAuthentication,
+              this.clientGrant,
+              this.scope);
+      return requestToken(request);
+    } catch (ParseException | IOException e) {
+      throw new RuntimeException("Unable get credentials from INTEGRATION_TESTER variables", e);
+    }
+  }
+
+  public String getDataRootToken() {
+    try {
+      TokenRequest request =
+          new TokenRequest(
+              this.tokenEndpointURI, this.dataRootAuthentication, this.clientGrant, this.scope);
+      return requestToken(request);
+    } catch (ParseException | IOException e) {
+      throw new RuntimeException("Unable get credentials from INTEGRATION_TESTER variables", e);
+    }
+  }
+
+  private String requestToken(TokenRequest tokenRequest) throws ParseException, IOException {
+
+    TokenResponse parse = OIDCTokenResponseParser.parse(tokenRequest.toHTTPRequest().send());
+
+    if (!parse.indicatesSuccess()) {
+      throw new RuntimeException("Unable get credentials from INTEGRATION_TESTER variables");
+    }
+
+    JSONObject jsonObject = parse.toSuccessResponse().toJSONObject();
+    String idTokenValue = jsonObject.getAsString(ID_TOKEN);
+    if (Objects.isNull(idTokenValue) || idTokenValue.isEmpty()) {
+      throw new RuntimeException("Unable get credentials from INTEGRATION_TESTER variables");
+    }
+    return idTokenValue;
+  }
+}
diff --git a/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/util/RecordUtil.java b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/util/RecordUtil.java
new file mode 100644
index 0000000000000000000000000000000000000000..bcf1875d1f246105c648d5244a49a742b2a0391f
--- /dev/null
+++ b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/util/RecordUtil.java
@@ -0,0 +1,795 @@
+// Copyright 2017-2019, Schlumberger
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package org.opengroup.osdu.storage.util;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonObject;
+import org.opengroup.osdu.core.common.Constants;
+
+public class RecordUtil {
+	private static final String UNIT_OF_MEASURE_ID = "unitOfMeasureID";
+
+    public static String createDefaultJsonRecord(String id, String kind, String legalTag) {
+        JsonObject record = getDefaultRecordWithDefaultData(id, kind, legalTag);
+        JsonArray records = new JsonArray();
+        records.add(record);
+        return records.toString();
+    }
+
+	public static String createRecordWithDuplicateAclAndLegaltags(String id, String kind, String legalTag) {
+		JsonObject record = getDefaultRecordWithDefaultDataAndDuplicateAclAndLegaltags(id, kind, legalTag);
+		JsonArray records = new JsonArray();
+		records.add(record);
+		return records.toString();
+	}
+
+	public static String createDefaultJsonRecordWithParentId(String id, String kind, String legalTag, String parentId) {
+		JsonObject record = getDefaultRecordWithDefaultData(id, kind, legalTag);
+		JsonObject ancestryObject = new JsonObject();
+		JsonArray parents = new JsonArray();
+		parents.add(parentId);
+		ancestryObject.add("parents", parents);
+		record.add("ancestry", ancestryObject);
+		JsonArray records = new JsonArray();
+		records.add(record);
+		return records.toString();
+	}
+
+    public static String createDefaultJsonRecords(int recordsCount, String id, String kind, String legalTag) {
+        JsonArray records = new JsonArray();
+        for (int i = 0; i < recordsCount; i++) {
+            JsonObject record = getDefaultRecordWithDefaultData(id +i, kind, legalTag);
+            records.add(record);
+        }
+        return records.toString();
+    }
+
+	public static String createJsonRecordWithData(String id, String kind, String legalTag, String data) {
+
+		JsonObject dataJson = new JsonObject();
+		dataJson.addProperty("custom", data);
+		dataJson.addProperty("score-int", 58377304471659395L);
+		dataJson.addProperty("score-double", 58377304.471659395);
+
+		JsonObject record = getRecordWithInputData(id, kind, legalTag, dataJson);
+
+		JsonArray records = new JsonArray();
+		records.add(record);
+
+		return records.toString();
+	}
+
+	public static String createJsonRecordWithEntV2OnlyAcl(String id, String kind, String legalTag, String data) {
+		JsonObject dataJson = new JsonObject();
+		dataJson.addProperty("custom", data);
+		dataJson.addProperty("score-int", 58377304471659395L);
+		dataJson.addProperty("score-double", 58377304.471659395);
+
+		JsonObject record = getRecordWithInputDataAndAcl(id, kind, legalTag, dataJson);
+
+		JsonArray records = new JsonArray();
+		records.add(record);
+
+		return records.toString();
+
+	}
+
+	public static String createJsonRecordWithReference(int recordsCount, String id, String kind, String legalTag, String fromCrs, String conversionType) {
+
+		JsonArray records = new JsonArray();
+
+		for (int i = 0; i < recordsCount; i++) {
+
+			JsonObject data = new JsonObject();
+			data.addProperty("X", 16.00);
+			data.addProperty("Y", 10.00);
+			data.addProperty("Z", 0.0);
+
+			JsonArray propertyNames = new JsonArray();
+			propertyNames.add("X");
+			propertyNames.add("Y");
+			propertyNames.add("Z");
+
+			JsonObject meta = new JsonObject();
+			meta.addProperty(Constants.KIND, conversionType);
+			meta.addProperty(Constants.PERSISTABLE_REFERENCE, fromCrs);
+			meta.add(Constants.PROPERTY_NAMES, propertyNames);
+
+			JsonArray metaBlocks = new JsonArray();
+			metaBlocks.add(meta);
+
+			JsonObject record = getRecordWithInputData(id + i, kind, legalTag, data);
+			record.add(Constants.META, metaBlocks);
+
+			records.add(record);
+		}
+
+		return records.toString();
+	}
+
+	public static String createJsonRecordMissingValue(int recordsCount, String id, String kind, String legalTag, String fromCrs, String conversionType) {
+
+		JsonArray records = new JsonArray();
+
+		for (int i = 0; i < recordsCount; i++) {
+
+			JsonObject data = new JsonObject();
+			data.addProperty("X", 16.00);
+			data.addProperty("Z", 0.0);
+
+			JsonArray propertyNames = new JsonArray();
+			propertyNames.add("X");
+			propertyNames.add("Y");
+			propertyNames.add("Z");
+
+			JsonObject meta = new JsonObject();
+			meta.addProperty(Constants.KIND, conversionType);
+			meta.addProperty(Constants.PERSISTABLE_REFERENCE, fromCrs);
+			meta.add(Constants.PROPERTY_NAMES, propertyNames);
+
+			JsonArray metaBlocks = new JsonArray();
+			metaBlocks.add(meta);
+
+			JsonObject record = getRecordWithInputData(id + i, kind, legalTag, data);
+			record.add(Constants.META, metaBlocks);
+
+			records.add(record);
+		}
+
+		return records.toString();
+	}
+
+	public static String createJsonRecordNoMetaBlock(int recordsCount, String id, String kind, String legalTag) {
+
+		JsonArray records = new JsonArray();
+
+		for (int i = 0; i < recordsCount; i++) {
+			JsonObject data = new JsonObject();
+			data.addProperty("X", 16.00);
+			data.addProperty("Y", 16.00);
+			data.addProperty("Z", 0.0);
+
+			JsonObject record = getRecordWithInputData(id + i, kind, legalTag, data);
+			records.add(record);
+		}
+
+		return records.toString();
+	}
+
+	public static String createJsonRecordsWithDateFormat(int recordsCount, String id, String kind, String legalTag, String format, String propertyName, String date, String persistableReference) {
+
+		JsonArray records = new JsonArray();
+
+		for (int i = 0; i < recordsCount; i++) {
+			JsonObject data = new JsonObject();
+			data.addProperty(propertyName, date);
+
+			JsonArray propertyNames = new JsonArray();
+			propertyNames.add(propertyName);
+
+			JsonObject meta = new JsonObject();
+			meta.addProperty(Constants.PERSISTABLE_REFERENCE, persistableReference);
+			meta.addProperty(Constants.KIND, "DateTime");
+			meta.add(Constants.PROPERTY_NAMES, propertyNames);
+
+			JsonArray metas = new JsonArray();
+			metas.add(meta);
+
+			JsonObject record = getRecordWithInputData(id + i, kind, legalTag, data);
+			record.add(Constants.META, metas);
+			records.add(record);
+		}
+
+		return records.toString();
+	}
+
+	public static String createJsonRecordWithNestedProperty(int recordsNumber, String id, String kind, String legalTag, String fromCrs, String conversionType) {
+
+		JsonArray records = new JsonArray();
+
+		for (int i = 0; i < 8 + recordsNumber; i++) {
+
+			JsonArray pointValues1 = new JsonArray();
+			pointValues1.add(16.00);
+			pointValues1.add(10.00);
+			JsonArray pointValues2 = new JsonArray();
+			pointValues2.add(16.00);
+			pointValues2.add(10.00);
+			JsonArray points = new JsonArray();
+			points.add(pointValues1);
+			points.add(pointValues2);
+
+			JsonObject nestedProperty = new JsonObject();
+			nestedProperty.addProperty("crsKey", "Native");
+			nestedProperty.add(Constants.POINTS, points);
+
+			JsonObject data = new JsonObject();
+			data.addProperty("message", "integration-test-record");
+			data.add("projectOutlineLocalGeographic", nestedProperty);
+
+			JsonArray propertyNames = new JsonArray();
+			propertyNames.add("projectOutlineLocalGeographic");
+
+			JsonObject meta = new JsonObject();
+			meta.addProperty(Constants.KIND, conversionType);
+			meta.addProperty(Constants.PERSISTABLE_REFERENCE, fromCrs);
+			meta.add(Constants.PROPERTY_NAMES, propertyNames);
+
+			JsonArray metaBlocks = new JsonArray();
+			metaBlocks.add(meta);
+
+			JsonObject record = getRecordWithInputData(id + i, kind, legalTag, data);
+			record.add(Constants.META, metaBlocks);
+
+			records.add(record);
+		}
+
+		return records.toString();
+	}
+
+	public static String createJsonRecordWithNestedArrayOfProperties(int recordsNumber, String id, String kind, String legalTag, String fromRef, String conversionType, String unitOfMeasureID) {
+		JsonArray records = new JsonArray();
+
+		for (int i = 12; i < 12 + recordsNumber; i++) {
+
+			JsonArray nestedArray = new JsonArray();
+			JsonObject item1 = new JsonObject();
+			item1.addProperty("measuredDepth", 10.0);
+			item1.addProperty("otherField", "testValue1");
+			JsonObject item2 = new JsonObject();
+			item2.addProperty("measuredDepth", 20.0);
+			item2.addProperty("otherField", "testValue2");
+			nestedArray.add(item1);
+			nestedArray.add(item2);
+
+			JsonObject record = createJsonObjectRecordWithNestedArray(nestedArray, id + i, kind, legalTag, conversionType, fromRef, "markers[].measuredDepth", unitOfMeasureID);
+
+			records.add(record);
+		}
+
+		return records.toString();
+	}
+
+	public static String createJsonRecordWithNestedArrayOfPropertiesAndInvalidValues(int recordsNumber, String id, String kind, String legalTag, String fromRef, String conversionType, String unitOfMeasureID) {
+		JsonArray records = new JsonArray();
+
+		for (int i = 12; i < 12 + recordsNumber; i++) {
+
+			JsonArray nestedArray = new JsonArray();
+			JsonObject item1 = new JsonObject();
+			item1.addProperty("measuredDepth", 10.0);
+			item1.addProperty("otherField", "testValue1");
+			JsonObject item2 = new JsonObject();
+			item2.addProperty("measuredDepth", "invalidValue");
+			item2.addProperty("otherField", "testValue2");
+			nestedArray.add(item1);
+			nestedArray.add(item2);
+
+			JsonObject record = createJsonObjectRecordWithNestedArray(nestedArray, id + i, kind, legalTag, conversionType, fromRef, "markers[].measuredDepth", unitOfMeasureID);
+
+			records.add(record);
+		}
+
+		return records.toString();
+	}
+
+	public static String createJsonRecordWithInhomogeneousNestedArrayOfProperties(int recordsNumber, String id, String kind, String legalTag, String fromRef, String conversionType, String unitOfMeasureID) {
+		JsonArray records = new JsonArray();
+
+		for (int i = 13; i < 13 + recordsNumber; i++) {
+
+			JsonArray nestedArray = new JsonArray();
+			JsonObject item1 = new JsonObject();
+			item1.addProperty("measuredDepth", 10.0);
+			item1.addProperty("otherField", "testValue1");
+			JsonObject item2 = new JsonObject();
+			item2.addProperty("measuredDepth", 20.0);
+			item2.addProperty("otherField", "testValue2");
+			nestedArray.add(item1);
+			nestedArray.add(item2);
+
+			JsonObject record = createJsonObjectRecordWithNestedArray(nestedArray, id + i, kind, legalTag, conversionType, fromRef, "markers[1].measuredDepth", unitOfMeasureID);
+
+			records.add(record);
+		}
+
+		return records.toString();
+	}
+
+	public static String createJsonRecordWithInhomogeneousNestedArrayOfPropertiesAndInvalidValues(int recordsNumber, String id, String kind, String legalTag, String fromRef, String conversionType, String unitOfMeasureID) {
+		JsonArray records = new JsonArray();
+
+		for (int i = 13; i < 13 + recordsNumber; i++) {
+
+			JsonArray nestedArray = new JsonArray();
+			JsonObject item1 = new JsonObject();
+			item1.addProperty("measuredDepth", 10.0);
+			item1.addProperty("otherField", "testValue1");
+			JsonObject item2 = new JsonObject();
+			item2.addProperty("measuredDepth", "invalidValue");
+			item2.addProperty("otherField", "testValue2");
+			nestedArray.add(item1);
+			nestedArray.add(item2);
+
+			JsonObject record = createJsonObjectRecordWithNestedArray(nestedArray, id + i, kind, legalTag, conversionType, fromRef, "markers[1].measuredDepth", unitOfMeasureID);
+
+			records.add(record);
+		}
+
+		return records.toString();
+	}
+
+	public static String createJsonRecordWithInhomogeneousNestedArrayOfPropertiesAndIndexOutOfBoundary(int recordsNumber, String id, String kind, String legalTag, String fromRef, String conversionType, String unitOfMeasureID) {
+		JsonArray records = new JsonArray();
+
+		for (int i = 13; i < 13 + recordsNumber; i++) {
+
+			JsonArray nestedArray = new JsonArray();
+			JsonObject item1 = new JsonObject();
+			item1.addProperty("measuredDepth", 10.0);
+			item1.addProperty("otherField", "testValue1");
+			JsonObject item2 = new JsonObject();
+			item2.addProperty("measuredDepth", "20.0");
+			item2.addProperty("otherField", "testValue2");
+			nestedArray.add(item1);
+			nestedArray.add(item2);
+
+			JsonObject record = createJsonObjectRecordWithNestedArray(nestedArray, id + i, kind, legalTag, conversionType, fromRef, "markers[2].measuredDepth", unitOfMeasureID);
+
+			records.add(record);
+		}
+
+		return records.toString();
+	}
+
+	private static JsonObject createJsonObjectRecordWithNestedArray(JsonArray nestedArray, String id, String kind, String legalTag, String conversionType, String fromRef, String propertyName, String unitOfMeasureID) {
+		JsonObject data = new JsonObject();
+		data.addProperty("message", "integration-test-record");
+		data.add("markers", nestedArray);
+
+		JsonArray propertyNames = new JsonArray();
+		propertyNames.add(propertyName);
+
+		JsonObject meta = new JsonObject();
+		meta.addProperty(Constants.KIND, conversionType);
+		meta.addProperty(Constants.PERSISTABLE_REFERENCE, fromRef);
+		meta.addProperty(UNIT_OF_MEASURE_ID, unitOfMeasureID);
+		meta.add(Constants.PROPERTY_NAMES, propertyNames);
+
+		JsonArray metaBlocks = new JsonArray();
+		metaBlocks.add(meta);
+
+		JsonObject record = getRecordWithInputData(id, kind, legalTag, data);
+		record.add(Constants.META, metaBlocks);
+
+		return record;
+	}
+
+	private static JsonObject createJsonObjectRecordWithNestedArray(JsonArray nestedArray, String id, String kind, String legalTag, String conversionType, String fromRef, String unitOfMeasureID) {
+		JsonObject data = new JsonObject();
+		data.addProperty("message", "integration-test-record");
+		data.add("markers", nestedArray);
+
+		JsonArray propertyNames = new JsonArray();
+		propertyNames.add("markers[].measuredDepth");
+
+		JsonObject meta = new JsonObject();
+		meta.addProperty(Constants.KIND, conversionType);
+		meta.addProperty(Constants.PERSISTABLE_REFERENCE, fromRef);
+		meta.addProperty(UNIT_OF_MEASURE_ID, unitOfMeasureID);
+		meta.add(Constants.PROPERTY_NAMES, propertyNames);
+
+		JsonArray metaBlocks = new JsonArray();
+		metaBlocks.add(meta);
+
+		JsonObject record = getRecordWithInputData(id, kind, legalTag, data);
+		record.add(Constants.META, metaBlocks);
+
+		return record;
+	}
+
+	public static String createJsonRecordWithMultiplePairOfCoordinates(int recordsNumber, String id, String kind, String legalTag, String fromCrs, String conversionType) {
+		JsonArray records = new JsonArray();
+		for (int i = 0; i <  recordsNumber; i++) {
+
+			JsonObject data = new JsonObject();
+			data.addProperty("X", 16.00);
+			data.addProperty("Y", 10.00);
+			data.addProperty("LON", 16.00);
+			data.addProperty("LAT", 10.00);
+
+			JsonArray propertyNames = new JsonArray();
+			propertyNames.add("X");
+			propertyNames.add("Y");
+			propertyNames.add("LON");
+			propertyNames.add("LAT");
+
+			JsonObject meta = new JsonObject();
+			meta.addProperty(Constants.KIND, conversionType);
+			meta.addProperty(Constants.PERSISTABLE_REFERENCE, fromCrs);
+			meta.add(Constants.PROPERTY_NAMES, propertyNames);
+
+			JsonArray metaBlocks = new JsonArray();
+			metaBlocks.add(meta);
+
+			JsonObject record = getDefaultRecord(id + i, kind, legalTag);
+			record.add(Constants.DATA, data);
+			record.add(Constants.META, metaBlocks);
+
+			records.add(record);
+		}
+		return records.toString();
+	}
+
+	public static String createJsonRecordWithAsIngestedCoordinates(int recordsNumber, String id, String kind, String legalTag, String prCRS, String prUNITZ, String geometryType, String attributeType) {
+		JsonArray records = new JsonArray();
+		Gson gson = new Gson();
+		for (int i = 0; i <  recordsNumber; i++) {
+			JsonObject data = new JsonObject();
+
+			JsonObject properties = new JsonObject();
+			JsonObject geometry = new JsonObject();
+
+			switch (geometryType) {
+				case Constants.ANY_CRS_POINT:
+					geometry.add(Constants.COORDINATES, gson.toJsonTree(createCoordinates1(1,2)));
+					break;
+				case Constants.ANY_CRS_MULTIPOINT:
+				case Constants.ANY_CRS_LINE_STRING:
+					geometry.add(Constants.COORDINATES, gson.toJsonTree(createCoordinates2(1,2)));
+					break;
+				case Constants.ANY_CRS_MULTILINE_STRING:
+				case Constants.ANY_CRS_POLYGON:
+					geometry.add(Constants.COORDINATES, gson.toJsonTree(createCoordinates3(1,2)));
+					break;
+				case Constants.ANY_CRS_MULTIPOLYGON:
+					geometry.add(Constants.COORDINATES, gson.toJsonTree(createCoordinates4(1,2)));
+					break;
+				case Constants.ANY_CRS_GEOMETRY_COLLECTION:
+					JsonArray geometries = new JsonArray();
+					JsonObject geometriesObj = new JsonObject();
+					geometriesObj.addProperty(Constants.BBOX, (Boolean) null);
+					geometriesObj.addProperty(Constants.TYPE, Constants.POINT);
+					geometriesObj.add(Constants.COORDINATES, gson.toJsonTree(createCoordinates1(1,2)));
+					geometries.add(geometriesObj);
+					geometry.add(Constants.GEOMETRIES, geometries);
+					break;
+			}
+			geometry.addProperty(Constants.TYPE, geometryType);
+			geometry.addProperty(Constants.BBOX, (Boolean) null);
+
+			JsonArray features = new JsonArray();
+			JsonObject feature = new JsonObject();
+			feature.addProperty(Constants.BBOX, (Boolean) null);
+			feature.addProperty(Constants.TYPE, Constants.ANY_CRS_FEATURE);
+			feature.add(Constants.PROPERTIES, properties);
+			feature.add(Constants.GEOMETRY, geometry);
+			features.add(feature);
+
+			JsonObject asIngestedCoordinates = new JsonObject();
+			asIngestedCoordinates.addProperty(Constants.PERSISTABLE_REFERENCE_CRS, prCRS);
+			asIngestedCoordinates.addProperty(Constants.PERSISTABLE_REFERENCE_UNIT_Z, prUNITZ);
+			asIngestedCoordinates.addProperty(Constants.TYPE, Constants.ANY_CRS_FEATURE_COLLECTION);
+			asIngestedCoordinates.add(Constants.PROPERTIES, properties);
+			asIngestedCoordinates.add(Constants.FEATURES, features);
+
+			JsonObject validAttribute = new JsonObject();
+			validAttribute.add(Constants.AS_INGESTED_COORDINATES, asIngestedCoordinates);
+
+			data.add(attributeType, validAttribute);
+
+			JsonObject record = getDefaultRecord(id + i, kind, legalTag);
+			record.add(Constants.DATA, data);
+			records.add(record);
+		}
+		return records.toString();
+	}
+
+	public static String createJsonRecordWithInvalidAsIngestedCoordinates(int recordsNumber, String id, String kind, String legalTag, String prCRS, String prUNITZ, String geometryType, String attributeType) {
+		JsonArray records = new JsonArray();
+		Gson gson = new Gson();
+		for (int i = 0; i <  recordsNumber; i++) {
+			JsonObject data = new JsonObject();
+			JsonObject geometry = new JsonObject();
+			geometry.add(Constants.COORDINATES, gson.toJsonTree(createCoordinates1(1,2)));
+			geometry.addProperty(Constants.TYPE, geometryType);
+			geometry.addProperty(Constants.BBOX, (Boolean) null);
+
+			JsonArray features = new JsonArray();
+			JsonObject feature = new JsonObject();
+			feature.addProperty(Constants.BBOX, (Boolean) null);
+			feature.addProperty(Constants.TYPE, Constants.ANY_CRS_FEATURE);
+			feature.add(Constants.GEOMETRY, geometry);
+			features.add(feature);
+
+			JsonObject asIngestedCoordinates = new JsonObject();
+			asIngestedCoordinates.addProperty(Constants.PERSISTABLE_REFERENCE_CRS, prCRS);
+			asIngestedCoordinates.addProperty(Constants.PERSISTABLE_REFERENCE_UNIT_Z, prUNITZ);
+			asIngestedCoordinates.addProperty(Constants.TYPE, Constants.ANY_CRS_FEATURE_COLLECTION);
+			asIngestedCoordinates.add("features1", features);
+
+			JsonObject validAttribute = new JsonObject();
+			validAttribute.add(Constants.AS_INGESTED_COORDINATES, asIngestedCoordinates);
+
+			data.add(attributeType, validAttribute);
+
+			JsonObject record = getDefaultRecord(id + i, kind, legalTag);
+			record.add(Constants.DATA, data);
+			records.add(record);
+		}
+		return records.toString();
+	}
+
+	public static JsonObject buildUpdateTagBody(String id, String op, String val) {
+		JsonArray records = new JsonArray();
+		records.add(id);
+
+		JsonArray value = new JsonArray();
+		value.add(val);
+		JsonObject operation = new JsonObject();
+		operation.addProperty("op", op);
+		operation.addProperty("path", "/tags");
+		operation.add("value", value);
+		JsonArray ops = new JsonArray();
+		ops.add(operation);
+
+		JsonObject query = new JsonObject();
+		query.add("ids", records);
+
+		JsonObject updateBody = new JsonObject();
+		updateBody.add("query", query);
+		updateBody.add("ops", ops);
+
+		return updateBody;
+	}
+
+	public static String createJsonRecordWithWGS84Coordinates(int recordsNumber, String id, String kind, String legalTag, String prCRS, String prUNITZ, String geometryType, String attributeType) {
+		JsonArray records = new JsonArray();
+		Gson gson = new Gson();
+		for (int i = 0; i <  recordsNumber; i++) {
+			JsonObject data = new JsonObject();
+
+			JsonArray coordinates = new JsonArray();
+			coordinates.add(313405.9477893702);
+			coordinates.add(6544797.620047403);
+			coordinates.add(6.561679790026246);
+
+			JsonObject geometry = new JsonObject();
+			geometry.add(Constants.COORDINATES, gson.toJsonTree(createCoordinates1(1,2)));
+			geometry.addProperty(Constants.TYPE, geometryType);
+			geometry.addProperty(Constants.BBOX, (Boolean) null);
+
+			JsonArray features = new JsonArray();
+			JsonObject feature = new JsonObject();
+			feature.addProperty(Constants.BBOX, (Boolean) null);
+			feature.addProperty(Constants.TYPE, Constants.ANY_CRS_FEATURE);
+			feature.add(Constants.GEOMETRY, geometry);
+			features.add(feature);
+
+			JsonObject asIngestedCoordinates = new JsonObject();
+			asIngestedCoordinates.addProperty(Constants.PERSISTABLE_REFERENCE_CRS, prCRS);
+			asIngestedCoordinates.addProperty(Constants.PERSISTABLE_REFERENCE_UNIT_Z, prUNITZ);
+			asIngestedCoordinates.addProperty(Constants.TYPE, Constants.ANY_CRS_FEATURE_COLLECTION);
+			asIngestedCoordinates.add(Constants.FEATURES, features);
+
+			JsonArray wgsCoordinates = new JsonArray();
+			wgsCoordinates.add(5.7500000010406245);
+			wgsCoordinates.add(59.000000000399105);
+			wgsCoordinates.add(1.9999999999999998);
+
+			JsonObject wgsGeometry = new JsonObject();
+			wgsGeometry.add(Constants.COORDINATES, wgsCoordinates);
+			wgsGeometry.addProperty(Constants.TYPE, Constants.POINT);
+			wgsGeometry.addProperty(Constants.BBOX, (Boolean) null);
+
+			JsonArray wgsFeatures = new JsonArray();
+			JsonObject wgsFeature = new JsonObject();
+			wgsFeature.addProperty(Constants.BBOX, (Boolean) null);
+			wgsFeature.add(Constants.GEOMETRY, wgsGeometry);
+			wgsFeatures.add(wgsFeature);
+
+			JsonObject wgs84Coordinates = new JsonObject();
+			wgs84Coordinates.addProperty(Constants.PERSISTABLE_REFERENCE_CRS, (Boolean) null);
+			wgs84Coordinates.addProperty(Constants.PERSISTABLE_REFERENCE_UNIT_Z, prUNITZ);
+			wgs84Coordinates.addProperty(Constants.TYPE, Constants.FEATURE_COLLECTION);
+			wgs84Coordinates.add(Constants.FEATURES, wgsFeatures);
+
+			JsonObject validAttribute = new JsonObject();
+			validAttribute.add(Constants.AS_INGESTED_COORDINATES, asIngestedCoordinates);
+			validAttribute.add(Constants.WGS84_COORDINATES, wgs84Coordinates);
+
+			data.add(attributeType, validAttribute);
+
+			JsonObject record = getDefaultRecord(id + i, kind, legalTag);
+			record.add(Constants.DATA, data);
+			records.add(record);
+		}
+		return records.toString();
+	}
+
+	public static String createJsonRecordWithCustomAcl(String newRecordId, String kind,
+			String legalTag, String dataGroupEmail) {
+		JsonObject record = getDefaultRecordWithCustomAcl(newRecordId, kind, legalTag, dataGroupEmail);
+		JsonObject dataJson = new JsonObject();
+		dataJson.addProperty("score-int", 58377304471659395L);
+		dataJson.addProperty("score-double", 58377304.471659395);
+		record.add("data", dataJson);
+		JsonArray records = new JsonArray();
+		records.add(record);
+		return records.toString();
+	}
+
+	private static double[][][][] createCoordinates4(int mode, int dimension) {
+		double[][][][] pts_s = new double[2][2][5][dimension];
+		for (int l = 0; l < 2; l++) {
+			for (int k = 0; k < pts_s[0].length; k++) {
+				double[][][] pts = createCoordinates3(mode, dimension);
+				for (int j = 0; j < pts[0].length; j++) {
+					for (int i = 0; i < dimension; i++) {
+						pts_s[l][k][j][i] = pts[k][j][i] + (k + l) * 4;
+					}
+				}
+			}
+		}
+		return pts_s;
+	}
+
+	private static double[] createCoordinates1(int mode, int dimension) {
+		double[] pt_ac = new double[]{500000, 6500000, 1000};
+		double[] pt_gj = new double[]{3, 60, 2000};
+		double[] pts = new double[dimension];
+		if (mode == 0) System.arraycopy(pt_gj, 0, pts, 0, dimension);
+		else System.arraycopy(pt_ac, 0, pts, 0, dimension);
+		return pts;
+	}
+
+	private static double[][] createCoordinates2(int mode, int dimension) {
+		double[][] s = new double[][]{{-1, 1, 10}, {1, 1, 10}, {1, -1, 20}, {-1, -1, 20}, {-1, 1, 10}};
+		double[][] pts = new double[5][dimension];
+		double[] pt = createCoordinates1(mode, dimension);
+		for (int j = 0; j < 5; j++) {
+			for (int i = 0; i < dimension; i++) {
+				pts[j][i] = pt[i] + s[j][i];
+			}
+		}
+		return pts;
+	}
+
+	private static double[][][] createCoordinates3(int mode, int dimension) {
+		double[][][] pts_s = new double[2][5][dimension];
+		for (int k = 0; k < pts_s.length; k++) {
+			double[][] pts = createCoordinates2(mode, dimension);
+			for (int j = 0; j < pts.length; j++) {
+				for (int i = 0; i < dimension; i++) {
+					pts_s[k][j][i] = pts[j][i] + k * 4;
+				}
+			}
+		}
+		return pts_s;
+	}
+
+	private static JsonObject getDefaultRecord(String id, String kind, String legalTag) {
+		JsonArray acls = new JsonArray();
+		acls.add(TestUtils.getAcl());
+		return getDefaultRecordFromAcl(id, kind, legalTag, acls);
+	}
+
+	private static JsonObject getDefaultRecordWithDuplicateAclsAndLegaltags(String id, String kind, String legalTag) {
+		JsonArray acls = new JsonArray();
+		acls.add(TestUtils.getAcl());
+		acls.add(TestUtils.getAcl());
+		return getDefaultRecordWithDuplicateAclAndLegaltags(id, kind, legalTag, acls);
+	}
+
+	private static JsonObject getDefaultRecordWithCustomAcl(String id, String kind, String legalTag, String acl) {
+		JsonArray acls = new JsonArray();
+		acls.add(acl);
+		return getDefaultRecordFromAcl(id, kind, legalTag, acls);
+	}
+
+	private static JsonObject getDefaultRecordWithEntV2OnlyAcl(String id, String kind, String legalTag) {
+		JsonArray acls = new JsonArray();
+		acls.add(TestUtils.getEntV2OnlyAcl());
+		return getDefaultRecordFromAcl(id, kind, legalTag, acls);
+	}
+
+	private static JsonObject getDefaultRecordFromAcl(String id, String kind, String legalTag, JsonArray acls) {
+		JsonObject acl = new JsonObject();
+		acl.add("viewers", acls);
+		acl.add("owners", acls);
+
+		JsonArray tags = new JsonArray();
+		tags.add(legalTag);
+
+		JsonArray ordcJson = new JsonArray();
+		ordcJson.add("BR");
+
+		JsonObject legal = new JsonObject();
+		legal.add("legaltags", tags);
+		legal.add("otherRelevantDataCountries", ordcJson);
+
+		JsonObject record = new JsonObject();
+		record.addProperty("id", id);
+		record.addProperty("kind", kind);
+		record.add("acl", acl);
+		record.add("legal", legal);
+		return record;
+	}
+
+	private static JsonObject getDefaultRecordWithDuplicateAclAndLegaltags(String id, String kind, String legalTag, JsonArray acls) {
+		JsonObject acl = new JsonObject();
+		acl.add("viewers", acls);
+		acl.add("owners", acls);
+
+		JsonArray tags = new JsonArray();
+		tags.add(legalTag);
+		tags.add(legalTag);
+
+		JsonArray ordcJson = new JsonArray();
+		ordcJson.add("BR");
+
+		JsonObject legal = new JsonObject();
+		legal.add("legaltags", tags);
+		legal.add("otherRelevantDataCountries", ordcJson);
+
+		JsonObject record = new JsonObject();
+		record.addProperty("id", id);
+		record.addProperty("kind", kind);
+		record.add("acl", acl);
+		record.add("legal", legal);
+		return record;
+	}
+
+	private static JsonObject getDefaultRecordWithDefaultData(String id, String kind, String legalTag) {
+		JsonObject data = new JsonObject();
+		data.add("int-tag", getNumberPropertyObject("score-int", 58377304471659395L));
+		data.add("double-tag", getNumberPropertyObject("score-double", 58377304.471659395));
+		data.addProperty("count", 123456789L);
+        return getRecordWithInputData(id, kind, legalTag, data);
+	}
+
+	private static JsonObject getDefaultRecordWithDefaultDataAndDuplicateAclAndLegaltags(String id, String kind, String legalTag) {
+		JsonObject data = new JsonObject();
+		data.add("int-tag", getNumberPropertyObject("score-int", 58377304471659395L));
+		data.add("double-tag", getNumberPropertyObject("score-double", 58377304.471659395));
+		data.addProperty("count", 123456789L);
+        return getRecordWithInputDataAndDuplicateAclsAndLegaltags(id, kind, legalTag, data);
+	}
+
+	private static JsonObject getRecordWithInputData(String id, String kind, String legalTag, JsonObject data) {
+		JsonObject record = getDefaultRecord(id, kind, legalTag);
+		record.add("data", data);
+		return record;
+	}
+
+	private static JsonObject getRecordWithInputDataAndDuplicateAclsAndLegaltags(String id, String kind, String legalTag, JsonObject data) {
+		JsonObject record = getDefaultRecordWithDuplicateAclsAndLegaltags(id, kind, legalTag);
+		record.add("data", data);
+		return record;
+	}
+
+	private static JsonObject getRecordWithInputDataAndAcl(String id, String kind, String legalTag, JsonObject data) {
+		JsonObject record = getDefaultRecordWithEntV2OnlyAcl(id, kind, legalTag);
+		record.add("data", data);
+		return record;
+	}
+
+	private static JsonObject getNumberPropertyObject(String propertyName, Number intValue) {
+		JsonObject numberProperty = new JsonObject();
+		numberProperty.addProperty(propertyName, intValue);
+		return numberProperty;
+	}
+}
diff --git a/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/util/ReplayUtils.java b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/util/ReplayUtils.java
new file mode 100644
index 0000000000000000000000000000000000000000..62eb3cda5e28cb5ff76d9242ec66ba25730bf21f
--- /dev/null
+++ b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/util/ReplayUtils.java
@@ -0,0 +1,119 @@
+// Copyright © Microsoft Corporation
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package org.opengroup.osdu.storage.util;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import java.io.IOException;
+import java.util.List;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
+import org.apache.hc.core5.http.ParseException;
+import org.apache.hc.core5.http.ProtocolException;
+import org.apache.hc.core5.http.io.entity.EntityUtils;
+import org.opengroup.osdu.storage.model.ReplayStatusResponseHelper;
+public class ReplayUtils {
+
+    public static String createJsonEmpty() {
+
+        JsonObject requestBody = new JsonObject();
+        return requestBody.toString();
+    }
+
+    public static String createJsonWithoutOperationName(List<String> kindList) {
+
+        JsonObject requestBody = new JsonObject();
+        JsonObject filter = new JsonObject();
+        requestBody.add("filter", filter);
+        JsonArray kindArray = new JsonArray();
+        filter.add("kinds", kindArray);
+        for (String kind : kindList)
+            kindArray.add(kind);
+        return requestBody.toString();
+    }
+
+
+    public static String createJsonWithOperationName(String operation) {
+
+        JsonObject requestBody = new JsonObject();
+        requestBody.addProperty("operation", operation);
+        return requestBody.toString();
+    }
+
+    public static String createJsonWithEmptyFilter(String operation) {
+
+        JsonObject requestBody = new JsonObject();
+        requestBody.addProperty("operation", operation);
+        JsonObject filter = new JsonObject();
+        requestBody.add("filter", filter);
+        return requestBody.toString();
+    }
+
+    public static String createJsonWithKind(String operation, List<String> kindList) {
+
+        JsonObject requestBody = new JsonObject();
+        JsonObject filter = new JsonObject();
+        JsonArray kindArray = new JsonArray();
+        requestBody.addProperty("operation", operation);
+        requestBody.add("filter", filter);
+        filter.add("kinds", kindArray);
+        for (String kind : kindList)
+            kindArray.add(kind);
+        return requestBody.toString();
+    }
+
+    public static String getFieldFromResponse(CloseableHttpResponse response, String field) throws IOException, ParseException {
+
+        return JsonParser.parseString(EntityUtils.toString(response.getEntity()))
+                         .getAsJsonObject()
+                         .get(field)
+                         .getAsString();
+    }
+
+    public static String getSearchCountQueryForKind(String kind) {
+
+        JsonObject requestBody = new JsonObject();
+        requestBody.addProperty("kind", kind);
+        requestBody.addProperty("limit", 1);
+        requestBody.addProperty("trackTotalCount", true);
+        return requestBody.toString();
+    }
+
+    public static String getSearchUrl() {
+
+        String searchUrl = System.getProperty("SEARCH_URL", System.getenv("SEARCH_URL"));
+        if (searchUrl == null || searchUrl.contains("-null")) {
+            throw new IllegalArgumentException("Invalid SEARCH_URL: " + searchUrl);
+        }
+        return searchUrl;
+    }
+
+    public static String getIndexerUrl() {
+
+        String indexerUrl = System.getProperty("INDEXER_URL", System.getenv("INDEXER_URL"));
+        if (indexerUrl == null || indexerUrl.contains("-null")) {
+            throw new IllegalArgumentException("Invalid INDEXER_URL: " + indexerUrl);
+        }
+        return indexerUrl;
+    }
+
+    public static ReplayStatusResponseHelper getConvertedReplayStatusResponseFromResponse(CloseableHttpResponse response) throws ProtocolException, IOException {
+
+        String json = EntityUtils.toString(response.getEntity());
+        Gson gson = new Gson();
+        return gson.fromJson(json, ReplayStatusResponseHelper.class);
+    }
+}
diff --git a/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/util/TenantUtils.java b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/util/TenantUtils.java
new file mode 100644
index 0000000000000000000000000000000000000000..d9427a3cef17554c98add1a705abfe85eec1efa3
--- /dev/null
+++ b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/util/TenantUtils.java
@@ -0,0 +1,26 @@
+// Copyright 2017-2019, Schlumberger
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package org.opengroup.osdu.storage.util;
+
+public class TenantUtils {
+
+	public static String getTenantName() {
+		return System.getProperty("TENANT_NAME", System.getenv("TENANT_NAME"));
+	}
+
+	public static String getFirstTenantName() {
+		return getTenantName();
+	}
+}
diff --git a/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/util/TestBase.java b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/util/TestBase.java
new file mode 100644
index 0000000000000000000000000000000000000000..b6e871962196768399d0f13ae44009c155463309
--- /dev/null
+++ b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/util/TestBase.java
@@ -0,0 +1,30 @@
+// Copyright 2017-2019, Schlumberger
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package org.opengroup.osdu.storage.util;
+
+import com.google.gson.Gson;
+
+public abstract class TestBase {
+
+	protected TestUtils	testUtils = null;
+	protected ConfigUtils configUtils = null;
+
+	public static final Gson GSON = new Gson();
+
+    public abstract void setup() throws Exception;
+
+    public abstract void tearDown() throws Exception;
+
+}
diff --git a/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/util/TestUtils.java b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/util/TestUtils.java
new file mode 100644
index 0000000000000000000000000000000000000000..75cb9af4ecd93885b289602a02a69ebc65d8eae5
--- /dev/null
+++ b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/util/TestUtils.java
@@ -0,0 +1,239 @@
+// Copyright 2017-2019, Schlumberger
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package org.opengroup.osdu.storage.util;
+
+import static org.apache.http.HttpStatus.SC_CREATED;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.opengroup.osdu.storage.util.HeaderUtils.getHeadersWithxCollaboration;
+import static org.opengroup.osdu.storage.util.TestBase.GSON;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonObject;
+import jakarta.ws.rs.core.MediaType;
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+import org.apache.commons.lang3.NotImplementedException;
+import org.apache.hc.client5.http.config.ConnectionConfig;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
+import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
+import org.apache.hc.client5.http.impl.io.BasicHttpClientConnectionManager;
+import org.apache.hc.core5.http.ClassicHttpRequest;
+import org.apache.hc.core5.http.ContentType;
+import org.apache.hc.core5.http.HttpStatus;
+import org.apache.hc.core5.http.ParseException;
+import org.apache.hc.core5.http.io.entity.EntityUtils;
+import org.apache.hc.core5.http.io.support.ClassicRequestBuilder;
+
+public abstract class TestUtils {
+
+    public static final String STORAGE_TEST_GROUP_ENT_V_2 = "data.storage-integration-test-acl.ent-v2";
+    public static final String STORAGE_TEST_GROUP_ENT_V_2_DESCRIPTION = "EntV2 Storage test group.";
+
+    protected static String token = null;
+    protected static String noDataAccesstoken = null;
+    protected static String dataRootToken = null;
+    private static Gson gson = new Gson();
+
+    protected static String groupId = System.getProperty("ENTITLEMENTS_DOMAIN", System.getenv("ENTITLEMENTS_DOMAIN"));
+
+    public static final String getGroupId() {
+        return groupId;
+    }
+
+    public static final String getAclSuffix() {
+        return String.format("%s.%s", TenantUtils.getTenantName(), groupId);
+    }
+
+    public static final String getAcl() {
+        return String.format("data.test1@%s", getAclSuffix());
+    }
+
+    public static final String getEntV2OnlyAcl() {
+        return String.format(STORAGE_TEST_GROUP_ENT_V_2 + "@%s", getAclSuffix());
+    }
+
+    public static final String getIntegrationTesterAcl() {
+        return String.format("data.integration.test@%s", getAclSuffix());
+    }
+
+    public static String getApiPath(String api) throws MalformedURLException {
+        String baseUrl = System.getProperty("STORAGE_URL", System.getenv("STORAGE_URL"));
+        if (baseUrl == null || baseUrl.contains("-null")) {
+            baseUrl = "https://localhost:8443/api/storage/v2/";
+        }
+        URL mergedURL = new URL(baseUrl + api);
+        System.out.println(mergedURL.toString());
+        return mergedURL.toString();
+    }
+
+    public static void assertRecordVersion(CloseableHttpResponse response, Long expectedVersion) {
+        assertEquals(HttpStatus.SC_OK, response.getCode());
+
+        String responseBody;
+        try {
+            responseBody = EntityUtils.toString(response.getEntity());
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        } catch (ParseException e) {
+            throw new RuntimeException(e);
+        }
+        DummyRecordsHelper.RecordResultMock result = gson.fromJson(responseBody, DummyRecordsHelper.RecordResultMock.class);
+        assertEquals(expectedVersion.longValue(), Long.parseLong(result.version));
+    }
+
+    public static String assertRecordVersionAndReturnResponseBody(CloseableHttpResponse response, Long expectedVersion) {
+        assertEquals(HttpStatus.SC_OK, response.getCode());
+
+        String responseBody;
+        try {
+            responseBody = EntityUtils.toString(response.getEntity());
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        } catch (ParseException e) {
+            throw new RuntimeException(e);
+        }
+        DummyRecordsHelper.RecordResultMock result = gson.fromJson(responseBody, DummyRecordsHelper.RecordResultMock.class);
+        assertEquals(expectedVersion.longValue(), Long.parseLong(result.version));
+        return responseBody;
+    }
+
+    public abstract String getToken() throws Exception;
+
+    public abstract String getNoDataAccessToken() throws Exception;
+
+    public String getDataRootUserToken() throws Exception{
+        throw new NotImplementedException();
+    }
+
+    public static CloseableHttpResponse sendWithCustomMediaType(String path, String httpMethod, Map<String, String> headers, String contentType, String requestBody,
+                                                                String query) throws Exception {
+
+        log(httpMethod, TestUtils.getApiPath(path + query), headers, requestBody);
+        BasicHttpClientConnectionManager cm = createBasicHttpClientConnectionManager();
+        headers.put("Content-Type", contentType);
+        ClassicHttpRequest httpRequest = createHttpRequest(TestUtils.getApiPath(path + query), httpMethod, requestBody, headers);
+
+        try (CloseableHttpClient httpClient = HttpClientBuilder.create().setConnectionManager(cm).build()) {
+            return httpClient.execute(httpRequest, new CustomHttpClientResponseHandler());
+        }
+    }
+
+    public static CloseableHttpResponse send(String path, String httpMethod, Map<String, String> headers, String requestBody,
+                                             String query) throws Exception {
+
+        log(httpMethod, TestUtils.getApiPath(path + query), headers, requestBody);
+
+        BasicHttpClientConnectionManager cm = createBasicHttpClientConnectionManager();
+        headers.put("Content-Type", MediaType.APPLICATION_JSON);
+        headers.put("Accept-Charset","utf-8");
+        ClassicHttpRequest httpRequest = createHttpRequest(TestUtils.getApiPath(path + query), httpMethod, requestBody, headers);
+
+        try (CloseableHttpClient httpClient = HttpClientBuilder.create().setConnectionManager(cm).build()) {
+            return httpClient.execute(httpRequest, new CustomHttpClientResponseHandler());
+        }
+    }
+
+    public static CloseableHttpResponse send(String url, String path, String httpMethod, Map<String, String> headers,
+                                             String requestBody, String query) throws Exception {
+
+        log(httpMethod, url + path, headers, requestBody);
+
+        BasicHttpClientConnectionManager cm = createBasicHttpClientConnectionManager();
+        headers.put("Content-Type", MediaType.APPLICATION_JSON);
+        ClassicHttpRequest httpRequest = createHttpRequest(url + path, httpMethod, requestBody, headers);
+
+        try (CloseableHttpClient httpClient = HttpClientBuilder.create().setConnectionManager(cm).build()) {
+            return httpClient.execute(httpRequest, new CustomHttpClientResponseHandler());
+        }
+    }
+
+    private static void log(String method, String url, Map<String, String> headers, String body) {
+        System.out.println(String.format("%s: %s", method, url));
+        System.out.println(body);
+    }
+
+    @SuppressWarnings("unchecked")
+    public static <T> T getResult(CloseableHttpResponse response, int exepectedStatus, Class<T> classOfT) {
+        assertEquals(exepectedStatus, response.getCode());
+        if (exepectedStatus == HttpStatus.SC_NO_CONTENT) {
+            return null;
+        }
+
+        assertTrue(response.getEntity().getContentType().contains("application/json"));
+        String json;
+        try {
+            json = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        } catch (ParseException e) {
+            throw new RuntimeException(e);
+        }
+        if (classOfT == String.class) {
+            return (T) json;
+        }
+        return gson.fromJson(json, classOfT);
+    }
+
+    public static Long createRecordInCollaborationContext_AndReturnVersion(String recordId, String kind, String legaltag, String collaborationId, String applicationName, String tenant_name, String token) throws Exception {
+        String jsonInput = RecordUtil.createDefaultJsonRecord(recordId, kind, legaltag);
+
+        CloseableHttpResponse response = TestUtils.send("records", "PUT", getHeadersWithxCollaboration(collaborationId, applicationName, tenant_name, token), jsonInput, "");
+        assertEquals(SC_CREATED, response.getCode());
+        assertTrue(response.getEntity().getContentType().contains("application/json"));
+
+        String responseBody = EntityUtils.toString(response.getEntity());
+        DummyRecordsHelper.CreateRecordResponse result = GSON.fromJson(responseBody, DummyRecordsHelper.CreateRecordResponse.class);
+
+        return Long.parseLong(result.recordIdVersions[0].split(":")[3]);
+    }
+
+    private static ClassicHttpRequest createHttpRequest(String path, String httpMethod, String requestBody,
+                                                        Map<String, String> headers) throws MalformedURLException {
+        ClassicRequestBuilder classicRequestBuilder = ClassicRequestBuilder.create(httpMethod)
+                .setUri(path)
+                .setEntity(requestBody, ContentType.APPLICATION_JSON);
+        headers.forEach(classicRequestBuilder::addHeader);
+        return classicRequestBuilder.build();
+    }
+
+
+    private static BasicHttpClientConnectionManager createBasicHttpClientConnectionManager() {
+    ConnectionConfig connConfig = ConnectionConfig.custom()
+            .setConnectTimeout(1500000, TimeUnit.MILLISECONDS)
+            .setSocketTimeout(1500000, TimeUnit.MILLISECONDS)
+            .build();
+    BasicHttpClientConnectionManager cm = new BasicHttpClientConnectionManager();
+    cm.setConnectionConfig(connConfig);
+    return cm;
+  }
+
+    public static JsonObject getCopyRecordRequest(String target, String stringId) {
+        JsonObject data = new JsonObject();
+        JsonArray array = new JsonArray();
+        JsonObject records = new JsonObject();
+        records.addProperty("id", stringId);
+        array.add(records);
+        data.addProperty("target", target);
+        data.add("records", array);
+        return data;
+    }
+}
diff --git a/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/util/TokenTestUtils.java b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/util/TokenTestUtils.java
new file mode 100644
index 0000000000000000000000000000000000000000..892f70e48a8f42b279041ee4f5497608261eb678
--- /dev/null
+++ b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/util/TokenTestUtils.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2020-2023 Google LLC
+ * Copyright 2020-2023 EPAM Systems, Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.opengroup.osdu.storage.util;
+
+import com.google.common.base.Strings;
+
+public class TokenTestUtils extends TestUtils {
+
+  public static final String INTEGRATION_TESTER_TOKEN = "PRIVILEGED_USER_TOKEN";
+  public static final String NO_DATA_ACCESS_TOKEN = "NO_ACCESS_USER_TOKEN";
+  public static final String DATA_ROOT_TOKEN = "ROOT_USER_TOKEN";
+  private OpenIDTokenProvider openIDTokenProvider;
+
+  public TokenTestUtils() {
+    token = System.getProperty(INTEGRATION_TESTER_TOKEN, System.getenv(INTEGRATION_TESTER_TOKEN));
+    noDataAccesstoken = System.getProperty(NO_DATA_ACCESS_TOKEN, System.getenv(NO_DATA_ACCESS_TOKEN));
+    dataRootToken = System.getProperty(DATA_ROOT_TOKEN, System.getenv(DATA_ROOT_TOKEN));
+
+    if (Strings.isNullOrEmpty(token) || Strings.isNullOrEmpty(noDataAccesstoken) || Strings.isNullOrEmpty(dataRootToken)) {
+      openIDTokenProvider = new OpenIDTokenProvider();
+    }
+  }
+
+  @Override
+  public synchronized String getToken() throws Exception {
+    if (Strings.isNullOrEmpty(token)) {
+      token = openIDTokenProvider.getToken();
+    }
+    return "Bearer " + token;
+  }
+
+  @Override
+  public synchronized String getNoDataAccessToken() throws Exception {
+    if (Strings.isNullOrEmpty(noDataAccesstoken)) {
+      noDataAccesstoken = openIDTokenProvider.getNoAccessToken();
+    }
+    return "Bearer " + noDataAccesstoken;
+  }
+
+  @Override
+  public String getDataRootUserToken() throws Exception {
+    if (Strings.isNullOrEmpty(dataRootToken)) {
+      dataRootToken = openIDTokenProvider.getDataRootToken();
+    }
+    return "Bearer " + dataRootToken;
+  }
+}
diff --git a/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/util/VersionInfoUtils.java b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/util/VersionInfoUtils.java
new file mode 100644
index 0000000000000000000000000000000000000000..f686e0ef0eadcd99e2c06edd0d8fd910e04b778c
--- /dev/null
+++ b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/util/VersionInfoUtils.java
@@ -0,0 +1,53 @@
+/*
+ *  Copyright 2020-2024 Google LLC
+ *  Copyright 2020-2024 EPAM Systems, Inc
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+package org.opengroup.osdu.storage.util;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.google.gson.Gson;
+import java.io.IOException;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
+import org.apache.hc.core5.http.ParseException;
+import org.apache.hc.core5.http.io.entity.EntityUtils;
+
+public class VersionInfoUtils {
+
+  public VersionInfo getVersionInfoFromResponse(CloseableHttpResponse response) {
+    assertTrue(response.getEntity().getContentType().contains("application/json"));
+    String json = null;
+    try {
+      json = EntityUtils.toString(response.getEntity());
+    } catch (IOException e) {
+      throw new RuntimeException(e);
+    } catch (ParseException e) {
+      throw new RuntimeException(e);
+    }
+    Gson gson = new Gson();
+    return gson.fromJson(json, VersionInfo.class);
+  }
+
+  public class VersionInfo {
+    public String groupId;
+    public String artifactId;
+    public String version;
+    public String buildTime;
+    public String branch;
+    public String commitId;
+    public String commitMessage;
+  }
+}
diff --git a/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/util/conf/OpenIDProviderConfig.java b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/util/conf/OpenIDProviderConfig.java
new file mode 100644
index 0000000000000000000000000000000000000000..411ef726e261f30955b24ffbcc4bc8c232a733ed
--- /dev/null
+++ b/storage-acceptance-test/src/test/java/org/opengroup/osdu/storage/util/conf/OpenIDProviderConfig.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright 2020-2023 Google LLC
+ * Copyright 2020-2023 EPAM Systems, Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.opengroup.osdu.storage.util.conf;
+
+import com.nimbusds.oauth2.sdk.http.HTTPRequest;
+import com.nimbusds.oauth2.sdk.http.HTTPResponse;
+import com.nimbusds.oauth2.sdk.id.Issuer;
+import com.nimbusds.openid.connect.sdk.op.OIDCProviderConfigurationRequest;
+import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata;
+
+public class OpenIDProviderConfig {
+
+  public static final String TEST_OPENID_PROVIDER_CLIENT_ID = "PRIVILEGED_USER_OPENID_PROVIDER_CLIENT_ID";
+  public static final String TEST_OPENID_PROVIDER_CLIENT_SECRET =
+      "PRIVILEGED_USER_OPENID_PROVIDER_CLIENT_SECRET";
+  public static final String TEST_NO_ACCESS_OPENID_PROVIDER_CLIENT_ID =
+      "NO_ACCESS_USER_OPENID_PROVIDER_CLIENT_ID";
+  public static final String TEST_NO_ACCESS_OPENID_PROVIDER_CLIENT_SECRET =
+      "NO_ACCESS_USER_OPENID_PROVIDER_CLIENT_SECRET";
+  public static final String DATA_ROOT_OPENID_PROVIDER_CLIENT_ID =
+      "ROOT_USER_OPENID_PROVIDER_CLIENT_ID";
+  public static final String DATA_ROOT_OPENID_PROVIDER_CLIENT_SECRET =
+      "ROOT_USER_OPENID_PROVIDER_CLIENT_SECRET";
+  public static final String TEST_OPENID_PROVIDER_URL = "TEST_OPENID_PROVIDER_URL";
+
+  private String clientId;
+  private String clientSecret;
+  private String noAccessClientId;
+  private String noAccessClientSecret;
+  private String dataRootClientId;
+  private String dataRootClientSecret;
+  private String url;
+  private final String[] scopes = {"openid"};
+  private static final OpenIDProviderConfig openIDProviderConfig = new OpenIDProviderConfig();
+  private static OIDCProviderMetadata providerMetadata;
+
+  public static OpenIDProviderConfig Instance() {
+    try {
+      openIDProviderConfig.clientId =
+          System.getProperty(
+              TEST_OPENID_PROVIDER_CLIENT_ID, System.getenv(TEST_OPENID_PROVIDER_CLIENT_ID));
+
+      openIDProviderConfig.clientSecret =
+          System.getProperty(
+              TEST_OPENID_PROVIDER_CLIENT_SECRET,
+              System.getenv(TEST_OPENID_PROVIDER_CLIENT_SECRET));
+
+      openIDProviderConfig.noAccessClientId =
+          System.getProperty(
+              TEST_NO_ACCESS_OPENID_PROVIDER_CLIENT_ID,
+              System.getenv(TEST_NO_ACCESS_OPENID_PROVIDER_CLIENT_ID));
+
+      openIDProviderConfig.noAccessClientSecret =
+          System.getProperty(
+              TEST_NO_ACCESS_OPENID_PROVIDER_CLIENT_SECRET,
+              System.getenv(TEST_NO_ACCESS_OPENID_PROVIDER_CLIENT_SECRET));
+
+      openIDProviderConfig.url =
+          System.getProperty(TEST_OPENID_PROVIDER_URL, System.getenv(TEST_OPENID_PROVIDER_URL));
+
+      openIDProviderConfig.dataRootClientId =
+          System.getProperty(
+              DATA_ROOT_OPENID_PROVIDER_CLIENT_ID,
+              System.getenv(DATA_ROOT_OPENID_PROVIDER_CLIENT_ID));
+
+      openIDProviderConfig.dataRootClientSecret =
+          System.getProperty(
+              DATA_ROOT_OPENID_PROVIDER_CLIENT_SECRET,
+              System.getenv(DATA_ROOT_OPENID_PROVIDER_CLIENT_SECRET));
+
+      Issuer issuer = new Issuer(openIDProviderConfig.url);
+      OIDCProviderConfigurationRequest request = new OIDCProviderConfigurationRequest(issuer);
+      HTTPRequest httpRequest = request.toHTTPRequest();
+      HTTPResponse httpResponse = httpRequest.send();
+      providerMetadata = OIDCProviderMetadata.parse(httpResponse.getContentAsJSONObject());
+    } catch (Exception e) {
+      throw new RuntimeException("Malformed token provider configuration", e);
+    }
+    return openIDProviderConfig;
+  }
+
+  public String getClientId() {
+    return clientId;
+  }
+
+  public String getClientSecret() {
+    return clientSecret;
+  }
+
+  public String getNoAccessClientId() {
+    return noAccessClientId;
+  }
+
+  public String getNoAccessClientSecret() {
+    return noAccessClientSecret;
+  }
+
+  public String[] getScopes() {
+    return scopes;
+  }
+
+  public OIDCProviderMetadata getProviderMetadata() {
+    return providerMetadata;
+  }
+
+  public String getDataRootClientId() {
+    return dataRootClientId;
+  }
+
+  public String getDataRootClientSecret() {
+    return dataRootClientSecret;
+  }
+}
diff --git a/storage-acceptance-test/src/test/resources/test.properties b/storage-acceptance-test/src/test/resources/test.properties
new file mode 100644
index 0000000000000000000000000000000000000000..2674162d436b19dda249978c2701708aedb565c5
--- /dev/null
+++ b/storage-acceptance-test/src/test/resources/test.properties
@@ -0,0 +1,4 @@
+enableEncodedSpecialCharactersInURL=true
+opa.integration.enabled=true
+collaboration.enabled=false
+test.replay.enabled=false