org.talend.dataprep.preparation.service.PreparationControllerTest.java Source code

Java tutorial

Introduction

Here is the source code for org.talend.dataprep.preparation.service.PreparationControllerTest.java

Source

// ============================================================================
// Copyright (C) 2006-2016 Talend Inc. - www.talend.com
//
// This source code is available under agreement available at
// https://github.com/Talend/data-prep/blob/master/LICENSE
//
// You should have received a copy of the agreement
// along with this program; if not, write to Talend SA
// 9 rue Pages 92150 Suresnes, France
//
// ============================================================================

package org.talend.dataprep.preparation.service;

import static com.jayway.restassured.RestAssured.given;
import static com.jayway.restassured.RestAssured.when;
import static java.util.Arrays.asList;
import static java.util.Collections.singletonList;
import static junit.framework.TestCase.assertTrue;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import static org.talend.dataprep.api.folder.FolderContentType.PREPARATION;
import static org.talend.dataprep.preparation.service.EntityBuilder.*;
import static org.talend.dataprep.preparation.service.PreparationControllerTestClient.appendStepsToPrep;
import static org.talend.dataprep.test.SameJSONFile.sameJSONAsFile;
import static uk.co.datumedge.hamcrest.json.SameJSONAs.sameJSONAs;

import java.io.IOException;
import java.io.InputStream;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;

import org.apache.commons.io.IOUtils;
import org.junit.Ignore;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.talend.dataprep.api.dataset.ColumnMetadata;
import org.talend.dataprep.api.dataset.RowMetadata;
import org.talend.dataprep.api.folder.Folder;
import org.talend.dataprep.api.folder.FolderEntry;
import org.talend.dataprep.api.preparation.*;
import org.talend.dataprep.lock.store.LockedResource;
import org.talend.dataprep.preparation.BasePreparationTest;
import org.talend.dataprep.transformation.actions.common.ImplicitParameters;
import org.talend.dataprep.util.SortAndOrderHelper;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.jayway.restassured.http.ContentType;
import com.jayway.restassured.path.json.JsonPath;
import com.jayway.restassured.response.Response;

/**
 * Unit test for the preparation service.
 */
public class PreparationControllerTest extends BasePreparationTest {

    @Autowired
    private PreparationUtils preparationUtils;

    @Test
    public void CORSHeaders() throws Exception {
        given().header("Origin", "fake.host.to.trigger.cors").when().get("/preparations").then()
                .header("Access-Control-Allow-Origin", "fake.host.to.trigger.cors");
    }

    // ------------------------------------------------------------------------------------------------------------------
    // ------------------------------------------------------GETTER------------------------------------------------------
    // ------------------------------------------------------------------------------------------------------------------
    @Test
    public void shouldListAllPreparations() throws Exception {
        // given
        when().get("/preparations/details").then().statusCode(HttpStatus.OK.value()).body(sameJSONAs("[]"));

        final Preparation preparation = new Preparation("#548425458", "1234", rootStep.id(),
                versionService.version().getVersionId());
        preparation.setCreationDate(0);
        preparation.setLastModificationDate(12345);
        repository.add(preparation);

        // when
        final Response response = when().get("/preparations/details");

        // then
        response.then().statusCode(HttpStatus.OK.value())
                .body(sameJSONAs("[{\"id\":\"#548425458\"," + "\"dataSetId\":\"1234\"," + "\"creationDate\":0,"
                        + "\"lastModificationDate\":12345}]").allowingExtraUnexpectedFields());

        // given
        final Preparation preparation1 = new Preparation("#1438725", "5678", rootStep.id(),
                versionService.version().getVersionId());
        preparation1.setCreationDate(500);
        preparation1.setLastModificationDate(456789);
        repository.add(preparation1);

        // when
        final Response responseMultiple = when().get("/preparations/details");

        // then
        //@formatter:off
        responseMultiple.then().statusCode(HttpStatus.OK.value())
                .body(sameJSONAs("[" + "{\"id\":\"#548425458\"," + "\"dataSetId\":\"1234\"," + "\"creationDate\":0,"
                        + "\"lastModificationDate\":12345," + "}," + "{\"id\":\"#1438725\","
                        + "\"dataSetId\":\"5678\"," + "\"creationDate\":500," + "\"lastModificationDate\":456789,"
                        + "}" + "]").allowingExtraUnexpectedFields().allowingAnyArrayOrdering());
        //@formatter:on
    }

    @Test
    public void shouldListByFolder() throws Exception {
        // given
        final Folder rootFolder = folderRepository.addFolder(home.getId(), "/root");
        final List<String> rootPreparations = new ArrayList<>(1);
        rootPreparations.add(
                createPreparationWithAPI("{\"name\": \"prep_1\", \"dataSetId\": \"1234\"}", rootFolder.getId()));

        final Folder threePrepsFolder = folderRepository.addFolder(rootFolder.getId(), "three_preps");
        final List<String> threePreparations = new ArrayList<>(3);
        threePreparations.add(createPreparationWithAPI("{\"name\": \"prep_2\", \"dataSetId\": \"1234\"}",
                threePrepsFolder.getId()));
        threePreparations.add(createPreparationWithAPI("{\"name\": \"prep_3\", \"dataSetId\": \"1234\"}",
                threePrepsFolder.getId()));
        threePreparations.add(createPreparationWithAPI("{\"name\": \"prep_4\", \"dataSetId\": \"1234\"}",
                threePrepsFolder.getId()));

        final Folder noPrepsFolder = folderRepository.addFolder(threePrepsFolder.getId(), "no_prep");
        List<String> noPreparations = new ArrayList<>();

        // then
        checkSearchFolder(rootFolder.getId(), rootPreparations, SortAndOrderHelper.Sort.CREATION_DATE.camelName());
        checkSearchFolder(threePrepsFolder.getId(), threePreparations,
                SortAndOrderHelper.Sort.CREATION_DATE.camelName());
        checkSearchFolder(noPrepsFolder.getId(), noPreparations, SortAndOrderHelper.Sort.CREATION_DATE.camelName());
    }

    @Test
    public void test_TDP_2158() throws Exception {
        // given
        final Folder rootFolder = folderRepository.addFolder(home.getId(), "/root");
        final List<String> rootPreparations = new ArrayList<>(1);
        rootPreparations.add(
                createPreparationWithAPI("{\"name\": \"prep_1\", \"dataSetId\": \"1234\"}", rootFolder.getId()));

        final Folder threePrepsFolder = folderRepository.addFolder(rootFolder.getId(), "three_preps");
        final List<String> threePreparations = new ArrayList<>(3);
        threePreparations.add(createPreparationWithAPI("{\"name\": \"prep_2\", \"dataSetId\": \"1234\"}",
                threePrepsFolder.getId()));
        threePreparations.add(createPreparationWithAPI("{\"name\": \"Prep_3\", \"dataSetId\": \"1234\"}",
                threePrepsFolder.getId()));
        threePreparations.add(createPreparationWithAPI("{\"name\": \"prep_4\", \"dataSetId\": \"1234\"}",
                threePrepsFolder.getId()));

        final Folder noPrepsFolder = folderRepository.addFolder(threePrepsFolder.getId(), "no_prep");
        List<String> noPreparations = new ArrayList<>();

        // then
        checkSearchFolder(rootFolder.getId(), rootPreparations, SortAndOrderHelper.Sort.CREATION_DATE.camelName());
        checkSearchFolder(threePrepsFolder.getId(), threePreparations, SortAndOrderHelper.Sort.NAME.camelName());
        checkSearchFolder(noPrepsFolder.getId(), noPreparations, SortAndOrderHelper.Sort.CREATION_DATE.camelName());
    }

    /**
     * Check that the folder search is correct.
     *
     * @param folderId the folder to search.
     * @param expectedIds the expected preparations id.
     * @throws IOException if an error occurs.
     */
    private void checkSearchFolder(final String folderId, final List<String> expectedIds, String sort)
            throws IOException {
        // when
        final Response response = given() //
                .queryParam("folderId", folderId) //
                .queryParam("sort", sort) //
                .queryParam("order", "asc") //
                .when().expect().statusCode(200).log().ifError() //
                .get("/preparations/search");

        // then
        assertThat(response.getStatusCode(), is(200));
        final JsonNode rootNode = mapper.reader().readTree(response.asInputStream());
        assertTrue(rootNode.isArray());
        assertEquals(expectedIds.size(), rootNode.size());
        final List<String> actualIds = StreamSupport.stream(rootNode.spliterator(), false)
                .map(n -> n.get("id").asText()).collect(Collectors.toList());
        assertEquals(expectedIds, actualIds);
    }

    @Test
    public void shouldSearchByName() throws Exception {
        // given
        final String tdpId = createPreparation("1234", "talendDataPrep");
        final String dpId = createPreparation("4567", "dataPrep");
        final String pId = createPreparation("8901", "prep");

        // when then
        checkSearchByName("prep", false, asList(pId, dpId, tdpId));
    }

