├── .gitignore ├── src ├── test │ ├── resources │ │ ├── mockito-extensions │ │ │ └── org.mockito.plugins.MockMaker │ │ └── three │ │ │ └── days │ │ │ └── xiola.apk │ └── java │ │ └── io │ │ └── jenkins │ │ └── plugins │ │ └── appcenter │ │ ├── util │ │ ├── TestFileUtil.java │ │ ├── TestUtil.java │ │ ├── MockWebServerUtil.java │ │ └── RemoteFileUtilsTest.java │ │ ├── validator │ │ ├── PathPlaceholderValidatorTest.java │ │ ├── AppNameValidatorTest.java │ │ ├── ApiTokenValidatorTest.java │ │ ├── BranchNameValidatorTest.java │ │ ├── CommitHashValidatorTest.java │ │ ├── DistributionGroupsValidatorTest.java │ │ ├── PathToAppValidatorTest.java │ │ ├── PathToDebugSymbolsValidatorTest.java │ │ ├── PathToReleaseNotesValidatorTest.java │ │ └── UsernameValidatorTest.java │ │ ├── RoundTripTest.java │ │ ├── FreestyleTest.java │ │ ├── NodeTest.java │ │ ├── task │ │ └── internal │ │ │ ├── UpdateReleaseTaskTest.java │ │ │ ├── SetMetadataTaskTest.java │ │ │ ├── CreateUploadResourceTaskTest.java │ │ │ ├── UploadAppToResourceTaskTest.java │ │ │ ├── FinishReleaseTaskTest.java │ │ │ └── PollForReleaseTaskTest.java │ │ ├── ConfigurationTest.java │ │ ├── ProxyTest.java │ │ └── EnvInterpolationTest.java └── main │ ├── resources │ ├── index.jelly │ └── io │ │ └── jenkins │ │ └── plugins │ │ └── appcenter │ │ ├── AppCenterRecorder │ │ ├── help-releaseNotes.html │ │ ├── help-branchName.html │ │ ├── help-pathToApp.html │ │ ├── help-pathToDebugSymbols.html │ │ ├── help-commitHash.html │ │ ├── help-buildVersion.html │ │ ├── help-pathToReleaseNotes.html │ │ ├── help-apiToken.html │ │ ├── help-distributionGroups.html │ │ ├── help-appName.html │ │ ├── help-ownerName.html │ │ └── config.jelly │ │ └── Messages.properties │ └── java │ └── io │ └── jenkins │ └── plugins │ └── appcenter │ ├── AppCenterException.java │ ├── validator │ ├── ApiTokenValidator.java │ ├── AppNameValidator.java │ ├── BranchNameValidator.java │ ├── CommitHashValidator.java │ ├── Validator.java │ ├── BuildVersionValidator.java │ ├── PathToAppValidator.java │ ├── PathToDebugSymbolsValidator.java │ ├── PathToReleaseNotesValidator.java │ ├── DistributionGroupsValidator.java │ ├── UsernameValidator.java │ └── PathPlaceholderValidator.java │ ├── di │ ├── AuthModule.java │ ├── JenkinsModule.java │ ├── AppCenterComponent.java │ └── UploadModule.java │ ├── util │ ├── ParserFactory.java │ ├── AndroidParser.java │ └── RemoteFileUtils.java │ ├── task │ ├── internal │ │ ├── AppCenterTask.java │ │ ├── UpdateReleaseTask.java │ │ ├── SetMetadataTask.java │ │ ├── FinishReleaseTask.java │ │ ├── PollForReleaseTask.java │ │ ├── CreateUploadResourceTask.java │ │ ├── DistributeResourceTask.java │ │ └── UploadAppToResourceTask.java │ └── UploadTask.java │ ├── api │ ├── UploadService.java │ ├── AppCenterService.java │ └── AppCenterServiceFactory.java │ ├── model │ └── appcenter │ │ ├── Failure.java │ │ ├── SetMetadataResponse.java │ │ ├── SymbolUploadEndRequest.java │ │ ├── ReleaseUploadEndRequest.java │ │ ├── ReleaseDetailsUpdateResponse.java │ │ ├── DestinationId.java │ │ ├── UpdateReleaseUploadRequest.java │ │ ├── UploadedSymbolInfo.java │ │ ├── SymbolUploadUserInfo.java │ │ ├── ReleaseUploadEndResponse.java │ │ ├── ReleaseUploadBeginRequest.java │ │ ├── ErrorDetails.java │ │ ├── UpdateReleaseUploadResponse.java │ │ ├── BuildInfo.java │ │ ├── DestinationError.java │ │ ├── SymbolUploadBeginResponse.java │ │ ├── ReleaseUploadBeginResponse.java │ │ ├── ReleaseUpdateRequest.java │ │ ├── ReleaseUpdateError.java │ │ ├── SymbolUploadBeginRequest.java │ │ ├── PollForReleaseResponse.java │ │ └── SymbolUpload.java │ └── AppCenterLogger.java ├── Jenkinsfile ├── .editorconfig ├── .github ├── release-drafter.yml └── workflows │ └── release-drafter.yml ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *.iml 3 | *.ipr 4 | *.iws 5 | .idea 6 | target 7 | work 8 | -------------------------------------------------------------------------------- /src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker: -------------------------------------------------------------------------------- 1 | mock-maker-inline -------------------------------------------------------------------------------- /src/test/resources/three/days/xiola.apk: -------------------------------------------------------------------------------- 1 | at this moment, you should be with us... 2 | -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | // Build the plugin using https://github.com/jenkins-infra/pipeline-library 2 | buildPlugin(useContainerAgent: true) 3 | -------------------------------------------------------------------------------- /src/main/resources/index.jelly: -------------------------------------------------------------------------------- 1 | 2 |
3 | Upload new versions of your Android, iOS, MacOS, and (limited) Windows applications to AppCenter. 4 |
-------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | charset = utf-8 3 | end_of_line = lf 4 | insert_final_newline = false 5 | indent_style = space 6 | indent_size = 4 7 | max_line_length = 180 8 | 9 | [*.{json,yml}] 10 | indent_size = 2 -------------------------------------------------------------------------------- /src/main/resources/io/jenkins/plugins/appcenter/AppCenterRecorder/help-releaseNotes.html: -------------------------------------------------------------------------------- 1 |
2 |

3 | Supports Markdown syntax. NOTE: Limited to 5000 characters or less. 4 |

5 |
-------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | # from https://github.com/jenkinsci/.github/blob/master/.github/release-drafter.yml 2 | _extends: .github 3 | name-template: AppCenter Plugin $NEXT_PATCH_VERSION 4 | tag-template: appcenter-$NEXT_PATCH_VERSION 5 | version-template: $MAJOR.$MINOR.$PATCH -------------------------------------------------------------------------------- /src/main/resources/io/jenkins/plugins/appcenter/AppCenterRecorder/help-branchName.html: -------------------------------------------------------------------------------- 1 |
2 |

3 | Name of the branch being built. Supports variable substitution. 4 |

5 |
6 |

7 | For example: origin/master or $GIT_BRANCH 8 |

9 |
-------------------------------------------------------------------------------- /src/main/resources/io/jenkins/plugins/appcenter/AppCenterRecorder/help-pathToApp.html: -------------------------------------------------------------------------------- 1 |
2 |

3 | Relative path to app. Supports variable substitution. 4 |

5 |
6 |

7 | For example: three/days/xiola.apk or three/days/${APP}.apk 8 |

9 |
-------------------------------------------------------------------------------- /src/main/resources/io/jenkins/plugins/appcenter/AppCenterRecorder/help-pathToDebugSymbols.html: -------------------------------------------------------------------------------- 1 |
2 |

3 | Relative path to debug symbols. Supports variable substitution. 4 |

5 |
6 |

7 | For example: three/days/casey.apk or three/days/${SYMBOLS}.apk 8 |

9 |
-------------------------------------------------------------------------------- /src/main/resources/io/jenkins/plugins/appcenter/AppCenterRecorder/help-commitHash.html: -------------------------------------------------------------------------------- 1 |
2 |

3 | Commit hash of the commit being build. Supports variable substitution. 4 |

5 |
6 |

7 | For example: 0e62d85530892a9178ff2b55ac30e8ede56c9c0e or $GIT_COMMIT 8 |

9 |
-------------------------------------------------------------------------------- /src/main/resources/io/jenkins/plugins/appcenter/AppCenterRecorder/help-buildVersion.html: -------------------------------------------------------------------------------- 1 |
2 |

3 | NOTE: Build version might be mandatory on certain platform releases. Supports variable substitution. 4 |

5 |
6 |

7 | For example: 1.2.0 or ${version} 8 |

9 |
-------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/appcenter/AppCenterException.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.appcenter; 2 | 3 | public final class AppCenterException extends Exception { 4 | 5 | public AppCenterException(String s) { 6 | super(s); 7 | } 8 | 9 | public AppCenterException(String s, Throwable throwable) { 10 | super(s, throwable); 11 | } 12 | } -------------------------------------------------------------------------------- /src/main/resources/io/jenkins/plugins/appcenter/AppCenterRecorder/help-pathToReleaseNotes.html: -------------------------------------------------------------------------------- 1 |
2 |

3 | Relative path to release notes. Supports variable substitution. Supports Markdown syntax. NOTE: Limited to 5000 characters or less. 4 |

5 |
6 |

7 | For example: three/days/linearnotes.md or three/days/${NOTES}.md 8 |

9 |
-------------------------------------------------------------------------------- /src/main/resources/io/jenkins/plugins/appcenter/AppCenterRecorder/help-apiToken.html: -------------------------------------------------------------------------------- 1 |
2 |

3 | An API Token is used for authentication for all App Center API calls. Must have full access. 4 |

5 |
6 |

7 | Visit https://docs.microsoft.com/en-us/appcenter/api-docs/index for further information. 8 |

9 |
-------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/appcenter/validator/ApiTokenValidator.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.appcenter.validator; 2 | 3 | import javax.annotation.Nonnull; 4 | import java.util.function.Predicate; 5 | 6 | public final class ApiTokenValidator extends Validator { 7 | 8 | @Nonnull 9 | @Override 10 | protected Predicate predicate() { 11 | return value -> !value.contains(" "); 12 | } 13 | 14 | } -------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/appcenter/validator/AppNameValidator.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.appcenter.validator; 2 | 3 | import javax.annotation.Nonnull; 4 | import java.util.function.Predicate; 5 | 6 | public final class AppNameValidator extends Validator { 7 | 8 | @Nonnull 9 | @Override 10 | protected Predicate predicate() { 11 | return value -> !value.contains(" "); 12 | } 13 | 14 | } -------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/appcenter/validator/BranchNameValidator.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.appcenter.validator; 2 | 3 | import javax.annotation.Nonnull; 4 | import java.util.function.Predicate; 5 | 6 | public final class BranchNameValidator extends Validator { 7 | 8 | @Nonnull 9 | @Override 10 | protected Predicate predicate() { 11 | return value -> !value.contains(" "); 12 | } 13 | 14 | } -------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/appcenter/validator/CommitHashValidator.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.appcenter.validator; 2 | 3 | import javax.annotation.Nonnull; 4 | import java.util.function.Predicate; 5 | 6 | public final class CommitHashValidator extends Validator { 7 | 8 | @Nonnull 9 | @Override 10 | protected Predicate predicate() { 11 | return value -> !value.contains(" "); 12 | } 13 | 14 | } -------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/appcenter/validator/Validator.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.appcenter.validator; 2 | 3 | import javax.annotation.Nonnull; 4 | import java.util.function.Predicate; 5 | 6 | public abstract class Validator { 7 | 8 | @Nonnull 9 | protected abstract Predicate predicate(); 10 | 11 | public boolean isValid(@Nonnull String value) { 12 | return predicate().test(value); 13 | } 14 | } -------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/appcenter/validator/BuildVersionValidator.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.appcenter.validator; 2 | 3 | import java.util.function.Predicate; 4 | 5 | import javax.annotation.Nonnull; 6 | 7 | public final class BuildVersionValidator extends Validator { 8 | 9 | @Nonnull 10 | @Override 11 | protected Predicate predicate() { 12 | return value -> !value.contains(" "); 13 | } 14 | 15 | } -------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/appcenter/validator/PathToAppValidator.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.appcenter.validator; 2 | 3 | import hudson.Util; 4 | 5 | import javax.annotation.Nonnull; 6 | import java.util.function.Predicate; 7 | 8 | public final class PathToAppValidator extends Validator { 9 | 10 | @Nonnull 11 | @Override 12 | protected Predicate predicate() { 13 | return Util::isRelativePath; 14 | } 15 | } -------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/appcenter/validator/PathToDebugSymbolsValidator.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.appcenter.validator; 2 | 3 | import hudson.Util; 4 | 5 | import javax.annotation.Nonnull; 6 | import java.util.function.Predicate; 7 | 8 | public final class PathToDebugSymbolsValidator extends Validator { 9 | 10 | @Nonnull 11 | @Override 12 | protected Predicate predicate() { 13 | return Util::isRelativePath; 14 | } 15 | } -------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/appcenter/validator/PathToReleaseNotesValidator.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.appcenter.validator; 2 | 3 | import hudson.Util; 4 | 5 | import javax.annotation.Nonnull; 6 | import java.util.function.Predicate; 7 | 8 | public final class PathToReleaseNotesValidator extends Validator { 9 | 10 | @Nonnull 11 | @Override 12 | protected Predicate predicate() { 13 | return Util::isRelativePath; 14 | } 15 | } -------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/appcenter/validator/DistributionGroupsValidator.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.appcenter.validator; 2 | 3 | import javax.annotation.Nonnull; 4 | import java.util.function.Predicate; 5 | 6 | public final class DistributionGroupsValidator extends Validator { 7 | 8 | @Nonnull 9 | @Override 10 | protected Predicate predicate() { 11 | return value -> !value.replace(",", "").trim().isEmpty(); 12 | } 13 | 14 | } -------------------------------------------------------------------------------- /src/main/resources/io/jenkins/plugins/appcenter/AppCenterRecorder/help-distributionGroups.html: -------------------------------------------------------------------------------- 1 |
2 |

3 | NOTE: Distribution groups must have permission for this app to be distributed to. 4 |

5 |
6 |

7 | Check your distribution groups at https://appcenter.ms/apps. 8 |

9 |

10 | For example perry, dave, stephen, eric 11 |

12 |
-------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/appcenter/di/AuthModule.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.appcenter.di; 2 | 3 | import dagger.Module; 4 | import dagger.Provides; 5 | import hudson.util.Secret; 6 | import io.jenkins.plugins.appcenter.AppCenterRecorder; 7 | 8 | import javax.inject.Singleton; 9 | 10 | @Module 11 | final class AuthModule { 12 | @Provides 13 | @Singleton 14 | static Secret provideApiToken(AppCenterRecorder appCenterRecorder) { 15 | return appCenterRecorder.getApiToken(); 16 | } 17 | } -------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/appcenter/validator/UsernameValidator.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.appcenter.validator; 2 | 3 | import javax.annotation.Nonnull; 4 | import java.util.function.Predicate; 5 | import java.util.regex.Pattern; 6 | 7 | public final class UsernameValidator extends Validator { 8 | 9 | @Nonnull 10 | @Override 11 | protected Predicate predicate() { 12 | return Pattern.compile("^(?![a-zA-Z0-9-_.]*+$)") 13 | .asPredicate() 14 | .negate(); 15 | } 16 | 17 | } -------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/appcenter/validator/PathPlaceholderValidator.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.appcenter.validator; 2 | 3 | import javax.annotation.Nonnull; 4 | import java.util.function.Predicate; 5 | import java.util.regex.Pattern; 6 | 7 | public final class PathPlaceholderValidator extends Validator { 8 | 9 | @Nonnull 10 | @Override 11 | protected Predicate predicate() { 12 | return Pattern.compile("^\\$\\{[^}]+}") 13 | .asPredicate() 14 | .negate(); 15 | } 16 | 17 | } -------------------------------------------------------------------------------- /.github/workflows/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name: Release Drafter 2 | 3 | on: 4 | push: 5 | # branches to consider in the event; optional, defaults to all 6 | branches: 7 | - master 8 | 9 | jobs: 10 | update_release_draft: 11 | runs-on: ubuntu-latest 12 | if: github.repository == 'jenkinsci/appcenter-plugin' 13 | steps: 14 | # Drafts your next Release notes as Pull Requests are merged into "master" 15 | - uses: release-drafter/release-drafter@v5 16 | env: 17 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/appcenter/util/ParserFactory.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.appcenter.util; 2 | 3 | import javax.annotation.Nonnull; 4 | import javax.inject.Inject; 5 | import java.io.File; 6 | import java.io.Serializable; 7 | 8 | public final class ParserFactory implements Serializable { 9 | 10 | private static final long serialVersionUID = 1L; 11 | 12 | @Inject 13 | ParserFactory() { 14 | } 15 | 16 | @Nonnull 17 | public AndroidParser androidParser(final @Nonnull File file) { 18 | return new AndroidParser(file); 19 | } 20 | } -------------------------------------------------------------------------------- /src/main/resources/io/jenkins/plugins/appcenter/AppCenterRecorder/help-appName.html: -------------------------------------------------------------------------------- 1 |
2 |

3 | The name of the app in AppCenter. NOTE: This may differ to the display name that you see in the web UI. 4 |

5 |
6 |

7 | Visit https://appcenter.ms/apps, select your app, inspect the URL. 8 |

9 |

10 | For example the URL in AppCenter might look like: https://appcenter.ms/users/xiola-3/apps/casey-1. Here, the appName is casey-1. 11 |

12 |
-------------------------------------------------------------------------------- /src/main/resources/io/jenkins/plugins/appcenter/AppCenterRecorder/help-ownerName.html: -------------------------------------------------------------------------------- 1 |
2 |

3 | The name of the owner in AppCenter. NOTE: This may differ to the display name that you see in the web UI. 4 |

5 |
6 |

7 | Visit https://appcenter.ms/apps, select your app, inspect the URL. 8 |

9 |

10 | For example the URL in AppCenter might look like: https://appcenter.ms/users/xiola-3/apps/casey-1. Here, the ownerName is xiola-3. 11 |

