/*
 * Copyright 2015 Amazon.com, Inc. or its affiliates. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License").
 * You may not use this file except in compliance with the License.
 * A copy of the License is located at
 *
 *  http://aws.amazon.com/apache2.0
 *
 * or in the "license" file accompanying this file. This file 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 com.amazonaws.codepipeline.jenkinsplugin;

import static com.amazonaws.codepipeline.jenkinsplugin.TestUtils.assertContainsIgnoreCase;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.when;

import java.io.ByteArrayOutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;


import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

import com.amazonaws.codepipeline.jenkinsplugin.CodePipelineStateModel.CompressionType;
import com.amazonaws.services.codepipeline.AWSCodePipeline;
import com.amazonaws.services.codepipeline.model.ActionContext;
import com.amazonaws.services.codepipeline.model.Artifact;
import com.amazonaws.services.codepipeline.model.FailureType;
import com.amazonaws.services.codepipeline.model.Job;
import com.amazonaws.services.codepipeline.model.JobData;
import com.amazonaws.services.codepipeline.model.PipelineContext;
import com.amazonaws.services.codepipeline.model.PutJobFailureResultRequest;
import com.amazonaws.services.codepipeline.model.PutJobSuccessResultRequest;
import com.amazonaws.services.codepipeline.model.StageContext;
import com.amazonaws.services.s3.model.AmazonS3Exception;

import hudson.EnvVars;
import hudson.model.AbstractBuild;
import hudson.model.AbstractProject;
import hudson.model.BuildListener;
import hudson.model.Result;
import hudson.model.TaskListener;
import jenkins.model.Jenkins;
import net.sf.json.JSONArray;
import net.sf.json.JSONObject;

public class AWSCodePipelinePublisherTest {

    private static final String REGION = "us-east-1";
    private static final String ACCESS_KEY = "1234";
    private static final String SECRET_KEY = "4321";
    private static final String PROXY_HOST = "";
    private static final int PROXY_PORT = 0;
    private static final String PLUGIN_VERSION = "aws-codepipeline/unknown jenkins/" + Jenkins.getVersion();
    private static final String BUILD_ID = "34";

    private static final String PUBLISHING_ARTIFACTS_MESSAGE = "[AWS CodePipeline Plugin] Publishing artifacts";
    private static final String PUT_JOB_FAILURE_MESSAGE = "[AWS CodePipeline Plugin] Build failed, calling PutJobFailureResult";
    private static final String PUT_JOB_SUCCESS_MESSAGE = "[AWS CodePipeline Plugin] Build succeeded, calling PutJobSuccessResult";

    @Mock private AWSClientFactory mockFactory;
    @Mock private AWSClients mockAWS;
    @Mock private AWSCodePipeline mockCodePipelineClient;
    @Mock private AbstractBuild mockBuild;
    @Mock private AbstractProject<?, ?> mockProject;
    @Mock private EnvVars vars;
    @Mock private Job mockJob;
    @Mock private JobData mockJobData;

    @Captor private ArgumentCaptor<PutJobSuccessResultRequest> putJobSuccessResultRequest;
    @Captor private ArgumentCaptor<PutJobFailureResultRequest> putJobFailureResultRequest;

    private String jobId;
    private CodePipelineStateModel model;
    private JSONArray outputLocations;
    private ByteArrayOutputStream outContent;

    private AWSCodePipelinePublisherMock publisher;

    @Before
    public void setUp() throws Throwable {
        MockitoAnnotations.initMocks(this);
        outContent =  TestUtils.setOutputStream();

        outputLocations = generateOutputLocations(Arrays.asList("output_1", "output_2"), Arrays.asList("artifact_1", "artifact_2"));

        publisher = new AWSCodePipelinePublisherMock(outputLocations, mockFactory);

        jobId = UUID.randomUUID().toString();

        model = new CodePipelineStateModel();
        model.setJob(mockJob);
        model.setAwsAccessKey(ACCESS_KEY);
        model.setAwsSecretKey(SECRET_KEY);
        model.setRegion(REGION);
        model.setProxyHost(PROXY_HOST);
        model.setProxyPort(PROXY_PORT);

        CodePipelineStateService.setModel(model);

        when(mockFactory.getAwsClient(anyString(), anyString(), anyString(), anyInt(), anyString(), anyString())).thenReturn(mockAWS);
        when(mockJob.getId()).thenReturn(jobId);
        when(mockJob.getData()).thenReturn(mockJobData);

        when(mockAWS.getCodePipelineClient()).thenReturn(mockCodePipelineClient);

        when(mockBuild.getId()).thenReturn(BUILD_ID);
        when(mockBuild.getResult()).thenReturn(Result.SUCCESS);
        when(mockBuild.getProject()).thenReturn(mockProject);
        when(mockBuild.getEnvironment(any(TaskListener.class))).thenReturn(vars);

        when(vars.get(any(String.class))).thenReturn("Project");
        when(mockProject.getName()).thenReturn("Project");
    }

    @Test
    public void putsJobSuccessWhenBuildSucceeds() {
        // given
        when(mockBuild.getResult()).thenReturn(Result.SUCCESS);
        when(mockJobData.getOutputArtifacts()).thenReturn(generateOutputArtifactsWithNames(Arrays.asList("artifact_1", "artifact_2")));

        // when
        assertTrue(publisher.perform(mockBuild, null, null));

        // then
        final InOrder inOrder = inOrder(mockFactory, mockAWS, mockCodePipelineClient);
        inOrder.verify(mockFactory).getAwsClient(ACCESS_KEY, SECRET_KEY, PROXY_HOST, PROXY_PORT, REGION, PLUGIN_VERSION);
        inOrder.verify(mockAWS).getCodePipelineClient();
        inOrder.verify(mockCodePipelineClient).putJobSuccessResult(putJobSuccessResultRequest.capture());

        final PutJobSuccessResultRequest request = putJobSuccessResultRequest.getValue();
        assertEquals(jobId, request.getJobId());
        assertEquals(BUILD_ID, request.getExecutionDetails().getExternalExecutionId());
        assertEquals("Finished", request.getExecutionDetails().getSummary());

        assertContainsIgnoreCase(PUBLISHING_ARTIFACTS_MESSAGE, outContent.toString());
        assertContainsIgnoreCase(PUT_JOB_SUCCESS_MESSAGE, outContent.toString());
    }

    @Test
    public void putsJobSuccessWhenBuildSucceedsWithOneOutputLocation() {
        // given
        when(mockBuild.getResult()).thenReturn(Result.SUCCESS);

        JSONArray outputs = generateOutputLocations(Arrays.asList(""), Arrays.asList(""));
        publisher = new AWSCodePipelinePublisherMock(outputs, mockFactory);

        when(mockJobData.getOutputArtifacts()).thenReturn(generateOutputArtifactsWithNames(Arrays.asList("artifact_1")));

        // when
        assertTrue(publisher.perform(mockBuild, null, null));

        // then
        final InOrder inOrder = inOrder(mockFactory, mockAWS, mockCodePipelineClient);
        inOrder.verify(mockFactory).getAwsClient(ACCESS_KEY, SECRET_KEY, PROXY_HOST, PROXY_PORT, REGION, PLUGIN_VERSION);
        inOrder.verify(mockAWS).getCodePipelineClient();
        inOrder.verify(mockCodePipelineClient).putJobSuccessResult(putJobSuccessResultRequest.capture());

        final PutJobSuccessResultRequest request = putJobSuccessResultRequest.getValue();
        assertEquals(jobId, request.getJobId());
        assertEquals(BUILD_ID, request.getExecutionDetails().getExternalExecutionId());
        assertEquals("Finished", request.getExecutionDetails().getSummary());

        assertContainsIgnoreCase(PUBLISHING_ARTIFACTS_MESSAGE, outContent.toString());
        assertContainsIgnoreCase(PUT_JOB_SUCCESS_MESSAGE, outContent.toString());
    }

    @Test
    public void putsJobSuccessWhenBuildSucceedsWithNoLocationsSpecified() {
        // given
        when(mockBuild.getResult()).thenReturn(Result.SUCCESS);

        JSONArray outputs = generateOutputLocations(Arrays.asList("", ""), Arrays.asList("", ""));
        publisher = new AWSCodePipelinePublisherMock(outputs, mockFactory);

        when(mockJobData.getOutputArtifacts()).thenReturn(generateOutputArtifactsWithNames(Arrays.asList("artifact_1", "artifact_2")));

        // when
        assertTrue(publisher.perform(mockBuild, null, null));

        // then
        final InOrder inOrder = inOrder(mockFactory, mockAWS, mockCodePipelineClient);
        inOrder.verify(mockFactory).getAwsClient(ACCESS_KEY, SECRET_KEY, PROXY_HOST, PROXY_PORT, REGION, PLUGIN_VERSION);
        inOrder.verify(mockAWS).getCodePipelineClient();
        inOrder.verify(mockCodePipelineClient).putJobSuccessResult(putJobSuccessResultRequest.capture());

        final PutJobSuccessResultRequest request = putJobSuccessResultRequest.getValue();
        assertEquals(jobId, request.getJobId());
        assertEquals(BUILD_ID, request.getExecutionDetails().getExternalExecutionId());
        assertEquals("Finished", request.getExecutionDetails().getSummary());

        assertContainsIgnoreCase(PUBLISHING_ARTIFACTS_MESSAGE, outContent.toString());
        assertContainsIgnoreCase(PUT_JOB_SUCCESS_MESSAGE, outContent.toString());
    }

    @Test
    public void putsJobSuccessWhenBuildSucceedsWithNoArtifactNamesSpecified() {
        // given
        when(mockBuild.getResult()).thenReturn(Result.SUCCESS);

        JSONArray outputs = generateOutputLocations(Arrays.asList("output_1", "output_2"), Arrays.asList("", ""));
        publisher = new AWSCodePipelinePublisherMock(outputs, mockFactory);

        when(mockJobData.getOutputArtifacts()).thenReturn(generateOutputArtifactsWithNames(Arrays.asList("artifact_1", "artifact_2")));

        // when
        assertTrue(publisher.perform(mockBuild, null, null));

        // then
        final InOrder inOrder = inOrder(mockFactory, mockAWS, mockCodePipelineClient);
        inOrder.verify(mockFactory).getAwsClient(ACCESS_KEY, SECRET_KEY, PROXY_HOST, PROXY_PORT, REGION, PLUGIN_VERSION);
        inOrder.verify(mockAWS).getCodePipelineClient();
        inOrder.verify(mockCodePipelineClient).putJobSuccessResult(putJobSuccessResultRequest.capture());

        final PutJobSuccessResultRequest request = putJobSuccessResultRequest.getValue();
        assertEquals(jobId, request.getJobId());
        assertEquals(BUILD_ID, request.getExecutionDetails().getExternalExecutionId());
        assertEquals("Finished", request.getExecutionDetails().getSummary());

        assertContainsIgnoreCase(PUBLISHING_ARTIFACTS_MESSAGE, outContent.toString());
        assertContainsIgnoreCase(PUT_JOB_SUCCESS_MESSAGE, outContent.toString());
    }

    @Test
    public void putsJobFailedWhenBuildFails() {
        // given
        when(mockBuild.getResult()).thenReturn(Result.FAILURE);
        model.setActionTypeCategory("Build");

        when(mockJobData.getOutputArtifacts()).thenReturn(generateOutputArtifactsWithNames(Arrays.asList("artifact_1", "artifact_2")));

        // when
        assertFalse(publisher.perform(mockBuild, null, null));

        // then
        final InOrder inOrder = inOrder(mockFactory, mockAWS, mockCodePipelineClient);
        inOrder.verify(mockFactory).getAwsClient(ACCESS_KEY, SECRET_KEY, PROXY_HOST, PROXY_PORT, REGION, PLUGIN_VERSION);
        inOrder.verify(mockAWS).getCodePipelineClient();
        inOrder.verify(mockCodePipelineClient).putJobFailureResult(putJobFailureResultRequest.capture());

        final PutJobFailureResultRequest request = putJobFailureResultRequest.getValue();
        assertEquals(jobId, request.getJobId());
        assertEquals(BUILD_ID, request.getFailureDetails().getExternalExecutionId());
        assertEquals("Build failed", request.getFailureDetails().getMessage());
        assertEquals(FailureType.JobFailed.toString(), request.getFailureDetails().getType());

        assertContainsIgnoreCase(PUBLISHING_ARTIFACTS_MESSAGE, outContent.toString());
        assertContainsIgnoreCase(PUT_JOB_FAILURE_MESSAGE, outContent.toString());
    }

    @Test
    public void skipsPutJobResultWhenSkipPutJobFailureFlagIsSetInModel() {
        // given
        when(mockBuild.getResult()).thenReturn(Result.FAILURE);
        model.setActionTypeCategory("Build");
        model.setSkipPutJobResult(true);

        final List<Artifact> outputBuildArtifacts = new ArrayList<>();
        outputBuildArtifacts.add(new Artifact());
        outputBuildArtifacts.add(new Artifact());
        when(mockJobData.getOutputArtifacts()).thenReturn(outputBuildArtifacts);

        // when
        assertFalse(publisher.perform(mockBuild, null, null));

        // then
        final InOrder inOrder = inOrder(mockFactory, mockAWS, mockCodePipelineClient);
        inOrder.verify(mockFactory, never()).getAwsClient(ACCESS_KEY, SECRET_KEY, PROXY_HOST, PROXY_PORT, REGION, PLUGIN_VERSION);
        inOrder.verify(mockAWS, never()).getCodePipelineClient();

        final String expected = String.format(
                "[AWS CodePipeline Plugin] Skipping PutJobFailureResult call for the job with ID %s",
                model.getJob().getId());
        assertContainsIgnoreCase(expected, outContent.toString());
    }

    @Test
    public void putsJobFailedWhenTheNumberOfOutputArtifactsDoNotMatch() {
        // given
        when(mockBuild.getResult()).thenReturn(Result.SUCCESS);
        model.setActionTypeCategory("Test");

        final List<Artifact> outputArtifacts = new ArrayList<>();
        outputArtifacts.add(new Artifact());
        final PipelineContext pipelineContext = new PipelineContext()
                .withPipelineName("JenkinsPipeline")
                .withStage(new StageContext().withName("Build"))
                .withAction(new ActionContext().withName("JenkinsAction"));

        when(mockJobData.getOutputArtifacts()).thenReturn(outputArtifacts);
        when(mockJobData.getPipelineContext()).thenReturn(pipelineContext);

        // when
        assertFalse(publisher.perform(mockBuild, null, null));

        // then
        final InOrder inOrder = inOrder(mockFactory, mockAWS, mockCodePipelineClient);
        inOrder.verify(mockFactory).getAwsClient(ACCESS_KEY, SECRET_KEY, PROXY_HOST, PROXY_PORT, REGION, PLUGIN_VERSION);
        inOrder.verify(mockAWS).getCodePipelineClient();
        inOrder.verify(mockCodePipelineClient).putJobFailureResult(putJobFailureResultRequest.capture());

        final PutJobFailureResultRequest request = putJobFailureResultRequest.getValue();
        assertEquals(jobId, request.getJobId());
        assertEquals(BUILD_ID, request.getFailureDetails().getExternalExecutionId());
        assertEquals(FailureType.JobFailed.toString(), request.getFailureDetails().getType());
        assertTrue(request.getFailureDetails().getMessage().startsWith("Failed to upload output artifact(s): "
                    + "The number of output artifacts in the Jenkins"));

        final String expected = "[AWS CodePipeline Plugin] The number of output artifacts in the Jenkins project and in "
                + "the pipeline action do not match. Configure the output locations of your Jenkins project to match "
                + "the pipeline action's output artifacts. Number of output locations in Jenkins project: 2, number of "
                + "output artifacts in the pipeline action: 1 [Pipeline: JenkinsPipeline, stage: Build, action: JenkinsAction].";

        assertContainsIgnoreCase(PUBLISHING_ARTIFACTS_MESSAGE, outContent.toString());
        assertContainsIgnoreCase(PUT_JOB_FAILURE_MESSAGE, outContent.toString());
        assertContainsIgnoreCase(expected, outContent.toString());
    }

    @Test
    public void putsJobFailedWhenNotAllOutputArtifactNamesAreEntered() {
        // given
        when(mockBuild.getResult()).thenReturn(Result.SUCCESS);
        model.setActionTypeCategory("Test");

        JSONArray outputs = generateOutputLocations(Arrays.asList("output_1", "output_2"), Arrays.asList("artifact_1", ""));

        publisher = new AWSCodePipelinePublisherMock(outputs, mockFactory);

        final PipelineContext pipelineContext = new PipelineContext()
                .withPipelineName("JenkinsPipeline")
                .withStage(new StageContext().withName("Build"))
                .withAction(new ActionContext().withName("JenkinsAction"));

        when(mockJobData.getOutputArtifacts()).thenReturn(generateOutputArtifactsWithNames(Arrays.asList("artifact_1", "artifact_2")));
        when(mockJobData.getPipelineContext()).thenReturn(pipelineContext);

        // when
        assertFalse(publisher.perform(mockBuild, null, null));

        // then
        final InOrder inOrder = inOrder(mockFactory, mockAWS, mockCodePipelineClient);
        inOrder.verify(mockFactory).getAwsClient(ACCESS_KEY, SECRET_KEY, PROXY_HOST, PROXY_PORT, REGION, PLUGIN_VERSION);
        inOrder.verify(mockAWS).getCodePipelineClient();
        inOrder.verify(mockCodePipelineClient).putJobFailureResult(putJobFailureResultRequest.capture());

        final PutJobFailureResultRequest request = putJobFailureResultRequest.getValue();
        assertEquals(jobId, request.getJobId());
        assertEquals(BUILD_ID, request.getFailureDetails().getExternalExecutionId());
        assertEquals(FailureType.JobFailed.toString(), request.getFailureDetails().getType());
        assertTrue(request.getFailureDetails().getMessage().endsWith("Either configure the artifact name of each "
                + "location to match output artifacts for the pipeline action, or leave the field blank. "
                + "[Pipeline: JenkinsPipeline, stage: Build, action: JenkinsAction]."));

        final String expected = "[AWS CodePipeline Plugin] Artifact names in the Jenkins project do not match output "
                + "artifacts in the pipeline action. Either configure the artifact name of each location to match "
                + "output artifacts for the pipeline action, or leave the field blank. "
                + "[Pipeline: JenkinsPipeline, stage: Build, action: JenkinsAction].";

        assertContainsIgnoreCase(PUBLISHING_ARTIFACTS_MESSAGE, outContent.toString());
        assertContainsIgnoreCase(PUT_JOB_FAILURE_MESSAGE, outContent.toString());
        assertContainsIgnoreCase(expected, outContent.toString());
    }

    @Test
    public void putsJobFailedWhenOutputArtifactNamesDoNotMatch() {
        // given
        when(mockBuild.getResult()).thenReturn(Result.SUCCESS);
        model.setActionTypeCategory("Test");

        final PipelineContext pipelineContext = new PipelineContext()
                .withPipelineName("JenkinsPipeline")
                .withStage(new StageContext().withName("Build"))
                .withAction(new ActionContext().withName("JenkinsAction"));

        when(mockJobData.getOutputArtifacts()).thenReturn(generateOutputArtifactsWithNames(Arrays.asList("artifact1", "artifact_2")));
        when(mockJobData.getPipelineContext()).thenReturn(pipelineContext);

        // when
        assertFalse(publisher.perform(mockBuild, null, null));

        // then
        final InOrder inOrder = inOrder(mockFactory, mockAWS, mockCodePipelineClient);
        inOrder.verify(mockFactory).getAwsClient(ACCESS_KEY, SECRET_KEY, PROXY_HOST, PROXY_PORT, REGION, PLUGIN_VERSION);
        inOrder.verify(mockAWS).getCodePipelineClient();
        inOrder.verify(mockCodePipelineClient).putJobFailureResult(putJobFailureResultRequest.capture());

        final PutJobFailureResultRequest request = putJobFailureResultRequest.getValue();
        assertEquals(jobId, request.getJobId());
        assertEquals(BUILD_ID, request.getFailureDetails().getExternalExecutionId());
        assertEquals(FailureType.JobFailed.toString(), request.getFailureDetails().getType());
        assertTrue(request.getFailureDetails().getMessage().endsWith("Either configure the artifact name of each "
                + "location to match output artifacts for the pipeline action, or leave the field blank. "
                + "[Pipeline: JenkinsPipeline, stage: Build, action: JenkinsAction]."));

        final String expected = "[AWS CodePipeline Plugin] Artifact names in the Jenkins project do not match output "
                + "artifacts in the pipeline action. Either configure the artifact name of each location to match "
                + "output artifacts for the pipeline action, or leave the field blank. "
                + "[Pipeline: JenkinsPipeline, stage: Build, action: JenkinsAction].";

        assertContainsIgnoreCase(PUBLISHING_ARTIFACTS_MESSAGE, outContent.toString());
        assertContainsIgnoreCase(PUT_JOB_FAILURE_MESSAGE, outContent.toString());
        assertContainsIgnoreCase(expected, outContent.toString());
    }

    @Test
    public void putsJobFailedWhenArtifactUploadFails() {
        // given
        when(mockBuild.getResult()).thenReturn(Result.SUCCESS);

        when(mockJobData.getOutputArtifacts()).thenReturn(generateOutputArtifactsWithNames(Arrays.asList("artifact_1", "artifact_2")));

        final AWSCodePipelinePublisherMockS3Exception uploadFailurePublisher
            = new AWSCodePipelinePublisherMockS3Exception(outputLocations, mockFactory);

        // when
        assertFalse(uploadFailurePublisher.perform(mockBuild, null, null));

        // then
        final InOrder inOrder = inOrder(mockFactory, mockAWS, mockCodePipelineClient);
        inOrder.verify(mockFactory).getAwsClient(ACCESS_KEY, SECRET_KEY, PROXY_HOST, PROXY_PORT, REGION, PLUGIN_VERSION);
        inOrder.verify(mockAWS).getCodePipelineClient();
        inOrder.verify(mockCodePipelineClient).putJobFailureResult(putJobFailureResultRequest.capture());

        final PutJobFailureResultRequest request = putJobFailureResultRequest.getValue();
        assertEquals(jobId, request.getJobId());
        assertEquals(BUILD_ID, request.getFailureDetails().getExternalExecutionId());
        assertEquals("Failed to upload output artifact(s): S3 root cause", request.getFailureDetails().getMessage());
        assertEquals(FailureType.JobFailed.toString(), request.getFailureDetails().getType());

        assertContainsIgnoreCase(PUBLISHING_ARTIFACTS_MESSAGE, outContent.toString());
        assertContainsIgnoreCase(PUT_JOB_FAILURE_MESSAGE, outContent.toString());
    }

    @Test
    public void putsJobFailedWhenUploadThrowsError() {
        // given
        when(mockBuild.getResult()).thenReturn(Result.SUCCESS);

        when(mockJobData.getOutputArtifacts()).thenReturn(generateOutputArtifactsWithNames(Arrays.asList("artifact_1", "artifact_2")));

        final AWSCodePipelinePublisher uploadFailurePublisher
                = new AWSCodePipelinePublisherMockError(outputLocations, mockFactory);

        // when
        try {
            assertFalse(uploadFailurePublisher.perform(mockBuild, null, null));
            fail("Expected MockError not thrown");
        } catch (final TestError e) {
            // empty
        }

        // then
        final InOrder inOrder = inOrder(mockFactory, mockAWS, mockCodePipelineClient);
        inOrder.verify(mockFactory).getAwsClient(ACCESS_KEY, SECRET_KEY, PROXY_HOST, PROXY_PORT, REGION, PLUGIN_VERSION);
        inOrder.verify(mockAWS).getCodePipelineClient();
        inOrder.verify(mockCodePipelineClient).putJobFailureResult(putJobFailureResultRequest.capture());

        final PutJobFailureResultRequest request = putJobFailureResultRequest.getValue();
        assertEquals(jobId, request.getJobId());
        assertEquals(BUILD_ID, request.getFailureDetails().getExternalExecutionId());
        assertEquals("Failed to upload output artifact(s): Error", request.getFailureDetails().getMessage());
        assertEquals(FailureType.JobFailed.toString(), request.getFailureDetails().getType());

        assertContainsIgnoreCase(PUBLISHING_ARTIFACTS_MESSAGE, outContent.toString());
        assertContainsIgnoreCase(PUT_JOB_FAILURE_MESSAGE, outContent.toString());
    }

    @Test
    public void cleanUpSuccess() {
        // given
        model.setCompressionType(CompressionType.Zip);

        // when
        publisher.cleanUp(model);

        // then
        assertNull(model.getJob());
        assertEquals(CompressionType.None, model.getCompressionType());
        assertNull(CodePipelineStateService.getModel());
    }

    // -----Setup and Util Methods----- //

    public class AWSCodePipelinePublisherMock extends AWSCodePipelinePublisher {
        public AWSCodePipelinePublisherMock(final JSONArray outputLocations, final AWSClientFactory mockFactory) {
            super(outputLocations, mockFactory);
        }

        @Override
        public void callPublish(final AbstractBuild<?,?> action, final CodePipelineStateModel model, final BuildListener listener) {
            // Do nothing...
        }
    }

    public class AWSCodePipelinePublisherMockS3Exception extends AWSCodePipelinePublisher {
        public AWSCodePipelinePublisherMockS3Exception(final JSONArray outputLocations, final AWSClientFactory mockFactory) {
            super(outputLocations, mockFactory);
        }

        @Override
        public void callPublish(final AbstractBuild<?,?> action, final CodePipelineStateModel model, final BuildListener listener) {
            throw new AmazonS3Exception("S3 root cause");
        }
    }

    private static class AWSCodePipelinePublisherMockError extends AWSCodePipelinePublisher {
        public AWSCodePipelinePublisherMockError(final JSONArray outputLocations, final AWSClientFactory mockFactory) {
            super(outputLocations, mockFactory);
        }

        @Override
        public void callPublish(final AbstractBuild<?,?> action, final CodePipelineStateModel model, final BuildListener listener) {
            throw new TestError();
        }
    }

    private static class TestError extends Error {
        public TestError() {
            super("Error");
        }
    }

    private List<Artifact> generateOutputArtifactsWithNames(List<String> artifactNames) {
        final List<Artifact> outputArtifacts = new ArrayList<>();
        for (String artifactName : artifactNames) {
            Artifact artifact = new Artifact();
            artifact.setName(artifactName);
            outputArtifacts.add(artifact);
        }
        return outputArtifacts;
    }

    private JSONArray generateOutputLocations(List<String> locations, List<String> artifactNames) {
        assertEquals(locations.size(), artifactNames.size());

        JSONArray outputs = new JSONArray();
        for (int i = 0; i < locations.size(); i++) {
            JSONObject jsonObject = new JSONObject();
            jsonObject.put("location", locations.get(i));
            jsonObject.put("artifactName", artifactNames.get(i));

            outputs.add(jsonObject);
        }

        return outputs;
    }

}