    private void checkSearchByName(String name, boolean exactMatch, List<String> expectedIds) throws IOException {
        // when
        final Response response = given() //
                .queryParam("name", name) //
                .queryParam("exactMatch", exactMatch) //
                .queryParam("sort", "creationDate") //
                .queryParam("order", "asc") //
                .when().expect().statusCode(200).log().ifError() //
                .get("/preparations/search");

        // then
        assertThat(response.getStatusCode(), is(200));
        final JsonNode rootNode = mapper.reader().readTree(response.asInputStream());
        assertTrue(rootNode.isArray());
        assertEquals(expectedIds.size(), rootNode.size());
        final List<String> actualIds = StreamSupport.stream(rootNode.spliterator(), false)
                .map(n -> n.get("id").asText()).collect(Collectors.toList());
        assertEquals(expectedIds.size(), actualIds.stream().filter(expectedIds::contains).count());
    }

    /**
     * See <a href="https://jira.talendforge.org/browse/TDP-1604">TDP-1604</a>
     */
    @Test
    public void listPreparationsShouldBeOrderedByLastModificationDate() throws Exception {
        // given
        when().get("/preparations/details").then().statusCode(HttpStatus.OK.value()).body(sameJSONAs("[]"));

        List<String> preparationIds = new ArrayList<>(5);
        for (int i = 0; i < 5; i++) {
            final long now = new Date().getTime();
            Preparation preparation = new Preparation(UUID.randomUUID().toString(), "12345", rootStep.id(),
                    versionService.version().getVersionId());
            preparation.setName("prep-" + i);
            preparation.setLastModificationDate(now);
            repository.add(preparation);
            preparationIds.add(0, preparation.id()); // last modified preparation is first
            Thread.sleep(100); // to make sure the last modification date is older
        }

        // when
        final InputStream responseContent = when().get("/preparations/details").asInputStream();
        final List<Preparation> actual = mapper.readValue(responseContent, new TypeReference<List<Preparation>>() {
        });

        // then
        assertEquals(preparationIds.size(), actual.size());
        for (int i = 0; i < actual.size(); i++) {
            assertEquals(preparationIds.get(i), actual.get(i).getId());
        }
    }

    @Test
    public void list() throws Exception {
        // given
        when().get("/preparations").then().statusCode(HttpStatus.OK.value()).body(sameJSONAs("[]"));
        final Preparation preparation1 = new Preparation("#18875", "1234", rootStep.id(),
                versionService.version().getVersionId());
        preparation1.setCreationDate(0);
        repository.add(preparation1);

        when().get("/preparations").then().statusCode(HttpStatus.OK.value()).body(sameJSONAs("[\"#18875\"]"));
        final Preparation preparation2 = new Preparation("#483275", "5678", rootStep.id(),
                versionService.version().getVersionId());
        preparation2.setCreationDate(0);
        repository.add(preparation2);

        // when
        final List<String> list = when().get("/preparations").jsonPath().getList("");

        // then
        assertThat(list, hasItems("#18875", "#483275"));
    }

    @Test
    public void getDetails() throws Exception {
        // given
        final Preparation preparation = new Preparation("8b6281c5e99c41313a83777c3ab43b06adda9e5c", "1234",
                rootStep.id(), versionService.version().getVersionId());
        preparation.setCreationDate(0);
        preparation.setLastModificationDate(12345);
        repository.add(preparation);

        // when
        final String preparationDetails = when().get("/preparations/{id}/details", preparation.id()).asString();

        // then
        final InputStream expected = PreparationControllerTest.class.getResourceAsStream("preparation_1234.json");
        assertThat(preparationDetails, sameJSONAsFile(expected));
    }

    @Test
    public void cannotCopyUnknownPreparation() throws Exception {

        // given
        String unknownId = "0001";

        // when
        final Response response = given() //
                .queryParam("name", "the new preparation") //
                .queryParam("destination", "new preparation") //
                .when() //
                .expect().statusCode(404).log().ifError() //
                .post("/preparations/{id}/copy", unknownId);

        // then
        assertThat(response.getStatusCode(), is(404));

    }

    @Test
    public void shouldCopy() throws Exception {
        // given
        final Folder fromFolder = folderRepository.addFolder(home.getId(), "from");
        final Folder toFolder = folderRepository.addFolder(home.getId(), "to");

        final String originalId = createPreparationWithAPI(
                "{\"name\": \"test_name\", \"dataSetId\": \"1234\", \"rowMetadata\":{\"columns\":[]}}}",
                fromFolder.getId());
        // Change the author, to make it different from system user.
        repository.get(originalId, Preparation.class).setAuthor("tagada");

        // when
        final Response response = given() //
                .queryParam("name", "the new preparation") //
                .queryParam("destination", toFolder.getId()) //
                .when() //
                .expect().statusCode(200).log().ifError() //
                .post("/preparations/{id}/copy", originalId);
        final String copyId = response.asString();

        // then
        assertThat(response.getStatusCode(), is(200));
        final Iterator<FolderEntry> iterator = folderRepository.entries(toFolder.getId(), PREPARATION).iterator();
        assertTrue(iterator.hasNext());
        final FolderEntry copy = iterator.next();
        assertEquals(copy.getContentId(), copyId);
        assertEquals("the new preparation", repository.get(copyId, Preparation.class).getName());

        // Assert that the author is the system user, and not the original author of the prep:
        assertEquals(System.getProperty("user.name"), repository.get(copyId, Preparation.class).getAuthor());
        // row metadata its nullity after copy caused https://jira.talendforge.org/browse/TDP-3379
        assertNotNull(repository.get(copyId, Preparation.class).getRowMetadata());
    }

    @Test
    public void cannotCopyIfAnExistingPreparationAlreadyExists() throws Exception {
        // given
        final String name = "my preparation";
        final Folder folder = folderRepository.addFolder(home.getId(), "great_folder");

        final String originalId = createPreparationWithAPI("{\"name\": \"" + name + "\", \"dataSetId\": \"1234\"}",
                folder.getId());

        // when
        final Response response = given() //
                .queryParam("name", name) //
                .queryParam("destination", folder.getId()) //
                .when() //
                .expect().statusCode(409).log().ifError() //
                .post("/preparations/{id}/copy", originalId);

        // then
        assertThat(response.getStatusCode(), is(409));
    }

    @Test
    public void shouldCopyWithDefaultParameters() throws Exception {
        // given
        final Folder folder = folderRepository.addFolder(home.getId(), "yet_another_folder");
        final String originalId = createPreparationWithAPI("{\"name\": \"prep_1\", \"dataSetId\": \"1234\"}",
                folder.getId());
        final Folder toFolder = folderRepository.addFolder(home.getId(), "to");

        // when
        final Response response = given() //
                .queryParam("destination", toFolder.getId()) //
                .when() //
                .expect().statusCode(200).log().ifError() //
                .post("/preparations/{id}/copy", originalId);
        final String copyId = response.asString();

        // then
        assertThat(response.getStatusCode(), is(200));
        final Iterator<FolderEntry> iterator = folderRepository.entries(toFolder.getId(), PREPARATION).iterator();
        boolean found = false;
        while (iterator.hasNext()) {
            final FolderEntry entry = iterator.next();
            if (entry.getContentId().equals(copyId)) {
                found = true;
                assertEquals("prep_1 Copy", repository.get(entry.getContentId(), Preparation.class).getName());
            }
        }
        assertTrue(found);
    }

    @Test
    public void shouldMove() throws Exception {
        // given
        final Folder fromFolder = folderRepository.addFolder(home.getId(), "from");
        final Folder toFolder = folderRepository.addFolder(home.getId(), "to");

        final String originalId = createPreparationWithAPI("{\"name\": \"test_move\", \"dataSetId\": \"7535\"}",
                fromFolder.getId());

        // when
        final Response response = given() //
                .queryParam("folder", fromFolder.getId()) //
                .queryParam("destination", toFolder.getId()) //
                .queryParam("newName", "moved preparation") //
                .when() //
                .expect().statusCode(200).log().ifError() //
                .put("/preparations/{id}/move", originalId);

        // then
        assertThat(response.getStatusCode(), is(200));
        final Iterator<FolderEntry> iterator = folderRepository.entries(toFolder.getId(), PREPARATION).iterator();
        assertTrue(iterator.hasNext());
        final FolderEntry copy = iterator.next();
        assertEquals(copy.getContentId(), originalId);
        assertEquals("moved preparation", repository.get(originalId, Preparation.class).getName());
    }

    @Test
    public void cannotMovePreparationIfTargetPathAndNameExists() throws Exception {
        // given
        final Folder fromFolder = folderRepository.addFolder(home.getId(), "from");
        final Folder toFolder = folderRepository.addFolder(home.getId(), "to");

        final String name = "super preparation";
        final String preparationId = createPreparationWithAPI(
                "{\"name\": \"another preparation\", \"dataSetId\": \"7535\"}", fromFolder.getId());
        createPreparationWithAPI("{\"name\": \"" + name + "\", \"dataSetId\": \"7384\"}", toFolder.getId());

        // when
        final Response response = given() //
                .queryParam("folder", fromFolder.getId()) //
                .queryParam("destination", toFolder.getId()) //
                .queryParam("newName", name) //
                .when() //
                .expect().statusCode(409).log().ifError() //
                .put("/preparations/{id}/move", preparationId);

        // then
        assertThat(response.getStatusCode(), is(409));
    }

