/* * 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.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.Matchers.isA; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import hudson.AbortException; import hudson.EnvVars; import hudson.FilePath; import hudson.model.TaskListener; import hudson.model.AbstractBuild; import hudson.scm.PollingResult; import hudson.util.FormValidation; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.UUID; import org.apache.commons.lang.RandomStringUtils; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Suite; import org.junit.runners.model.InitializationError; import org.junit.runners.model.RunnerBuilder; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.InOrder; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import com.amazonaws.auth.AWSCredentialsProvider; import com.amazonaws.codepipeline.jenkinsplugin.CodePipelineStateModel.CategoryType; import com.amazonaws.services.codepipeline.AWSCodePipeline; import com.amazonaws.services.codepipeline.model.AcknowledgeJobRequest; import com.amazonaws.services.codepipeline.model.AcknowledgeJobResult; import com.amazonaws.services.codepipeline.model.ActionOwner; import com.amazonaws.services.codepipeline.model.ActionTypeId; import com.amazonaws.services.codepipeline.model.Artifact; import com.amazonaws.services.codepipeline.model.InvalidNonceException; import com.amazonaws.services.codepipeline.model.Job; import com.amazonaws.services.codepipeline.model.JobData; import com.amazonaws.services.codepipeline.model.PollForJobsRequest; import com.amazonaws.services.codepipeline.model.PollForJobsResult; @RunWith(AWSCodePipelineSCMTest.class) @Suite.SuiteClasses({ AWSCodePipelineSCMTest.PollForJobsTests.class, AWSCodePipelineSCMTest.CheckoutTests.class, AWSCodePipelineSCMTest.SCMDescriptorTests.class }) public class AWSCodePipelineSCMTest extends Suite { public AWSCodePipelineSCMTest(final Class<?> klass, final RunnerBuilder builder) throws InitializationError { super(klass, builder); } private static class TestBase { protected static final String PROJECT_NAME = "Project"; protected static final boolean CLEAR_WORKSPACE = false; protected static final String REGION = "us-east-1"; protected static final String ACCESS_KEY = "1234"; protected static final String SECRET_KEY = "4321"; protected static final String PROXY_HOST = ""; protected static final int PROXY_PORT = 0; protected static final String PLUGIN_VERSION = "aws-codepipeline:unknown"; protected String jobId; protected String jobNonce; public void setUp() throws IOException, InterruptedException { MockitoAnnotations.initMocks(this); jobId = UUID.randomUUID().toString(); jobNonce = UUID.randomUUID().toString(); } } private static class PollingTestBase extends TestBase { protected static final ActionTypeId ACTION_TYPE = new ActionTypeId() .withCategory("Build") .withOwner(ActionOwner.Custom) .withProvider("Jenkins-Build") .withVersion("1"); @Mock protected AWSClientFactory mockFactory; @Mock protected AWSClients mockAWSClients; @Mock protected AWSCodePipeline codePipelineClient; @Mock protected PollForJobsResult pollForJobsResult; @Captor protected ArgumentCaptor<PollForJobsRequest> pollForJobsRequest; protected AWSCodePipelineSCM scm; protected Job job; protected ByteArrayOutputStream outContent; @Before public void setUp() throws IOException, InterruptedException { super.setUp(); when(mockFactory.getAwsClient(anyString(), anyString(), anyString(), anyInt(), anyString(), anyString())) .thenReturn(mockAWSClients); when(mockAWSClients.getCodePipelineClient()).thenReturn(codePipelineClient); job = new Job() .withId(jobId) .withData(new JobData() .withOutputArtifacts(new ArrayList<Artifact>()) .withInputArtifacts(new ArrayList<Artifact>())) .withNonce(jobNonce); scm = new AWSCodePipelineSCM( PROJECT_NAME, CLEAR_WORKSPACE, REGION, ACCESS_KEY, SECRET_KEY, PROXY_HOST, String.valueOf(PROXY_PORT), ACTION_TYPE.getCategory(), ACTION_TYPE.getProvider(), ACTION_TYPE.getVersion(), mockFactory); outContent = TestUtils.setOutputStream(); when(codePipelineClient.pollForJobs(any(PollForJobsRequest.class))).thenReturn(pollForJobsResult); when(pollForJobsResult.getJobs()).thenReturn(Collections.singletonList(job)); } } public static class PollForJobsTests extends PollingTestBase { @Before public void setUp() throws IOException, InterruptedException { super.setUp(); } @Test public void returnsBuildNowWhenThereIsAJob() throws InterruptedException, IOException { // when assertEquals(PollingResult.BUILD_NOW, scm.pollForJobs(ACTION_TYPE, null)); // then final String expectedMessage = String.format("[AWS CodePipeline Plugin] Received job with ID: %1$s\n", jobId); assertContainsIgnoreCase(expectedMessage, outContent.toString()); final InOrder inOrder = inOrder(mockFactory, mockAWSClients, codePipelineClient); inOrder.verify(mockFactory).getAwsClient(ACCESS_KEY, SECRET_KEY, PROXY_HOST, PROXY_PORT, REGION, PLUGIN_VERSION); inOrder.verify(mockAWSClients).getCodePipelineClient(); inOrder.verify(codePipelineClient).pollForJobs(pollForJobsRequest.capture()); final PollForJobsRequest pollRequest = pollForJobsRequest.getValue(); assertEquals(ACTION_TYPE, pollRequest.getActionTypeId()); assertEquals(1, pollRequest.getMaxBatchSize().intValue()); assertEquals(1, pollRequest.getQueryParam().size()); assertEquals(PROJECT_NAME, pollRequest.getQueryParam().get("ProjectName")); } @Test public void returnsNoChangesWhenThereAreNoJobs() throws InterruptedException, IOException { // given when(pollForJobsResult.getJobs()).thenReturn(new ArrayList<Job>()); // when assertEquals(PollingResult.NO_CHANGES, scm.pollForJobs(ACTION_TYPE, null)); // then assertContainsIgnoreCase("No jobs found.", outContent.toString()); final InOrder inOrder = inOrder(mockFactory, mockAWSClients, codePipelineClient); inOrder.verify(mockFactory).getAwsClient(ACCESS_KEY, SECRET_KEY, PROXY_HOST, PROXY_PORT, REGION, PLUGIN_VERSION); inOrder.verify(mockAWSClients).getCodePipelineClient(); inOrder.verify(codePipelineClient).pollForJobs(any(PollForJobsRequest.class)); } } public static class CheckoutTests extends PollingTestBase { private FilePath workspacePath; @Mock private AcknowledgeJobResult acknowledgeJobResult; @Captor private ArgumentCaptor<AcknowledgeJobRequest> acknowledgeJobRequest; @Before public void setUp() throws IOException, InterruptedException { super.setUp(); final File tempFile = File.createTempFile("workspacePath", "tmp"); tempFile.deleteOnExit(); workspacePath = new FilePath(tempFile); when(codePipelineClient.acknowledgeJob(any(AcknowledgeJobRequest.class))).thenReturn(acknowledgeJobResult); when(acknowledgeJobResult.getStatus()).thenReturn("InProgress"); } @Test public void acknowledgesJobAndDownloadsInputArtifacts() throws InterruptedException, IOException { // given assertEquals(PollingResult.BUILD_NOW, scm.pollForJobs(ACTION_TYPE, null)); // when assertTrue(scm.checkout(null, null, workspacePath, null, null)); // then final String expectedMessage = String.format("[AWS CodePipeline Plugin] Received job with ID: %1$s\n" + "[AWS CodePipeline Plugin] Job '%1$s' received\n" + "[AWS CodePipeline Plugin] Acknowledged job with ID: %1$s\n", jobId); assertContainsIgnoreCase(expectedMessage, outContent.toString()); final InOrder inOrder = inOrder(mockFactory, mockAWSClients, codePipelineClient); inOrder.verify(codePipelineClient).acknowledgeJob(acknowledgeJobRequest.capture()); // verifying that we are initializing s3 client to download artifacts. inOrder.verify(mockFactory).getAwsClient(ACCESS_KEY, SECRET_KEY, PROXY_HOST, PROXY_PORT, REGION, PLUGIN_VERSION); inOrder.verify(mockAWSClients).getS3Client(isA(AWSCredentialsProvider.class)); assertEquals(jobId, acknowledgeJobRequest.getValue().getJobId()); assertEquals(jobNonce, acknowledgeJobRequest.getValue().getNonce()); } @Test public void doesNotDownloadArtifactsWhenAcknowledgeJobDoesNotReturnInProgressJob() throws InterruptedException, IOException { // given when(acknowledgeJobResult.getStatus()).thenReturn("Created"); assertEquals(PollingResult.BUILD_NOW, scm.pollForJobs(ACTION_TYPE, null)); // when try { scm.checkout(null, null, workspacePath, null, null); fail("Should not reach here."); } catch (final AbortException e) { // then final String exceptionMessage = String.format("Failed to acknowledge job with ID: %s", jobId); assertContainsIgnoreCase(exceptionMessage, e.getMessage()); final String outMessage = String.format("[AWS CodePipeline Plugin] Received job with ID: %1$s\n" + "[AWS CodePipeline Plugin] Job '%1$s' received\n", jobId); assertContainsIgnoreCase(outMessage, outContent.toString()); final InOrder inOrder = inOrder(mockFactory, mockAWSClients, codePipelineClient); inOrder.verify(codePipelineClient).acknowledgeJob(any(AcknowledgeJobRequest.class)); // verifying s3 client not being invoked for downloading artifacts. verify(mockAWSClients, never()).getS3Client(isA(AWSCredentialsProvider.class)); } } @Test public void doesNotDownloadArtifactsWhenAcknowledgeJobThrowsInvalidNonceException() throws InterruptedException, IOException { // given when(codePipelineClient.acknowledgeJob(any(AcknowledgeJobRequest.class))) .thenThrow(new InvalidNonceException("job was already acknowledged")); assertEquals(PollingResult.BUILD_NOW, scm.pollForJobs(ACTION_TYPE, null)); // when try { scm.checkout(null, null, workspacePath, null, null); } catch (final AbortException e) { // then final String exceptionMessage = String.format("Job with ID %s was already acknowledged", jobId); assertContainsIgnoreCase(exceptionMessage, e.getMessage()); final String outMessage = String.format("[AWS CodePipeline Plugin] Received job with ID: %1$s\n" + "[AWS CodePipeline Plugin] Job '%1$s' received\n", jobId); assertContainsIgnoreCase(outMessage, outContent.toString()); final InOrder inOrder = inOrder(mockFactory, mockAWSClients, codePipelineClient); inOrder.verify(codePipelineClient).acknowledgeJob(any(AcknowledgeJobRequest.class)); // verifying s3 client not being invoked for downloading artifacts. verify(mockAWSClients, never()).getS3Client(isA(AWSCredentialsProvider.class)); } } } public static class SCMDescriptorTests extends TestBase { @Mock private AbstractBuild<?, ?> mockBuild; @Mock private EnvVars envVars; @Mock private CodePipelineStateModel model; @Mock private Job mockJob; private AWSCodePipelineSCM.DescriptorImpl descriptor; @Before public void setUp() throws IOException, InterruptedException { super.setUp(); descriptor = new AWSCodePipelineSCM.DescriptorImpl(false); when(mockBuild.getEnvironment(any(TaskListener.class))).thenReturn(envVars); when(envVars.get(any(String.class))).thenReturn("Project"); when(model.getJob()).thenReturn(mockJob); when(mockJob.getId()).thenReturn(jobId); CodePipelineStateService.setModel(model); } @After public void tearDown() { CodePipelineStateService.removeModel(); } @Test public void doCheckCategorySucceedsWithBuildCategory() { assertEquals(FormValidation.ok(), descriptor.doCheckCategory(CategoryType.Build.name())); } @Test public void doCheckCategorySucceedsWithTestCategory() { assertEquals(FormValidation.ok(), descriptor.doCheckCategory(CategoryType.Test.name())); } @Test public void doCheckCategoryFailsIfNoCategoryIsChosen() { final CategoryType category = CategoryType.PleaseChooseACategory; assertContainsIgnoreCase( "Please select a Category Type", descriptor.doCheckCategory(category.name()).toString()); assertValidationMessage(FormValidation.error(""), descriptor.doCheckCategory(category.name())); } @Test public void versionCheckShouldSucceedSimpleVersionNumber() { final String version = RandomStringUtils.randomNumeric(1); assertEquals(FormValidation.ok(), descriptor.doCheckVersion(version)); } @Test public void versionCheckShouldSucceedWithMaxNumber() { final String version = RandomStringUtils.randomNumeric(Validation.MAX_VERSION_LENGTH); assertEquals(FormValidation.ok(), descriptor.doCheckVersion(version)); } @Test public void versionCheckShouldFailWithEmpty() { final String version = ""; assertContainsIgnoreCase( "Please enter a Version", descriptor.doCheckVersion(version).toString()); assertValidationMessage(FormValidation.error(""), descriptor.doCheckVersion(version)); } @Test public void versionCheckShouldFailWithNegativeNumber() { final String version = "-1"; assertContainsIgnoreCase( "Version must be greater than or equal to 0", descriptor.doCheckVersion(version).toString()); assertValidationMessage(FormValidation.error(""), descriptor.doCheckVersion(version)); } @Test public void versionCheckShouldFailWithLetters() { final String version = RandomStringUtils.randomAlphabetic(5); assertContainsIgnoreCase( "Version must be a number", descriptor.doCheckVersion(version).toString()); assertValidationMessage(FormValidation.error(""), descriptor.doCheckVersion(version)); } @Test public void versionCheckShouldFailWithTooLongValue() { final String version = RandomStringUtils.randomNumeric(Validation.MAX_VERSION_LENGTH + 1); assertContainsIgnoreCase( "Version can only be " + Validation.MAX_VERSION_LENGTH + " characters in length, you entered " + version.length(), descriptor.doCheckVersion(version).toString()); assertValidationMessage(FormValidation.error(""), descriptor.doCheckVersion(version)); } @Test public void providerCheckSucceeds() { final String provider = "Jenkins"; assertEquals(FormValidation.ok(), descriptor.doCheckProvider(provider)); } @Test public void providerCheckFailsEmpty() { final String provider = ""; assertContainsIgnoreCase( "Please enter a Provider, typically "Jenkins" or your Project Name", descriptor.doCheckProvider(provider).toString()); assertValidationMessage(FormValidation.error(""), descriptor.doCheckProvider(provider)); } @Test public void providerCheckFailsTooLong() { String provider = "Jenkins-Build"; while (provider.length() < Validation.MAX_PROVIDER_LENGTH) { provider = provider + provider; } assertContainsIgnoreCase( "The Provider name is too long, the name should be ", descriptor.doCheckProvider(provider).toString()); assertValidationMessage(FormValidation.error(""), descriptor.doCheckProvider(provider)); } @Test public void proxyPortIsNumberSuccess() { final String proxyPort = "0"; assertEquals(FormValidation.ok(), descriptor.doCheckProxyPort(proxyPort)); } @Test public void proxyPortIsLessThanZeroFailure() { final String proxyPort = "-1"; assertContainsIgnoreCase( "Proxy Port must be between 0 and 65535", descriptor.doCheckProxyPort(proxyPort).toString()); assertValidationMessage(FormValidation.error(""), descriptor.doCheckProxyPort(proxyPort)); } @Test public void proxyPortIsGreaterThan65535Failure() { final String proxyPort = "65536"; assertContainsIgnoreCase( "Proxy Port must be between 0 and 65535", descriptor.doCheckProxyPort(proxyPort).toString()); assertValidationMessage(FormValidation.error(""), descriptor.doCheckProxyPort(proxyPort)); } @Test public void proxyPortIsNotANumberFailure() { final String proxyPort = RandomStringUtils.randomAlphabetic(4); assertContainsIgnoreCase( "Proxy Port must be a number", descriptor.doCheckProxyPort(proxyPort).toString()); assertValidationMessage(FormValidation.error(""), descriptor.doCheckProxyPort(proxyPort)); } private void assertValidationMessage(final FormValidation expected, final FormValidation actualMessage) { // Since FormValidations have "%NUM" syntax for each error, we just take that off to compare the // two values, otherwise they would never be equal. assertEquals(expected.toString().substring(0, expected.toString().indexOf('$')), actualMessage.toString().substring(0, actualMessage.toString().indexOf('$'))); } } }