12 |
-------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/appcenter/task/internal/AppCenterTask.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.appcenter.task.internal; 2 | 3 | import javax.annotation.Nonnull; 4 | import java.io.Serializable; 5 | import java.util.concurrent.CompletableFuture; 6 | 7 | /** 8 | * Task that represents an internal Jenkins AppCenter plugin operation. 9 | * 10 | * @param Request type 11 | */ 12 | public interface AppCenterTask extends Serializable { 13 | /** 14 | * Execute a task given a request and returns a result as a CompletableFuture. 15 | * 16 | * @param request T: Request 17 | * @return CompletableFuture: An expectation of a result of type T 18 | */ 19 | @Nonnull 20 | CompletableFuture execute(@Nonnull T request); 21 | } -------------------------------------------------------------------------------- /src/test/java/io/jenkins/plugins/appcenter/util/TestFileUtil.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.appcenter.util; 2 | 3 | import javax.annotation.Nonnull; 4 | import java.io.File; 5 | 6 | public final class TestFileUtil { 7 | 8 | public static final String TEST_FILE_PATH = "src/test/resources/three/days/xiola.apk"; 9 | 10 | @Nonnull 11 | public static File createFileForTesting() { 12 | return new File(TEST_FILE_PATH); 13 | } 14 | 15 | @Nonnull 16 | public static File createLargeFileForTesting() { 17 | return new File(TEST_FILE_PATH) { 18 | @Override 19 | public long length() { 20 | return (1024 * 1024) * 512; // Double the max size allowed to upload. 21 | } 22 | }; 23 | } 24 | } -------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/appcenter/api/UploadService.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.appcenter.api; 2 | 3 | import okhttp3.MultipartBody; 4 | import okhttp3.RequestBody; 5 | import retrofit2.http.Body; 6 | import retrofit2.http.Headers; 7 | import retrofit2.http.Multipart; 8 | import retrofit2.http.POST; 9 | import retrofit2.http.PUT; 10 | import retrofit2.http.Part; 11 | import retrofit2.http.Url; 12 | 13 | import javax.annotation.Nonnull; 14 | import java.util.concurrent.CompletableFuture; 15 | 16 | public interface UploadService { 17 | 18 | @Multipart 19 | @POST 20 | CompletableFuture uploadApp(@Url @Nonnull String url, @Part @Nonnull MultipartBody.Part file); 21 | 22 | @Headers("x-ms-blob-type: BlockBlob") 23 | @PUT 24 | CompletableFuture uploadSymbols(@Url @Nonnull String url, @Body @Nonnull RequestBody file); 25 | 26 | } -------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/appcenter/model/appcenter/Failure.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.appcenter.model.appcenter; 2 | 3 | import javax.annotation.Nonnull; 4 | import java.util.Objects; 5 | 6 | public final class Failure { 7 | @Nonnull 8 | public final String message; 9 | 10 | public Failure(@Nonnull String message) { 11 | this.message = message; 12 | } 13 | 14 | @Override 15 | public String toString() { 16 | return "Failure{" + 17 | "message='" + message + '\'' + 18 | '}'; 19 | } 20 | 21 | @Override 22 | public boolean equals(Object o) { 23 | if (this == o) return true; 24 | if (o == null || getClass() != o.getClass()) return false; 25 | Failure failure = (Failure) o; 26 | return message.equals(failure.message); 27 | } 28 | 29 | @Override 30 | public int hashCode() { 31 | return Objects.hash(message); 32 | } 33 | } -------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/appcenter/model/appcenter/SetMetadataResponse.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.appcenter.model.appcenter; 2 | 3 | import javax.annotation.Nonnull; 4 | import java.util.Objects; 5 | 6 | public final class SetMetadataResponse { 7 | @Nonnull 8 | public final Integer chunk_size; 9 | 10 | public SetMetadataResponse(@Nonnull Integer chunkSize) { 11 | this.chunk_size = chunkSize; 12 | } 13 | 14 | @Override 15 | public String toString() { 16 | return "SetMetadataResponse{" + 17 | "chunk_size=" + chunk_size + 18 | '}'; 19 | } 20 | 21 | @Override 22 | public boolean equals(Object o) { 23 | if (this == o) return true; 24 | if (o == null || getClass() != o.getClass()) return false; 25 | SetMetadataResponse that = (SetMetadataResponse) o; 26 | return chunk_size.equals(that.chunk_size); 27 | } 28 | 29 | @Override 30 | public int hashCode() { 31 | return Objects.hash(chunk_size); 32 | } 33 | } -------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/appcenter/model/appcenter/SymbolUploadEndRequest.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.appcenter.model.appcenter; 2 | 3 | import javax.annotation.Nonnull; 4 | import java.util.Objects; 5 | 6 | public final class SymbolUploadEndRequest { 7 | @Nonnull 8 | public final StatusEnum status; 9 | 10 | public SymbolUploadEndRequest(@Nonnull StatusEnum status) { 11 | this.status = status; 12 | } 13 | 14 | @Override 15 | public String toString() { 16 | return "SymbolUploadEndRequest{" + 17 | "status=" + status + 18 | '}'; 19 | } 20 | 21 | @Override 22 | public boolean equals(Object o) { 23 | if (this == o) return true; 24 | if (o == null || getClass() != o.getClass()) return false; 25 | SymbolUploadEndRequest that = (SymbolUploadEndRequest) o; 26 | return status == that.status; 27 | } 28 | 29 | @Override 30 | public int hashCode() { 31 | return Objects.hash(status); 32 | } 33 | 34 | public enum StatusEnum { 35 | committed, 36 | aborted 37 | } 38 | } -------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/appcenter/model/appcenter/ReleaseUploadEndRequest.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.appcenter.model.appcenter; 2 | 3 | import javax.annotation.Nonnull; 4 | import java.util.Objects; 5 | 6 | public final class ReleaseUploadEndRequest { 7 | 8 | @Nonnull 9 | public final StatusEnum status; 10 | 11 | public ReleaseUploadEndRequest(@Nonnull StatusEnum status) { 12 | this.status = status; 13 | } 14 | 15 | @Override 16 | public String toString() { 17 | return "ReleaseUploadEndRequest{" + 18 | "status=" + status + 19 | '}'; 20 | } 21 | 22 | @Override 23 | public boolean equals(Object o) { 24 | if (this == o) return true; 25 | if (o == null || getClass() != o.getClass()) return false; 26 | ReleaseUploadEndRequest that = (ReleaseUploadEndRequest) o; 27 | return status == that.status; 28 | } 29 | 30 | @Override 31 | public int hashCode() { 32 | return Objects.hash(status); 33 | } 34 | 35 | public enum StatusEnum { 36 | committed, 37 | aborted 38 | } 39 | } -------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/appcenter/model/appcenter/ReleaseDetailsUpdateResponse.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.appcenter.model.appcenter; 2 | 3 | import javax.annotation.Nullable; 4 | import java.util.Objects; 5 | 6 | public final class ReleaseDetailsUpdateResponse { 7 | @Nullable 8 | public final String release_notes; 9 | 10 | public ReleaseDetailsUpdateResponse(@Nullable String releaseNotes) { 11 | this.release_notes = releaseNotes; 12 | } 13 | 14 | @Override 15 | public String toString() { 16 | return "ReleaseDetailsUpdateResponse{" + 17 | "release_notes='" + release_notes + '\'' + 18 | '}'; 19 | } 20 | 21 | @Override 22 | public boolean equals(Object o) { 23 | if (this == o) return true; 24 | if (o == null || getClass() != o.getClass()) return false; 25 | ReleaseDetailsUpdateResponse that = (ReleaseDetailsUpdateResponse) o; 26 | return Objects.equals(release_notes, that.release_notes); 27 | } 28 | 29 | @Override 30 | public int hashCode() { 31 | return Objects.hash(release_notes); 32 | } 33 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Mez Pahlan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/appcenter/model/appcenter/DestinationId.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.appcenter.model.appcenter; 2 | 3 | import javax.annotation.Nullable; 4 | import java.util.Objects; 5 | 6 | public final class DestinationId { 7 | @Nullable 8 | public final String name; 9 | @Nullable 10 | public final String id; 11 | 12 | public DestinationId(@Nullable String name, @Nullable String id) { 13 | this.name = name; 14 | this.id = id; 15 | } 16 | 17 | @Override 18 | public String toString() { 19 | return "DestinationId{" + 20 | "name='" + name + '\'' + 21 | ", id='" + id + '\'' + 22 | '}'; 23 | } 24 | 25 | @Override 26 | public boolean equals(Object o) { 27 | if (this == o) return true; 28 | if (o == null || getClass() != o.getClass()) return false; 29 | DestinationId that = (DestinationId) o; 30 | return Objects.equals(name, that.name) && 31 | Objects.equals(id, that.id); 32 | } 33 | 34 | @Override 35 | public int hashCode() { 36 | return Objects.hash(name, id); 37 | } 38 | } -------------------------------------------------------------------------------- /src/test/java/io/jenkins/plugins/appcenter/validator/PathPlaceholderValidatorTest.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.appcenter.validator; 2 | 3 | import org.junit.Before; 4 | import org.junit.Test; 5 | 6 | import static com.google.common.truth.Truth.assertThat; 7 | 8 | public class PathPlaceholderValidatorTest { 9 | private PathPlaceholderValidator validator; 10 | 11 | @Before 12 | public void setUp() { 13 | validator = new PathPlaceholderValidator(); 14 | } 15 | 16 | @Test 17 | public void should_ReturnTrue_When_PathDoesNotStartsWithEnvVariable() { 18 | // Given 19 | final String value = "relative/path/to/${SOME_ENV_VAR}.ipa"; 20 | 21 | // When 22 | final boolean result = validator.isValid(value); 23 | 24 | // Then 25 | assertThat(result).isTrue(); 26 | } 27 | 28 | @Test 29 | public void should_ReturnFalse_When_PathStartsWithEnvVariable() { 30 | // Given 31 | final String value = "${SOME_ENV_VAR}.ipa"; 32 | 33 | // When 34 | final boolean result = validator.isValid(value); 35 | 36 | // Then 37 | assertThat(result).isFalse(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/appcenter/di/JenkinsModule.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.appcenter.di; 2 | 3 | import dagger.Module; 4 | import dagger.Provides; 5 | import hudson.EnvVars; 6 | import hudson.ProxyConfiguration; 7 | import hudson.model.Run; 8 | import hudson.model.TaskListener; 9 | import jenkins.model.Jenkins; 10 | 11 | import javax.annotation.Nullable; 12 | import javax.inject.Singleton; 13 | import java.io.IOException; 14 | import java.io.PrintStream; 15 | 16 | @Module 17 | final class JenkinsModule { 18 | 19 | @Provides 20 | @Nullable 21 | @Singleton 22 | static ProxyConfiguration provideProxyConfiguration(Jenkins jenkins) { 23 | return jenkins.proxy; 24 | } 25 | 26 | @Provides 27 | @Singleton 28 | static EnvVars provideEnvVars(Run run, TaskListener taskListener) throws RuntimeException { 29 | final PrintStream logger = taskListener.getLogger(); 30 | try { 31 | return run.getEnvironment(taskListener); 32 | } catch (IOException | InterruptedException e) { 33 | e.printStackTrace(logger); 34 | throw new RuntimeException("Failed to get Environment Variables."); 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/appcenter/model/appcenter/UpdateReleaseUploadRequest.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.appcenter.model.appcenter; 2 | 3 | import javax.annotation.Nonnull; 4 | import java.util.Objects; 5 | 6 | public final class UpdateReleaseUploadRequest { 7 | @Nonnull 8 | public final StatusEnum upload_status; 9 | 10 | public UpdateReleaseUploadRequest(@Nonnull StatusEnum upload_status) { 11 | this.upload_status = upload_status; 12 | } 13 | 14 | @Override 15 | public String toString() { 16 | return "UpdateReleaseUploadRequest{" + 17 | "upload_status=" + upload_status + 18 | '}'; 19 | } 20 | 21 | @Override 22 | public boolean equals(Object o) { 23 | if (this == o) return true; 24 | if (o == null || getClass() != o.getClass()) return false; 25 | UpdateReleaseUploadRequest that = (UpdateReleaseUploadRequest) o; 26 | return upload_status == that.upload_status; 27 | } 28 | 29 | @Override 30 | public int hashCode() { 31 | return Objects.hash(upload_status); 32 | } 33 | 34 | public enum StatusEnum { 35 | uploadFinished, 36 | uploadCanceled 37 | } 38 | } -------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/appcenter/util/AndroidParser.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.appcenter.util; 2 | 3 | import net.dongliu.apk.parser.ApkParsers; 4 | import net.dongliu.apk.parser.bean.ApkMeta; 5 | 6 | import javax.annotation.Nonnull; 7 | import javax.annotation.Nullable; 8 | import java.io.File; 9 | import java.io.IOException; 10 | 11 | public final class AndroidParser { 12 | 13 | @Nonnull 14 | private final File file; 15 | @Nullable 16 | private ApkMeta apkMeta; 17 | 18 | AndroidParser(final @Nonnull File file) { 19 | this.file = file; 20 | } 21 | 22 | @Nonnull 23 | public String versionCode() throws IOException { 24 | return metaInfo().getVersionCode().toString(); 25 | } 26 | 27 | @Nonnull 28 | public String versionName() throws IOException { 29 | return metaInfo().getVersionName(); 30 | } 31 | 32 | @Nonnull 33 | public String fileName() { 34 | return file.getName(); 35 | } 36 | 37 | @Nonnull 38 | private ApkMeta metaInfo() throws IOException { 39 | if (apkMeta != null) return apkMeta; 40 | 41 | apkMeta = ApkParsers.getMetaInfo(file); 42 | 43 | return apkMeta; 44 | } 45 | } -------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/appcenter/model/appcenter/UploadedSymbolInfo.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.appcenter.model.appcenter; 2 | 3 | import javax.annotation.Nonnull; 4 | import java.util.Objects; 5 | 6 | public final class UploadedSymbolInfo { 7 | @Nonnull 8 | public final String symbol_id; 9 | @Nonnull 10 | public final String platform; 11 | 12 | public UploadedSymbolInfo(@Nonnull String symbolId, @Nonnull String platform) { 13 | this.symbol_id = symbolId; 14 | this.platform = platform; 15 | } 16 | 17 | @Override 18 | public String toString() { 19 | return "UploadedSymbolInfo{" + 20 | "symbol_id='" + symbol_id + '\'' + 21 | ", platform='" + platform + '\'' + 22 | '}'; 23 | } 24 | 25 | @Override 26 | public boolean equals(Object o) { 27 | if (this == o) return true; 28 | if (o == null || getClass() != o.getClass()) return false; 29 | UploadedSymbolInfo that = (UploadedSymbolInfo) o; 30 | return symbol_id.equals(that.symbol_id) && 31 | platform.equals(that.platform); 32 | } 33 | 34 | @Override 35 | public int hashCode() { 36 | return Objects.hash(symbol_id, platform); 37 | } 38 | } -------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/appcenter/di/AppCenterComponent.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.appcenter.di; 2 | 3 | import dagger.BindsInstance; 4 | import dagger.Component; 5 | import hudson.FilePath; 6 | import hudson.model.Run; 7 | import hudson.model.TaskListener; 8 | import io.jenkins.plugins.appcenter.AppCenterRecorder; 9 | import io.jenkins.plugins.appcenter.task.UploadTask; 10 | import jenkins.model.Jenkins; 11 | 12 | import javax.annotation.Nullable; 13 | import javax.inject.Named; 14 | import javax.inject.Singleton; 15 | 16 | @Singleton 17 | @Component(modules = {JenkinsModule.class, AuthModule.class, UploadModule.class}) 18 | public interface AppCenterComponent { 19 | 20 | UploadTask uploadTask(); 21 | 22 | @Component.Factory 23 | interface Factory { 24 | AppCenterComponent create(@BindsInstance AppCenterRecorder appCenterRecorder, 25 | @BindsInstance Jenkins jenkins, 26 | @BindsInstance Run run, 27 | @BindsInstance FilePath filePath, 28 | @BindsInstance TaskListener taskListener, 29 | @BindsInstance @Nullable @Named("baseUrl") String baseUrl); 30 | } 31 | } -------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/appcenter/model/appcenter/SymbolUploadUserInfo.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.appcenter.model.appcenter; 2 | 3 | import javax.annotation.Nullable; 4 | import java.util.Objects; 5 | 6 | public final class SymbolUploadUserInfo { 7 | @Nullable 8 | public final String email; 9 | @Nullable 10 | public final String display_name; 11 | 12 | public SymbolUploadUserInfo(@Nullable String email, @Nullable String displayName) { 13 | this.email = email; 14 | this.display_name = displayName; 15 | } 16 | 17 | @Override 18 | public String toString() { 19 | return "SymbolUploadUserInfo{" + 20 | "email='" + email + '\'' + 21 | ", display_name='" + display_name + '\'' + 22 | '}'; 23 | } 24 | 25 | @Override 26 | public boolean equals(Object o) { 27 | if (this == o) return true; 28 | if (o == null || getClass() != o.getClass()) return false; 29 | SymbolUploadUserInfo that = (SymbolUploadUserInfo) o; 30 | return Objects.equals(email, that.email) && 31 | Objects.equals(display_name, that.display_name); 32 | } 33 | 34 | @Override 35 | public int hashCode() { 36 | return Objects.hash(email, display_name); 37 | } 38 | } -------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/appcenter/model/appcenter/ReleaseUploadEndResponse.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.appcenter.model.appcenter; 2 | 3 | import javax.annotation.Nullable; 4 | import java.util.Objects; 5 | 6 | public final class ReleaseUploadEndResponse { 7 | 8 | @Nullable 9 | public final Integer release_id; 10 | @Nullable 11 | public final String release_url; 12 | 13 | public ReleaseUploadEndResponse(@Nullable Integer releaseId, @Nullable String releaseUrl) { 14 | this.release_id = releaseId; 15 | this.release_url = releaseUrl; 16 | } 17 | 18 | @Override 19 | public String toString() { 20 | return "ReleaseUploadEndResponse{" + 21 | "release_id='" + release_id + '\'' + 22 | ", release_url='" + release_url + '\'' + 23 | '}'; 24 | } 25 | 26 | @Override 27 | public boolean equals(Object o) { 28 | if (this == o) return true; 29 | if (o == null || getClass() != o.getClass()) return false; 30 | ReleaseUploadEndResponse that = (ReleaseUploadEndResponse) o; 31 | return Objects.equals(release_id, that.release_id) && 32 | Objects.equals(release_url, that.release_url); 33 | } 34 | 35 | @Override 36 | public int hashCode() { 37 | return Objects.hash(release_id, release_url); 38 | } 39 | } -------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/appcenter/model/appcenter/ReleaseUploadBeginRequest.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.appcenter.model.appcenter; 2 | 3 | import javax.annotation.Nullable; 4 | import java.util.Objects; 5 | 6 | public final class ReleaseUploadBeginRequest { 7 | 8 | @Nullable 9 | public final String build_version; 10 | @Nullable 11 | public final String build_number; 12 | 13 | public ReleaseUploadBeginRequest(@Nullable String buildVersion, @Nullable String buildNumber) { 14 | this.build_version = buildVersion; 15 | this.build_number = buildNumber; 16 | } 17 | 18 | @Override 19 | public String toString() { 20 | return "ReleaseUploadBeginRequest{" + 21 | ", build_version='" + build_version + '\'' + 22 | ", build_number='" + build_number + '\'' + 23 | '}'; 24 | } 25 | 26 | @Override 27 | public boolean equals(Object o) { 28 | if (this == o) return true; 29 | if (o == null || getClass() != o.getClass()) return false; 30 | ReleaseUploadBeginRequest that = (ReleaseUploadBeginRequest) o; 31 | return Objects.equals(build_version, that.build_version) && 32 | Objects.equals(build_number, that.build_number); 33 | } 34 | 35 | @Override 36 | public int hashCode() { 37 | return Objects.hash(build_version, build_number); 38 | } 39 | } -------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/appcenter/model/appcenter/ErrorDetails.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.appcenter.model.appcenter; 2 | 3 | import javax.annotation.Nonnull; 4 | import java.util.Objects; 5 | 6 | public final class ErrorDetails { 7 | 8 | @Nonnull 9 | public final CodeEnum code; 10 | 11 | @Nonnull 12 | public final String message; 13 | 14 | public ErrorDetails(@Nonnull CodeEnum code, @Nonnull String message) { 15 | this.code = code; 16 | this.message = message; 17 | } 18 | 19 | @Override 20 | public String toString() { 21 | return "ErrorDetails{" + 22 | "code=" + code + 23 | ", message='" + message + '\'' + 24 | '}'; 25 | } 26 | 27 | @Override 28 | public boolean equals(Object o) { 29 | if (this == o) return true; 30 | if (o == null || getClass() != o.getClass()) return false; 31 | ErrorDetails that = (ErrorDetails) o; 32 | return code == that.code && 33 | message.equals(that.message); 34 | } 35 | 36 | @Override 37 | public int hashCode() { 38 | return Objects.hash(code, message); 39 | } 40 | 41 | public enum CodeEnum { 42 | BadRequest, 43 | Conflict, 44 | NotAcceptable, 45 | NotFound, 46 | InternalServerError, 47 | Unauthorized, 48 | TooManyRequests 49 | } 50 | } -------------------------------------------------------------------------------- /src/test/java/io/jenkins/plugins/appcenter/RoundTripTest.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.appcenter; 2 | 3 | import hudson.util.Secret; 4 | import org.junit.ClassRule; 5 | import org.junit.Test; 6 | import org.jvnet.hudson.test.JenkinsRule; 7 | 8 | import static com.google.common.truth.Truth.assertThat; 9 | 10 | public class RoundTripTest { 11 | 12 | @ClassRule 13 | public static JenkinsRule jenkinsRule = new JenkinsRule(); 14 | 15 | @Test 16 | public void should_Configure_AppCenterRecorder_With_Required_Inputs() throws Exception { 17 | // Given 18 | final AppCenterRecorder appCenterRecorder = new AppCenterRecorder( 19 | "at-this-moment-you-should-be-with-us", 20 | "janes-addiction", 21 | "ritual-de-lo-habitual", 22 | "three/days/xiola.apk", 23 | "casey, niccoli" 24 | ); 25 | 26 | // When 27 | jenkinsRule.configRoundtrip(appCenterRecorder); 28 | 29 | // Then 30 | assertThat(appCenterRecorder.getApiToken()).isEqualTo(Secret.fromString("at-this-moment-you-should-be-with-us")); 31 | assertThat(appCenterRecorder.getOwnerName()).isEqualTo("janes-addiction"); 32 | assertThat(appCenterRecorder.getAppName()).isEqualTo("ritual-de-lo-habitual"); 33 | assertThat(appCenterRecorder.getPathToApp()).isEqualTo("three/days/xiola.apk"); 34 | assertThat(appCenterRecorder.getDistributionGroups()).isEqualTo("casey, niccoli"); 35 | } 36 | } -------------------------------------------------------------------------------- /src/test/java/io/jenkins/plugins/appcenter/util/TestUtil.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.appcenter.util; 2 | 3 | import hudson.Launcher; 4 | import hudson.model.AbstractBuild; 5 | import hudson.model.BuildListener; 6 | import org.jvnet.hudson.test.TestBuilder; 7 | 8 | import javax.annotation.Nonnull; 9 | import java.io.IOException; 10 | import java.util.Objects; 11 | 12 | public final class TestUtil { 13 | public static TestBuilder createFile(final @Nonnull String pathToFile) { 14 | return createFile(pathToFile, "all of us with wings"); 15 | } 16 | 17 | public static TestBuilder createFile(final @Nonnull String pathToFile, final @Nonnull String content) { 18 | return new TestAppWriter(pathToFile, content); 19 | } 20 | 21 | private static class TestAppWriter extends TestBuilder { 22 | 23 | @Nonnull 24 | private final String pathToFile; 25 | @Nonnull 26 | private final String content; 27 | 28 | private TestAppWriter(final @Nonnull String pathToFile, final @Nonnull String content) { 29 | this.pathToFile = pathToFile; 30 | this.content = content; 31 | } 32 | 33 | public boolean perform(AbstractBuild build, Launcher launcher, BuildListener listener) 34 | throws InterruptedException, IOException { 35 | Objects.requireNonNull(build.getWorkspace()).child(pathToFile).write(content, "UTF-8"); 36 | return true; 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/appcenter/model/appcenter/UpdateReleaseUploadResponse.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.appcenter.model.appcenter; 2 | 3 | import javax.annotation.Nonnull; 4 | import java.util.Objects; 5 | 6 | public final class UpdateReleaseUploadResponse { 7 | @Nonnull 8 | public final String id; 9 | @Nonnull 10 | public final StatusEnum upload_status; 11 | 12 | public UpdateReleaseUploadResponse(@Nonnull String id, 13 | @Nonnull StatusEnum upload_status) { 14 | 15 | this.id = id; 16 | this.upload_status = upload_status; 17 | } 18 | 19 | @Override 20 | public String toString() { 21 | return "UpdateReleaseUploadResponse{" + 22 | "id='" + id + '\'' + 23 | ", upload_status=" + upload_status + 24 | '}'; 25 | } 26 | 27 | @Override 28 | public boolean equals(Object o) { 29 | if (this == o) return true; 30 | if (o == null || getClass() != o.getClass()) return false; 31 | UpdateReleaseUploadResponse that = (UpdateReleaseUploadResponse) o; 32 | return id.equals(that.id) && upload_status == that.upload_status; 33 | } 34 | 35 | @Override 36 | public int hashCode() { 37 | return Objects.hash(id, upload_status); 38 | } 39 | 40 | public enum StatusEnum { 41 | uploadStarted, 42 | uploadFinished, 43 | uploadCanceled, 44 | readyToBePublished, 45 | malwareDetected, 46 | error 47 | } 48 | } -------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/appcenter/model/appcenter/BuildInfo.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.appcenter.model.appcenter; 2 | 3 | import javax.annotation.Nullable; 4 | import java.util.Objects; 5 | 6 | public final class BuildInfo { 7 | @Nullable 8 | public final String branch_name; 9 | @Nullable 10 | public final String commit_hash; 11 | @Nullable 12 | public final String commit_message; 13 | 14 | public BuildInfo(@Nullable String branchName, @Nullable String commitHash, @Nullable String commitMessage) { 15 | this.branch_name = branchName; 16 | this.commit_hash = commitHash; 17 | this.commit_message = commitMessage; 18 | } 19 | 20 | @Override 21 | public String toString() { 22 | return "BuildInfo{" + 23 | "branch_name='" + branch_name + '\'' + 24 | ", commit_hash='" + commit_hash + '\'' + 25 | ", commit_message='" + commit_message + '\'' + 26 | '}'; 27 | } 28 | 29 | @Override 30 | public boolean equals(Object o) { 31 | if (this == o) return true; 32 | if (o == null || getClass() != o.getClass()) return false; 33 | BuildInfo buildInfo = (BuildInfo) o; 34 | return Objects.equals(branch_name, buildInfo.branch_name) && 35 | Objects.equals(commit_hash, buildInfo.commit_hash) && 36 | Objects.equals(commit_message, buildInfo.commit_message); 37 | } 38 | 39 | @Override 40 | public int hashCode() { 41 | return Objects.hash(branch_name, commit_hash, commit_message); 42 | } 43 | } -------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/appcenter/model/appcenter/DestinationError.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.appcenter.model.appcenter; 2 | 3 | import javax.annotation.Nullable; 4 | import java.util.Objects; 5 | 6 | public final class DestinationError { 7 | @Nullable 8 | public final String code; 9 | 10 | @Nullable 11 | public final String message; 12 | 13 | @Nullable 14 | public final String id; 15 | 16 | @Nullable 17 | public final String name; 18 | 19 | public DestinationError(@Nullable String code, @Nullable String message, @Nullable String id, @Nullable String name) { 20 | this.code = code; 21 | this.message = message; 22 | this.id = id; 23 | this.name = name; 24 | } 25 | 26 | @Override 27 | public String toString() { 28 | return "DestinationError{" + 29 | "code='" + code + '\'' + 30 | ", message='" + message + '\'' + 31 | ", id='" + id + '\'' + 32 | ", name='" + name + '\'' + 33 | '}'; 34 | } 35 | 36 | @Override 37 | public boolean equals(Object o) { 38 | if (this == o) return true; 39 | if (o == null || getClass() != o.getClass()) return false; 40 | DestinationError that = (DestinationError) o; 41 | return Objects.equals(code, that.code) && 42 | Objects.equals(message, that.message) && 43 | Objects.equals(id, that.id) && 44 | Objects.equals(name, that.name); 45 | } 46 | 47 | @Override 48 | public int hashCode() { 49 | return Objects.hash(code, message, id, name); 50 | } 51 | } -------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/appcenter/di/UploadModule.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.appcenter.di; 2 | 3 | import dagger.Module; 4 | import dagger.Provides; 5 | import hudson.EnvVars; 6 | import io.jenkins.plugins.appcenter.AppCenterRecorder; 7 | import io.jenkins.plugins.appcenter.task.request.UploadRequest; 8 | 9 | import javax.inject.Singleton; 10 | 11 | @Module 12 | final class UploadModule { 13 | 14 | @Provides 15 | @Singleton 16 | static UploadRequest provideUploadRequest(AppCenterRecorder appCenterRecorder, EnvVars envVars) { 17 | return new UploadRequest.Builder() 18 | .setOwnerName(envVars.expand(appCenterRecorder.getOwnerName())) 19 | .setAppName(envVars.expand(appCenterRecorder.getAppName())) 20 | .setPathToApp(envVars.expand(appCenterRecorder.getPathToApp())) 21 | .setDestinationGroups(envVars.expand(appCenterRecorder.getDistributionGroups())) 22 | .setReleaseNotes(envVars.expand(appCenterRecorder.getReleaseNotes())) 23 | .setPathToReleaseNotes(envVars.expand(appCenterRecorder.getPathToReleaseNotes())) 24 | .setNotifyTesters(appCenterRecorder.getNotifyTesters()) 25 | .setMandatoryUpdate(appCenterRecorder.getMandatoryUpdate()) 26 | .setBuildVersion(envVars.expand(appCenterRecorder.getBuildVersion())) 27 | .setPathToDebugSymbols(envVars.expand(appCenterRecorder.getPathToDebugSymbols())) 28 | .setCommitHash(envVars.expand(appCenterRecorder.getCommitHash())) 29 | .setBranchName(envVars.expand(appCenterRecorder.getBranchName())) 30 | .build(); 31 | } 32 | } -------------------------------------------------------------------------------- /src/main/resources/io/jenkins/plugins/appcenter/Messages.properties: -------------------------------------------------------------------------------- 1 | AppCenterRecorder.DescriptorImpl.warnings.mustNotStartWithEnvVar=Paths should not start with placeholders 2 | AppCenterRecorder.DescriptorImpl.errors.upstreamBuildFailure=Skipping due to upstream build failure 3 | AppCenterRecorder.DescriptorImpl.errors.missingApiToken=Please specify an AppCenter API Token 4 | AppCenterRecorder.DescriptorImpl.errors.invalidApiToken=API Token cannot contain whitespace 5 | AppCenterRecorder.DescriptorImpl.errors.missingOwnerName=Please specify an AppCenter Owner Name 6 | AppCenterRecorder.DescriptorImpl.errors.invalidOwnerName=Username can only contain letters, numbers, dashes, underscores, or full stops 7 | AppCenterRecorder.DescriptorImpl.errors.missingAppName=Please specify an AppCenter App Name 8 | AppCenterRecorder.DescriptorImpl.errors.invalidAppName=App name cannot contain whitespace 9 | AppCenterRecorder.DescriptorImpl.errors.invalidBranchName=Branch name cannot contain whitespace 10 | AppCenterRecorder.DescriptorImpl.errors.invalidCommitHash=Commit hash cannot contain whitespace 11 | AppCenterRecorder.DescriptorImpl.errors.missingDistributionGroups=Please specify Distribution Groups 12 | AppCenterRecorder.DescriptorImpl.errors.invalidDistributionGroups=Distribution Groups cannot be empty 13 | AppCenterRecorder.DescriptorImpl.errors.missingPathToApp=Please specify a path to the app you wish to upload 14 | AppCenterRecorder.DescriptorImpl.errors.invalidPath=Invalid path 15 | AppCenterRecorder.DescriptorImpl.errors.invalidBuildVersion=Build version cannot contain whitespace 16 | AppCenterRecorder.DescriptorImpl.DisplayName=Upload app to AppCenter -------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/appcenter/model/appcenter/SymbolUploadBeginResponse.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.appcenter.model.appcenter; 2 | 3 | import javax.annotation.Nonnull; 4 | import java.util.Objects; 5 | 6 | public final class SymbolUploadBeginResponse { 7 | @Nonnull 8 | public final String symbol_upload_id; 9 | @Nonnull 10 | public final String upload_url; 11 | @Nonnull 12 | public final String expiration_date; 13 | 14 | public SymbolUploadBeginResponse(@Nonnull String symbolUploadId, @Nonnull String uploadUrl, @Nonnull String expirationDate) { 15 | this.symbol_upload_id = symbolUploadId; 16 | this.upload_url = uploadUrl; 17 | this.expiration_date = expirationDate; 18 | } 19 | 20 | @Override 21 | public String toString() { 22 | return "SymbolUploadBeginResponse{" + 23 | "symbol_upload_id='" + symbol_upload_id + '\'' + 24 | ", upload_url='" + upload_url + '\'' + 25 | ", expiration_date='" + expiration_date + '\'' + 26 | '}'; 27 | } 28 | 29 | @Override 30 | public boolean equals(Object o) { 31 | if (this == o) return true; 32 | if (o == null || getClass() != o.getClass()) return false; 33 | SymbolUploadBeginResponse that = (SymbolUploadBeginResponse) o; 34 | return symbol_upload_id.equals(that.symbol_upload_id) && 35 | upload_url.equals(that.upload_url) && 36 | expiration_date.equals(that.expiration_date); 37 | } 38 | 39 | @Override 40 | public int hashCode() { 41 | return Objects.hash(symbol_upload_id, upload_url, expiration_date); 42 | } 43 | } -------------------------------------------------------------------------------- /src/test/java/io/jenkins/plugins/appcenter/validator/AppNameValidatorTest.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.appcenter.validator; 2 | 3 | 4 | import org.junit.Before; 5 | import org.junit.Test; 6 | 7 | import static com.google.common.truth.Truth.assertThat; 8 | 9 | public class AppNameValidatorTest { 10 | 11 | private AppNameValidator validator; 12 | 13 | @Before 14 | public void setUp() { 15 | validator = new AppNameValidator(); 16 | } 17 | 18 | @Test 19 | public void should_ReturnFalse_When_AppNameContainsSingleSpace() { 20 | // Given 21 | final String value = " "; 22 | 23 | // When 24 | final boolean result = validator.isValid(value); 25 | 26 | // Then 27 | assertThat(result).isFalse(); 28 | } 29 | 30 | @Test 31 | public void should_ReturnFalse_When_AppNameContainsMultipleSpace() { 32 | // Given 33 | final String value = " "; 34 | 35 | // When 36 | final boolean result = validator.isValid(value); 37 | 38 | // Then 39 | assertThat(result).isFalse(); 40 | } 41 | 42 | @Test 43 | public void should_ReturnFalse_When_AppNameContainsSpaces() { 44 | // Given 45 | final String value = " my super duper app "; 46 | 47 | // When 48 | final boolean result = validator.isValid(value); 49 | 50 | // Then 51 | assertThat(result).isFalse(); 52 | } 53 | 54 | @Test 55 | public void should_ReturnTrue_When_AppNameDoesNotContainSpaces() { 56 | // Given 57 | final String value = "my-super-duper-app"; 58 | 59 | // When 60 | final boolean result = validator.isValid(value); 61 | 62 | // Then 63 | assertThat(result).isTrue(); 64 | } 65 | } -------------------------------------------------------------------------------- /src/test/java/io/jenkins/plugins/appcenter/validator/ApiTokenValidatorTest.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.appcenter.validator; 2 | 3 | import org.junit.Before; 4 | import org.junit.Test; 5 | 6 | import static com.google.common.truth.Truth.assertThat; 7 | 8 | public class ApiTokenValidatorTest { 9 | 10 | private ApiTokenValidator validator; 11 | 12 | @Before 13 | public void setUp() { 14 | validator = new ApiTokenValidator(); 15 | } 16 | 17 | @Test 18 | public void should_ReturnFalse_When_ApiTokenContainsSingleSpace() { 19 | // Given 20 | final String value = " "; 21 | 22 | // When 23 | final boolean result = validator.isValid(value); 24 | 25 | // Then 26 | assertThat(result).isFalse(); 27 | } 28 | 29 | @Test 30 | public void should_ReturnFalse_When_ApiTokenContainsMultipleSpace() { 31 | // Given 32 | final String value = " "; 33 | 34 | // When 35 | final boolean result = validator.isValid(value); 36 | 37 | // Then 38 | assertThat(result).isFalse(); 39 | } 40 | 41 | @Test 42 | public void should_ReturnFalse_When_ApiTokenContainsSpaces() { 43 | // Given 44 | final String value = " a12b3 4cd 5f89 98av3 "; 45 | 46 | // When 47 | final boolean result = validator.isValid(value); 48 | 49 | // Then 50 | assertThat(result).isFalse(); 51 | } 52 | 53 | @Test 54 | public void should_ReturnTrue_When_ApiTokenDoesNotContainSpaces() { 55 | // Given 56 | final String value = "a12b3-4cd-5f89-98av3"; 57 | 58 | // When 59 | final boolean result = validator.isValid(value); 60 | 61 | // Then 62 | assertThat(result).isTrue(); 63 | } 64 | } -------------------------------------------------------------------------------- /src/test/java/io/jenkins/plugins/appcenter/validator/BranchNameValidatorTest.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.appcenter.validator; 2 | 3 | 4 | import org.junit.Before; 5 | import org.junit.Test; 6 | 7 | import static com.google.common.truth.Truth.assertThat; 8 | 9 | public class BranchNameValidatorTest { 10 | 11 | private BranchNameValidator validator; 12 | 13 | @Before 14 | public void setUp() { 15 | validator = new BranchNameValidator(); 16 | } 17 | 18 | @Test 19 | public void should_ReturnFalse_When_BranchNameContainsSingleSpace() { 20 | // Given 21 | final String value = " "; 22 | 23 | // When 24 | final boolean result = validator.isValid(value); 25 | 26 | // Then 27 | assertThat(result).isFalse(); 28 | } 29 | 30 | @Test 31 | public void should_ReturnFalse_When_BranchNameContainsMultipleSpace() { 32 | // Given 33 | final String value = " "; 34 | 35 | // When 36 | final boolean result = validator.isValid(value); 37 | 38 | // Then 39 | assertThat(result).isFalse(); 40 | } 41 | 42 | @Test 43 | public void should_ReturnFalse_When_BranchNameContainsSpaces() { 44 | // Given 45 | final String value = "origin/ invalid branch"; 46 | 47 | // When 48 | final boolean result = validator.isValid(value); 49 | 50 | // Then 51 | assertThat(result).isFalse(); 52 | } 53 | 54 | @Test 55 | public void should_ReturnTrue_When_BranchNameDoesNotContainSpaces() { 56 | // Given 57 | final String value = "origin/master"; 58 | 59 | // When 60 | final boolean result = validator.isValid(value); 61 | 62 | // Then 63 | assertThat(result).isTrue(); 64 | } 65 | } -------------------------------------------------------------------------------- /src/test/java/io/jenkins/plugins/appcenter/validator/CommitHashValidatorTest.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.appcenter.validator; 2 | 3 | 4 | import org.junit.Before; 5 | import org.junit.Test; 6 | 7 | import static com.google.common.truth.Truth.assertThat; 8 | 9 | public class CommitHashValidatorTest { 10 | 11 | private CommitHashValidator validator; 12 | 13 | @Before 14 | public void setUp() { 15 | validator = new CommitHashValidator(); 16 | } 17 | 18 | @Test 19 | public void should_ReturnFalse_When_CommitHashContainsSingleSpace() { 20 | // Given 21 | final String value = " "; 22 | 23 | // When 24 | final boolean result = validator.isValid(value); 25 | 26 | // Then 27 | assertThat(result).isFalse(); 28 | } 29 | 30 | @Test 31 | public void should_ReturnFalse_When_CommitHashContainsMultipleSpace() { 32 | // Given 33 | final String value = " "; 34 | 35 | // When 36 | final boolean result = validator.isValid(value); 37 | 38 | // Then 39 | assertThat(result).isFalse(); 40 | } 41 | 42 | @Test 43 | public void should_ReturnFalse_When_CommitHashContainsSpaces() { 44 | // Given 45 | final String value = "ffa8d2d2ad619d13 20f94d1865d39647e9e8e278"; 46 | 47 | // When 48 | final boolean result = validator.isValid(value); 49 | 50 | // Then 51 | assertThat(result).isFalse(); 52 | } 53 | 54 | @Test 55 | public void should_ReturnTrue_When_CommitHashDoesNotContainSpaces() { 56 | // Given 57 | final String value = "ffa8d2d2ad619d1320f94d1865d39647e9e8e278"; 58 | 59 | // When 60 | final boolean result = validator.isValid(value); 61 | 62 | // Then 63 | assertThat(result).isTrue(); 64 | } 65 | } -------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/appcenter/AppCenterLogger.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.appcenter; 2 | 3 | import okhttp3.ResponseBody; 4 | import retrofit2.HttpException; 5 | import retrofit2.Response; 6 | 7 | import javax.annotation.Nonnull; 8 | import java.io.IOException; 9 | import java.io.PrintStream; 10 | 11 | import static java.util.Objects.requireNonNull; 12 | 13 | public interface AppCenterLogger { 14 | 15 | PrintStream getLogger(); 16 | 17 | default void log(String message) { 18 | getLogger().println(message); 19 | } 20 | 21 | default AppCenterException logFailure(@Nonnull String message) { 22 | requireNonNull(message, "message cannot be null."); 23 | return new AppCenterException(message); 24 | } 25 | 26 | default AppCenterException logFailure(@Nonnull String message, @Nonnull Throwable throwable) { 27 | requireNonNull(message, "message cannot be null."); 28 | requireNonNull(throwable, "throwable cannot be null."); 29 | 30 | // Error could be an HttPException or it could not be 31 | if (throwable instanceof HttpException) { 32 | try { 33 | final HttpException httpException = (HttpException) throwable; 34 | final Response response = requireNonNull(httpException.response(), "response cannot be null."); 35 | final ResponseBody responseBody = requireNonNull(response.errorBody(), "errorBody cannot be null."); 36 | final String json = responseBody.string(); 37 | return logFailure(String.format("%1$s: %2$s: %3$s", message, httpException.getLocalizedMessage(), json)); 38 | } catch (IOException e) { 39 | return new AppCenterException(message, e); 40 | } 41 | } 42 | 43 | return new AppCenterException(message, throwable); 44 | } 45 | } -------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/appcenter/model/appcenter/ReleaseUploadBeginResponse.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.appcenter.model.appcenter; 2 | 3 | import javax.annotation.Nonnull; 4 | import java.util.Objects; 5 | 6 | public final class ReleaseUploadBeginResponse { 7 | @Nonnull 8 | public final String id; 9 | @Nonnull 10 | public final String upload_domain; 11 | @Nonnull 12 | public final String token; 13 | @Nonnull 14 | public final String url_encoded_token; 15 | @Nonnull 16 | public final String package_asset_id; 17 | 18 | public ReleaseUploadBeginResponse(@Nonnull String id, @Nonnull String uploadDomain, @Nonnull String token, @Nonnull String urlEncodedToken, @Nonnull String packageAssetId) { 19 | this.id = id; 20 | this.upload_domain = uploadDomain; 21 | this.token = token; 22 | this.url_encoded_token = urlEncodedToken; 23 | this.package_asset_id = packageAssetId; 24 | } 25 | 26 | @Override 27 | public String toString() { 28 | return "ReleaseUploadBeginResponse{" + 29 | "id='" + id + '\'' + 30 | ", upload_domain='" + upload_domain + '\'' + 31 | ", token='" + token + '\'' + 32 | ", url_encoded_token='" + url_encoded_token + '\'' + 33 | ", package_asset_id='" + package_asset_id + '\'' + 34 | '}'; 35 | } 36 | 37 | @Override 38 | public boolean equals(Object o) { 39 | if (this == o) return true; 40 | if (o == null || getClass() != o.getClass()) return false; 41 | ReleaseUploadBeginResponse that = (ReleaseUploadBeginResponse) o; 42 | return id.equals(that.id) && 43 | upload_domain.equals(that.upload_domain) && 44 | token.equals(that.token) && 45 | url_encoded_token.equals(that.url_encoded_token) && 46 | package_asset_id.equals(that.package_asset_id); 47 | } 48 | 49 | @Override 50 | public int hashCode() { 51 | return Objects.hash(id, upload_domain, token, url_encoded_token, package_asset_id); 52 | } 53 | } -------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/appcenter/model/appcenter/ReleaseUpdateRequest.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.appcenter.model.appcenter; 2 | 3 | import javax.annotation.Nullable; 4 | import java.util.List; 5 | import java.util.Objects; 6 | 7 | public final class ReleaseUpdateRequest { 8 | 9 | @Nullable 10 | public final String release_notes; 11 | 12 | @Nullable 13 | public final Boolean mandatory_update; 14 | 15 | @Nullable 16 | public final List destinations; 17 | 18 | @Nullable 19 | public final BuildInfo build; 20 | 21 | @Nullable 22 | public final Boolean notify_testers; 23 | 24 | public ReleaseUpdateRequest(@Nullable String releaseNotes, @Nullable Boolean mandatoryUpdate, @Nullable List destinations, @Nullable BuildInfo build, @Nullable Boolean notifyTesters) { 25 | this.release_notes = releaseNotes; 26 | this.mandatory_update = mandatoryUpdate; 27 | this.destinations = destinations; 28 | this.build = build; 29 | this.notify_testers = notifyTesters; 30 | } 31 | 32 | @Override 33 | public String toString() { 34 | return "ReleaseUpdateRequest{" + 35 | "release_notes='" + release_notes + '\'' + 36 | ", mandatory_update=" + mandatory_update + 37 | ", destinations=" + destinations + 38 | ", build=" + build + 39 | ", notify_testers=" + notify_testers + 40 | '}'; 41 | } 42 | 43 | @Override 44 | public boolean equals(Object o) { 45 | if (this == o) return true; 46 | if (o == null || getClass() != o.getClass()) return false; 47 | ReleaseUpdateRequest that = (ReleaseUpdateRequest) o; 48 | return Objects.equals(release_notes, that.release_notes) && 49 | Objects.equals(mandatory_update, that.mandatory_update) && 50 | Objects.equals(destinations, that.destinations) && 51 | Objects.equals(build, that.build) && 52 | Objects.equals(notify_testers, that.notify_testers); 53 | } 54 | 55 | @Override 56 | public int hashCode() { 57 | return Objects.hash(release_notes, mandatory_update, destinations, build, notify_testers); 58 | } 59 | } -------------------------------------------------------------------------------- /src/test/java/io/jenkins/plugins/appcenter/validator/DistributionGroupsValidatorTest.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.appcenter.validator; 2 | 3 | import org.junit.Before; 4 | import org.junit.Test; 5 | 6 | import static com.google.common.truth.Truth.assertThat; 7 | 8 | public class DistributionGroupsValidatorTest { 9 | 10 | private Validator validator; 11 | 12 | @Before 13 | public void setUp() { 14 | validator = new DistributionGroupsValidator(); 15 | } 16 | 17 | @Test 18 | public void should_ReturnTrue_When_DistributionGroupsContainsAlphanumericCharacters() { 19 | // Given 20 | final String value = "Collaborators"; 21 | 22 | // When 23 | final boolean result = validator.isValid(value); 24 | 25 | // Then 26 | assertThat(result).isTrue(); 27 | } 28 | 29 | @Test 30 | public void should_ReturnTrue_When_DistributionGroupsContainsMultipleGroups() { 31 | // Given 32 | final String value = "Collaborators, internal,beta-testers "; 33 | 34 | // When 35 | final boolean result = validator.isValid(value); 36 | 37 | // Then 38 | assertThat(result).isTrue(); 39 | } 40 | 41 | @Test 42 | public void should_ReturnTrue_When_DistributionGroupsContainsComplexCharacters() { 43 | // Given 44 | final String value = "Internal test group 5 漢字 !"; 45 | 46 | // When 47 | final boolean result = validator.isValid(value); 48 | 49 | // Then 50 | assertThat(result).isTrue(); 51 | } 52 | 53 | @Test 54 | public void should_ReturnFalse_When_DistributionsGroupIsEmpty() { 55 | // Given 56 | final String value = " "; 57 | 58 | // When 59 | final boolean result = validator.isValid(value); 60 | 61 | // Then 62 | assertThat(result).isFalse(); 63 | } 64 | 65 | @Test 66 | public void should_ReturnFalse_When_DistributionsGroupContainsOnlySeparators() { 67 | // Given 68 | final String value = " ,,, ,, ,,, ,,,, , , ,"; 69 | 70 | // When 71 | final boolean result = validator.isValid(value); 72 | 73 | // Then 74 | assertThat(result).isFalse(); 75 | } 76 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AppCenter Plugin 2 | [![Jenkins Plugin](https://img.shields.io/jenkins/plugin/v/appcenter.svg)](https://plugins.jenkins.io/appcenter) 3 | [![GitHub release](https://img.shields.io/github/release/jenkinsci/appcenter-plugin.svg?label=release)](https://github.com/jenkinsci/appcenter-plugin/releases/latest) 4 | [![Jenkins Plugin Installs](https://img.shields.io/jenkins/plugin/i/appcenter.svg?color=blue)](https://plugins.jenkins.io/appcenter) 5 | 6 | Jenkins plugin to upload artefacts to [AppCenter](https://appcenter.ms). A replacement for the [HockeyApp](https://plugins.jenkins.io/hockeyapp) 7 | plugin. 8 | 9 | ## Roadmap 10 | 11 | This plugin is currently in Alpha and looking for contributors. To begin with it will aim to support the upload 12 | functionality of AppCenter. When the APIs for AppCenter become stable this plugin will be eligible to be moved out of 13 | Alpha. 14 | 15 | These are the [outstanding tickets](https://issues.jenkins-ci.org/issues/?filter=20347) for this project. 16 | 17 | ## Contributing 18 | 19 | If you would like to contribute it would be massively helpful if you followed these steps: 20 | 21 | 1. Create an issue first in the [Jenkins issue tracker](https://issues.jenkins-ci.org). 22 | * Use the component `appcenter-plugin`. 23 | 2. Create a branch from `master` referencing your issue id. 24 | 3. Commit, commit, commit. 25 | 4. Push your changes and file a PR. 26 | 27 | ## Usage Instructions 28 | 29 | Up to date syntax for this plugin can always be found in the Jenkins Pipeline Syntax Generator. However in its 30 | simplest form you can upload an artefact to AppCenter like this: 31 | 32 | ```Groovy 33 | stage('Publish') { 34 | environment { 35 | APPCENTER_API_TOKEN = credentials('at-this-moment-you-should-be-with-us') 36 | } 37 | steps { 38 | appCenter apiToken: APPCENTER_API_TOKEN, 39 | ownerName: 'janes-addiction', 40 | appName: 'ritual-de-lo-habitual', 41 | pathToApp: 'three/days/xiola.apk', 42 | distributionGroups: 'casey, niccoli' 43 | } 44 | } 45 | ``` 46 | 47 | It may sound obvious but ensure the file you are trying to upload is available on the node that you are running the 48 | plugin from. 49 | 50 | ## Changelog 51 | 52 | See [release page](https://github.com/jenkinsci/appcenter-plugin/releases). -------------------------------------------------------------------------------- /src/test/java/io/jenkins/plugins/appcenter/FreestyleTest.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.appcenter; 2 | 3 | import hudson.model.FreeStyleBuild; 4 | import hudson.model.FreeStyleProject; 5 | import hudson.model.Result; 6 | import io.jenkins.plugins.appcenter.util.MockWebServerUtil; 7 | import io.jenkins.plugins.appcenter.util.TestUtil; 8 | import okhttp3.mockwebserver.MockWebServer; 9 | import org.junit.Before; 10 | import org.junit.ClassRule; 11 | import org.junit.Rule; 12 | import org.junit.Test; 13 | import org.jvnet.hudson.test.JenkinsRule; 14 | 15 | import java.io.IOException; 16 | 17 | public class FreestyleTest { 18 | 19 | @ClassRule 20 | public static JenkinsRule jenkinsRule = new JenkinsRule(); 21 | 22 | @Rule 23 | public MockWebServer mockWebServer = new MockWebServer(); 24 | 25 | private FreeStyleProject freeStyleProject; 26 | 27 | @Before 28 | public void setUp() throws IOException { 29 | freeStyleProject = jenkinsRule.createFreeStyleProject(); 30 | freeStyleProject.getBuildersList().add(TestUtil.createFile("three/days/xiola.apk")); 31 | 32 | final AppCenterRecorder appCenterRecorder = new AppCenterRecorder("at-this-moment-you-should-be-with-us", "janes-addiction", "ritual-de-lo-habitual", "three/days/xiola.apk", "casey, niccoli"); 33 | appCenterRecorder.setBaseUrl(mockWebServer.url("/").toString()); 34 | freeStyleProject.getPublishersList().add(appCenterRecorder); 35 | } 36 | 37 | @Test 38 | public void should_SetBuildResultFailure_When_UploadTaskFails() throws Exception { 39 | // Given 40 | MockWebServerUtil.enqueueFailure(mockWebServer); 41 | 42 | // When 43 | final FreeStyleBuild freeStyleBuild = freeStyleProject.scheduleBuild2(0).get(); 44 | 45 | // Then 46 | jenkinsRule.assertBuildStatus(Result.FAILURE, freeStyleBuild); 47 | } 48 | 49 | @Test 50 | public void should_SetBuildResultSuccess_When_AppCenterAcceptsAllRequests() throws Exception { 51 | // Given 52 | MockWebServerUtil.enqueueSuccess(mockWebServer); 53 | 54 | // When 55 | final FreeStyleBuild freeStyleBuild = freeStyleProject.scheduleBuild2(0).get(); 56 | 57 | // Then 58 | jenkinsRule.assertBuildStatus(Result.SUCCESS, freeStyleBuild); 59 | } 60 | } -------------------------------------------------------------------------------- /src/test/java/io/jenkins/plugins/appcenter/NodeTest.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.appcenter; 2 | 3 | import hudson.model.FreeStyleBuild; 4 | import hudson.model.FreeStyleProject; 5 | import hudson.model.Node; 6 | import hudson.model.Result; 7 | import hudson.slaves.RetentionStrategy; 8 | import io.jenkins.plugins.appcenter.util.MockWebServerUtil; 9 | import io.jenkins.plugins.appcenter.util.TestUtil; 10 | import okhttp3.mockwebserver.MockWebServer; 11 | import org.jenkinci.plugins.mock_slave.MockSlave; 12 | import org.junit.Before; 13 | import org.junit.ClassRule; 14 | import org.junit.Rule; 15 | import org.junit.Test; 16 | import org.jvnet.hudson.test.JenkinsRule; 17 | 18 | import java.util.Collections; 19 | 20 | import static com.google.common.truth.Truth.assertThat; 21 | 22 | public class NodeTest { 23 | 24 | @ClassRule 25 | public static JenkinsRule jenkinsRule = new JenkinsRule(); 26 | 27 | @Rule 28 | public MockWebServer mockWebServer = new MockWebServer(); 29 | 30 | private FreeStyleProject freeStyleProject; 31 | 32 | private Node slave; 33 | 34 | @Before 35 | public void setUp() throws Exception { 36 | freeStyleProject = jenkinsRule.createFreeStyleProject(); 37 | freeStyleProject.getBuildersList().add(TestUtil.createFile("three/days/xiola.apk")); 38 | 39 | final AppCenterRecorder appCenterRecorder = new AppCenterRecorder("at-this-moment-you-should-be-with-us", "janes-addiction", "ritual-de-lo-habitual", "three/days/xiola.apk", "casey, niccoli"); 40 | appCenterRecorder.setBaseUrl(mockWebServer.url("/").toString()); 41 | freeStyleProject.getPublishersList().add(appCenterRecorder); 42 | 43 | slave = new MockSlave("test-slave", 1, Node.Mode.NORMAL, "", RetentionStrategy.Always.INSTANCE, Collections.emptyList()); 44 | jenkinsRule.jenkins.addNode(slave); 45 | freeStyleProject.setAssignedNode(slave); 46 | } 47 | 48 | @Test 49 | public void should_BuildFreeStyleProject_When_RunOnANode() throws Exception { 50 | // Given 51 | MockWebServerUtil.enqueueSuccess(mockWebServer); 52 | 53 | // When 54 | final FreeStyleBuild freeStyleBuild = freeStyleProject.scheduleBuild2(0).get(); 55 | 56 | // Then 57 | jenkinsRule.assertBuildStatus(Result.SUCCESS, freeStyleBuild); 58 | assertThat(freeStyleBuild.getBuiltOn()).isEqualTo(slave); 59 | } 60 | } -------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/appcenter/model/appcenter/ReleaseUpdateError.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.appcenter.model.appcenter; 2 | 3 | import javax.annotation.Nonnull; 4 | import javax.annotation.Nullable; 5 | import java.util.List; 6 | import java.util.Objects; 7 | 8 | public final class ReleaseUpdateError { 9 | 10 | @Nonnull 11 | public final CodeEnum code; 12 | 13 | @Nonnull 14 | public final String message; 15 | 16 | @Nullable 17 | public final String release_notes; 18 | 19 | @Nullable 20 | public final Boolean mandatory_update; 21 | 22 | @Nullable 23 | public final List destinations; 24 | 25 | public ReleaseUpdateError(@Nonnull CodeEnum code, @Nonnull String message, @Nullable String releaseNotes, @Nullable Boolean mandatoryUpdate, @Nullable List destinations) { 26 | this.code = code; 27 | this.message = message; 28 | this.release_notes = releaseNotes; 29 | this.mandatory_update = mandatoryUpdate; 30 | this.destinations = destinations; 31 | } 32 | 33 | @Override 34 | public String toString() { 35 | return "ReleaseUpdateError{" + 36 | "code=" + code + 37 | ", message='" + message + '\'' + 38 | ", release_notes='" + release_notes + '\'' + 39 | ", mandatory_update=" + mandatory_update + 40 | ", destinations=" + destinations + 41 | '}'; 42 | } 43 | 44 | @Override 45 | public boolean equals(Object o) { 46 | if (this == o) return true; 47 | if (o == null || getClass() != o.getClass()) return false; 48 | ReleaseUpdateError that = (ReleaseUpdateError) o; 49 | return code == that.code && 50 | message.equals(that.message) && 51 | Objects.equals(release_notes, that.release_notes) && 52 | Objects.equals(mandatory_update, that.mandatory_update) && 53 | Objects.equals(destinations, that.destinations); 54 | } 55 | 56 | @Override 57 | public int hashCode() { 58 | return Objects.hash(code, message, release_notes, mandatory_update, destinations); 59 | } 60 | 61 | public enum CodeEnum { 62 | BadRequest, 63 | Conflict, 64 | NotAcceptable, 65 | NotFound, 66 | InternalServerError, 67 | Unauthorized, 68 | TooManyRequests 69 | } 70 | } -------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/appcenter/model/appcenter/SymbolUploadBeginRequest.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.appcenter.model.appcenter; 2 | 3 | import javax.annotation.Nonnull; 4 | import javax.annotation.Nullable; 5 | import java.io.Serializable; 6 | import java.util.Objects; 7 | 8 | public final class SymbolUploadBeginRequest implements Serializable { 9 | 10 | private static final long serialVersionUID = 1L; 11 | 12 | @Nonnull 13 | public final SymbolTypeEnum symbol_type; 14 | @Nullable 15 | public final String client_callback; 16 | @Nullable 17 | public final String file_name; 18 | @Nullable 19 | public final String build; 20 | @Nullable 21 | public final String version; 22 | 23 | public SymbolUploadBeginRequest(@Nonnull SymbolTypeEnum symbolTypeEnum, @Nullable String clientCallback, @Nullable String fileName, @Nullable String build, @Nullable String version) { 24 | this.symbol_type = symbolTypeEnum; 25 | this.client_callback = clientCallback; 26 | this.file_name = fileName; 27 | this.build = build; 28 | this.version = version; 29 | } 30 | 31 | @Override 32 | public String toString() { 33 | return "SymbolUploadBeginRequest{" + 34 | "symbol_type=" + symbol_type + 35 | ", client_callback='" + client_callback + '\'' + 36 | ", file_name='" + file_name + '\'' + 37 | ", build='" + build + '\'' + 38 | ", version='" + version + '\'' + 39 | '}'; 40 | } 41 | 42 | @Override 43 | public boolean equals(Object o) { 44 | if (this == o) return true; 45 | if (o == null || getClass() != o.getClass()) return false; 46 | SymbolUploadBeginRequest that = (SymbolUploadBeginRequest) o; 47 | return symbol_type == that.symbol_type && 48 | Objects.equals(client_callback, that.client_callback) && 49 | Objects.equals(file_name, that.file_name) && 50 | Objects.equals(build, that.build) && 51 | Objects.equals(version, that.version); 52 | } 53 | 54 | @Override 55 | public int hashCode() { 56 | return Objects.hash(symbol_type, client_callback, file_name, build, version); 57 | } 58 | 59 | public enum SymbolTypeEnum { 60 | Apple, 61 | JavaScript, 62 | Breakpad, 63 | AndroidProguard, 64 | UWP 65 | } 66 | } -------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/appcenter/model/appcenter/PollForReleaseResponse.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.appcenter.model.appcenter; 2 | 3 | import javax.annotation.Nonnull; 4 | import javax.annotation.Nullable; 5 | import java.util.Objects; 6 | 7 | public final class PollForReleaseResponse { 8 | @Nonnull 9 | public final String id; 10 | @Nonnull 11 | public final StatusEnum upload_status; 12 | @Nullable 13 | public final String error_details; 14 | @Nullable 15 | public final Integer release_distinct_id; 16 | @Nullable 17 | public final String release_url; 18 | 19 | public PollForReleaseResponse(@Nonnull String id, 20 | @Nonnull StatusEnum upload_status, 21 | @Nullable String error_details, 22 | @Nullable Integer release_distinct_id, 23 | @Nullable String release_url) { 24 | 25 | this.id = id; 26 | this.upload_status = upload_status; 27 | this.error_details = error_details; 28 | this.release_distinct_id = release_distinct_id; 29 | this.release_url = release_url; 30 | } 31 | 32 | @Override 33 | public String toString() { 34 | return "PollForReleaseResponse{" + 35 | "id='" + id + '\'' + 36 | ", upload_status=" + upload_status + 37 | ", error_details='" + error_details + '\'' + 38 | ", release_distinct_id=" + release_distinct_id + 39 | ", release_url='" + release_url + '\'' + 40 | '}'; 41 | } 42 | 43 | @Override 44 | public boolean equals(Object o) { 45 | if (this == o) return true; 46 | if (o == null || getClass() != o.getClass()) return false; 47 | PollForReleaseResponse that = (PollForReleaseResponse) o; 48 | return id.equals(that.id) && upload_status == that.upload_status && Objects.equals(error_details, that.error_details) && Objects.equals(release_distinct_id, that.release_distinct_id) && Objects.equals(release_url, that.release_url); 49 | } 50 | 51 | @Override 52 | public int hashCode() { 53 | return Objects.hash(id, upload_status, error_details, release_distinct_id, release_url); 54 | } 55 | 56 | public enum StatusEnum { 57 | uploadStarted, 58 | uploadFinished, 59 | readyToBePublished, 60 | malwareDetected, 61 | error 62 | } 63 | } -------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/appcenter/util/RemoteFileUtils.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.appcenter.util; 2 | 3 | import hudson.FilePath; 4 | 5 | import javax.annotation.Nonnull; 6 | import javax.annotation.Nullable; 7 | import javax.inject.Inject; 8 | import java.io.File; 9 | import java.io.Serializable; 10 | 11 | public class RemoteFileUtils implements Serializable { 12 | 13 | private static final long serialVersionUID = 1L; 14 | 15 | @Nonnull 16 | private final FilePath filePath; 17 | 18 | @Nullable 19 | private File file; 20 | 21 | @Inject 22 | RemoteFileUtils(@Nonnull final FilePath filePath) { 23 | this.filePath = filePath; 24 | } 25 | 26 | @Nonnull 27 | public File getRemoteFile(@Nonnull String pathToRemoteFile) { 28 | if (file == null) { 29 | file = new File(filePath.child(pathToRemoteFile).getRemote()); 30 | } 31 | 32 | return file; 33 | } 34 | 35 | @Nonnull 36 | public String getFileName(@Nonnull String pathToRemoveFile) { 37 | return getRemoteFile(pathToRemoveFile).getName(); 38 | } 39 | 40 | public long getFileSize(@Nonnull String pathToRemoveFile) { 41 | return getRemoteFile(pathToRemoveFile).length(); 42 | } 43 | 44 | @Nonnull 45 | public String getContentType(@Nonnull String pathToApp) { 46 | if (pathToApp.endsWith(".apk") || pathToApp.endsWith(".aab")) return "application/vnd.android.package-archive"; 47 | if (pathToApp.endsWith(".msi")) return "application/x-msi"; 48 | if (pathToApp.endsWith(".plist")) return "application/xml"; 49 | if (pathToApp.endsWith(".aetx")) return "application/c-x509-ca-cert"; 50 | if (pathToApp.endsWith(".cer")) return "application/pkix-cert"; 51 | if (pathToApp.endsWith("xap")) return "application/x-silverlight-app"; 52 | if (pathToApp.endsWith(".appx")) return "application/x-appx"; 53 | if (pathToApp.endsWith(".appxbundle")) return "application/x-appxbundle"; 54 | if (pathToApp.endsWith(".appxupload") || pathToApp.endsWith(".appxsym")) return "application/x-appxupload"; 55 | if (pathToApp.endsWith(".msix")) return "application/x-msix"; 56 | if (pathToApp.endsWith(".msixbundle")) return "application/x-msixbundle"; 57 | if (pathToApp.endsWith(".msixupload") || pathToApp.endsWith(".msixsym")) return "application/x-msixupload"; 58 | 59 | // Otherwise 60 | return "application/octet-stream"; 61 | } 62 | } -------------------------------------------------------------------------------- /src/test/java/io/jenkins/plugins/appcenter/validator/PathToAppValidatorTest.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.appcenter.validator; 2 | 3 | import org.junit.Before; 4 | import org.junit.Test; 5 | 6 | import static com.google.common.truth.Truth.assertThat; 7 | 8 | public class PathToAppValidatorTest { 9 | 10 | private PathToAppValidator validator; 11 | 12 | @Before 13 | public void setUp() { 14 | validator = new PathToAppValidator(); 15 | } 16 | 17 | @Test 18 | public void should_ReturnFalse_When_PathIsAbsolute_Windows() { 19 | // Given 20 | final String value = "C:\\\\windows\\path\\to\\app.ipa"; 21 | 22 | // When 23 | final boolean result = validator.isValid(value); 24 | 25 | // Then 26 | assertThat(result).isFalse(); 27 | } 28 | 29 | @Test 30 | public void should_ReturnFalse_When_PathIsAbsolute_UnixLike() { 31 | // Given 32 | final String value = "/unix-like/path/to/app.apk"; 33 | 34 | // When 35 | final boolean result = validator.isValid(value); 36 | 37 | // Then 38 | assertThat(result).isFalse(); 39 | } 40 | 41 | @Test 42 | public void should_ReturnTrue_When_PathIsRelative_Windows() { 43 | // Given 44 | final String value = "path\\to\\app.ipa"; 45 | 46 | // When 47 | final boolean result = validator.isValid(value); 48 | 49 | // Then 50 | assertThat(result).isTrue(); 51 | } 52 | 53 | @Test 54 | public void should_ReturnTrue_When_PathIsRelative_UnixLike() { 55 | // Given 56 | final String value = "path/to/app.apk"; 57 | 58 | // When 59 | final boolean result = validator.isValid(value); 60 | 61 | // Then 62 | assertThat(result).isTrue(); 63 | } 64 | 65 | @Test 66 | public void should_ReturnTrue_When_PathContainsEnvVars() { 67 | // Given 68 | final String value = "path/to/app-${BUILD_NUMBER}.apk"; 69 | 70 | // When 71 | final boolean result = validator.isValid(value); 72 | 73 | // Then 74 | assertThat(result).isTrue(); 75 | } 76 | 77 | @Test 78 | public void should_ReturnWarning_When_PathStartsWithEnvVars() { 79 | // Given 80 | final String value = "${SOME_ENV_VAR}.apk"; 81 | 82 | // When 83 | final boolean result = validator.isValid(value); 84 | 85 | // Then 86 | assertThat(result).isTrue(); 87 | } 88 | } -------------------------------------------------------------------------------- /src/test/java/io/jenkins/plugins/appcenter/validator/PathToDebugSymbolsValidatorTest.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.appcenter.validator; 2 | 3 | import org.junit.Before; 4 | import org.junit.Test; 5 | 6 | import static com.google.common.truth.Truth.assertThat; 7 | 8 | public class PathToDebugSymbolsValidatorTest { 9 | 10 | private PathToDebugSymbolsValidator validator; 11 | 12 | @Before 13 | public void setUp() { 14 | validator = new PathToDebugSymbolsValidator(); 15 | } 16 | 17 | @Test 18 | public void should_ReturnFalse_When_PathIsAbsolute_Windows() { 19 | // Given 20 | final String value = "C:\\\\windows\\path\\to\\symbols.zip"; 21 | 22 | // When 23 | final boolean result = validator.isValid(value); 24 | 25 | // Then 26 | assertThat(result).isFalse(); 27 | } 28 | 29 | @Test 30 | public void should_ReturnFalse_When_PathIsAbsolute_UnixLike() { 31 | // Given 32 | final String value = "/unix-like/path/to/mappings.txt"; 33 | 34 | // When 35 | final boolean result = validator.isValid(value); 36 | 37 | // Then 38 | assertThat(result).isFalse(); 39 | } 40 | 41 | @Test 42 | public void should_ReturnTrue_When_PathIsRelative_Windows() { 43 | // Given 44 | final String value = "path\\to\\symbols.zip"; 45 | 46 | // When 47 | final boolean result = validator.isValid(value); 48 | 49 | // Then 50 | assertThat(result).isTrue(); 51 | } 52 | 53 | @Test 54 | public void should_ReturnTrue_When_PathIsRelative_UnixLike() { 55 | // Given 56 | final String value = "path/to/mappings.txt"; 57 | 58 | // When 59 | final boolean result = validator.isValid(value); 60 | 61 | // Then 62 | assertThat(result).isTrue(); 63 | } 64 | 65 | @Test 66 | public void should_ReturnTrue_When_PathContainsEnvVars() { 67 | // Given 68 | final String value = "path/to/mapping-${BUILD_NUMBER}.txt"; 69 | 70 | // When 71 | final boolean result = validator.isValid(value); 72 | 73 | // Then 74 | assertThat(result).isTrue(); 75 | } 76 | 77 | @Test 78 | public void should_ReturnWarning_When_PathStartsWithEnvVars() { 79 | // Given 80 | final String value = "${SOME_ENV_VAR}.apk"; 81 | 82 | // When 83 | final boolean result = validator.isValid(value); 84 | 85 | // Then 86 | assertThat(result).isTrue(); 87 | } 88 | } -------------------------------------------------------------------------------- /src/test/java/io/jenkins/plugins/appcenter/validator/PathToReleaseNotesValidatorTest.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.appcenter.validator; 2 | 3 | import org.junit.Before; 4 | import org.junit.Test; 5 | 6 | import static com.google.common.truth.Truth.assertThat; 7 | 8 | public class PathToReleaseNotesValidatorTest { 9 | 10 | private PathToReleaseNotesValidator validator; 11 | 12 | @Before 13 | public void setUp() { 14 | validator = new PathToReleaseNotesValidator(); 15 | } 16 | 17 | @Test 18 | public void should_ReturnFalse_When_PathIsAbsolute_Windows() { 19 | // Given 20 | final String value = "C:\\\\windows\\path\\to\\release-notes.md"; 21 | 22 | // When 23 | final boolean result = validator.isValid(value); 24 | 25 | // Then 26 | assertThat(result).isFalse(); 27 | } 28 | 29 | @Test 30 | public void should_ReturnFalse_When_PathIsAbsolute_UnixLike() { 31 | // Given 32 | final String value = "/unix-like/path/to/release-notes.md"; 33 | 34 | // When 35 | final boolean result = validator.isValid(value); 36 | 37 | // Then 38 | assertThat(result).isFalse(); 39 | } 40 | 41 | @Test 42 | public void should_ReturnTrue_When_PathIsRelative_Windows() { 43 | // Given 44 | final String value = "path\\to\\release-notes.md"; 45 | 46 | // When 47 | final boolean result = validator.isValid(value); 48 | 49 | // Then 50 | assertThat(result).isTrue(); 51 | } 52 | 53 | @Test 54 | public void should_ReturnTrue_When_PathIsRelative_UnixLike() { 55 | // Given 56 | final String value = "path/to/release-notes.md"; 57 | 58 | // When 59 | final boolean result = validator.isValid(value); 60 | 61 | // Then 62 | assertThat(result).isTrue(); 63 | } 64 | 65 | @Test 66 | public void should_ReturnTrue_When_PathContainsEnvVars() { 67 | // Given 68 | final String value = "path/to/release-notes-${BUILD_NUMBER}.md"; 69 | 70 | // When 71 | final boolean result = validator.isValid(value); 72 | 73 | // Then 74 | assertThat(result).isTrue(); 75 | } 76 | 77 | @Test 78 | public void should_ReturnWarning_When_PathStartsWithEnvVars() { 79 | // Given 80 | final String value = "${SOME_ENV_VAR}.apk"; 81 | 82 | // When 83 | final boolean result = validator.isValid(value); 84 | 85 | // Then 86 | assertThat(result).isTrue(); 87 | } 88 | } -------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/appcenter/task/internal/UpdateReleaseTask.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.appcenter.task.internal; 2 | 3 | import hudson.model.TaskListener; 4 | import io.jenkins.plugins.appcenter.AppCenterException; 5 | import io.jenkins.plugins.appcenter.AppCenterLogger; 6 | import io.jenkins.plugins.appcenter.api.AppCenterServiceFactory; 7 | import io.jenkins.plugins.appcenter.model.appcenter.UpdateReleaseUploadRequest; 8 | import io.jenkins.plugins.appcenter.task.request.UploadRequest; 9 | 10 | import javax.annotation.Nonnull; 11 | import javax.inject.Inject; 12 | import javax.inject.Singleton; 13 | import java.io.PrintStream; 14 | import java.util.concurrent.CompletableFuture; 15 | 16 | import static java.util.Objects.requireNonNull; 17 | 18 | @Singleton 19 | public final class UpdateReleaseTask implements AppCenterTask, AppCenterLogger { 20 | 21 | private static final long serialVersionUID = 1L; 22 | 23 | @Nonnull 24 | private final TaskListener taskListener; 25 | @Nonnull 26 | private final AppCenterServiceFactory factory; 27 | 28 | @Inject 29 | UpdateReleaseTask(@Nonnull final TaskListener taskListener, 30 | @Nonnull final AppCenterServiceFactory factory) { 31 | this.taskListener = taskListener; 32 | this.factory = factory; 33 | } 34 | 35 | @Nonnull 36 | @Override 37 | public CompletableFuture execute(@Nonnull UploadRequest request) { 38 | return updateRelease(request); 39 | } 40 | 41 | @Nonnull 42 | private CompletableFuture updateRelease(@Nonnull UploadRequest request) { 43 | final String uploadId = requireNonNull(request.uploadId, "uploadId cannot be null"); 44 | 45 | log("Updating release."); 46 | 47 | final CompletableFuture future = new CompletableFuture<>(); 48 | 49 | final UpdateReleaseUploadRequest updateReleaseUploadRequest = new UpdateReleaseUploadRequest(UpdateReleaseUploadRequest.StatusEnum.uploadFinished); 50 | 51 | factory.createAppCenterService() 52 | .updateReleaseUpload(request.ownerName, request.appName, uploadId, updateReleaseUploadRequest) 53 | .whenComplete((updateReleaseUploadResponse, throwable) -> { 54 | if (throwable != null) { 55 | final AppCenterException exception = logFailure("Updating release unsuccessful", throwable); 56 | future.completeExceptionally(exception); 57 | } else { 58 | log("Updating release release successful."); 59 | future.complete(request); 60 | } 61 | }); 62 | 63 | return future; 64 | } 65 | 66 | @Override 67 | public PrintStream getLogger() { 68 | return taskListener.getLogger(); 69 | } 70 | } -------------------------------------------------------------------------------- /src/test/java/io/jenkins/plugins/appcenter/validator/UsernameValidatorTest.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.appcenter.validator; 2 | 3 | import org.junit.Before; 4 | import org.junit.Test; 5 | 6 | import static com.google.common.truth.Truth.assertThat; 7 | 8 | public class UsernameValidatorTest { 9 | 10 | private Validator validator; 11 | 12 | @Before 13 | public void setUp() { 14 | validator = new UsernameValidator(); 15 | } 16 | 17 | @Test 18 | public void should_ReturnTrue_When_ApiTokenIsLowerCaseLettersOnly() { 19 | // Given 20 | final String value = "johndoe"; 21 | 22 | // When 23 | final boolean result = validator.isValid(value); 24 | 25 | // Then 26 | assertThat(result).isTrue(); 27 | } 28 | 29 | @Test 30 | public void should_ReturnTrue_When_ApiTokenIsUpperCaseLettersOnly() { 31 | // Given 32 | final String value = "JOHNDOE"; 33 | 34 | // When 35 | final boolean result = validator.isValid(value); 36 | 37 | // Then 38 | assertThat(result).isTrue(); 39 | } 40 | 41 | @Test 42 | public void should_ReturnTrue_When_ApiTokenIsNumbersOnly() { 43 | // Given 44 | final String value = "1234567890"; 45 | 46 | // When 47 | final boolean result = validator.isValid(value); 48 | 49 | // Then 50 | assertThat(result).isTrue(); 51 | } 52 | 53 | @Test 54 | public void should_ReturnTrue_When_ApiTokenIsMixOfNumbersAndLetters() { 55 | // Given 56 | final String value = "j0hNDo3"; 57 | 58 | // When 59 | final boolean result = validator.isValid(value); 60 | 61 | // Then 62 | assertThat(result).isTrue(); 63 | } 64 | 65 | @Test 66 | public void should_ReturnTrue_When_ApiTokenIsMixOfNumbersAndLettersAndDashesAndDotsAndUnderscores() { 67 | // Given 68 | final String value = "j0hNDo3-._"; 69 | 70 | // When 71 | final boolean result = validator.isValid(value); 72 | 73 | // Then 74 | assertThat(result).isTrue(); 75 | } 76 | 77 | @Test 78 | public void should_ReturnFalse_When_ApiTokenIsBlank() { 79 | // Given 80 | final String value = " "; 81 | 82 | // When 83 | final boolean result = validator.isValid(value); 84 | 85 | // Then 86 | assertThat(result).isFalse(); 87 | } 88 | 89 | @Test 90 | public void should_ReturnFalse_When_ApiTokenContainsExclamation() { 91 | // Given 92 | final String value = "johndoe!"; 93 | 94 | // When 95 | final boolean result = validator.isValid(value); 96 | 97 | // Then 98 | assertThat(result).isFalse(); 99 | } 100 | } -------------------------------------------------------------------------------- /src/main/resources/io/jenkins/plugins/appcenter/AppCenterRecorder/config.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 11 | 12 | 13 | 14 | 16 | 17 | 18 | 19 | 21 | 22 | 23 | 24 | 26 | 27 | 28 | 29 | 31 | 32 | 33 | 34 | 36 | 37 | 38 | 39 | 41 | 42 | 43 | 44 | 46 | 47 | 48 | 49 | 51 | 52 | 53 | 54 | 56 | 57 | 58 | 59 | 60 | 61 | 63 | 64 | 65 | 66 | 68 | 69 | 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/appcenter/task/UploadTask.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.appcenter.task; 2 | 3 | import io.jenkins.plugins.appcenter.AppCenterException; 4 | import io.jenkins.plugins.appcenter.task.internal.*; 5 | import io.jenkins.plugins.appcenter.task.request.UploadRequest; 6 | import jenkins.security.MasterToSlaveCallable; 7 | 8 | import javax.inject.Inject; 9 | import javax.inject.Singleton; 10 | import java.util.concurrent.CompletableFuture; 11 | 12 | @Singleton 13 | public final class UploadTask extends MasterToSlaveCallable { 14 | 15 | private final PrerequisitesTask prerequisitesTask; 16 | private final CreateUploadResourceTask createUploadResource; 17 | private final SetMetadataTask setMetadataTask; 18 | private final UploadAppToResourceTask uploadAppToResource; 19 | private final FinishReleaseTask finishReleaseTask; 20 | private final UpdateReleaseTask updateReleaseTask; 21 | private final PollForReleaseTask pollForReleaseTask; 22 | private final DistributeResourceTask distributeResource; 23 | private final UploadRequest originalRequest; 24 | 25 | @Inject 26 | UploadTask(final PrerequisitesTask prerequisitesTask, 27 | final CreateUploadResourceTask createUploadResource, 28 | final SetMetadataTask setMetadataTask, 29 | final UploadAppToResourceTask uploadAppToResource, 30 | final FinishReleaseTask finishReleaseTask, 31 | final UpdateReleaseTask updateReleaseTask, 32 | final PollForReleaseTask pollForReleaseTask, 33 | final DistributeResourceTask distributeResource, 34 | final UploadRequest request) { 35 | this.prerequisitesTask = prerequisitesTask; 36 | this.createUploadResource = createUploadResource; 37 | this.setMetadataTask = setMetadataTask; 38 | this.uploadAppToResource = uploadAppToResource; 39 | this.finishReleaseTask = finishReleaseTask; 40 | this.updateReleaseTask = updateReleaseTask; 41 | this.pollForReleaseTask = pollForReleaseTask; 42 | this.distributeResource = distributeResource; 43 | this.originalRequest = request; 44 | } 45 | 46 | @Override 47 | public Boolean call() { 48 | final CompletableFuture future = new CompletableFuture<>(); 49 | 50 | prerequisitesTask.execute(originalRequest) 51 | .thenCompose(createUploadResource::execute) 52 | .thenCompose(setMetadataTask::execute) 53 | .thenCompose(uploadAppToResource::execute) 54 | .thenCompose(finishReleaseTask::execute) 55 | .thenCompose(updateReleaseTask::execute) 56 | .thenCompose(pollForReleaseTask::execute) 57 | .thenCompose(distributeResource::execute) 58 | .whenComplete((uploadRequest, throwable) -> { 59 | if (throwable != null) { 60 | future.completeExceptionally(throwable); 61 | } else { 62 | future.complete(true); 63 | } 64 | }) 65 | .join(); 66 | 67 | return future.join(); 68 | } 69 | } -------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/appcenter/api/AppCenterService.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.appcenter.api; 2 | 3 | import io.jenkins.plugins.appcenter.model.appcenter.*; 4 | import okhttp3.RequestBody; 5 | import okhttp3.ResponseBody; 6 | import retrofit2.http.*; 7 | 8 | import javax.annotation.Nonnull; 9 | import java.util.concurrent.CompletableFuture; 10 | 11 | public interface AppCenterService { 12 | 13 | @POST("v0.1/apps/{owner_name}/{app_name}/uploads/releases") 14 | CompletableFuture releaseUploadsCreate( 15 | @Path("owner_name") @Nonnull String user, 16 | @Path("app_name") @Nonnull String appName, 17 | @Body @Nonnull ReleaseUploadBeginRequest releaseUploadBeginRequest); 18 | 19 | @POST 20 | CompletableFuture setMetaData(@Url @Nonnull String url); 21 | 22 | @Headers("Content-Type: application/octet-stream") 23 | @POST 24 | CompletableFuture uploadApp(@Url @Nonnull String url, @Body @Nonnull RequestBody file); 25 | 26 | @POST 27 | CompletableFuture finishRelease(@Url @Nonnull String url); 28 | 29 | @PATCH("v0.1/apps/{owner_name}/{app_name}/uploads/releases/{upload_id}") 30 | CompletableFuture updateReleaseUpload( 31 | @Path("owner_name") @Nonnull String user, 32 | @Path("app_name") @Nonnull String appName, 33 | @Path("upload_id") @Nonnull String uploadId, 34 | @Body @Nonnull UpdateReleaseUploadRequest updateReleaseUploadRequest); 35 | 36 | @GET("v0.1/apps/{owner_name}/{app_name}/uploads/releases/{upload_id}") 37 | CompletableFuture pollForRelease( 38 | @Path("owner_name") @Nonnull String user, 39 | @Path("app_name") @Nonnull String appName, 40 | @Path("upload_id") @Nonnull String uploadId); 41 | 42 | @PATCH("v0.1/apps/{owner_name}/{app_name}/release_uploads/{upload_id}") 43 | CompletableFuture releaseUploadsComplete( 44 | @Path("owner_name") @Nonnull String user, 45 | @Path("app_name") @Nonnull String appName, 46 | @Path("upload_id") @Nonnull String uploadId, 47 | @Body @Nonnull ReleaseUploadEndRequest releaseUploadEndRequest); 48 | 49 | @PATCH("v0.1/apps/{owner_name}/{app_name}/releases/{release_id}") 50 | CompletableFuture releasesUpdate( 51 | @Path("owner_name") @Nonnull String user, 52 | @Path("app_name") @Nonnull String appName, 53 | @Path("release_id") @Nonnull Integer releaseId, 54 | @Body @Nonnull ReleaseUpdateRequest releaseUpdateRequest); 55 | 56 | @POST("v0.1/apps/{owner_name}/{app_name}/symbol_uploads") 57 | CompletableFuture symbolUploadsCreate( 58 | @Path("owner_name") @Nonnull String user, 59 | @Path("app_name") @Nonnull String appName, 60 | @Body @Nonnull SymbolUploadBeginRequest symbolUploadBeginRequest); 61 | 62 | @PATCH("v0.1/apps/{owner_name}/{app_name}/symbol_uploads/{symbol_upload_id}") 63 | CompletableFuture symbolUploadsComplete( 64 | @Path("owner_name") @Nonnull String user, 65 | @Path("app_name") @Nonnull String appName, 66 | @Path("symbol_upload_id") @Nonnull String uploadId, 67 | @Body @Nonnull SymbolUploadEndRequest symbolUploadEndRequest); 68 | } -------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/appcenter/task/internal/SetMetadataTask.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.appcenter.task.internal; 2 | 3 | import hudson.model.TaskListener; 4 | import io.jenkins.plugins.appcenter.AppCenterException; 5 | import io.jenkins.plugins.appcenter.AppCenterLogger; 6 | import io.jenkins.plugins.appcenter.api.AppCenterServiceFactory; 7 | import io.jenkins.plugins.appcenter.task.request.UploadRequest; 8 | import io.jenkins.plugins.appcenter.util.RemoteFileUtils; 9 | 10 | import javax.annotation.Nonnull; 11 | import javax.inject.Inject; 12 | import javax.inject.Singleton; 13 | import java.io.PrintStream; 14 | import java.util.concurrent.CompletableFuture; 15 | 16 | import static java.util.Objects.requireNonNull; 17 | 18 | @Singleton 19 | public final class SetMetadataTask implements AppCenterTask, AppCenterLogger { 20 | 21 | private static final long serialVersionUID = 1L; 22 | 23 | @Nonnull 24 | private final TaskListener taskListener; 25 | @Nonnull 26 | private final AppCenterServiceFactory factory; 27 | @Nonnull 28 | private final RemoteFileUtils remoteFileUtils; 29 | 30 | @Inject 31 | SetMetadataTask(@Nonnull final TaskListener taskListener, 32 | @Nonnull final AppCenterServiceFactory factory, 33 | @Nonnull final RemoteFileUtils remoteFileUtils) { 34 | this.taskListener = taskListener; 35 | this.factory = factory; 36 | this.remoteFileUtils = remoteFileUtils; 37 | } 38 | 39 | @Nonnull 40 | @Override 41 | public CompletableFuture execute(@Nonnull UploadRequest request) { 42 | return setMetadata(request); 43 | } 44 | 45 | @Nonnull 46 | private CompletableFuture setMetadata(@Nonnull UploadRequest request) { 47 | final String uploadDomain = requireNonNull(request.uploadDomain, "uploadDomain cannot be null"); 48 | final String packageAssetId = requireNonNull(request.packageAssetId, "packageAssetId cannot be null"); 49 | final String token = requireNonNull(request.token, "token cannot be null"); 50 | 51 | log("Setting metadata."); 52 | 53 | final CompletableFuture future = new CompletableFuture<>(); 54 | 55 | final String url = getUrl(request.pathToApp, uploadDomain, packageAssetId, token); 56 | 57 | factory.createAppCenterService() 58 | .setMetaData(url) 59 | .whenComplete((setMetadataResponse, throwable) -> { 60 | if (throwable != null) { 61 | final AppCenterException exception = logFailure("Setting metadata unsuccessful", throwable); 62 | future.completeExceptionally(exception); 63 | } else { 64 | log("Setting metadata successful."); 65 | 66 | final UploadRequest uploadRequest = request.newBuilder() 67 | .setChunkSize(setMetadataResponse.chunk_size) 68 | .build(); 69 | future.complete(uploadRequest); 70 | } 71 | }); 72 | 73 | return future; 74 | } 75 | 76 | 77 | @Nonnull 78 | private String getUrl(@Nonnull String pathToApp, @Nonnull String uploadDomain, @Nonnull String packageAssetId, @Nonnull String token) { 79 | final String fileName = remoteFileUtils.getFileName(pathToApp); 80 | final long fileSize = remoteFileUtils.getFileSize(pathToApp); 81 | final String contentType = remoteFileUtils.getContentType(pathToApp); 82 | 83 | return String.format("%1$s/upload/set_metadata/%2$s?file_name=%3$s&file_size=%4$d&token=%5$s&content_type=%6$s", uploadDomain, packageAssetId, fileName, fileSize, token, contentType); 84 | } 85 | 86 | @Override 87 | public PrintStream getLogger() { 88 | return taskListener.getLogger(); 89 | } 90 | } -------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/appcenter/model/appcenter/SymbolUpload.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.appcenter.model.appcenter; 2 | 3 | import javax.annotation.Nonnull; 4 | import javax.annotation.Nullable; 5 | import java.util.List; 6 | import java.util.Objects; 7 | 8 | public final class SymbolUpload { 9 | @Nonnull 10 | public final String symbol_upload_id; 11 | 12 | @Nonnull 13 | public final String app_id; 14 | 15 | @Nullable 16 | public final SymbolUploadUserInfo user; 17 | 18 | @Nonnull 19 | public final StatusEnum status; 20 | 21 | @Nonnull 22 | public final SymbolTypeEnum symbol_type; 23 | 24 | @Nullable 25 | public final List symbols_uploaded; 26 | 27 | @Nullable 28 | public final OriginEnum origin; 29 | 30 | @Nullable 31 | public final String file_name; 32 | 33 | @Nullable 34 | public final Integer file_size; 35 | 36 | @Nullable 37 | public final String timestamp; 38 | 39 | public SymbolUpload(@Nonnull String symbolUploadId, @Nonnull String appId, @Nullable SymbolUploadUserInfo user, @Nonnull StatusEnum status, @Nonnull SymbolTypeEnum symbolType, @Nullable List symbolsUploaded, @Nullable OriginEnum origin, @Nullable String fileName, @Nullable Integer fileSize, @Nullable String timestamp) { 40 | this.symbol_upload_id = symbolUploadId; 41 | this.app_id = appId; 42 | this.user = user; 43 | this.status = status; 44 | this.symbol_type = symbolType; 45 | this.symbols_uploaded = symbolsUploaded; 46 | this.origin = origin; 47 | this.file_name = fileName; 48 | this.file_size = fileSize; 49 | this.timestamp = timestamp; 50 | } 51 | 52 | @Override 53 | public String toString() { 54 | return "SymbolUpload{" + 55 | "symbol_upload_id='" + symbol_upload_id + '\'' + 56 | ", app_id='" + app_id + '\'' + 57 | ", user=" + user + 58 | ", status=" + status + 59 | ", symbol_type=" + symbol_type + 60 | ", symbols_uploaded=" + symbols_uploaded + 61 | ", origin=" + origin + 62 | ", file_name='" + file_name + '\'' + 63 | ", file_size=" + file_size + 64 | ", timestamp='" + timestamp + '\'' + 65 | '}'; 66 | } 67 | 68 | @Override 69 | public boolean equals(Object o) { 70 | if (this == o) return true; 71 | if (o == null || getClass() != o.getClass()) return false; 72 | SymbolUpload that = (SymbolUpload) o; 73 | return symbol_upload_id.equals(that.symbol_upload_id) && 74 | app_id.equals(that.app_id) && 75 | Objects.equals(user, that.user) && 76 | status == that.status && 77 | symbol_type == that.symbol_type && 78 | Objects.equals(symbols_uploaded, that.symbols_uploaded) && 79 | origin == that.origin && 80 | Objects.equals(file_name, that.file_name) && 81 | Objects.equals(file_size, that.file_size) && 82 | Objects.equals(timestamp, that.timestamp); 83 | } 84 | 85 | @Override 86 | public int hashCode() { 87 | return Objects.hash(symbol_upload_id, app_id, user, status, symbol_type, symbols_uploaded, origin, file_name, file_size, timestamp); 88 | } 89 | 90 | public enum StatusEnum { 91 | created, 92 | committed, 93 | aborted, 94 | processing, 95 | indexed, 96 | failed 97 | } 98 | 99 | public enum SymbolTypeEnum { 100 | Apple, 101 | JavaScript, 102 | Breakpad, 103 | AndroidProguard, 104 | UWP 105 | } 106 | 107 | public enum OriginEnum { 108 | User, 109 | System 110 | } 111 | } -------------------------------------------------------------------------------- /src/test/java/io/jenkins/plugins/appcenter/task/internal/UpdateReleaseTaskTest.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.appcenter.task.internal; 2 | 3 | import hudson.ProxyConfiguration; 4 | import hudson.model.TaskListener; 5 | import hudson.util.Secret; 6 | import io.jenkins.plugins.appcenter.AppCenterException; 7 | import io.jenkins.plugins.appcenter.api.AppCenterServiceFactory; 8 | import io.jenkins.plugins.appcenter.task.request.UploadRequest; 9 | import okhttp3.mockwebserver.MockResponse; 10 | import okhttp3.mockwebserver.MockWebServer; 11 | import org.junit.Before; 12 | import org.junit.Rule; 13 | import org.junit.Test; 14 | import org.junit.function.ThrowingRunnable; 15 | import org.junit.runner.RunWith; 16 | import org.mockito.Mock; 17 | import org.mockito.junit.MockitoJUnitRunner; 18 | 19 | import java.io.PrintStream; 20 | import java.util.concurrent.ExecutionException; 21 | 22 | import static com.google.common.truth.Truth.assertThat; 23 | import static org.junit.Assert.assertThrows; 24 | import static org.mockito.BDDMockito.given; 25 | 26 | @RunWith(MockitoJUnitRunner.class) 27 | public class UpdateReleaseTaskTest { 28 | 29 | @Rule 30 | public MockWebServer mockWebServer = new MockWebServer(); 31 | 32 | @Mock 33 | TaskListener mockTaskListener; 34 | 35 | @Mock 36 | PrintStream mockLogger; 37 | 38 | @Mock 39 | ProxyConfiguration mockProxyConfig; 40 | 41 | private UploadRequest baseRequest; 42 | 43 | private UpdateReleaseTask task; 44 | 45 | @Before 46 | public void setUp() { 47 | baseRequest = new UploadRequest.Builder() 48 | .setOwnerName("owner-name") 49 | .setAppName("app-name") 50 | .setPathToApp("path-to-app") 51 | .build(); 52 | given(mockTaskListener.getLogger()).willReturn(mockLogger); 53 | final AppCenterServiceFactory factory = new AppCenterServiceFactory(Secret.fromString("secret-token"), mockWebServer.url("/").toString(), mockProxyConfig); 54 | task = new UpdateReleaseTask(mockTaskListener, factory); 55 | } 56 | 57 | @Test 58 | public void should_ReturnException_When_UploadIdIsMissing() { 59 | // Given 60 | final UploadRequest uploadRequest = baseRequest.newBuilder() 61 | .build(); 62 | 63 | // When 64 | final ThrowingRunnable throwingRunnable = () -> task.execute(uploadRequest).get(); 65 | 66 | // Then 67 | final NullPointerException exception = assertThrows(NullPointerException.class, throwingRunnable); 68 | assertThat(exception).hasMessageThat().contains("uploadId cannot be null"); 69 | } 70 | 71 | @Test 72 | public void should_ReturnResponse_When_RequestIsSuccessful() throws Exception { 73 | // Given 74 | final UploadRequest uploadRequest = baseRequest.newBuilder() 75 | .setUploadId("upload_id") 76 | .build(); 77 | mockWebServer.enqueue(new MockResponse().setResponseCode(200).setBody("{" + 78 | "\"id\": \"upload_id\",\n" + 79 | "\"upload_status\": \"uploadFinished\"\n" + 80 | "}") 81 | ); 82 | 83 | // When 84 | final UploadRequest actual = task.execute(uploadRequest).get(); 85 | 86 | // Then 87 | assertThat(actual) 88 | .isEqualTo(uploadRequest); 89 | } 90 | 91 | @Test 92 | public void should_ReturnException_When_RequestIsUnSuccessful() { 93 | // Given 94 | final UploadRequest uploadRequest = baseRequest.newBuilder() 95 | .setUploadId("upload_id") 96 | .build(); 97 | mockWebServer.enqueue(new MockResponse().setResponseCode(400)); 98 | 99 | // When 100 | final ThrowingRunnable throwingRunnable = () -> task.execute(uploadRequest).get(); 101 | 102 | // Then 103 | final ExecutionException exception = assertThrows(ExecutionException.class, throwingRunnable); 104 | assertThat(exception).hasCauseThat().isInstanceOf(AppCenterException.class); 105 | assertThat(exception).hasCauseThat().hasMessageThat().contains("Updating release unsuccessful"); 106 | } 107 | } -------------------------------------------------------------------------------- /src/test/java/io/jenkins/plugins/appcenter/ConfigurationTest.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.appcenter; 2 | 3 | import com.gargoylesoftware.htmlunit.html.HtmlForm; 4 | import hudson.model.FreeStyleProject; 5 | import org.junit.Before; 6 | import org.junit.ClassRule; 7 | import org.junit.Test; 8 | import org.jvnet.hudson.test.JenkinsRule; 9 | 10 | public class ConfigurationTest { 11 | 12 | @ClassRule 13 | public static JenkinsRule jenkinsRule = new JenkinsRule(); 14 | 15 | private FreeStyleProject freeStyleProject; 16 | 17 | @Before 18 | public void setUp() throws Exception { 19 | freeStyleProject = jenkinsRule.createFreeStyleProject(); 20 | } 21 | 22 | @Test 23 | public void should_Configure_RequiredParameters_ViaWebForm() throws Exception { 24 | // Given 25 | final AppCenterRecorder appCenterRecorder = new AppCenterRecorder("at-this-moment-you-should-be-with-us", "janes-addiction", "ritual-de-lo-habitual", "three/days/xiola.apk", "casey, niccoli"); 26 | freeStyleProject.getPublishersList().add(appCenterRecorder); 27 | 28 | final HtmlForm htmlForm = jenkinsRule.createWebClient().getPage(freeStyleProject, "configure").getFormByName("config"); 29 | jenkinsRule.submit(htmlForm); 30 | 31 | // When 32 | final AppCenterRecorder configuredAppCenterRecorder = freeStyleProject.getPublishersList().get(AppCenterRecorder.class); 33 | 34 | // Then 35 | jenkinsRule.assertEqualDataBoundBeans(appCenterRecorder, configuredAppCenterRecorder); 36 | } 37 | 38 | @Test 39 | public void should_Configure_OptionalReleaseNotes_ViaWebForm() throws Exception { 40 | // Given 41 | final AppCenterRecorder appCenterRecorder = new AppCenterRecorder("at-this-moment-you-should-be-with-us", "janes-addiction", "ritual-de-lo-habitual", "three/days/xiola.apk", "casey, niccoli"); 42 | appCenterRecorder.setReleaseNotes("I miss you my dear Xiola"); 43 | freeStyleProject.getPublishersList().add(appCenterRecorder); 44 | 45 | final HtmlForm htmlForm = jenkinsRule.createWebClient().getPage(freeStyleProject, "configure").getFormByName("config"); 46 | jenkinsRule.submit(htmlForm); 47 | 48 | // When 49 | final AppCenterRecorder configuredAppCenterRecorder = freeStyleProject.getPublishersList().get(AppCenterRecorder.class); 50 | 51 | // Then 52 | jenkinsRule.assertEqualDataBoundBeans(appCenterRecorder, configuredAppCenterRecorder); 53 | } 54 | 55 | @Test 56 | public void should_Configure_OptionalNotifyTesters_ViaWebForm() throws Exception { 57 | // Given 58 | final AppCenterRecorder appCenterRecorder = new AppCenterRecorder("at-this-moment-you-should-be-with-us", "janes-addiction", "ritual-de-lo-habitual", "three/days/xiola.apk", "casey, niccoli"); 59 | appCenterRecorder.setNotifyTesters(false); 60 | freeStyleProject.getPublishersList().add(appCenterRecorder); 61 | 62 | final HtmlForm htmlForm = jenkinsRule.createWebClient().getPage(freeStyleProject, "configure").getFormByName("config"); 63 | jenkinsRule.submit(htmlForm); 64 | 65 | // When 66 | final AppCenterRecorder configuredAppCenterRecorder = freeStyleProject.getPublishersList().get(AppCenterRecorder.class); 67 | 68 | // Then 69 | jenkinsRule.assertEqualDataBoundBeans(appCenterRecorder, configuredAppCenterRecorder); 70 | } 71 | 72 | @Test 73 | public void should_Configure_OptionalDebugSymbols_ViaWebForm() throws Exception { 74 | // Given 75 | final AppCenterRecorder appCenterRecorder = new AppCenterRecorder("at-this-moment-you-should-be-with-us", "janes-addiction", "ritual-de-lo-habitual", "three/days/xiola.apk", "casey, niccoli"); 76 | appCenterRecorder.setPathToDebugSymbols("path/to/linear-notes.txt"); 77 | freeStyleProject.getPublishersList().add(appCenterRecorder); 78 | 79 | final HtmlForm htmlForm = jenkinsRule.createWebClient().getPage(freeStyleProject, "configure").getFormByName("config"); 80 | jenkinsRule.submit(htmlForm); 81 | 82 | // When 83 | final AppCenterRecorder configuredAppCenterRecorder = freeStyleProject.getPublishersList().get(AppCenterRecorder.class); 84 | 85 | // Then 86 | jenkinsRule.assertEqualDataBoundBeans(appCenterRecorder, configuredAppCenterRecorder); 87 | } 88 | } -------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/appcenter/task/internal/FinishReleaseTask.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.appcenter.task.internal; 2 | 3 | import hudson.model.TaskListener; 4 | import io.jenkins.plugins.appcenter.AppCenterException; 5 | import io.jenkins.plugins.appcenter.AppCenterLogger; 6 | import io.jenkins.plugins.appcenter.api.AppCenterServiceFactory; 7 | import io.jenkins.plugins.appcenter.model.appcenter.SymbolUploadEndRequest; 8 | import io.jenkins.plugins.appcenter.task.request.UploadRequest; 9 | 10 | import javax.annotation.Nonnull; 11 | import javax.inject.Inject; 12 | import javax.inject.Singleton; 13 | import java.io.PrintStream; 14 | import java.util.concurrent.CompletableFuture; 15 | 16 | import static java.util.Objects.requireNonNull; 17 | 18 | @Singleton 19 | public final class FinishReleaseTask implements AppCenterTask, AppCenterLogger { 20 | 21 | private static final long serialVersionUID = 1L; 22 | 23 | @Nonnull 24 | private final TaskListener taskListener; 25 | @Nonnull 26 | private final AppCenterServiceFactory factory; 27 | 28 | @Inject 29 | FinishReleaseTask(@Nonnull final TaskListener taskListener, 30 | @Nonnull final AppCenterServiceFactory factory) { 31 | this.taskListener = taskListener; 32 | this.factory = factory; 33 | } 34 | 35 | @Nonnull 36 | @Override 37 | public CompletableFuture execute(@Nonnull UploadRequest request) { 38 | 39 | 40 | if (request.symbolUploadId == null) { 41 | return finishRelease(request); 42 | } else { 43 | return finishRelease(request) 44 | .thenCompose(this::finishSymbolRelease); 45 | } 46 | } 47 | 48 | @Nonnull 49 | private CompletableFuture finishRelease(@Nonnull UploadRequest request) { 50 | final String uploadDomain = requireNonNull(request.uploadDomain, "uploadDomain cannot be null"); 51 | final String packageAssetId = requireNonNull(request.packageAssetId, "packageAssetId cannot be null"); 52 | final String token = requireNonNull(request.token, "token cannot be null"); 53 | 54 | log("Finishing release."); 55 | 56 | final CompletableFuture future = new CompletableFuture<>(); 57 | 58 | final String url = getUrl(uploadDomain, packageAssetId, token); 59 | 60 | factory.createAppCenterService() 61 | .finishRelease(url) 62 | .whenComplete((finishReleaseResponse, throwable) -> { 63 | if (throwable != null) { 64 | final AppCenterException exception = logFailure("Finishing release unsuccessful", throwable); 65 | future.completeExceptionally(exception); 66 | } else { 67 | log("Finishing release successful."); 68 | future.complete(request); 69 | } 70 | }); 71 | 72 | return future; 73 | } 74 | 75 | @Nonnull 76 | private String getUrl(@Nonnull String uploadDomain, @Nonnull String packageAssetId, @Nonnull String token) { 77 | return String.format("%1$s/upload/finished/%2$s?token=%3$s", uploadDomain, packageAssetId, token); 78 | } 79 | 80 | @Nonnull 81 | private CompletableFuture finishSymbolRelease(@Nonnull UploadRequest request) { 82 | final String symbolUploadId = requireNonNull(request.symbolUploadId, "symbolUploadId cannot be null"); 83 | 84 | log("Finishing symbol release."); 85 | 86 | final CompletableFuture future = new CompletableFuture<>(); 87 | final SymbolUploadEndRequest symbolUploadEndRequest = new SymbolUploadEndRequest(SymbolUploadEndRequest.StatusEnum.committed); 88 | 89 | factory.createAppCenterService() 90 | .symbolUploadsComplete(request.ownerName, request.appName, symbolUploadId, symbolUploadEndRequest) 91 | .whenComplete((symbolUploadEndResponse, throwable) -> { 92 | if (throwable != null) { 93 | final AppCenterException exception = logFailure("Finishing symbol release unsuccessful: ", throwable); 94 | future.completeExceptionally(exception); 95 | } else { 96 | log("Finishing symbol release successful."); 97 | future.complete(request); 98 | } 99 | }); 100 | 101 | return future; 102 | } 103 | 104 | @Override 105 | public PrintStream getLogger() { 106 | return taskListener.getLogger(); 107 | } 108 | } -------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/appcenter/task/internal/PollForReleaseTask.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.appcenter.task.internal; 2 | 3 | import hudson.model.TaskListener; 4 | import io.jenkins.plugins.appcenter.AppCenterException; 5 | import io.jenkins.plugins.appcenter.AppCenterLogger; 6 | import io.jenkins.plugins.appcenter.api.AppCenterServiceFactory; 7 | import io.jenkins.plugins.appcenter.task.request.UploadRequest; 8 | 9 | import javax.annotation.Nonnull; 10 | import javax.inject.Inject; 11 | import javax.inject.Singleton; 12 | import java.io.PrintStream; 13 | import java.util.concurrent.CompletableFuture; 14 | import java.util.concurrent.TimeUnit; 15 | 16 | import static java.util.Objects.requireNonNull; 17 | 18 | @Singleton 19 | public final class PollForReleaseTask implements AppCenterTask, AppCenterLogger { 20 | 21 | private static final long serialVersionUID = 1L; 22 | 23 | @Nonnull 24 | private final TaskListener taskListener; 25 | @Nonnull 26 | private final AppCenterServiceFactory factory; 27 | 28 | @Inject 29 | PollForReleaseTask(@Nonnull final TaskListener taskListener, 30 | @Nonnull final AppCenterServiceFactory factory) { 31 | this.taskListener = taskListener; 32 | this.factory = factory; 33 | } 34 | 35 | @Nonnull 36 | @Override 37 | public CompletableFuture execute(@Nonnull UploadRequest request) { 38 | return pollForRelease(request); 39 | } 40 | 41 | @Nonnull 42 | private CompletableFuture pollForRelease(@Nonnull UploadRequest request) { 43 | final String uploadId = requireNonNull(request.uploadId, "uploadId cannot be null"); 44 | 45 | log("Polling for app release."); 46 | 47 | final CompletableFuture future = new CompletableFuture<>(); 48 | 49 | poll(request, uploadId, future); 50 | 51 | return future; 52 | } 53 | 54 | private void poll(@Nonnull UploadRequest request, @Nonnull String uploadId, @Nonnull CompletableFuture future) { 55 | poll(request, uploadId, future, 0); 56 | } 57 | 58 | private void poll(@Nonnull UploadRequest request, @Nonnull String uploadId, @Nonnull CompletableFuture future, int timeoutExponent) { 59 | factory.createAppCenterService() 60 | .pollForRelease(request.ownerName, request.appName, uploadId) 61 | .whenComplete((pollForReleaseResponse, throwable) -> { 62 | if (throwable != null) { 63 | final AppCenterException exception = logFailure("Polling for app release unsuccessful", throwable); 64 | future.completeExceptionally(exception); 65 | } else { 66 | switch (pollForReleaseResponse.upload_status) { 67 | case uploadStarted: 68 | case uploadFinished: 69 | retryPolling(request, uploadId, future, timeoutExponent); 70 | break; 71 | case readyToBePublished: 72 | log("Polling for app release successful."); 73 | final UploadRequest uploadRequest = request.newBuilder() 74 | .setReleaseId(pollForReleaseResponse.release_distinct_id) 75 | .build(); 76 | future.complete(uploadRequest); 77 | break; 78 | case malwareDetected: 79 | case error: 80 | future.completeExceptionally(logFailure("Polling for app release successful however was rejected by server: " + pollForReleaseResponse.error_details)); 81 | break; 82 | default: 83 | future.completeExceptionally(logFailure("Polling for app release successful however unexpected enum returned from server: " + pollForReleaseResponse.upload_status)); 84 | } 85 | } 86 | }); 87 | } 88 | 89 | private void retryPolling(@Nonnull UploadRequest request, @Nonnull String uploadId, @Nonnull CompletableFuture future, int timeoutExponent) { 90 | try { 91 | final double pow = Math.pow(2, timeoutExponent); 92 | final long timeout = Double.valueOf(pow).longValue(); 93 | log(String.format("Polling for app release successful however not yet ready will try again in %d seconds.", timeout)); 94 | TimeUnit.SECONDS.sleep(timeout); 95 | poll(request, uploadId, future, timeoutExponent + 1); 96 | } catch (InterruptedException e) { 97 | e.printStackTrace(); 98 | } 99 | } 100 | 101 | @Override 102 | public PrintStream getLogger() { 103 | return taskListener.getLogger(); 104 | } 105 | } -------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/appcenter/task/internal/CreateUploadResourceTask.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.appcenter.task.internal; 2 | 3 | import hudson.model.TaskListener; 4 | import io.jenkins.plugins.appcenter.AppCenterException; 5 | import io.jenkins.plugins.appcenter.AppCenterLogger; 6 | import io.jenkins.plugins.appcenter.api.AppCenterServiceFactory; 7 | import io.jenkins.plugins.appcenter.model.appcenter.ReleaseUploadBeginRequest; 8 | import io.jenkins.plugins.appcenter.model.appcenter.SymbolUploadBeginRequest; 9 | import io.jenkins.plugins.appcenter.task.request.UploadRequest; 10 | 11 | import javax.annotation.Nonnull; 12 | import javax.inject.Inject; 13 | import javax.inject.Singleton; 14 | import java.io.PrintStream; 15 | import java.util.concurrent.CompletableFuture; 16 | 17 | import static java.util.Objects.requireNonNull; 18 | 19 | @Singleton 20 | public final class CreateUploadResourceTask implements AppCenterTask, AppCenterLogger { 21 | 22 | private static final long serialVersionUID = 1L; 23 | 24 | @Nonnull 25 | private final TaskListener taskListener; 26 | @Nonnull 27 | private final AppCenterServiceFactory factory; 28 | 29 | @Inject 30 | CreateUploadResourceTask(@Nonnull final TaskListener taskListener, 31 | @Nonnull final AppCenterServiceFactory factory) { 32 | this.taskListener = taskListener; 33 | this.factory = factory; 34 | } 35 | 36 | @Nonnull 37 | @Override 38 | public CompletableFuture execute(@Nonnull UploadRequest request) { 39 | if (request.symbolUploadRequest == null) { 40 | return createUploadResourceForApp(request); 41 | } else { 42 | return createUploadResourceForApp(request) 43 | .thenCompose(this::createUploadResourceForDebugSymbols); 44 | } 45 | } 46 | 47 | @Nonnull 48 | private CompletableFuture createUploadResourceForApp(@Nonnull UploadRequest request) { 49 | log("Creating an upload resource for app."); 50 | 51 | final CompletableFuture future = new CompletableFuture<>(); 52 | 53 | final ReleaseUploadBeginRequest releaseUploadBeginRequest = new ReleaseUploadBeginRequest(request.buildVersion, null); 54 | factory.createAppCenterService() 55 | .releaseUploadsCreate(request.ownerName, request.appName, releaseUploadBeginRequest) 56 | .whenComplete((releaseUploadBeginResponse, throwable) -> { 57 | if (throwable != null) { 58 | final AppCenterException exception = logFailure("Create upload resource for app unsuccessful", throwable); 59 | future.completeExceptionally(exception); 60 | } else { 61 | log("Create upload resource for app successful."); 62 | final UploadRequest uploadRequest = request.newBuilder() 63 | .setUploadId(releaseUploadBeginResponse.id) 64 | .setUploadDomain(releaseUploadBeginResponse.upload_domain) 65 | .setToken(releaseUploadBeginResponse.url_encoded_token) 66 | .setPackageAssetId(releaseUploadBeginResponse.package_asset_id) 67 | .build(); 68 | future.complete(uploadRequest); 69 | } 70 | }); 71 | 72 | return future; 73 | } 74 | 75 | @Nonnull 76 | private CompletableFuture createUploadResourceForDebugSymbols(@Nonnull UploadRequest request) { 77 | final SymbolUploadBeginRequest symbolUploadRequest = requireNonNull(request.symbolUploadRequest, "symbolUploadRequest cannot be null"); 78 | 79 | log("Creating an upload resource for debug symbols."); 80 | 81 | final CompletableFuture future = new CompletableFuture<>(); 82 | 83 | factory.createAppCenterService() 84 | .symbolUploadsCreate(request.ownerName, request.appName, symbolUploadRequest) 85 | .whenComplete((symbolsUploadBeginResponse, throwable) -> { 86 | if (throwable != null) { 87 | final AppCenterException exception = logFailure("Create upload resource for debug symbols unsuccessful: ", throwable); 88 | future.completeExceptionally(exception); 89 | } else { 90 | log("Create upload resource for debug symbols successful."); 91 | final UploadRequest uploadRequest = request.newBuilder() 92 | .setSymbolUploadUrl(symbolsUploadBeginResponse.upload_url) 93 | .setSymbolUploadId(symbolsUploadBeginResponse.symbol_upload_id) 94 | .build(); 95 | future.complete(uploadRequest); 96 | } 97 | }); 98 | 99 | return future; 100 | } 101 | 102 | @Override 103 | public PrintStream getLogger() { 104 | return taskListener.getLogger(); 105 | } 106 | } -------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/appcenter/task/internal/DistributeResourceTask.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.appcenter.task.internal; 2 | 3 | import hudson.FilePath; 4 | import hudson.model.TaskListener; 5 | import io.jenkins.plugins.appcenter.AppCenterException; 6 | import io.jenkins.plugins.appcenter.AppCenterLogger; 7 | import io.jenkins.plugins.appcenter.api.AppCenterServiceFactory; 8 | import io.jenkins.plugins.appcenter.model.appcenter.BuildInfo; 9 | import io.jenkins.plugins.appcenter.model.appcenter.DestinationId; 10 | import io.jenkins.plugins.appcenter.model.appcenter.ReleaseUpdateRequest; 11 | import io.jenkins.plugins.appcenter.task.request.UploadRequest; 12 | import org.apache.commons.lang.StringUtils; 13 | 14 | import javax.annotation.Nonnull; 15 | import javax.annotation.Nullable; 16 | import javax.inject.Inject; 17 | import javax.inject.Singleton; 18 | import java.io.IOException; 19 | import java.io.PrintStream; 20 | import java.util.List; 21 | import java.util.concurrent.CompletableFuture; 22 | import java.util.stream.Collectors; 23 | import java.util.stream.Stream; 24 | 25 | import static java.util.Objects.requireNonNull; 26 | 27 | @Singleton 28 | public final class DistributeResourceTask implements AppCenterTask, AppCenterLogger { 29 | 30 | private static final long serialVersionUID = 1L; 31 | 32 | @Nonnull 33 | private final TaskListener taskListener; 34 | @Nonnull 35 | private final FilePath filePath; 36 | @Nonnull 37 | private final AppCenterServiceFactory factory; 38 | 39 | @Inject 40 | DistributeResourceTask(@Nonnull final TaskListener taskListener, 41 | @Nonnull final FilePath filePath, 42 | @Nonnull final AppCenterServiceFactory factory) { 43 | this.taskListener = taskListener; 44 | this.filePath = filePath; 45 | this.factory = factory; 46 | } 47 | 48 | @Nonnull 49 | @Override 50 | public CompletableFuture execute(@Nonnull UploadRequest request) { 51 | final Integer releaseId = requireNonNull(request.releaseId, "releaseId cannot be null"); 52 | 53 | log("Distributing resource."); 54 | 55 | final CompletableFuture future = new CompletableFuture<>(); 56 | 57 | final String releaseNotes = parseReleaseNotes(request); 58 | final boolean mandatoryUpdate = request.mandatoryUpdate; 59 | final List destinations = Stream.of(request.destinationGroups.split(",")) 60 | .map(String::trim) 61 | .map(name -> new DestinationId(name, null)) 62 | .collect(Collectors.toList()); 63 | final boolean notifyTesters = request.notifyTesters; 64 | 65 | final ReleaseUpdateRequest releaseDetailsUpdateRequest = new ReleaseUpdateRequest(releaseNotes, mandatoryUpdate, destinations, createBuildInfo(request), notifyTesters); 66 | 67 | factory.createAppCenterService() 68 | .releasesUpdate(request.ownerName, request.appName, releaseId, releaseDetailsUpdateRequest) 69 | .whenComplete((releaseUploadBeginResponse, throwable) -> { 70 | if (throwable != null) { 71 | final AppCenterException exception = logFailure("Distributing resource unsuccessful", throwable); 72 | future.completeExceptionally(exception); 73 | } else { 74 | log("Distributing resource successful."); 75 | future.complete(request); 76 | } 77 | }); 78 | 79 | return future; 80 | } 81 | 82 | @Nonnull 83 | private String parseReleaseNotes(@Nonnull UploadRequest request) { 84 | final String releaseNotesFromFile = parseReleaseNotesFromFile(request.pathToReleaseNotes); 85 | final String separator = (!request.releaseNotes.isEmpty() && !releaseNotesFromFile.isEmpty()) ? "\n\n" : ""; 86 | final String combinedReleaseNotes = request.releaseNotes + separator + releaseNotesFromFile; 87 | 88 | return StringUtils.left(combinedReleaseNotes, 5000); 89 | } 90 | 91 | @Nonnull 92 | private String parseReleaseNotesFromFile(@Nonnull String pathToReleaseNotes) { 93 | if (pathToReleaseNotes.isEmpty()) return ""; 94 | 95 | final FilePath releaseNotesFilePath = filePath.child(pathToReleaseNotes); 96 | try { 97 | return releaseNotesFilePath.readToString(); 98 | } catch (IOException | InterruptedException e) { 99 | log(String.format("Unable to read release note file due to: %1$s", e)); 100 | return ""; 101 | } 102 | 103 | } 104 | 105 | @Nullable 106 | private BuildInfo createBuildInfo(@Nonnull UploadRequest request) { 107 | if (request.branchName == null && request.commitHash == null) return null; 108 | return new BuildInfo(request.branchName, request.commitHash, null); 109 | } 110 | 111 | @Override 112 | public PrintStream getLogger() { 113 | return taskListener.getLogger(); 114 | } 115 | } -------------------------------------------------------------------------------- /src/test/java/io/jenkins/plugins/appcenter/ProxyTest.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.appcenter; 2 | 3 | import com.google.common.net.HttpHeaders; 4 | import hudson.ProxyConfiguration; 5 | import hudson.model.FreeStyleBuild; 6 | import hudson.model.FreeStyleProject; 7 | import hudson.model.Result; 8 | import io.jenkins.plugins.appcenter.util.MockWebServerUtil; 9 | import io.jenkins.plugins.appcenter.util.TestUtil; 10 | import okhttp3.Credentials; 11 | import okhttp3.mockwebserver.MockWebServer; 12 | import org.junit.Before; 13 | import org.junit.ClassRule; 14 | import org.junit.Rule; 15 | import org.junit.Test; 16 | import org.jvnet.hudson.test.JenkinsRule; 17 | 18 | import java.io.IOException; 19 | 20 | import static com.google.common.truth.Truth.assertThat; 21 | 22 | public class ProxyTest { 23 | 24 | @ClassRule 25 | public static JenkinsRule jenkinsRule = new JenkinsRule(); 26 | 27 | @Rule 28 | public MockWebServer mockWebServer = new MockWebServer(); 29 | 30 | @Rule 31 | public MockWebServer proxyWebServer = new MockWebServer(); 32 | 33 | private FreeStyleProject freeStyleProject; 34 | 35 | @Before 36 | public void setUp() throws IOException { 37 | freeStyleProject = jenkinsRule.createFreeStyleProject(); 38 | freeStyleProject.getBuildersList().add(TestUtil.createFile("three/days/xiola.apk")); 39 | 40 | final AppCenterRecorder appCenterRecorder = new AppCenterRecorder("at-this-moment-you-should-be-with-us", "janes-addiction", "ritual-de-lo-habitual", "three/days/xiola.apk", "casey, niccoli"); 41 | appCenterRecorder.setBaseUrl(mockWebServer.url("/").toString()); // Notice this is *not* set to the proxy address 42 | freeStyleProject.getPublishersList().add(appCenterRecorder); 43 | } 44 | 45 | @Test 46 | public void should_SendRequestsDirectly_When_NoProxyConfigurationFound() throws Exception { 47 | // Given 48 | jenkinsRule.jenkins.proxy = null; 49 | MockWebServerUtil.enqueueSuccess(mockWebServer); 50 | 51 | // When 52 | final FreeStyleBuild freeStyleBuild = freeStyleProject.scheduleBuild2(0).get(); 53 | 54 | // Then 55 | jenkinsRule.assertBuildStatus(Result.SUCCESS, freeStyleBuild); 56 | assertThat(proxyWebServer.getRequestCount()).isEqualTo(0); 57 | assertThat(mockWebServer.getRequestCount()).isEqualTo(7); 58 | } 59 | 60 | @Test 61 | public void should_SendRequestsToProxy_When_ProxyConfigurationFound() throws Exception { 62 | // Given 63 | jenkinsRule.jenkins.proxy = new ProxyConfiguration(proxyWebServer.getHostName(), proxyWebServer.getPort()); 64 | MockWebServerUtil.enqueueSuccess(proxyWebServer); 65 | 66 | // When 67 | final FreeStyleBuild freeStyleBuild = freeStyleProject.scheduleBuild2(0).get(); 68 | 69 | // Then 70 | jenkinsRule.assertBuildStatus(Result.SUCCESS, freeStyleBuild); 71 | assertThat(proxyWebServer.getRequestCount()).isEqualTo(7); 72 | assertThat(mockWebServer.getRequestCount()).isEqualTo(0); 73 | } 74 | 75 | 76 | @Test 77 | public void should_SendProxyAuthorizationHeader_When_ProxyCredentialsConfigured() throws Exception { 78 | // Given 79 | final String userName = "user"; 80 | final String password = "password"; 81 | jenkinsRule.jenkins.proxy = new ProxyConfiguration(proxyWebServer.getHostName(), proxyWebServer.getPort(), userName, password); 82 | 83 | MockWebServerUtil.enqueueProxyAuthRequired(proxyWebServer); // first request rejected and proxy authentication requested 84 | MockWebServerUtil.enqueueSuccess(proxyWebServer); 85 | 86 | // When 87 | final FreeStyleBuild freeStyleBuild = freeStyleProject.scheduleBuild2(0).get(); 88 | 89 | // Then 90 | jenkinsRule.assertBuildStatus(Result.SUCCESS, freeStyleBuild); 91 | assertThat(proxyWebServer.getRequestCount()).isEqualTo(8); 92 | assertThat(mockWebServer.getRequestCount()).isEqualTo(0); 93 | // proxy auth is performed on second request 94 | assertThat(proxyWebServer.takeRequest().getHeader(HttpHeaders.PROXY_AUTHORIZATION)).isNull(); 95 | assertThat(proxyWebServer.takeRequest().getHeader(HttpHeaders.PROXY_AUTHORIZATION)).isEqualTo(Credentials.basic(userName, password)); 96 | } 97 | 98 | @Test 99 | public void should_SendAllRequestsDirectly_When_NoProxyHostConfigured() throws Exception { 100 | // Given 101 | final String noProxyHost = mockWebServer.url("/").url().getHost(); 102 | jenkinsRule.jenkins.proxy = new ProxyConfiguration(proxyWebServer.getHostName(), proxyWebServer.getPort(), null, null, noProxyHost); 103 | 104 | MockWebServerUtil.enqueueSuccess(mockWebServer); 105 | 106 | // When 107 | final FreeStyleBuild freeStyleBuild = freeStyleProject.scheduleBuild2(0).get(); 108 | 109 | // Then 110 | jenkinsRule.assertBuildStatus(Result.SUCCESS, freeStyleBuild); 111 | assertThat(proxyWebServer.getRequestCount()).isEqualTo(0); 112 | assertThat(mockWebServer.getRequestCount()).isEqualTo(7); 113 | } 114 | } -------------------------------------------------------------------------------- /src/test/java/io/jenkins/plugins/appcenter/task/internal/SetMetadataTaskTest.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.appcenter.task.internal; 2 | 3 | import hudson.ProxyConfiguration; 4 | import hudson.model.TaskListener; 5 | import hudson.util.Secret; 6 | import io.jenkins.plugins.appcenter.AppCenterException; 7 | import io.jenkins.plugins.appcenter.api.AppCenterServiceFactory; 8 | import io.jenkins.plugins.appcenter.task.request.UploadRequest; 9 | import io.jenkins.plugins.appcenter.util.RemoteFileUtils; 10 | import okhttp3.mockwebserver.MockResponse; 11 | import okhttp3.mockwebserver.MockWebServer; 12 | import org.junit.Before; 13 | import org.junit.Rule; 14 | import org.junit.Test; 15 | import org.junit.function.ThrowingRunnable; 16 | import org.junit.runner.RunWith; 17 | import org.mockito.Mock; 18 | import org.mockito.junit.MockitoJUnitRunner; 19 | 20 | import java.io.PrintStream; 21 | import java.util.concurrent.ExecutionException; 22 | 23 | import static com.google.common.truth.Truth.assertThat; 24 | import static org.junit.Assert.assertThrows; 25 | import static org.mockito.BDDMockito.given; 26 | 27 | @RunWith(MockitoJUnitRunner.class) 28 | public class SetMetadataTaskTest { 29 | 30 | @Rule 31 | public MockWebServer mockWebServer = new MockWebServer(); 32 | 33 | @Mock 34 | TaskListener mockTaskListener; 35 | 36 | @Mock 37 | RemoteFileUtils remoteFileUtils; 38 | 39 | @Mock 40 | PrintStream mockLogger; 41 | 42 | @Mock 43 | ProxyConfiguration mockProxyConfig; 44 | 45 | private UploadRequest baseRequest; 46 | 47 | private SetMetadataTask task; 48 | 49 | @Before 50 | public void setUp() { 51 | baseRequest = new UploadRequest.Builder() 52 | .setOwnerName("owner-name") 53 | .setAppName("app-name") 54 | .setPathToApp("path-to-app") 55 | .build(); 56 | given(mockTaskListener.getLogger()).willReturn(mockLogger); 57 | final AppCenterServiceFactory factory = new AppCenterServiceFactory(Secret.fromString("secret-token"), mockWebServer.url("/").toString(), mockProxyConfig); 58 | task = new SetMetadataTask(mockTaskListener, factory, remoteFileUtils); 59 | } 60 | 61 | @Test 62 | public void should_ReturnException_When_UploadDomainIsMissing() { 63 | // Given 64 | final UploadRequest uploadRequest = baseRequest.newBuilder() 65 | .build(); 66 | 67 | // When 68 | final ThrowingRunnable throwingRunnable = () -> task.execute(uploadRequest).get(); 69 | 70 | // Then 71 | final NullPointerException exception = assertThrows(NullPointerException.class, throwingRunnable); 72 | assertThat(exception).hasMessageThat().contains("uploadDomain cannot be null"); 73 | } 74 | 75 | @Test 76 | public void should_ReturnException_When_PackageAssetIdIsMissing() { 77 | // Given 78 | final UploadRequest uploadRequest = baseRequest.newBuilder() 79 | .setUploadDomain("upload-domain") 80 | .build(); 81 | 82 | // When 83 | final ThrowingRunnable throwingRunnable = () -> task.execute(uploadRequest).get(); 84 | 85 | // Then 86 | final NullPointerException exception = assertThrows(NullPointerException.class, throwingRunnable); 87 | assertThat(exception).hasMessageThat().contains("packageAssetId cannot be null"); 88 | } 89 | 90 | @Test 91 | public void should_ReturnException_When_TokenIsMissing() { 92 | // Given 93 | final UploadRequest uploadRequest = baseRequest.newBuilder() 94 | .setUploadDomain("upload-domain") 95 | .setPackageAssetId("package_asset_id") 96 | .build(); 97 | 98 | // When 99 | final ThrowingRunnable throwingRunnable = () -> task.execute(uploadRequest).get(); 100 | 101 | // Then 102 | final NullPointerException exception = assertThrows(NullPointerException.class, throwingRunnable); 103 | assertThat(exception).hasMessageThat().contains("token cannot be null"); 104 | } 105 | 106 | @Test 107 | public void should_ReturnResponse_When_RequestIsSuccessful() throws Exception { 108 | // Given 109 | final UploadRequest uploadRequest = baseRequest.newBuilder() 110 | .setUploadDomain("upload-domain") 111 | .setPackageAssetId("package_asset_id") 112 | .setToken("token") 113 | .build(); 114 | final UploadRequest expected = uploadRequest.newBuilder().setChunkSize(4098).build(); 115 | mockWebServer.enqueue(new MockResponse().setResponseCode(200).setBody("{\n" + 116 | " \"chunk_size\": 4098\n" + 117 | "}")); 118 | 119 | // When 120 | final UploadRequest actual = task.execute(uploadRequest).get(); 121 | 122 | // Then 123 | assertThat(actual) 124 | .isEqualTo(expected); 125 | } 126 | 127 | @Test 128 | public void should_ReturnException_When_RequestIsUnSuccessful() { 129 | // Given 130 | final UploadRequest uploadRequest = baseRequest.newBuilder() 131 | .setUploadDomain("upload-domain") 132 | .setPackageAssetId("package_asset_id") 133 | .setToken("token") 134 | .build(); 135 | mockWebServer.enqueue(new MockResponse().setResponseCode(400)); 136 | 137 | // When 138 | final ThrowingRunnable throwingRunnable = () -> task.execute(uploadRequest).get(); 139 | 140 | // Then 141 | final ExecutionException exception = assertThrows(ExecutionException.class, throwingRunnable); 142 | assertThat(exception).hasCauseThat().isInstanceOf(AppCenterException.class); 143 | assertThat(exception).hasCauseThat().hasMessageThat().contains("Setting metadata unsuccessful"); 144 | } 145 | } -------------------------------------------------------------------------------- /src/test/java/io/jenkins/plugins/appcenter/task/internal/CreateUploadResourceTaskTest.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.appcenter.task.internal; 2 | 3 | import hudson.ProxyConfiguration; 4 | import hudson.model.TaskListener; 5 | import hudson.util.Secret; 6 | import io.jenkins.plugins.appcenter.AppCenterException; 7 | import io.jenkins.plugins.appcenter.api.AppCenterServiceFactory; 8 | import io.jenkins.plugins.appcenter.model.appcenter.SymbolUploadBeginRequest; 9 | import io.jenkins.plugins.appcenter.task.request.UploadRequest; 10 | import okhttp3.mockwebserver.MockResponse; 11 | import okhttp3.mockwebserver.MockWebServer; 12 | import org.junit.Before; 13 | import org.junit.Rule; 14 | import org.junit.Test; 15 | import org.junit.function.ThrowingRunnable; 16 | import org.junit.runner.RunWith; 17 | import org.mockito.Mock; 18 | import org.mockito.junit.MockitoJUnitRunner; 19 | 20 | import java.io.PrintStream; 21 | import java.util.concurrent.ExecutionException; 22 | 23 | import static com.google.common.truth.Truth.assertThat; 24 | import static org.junit.Assert.assertThrows; 25 | import static org.mockito.BDDMockito.given; 26 | 27 | @RunWith(MockitoJUnitRunner.class) 28 | public class CreateUploadResourceTaskTest { 29 | 30 | @Rule 31 | public MockWebServer mockWebServer = new MockWebServer(); 32 | 33 | @Mock 34 | TaskListener mockTaskListener; 35 | 36 | @Mock 37 | PrintStream mockLogger; 38 | 39 | @Mock 40 | ProxyConfiguration mockProxyConfig; 41 | 42 | private UploadRequest baseRequest; 43 | 44 | private CreateUploadResourceTask task; 45 | 46 | @Before 47 | public void setUp() { 48 | baseRequest = new UploadRequest.Builder() 49 | .setOwnerName("owner-name") 50 | .setAppName("app-name") 51 | .build(); 52 | given(mockTaskListener.getLogger()).willReturn(mockLogger); 53 | final AppCenterServiceFactory factory = new AppCenterServiceFactory(Secret.fromString("secret-token"), mockWebServer.url("/").toString(), mockProxyConfig); 54 | task = new CreateUploadResourceTask(mockTaskListener, factory); 55 | } 56 | 57 | @Test 58 | public void should_ReturnResponse_When_RequestIsSuccessful() throws Exception { 59 | // Given 60 | final UploadRequest expected = baseRequest.newBuilder().setUploadId("string").setUploadDomain("string").setToken("string").setPackageAssetId("string").build(); 61 | mockWebServer.enqueue(new MockResponse().setResponseCode(201).setBody("{\n" + 62 | " \"id\": \"string\",\n" + 63 | " \"upload_domain\": \"string\",\n" + 64 | " \"url_encoded_token\": \"string\",\n" + 65 | " \"package_asset_id\": \"string\"\n" + 66 | "}")); 67 | 68 | // When 69 | final UploadRequest actual = task.execute(baseRequest).get(); 70 | 71 | // Then 72 | assertThat(actual) 73 | .isEqualTo(expected); 74 | } 75 | 76 | @Test 77 | public void should_ReturnResponse_When_DebugSymbolsAreFound() throws Exception { 78 | // Given 79 | final UploadRequest request = baseRequest.newBuilder() 80 | .setPathToDebugSymbols("path/to/mappings.txt") 81 | .setSymbolUploadRequest(new SymbolUploadBeginRequest(SymbolUploadBeginRequest.SymbolTypeEnum.AndroidProguard, null, "mappings.txt", "1", "1.0.0")) 82 | .build(); 83 | final UploadRequest expected = request.newBuilder().setUploadId("string").setUploadDomain("string").setToken("string").setPackageAssetId("string").setSymbolUploadId("string").setSymbolUploadUrl("string").build(); 84 | mockWebServer.enqueue(new MockResponse().setResponseCode(201).setBody("{\n" + 85 | " \"id\": \"string\",\n" + 86 | " \"upload_domain\": \"string\",\n" + 87 | " \"url_encoded_token\": \"string\",\n" + 88 | " \"package_asset_id\": \"string\"\n" + 89 | "}")); 90 | mockWebServer.enqueue(new MockResponse().setResponseCode(200).setBody("{\n" + 91 | " \"symbol_upload_id\": \"string\",\n" + 92 | " \"upload_url\": \"string\",\n" + 93 | " \"expiration_date\": \"2019-11-17T12:01:43.953Z\"\n" + 94 | "}")); 95 | 96 | // When 97 | final UploadRequest actual = task.execute(request).get(); 98 | 99 | // Then 100 | assertThat(actual) 101 | .isEqualTo(expected); 102 | } 103 | 104 | @Test 105 | public void should_ReturnResponse_When_RequestIsSuccessful_NonAsciiCharactersInFileName() throws Exception { 106 | // Given 107 | final UploadRequest request = baseRequest.newBuilder().setAppName("åþþ ñåmë").build(); 108 | final UploadRequest expected = request.newBuilder().setUploadId("string").setUploadDomain("string").setToken("string").setPackageAssetId("string").build(); 109 | mockWebServer.enqueue(new MockResponse().setResponseCode(201).setBody("{\n" + 110 | " \"id\": \"string\",\n" + 111 | " \"upload_domain\": \"string\",\n" + 112 | " \"url_encoded_token\": \"string\",\n" + 113 | " \"package_asset_id\": \"string\"\n" + 114 | "}")); 115 | 116 | // When 117 | final UploadRequest actual = task.execute(request).get(); 118 | 119 | // Then 120 | assertThat(actual) 121 | .isEqualTo(expected); 122 | } 123 | 124 | @Test 125 | public void should_ReturnException_When_RequestIsUnSuccessful() { 126 | // Given 127 | mockWebServer.enqueue(new MockResponse().setResponseCode(500)); 128 | 129 | // When 130 | final ThrowingRunnable throwingRunnable = () -> task.execute(baseRequest).get(); 131 | 132 | // Then 133 | final ExecutionException exception = assertThrows(ExecutionException.class, throwingRunnable); 134 | assertThat(exception).hasCauseThat().isInstanceOf(AppCenterException.class); 135 | assertThat(exception).hasCauseThat().hasMessageThat().contains("Create upload resource for app unsuccessful: HTTP 500 Server Error: "); 136 | } 137 | } -------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/appcenter/api/AppCenterServiceFactory.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.appcenter.api; 2 | 3 | import com.azure.core.http.okhttp.OkHttpAsyncHttpClientBuilder; 4 | import com.azure.storage.blob.BlobClient; 5 | import com.azure.storage.blob.BlobClientBuilder; 6 | import com.google.common.net.HttpHeaders; 7 | import hudson.ProxyConfiguration; 8 | import hudson.util.Secret; 9 | import okhttp3.*; 10 | import okhttp3.logging.HttpLoggingInterceptor; 11 | import retrofit2.Retrofit; 12 | import retrofit2.converter.moshi.MoshiConverterFactory; 13 | 14 | import javax.annotation.Nonnull; 15 | import javax.annotation.Nullable; 16 | import javax.inject.Inject; 17 | import javax.inject.Named; 18 | import javax.inject.Singleton; 19 | import java.io.Serializable; 20 | import java.net.Proxy; 21 | import java.util.concurrent.TimeUnit; 22 | 23 | import static org.apache.commons.lang.StringUtils.isNotBlank; 24 | 25 | @Singleton 26 | public final class AppCenterServiceFactory implements Serializable { 27 | 28 | private static final String APPCENTER_BASE_URL = "https://api.appcenter.ms/"; 29 | 30 | private static final long serialVersionUID = 1L; 31 | private static final int timeoutSeconds = 60; 32 | 33 | @Nonnull 34 | private final Secret apiToken; 35 | @Nonnull 36 | private final String baseUrl; 37 | @Nullable 38 | private final ProxyConfiguration proxyConfiguration; 39 | 40 | @Inject 41 | public AppCenterServiceFactory(@Nonnull Secret apiToken, @Nullable @Named("baseUrl") String baseUrl, @Nullable ProxyConfiguration proxyConfiguration) { 42 | this.apiToken = apiToken; 43 | this.baseUrl = baseUrl != null ? baseUrl : APPCENTER_BASE_URL; 44 | this.proxyConfiguration = proxyConfiguration; 45 | } 46 | 47 | public AppCenterService createAppCenterService() { 48 | final HttpUrl baseHttpUrl = HttpUrl.get(baseUrl); 49 | 50 | final OkHttpClient.Builder builder = createHttpClientBuilder(baseHttpUrl) 51 | .addInterceptor(chain -> { 52 | final Request request = chain.request(); 53 | 54 | final Headers newHeaders = request.headers().newBuilder() 55 | .add("Accept", "application/json") 56 | .add("Content-Type", "application/json") 57 | .add("X-API-Token", Secret.toString(apiToken)) 58 | .build(); 59 | 60 | final Request newRequest = request.newBuilder() 61 | .headers(newHeaders) 62 | .build(); 63 | 64 | return chain.proceed(newRequest); 65 | }); 66 | 67 | final Retrofit retrofit = new Retrofit.Builder() 68 | .baseUrl(baseHttpUrl) 69 | .client(builder.build()) 70 | .addConverterFactory(MoshiConverterFactory.create()) 71 | .build(); 72 | 73 | return retrofit.create(AppCenterService.class); 74 | } 75 | 76 | public UploadService createUploadService(@Nonnull final String uploadUrl) { 77 | final HttpUrl httpUploadUrl = HttpUrl.get(uploadUrl); 78 | final HttpUrl baseUrl = HttpUrl.get(String.format("%1$s://%2$s/", httpUploadUrl.scheme(), httpUploadUrl.host())); 79 | 80 | final Retrofit retrofit = new Retrofit.Builder() 81 | .baseUrl(baseUrl) 82 | .client(createHttpClientBuilder(baseUrl).build()) 83 | .addConverterFactory(MoshiConverterFactory.create()) 84 | .build(); 85 | 86 | return retrofit.create(UploadService.class); 87 | } 88 | 89 | public BlobClient createBlobUploadService(@Nonnull final String uploadUrl) { 90 | final HttpUrl httpUploadUrl = HttpUrl.get(uploadUrl); 91 | final OkHttpAsyncHttpClientBuilder okHttpAsyncHttpClientBuilder = new OkHttpAsyncHttpClientBuilder(createHttpClientBuilder(httpUploadUrl).build()); 92 | 93 | return new BlobClientBuilder() 94 | .endpoint(workaroundAzureSdkForJava15827(httpUploadUrl)) 95 | .httpClient(okHttpAsyncHttpClientBuilder.build()) 96 | .buildClient(); 97 | } 98 | 99 | @Nonnull 100 | private String workaroundAzureSdkForJava15827(@Nonnull HttpUrl httpUploadUrl) { 101 | // Workaround for bug in Azure Blob Storage, as AppCenter returns the upload URL with a port attached. 102 | // See https://github.com/Azure/azure-sdk-for-java/issues/15827 103 | 104 | // toString() will remove the well known HTTPS of 443 or keep it if there is a custom port. Hence this results in stripping the port if needed (e.g. if we are trying to 105 | // connect to https://.blob.core.windows.net:443) or keeps it (e.g. if we are trying to connect to test servers https://127.0.0.1:1234) 106 | return httpUploadUrl.toString(); 107 | } 108 | 109 | private OkHttpClient.Builder createHttpClientBuilder(@Nonnull final HttpUrl httpUrl) { 110 | final HttpLoggingInterceptor logging = new HttpLoggingInterceptor(); 111 | logging.setLevel(HttpLoggingInterceptor.Level.HEADERS); 112 | 113 | return new OkHttpClient.Builder() 114 | .addInterceptor(logging) 115 | .proxy(setProxy(proxyConfiguration, httpUrl.host())) 116 | .proxyAuthenticator(setProxyAuthenticator(proxyConfiguration)) 117 | .connectTimeout(timeoutSeconds, TimeUnit.SECONDS) 118 | .readTimeout(timeoutSeconds, TimeUnit.SECONDS) 119 | .writeTimeout(timeoutSeconds, TimeUnit.SECONDS); 120 | } 121 | 122 | private Proxy setProxy(@Nullable final ProxyConfiguration proxyConfiguration, 123 | @Nonnull final String host) { 124 | if (proxyConfiguration != null) { 125 | return proxyConfiguration.createProxy(host); 126 | } else { 127 | return Proxy.NO_PROXY; 128 | } 129 | } 130 | 131 | private Authenticator setProxyAuthenticator(@Nullable final ProxyConfiguration proxyConfiguration) { 132 | if (proxyConfiguration != null) { 133 | final String username = proxyConfiguration.getUserName(); 134 | final String password = proxyConfiguration.getPassword(); 135 | 136 | if (isNotBlank(username) && isNotBlank(password)) { 137 | final String credentials = Credentials.basic(username, password); 138 | 139 | return (route, response) -> response 140 | .request() 141 | .newBuilder() 142 | .header(HttpHeaders.PROXY_AUTHORIZATION, credentials) 143 | .build(); 144 | } 145 | 146 | return Authenticator.NONE; 147 | } 148 | 149 | return Authenticator.NONE; 150 | } 151 | } -------------------------------------------------------------------------------- /src/test/java/io/jenkins/plugins/appcenter/util/MockWebServerUtil.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.appcenter.util; 2 | 3 | import okhttp3.mockwebserver.MockResponse; 4 | import okhttp3.mockwebserver.MockWebServer; 5 | 6 | import javax.annotation.Nonnull; 7 | 8 | import static java.net.HttpURLConnection.*; 9 | 10 | public class MockWebServerUtil { 11 | 12 | public static void enqueueSuccess(final @Nonnull MockWebServer mockWebServer) { 13 | enqueueSuccess(mockWebServer, mockWebServer); 14 | } 15 | 16 | public static void enqueueUploadViaProxy(final @Nonnull MockWebServer mockWebServer, final @Nonnull MockWebServer proxyWebServer) { 17 | enqueueSuccess(mockWebServer, proxyWebServer); 18 | } 19 | 20 | public static void enqueueAppCenterViaProxy(final @Nonnull MockWebServer mockWebServer, final @Nonnull MockWebServer proxyWebServer) { 21 | enqueueSuccess(proxyWebServer, mockWebServer); 22 | } 23 | 24 | private static void enqueueSuccess(final @Nonnull MockWebServer mockAppCenterServer, final @Nonnull MockWebServer mockUploadServer) { 25 | // Create upload resource for app 26 | mockAppCenterServer.enqueue(new MockResponse().setResponseCode(HTTP_CREATED).setBody("{\n" + 27 | " \"id\": \"string\",\n" + 28 | " \"upload_domain\": \"" + mockUploadServer.url("/").toString() + "\",\n" + 29 | " \"asset_domain\": \"string\",\n" + 30 | " \"url_encoded_token\": \"string\",\n" + 31 | " \"package_asset_id\": \"string\"\n" + 32 | "}")); 33 | 34 | // Set Metadata 35 | mockAppCenterServer.enqueue(new MockResponse().setResponseCode(HTTP_OK).setBody("{\n" + 36 | " \"chunk_size\": 1234\n" + 37 | "}")); 38 | 39 | // Upload app 40 | mockAppCenterServer.enqueue(new MockResponse().setResponseCode(HTTP_OK)); 41 | 42 | // Finish Release 43 | mockAppCenterServer.enqueue(new MockResponse().setResponseCode(HTTP_OK)); 44 | 45 | // Update Release 46 | mockAppCenterServer.enqueue(new MockResponse().setResponseCode(HTTP_OK).setBody("{\n" + 47 | " \"id\": \"1234\",\n" + 48 | " \"upload_status\": \"uploadFinished\"\n" + 49 | "}")); 50 | 51 | // Poll For Release 52 | mockAppCenterServer.enqueue(new MockResponse().setResponseCode(HTTP_OK).setBody("{\n" + 53 | " \"id\": \"1234\",\n" + 54 | " \"upload_status\": \"readyToBePublished\",\n" + 55 | " \"release_distinct_id\": \"4321\",\n" + 56 | " \"release_url\": \"string\"\n" + 57 | "}")); 58 | 59 | // Distribute Resource 60 | mockAppCenterServer.enqueue(new MockResponse().setResponseCode(HTTP_OK).setBody("{\n" + 61 | " \"release_notes\": \"string\"\n" + 62 | "}")); 63 | } 64 | 65 | public static void enqueueSuccessWithSymbols(final @Nonnull MockWebServer mockAppCenterServer) { 66 | // Create upload resource for app 67 | mockAppCenterServer.enqueue(new MockResponse().setResponseCode(HTTP_CREATED).setBody("{\n" + 68 | " \"id\": \"string\",\n" + 69 | " \"upload_domain\": \"" + mockAppCenterServer.url("/").toString() + "\",\n" + 70 | " \"asset_domain\": \"string\",\n" + 71 | " \"url_encoded_token\": \"string\",\n" + 72 | " \"package_asset_id\": \"string\"\n" + 73 | "}")); 74 | 75 | // Create upload resource for debug symbols 76 | mockAppCenterServer.enqueue(new MockResponse().setResponseCode(HTTP_CREATED).setBody("{\n" + 77 | " \"symbol_upload_id\": \"string\",\n" + 78 | " \"upload_url\": \"" + mockAppCenterServer.url("/").toString() + "\",\n" + 79 | " \"expiration_date\": \"2020-03-18T21:16:22.188Z\"\n" + 80 | "}")); 81 | 82 | // Set Metadata 83 | mockAppCenterServer.enqueue(new MockResponse().setResponseCode(HTTP_OK).setBody("{\n" + 84 | " \"chunk_size\": 1234\n" + 85 | "}")); 86 | 87 | // Upload app 88 | mockAppCenterServer.enqueue(new MockResponse().setResponseCode(HTTP_OK)); 89 | 90 | // Upload debug symbols 91 | mockAppCenterServer.enqueue(new MockResponse().setResponseCode(HTTP_OK)); 92 | 93 | // Finish Release 94 | mockAppCenterServer.enqueue(new MockResponse().setResponseCode(HTTP_OK)); 95 | 96 | // Finish symbol release 97 | mockAppCenterServer.enqueue(new MockResponse().setResponseCode(HTTP_OK).setBody("{\n" + 98 | " \"symbol_upload_id\": \"string\",\n" + 99 | " \"app_id\": \"string\",\n" + 100 | " \"user\": {\n" + 101 | " \"email\": \"string\",\n" + 102 | " \"display_name\": \"string\"\n" + 103 | " },\n" + 104 | " \"status\": \"created\",\n" + 105 | " \"symbol_type\": \"AndroidProguard\",\n" + 106 | " \"symbols_uploaded\": [\n" + 107 | " {\n" + 108 | " \"symbol_id\": \"string\",\n" + 109 | " \"platform\": \"string\"\n" + 110 | " }\n" + 111 | " ],\n" + 112 | " \"origin\": \"User\",\n" + 113 | " \"file_name\": \"string\",\n" + 114 | " \"file_size\": 0,\n" + 115 | " \"timestamp\": \"2019-11-17T12:12:06.701Z\"\n" + 116 | "}")); 117 | 118 | // Update Release 119 | mockAppCenterServer.enqueue(new MockResponse().setResponseCode(HTTP_OK).setBody("{\n" + 120 | " \"id\": \"1234\",\n" + 121 | " \"upload_status\": \"uploadFinished\"\n" + 122 | "}")); 123 | 124 | // Poll For Release 125 | mockAppCenterServer.enqueue(new MockResponse().setResponseCode(HTTP_OK).setBody("{\n" + 126 | " \"id\": \"1234\",\n" + 127 | " \"upload_status\": \"readyToBePublished\",\n" + 128 | " \"release_distinct_id\": \"4321\",\n" + 129 | " \"release_url\": \"string\"\n" + 130 | "}")); 131 | 132 | // Distribute app 133 | mockAppCenterServer.enqueue(new MockResponse().setResponseCode(HTTP_OK).setBody("{\n" + 134 | " \"release_notes\": \"string\"\n" + 135 | "}")); 136 | } 137 | 138 | public static void enqueueFailure(final @Nonnull MockWebServer mockWebServer) { 139 | mockWebServer.enqueue(new MockResponse().setResponseCode(HTTP_INTERNAL_ERROR)); 140 | } 141 | 142 | public static void enqueueProxyAuthRequired(final @Nonnull MockWebServer proxyWebServer) { 143 | proxyWebServer.enqueue(new MockResponse().setResponseCode(HTTP_PROXY_AUTH)); 144 | } 145 | } -------------------------------------------------------------------------------- /src/test/java/io/jenkins/plugins/appcenter/task/internal/UploadAppToResourceTaskTest.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.appcenter.task.internal; 2 | 3 | import hudson.ProxyConfiguration; 4 | import hudson.model.TaskListener; 5 | import hudson.util.Secret; 6 | import io.jenkins.plugins.appcenter.AppCenterException; 7 | import io.jenkins.plugins.appcenter.api.AppCenterServiceFactory; 8 | import io.jenkins.plugins.appcenter.task.request.UploadRequest; 9 | import io.jenkins.plugins.appcenter.util.RemoteFileUtils; 10 | import io.jenkins.plugins.appcenter.util.TestFileUtil; 11 | import okhttp3.Headers; 12 | import okhttp3.mockwebserver.MockResponse; 13 | import okhttp3.mockwebserver.MockWebServer; 14 | import okhttp3.mockwebserver.RecordedRequest; 15 | import org.junit.Before; 16 | import org.junit.Rule; 17 | import org.junit.Test; 18 | import org.junit.function.ThrowingRunnable; 19 | import org.junit.runner.RunWith; 20 | import org.mockito.Mock; 21 | import org.mockito.junit.MockitoJUnitRunner; 22 | 23 | import java.io.PrintStream; 24 | import java.util.concurrent.ExecutionException; 25 | 26 | import static com.google.common.truth.Truth.assertThat; 27 | import static org.junit.Assert.assertThrows; 28 | import static org.mockito.ArgumentMatchers.anyString; 29 | import static org.mockito.BDDMockito.given; 30 | 31 | @RunWith(MockitoJUnitRunner.class) 32 | public class UploadAppToResourceTaskTest { 33 | 34 | @Rule 35 | public MockWebServer mockWebServer = new MockWebServer(); 36 | 37 | @Mock 38 | TaskListener mockTaskListener; 39 | 40 | @Mock 41 | PrintStream mockLogger; 42 | 43 | @Mock 44 | ProxyConfiguration mockProxyConfig; 45 | 46 | @Mock 47 | RemoteFileUtils mockRemoteFileUtils; 48 | 49 | private UploadRequest baseRequest; 50 | 51 | private UploadAppToResourceTask task; 52 | 53 | @Before 54 | public void setUp() { 55 | baseRequest = new UploadRequest.Builder() 56 | .setUploadDomain(mockWebServer.url("upload").toString()) 57 | .setPackageAssetId("package-asset-id") 58 | .setToken("token") 59 | .setChunkSize(4098) 60 | .setUploadId("upload-id") 61 | .setPathToApp("three/days/xiola.apk") 62 | .build(); 63 | given(mockTaskListener.getLogger()).willReturn(mockLogger); 64 | given(mockRemoteFileUtils.getRemoteFile(anyString())).willReturn(TestFileUtil.createFileForTesting()); 65 | final AppCenterServiceFactory factory = new AppCenterServiceFactory(Secret.fromString("secret-token"), mockWebServer.url("/").toString(), mockProxyConfig); 66 | task = new UploadAppToResourceTask(mockTaskListener, factory, mockRemoteFileUtils); 67 | } 68 | 69 | @Test 70 | public void should_ReturnUploadId_When_RequestIsSuccess() throws Exception { 71 | // Given 72 | mockWebServer.enqueue(new MockResponse().setResponseCode(200)); 73 | 74 | // When 75 | final UploadRequest result = task.execute(baseRequest).get(); 76 | 77 | // Then 78 | assertThat(result) 79 | .isEqualTo(baseRequest); 80 | } 81 | 82 | @Test 83 | public void should_ReturnDebugSymbolUploadId_When_DebugSymbolsAreFound() throws Exception { 84 | // Given 85 | final UploadRequest request = baseRequest.newBuilder() 86 | .setPathToDebugSymbols("string") 87 | .setSymbolUploadUrl(mockWebServer.url("upload-debug-symbols").toString()) 88 | .setSymbolUploadId("string") 89 | .build(); 90 | 91 | mockWebServer.enqueue(new MockResponse().setResponseCode(200)); 92 | mockWebServer.enqueue(new MockResponse().setResponseCode(200)); 93 | 94 | // When 95 | final UploadRequest result = task.execute(request).get(); 96 | 97 | // Then 98 | assertThat(result) 99 | .isEqualTo(request); 100 | } 101 | 102 | @Test 103 | public void should_ReturnDebugSymbolUploadId_When_DebugSymbolsAreFound_ChunkedMode() throws Exception { 104 | // Given 105 | final UploadRequest request = baseRequest.newBuilder() 106 | .setPathToDebugSymbols("string") 107 | .setSymbolUploadUrl(mockWebServer.url("perry/casey/xiola").toString()) 108 | .setSymbolUploadId("string") 109 | .build(); 110 | 111 | mockWebServer.enqueue(new MockResponse().setResponseCode(200)); 112 | mockWebServer.enqueue(new MockResponse().setResponseCode(201) 113 | .setHeaders(Headers.of( 114 | "ETag", "0x8CB171BA9E94B0B", 115 | "Last-Modified", "Thu, 01 Jan 1970 00:00:00 GMT", 116 | "Content-MD5", "sQqNsWTgdUEFt6mb5y4/5Q==", 117 | "x-ms-request-server-encrypted", "false", 118 | "x-ms-version-id", "Thu, 01 Jan 1970 00:00:00 GMT" 119 | )) 120 | .setChunkedBody("", 1) 121 | ); 122 | 123 | given(mockRemoteFileUtils.getRemoteFile(anyString())).willReturn(TestFileUtil.createFileForTesting(), TestFileUtil.createLargeFileForTesting()); 124 | 125 | // When 126 | final UploadRequest result = task.execute(request).get(); 127 | 128 | // Then 129 | assertThat(result) 130 | .isEqualTo(request); 131 | } 132 | 133 | @Test 134 | public void should_ReturnException_When_RequestIsUnSuccessful() { 135 | // Given 136 | mockWebServer.enqueue(new MockResponse().setResponseCode(500)); 137 | 138 | // When 139 | final ThrowingRunnable throwingRunnable = () -> task.execute(baseRequest).get(); 140 | 141 | // Then 142 | final ExecutionException exception = assertThrows(ExecutionException.class, throwingRunnable); 143 | assertThat(exception).hasCauseThat().isInstanceOf(AppCenterException.class); 144 | assertThat(exception).hasCauseThat().hasMessageThat().contains("Upload app to resource unsuccessful: HTTP 500 Server Error: "); 145 | } 146 | 147 | @Test 148 | public void should_SendRequestToUploadUrl() throws Exception { 149 | // Given 150 | mockWebServer.enqueue(new MockResponse().setResponseCode(200)); 151 | task.execute(baseRequest).get(); 152 | 153 | // When 154 | final RecordedRequest recordedRequest = mockWebServer.takeRequest(); 155 | 156 | // Then 157 | assertThat(recordedRequest.getPath()) 158 | .isEqualTo("/upload/upload/upload_chunk/package-asset-id?token=token&block_number=1"); 159 | } 160 | 161 | @Test 162 | public void should_SendRequestAsPost() throws Exception { 163 | // Given 164 | mockWebServer.enqueue(new MockResponse().setResponseCode(200)); 165 | task.execute(baseRequest).get(); 166 | 167 | // When 168 | final RecordedRequest recordedRequest = mockWebServer.takeRequest(); 169 | 170 | // Then 171 | assertThat(recordedRequest.getMethod()) 172 | .isEqualTo("POST"); 173 | } 174 | } -------------------------------------------------------------------------------- /src/test/java/io/jenkins/plugins/appcenter/EnvInterpolationTest.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.appcenter; 2 | 3 | import hudson.EnvVars; 4 | import hudson.model.FreeStyleBuild; 5 | import hudson.model.FreeStyleProject; 6 | import hudson.model.Result; 7 | import hudson.slaves.EnvironmentVariablesNodeProperty; 8 | import io.jenkins.plugins.appcenter.util.MockWebServerUtil; 9 | import io.jenkins.plugins.appcenter.util.TestUtil; 10 | import okhttp3.mockwebserver.MockWebServer; 11 | import okhttp3.mockwebserver.RecordedRequest; 12 | import org.junit.Before; 13 | import org.junit.ClassRule; 14 | import org.junit.Rule; 15 | import org.junit.Test; 16 | import org.jvnet.hudson.test.JenkinsRule; 17 | 18 | import java.io.IOException; 19 | 20 | import static com.google.common.truth.Truth.assertThat; 21 | 22 | public class EnvInterpolationTest { 23 | 24 | @ClassRule 25 | public static JenkinsRule jenkinsRule = new JenkinsRule(); 26 | 27 | @Rule 28 | public MockWebServer mockWebServer = new MockWebServer(); 29 | 30 | private FreeStyleProject freeStyleProject; 31 | 32 | @Before 33 | public void setUp() throws IOException { 34 | freeStyleProject = jenkinsRule.createFreeStyleProject(); 35 | freeStyleProject.getBuildersList().add(TestUtil.createFile("three/days/xiola.ipa")); 36 | freeStyleProject.getBuildersList().add(TestUtil.createFile("three/days/blue.zip")); 37 | freeStyleProject.getBuildersList().add(TestUtil.createFile("three/days/linear-notes.md", "I prepared the room tonight with Christmas lights.")); 38 | 39 | final EnvironmentVariablesNodeProperty prop = new EnvironmentVariablesNodeProperty(); 40 | final EnvVars envVars = prop.getEnvVars(); 41 | envVars.put("OWNER_NAME", "janes-addiction"); 42 | envVars.put("APP_NAME", "ritual-de-lo-habitual"); 43 | envVars.put("PATH_TO_APP", "three/days/xiola.ipa"); 44 | envVars.put("BUILD_VERSION", "1.2.3"); 45 | envVars.put("PATH_TO_DEBUG_SYMBOLS", "three/days/blue.zip"); 46 | envVars.put("DISTRIBUTION_GROUPS", "casey, niccoli"); 47 | envVars.put("RELEASE_NOTES", "I miss you my dear Xiola"); 48 | envVars.put("PATH_TO_RELEASE_NOTES", "three/days/linear-notes.md"); 49 | 50 | jenkinsRule.jenkins.getGlobalNodeProperties().add(prop); 51 | 52 | final AppCenterRecorder appCenterRecorder = new AppCenterRecorder( 53 | "at-this-moment-you-should-be-with-us", 54 | "${OWNER_NAME}", 55 | "${APP_NAME}", 56 | "${PATH_TO_APP}", 57 | "${DISTRIBUTION_GROUPS}" 58 | ); 59 | appCenterRecorder.setBuildVersion("${BUILD_VERSION}"); 60 | appCenterRecorder.setPathToDebugSymbols("${PATH_TO_DEBUG_SYMBOLS}"); 61 | appCenterRecorder.setReleaseNotes("${RELEASE_NOTES}"); 62 | appCenterRecorder.setPathToReleaseNotes("${PATH_TO_RELEASE_NOTES}"); 63 | appCenterRecorder.setBaseUrl(mockWebServer.url("/").toString()); 64 | 65 | freeStyleProject.getPublishersList().add(appCenterRecorder); 66 | } 67 | 68 | @Test 69 | public void should_InterpolateEnv_InOwnerName() throws Exception { 70 | // Given 71 | MockWebServerUtil.enqueueSuccessWithSymbols(mockWebServer); 72 | 73 | // When 74 | final FreeStyleBuild freeStyleBuild = freeStyleProject.scheduleBuild2(0).get(); 75 | 76 | // Then 77 | jenkinsRule.assertBuildStatus(Result.SUCCESS, freeStyleBuild); 78 | final RecordedRequest recordedRequest = mockWebServer.takeRequest(); 79 | assertThat(recordedRequest.getPath()).contains("janes-addiction"); 80 | } 81 | 82 | @Test 83 | public void should_InterpolateEnv_InAppName() throws Exception { 84 | // Given 85 | MockWebServerUtil.enqueueSuccessWithSymbols(mockWebServer); 86 | 87 | // When 88 | final FreeStyleBuild freeStyleBuild = freeStyleProject.scheduleBuild2(0).get(); 89 | 90 | // Then 91 | jenkinsRule.assertBuildStatus(Result.SUCCESS, freeStyleBuild); 92 | final RecordedRequest recordedRequest = mockWebServer.takeRequest(); 93 | assertThat(recordedRequest.getPath()).contains("ritual-de-lo-habitual"); 94 | } 95 | 96 | @Test 97 | public void should_InterpolateEnv_InAppPath() throws Exception { 98 | // Given 99 | MockWebServerUtil.enqueueSuccessWithSymbols(mockWebServer); 100 | 101 | // When 102 | final FreeStyleBuild freeStyleBuild = freeStyleProject.scheduleBuild2(0).get(); 103 | 104 | // Then 105 | jenkinsRule.assertBuildStatus(Result.SUCCESS, freeStyleBuild); 106 | mockWebServer.takeRequest(); 107 | mockWebServer.takeRequest(); 108 | final RecordedRequest recordedRequest = mockWebServer.takeRequest(); 109 | assertThat(recordedRequest.getPath().contains("file_name=xiola.ipa")); 110 | } 111 | 112 | @Test 113 | public void should_InterpolateEnv_InDebugSymbolPath() throws Exception { 114 | // Given 115 | MockWebServerUtil.enqueueSuccessWithSymbols(mockWebServer); 116 | 117 | // When 118 | final FreeStyleBuild freeStyleBuild = freeStyleProject.scheduleBuild2(0).get(); 119 | 120 | // Then 121 | jenkinsRule.assertBuildStatus(Result.SUCCESS, freeStyleBuild); 122 | mockWebServer.takeRequest(); 123 | final RecordedRequest recordedRequest = mockWebServer.takeRequest(); 124 | assertThat(recordedRequest.getBody().readUtf8()).contains("\"symbol_type\":\"Apple\""); 125 | } 126 | 127 | @Test 128 | public void should_InterpolateEnv_InDestinationGroups() throws Exception { 129 | // Given 130 | MockWebServerUtil.enqueueSuccessWithSymbols(mockWebServer); 131 | 132 | // When 133 | final FreeStyleBuild freeStyleBuild = freeStyleProject.scheduleBuild2(0).get(); 134 | 135 | // Then 136 | jenkinsRule.assertBuildStatus(Result.SUCCESS, freeStyleBuild); 137 | mockWebServer.takeRequest(); 138 | mockWebServer.takeRequest(); 139 | mockWebServer.takeRequest(); 140 | mockWebServer.takeRequest(); 141 | mockWebServer.takeRequest(); 142 | mockWebServer.takeRequest(); 143 | mockWebServer.takeRequest(); 144 | mockWebServer.takeRequest(); 145 | mockWebServer.takeRequest(); 146 | final RecordedRequest recordedRequest = mockWebServer.takeRequest(); 147 | assertThat(recordedRequest.getBody().readUtf8()).contains("[{\"name\":\"casey\"},{\"name\":\"niccoli\"}]"); 148 | } 149 | 150 | @Test 151 | public void should_InterpolateEnv_InReleaseNotes() throws Exception { 152 | // Given 153 | MockWebServerUtil.enqueueSuccessWithSymbols(mockWebServer); 154 | 155 | // When 156 | final FreeStyleBuild freeStyleBuild = freeStyleProject.scheduleBuild2(0).get(); 157 | 158 | // Then 159 | jenkinsRule.assertBuildStatus(Result.SUCCESS, freeStyleBuild); 160 | mockWebServer.takeRequest(); 161 | mockWebServer.takeRequest(); 162 | mockWebServer.takeRequest(); 163 | mockWebServer.takeRequest(); 164 | mockWebServer.takeRequest(); 165 | mockWebServer.takeRequest(); 166 | mockWebServer.takeRequest(); 167 | mockWebServer.takeRequest(); 168 | mockWebServer.takeRequest(); 169 | final RecordedRequest recordedRequest = mockWebServer.takeRequest(); 170 | assertThat(recordedRequest.getBody().readUtf8()).contains("\"release_notes\":\"I miss you my dear Xiola\\n\\nI prepared the room tonight with Christmas lights.\""); 171 | } 172 | } -------------------------------------------------------------------------------- /src/test/java/io/jenkins/plugins/appcenter/task/internal/FinishReleaseTaskTest.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.appcenter.task.internal; 2 | 3 | import hudson.ProxyConfiguration; 4 | import hudson.model.TaskListener; 5 | import hudson.util.Secret; 6 | import io.jenkins.plugins.appcenter.AppCenterException; 7 | import io.jenkins.plugins.appcenter.api.AppCenterServiceFactory; 8 | import io.jenkins.plugins.appcenter.task.request.UploadRequest; 9 | import okhttp3.mockwebserver.MockResponse; 10 | import okhttp3.mockwebserver.MockWebServer; 11 | import org.junit.Before; 12 | import org.junit.Rule; 13 | import org.junit.Test; 14 | import org.junit.function.ThrowingRunnable; 15 | import org.junit.runner.RunWith; 16 | import org.mockito.Mock; 17 | import org.mockito.junit.MockitoJUnitRunner; 18 | 19 | import java.io.PrintStream; 20 | import java.util.concurrent.ExecutionException; 21 | 22 | import static com.google.common.truth.Truth.assertThat; 23 | import static org.junit.Assert.assertThrows; 24 | import static org.mockito.BDDMockito.given; 25 | 26 | @RunWith(MockitoJUnitRunner.class) 27 | public class FinishReleaseTaskTest { 28 | 29 | @Rule 30 | public MockWebServer mockWebServer = new MockWebServer(); 31 | 32 | @Mock 33 | TaskListener mockTaskListener; 34 | 35 | @Mock 36 | PrintStream mockLogger; 37 | 38 | @Mock 39 | ProxyConfiguration mockProxyConfig; 40 | 41 | private UploadRequest baseRequest; 42 | 43 | private FinishReleaseTask task; 44 | 45 | @Before 46 | public void setUp() { 47 | baseRequest = new UploadRequest.Builder() 48 | .setOwnerName("owner-name") 49 | .setAppName("app-name") 50 | .build(); 51 | given(mockTaskListener.getLogger()).willReturn(mockLogger); 52 | final AppCenterServiceFactory factory = new AppCenterServiceFactory(Secret.fromString("secret-token"), mockWebServer.url("/").toString(), mockProxyConfig); 53 | task = new FinishReleaseTask(mockTaskListener, factory); 54 | } 55 | 56 | @Test 57 | public void should_ReturnException_When_UploadDomainIsMissing() { 58 | // Given 59 | final UploadRequest uploadRequest = baseRequest.newBuilder() 60 | .build(); 61 | 62 | // When 63 | final ThrowingRunnable throwingRunnable = () -> task.execute(uploadRequest).get(); 64 | 65 | // Then 66 | final NullPointerException exception = assertThrows(NullPointerException.class, throwingRunnable); 67 | assertThat(exception).hasMessageThat().contains("uploadDomain cannot be null"); 68 | } 69 | 70 | @Test 71 | public void should_ReturnException_When_PackageAssetIdIsMissing() { 72 | // Given 73 | final UploadRequest uploadRequest = baseRequest.newBuilder() 74 | .setUploadDomain("upload-domain") 75 | .build(); 76 | 77 | // When 78 | final ThrowingRunnable throwingRunnable = () -> task.execute(uploadRequest).get(); 79 | 80 | // Then 81 | final NullPointerException exception = assertThrows(NullPointerException.class, throwingRunnable); 82 | assertThat(exception).hasMessageThat().contains("packageAssetId cannot be null"); 83 | } 84 | 85 | @Test 86 | public void should_ReturnException_When_TokenIsMissing() { 87 | // Given 88 | final UploadRequest uploadRequest = baseRequest.newBuilder() 89 | .setUploadDomain("upload-domain") 90 | .setPackageAssetId("package_asset_id") 91 | .build(); 92 | 93 | // When 94 | final ThrowingRunnable throwingRunnable = () -> task.execute(uploadRequest).get(); 95 | 96 | // Then 97 | final NullPointerException exception = assertThrows(NullPointerException.class, throwingRunnable); 98 | assertThat(exception).hasMessageThat().contains("token cannot be null"); 99 | } 100 | 101 | @Test 102 | public void should_ReturnResponse_When_RequestIsSuccessful() throws Exception { 103 | // Given 104 | final UploadRequest uploadRequest = baseRequest.newBuilder() 105 | .setUploadDomain("upload-domain") 106 | .setPackageAssetId("package_asset_id") 107 | .setToken("token") 108 | .build(); 109 | mockWebServer.enqueue(new MockResponse().setResponseCode(200)); 110 | 111 | // When 112 | final UploadRequest actual = task.execute(uploadRequest).get(); 113 | 114 | // Then 115 | assertThat(actual) 116 | .isEqualTo(uploadRequest); 117 | } 118 | 119 | @Test 120 | public void should_ReturnException_When_RequestIsUnSuccessful() { 121 | // Given 122 | final UploadRequest uploadRequest = baseRequest.newBuilder() 123 | .setUploadDomain("upload-domain") 124 | .setPackageAssetId("package_asset_id") 125 | .setToken("token") 126 | .build(); 127 | mockWebServer.enqueue(new MockResponse().setResponseCode(400)); 128 | 129 | // When 130 | final ThrowingRunnable throwingRunnable = () -> task.execute(uploadRequest).get(); 131 | 132 | // Then 133 | final ExecutionException exception = assertThrows(ExecutionException.class, throwingRunnable); 134 | assertThat(exception).hasCauseThat().isInstanceOf(AppCenterException.class); 135 | assertThat(exception).hasCauseThat().hasMessageThat().contains("Finishing release unsuccessful: HTTP 400 Client Error: "); 136 | } 137 | 138 | @Test 139 | public void should_ReturnResponse_When_SymbolRequestIsSuccessful() throws Exception { 140 | // Given 141 | final UploadRequest uploadRequest = baseRequest.newBuilder() 142 | .setUploadDomain("upload-domain") 143 | .setPackageAssetId("package_asset_id") 144 | .setToken("token") 145 | .setSymbolUploadId("symbol_upload_id") 146 | .build(); 147 | mockWebServer.enqueue(new MockResponse().setResponseCode(200)); 148 | mockWebServer.enqueue(new MockResponse().setResponseCode(200).setBody("{\n" + 149 | " \"symbol_upload_id\": \"string\",\n" + 150 | " \"app_id\": \"string\",\n" + 151 | " \"user\": {\n" + 152 | " \"email\": \"string\",\n" + 153 | " \"display_name\": \"string\"\n" + 154 | " },\n" + 155 | " \"status\": \"created\",\n" + 156 | " \"symbol_type\": \"AndroidProguard\",\n" + 157 | " \"symbols_uploaded\": [\n" + 158 | " {\n" + 159 | " \"symbol_id\": \"string\",\n" + 160 | " \"platform\": \"string\"\n" + 161 | " }\n" + 162 | " ],\n" + 163 | " \"origin\": \"User\",\n" + 164 | " \"file_name\": \"string\",\n" + 165 | " \"file_size\": 0,\n" + 166 | " \"timestamp\": \"2019-11-17T12:12:06.701Z\"\n" + 167 | "}")); 168 | 169 | // When 170 | final UploadRequest actual = task.execute(uploadRequest).get(); 171 | 172 | // Then 173 | assertThat(actual) 174 | .isEqualTo(uploadRequest); 175 | } 176 | 177 | @Test 178 | public void should_ReturnException_When_SymbolRequestIsUnSuccessful() { 179 | // Given 180 | final UploadRequest uploadRequest = baseRequest.newBuilder() 181 | .setUploadDomain("upload-domain") 182 | .setPackageAssetId("package_asset_id") 183 | .setToken("token") 184 | .setSymbolUploadId("symbol_upload_id") 185 | .build(); 186 | mockWebServer.enqueue(new MockResponse().setResponseCode(200)); 187 | mockWebServer.enqueue(new MockResponse().setResponseCode(400)); 188 | 189 | // When 190 | final ThrowingRunnable throwingRunnable = () -> task.execute(uploadRequest).get(); 191 | 192 | // Then 193 | final ExecutionException exception = assertThrows(ExecutionException.class, throwingRunnable); 194 | assertThat(exception).hasCauseThat().isInstanceOf(AppCenterException.class); 195 | assertThat(exception).hasCauseThat().hasMessageThat().contains("Finishing symbol release unsuccessful: "); 196 | } 197 | } -------------------------------------------------------------------------------- /src/test/java/io/jenkins/plugins/appcenter/task/internal/PollForReleaseTaskTest.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.appcenter.task.internal; 2 | 3 | import hudson.ProxyConfiguration; 4 | import hudson.model.TaskListener; 5 | import hudson.util.Secret; 6 | import io.jenkins.plugins.appcenter.AppCenterException; 7 | import io.jenkins.plugins.appcenter.api.AppCenterServiceFactory; 8 | import io.jenkins.plugins.appcenter.task.request.UploadRequest; 9 | import okhttp3.mockwebserver.MockResponse; 10 | import okhttp3.mockwebserver.MockWebServer; 11 | import org.junit.Before; 12 | import org.junit.Rule; 13 | import org.junit.Test; 14 | import org.junit.function.ThrowingRunnable; 15 | import org.junit.runner.RunWith; 16 | import org.mockito.Mock; 17 | import org.mockito.junit.MockitoJUnitRunner; 18 | 19 | import java.io.PrintStream; 20 | import java.util.concurrent.ExecutionException; 21 | 22 | import static com.google.common.truth.Truth.assertThat; 23 | import static org.junit.Assert.assertThrows; 24 | import static org.mockito.BDDMockito.given; 25 | 26 | @RunWith(MockitoJUnitRunner.class) 27 | public class PollForReleaseTaskTest { 28 | 29 | @Rule 30 | public MockWebServer mockWebServer = new MockWebServer(); 31 | 32 | @Mock 33 | TaskListener mockTaskListener; 34 | 35 | @Mock 36 | PrintStream mockLogger; 37 | 38 | @Mock 39 | ProxyConfiguration mockProxyConfig; 40 | 41 | private UploadRequest baseRequest; 42 | 43 | private PollForReleaseTask task; 44 | 45 | @Before 46 | public void setUp() { 47 | baseRequest = new UploadRequest.Builder() 48 | .setOwnerName("owner-name") 49 | .setAppName("app-name") 50 | .build(); 51 | given(mockTaskListener.getLogger()).willReturn(mockLogger); 52 | final AppCenterServiceFactory factory = new AppCenterServiceFactory(Secret.fromString("secret-token"), mockWebServer.url("/").toString(), mockProxyConfig); 53 | task = new PollForReleaseTask(mockTaskListener, factory); 54 | } 55 | 56 | @Test 57 | public void should_ReturnException_When_UploadIdIsMissing() { 58 | // Given 59 | final UploadRequest uploadRequest = baseRequest.newBuilder() 60 | .build(); 61 | 62 | // When 63 | final ThrowingRunnable throwingRunnable = () -> task.execute(uploadRequest).get(); 64 | 65 | // Then 66 | final NullPointerException exception = assertThrows(NullPointerException.class, throwingRunnable); 67 | assertThat(exception).hasMessageThat().contains("uploadId cannot be null"); 68 | } 69 | 70 | @Test 71 | public void should_RetryPolling_When_StatusIsStartedOrFinished() throws Exception { 72 | // Given 73 | final UploadRequest uploadRequest = baseRequest.newBuilder() 74 | .setUploadId("upload_id") 75 | .build(); 76 | mockWebServer.enqueue(new MockResponse().setResponseCode(200).setBody("{\n" + 77 | " \"upload_status\": \"uploadStarted\" \n" + 78 | "}")); 79 | mockWebServer.enqueue(new MockResponse().setResponseCode(200).setBody("{\n" + 80 | " \"upload_status\": \"uploadFinished\" \n" + 81 | "}")); 82 | mockWebServer.enqueue(new MockResponse().setResponseCode(200).setBody("{\n" + 83 | " \"upload_status\": \"readyToBePublished\",\n" + 84 | " \"release_distinct_id\": 1234\n" + 85 | "}")); 86 | 87 | // When 88 | task.execute(uploadRequest).get(); 89 | 90 | // Then 91 | assertThat(mockWebServer.getRequestCount()) 92 | .isEqualTo(3); 93 | } 94 | 95 | @Test 96 | public void should_ReturnResponse_When_RequestIsSuccessful() throws Exception { 97 | // Given 98 | final UploadRequest uploadRequest = baseRequest.newBuilder() 99 | .setUploadId("upload_id") 100 | .build(); 101 | final UploadRequest expected = uploadRequest.newBuilder().setReleaseId(1234).build(); 102 | mockWebServer.enqueue(new MockResponse().setResponseCode(200).setBody("{\n" + 103 | " \"upload_status\": \"readyToBePublished\",\n" + 104 | " \"release_distinct_id\": 1234\n" + 105 | "}")); 106 | 107 | // When 108 | final UploadRequest actual = task.execute(uploadRequest).get(); 109 | 110 | // Then 111 | assertThat(actual) 112 | .isEqualTo(expected); 113 | } 114 | 115 | @Test 116 | public void should_ReturnException_When_StatusIsMalware() { 117 | // Given 118 | final UploadRequest uploadRequest = baseRequest.newBuilder() 119 | .setUploadId("upload_id") 120 | .build(); 121 | mockWebServer.enqueue(new MockResponse().setResponseCode(200).setBody("{\n" + 122 | " \"upload_status\": \"malwareDetected\",\n" + 123 | " \"error_details\": \"we found this does it belong to you?\"\n" + 124 | "}")); 125 | 126 | // When 127 | final ThrowingRunnable throwingRunnable = () -> task.execute(uploadRequest).get(); 128 | 129 | // Then 130 | final ExecutionException exception = assertThrows(ExecutionException.class, throwingRunnable); 131 | assertThat(exception).hasCauseThat().isInstanceOf(AppCenterException.class); 132 | assertThat(exception).hasCauseThat().hasMessageThat().contains("Polling for app release successful however was rejected by server: we found this does it belong to you?"); 133 | } 134 | 135 | @Test 136 | public void should_ReturnException_When_StatusIsError() { 137 | // Given 138 | final UploadRequest uploadRequest = baseRequest.newBuilder() 139 | .setUploadId("upload_id") 140 | .build(); 141 | mockWebServer.enqueue(new MockResponse().setResponseCode(200).setBody("{\n" + 142 | " \"upload_status\": \"error\",\n" + 143 | " \"error_details\": \"error error error\"\n" + 144 | "}")); 145 | 146 | // When 147 | final ThrowingRunnable throwingRunnable = () -> task.execute(uploadRequest).get(); 148 | 149 | // Then 150 | final ExecutionException exception = assertThrows(ExecutionException.class, throwingRunnable); 151 | assertThat(exception).hasCauseThat().isInstanceOf(AppCenterException.class); 152 | assertThat(exception).hasCauseThat().hasMessageThat().contains("Polling for app release successful however was rejected by server: error error error"); 153 | } 154 | 155 | @Test 156 | public void should_ReturnException_When_StatusIsUnknown() { 157 | // Given 158 | final UploadRequest uploadRequest = baseRequest.newBuilder() 159 | .setUploadId("upload_id") 160 | .build(); 161 | mockWebServer.enqueue(new MockResponse().setResponseCode(200).setBody("{\n" + 162 | " \"upload_status\": \"huggobilly\",\n" + 163 | " \"error_details\": \"hubally goobally hobbilly goobilly\"\n" + 164 | "}")); 165 | 166 | // When 167 | final ThrowingRunnable throwingRunnable = () -> task.execute(uploadRequest).get(); 168 | 169 | // Then 170 | final ExecutionException exception = assertThrows(ExecutionException.class, throwingRunnable); 171 | assertThat(exception).hasCauseThat().isInstanceOf(AppCenterException.class); 172 | assertThat(exception).hasCauseThat().hasMessageThat().contains("Polling for app release unsuccessful"); 173 | } 174 | 175 | @Test 176 | public void should_ReturnException_When_RequestIsUnSuccessful() { 177 | // Given 178 | final UploadRequest uploadRequest = baseRequest.newBuilder() 179 | .setUploadId("upload_id") 180 | .build(); 181 | mockWebServer.enqueue(new MockResponse().setResponseCode(400)); 182 | 183 | // When 184 | final ThrowingRunnable throwingRunnable = () -> task.execute(uploadRequest).get(); 185 | 186 | // Then 187 | final ExecutionException exception = assertThrows(ExecutionException.class, throwingRunnable); 188 | assertThat(exception).hasCauseThat().isInstanceOf(AppCenterException.class); 189 | assertThat(exception).hasCauseThat().hasMessageThat().contains("Polling for app release unsuccessful"); 190 | } 191 | } -------------------------------------------------------------------------------- /src/test/java/io/jenkins/plugins/appcenter/util/RemoteFileUtilsTest.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.appcenter.util; 2 | 3 | import hudson.FilePath; 4 | import org.apache.commons.lang3.SystemUtils; 5 | import org.junit.Before; 6 | import org.junit.Test; 7 | import org.junit.runner.RunWith; 8 | import org.mockito.Mock; 9 | import org.mockito.junit.MockitoJUnitRunner; 10 | 11 | import java.io.File; 12 | 13 | import static com.google.common.truth.Truth.assertThat; 14 | import static io.jenkins.plugins.appcenter.util.TestFileUtil.TEST_FILE_PATH; 15 | import static org.mockito.ArgumentMatchers.anyString; 16 | import static org.mockito.BDDMockito.given; 17 | 18 | @RunWith(MockitoJUnitRunner.class) 19 | public class RemoteFileUtilsTest { 20 | 21 | @Mock 22 | FilePath filePath; 23 | 24 | private RemoteFileUtils remoteFileUtils; 25 | 26 | @Before 27 | public void setUp() { 28 | given(filePath.child(anyString())).willReturn(filePath); 29 | remoteFileUtils = new RemoteFileUtils(filePath); 30 | } 31 | 32 | @Test 33 | public void should_ReturnFile_When_FileExists() { 34 | // Given 35 | final File expected = new File(TEST_FILE_PATH); 36 | given(filePath.getRemote()).willReturn(TEST_FILE_PATH); 37 | 38 | // When 39 | final File remoteFile = remoteFileUtils.getRemoteFile(TEST_FILE_PATH); 40 | 41 | // Then 42 | assertThat(remoteFile).isEqualTo(expected); 43 | } 44 | 45 | @Test 46 | public void should_ReturnFileName_When_FileExists() { 47 | // Given 48 | given(filePath.getRemote()).willReturn(TEST_FILE_PATH); 49 | 50 | // When 51 | final String fileName = remoteFileUtils.getFileName(TEST_FILE_PATH); 52 | 53 | // Then 54 | assertThat(fileName).isEqualTo("xiola.apk"); 55 | } 56 | 57 | @Test 58 | public void should_ReturnFileSize_When_FileExists() { 59 | // Given 60 | given(filePath.getRemote()).willReturn(TEST_FILE_PATH); 61 | 62 | // When 63 | final long fileSize = remoteFileUtils.getFileSize(TEST_FILE_PATH); 64 | 65 | // Then 66 | // Windows reports the file size differently so for the sake of simplicity we adjust our assertion here. 67 | assertThat(fileSize).isEqualTo(SystemUtils.IS_OS_UNIX ? 41 : 42); 68 | } 69 | 70 | @Test 71 | public void should_ReturnContentType_When_APK() { 72 | // Given 73 | final String pathToFile = "test.apk"; 74 | 75 | // When 76 | final String contentType = remoteFileUtils.getContentType(pathToFile); 77 | 78 | // Then 79 | assertThat(contentType).isEqualTo("application/vnd.android.package-archive"); 80 | } 81 | 82 | @Test 83 | public void should_ReturnContentType_When_AAB() { 84 | // Given 85 | final String pathToFile = "test.aab"; 86 | 87 | // When 88 | final String contentType = remoteFileUtils.getContentType(pathToFile); 89 | 90 | // Then 91 | assertThat(contentType).isEqualTo("application/vnd.android.package-archive"); 92 | } 93 | 94 | @Test 95 | public void should_ReturnContentType_When_MSI() { 96 | // Given 97 | final String pathToFile = "test.msi"; 98 | 99 | // When 100 | final String contentType = remoteFileUtils.getContentType(pathToFile); 101 | 102 | // Then 103 | assertThat(contentType).isEqualTo("application/x-msi"); 104 | } 105 | 106 | @Test 107 | public void should_ReturnContentType_When_PLIST() { 108 | // Given 109 | final String pathToFile = "test.plist"; 110 | 111 | // When 112 | final String contentType = remoteFileUtils.getContentType(pathToFile); 113 | 114 | // Then 115 | assertThat(contentType).isEqualTo("application/xml"); 116 | } 117 | 118 | @Test 119 | public void should_ReturnContentType_When_AETX() { 120 | // Given 121 | final String pathToFile = "test.aetx"; 122 | 123 | // When 124 | final String contentType = remoteFileUtils.getContentType(pathToFile); 125 | 126 | // Then 127 | assertThat(contentType).isEqualTo("application/c-x509-ca-cert"); 128 | } 129 | 130 | @Test 131 | public void should_ReturnContentType_When_CER() { 132 | // Given 133 | final String pathToFile = "test.cer"; 134 | 135 | // When 136 | final String contentType = remoteFileUtils.getContentType(pathToFile); 137 | 138 | // Then 139 | assertThat(contentType).isEqualTo("application/pkix-cert"); 140 | } 141 | 142 | @Test 143 | public void should_ReturnContentType_When_XAP() { 144 | // Given 145 | final String pathToFile = "test.xap"; 146 | 147 | // When 148 | final String contentType = remoteFileUtils.getContentType(pathToFile); 149 | 150 | // Then 151 | assertThat(contentType).isEqualTo("application/x-silverlight-app"); 152 | } 153 | 154 | @Test 155 | public void should_ReturnContentType_When_APPX() { 156 | // Given 157 | final String pathToFile = "test.appx"; 158 | 159 | // When 160 | final String contentType = remoteFileUtils.getContentType(pathToFile); 161 | 162 | // Then 163 | assertThat(contentType).isEqualTo("application/x-appx"); 164 | } 165 | 166 | @Test 167 | public void should_ReturnContentType_When_APPXBUNDLE() { 168 | // Given 169 | final String pathToFile = "test.appxbundle"; 170 | 171 | // When 172 | final String contentType = remoteFileUtils.getContentType(pathToFile); 173 | 174 | // Then 175 | assertThat(contentType).isEqualTo("application/x-appxbundle"); 176 | } 177 | 178 | @Test 179 | public void should_ReturnContentType_When_APPXUPLOAD() { 180 | // Given 181 | final String pathToFile = "test.appxupload"; 182 | 183 | // When 184 | final String contentType = remoteFileUtils.getContentType(pathToFile); 185 | 186 | // Then 187 | assertThat(contentType).isEqualTo("application/x-appxupload"); 188 | } 189 | 190 | @Test 191 | public void should_ReturnContentType_When_APPXSYM() { 192 | // Given 193 | final String pathToFile = "test.appxsym"; 194 | 195 | // When 196 | final String contentType = remoteFileUtils.getContentType(pathToFile); 197 | 198 | // Then 199 | assertThat(contentType).isEqualTo("application/x-appxupload"); 200 | } 201 | 202 | @Test 203 | public void should_ReturnContentType_When_MSIX() { 204 | // Given 205 | final String pathToFile = "test.msix"; 206 | 207 | // When 208 | final String contentType = remoteFileUtils.getContentType(pathToFile); 209 | 210 | // Then 211 | assertThat(contentType).isEqualTo("application/x-msix"); 212 | } 213 | 214 | @Test 215 | public void should_ReturnContentType_When_MSIXBUNDLE() { 216 | // Given 217 | final String pathToFile = "test.msixbundle"; 218 | 219 | // When 220 | final String contentType = remoteFileUtils.getContentType(pathToFile); 221 | 222 | // Then 223 | assertThat(contentType).isEqualTo("application/x-msixbundle"); 224 | } 225 | 226 | @Test 227 | public void should_ReturnContentType_When_MSIXUPLOAD() { 228 | // Given 229 | final String pathToFile = "test.msixupload"; 230 | 231 | // When 232 | final String contentType = remoteFileUtils.getContentType(pathToFile); 233 | 234 | // Then 235 | assertThat(contentType).isEqualTo("application/x-msixupload"); 236 | } 237 | 238 | @Test 239 | public void should_ReturnContentType_When_MSIXSYM() { 240 | // Given 241 | final String pathToFile = "test.msixsym"; 242 | 243 | // When 244 | final String contentType = remoteFileUtils.getContentType(pathToFile); 245 | 246 | // Then 247 | assertThat(contentType).isEqualTo("application/x-msixupload"); 248 | } 249 | 250 | @Test 251 | public void should_ReturnContentType_When_UNKNOWN() { 252 | // Given 253 | final String pathToFile = "test.foo"; 254 | 255 | // When 256 | final String contentType = remoteFileUtils.getContentType(pathToFile); 257 | 258 | // Then 259 | assertThat(contentType).isEqualTo("application/octet-stream"); 260 | } 261 | } -------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/appcenter/task/internal/UploadAppToResourceTask.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.appcenter.task.internal; 2 | 3 | import hudson.model.TaskListener; 4 | import io.jenkins.plugins.appcenter.AppCenterException; 5 | import io.jenkins.plugins.appcenter.AppCenterLogger; 6 | import io.jenkins.plugins.appcenter.api.AppCenterServiceFactory; 7 | import io.jenkins.plugins.appcenter.task.request.UploadRequest; 8 | import io.jenkins.plugins.appcenter.util.RemoteFileUtils; 9 | import okhttp3.RequestBody; 10 | import okio.Buffer; 11 | import okio.BufferedSource; 12 | import okio.Okio; 13 | 14 | import javax.annotation.Nonnull; 15 | import javax.inject.Inject; 16 | import javax.inject.Singleton; 17 | import java.io.File; 18 | import java.io.IOException; 19 | import java.io.PrintStream; 20 | import java.util.concurrent.CompletableFuture; 21 | 22 | import static java.util.Objects.requireNonNull; 23 | 24 | @Singleton 25 | public final class UploadAppToResourceTask implements AppCenterTask, AppCenterLogger { 26 | 27 | private static final long serialVersionUID = 1L; 28 | private static final int MAX_NON_CHUNKED_UPLOAD_SIZE = (1024 * 1024) * 256; // 256 MB in bytes 29 | 30 | @Nonnull 31 | private final TaskListener taskListener; 32 | @Nonnull 33 | private final AppCenterServiceFactory factory; 34 | @Nonnull 35 | private final RemoteFileUtils remoteFileUtils; 36 | 37 | @Inject 38 | UploadAppToResourceTask(@Nonnull final TaskListener taskListener, 39 | @Nonnull final AppCenterServiceFactory factory, 40 | @Nonnull final RemoteFileUtils remoteFileUtils) { 41 | this.taskListener = taskListener; 42 | this.factory = factory; 43 | this.remoteFileUtils = remoteFileUtils; 44 | } 45 | 46 | @Nonnull 47 | @Override 48 | public CompletableFuture execute(@Nonnull UploadRequest request) { 49 | if (request.symbolUploadUrl == null) { 50 | return uploadApp(request); 51 | } else { 52 | return uploadApp(request) 53 | .thenCompose(this::uploadSymbols); 54 | } 55 | } 56 | 57 | @Nonnull 58 | private CompletableFuture uploadApp(@Nonnull UploadRequest request) { 59 | requireNonNull(request.uploadDomain, "uploadDomain cannot be null"); 60 | requireNonNull(request.packageAssetId, "packageAssetId cannot be null"); 61 | requireNonNull(request.token, "token cannot be null"); 62 | final Integer chunkSize = requireNonNull(request.chunkSize, "chunkSize cannot be null"); 63 | 64 | log("Uploading app to resource."); 65 | 66 | final CompletableFuture future = new CompletableFuture<>(); 67 | 68 | int offset = 0; 69 | int blockNumber = 1; 70 | calculateChunks(request, offset, chunkSize, blockNumber, future); 71 | 72 | return future; 73 | } 74 | 75 | private void calculateChunks(@Nonnull UploadRequest request, int offset, int chunkSize, int blockNumber, @Nonnull CompletableFuture future) { 76 | // TODO: Retrofit (via OkHttp) is supposed to be able to do this natively if you set the contentLength to -1. Investigate 77 | final String url = getUrl(request); 78 | final File file = remoteFileUtils.getRemoteFile(request.pathToApp); 79 | final long fileSize = file.length(); 80 | final int noOfBlocks = (int) Math.ceil((double) fileSize / chunkSize); 81 | 82 | try (final BufferedSource bufferedSource = Okio.buffer(Okio.source(file))) { 83 | bufferedSource.skip(offset); 84 | final Buffer buffer = new Buffer(); 85 | bufferedSource.request(chunkSize); 86 | bufferedSource.read(buffer, chunkSize); 87 | final RequestBody requestFile = RequestBody.create(buffer.readByteArray(), null); 88 | upload(request, offset, chunkSize, url, requestFile, blockNumber, noOfBlocks, future); 89 | } catch (IOException e) { 90 | final AppCenterException exception = logFailure("Upload app to resource unsuccessful", e); 91 | future.completeExceptionally(exception); 92 | } 93 | } 94 | 95 | private void upload(@Nonnull UploadRequest request, int offset, int chunkSize, @Nonnull String url, @Nonnull RequestBody requestFile, int blockNumber, int noOfBlocks, @Nonnull CompletableFuture future) { 96 | factory.createAppCenterService() 97 | .uploadApp(url + "&block_number=" + blockNumber, requestFile) 98 | .whenComplete((responseBody, throwable) -> { 99 | if (throwable != null) { 100 | final AppCenterException exception = logFailure("Upload app to resource unsuccessful", throwable); 101 | future.completeExceptionally(exception); 102 | } else { 103 | log(String.format("Upload app to resource chunk %1$d / %2$d successful.", blockNumber, noOfBlocks)); 104 | if (blockNumber == noOfBlocks) { 105 | future.complete(request); 106 | } else { 107 | calculateChunks(request, offset + chunkSize, chunkSize, blockNumber + 1, future); 108 | } 109 | 110 | } 111 | }); 112 | } 113 | 114 | @Nonnull 115 | private String getUrl(@Nonnull UploadRequest request) { 116 | return String.format("%1$s/upload/upload_chunk/%2$s?token=%3$s", request.uploadDomain, request.packageAssetId, request.token); 117 | } 118 | 119 | @Nonnull 120 | private CompletableFuture uploadSymbols(@Nonnull UploadRequest request) { 121 | final String pathToDebugSymbols = request.pathToDebugSymbols; 122 | final String symbolUploadUrl = requireNonNull(request.symbolUploadUrl, "symbolUploadUrl cannot be null"); 123 | 124 | final File file = remoteFileUtils.getRemoteFile(pathToDebugSymbols); 125 | if (file.length() > MAX_NON_CHUNKED_UPLOAD_SIZE) { 126 | return uploadSymbolsChunked(request, symbolUploadUrl, file); 127 | } else { 128 | return uploadSymbolsComplete(request, symbolUploadUrl, file); 129 | } 130 | } 131 | 132 | @Nonnull 133 | private CompletableFuture uploadSymbolsChunked(@Nonnull UploadRequest request, @Nonnull String symbolUploadUrl, @Nonnull File file) { 134 | log("Uploading symbols to resource chunked."); 135 | 136 | final CompletableFuture future = new CompletableFuture<>(); 137 | 138 | CompletableFuture.runAsync(() -> factory.createBlobUploadService(symbolUploadUrl).uploadFromFile(file.getPath(), true)) 139 | .whenComplete((responseBody, throwable) -> { 140 | if (throwable != null) { 141 | final AppCenterException exception = logFailure("Upload symbols to resource chunked unsuccessful: ", throwable); 142 | future.completeExceptionally(exception); 143 | } else { 144 | log("Upload symbols to resource chunked successful."); 145 | future.complete(request); 146 | } 147 | }); 148 | 149 | return future; 150 | } 151 | 152 | @Nonnull 153 | private CompletableFuture uploadSymbolsComplete(@Nonnull UploadRequest request, @Nonnull String symbolUploadUrl, @Nonnull File file) { 154 | log("Uploading all symbols at once to resource."); 155 | 156 | final CompletableFuture future = new CompletableFuture<>(); 157 | final RequestBody requestFile = RequestBody.create(file, null); 158 | 159 | factory.createUploadService(symbolUploadUrl) 160 | .uploadSymbols(symbolUploadUrl, requestFile) 161 | .whenComplete((responseBody, throwable) -> { 162 | if (throwable != null) { 163 | final AppCenterException exception = logFailure("Upload symbols to resource unsuccessful: ", throwable); 164 | future.completeExceptionally(exception); 165 | } else { 166 | log("Upload symbols to resource successful."); 167 | future.complete(request); 168 | } 169 | }); 170 | 171 | return future; 172 | } 173 | 174 | 175 | @Override 176 | public PrintStream getLogger() { 177 | return taskListener.getLogger(); 178 | } 179 | } --------------------------------------------------------------------------------