    @Test
    public void shouldMoveWithDefaultParameters() throws Exception {
        // given
        final Folder fromFolder = folderRepository.addFolder(home.getId(), "from");
        final Folder toFolder = folderRepository.addFolder(home.getId(), "to");
        final String originalId = createPreparationWithAPI("{\"name\": \"yap\", \"dataSetId\": \"7535\"}",
                fromFolder.getId());

        // when
        final Response response = given() //
                .queryParam("folder", fromFolder.getId()) //
                .queryParam("destination", toFolder.getId()) //
                .when() //
                .expect().statusCode(200).log().ifError() //
                .put("/preparations/{id}/move", originalId);

        // then
        assertThat(response.getStatusCode(), is(200));
        final Iterator<FolderEntry> iterator = folderRepository.entries(toFolder.getId(), PREPARATION).iterator();
        assertTrue(iterator.hasNext());
        final FolderEntry copy = iterator.next();
        assertEquals(copy.getContentId(), originalId);
        assertEquals("yap", repository.get(originalId, Preparation.class).getName());
    }

    @Test
    public void cannotMovePreparationThatDoesntExist() throws Exception {
        // given
        final Folder fromFolder = folderRepository.addFolder(home.getId(), "from");
        final Folder toFolder = folderRepository.addFolder(home.getId(), "to");

        // when
        final Response response = given() //
                .queryParam("newName", "moved preparation") //
                .queryParam("folder", fromFolder.getId()) //
                .queryParam("destination", toFolder.getId()) //
                .when() //
                .expect().statusCode(404).log().ifError() //
                .put("/preparations/{id}/move", "ABC123XYZ");

        // then
        assertThat(response.getStatusCode(), is(404));
    }

    @Test
    public void shouldReturnListOfSpecificDatasetPreparations() throws Exception {
        // given : relevant preparation
        final String wantedDataSetId = "wanted";
        final String preparation1 = createPreparation(wantedDataSetId, "prep_1");
        final String preparation2 = createPreparation(wantedDataSetId, "prep_2");

        // given : noise data not to be returned by the service
        final String preparation3 = createPreparation("4523", "prep_3");
        final String preparation4 = createPreparation("7534", "prep_4");
        final String preparation5 = createPreparation("1598", "prep_5");

        // when
        final String result = when().get("/preparations/search?dataSetId=" + wantedDataSetId).asString();
        final List<String> preparationIds = JsonPath.from(result).get("id");

        // then
        assertThat(preparationIds, hasItem(preparation1));
        assertThat(preparationIds, hasItem(preparation2));
        assertThat(preparationIds, not(hasItem(preparation3)));
        assertThat(preparationIds, not(hasItem(preparation4)));
        assertThat(preparationIds, not(hasItem(preparation5)));
    }

    @Test
    public void shouldLocatePreparation() throws Exception {
        // given
        final Folder bar = folderRepository.addFolder(home.getId(), "/foo/bar");
        final String barEntry = createPreparationWithAPI("{\"name\": \"youpi\", \"dataSetId\": \"4824\"}",
                bar.getId());

        // when
        final Response response = given() //
                .when() //
                .expect().statusCode(200).log().ifError() //
                .get("/preparations/{id}/folder", barEntry);

        // then
        assertThat(response.getStatusCode(), is(200));
        final Folder actual = mapper.readValue(response.asInputStream(), Folder.class);
        assertEquals(bar.getId(), actual.getId());
    }

    @Test
    public void locateUnknownPreparationShouldSend404() throws Exception {
        // when
        final Response response = given() //
                .when() //
                .expect().statusCode(404).log().ifError() //
                .get("/preparations/{id}/folder", "unknown preparation");

        // then
        assertThat(response.getStatusCode(), is(404));
    }

    @Test
    public void shouldListErrors() throws Exception {
        // when
        final String errors = when().get("/preparations/errors").asString();

        final ObjectMapper mapper = new ObjectMapper();
        final JsonNode actualErrorCodes = mapper.readTree(errors);

        // then
        assertTrue(actualErrorCodes.isArray());
        assertTrue(actualErrorCodes.size() > 0);
        for (final JsonNode currentCode : actualErrorCodes) {
            assertTrue(currentCode.has("code"));
            assertTrue(currentCode.has("http-status-code"));
        }
    }

    @Test
    public void shouldCopySteps() throws Exception {
        // given
        final String referenceId = createPreparation("1234", "reference");
        applyTransformation(referenceId, "actions/append_upper_case.json");
        applyTransformation(referenceId, "actions/append_lower_case.json");
        final Preparation reference = repository.get(referenceId, Preparation.class);

        final String preparationId = createPreparation("42321", "preparation");

        // when
        final Response response = given() //
                .param("from", referenceId) //
                .when() //
                .expect().statusCode(200).log().ifError() //
                .put("/preparations/{id}/steps/copy", preparationId);

        // then
        assertThat(response.getStatusCode(), is(200));
        final Preparation preparation = repository.get(preparationId, Preparation.class);
        assertEquals(preparation.getHeadId(), reference.getHeadId());
    }

    @Test
    public void shouldNotCopyStepsFromPreparationNotFound() throws Exception {
        // when
        final Response response = given() //
                .param("from", "reference") //
                .when() //
                .expect().statusCode(404).log().ifError() //
                .put("/preparations/{id}/steps/copy", "prepNotFound");

        // then
        assertThat(response.getStatusCode(), is(404));
    }

    @Test
    public void shouldNotCopyStepsBecausePreparationIsNotEmpty() throws Exception {
        // given
        final String referenceId = createPreparation("8436587", "reference");
        applyTransformation(referenceId, "actions/append_upper_case.json");
        applyTransformation(referenceId, "actions/append_lower_case.json");

        final String preparationId = createPreparation("42321", "preparation");
        applyTransformation(preparationId, "actions/append_upper_case.json");

        // when
        final Response response = given() //
                .param("from", referenceId) //
                .when() //
                // .expect().statusCode(409).log().ifError() //
                .put("/preparations/{id}/steps/copy", preparationId);

        // then
        assertThat(response.getStatusCode(), is(409));
    }

    @Test
    public void shouldNotCopyStepsBecauseReferencePreparationIsNotFound() throws Exception {
        // given
        final String preparationId = createPreparation("154753", "preparation");
        final Preparation expected = repository.get(preparationId, Preparation.class);

        // when
        final Response response = given() //
                .param("from", "not to be found") //
                .when() //
                .expect().statusCode(200).log().ifError() //
                .put("/preparations/{id}/steps/copy", preparationId);

        // then
        assertThat(response.getStatusCode(), is(200));
        final Preparation actual = repository.get(preparationId, Preparation.class);
        assertEquals(expected, actual);
    }

    @Test
    public void shouldLockAFreshlyLockedPreparation() throws Exception {

        // given
        final String preparationId = createPreparation("154753", "preparation");

        // when
        final Response response = given() //
                .when() //
                .expect().statusCode(200).log().ifError() //
                .put("/preparations/{id}/lock", preparationId);

        // then
        assertThat(response.getStatusCode(), is(200));
    }

    @Test
    public void shouldLockAlreadyLockedPreparation() throws Exception {

        // given
        final String preparationId = createPreparation("154753", "preparation");

        // when
        given() //
                .when() //
                .expect().statusCode(200).log().ifError() //
                .put("/preparations/{id}/lock", preparationId);

        final Response response = given() //
                .when() //
                .expect().statusCode(200).log().ifError() //
                .put("/preparations/{id}/lock", preparationId);

        // then
        assertThat(response.getStatusCode(), is(200));
    }

    @Test
    public void shouldUnlockAPreviouslyLockedPreparation() throws Exception {

        // given
        final String preparationId = createPreparation("154753", "preparation");

        // when
        given() //
                .when() //
                .expect().statusCode(200).log().ifError() //
                .put("/preparations/{id}/lock", preparationId);

        final Response response = given() //
                .when() //
                .expect().statusCode(200).log().ifError() //
                .put("/preparations/{id}/unlock", preparationId);

        // then
        assertThat(response.getStatusCode(), is(200));
    }

    @Test
    public void shouldReleaseALockAfterAMove() throws Exception {

        // given
        final Folder fromFolder = folderRepository.addFolder(home.getId(), "from");
        final Folder toFolder = folderRepository.addFolder(home.getId(), "to");
        final String originalId = createPreparationWithAPI("{\"name\": \"yap\", \"dataSetId\": \"7535\"}",
                fromFolder.getId());
        Preparation expected = repository.get(originalId, Preparation.class);

        // when
        final Response response = given() //
                .queryParam("folder", fromFolder.getId()) //
                .queryParam("destination", toFolder.getId()) //
                .when() //
                .expect().statusCode(200).log().ifError() //
                .put("/preparations/{id}/move", originalId);

        // then
        assertThat(response.getStatusCode(), is(200));
        // assert that the resource is no more locked
        LockedResource lockedResource = lockRepository.get(expected);

        // then
        assertNull(lockedResource);
    }

    @Test
    public void shouldGetPreparation() throws Exception {
        // given
        final Folder fromFolder = folderRepository.addFolder(home.getId(), "from");
        final String preparationId = createPreparationWithAPI("{\"name\": \"yap\", \"dataSetId\": \"7535\"}",
                fromFolder.getId());
        final Preparation preparation = repository.get(preparationId, Preparation.class);
        final String expected = "{" + "\"id\":\"" + preparation.getId() + "\"," + "\"app-version\":\""
                + preparation.getAppVersion() + "\"," + "\"dataSetId\":\"7535\"," + "\"rowMetadata\":null,"
                + "\"author\":\"" + preparation.getAuthor() + "\"," + "\"name\":\"yap\"," + "\"creationDate\":"
                + preparation.getCreationDate() + "," + "\"lastModificationDate\":" + preparation.getCreationDate()
                + "," + "\"headId\":\"f6e172c33bdacbc69bca9d32b2bd78174712a171\"" + "}";

        // when
        final Response response = given() //
                .queryParam("id", preparationId) //
                .when() //
                .expect().statusCode(200).log().ifError() //
                .get("/preparations/{id}", preparationId);

        // then
        assertThat(response.asString(), sameJSONAs(expected).allowingExtraUnexpectedFields());
    }

    // ------------------------------------------------------------------------------------------------------------------
    // -----------------------------------------------------LIFECYCLE----------------------------------------------------
    // ------------------------------------------------------------------------------------------------------------------
    @Test
    public void create() throws Exception {
        // given
        assertThat(repository.list(Preparation.class).count(), is(0L));
        final String preparationId = createPreparationWithAPI(
                "{\"name\": \"test_name\", \"dataSetId\": \"1234\", \"rowMetadata\":{\"columns\":[]}}}}");
        assertThat(repository.list(Preparation.class).count(), is(1L));

        // when
        final Optional<Preparation> first = repository.list(Preparation.class).findFirst();
        assertThat(first.isPresent(), is(true));
        final Preparation preparation = first.get();

        // then
        assertThat(preparation.id(), is(preparationId));
        assertThat(preparation.getName(), is("test_name"));
        assertThat(preparation.getAuthor(), is(System.getProperty("user.name")));
        assertThat(preparation.getAppVersion(), is(versionService.version().getVersionId()));
    }

    @Test
    public void createInFolder() throws Exception {
        // given
        final String path = "/test/create/preparation";
        final Folder folder = folderRepository.addFolder(home.getId(), path);
        assertThat(folderRepository.entries(folder.getId(), PREPARATION).iterator().hasNext(), is(false));

        // when
        final String preparationId = createPreparationWithAPI(
                "{\"name\": \"another_preparation\", \"dataSetId\": \"75368\"}", folder.getId());

        // then
        final Iterator<FolderEntry> iterator = folderRepository.entries(folder.getId(), PREPARATION).iterator();
        assertThat(iterator.hasNext(), is(true));
        final FolderEntry entry = iterator.next();
        assertThat(entry.getContentId(), is(preparationId));
        assertThat(entry.getContentType(), is(PREPARATION));
    }

    @Test
    public void createWithSpecialCharacters() throws Exception {
        // given
        final List<Preparation> list = repository.list(Preparation.class).collect(Collectors.toList());
        assertThat(list.size(), is(0));

        // when
        final String preparationId = createPreparationWithAPI("{\"name\": \"\", \"dataSetId\": \"1234\"}");

        // then
        final Collection<Preparation> preparations = repository.list(Preparation.class)
                .collect(Collectors.toList());
        assertThat(preparationId, is(preparationId));
        assertThat(preparations.size(), is(1));

        final Preparation preparation = preparations.iterator().next();
        assertThat(preparation.id(), is(preparationId));
        assertThat(preparation.getName(), is(""));
    }

    @Test
    public void delete() throws Exception {
        // given
        assertThat(repository.list(Preparation.class).count(), is(0L));

        final String preparationId = createPreparationWithAPI(
                "{\"name\": \"test_name\", \"dataSetId\": \"1234\", \"rowMetadata\":{\"columns\":[]}}}}");
        final Collection<Preparation> preparations = repository.list(Preparation.class)
                .collect(Collectors.toList());
        assertThat(repository.list(Preparation.class).count(), is(1L));

        final Preparation preparation = preparations.iterator().next();
        assertThat(preparation.id(), is(preparationId));
        assertThat(preparation.getName(), is("test_name"));
        assertThat(folderRepository.findFolderEntries(preparationId, PREPARATION).iterator().hasNext(), is(true));

        // when
        when().delete("/preparations/{id}", preparationId).then().statusCode(HttpStatus.OK.value());

        // then
        assertThat(repository.list(Preparation.class).count(), is(0L));
        assertThat(folderRepository.findFolderEntries(preparationId, PREPARATION).iterator().hasNext(), is(false));
    }

    @Test
    public void testDeleteCleanUp() throws Exception {
        // given
        assertThat(repository.list(Preparation.class).count(), is(0L));
        final String preparationId = createPreparationWithAPI(
                "{\"name\": \"test_name\", \"dataSetId\": \"1234\", \"rowMetadata\":{\"columns\":[]}}}}");
        applyTransformation(preparationId, "actions/append_copy_lastname.json");

        assertThat(repository.list(Preparation.class).count(), is(1L));
        assertThat(repository.list(Step.class).count(), is(2L));
        assertThat(repository.list(PreparationActions.class).count(), is(2L));

        // when
        when().delete("/preparations/{id}", preparationId).then().statusCode(HttpStatus.OK.value());

        // then
        assertThat(repository.list(Preparation.class).count(), is(0L));
        assertThat(repository.list(Step.class).count(), is(1L));
        assertThat(repository.list(PreparationActions.class).count(), is(1L));
    }

    @Test
    public void testDeleteCleanUpWithSharedSteps() throws Exception {
        // given
        assertThat(repository.list(Preparation.class).count(), is(0L));
        final String preparationId1 = createPreparationWithAPI(
                "{\"name\": \"test_name\", \"dataSetId\": \"1234\", \"rowMetadata\":{\"columns\":[]}}}}");
        applyTransformation(preparationId1, "actions/append_copy_lastname.json");
        final String preparationId2 = createPreparationWithAPI(
                "{\"name\": \"test_name\", \"dataSetId\": \"1234\", \"rowMetadata\":{\"columns\":[]}}}");
        applyTransformation(preparationId2, "actions/append_copy_lastname.json");

        assertThat(repository.list(Preparation.class).count(), is(2L));
        assertThat(repository.list(Step.class).count(), is(3L));
        assertThat(repository.list(PreparationActions.class).count(), is(2L));

        // when
        when().delete("/preparations/{id}", preparationId1).then().statusCode(HttpStatus.OK.value());

        // then
        assertThat(repository.list(Preparation.class).count(), is(1L));
        assertThat(repository.list(Step.class).count(), is(2L));
        assertThat(repository.list(PreparationActions.class).count(), is(1L));
    }

    @Test
    public void update() throws Exception {
        assertThat(repository.list(Preparation.class).count(), is(0L));
        final String preparationId = createPreparationWithAPI(
                "{\"name\": \"test_name\", \"dataSetId\": \"1234\", \"rowMetadata\":{\"columns\":[]}}}}");

        final Preparation createdPreparation = repository.list(Preparation.class).iterator().next();
        assertThat(createdPreparation.getId(), is(preparationId));
        final long oldModificationDate = createdPreparation.getLastModificationDate();

        // Test preparation details update
        final String updatedId = given().contentType(ContentType.JSON) //
                .body("{\"name\": \"test_name_updated\", \"dataSetId\": \"1234\"}") //
                .when() //
                .put("/preparations/{id}", preparationId) //
                .asString();

        // Preparation id should not change (name is not part of preparation id).
        assertThat(updatedId, is(preparationId));
        final Collection<Preparation> preparations = repository.list(Preparation.class)
                .collect(Collectors.toList());
        assertThat(preparations.size(), is(1));
        final Preparation preparation = preparations.iterator().next();
        assertThat(preparation.getName(), is("test_name_updated"));
        assertThat(preparation.getLastModificationDate(), is(greaterThan(oldModificationDate)));
        assertThat(preparation.getAppVersion(), is(versionService.version().getVersionId()));
    }

    @Test
    public void updateWithSpecialArguments() throws Exception {
        // given
        final Preparation createdPreparation = new Preparation("#123", versionService.version().getVersionId());
        createdPreparation.setDataSetId("1234");
        createdPreparation.setName("test_name");

        final String preparationId = createPreparationWithAPI(
                mapper.writer().writeValueAsString(createdPreparation));

        // when
        final String updatedId = given().contentType(ContentType.JSON)
                .body("{\"name\": \"\", \"dataSetId\": \"1234\"}".getBytes("UTF-8")).when()
                .put("/preparations/{id}", preparationId).asString();

        // then
        // Preparation id should not change (new name)
        assertThat(updatedId, is(preparationId));
        final Collection<Preparation> preparations = repository.list(Preparation.class)
                .collect(Collectors.toList());
        assertThat(preparations.size(), is(1));
        final Preparation preparation = preparations.iterator().next();
        assertThat(preparation.id(), is(updatedId));
        assertThat(preparation.getName(), is(""));
    }

    @Test
    public void shouldChangePreparationHead() throws Exception {
        // given
        final String preparationId = createPreparation("1234", "my preparation");
        final String firstStepId = applyTransformation(preparationId, "actions/append_upper_case.json");
        final String secondStepId = applyTransformation(preparationId, "actions/append_lower_case.json");

        Preparation preparation = repository.get(preparationId, Preparation.class);
        assertThat(preparation.getHeadId(), is(secondStepId));

        // when
        given().when()//
                .put("/preparations/{id}/head/{stepId}", preparationId, firstStepId)//
                .then()//
                .statusCode(200);

        // then
        preparation = repository.get(preparationId, Preparation.class);
        assertThat(preparation.getHeadId(), is(firstStepId));
    }

    @Test
    public void shouldThrowExceptionOnPreparationHeadChangeWithUnknownStep() throws Exception {
        // given
        final String preparationId = createPreparation("1234", "my preparation");
        final String firstStepId = applyTransformation(preparationId, "actions/append_upper_case.json");

        Preparation preparation = repository.get(preparationId, Preparation.class);
        assertThat(preparation.getHeadId(), is(firstStepId));

        // when
        final Response response = given().when()//
                .put("/preparations/{id}/head/{stepId}", preparationId, "unknown_step_id");

        // then
        response.then()//
                .statusCode(404)//
                .assertThat()//
                .body("code", is("TDP_PS_PREPARATION_STEP_DOES_NOT_EXIST"));
    }

    // -----------------------------------------------------------------------------------------------------------------
    // --------------------------------------------------APPEND STEP----------------------------------------------------
    // -----------------------------------------------------------------------------------------------------------------
    @Test
    public void shouldAddActionStepAfterHead() throws Exception {
        // given
        final String preparationId = createPreparation("1234", "my preparation");
        Preparation preparation = repository.get(preparationId, Preparation.class);
        final long oldModificationDate = preparation.getLastModificationDate();

        assertThat(preparation.getHeadId(), is(rootStep.getId()));

        // when
        applyTransformation(preparationId, "actions/append_copy_lastname.json");

        // then
        preparation = repository.get(preparation.id(), Preparation.class);
        assertThat(preparation.getLastModificationDate(), is(greaterThan(oldModificationDate)));

        final Step head = repository.get(preparation.getHeadId(), Step.class);
        final PreparationActions headAction = repository.get(head.getContent().id(), PreparationActions.class);
        assertThat(headAction.getActions(), hasSize(1));
        assertThat(headAction.getActions().get(0).getName(), is("copy"));
    }

    @Test
    public void shouldAddMultipleActionStepAfterHead() throws Exception {
        // given
        final String preparationId = createPreparation("1234", "my preparation");
        Preparation preparation = repository.get(preparationId, Preparation.class);
        final long oldModificationDate = preparation.getLastModificationDate();

        assertThat(preparation.getHeadId(), is(rootStep.getId()));

        // when
        applyTransformation(preparationId, "actions/append_multi_upper_case.json");

        // then
        preparation = repository.get(preparation.id(), Preparation.class);
        assertThat(preparation.getLastModificationDate(), is(greaterThan(oldModificationDate)));

        final Step head = repository.get(preparation.getHeadId(), Step.class);
        final Step lastBeforeHead = repository.get(head.getParent().id(), Step.class);
        final PreparationActions headAction = repository.get(head.getContent().id(), PreparationActions.class);
        final PreparationActions lastBeforeHeadAction = repository.get(lastBeforeHead.getContent().id(),
                PreparationActions.class);

        // first step : contains only uppercase on lastname
        assertThat(lastBeforeHeadAction.getActions(), hasSize(1));
        assertThat(lastBeforeHeadAction.getActions().get(0).getName(), is("uppercase"));
        assertThat(lastBeforeHeadAction.getActions().get(0).getParameters().get("column_name"), is("lastname"));

        // second step : contains first step actions + uppercase on firstname
        assertThat(headAction.getActions(), hasSize(2));
        assertThat(headAction.getActions().get(0).getName(), is("uppercase"));
        assertThat(headAction.getActions().get(0).getParameters().get("column_name"), is("lastname"));
        assertThat(headAction.getActions().get(1).getName(), is("uppercase"));
        assertThat(headAction.getActions().get(1).getParameters().get("column_name"), is("firstname"));
    }

    @Test
    public void shouldAddActionWithFilterStepAfterHead() throws Exception {
        // given
        final String preparationId = createPreparation("1234", "my preparation");
        Preparation preparation = repository.get(preparationId, Preparation.class);
        final long oldModificationDate = preparation.getLastModificationDate();

        assertThat(preparation.getHeadId(), is(rootStep.getId()));

        // when
        applyTransformation(preparationId, "actions/append_copy_lastname_filter.json");

        // then
        preparation = repository.get(preparation.id(), Preparation.class);
        assertThat(preparation.getLastModificationDate(), is(greaterThan(oldModificationDate)));

        final Step head = repository.get(preparation.getHeadId(), Step.class);
        final PreparationActions headAction = repository.get(head.getContent().id(), PreparationActions.class);
        assertThat(headAction.getActions(), hasSize(1));
        final Action copyAction = headAction.getActions().get(0);
        assertThat(copyAction.getName(), is("copy"));
        assertThat(copyAction.getParameters().get(ImplicitParameters.FILTER.getKey()),
                is("{\"eq\":{\"field\":\"0001\",\"value\":\"value\"}}"));
    }

    @Test
    public void shouldSaveStepDiff() throws Exception {
        // given
        final String preparationId = createPreparation("1234", "my preparation");
        final Preparation preparation = repository.get(preparationId, Preparation.class);

        assertThat(preparation.getHeadId(), is(rootStep.getId()));

        // when
        applyTransformation(preparationId, "actions/append_copy_lastname.json");

        // then
        Optional<Step> first = repository.list(Step.class) //
                .filter(s -> s.getParent() != null && Objects.equals(s.getParent().id(), rootStep.getId())) //
                .findFirst();

        assertTrue(first.isPresent());

        final Step head = first.get();
        assertThat(head.getParent().id(), is(rootStep.getId()));
        assertThat(head.getDiff().getCreatedColumns(), hasSize(1));
        assertThat(head.getDiff().getCreatedColumns(), hasItem("0004"));
    }

    @Test
    public void shouldReturnErrorWhenScopeIsNotConsistentOnAppendTransformation() throws Exception {
        // given
        final String preparationId = createPreparation("1234", "my preparation");

        // when
        final Response request = given()
                .body(IOUtils.toString(PreparationControllerTest.class
                        .getResourceAsStream("error/incomplete_transformation_list_params.json")))//
                .contentType(ContentType.JSON)//
                .when()//
                .post("/preparations/{id}/actions", preparationId);

        // then
        request.then()//
                .statusCode(400)//
                .assertThat()//
                .body("code", is("TDP_BASE_MISSING_ACTION_SCOPE"));
    }

    // -----------------------------------------------------------------------------------------------------------------
    // --------------------------------------------------UPDATE STEP----------------------------------------------------
    // -----------------------------------------------------------------------------------------------------------------
    @Test
    public void shouldModifySingleAction() throws Exception {
        // given
        final String preparationId = createPreparation("1234", "my preparation");
        final String firstStepId = applyTransformation(preparationId, "actions/append_upper_case.json");

        Preparation preparation = repository.get(preparationId, Preparation.class);
        final long oldModificationDate = preparation.getLastModificationDate();

        // when
        given().body(IOUtils.toString(
                PreparationControllerTest.class.getResourceAsStream("actions/append_update_upper_case.json")))//
                .contentType(ContentType.JSON)//
                .when()//
                .put("/preparations/{id}/actions/{action}", preparationId, firstStepId);

        // then
        preparation = repository.get(preparationId, Preparation.class);
        assertThat(preparation.getLastModificationDate(), is(greaterThan(oldModificationDate)));

        final Step head = repository.get(preparation.getHeadId(), Step.class);
        assertThat(head.getParent().id(), is(rootStep.getId()));
    }

    @Test
    public void shouldModifyLastAction() throws Exception {
        // Initial preparation
        final String preparationId = createPreparation("1234", "my preparation");
        final String firstStepId = applyTransformation(preparationId, "actions/append_upper_case.json");
        final String secondStepId = applyTransformation(preparationId, "actions/append_lower_case.json");

        Preparation preparation = repository.get(preparationId, Preparation.class);
        final long oldModificationDate = preparation.getLastModificationDate();

        // when : update second (last) step
        given().body(IOUtils.toString(
                PreparationControllerTest.class.getResourceAsStream("actions/append_update_upper_case.json")))
                .contentType(ContentType.JSON).when()
                .put("/preparations/{id}/actions/{action}", preparationId, secondStepId);

        // then
        preparation = repository.get(preparationId, Preparation.class);
        assertThat(preparation.getLastModificationDate(), is(greaterThan(oldModificationDate)));

        final Step head = repository.get(preparation.getHeadId(), Step.class);
        assertThat(head.getParent().id(), is(firstStepId));
    }

    @Test
    public void shouldModifyFirstAction() throws Exception {
        // given
        final String preparationId = createPreparation("1234", "my preparation");
        final String firstStepId = applyTransformation(preparationId, "actions/append_upper_case.json");
        final String secondStepId = applyTransformation(preparationId, "actions/append_lower_case.json");

        Preparation preparation = repository.get(preparationId, Preparation.class);
        final long oldModificationDate = preparation.getLastModificationDate();

        // when
        given().body(IOUtils.toString(
                PreparationControllerTest.class.getResourceAsStream("actions/append_update_upper_case.json")))
                .contentType(ContentType.JSON).when().put("/preparations/{id}/actions/{action}", preparation.id(),
                        "a41184275b046d86c8d98d413ed019bc0a7f3c49");

        // then
        preparation = repository.get(preparationId, Preparation.class);
        assertThat(preparation.getHeadId(), is(secondStepId));
        assertThat(preparation.getLastModificationDate(), is(greaterThanOrEqualTo(oldModificationDate)));

        final Step head = repository.get(secondStepId, Step.class);
        assertThat(head.getParent().id(), is(firstStepId));

        final Step first = repository.get(firstStepId, Step.class);
        assertThat(first.getParent().id(), is(rootStep.getId()));
    }

    @Test
    public void updateAction_shouldSaveUpdatedStepDiffAndShiftColumnsIds() throws Exception {
        // given
        final String preparationId = createPreparation("1234", "my preparation");

        AppendStep firstStep = step(null, action("copy", paramsColAction("0003", "birth")));
        appendStepsToPrep(preparationId, firstStep);
        AppendStep secondStep = step(null, action("split",
                params("column_id", "0001", "column_name", "firstname", "scope", "column", "limit", "2")));
        appendStepsToPrep(preparationId, secondStep);

        final Preparation preparation = repository.get(preparationId, Preparation.class);
        final List<String> stepIds = preparationUtils.listStepsIds(preparation.getHeadId(), repository);
        assertEquals(3, stepIds.size());
        assertEquals(singletonList("0004"),
                repository.get(stepIds.get(1), Step.class).getDiff().getCreatedColumns());
        assertEquals(asList("0005", "0006"),
                repository.get(stepIds.get(2), Step.class).getDiff().getCreatedColumns());

        // when : +1 column
        given().body(step(diff("0004", "0005", "0006"), action("split", paramsColAction("0001", "firstname")))) //
                .contentType(ContentType.JSON) //
                .when() //
                .put("/preparations/{id}/actions/{action}", preparationId, stepIds.get(1));

        // then
        final String newHeadId = repository.get(preparationId, Preparation.class).getHeadId();
        final List<String> stepIdsAfter = preparationUtils.listStepsIds(newHeadId, repository);
        assertThatStepHasCreatedColumns(stepIdsAfter.get(1), "0004", "0005", "0006");
        assertThatStepHasCreatedColumns(stepIdsAfter.get(2), "0007", "0008");
    }

    @Test
    public void shouldReturnErrorWhenScopeIsNotConsistentOnTransformationUpdate() throws Exception {
        // given
        final String preparationId = createPreparation("1234", "my preparation");
        final String stepId = applyTransformation(preparationId, "actions/append_upper_case.json");

        // when
        final Response request = given()
                .body(IOUtils.toString(PreparationControllerTest.class
                        .getResourceAsStream("error/incomplete_transformation_params.json")))//
                .contentType(ContentType.JSON)//
                .when()//
                .put("/preparations/{id}/actions/{action}", preparationId, stepId);

        // then
        request.then()//
                .statusCode(400)//
                .assertThat()//
                .body("code", is("TDP_BASE_MISSING_ACTION_SCOPE"));
    }

    // -----------------------------------------------------------------------------------------------------------------
    // --------------------------------------------------DELETE STEP----------------------------------------------------
    // -----------------------------------------------------------------------------------------------------------------
    @Test
    public void shouldDeleteSingleStep() throws Exception {
        // given
        final String preparationId = createPreparation("1234", "My preparation");
        final String firstStepId = applyTransformation(preparationId, "actions/append_upper_case.json");
        final String secondStepId = applyTransformation(preparationId, "actions/append_lower_case.json");

        Step head = repository.get(secondStepId, Step.class);
        PreparationActions headActions = repository.get(head.getContent().id(), PreparationActions.class);
        assertThat(headActions.getActions(), hasSize(2));
        assertThat(headActions.getActions().get(0).getName(), is("uppercase"));
        assertThat(headActions.getActions().get(1).getName(), is("lowercase"));

        // when : delete second step in single mode
        when().delete("/preparations/{id}/actions/{action}", preparationId, firstStepId)//
                .then()//
                .statusCode(200);

        // then
        final Preparation preparation = repository.get(preparationId, Preparation.class);
        final String headId = preparation.getHeadId();

        head = repository.get(headId, Step.class);
        headActions = repository.get(head.getContent().id(), PreparationActions.class);
        assertThat(headActions.getActions(), hasSize(1));
        assertThat(headActions.getActions().get(0).getName(), is("lowercase"));
    }

    @Test
    public void shouldDeleteStepsThatApplyToCreatedColumn() throws Exception {
        // given
        final String preparationId = createPreparation("1234", "My preparation");
        appendStepsToPrep(preparationId, step(null, action("copy", paramsColAction("0001", "lastname"))));
        appendStepsToPrep(preparationId, step(null, action("uppercase", paramsColAction("0004", "lastname_copy"))));

        final Preparation preparation = repository.get(preparationId, Preparation.class);
        final List<String> stepIds = preparationUtils.listStepsIds(preparation.getHeadId(), repository);
        assertEquals(3, stepIds.size());

        // when : delete second step in single mode
        when().delete("/preparations/{id}/actions/{action}", preparationId, stepIds.get(1))//
                .then()//
                .statusCode(200);

        // then
        final Preparation preparationAfter = repository.get(preparationId, Preparation.class);
        final List<String> stepIdsAfter = preparationUtils.listStepsIds(preparationAfter.getHeadId(), repository);
        assertEquals(1, stepIdsAfter.size());
    }

    @Test
    public void shouldShiftColumnCreatedAfterStepWithAllActionsParametersOnThoseSteps() throws Exception {
        // given
        final String preparationId = createPreparation("1234", "My preparation");
        appendStepsToPrep(preparationId, step(null, action("copy", paramsColAction("0001", "lastname"))));
        appendStepsToPrep(preparationId, step(null, action("copy", paramsColAction("0001", "lastname"))));
        appendStepsToPrep(preparationId, step(null, action("uppercase", paramsColAction("0005", "lastname_copy"))));

        final Preparation preparation = repository.get(preparationId, Preparation.class);
        final List<String> stepIds = preparationUtils.listStepsIds(preparation.getHeadId(), repository);
        assertEquals(4, stepIds.size());

        // when : delete second step in single mode
        when().delete("/preparations/{id}/actions/{action}", preparationId, stepIds.get(1))//
                .then()//
                .statusCode(200);

        // then
        final Preparation preparationAfter = repository.get(preparationId, Preparation.class);
        final String headId = preparationAfter.getHeadId();
        Step head = repository.get(headId, Step.class);
        PreparationActions headActions = repository.get(head.getContent().id(), PreparationActions.class);
        assertThat(headActions.getActions(), hasSize(2));
        Action secondCopyAction = headActions.getActions().get(0);
        assertThat(secondCopyAction.getName(), is("copy"));
        assertThat(secondCopyAction.getParameters().get("column_id"), is("0001"));
        Action upperCaseAction = headActions.getActions().get(1);
        assertThat(upperCaseAction.getName(), is("uppercase"));
        assertThat(upperCaseAction.getParameters().get("column_id"), is("0004"));
    }

    @Test
    public void shouldThrowExceptionWhenStepToDeleteIsRoot() throws Exception {
        // given
        final String preparationId = createPreparation("1234", "My preparation");
        applyTransformation(preparationId, "actions/append_upper_case.json");
        applyTransformation(preparationId, "actions/append_lower_case.json");

        // when: delete ROOT
        final Response response = when().delete("/preparations/{id}/actions/{action}", preparationId,
                rootStep.getId());

        // then
        response.then().statusCode(403).assertThat().body("code",
                is("TDP_PS_PREPARATION_ROOT_STEP_CANNOT_BE_DELETED"));
    }

    @Test
    public void shouldThrowExceptionWhenStepDoesntExist() throws Exception {
        // given
        final String preparationId = createPreparation("1234", "My preparation");
        applyTransformation(preparationId, "actions/append_upper_case.json");
        applyTransformation(preparationId, "actions/append_lower_case.json");

        // when : delete unknown step
        final Response response = when().delete("/preparations/{id}/actions/{action}", preparationId, "azerty");

        // then
        response.then().statusCode(404).assertThat().body("code", is("TDP_PS_PREPARATION_STEP_DOES_NOT_EXIST"));
    }

    @Test
    public void shouldUpdateModificationDateOnDeleteStep() throws Exception {
        // given
        final String preparationId = createPreparation("1234", "My preparation");
        applyTransformation(preparationId, "actions/append_upper_case.json");
        final String secondStepId = applyTransformation(preparationId, "actions/append_lower_case.json");

        Preparation preparation = repository.get(preparationId, Preparation.class);
        final long oldModificationDate = preparation.getLastModificationDate();

        // when: delete LOWERCASE
        when().delete("/preparations/{id}/actions/{action}", preparation.id(), secondStepId);

        // then
        preparation = repository.get(preparation.id(), Preparation.class);
        assertThat(oldModificationDate, lessThan(preparation.getLastModificationDate()));
    }

    // -----------------------------------------------------------------------------------------------------------------
    // ------------------------------------------------STEPS REORDERING-------------------------------------------------
    // -----------------------------------------------------------------------------------------------------------------

    @Test
    public void shouldReturnANotFoundForNonExistingPreparation() {
        // @formatter:off
        given().queryParam("parentStepId", rootStep.getId()).when()
                .post("/preparations/{id}/steps/{stepId}/order", "DoesNotExist", rootStep.id())//
                .then()//
                .statusCode(404);
        // @formatter:on
    }

    @Test
    public void shouldReturnANotFoundForNonExistingStep() {
        final String preparationId = createPreparation("1234", "My preparation");
        // @formatter:off
        given().queryParam("parentStepId", rootStep.getId()).when()
                .post("/preparations/{id}/steps/{stepId}/order", preparationId, "DoesNotExist")//
                .then()//
                .statusCode(404);
        // @formatter:on
    }

    @Test
    public void shouldMoveAStepToFirstPositionIfLegal() throws Exception {
        // given
        final String preparationId = createPreparation("1234", "My preparation");
        applyTransformation(preparationId, "actions/append_upper_case.json");
        final String copyStepId = applyTransformation(preparationId, "actions/append_copy_lastname.json");
        applyTransformation(preparationId, "actions/append_rename_copy_lastname.json");
        final String headStepId = applyTransformation(preparationId, "actions/append_lower_case.json");

        Step head = repository.get(headStepId, Step.class);
        PreparationActions headActions = repository.get(head.getContent().id(), PreparationActions.class);
        assertThat(headActions.getActions(), hasSize(4));
        assertThat(headActions.getActions().get(0).getName(), is("uppercase"));
        assertThat(headActions.getActions().get(1).getName(), is("copy"));
        assertThat(headActions.getActions().get(2).getName(), is("rename_column"));
        assertThat(headActions.getActions().get(3).getName(), is("lowercase"));

        // @formatter:off
        given().queryParam("parentStepId", rootStep.getId()).when()
                .post("/preparations/{id}/steps/{stepId}/order", preparationId, copyStepId)//
                .then()//
                .statusCode(200);
        // @formatter:on
        // then
        final Preparation preparation = repository.get(preparationId, Preparation.class);
        final String newHeadStepId = preparation.getHeadId();

        head = repository.get(newHeadStepId, Step.class);
        headActions = repository.get(head.getContent().id(), PreparationActions.class);
        assertThat(headActions.getActions(), hasSize(4));
        assertThat(headActions.getActions().get(0).getName(), is("copy"));
        assertThat(headActions.getActions().get(1).getName(), is("uppercase"));
        assertThat(headActions.getActions().get(2).getName(), is("rename_column"));
        assertThat(headActions.getActions().get(3).getName(), is("lowercase"));
    }

    @Test
    public void shouldMoveAStepToLastPositionIfLegal() throws Exception {
        // given
        final String preparationId = createPreparation("1234", "My preparation");
        final String upperCaseStepId = applyTransformation(preparationId, "actions/append_upper_case.json");
        applyTransformation(preparationId, "actions/append_copy_lastname.json");
        applyTransformation(preparationId, "actions/append_rename_copy_lastname.json");
        final String headStepId = applyTransformation(preparationId, "actions/append_lower_case.json");

        Step head = repository.get(headStepId, Step.class);
        PreparationActions headActions = repository.get(head.getContent().id(), PreparationActions.class);
        assertThat(headActions.getActions(), hasSize(4));
        assertThat(headActions.getActions().get(0).getName(), is("uppercase"));
        assertThat(headActions.getActions().get(1).getName(), is("copy"));
        assertThat(headActions.getActions().get(2).getName(), is("rename_column"));
        assertThat(headActions.getActions().get(3).getName(), is("lowercase"));

        // when : delete second step in single mode
        // @formatter:off
        given().queryParam("parentStepId", headStepId).when()
                .post("/preparations/{id}/steps/{stepId}/order", preparationId, upperCaseStepId)//
                .then()//
                .statusCode(200);
        // @formatter:on
        // then
        final Preparation preparation = repository.get(preparationId, Preparation.class);
        final String newHeadStepId = preparation.getHeadId();

        head = repository.get(newHeadStepId, Step.class);
        headActions = repository.get(head.getContent().id(), PreparationActions.class);
        assertThat(headActions.getActions(), hasSize(4));

        assertThat(headActions.getActions().get(0).getName(), is("copy"));
        assertThat(headActions.getActions().get(1).getName(), is("rename_column"));
        assertThat(headActions.getActions().get(2).getName(), is("lowercase"));
        assertThat(headActions.getActions().get(3).getName(), is("uppercase"));
    }

    @Test
    public void shouldMoveAStepToNonBoundaryPositionIfLegal() throws Exception {
        // given
        final String preparationId = createPreparation("1234", "My preparation");
        final String upperCaseStepId = applyTransformation(preparationId, "actions/append_upper_case.json");
        final String copyStepId = applyTransformation(preparationId, "actions/append_copy_lastname.json");
        applyTransformation(preparationId, "actions/append_rename_copy_lastname.json");
        final String headStepId = applyTransformation(preparationId, "actions/append_lower_case.json");

        Step head = repository.get(headStepId, Step.class);
        PreparationActions headActions = repository.get(head.getContent().id(), PreparationActions.class);
        assertThat(headActions.getActions(), hasSize(4));
        assertThat(headActions.getActions().get(0).getName(), is("uppercase"));
        assertThat(headActions.getActions().get(1).getName(), is("copy"));
        assertThat(headActions.getActions().get(2).getName(), is("rename_column"));
        assertThat(headActions.getActions().get(3).getName(), is("lowercase"));

        // when : delete second step in single mode
        // @formatter:off
        given().queryParam("parentStepId", copyStepId).when()
                .post("/preparations/{id}/steps/{stepId}/order", preparationId, upperCaseStepId)//
                .then()//
                .statusCode(200);
        // @formatter:on
        // then
        final Preparation preparation = repository.get(preparationId, Preparation.class);
        final String newHeadStepId = preparation.getHeadId();

        head = repository.get(newHeadStepId, Step.class);
        headActions = repository.get(head.getContent().id(), PreparationActions.class);
        assertThat(headActions.getActions(), hasSize(4));

        assertThat(headActions.getActions().get(0).getName(), is("copy"));
        assertThat(headActions.getActions().get(1).getName(), is("uppercase"));
        assertThat(headActions.getActions().get(2).getName(), is("rename_column"));
        assertThat(headActions.getActions().get(3).getName(), is("lowercase"));
    }

    @Test
    public void shouldNotMoveAStepIfItUseANotYetCreatedColumn() throws Exception {
        // given
        final String preparationId = createPreparation("1234", "My preparation");
        appendStepsToPrep(preparationId, step(null, action("copy", paramsColAction("0001", "lastname"))));
        appendStepsToPrep(preparationId, step(null, action("copy", paramsColAction("0001", "lastname"))));
        appendStepsToPrep(preparationId, step(null, action("uppercase", paramsColAction("0005", "lastname_copy"))));

        final Preparation preparation = repository.get(preparationId, Preparation.class);
        final List<String> stepIds = preparationUtils.listStepsIds(preparation.getHeadId(), repository);
        assertEquals(4, stepIds.size());

        // when : delete second step in single mode
        given().queryParam("parentStepId", stepIds.get(1)).when()
                .post("/preparations/{id}/steps/{stepId}/order", preparationId, stepIds.get(3))//
                .then().statusCode(403);

        final Preparation preparationAfter = repository.get(preparationId, Preparation.class);
        final String newHeadStepId = preparationAfter.getHeadId();

        Step head = repository.get(newHeadStepId, Step.class);
        PreparationActions headActions = repository.get(head.getContent().id(), PreparationActions.class);
        assertThat(headActions.getActions(), hasSize(3));

        assertThat(headActions.getActions().get(0).getName(), is("copy"));
        assertThat(headActions.getActions().get(1).getName(), is("copy"));
        assertThat(headActions.getActions().get(2).getName(), is("uppercase"));
    }

    @Test
    public void shouldNotMoveAStepIfItUsesADeletedColumn() throws Exception {
        // given
        final String preparationId = createPreparation("1234", "My preparation");
        appendStepsToPrep(preparationId, step(null, action("uppercase", paramsColAction("0001", "lastname"))));
        appendStepsToPrep(preparationId, step(null, action("delete_column", paramsColAction("0001", "lastname"))));

        final Preparation preparation = repository.get(preparationId, Preparation.class);
        final List<String> stepIds = preparationUtils.listStepsIds(preparation.getHeadId(), repository);
        assertEquals(3, stepIds.size());

        // when : delete second step in single mode
        given().queryParam("parentStepId", stepIds.get(0)).when()
                .post("/preparations/{id}/steps/{stepId}/order", preparationId, stepIds.get(2))//
                .then().statusCode(403);

        final Preparation preparationAfter = repository.get(preparationId, Preparation.class);
        final String newHeadStepId = preparationAfter.getHeadId();

        Step head = repository.get(newHeadStepId, Step.class);
        PreparationActions headActions = repository.get(head.getContent().id(), PreparationActions.class);
        assertThat(headActions.getActions(), hasSize(2));

        assertThat(headActions.getActions().get(0).getName(), is("uppercase"));
        assertThat(headActions.getActions().get(1).getName(), is("delete_column"));
    }

    // -----------------------------------------------------------------------------------------------------------------
    // ----------------------------------------------------CHECKS-------------------------------------------------------
    // -----------------------------------------------------------------------------------------------------------------
    @Test
    public void shouldReturnHTTP404WhenNoPreparationUseDataset() throws Exception {
        // given
        final String datasetId = "3214a6748bc4f9674c85";
        final String preparationId = createPreparation("other_dataset", "My preparation");
        applyTransformation(preparationId, "actions/append_upper_case.json");

        repository.get(preparationId, Preparation.class);

        // when
        final Response response = when().head("/preparations/use/dataset/{id}", datasetId);

        // then
        assertThat(response.getStatusCode(), is(404));
    }

    @Test
    public void shouldReturnHTTP203WhenPreparationIsOnDataset() throws Exception {
        // given
        final String datasetId = "3214a6748bc4f9674c85";
        final String preparationId = createPreparation(datasetId, "My preparation");
        applyTransformation(preparationId, "actions/append_upper_case.json");

        repository.get(preparationId, Preparation.class);

        // when
        final Response response = when().head("/preparations/use/dataset/{id}", datasetId);

        // then
        assertThat(response.getStatusCode(), is(204));
    }

    @Test
    @Ignore("This test is flawed, it may be corrected but this behavior is already tested through PreparationAPI.")
    // TODO: this test is not testing what it says it is testing:
    // The 204 comes from the use of the dataset as preparation base, not in lookup
    public void shouldReturnHTTP204WhenPreparationHasLookupOnDataset() throws Exception {
        // given
        final String datasetId = "3214a6748bc4f9674c85";
        final String preparationId = createPreparation(datasetId, "My preparation");
        applyTransformation(preparationId, "actions/append_upper_case.json");

        final String lookupDatasetId = "687468453436851";
        final Map<String, String> parametersOnDataset = new HashMap<>();
        parametersOnDataset.put("lookup_ds_id", lookupDatasetId);
        final Map<String, String> parametersWithoutDataset = new HashMap<>();
        parametersWithoutDataset.put("other", "other");

        final List<Action> action1 = singletonList(
                Action.Builder.builder().withParameters(parametersWithoutDataset).build());
        final List<Action> action2 = singletonList(
                Action.Builder.builder().withParameters(parametersOnDataset).build());

        final PreparationActions prepAction1 = new PreparationActions().append(action1);
        final PreparationActions prepAction2 = new PreparationActions().append(action2);

        repository.add(prepAction1);
        repository.add(prepAction2);

        // when
        final Response response = when().head("/preparations/use/dataset/{id}", lookupDatasetId);

        // then
        assertThat(response.getStatusCode(), is(204));
    }

    @Test
    public void preparationsThatUseDataset_shouldDeleteDatasetIfUsedInLookupNotUsed() throws Exception {
        // given
        final String datasetId = "3214a6748bc4f9674c85";
        final String preparationId = createPreparation(datasetId, "My preparation");
        applyTransformation(preparationId, "actions/append_upper_case.json");

        final String lookupDatasetId = "687468453436851";
        final Map<String, String> parametersOnDataset = new HashMap<>();
        parametersOnDataset.put("lookup_ds_id", lookupDatasetId);
        final Map<String, String> parametersWithoutDataset = new HashMap<>();
        parametersWithoutDataset.put("other", "other");

        final List<Action> action1 = singletonList(
                Action.Builder.builder().withParameters(parametersWithoutDataset).build());
        final List<Action> action2 = singletonList(
                Action.Builder.builder().withParameters(parametersOnDataset).build());

        final PreparationActions prepAction1 = new PreparationActions().append(action1);
        final PreparationActions prepAction2 = new PreparationActions().append(action2);

        repository.add(prepAction1);
        repository.add(prepAction2);

        // when
        final Response response = when().head("/preparations/use/dataset/{id}", lookupDatasetId);

        // then
        // Because the lookup action is not used in any preparation:
        assertThat(response.getStatusCode(), is(404));
    }

    @Test
    public void shouldMakePreparationNotDistributed() throws Exception {
        // given
        final String preparationId = createPreparation("1234", "my preparation");
        applyTransformation(preparationId, "actions/append_make_line_header.json"); // Make line header can't be distributed
        applyTransformation(preparationId, "actions/append_upper_case.json");
        Preparation preparation = repository.get(preparationId, Preparation.class);

        // when
        final String preparationDetails = when().get("/preparations/{id}/details", preparation.id()).asString();

        // then
        assertThat(JsonPath.given(preparationDetails).get("allowDistributedRun"), is(false));
    }

    // -----------------------------------------------------------------------------------------------------------------
    // -----------------------------------------------------UTILS-------------------------------------------------------
    // -----------------------------------------------------------------------------------------------------------------

    /**
     * Create a new preparation with the given name and creation date to 0.
     *
     * @param datasetId the dataset id related to this preparation.
     * @param name preparation name.
     * @return The preparation id
     */
    private String createPreparation(final String datasetId, final String name) {
        final Preparation preparation = new Preparation(UUID.randomUUID().toString(), datasetId, rootStep.id(),
                versionService.version().getVersionId());
        preparation.setName(name);
        preparation.setCreationDate(0);
        RowMetadata rowMetadata = new RowMetadata();
        ColumnMetadata firstColumn = new ColumnMetadata();
        firstColumn.setId("0001");
        ColumnMetadata secondColumn = new ColumnMetadata();
        secondColumn.setId("0002");
        ColumnMetadata thirdColumn = new ColumnMetadata();
        thirdColumn.setId("0003");
        rowMetadata.setColumns(asList(firstColumn, secondColumn, thirdColumn));
        preparation.setRowMetadata(rowMetadata);
        repository.add(preparation);
        return preparation.id();
    }

    /**
     * Append an action to a preparation
     *
     * @param preparationId The preparation id
     * @param transformationFilePath The transformation json file path
     * @return The created step id
     */
    private String applyTransformation(final String preparationId, final String transformationFilePath)
            throws IOException {
        given().body(IOUtils.toString(PreparationControllerTest.class.getResourceAsStream(transformationFilePath)))//
                .contentType(ContentType.JSON)//
                .when()//
                .post("/preparations/{id}/actions", preparationId)//
                .then()//
                .statusCode(200);

        final Preparation preparation = repository.get(preparationId, Preparation.class);
        return preparation.getHeadId();
    }

    /**
     * Assert that step has exactly the wanted created columns ids
     *
     * @param stepId The step id
     * @param columnsIds The created columns ids
     */
    private void assertThatStepHasCreatedColumns(final String stepId, final String... columnsIds) {
        final Step head = repository.get(stepId, Step.class);
        assertThat(head.getDiff().getCreatedColumns(), hasSize(columnsIds.length));
        for (final String columnId : columnsIds) {
            assertThat(head.getDiff().getCreatedColumns(), hasItem(columnId));
        }
    }

}