From 134cee929ef0cbec9a0725b9ffc6625ac3d40fe8 Mon Sep 17 00:00:00 2001 From: Mez Pahlan Date: Tue, 8 Dec 2020 16:33:06 +0000 Subject: [PATCH 01/24] [JENKINS-64388] Move non null builder parameters --- .../jenkins/plugins/appcenter/task/request/UploadRequest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/io/jenkins/plugins/appcenter/task/request/UploadRequest.java b/src/main/java/io/jenkins/plugins/appcenter/task/request/UploadRequest.java index 2a60c9a..f0d045e 100644 --- a/src/main/java/io/jenkins/plugins/appcenter/task/request/UploadRequest.java +++ b/src/main/java/io/jenkins/plugins/appcenter/task/request/UploadRequest.java @@ -195,6 +195,8 @@ public Builder() { this.mandatoryUpdate = uploadRequest.mandatoryUpdate; this.buildVersion = uploadRequest.buildVersion; this.pathToDebugSymbols = uploadRequest.pathToDebugSymbols; + this.commitHash = uploadRequest.commitHash; + this.branchName = uploadRequest.branchName; // Expected to be nullable until they are added during UploadTask. this.uploadUrl = uploadRequest.uploadUrl; @@ -203,8 +205,6 @@ public Builder() { this.symbolUploadRequest = uploadRequest.symbolUploadRequest; this.symbolUploadUrl = uploadRequest.symbolUploadUrl; this.symbolUploadId = uploadRequest.symbolUploadId; - this.commitHash = uploadRequest.commitHash; - this.branchName = uploadRequest.branchName; } public Builder setOwnerName(@Nonnull String ownerName) { From e66184a14c1fd7a5ddc197cf01bbb6332eca4d7d Mon Sep 17 00:00:00 2001 From: Mez Pahlan Date: Tue, 8 Dec 2020 16:53:07 +0000 Subject: [PATCH 02/24] [JENKINS-64388] Update releaseUploadCreate task --- .../appcenter/api/AppCenterService.java | 18 +---- .../appcenter/ReleaseUploadBeginRequest.java | 11 +-- .../appcenter/ReleaseUploadBeginResponse.java | 51 ++++++------- .../internal/CreateUploadResourceTask.java | 9 ++- .../appcenter/task/request/UploadRequest.java | 76 ++++++++++++------- 5 files changed, 84 insertions(+), 81 deletions(-) diff --git a/src/main/java/io/jenkins/plugins/appcenter/api/AppCenterService.java b/src/main/java/io/jenkins/plugins/appcenter/api/AppCenterService.java index b80365e..ce0ddb5 100644 --- a/src/main/java/io/jenkins/plugins/appcenter/api/AppCenterService.java +++ b/src/main/java/io/jenkins/plugins/appcenter/api/AppCenterService.java @@ -1,26 +1,14 @@ package io.jenkins.plugins.appcenter.api; -import io.jenkins.plugins.appcenter.model.appcenter.ReleaseDetailsUpdateResponse; -import io.jenkins.plugins.appcenter.model.appcenter.ReleaseUpdateRequest; -import io.jenkins.plugins.appcenter.model.appcenter.ReleaseUploadBeginRequest; -import io.jenkins.plugins.appcenter.model.appcenter.ReleaseUploadBeginResponse; -import io.jenkins.plugins.appcenter.model.appcenter.ReleaseUploadEndRequest; -import io.jenkins.plugins.appcenter.model.appcenter.ReleaseUploadEndResponse; -import io.jenkins.plugins.appcenter.model.appcenter.SymbolUpload; -import io.jenkins.plugins.appcenter.model.appcenter.SymbolUploadBeginRequest; -import io.jenkins.plugins.appcenter.model.appcenter.SymbolUploadBeginResponse; -import io.jenkins.plugins.appcenter.model.appcenter.SymbolUploadEndRequest; -import retrofit2.http.Body; -import retrofit2.http.PATCH; -import retrofit2.http.POST; -import retrofit2.http.Path; +import io.jenkins.plugins.appcenter.model.appcenter.*; +import retrofit2.http.*; import javax.annotation.Nonnull; import java.util.concurrent.CompletableFuture; public interface AppCenterService { - @POST("v0.1/apps/{owner_name}/{app_name}/release_uploads") + @POST("v0.1/apps/{owner_name}/{app_name}/uploads/releases") CompletableFuture releaseUploadsCreate( @Path("owner_name") @Nonnull String user, @Path("app_name") @Nonnull String appName, diff --git a/src/main/java/io/jenkins/plugins/appcenter/model/appcenter/ReleaseUploadBeginRequest.java b/src/main/java/io/jenkins/plugins/appcenter/model/appcenter/ReleaseUploadBeginRequest.java index b13fa3b..380126b 100644 --- a/src/main/java/io/jenkins/plugins/appcenter/model/appcenter/ReleaseUploadBeginRequest.java +++ b/src/main/java/io/jenkins/plugins/appcenter/model/appcenter/ReleaseUploadBeginRequest.java @@ -5,15 +5,12 @@ public final class ReleaseUploadBeginRequest { - @Nullable - public final Integer release_id; @Nullable public final String build_version; @Nullable public final String build_number; - public ReleaseUploadBeginRequest(@Nullable Integer releaseId, @Nullable String buildVersion, @Nullable String buildNumber) { - this.release_id = releaseId; + public ReleaseUploadBeginRequest(@Nullable String buildVersion, @Nullable String buildNumber) { this.build_version = buildVersion; this.build_number = buildNumber; } @@ -21,7 +18,6 @@ public ReleaseUploadBeginRequest(@Nullable Integer releaseId, @Nullable String b @Override public String toString() { return "ReleaseUploadBeginRequest{" + - "release_id=" + release_id + ", build_version='" + build_version + '\'' + ", build_number='" + build_number + '\'' + '}'; @@ -32,13 +28,12 @@ public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; ReleaseUploadBeginRequest that = (ReleaseUploadBeginRequest) o; - return Objects.equals(release_id, that.release_id) && - Objects.equals(build_version, that.build_version) && + return Objects.equals(build_version, that.build_version) && Objects.equals(build_number, that.build_number); } @Override public int hashCode() { - return Objects.hash(release_id, build_version, build_number); + return Objects.hash(build_version, build_number); } } \ No newline at end of file diff --git a/src/main/java/io/jenkins/plugins/appcenter/model/appcenter/ReleaseUploadBeginResponse.java b/src/main/java/io/jenkins/plugins/appcenter/model/appcenter/ReleaseUploadBeginResponse.java index ac0cdf0..8410d85 100644 --- a/src/main/java/io/jenkins/plugins/appcenter/model/appcenter/ReleaseUploadBeginResponse.java +++ b/src/main/java/io/jenkins/plugins/appcenter/model/appcenter/ReleaseUploadBeginResponse.java @@ -1,37 +1,36 @@ package io.jenkins.plugins.appcenter.model.appcenter; import javax.annotation.Nonnull; -import javax.annotation.Nullable; import java.util.Objects; public final class ReleaseUploadBeginResponse { @Nonnull - public final String upload_id; + public final String id; @Nonnull - public final String upload_url; - @Nullable - public final String asset_id; - @Nullable - public final String asset_domain; - @Nullable - public final String asset_token; + public final String upload_domain; + @Nonnull + public final String token; + @Nonnull + public final String url_encoded_token; + @Nonnull + public final String package_asset_id; - public ReleaseUploadBeginResponse(@Nonnull String uploadId, @Nonnull String uploadUrl, @Nullable String assetId, @Nullable String assetDomain, @Nullable String assetToken) { - this.upload_id = uploadId; - this.upload_url = uploadUrl; - this.asset_id = assetId; - this.asset_domain = assetDomain; - this.asset_token = assetToken; + public ReleaseUploadBeginResponse(@Nonnull String id, @Nonnull String uploadDomain, @Nonnull String token, @Nonnull String urlEncodedToken, @Nonnull String packageAssetId) { + this.id = id; + this.upload_domain = uploadDomain; + this.token = token; + this.url_encoded_token = urlEncodedToken; + this.package_asset_id = packageAssetId; } @Override public String toString() { return "ReleaseUploadBeginResponse{" + - "upload_id='" + upload_id + '\'' + - ", upload_url='" + upload_url + '\'' + - ", asset_id='" + asset_id + '\'' + - ", asset_domain='" + asset_domain + '\'' + - ", asset_token='" + asset_token + '\'' + + "id='" + id + '\'' + + ", upload_domain='" + upload_domain + '\'' + + ", token='" + token + '\'' + + ", url_encoded_token='" + url_encoded_token + '\'' + + ", package_asset_id='" + package_asset_id + '\'' + '}'; } @@ -40,15 +39,15 @@ public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; ReleaseUploadBeginResponse that = (ReleaseUploadBeginResponse) o; - return upload_id.equals(that.upload_id) && - upload_url.equals(that.upload_url) && - Objects.equals(asset_id, that.asset_id) && - Objects.equals(asset_domain, that.asset_domain) && - Objects.equals(asset_token, that.asset_token); + return id.equals(that.id) && + upload_domain.equals(that.upload_domain) && + token.equals(that.token) && + url_encoded_token.equals(that.url_encoded_token) && + package_asset_id.equals(that.package_asset_id); } @Override public int hashCode() { - return Objects.hash(upload_id, upload_url, asset_id, asset_domain, asset_token); + return Objects.hash(id, upload_domain, token, url_encoded_token, package_asset_id); } } \ No newline at end of file diff --git a/src/main/java/io/jenkins/plugins/appcenter/task/internal/CreateUploadResourceTask.java b/src/main/java/io/jenkins/plugins/appcenter/task/internal/CreateUploadResourceTask.java index ef8b36b..13a90ae 100644 --- a/src/main/java/io/jenkins/plugins/appcenter/task/internal/CreateUploadResourceTask.java +++ b/src/main/java/io/jenkins/plugins/appcenter/task/internal/CreateUploadResourceTask.java @@ -50,8 +50,7 @@ private CompletableFuture createUploadResourceForApp(@Nonnull Upl final CompletableFuture future = new CompletableFuture<>(); - // TODO: Pass in the release_id as an optional parameter from the UI. Don't use it if not available - final ReleaseUploadBeginRequest releaseUploadBeginRequest = new ReleaseUploadBeginRequest(null, request.buildVersion, null); + final ReleaseUploadBeginRequest releaseUploadBeginRequest = new ReleaseUploadBeginRequest(request.buildVersion, null); factory.createAppCenterService() .releaseUploadsCreate(request.ownerName, request.appName, releaseUploadBeginRequest) .whenComplete((releaseUploadBeginResponse, throwable) -> { @@ -61,8 +60,10 @@ private CompletableFuture createUploadResourceForApp(@Nonnull Upl } else { log("Create upload resource for app successful."); final UploadRequest uploadRequest = request.newBuilder() - .setUploadUrl(releaseUploadBeginResponse.upload_url) - .setUploadId(releaseUploadBeginResponse.upload_id) + .setUploadId(releaseUploadBeginResponse.id) + .setUploadDomain(releaseUploadBeginResponse.upload_domain) + .setToken(releaseUploadBeginResponse.url_encoded_token) + .setPackageAssetId(releaseUploadBeginResponse.package_asset_id) .build(); future.complete(uploadRequest); } diff --git a/src/main/java/io/jenkins/plugins/appcenter/task/request/UploadRequest.java b/src/main/java/io/jenkins/plugins/appcenter/task/request/UploadRequest.java index f0d045e..35280a6 100644 --- a/src/main/java/io/jenkins/plugins/appcenter/task/request/UploadRequest.java +++ b/src/main/java/io/jenkins/plugins/appcenter/task/request/UploadRequest.java @@ -34,11 +34,17 @@ public final class UploadRequest implements Serializable { // Properties below this line are expected to be set during a run as these values will come from AppCenter during // execution they should be nullable prior to being set. - @Nullable - public final String uploadUrl; @Nullable public final String uploadId; @Nullable + public final String uploadDomain; + @Nullable + public final String token; + @Nullable + public final String packageAssetId; + @Nullable + public final Integer chunkSize; + @Nullable public final Integer releaseId; @Nullable public final SymbolUploadBeginRequest symbolUploadRequest; @@ -64,8 +70,11 @@ public String toString() { ", mandatoryUpdate=" + mandatoryUpdate + ", buildVersion='" + buildVersion + '\'' + ", pathToDebugSymbols='" + pathToDebugSymbols + '\'' + - ", uploadUrl='" + uploadUrl + '\'' + ", uploadId='" + uploadId + '\'' + + ", uploadDomain='" + uploadDomain + '\'' + + ", token='" + token + '\'' + + ", packageAssetId='" + packageAssetId + '\'' + + ", chunkSize=" + chunkSize + ", releaseId=" + releaseId + ", symbolUploadRequest=" + symbolUploadRequest + ", symbolUploadUrl='" + symbolUploadUrl + '\'' + @@ -80,27 +89,12 @@ public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; UploadRequest that = (UploadRequest) o; - return notifyTesters == that.notifyTesters && - mandatoryUpdate == that.mandatoryUpdate && - ownerName.equals(that.ownerName) && - appName.equals(that.appName) && - pathToApp.equals(that.pathToApp) && - destinationGroups.equals(that.destinationGroups) && - releaseNotes.equals(that.releaseNotes) && - pathToReleaseNotes.equals(that.pathToReleaseNotes) && - buildVersion.equals(that.buildVersion) && - pathToDebugSymbols.equals(that.pathToDebugSymbols) && - Objects.equals(uploadUrl, that.uploadUrl) && - Objects.equals(uploadId, that.uploadId) && - Objects.equals(releaseId, that.releaseId) && - Objects.equals(symbolUploadRequest, that.symbolUploadRequest) && - Objects.equals(symbolUploadUrl, that.symbolUploadUrl) && - Objects.equals(symbolUploadId, that.symbolUploadId); + return notifyTesters == that.notifyTesters && mandatoryUpdate == that.mandatoryUpdate && ownerName.equals(that.ownerName) && appName.equals(that.appName) && pathToApp.equals(that.pathToApp) && destinationGroups.equals(that.destinationGroups) && releaseNotes.equals(that.releaseNotes) && pathToReleaseNotes.equals(that.pathToReleaseNotes) && buildVersion.equals(that.buildVersion) && pathToDebugSymbols.equals(that.pathToDebugSymbols) && Objects.equals(uploadId, that.uploadId) && Objects.equals(uploadDomain, that.uploadDomain) && Objects.equals(token, that.token) && Objects.equals(packageAssetId, that.packageAssetId) && Objects.equals(chunkSize, that.chunkSize) && Objects.equals(releaseId, that.releaseId) && Objects.equals(symbolUploadRequest, that.symbolUploadRequest) && Objects.equals(symbolUploadUrl, that.symbolUploadUrl) && Objects.equals(symbolUploadId, that.symbolUploadId) && Objects.equals(commitHash, that.commitHash) && Objects.equals(branchName, that.branchName); } @Override public int hashCode() { - return Objects.hash(ownerName, appName, pathToApp, destinationGroups, releaseNotes, pathToReleaseNotes, mandatoryUpdate, notifyTesters, buildVersion, pathToDebugSymbols, uploadUrl, uploadId, releaseId, symbolUploadRequest, symbolUploadUrl, symbolUploadId); + return Objects.hash(ownerName, appName, pathToApp, destinationGroups, releaseNotes, pathToReleaseNotes, notifyTesters, mandatoryUpdate, buildVersion, pathToDebugSymbols, uploadId, uploadDomain, token, packageAssetId, chunkSize, releaseId, symbolUploadRequest, symbolUploadUrl, symbolUploadId, commitHash, branchName); } private UploadRequest(Builder builder) { @@ -116,8 +110,11 @@ private UploadRequest(Builder builder) { this.pathToDebugSymbols = builder.pathToDebugSymbols; // Expected to be nullable until they are added during UploadTask. - this.uploadUrl = builder.uploadUrl; this.uploadId = builder.uploadId; + this.uploadDomain = builder.uploadDomain; + this.token = builder.token; + this.packageAssetId = builder.packageAssetId; + this.chunkSize = builder.chunkSize; this.releaseId = builder.releaseId; this.symbolUploadRequest = builder.symbolUploadRequest; this.symbolUploadUrl = builder.symbolUploadUrl; @@ -153,10 +150,16 @@ public static final class Builder { // Expected to be nullable until they are added during UploadTask. @Nullable - private String uploadUrl; - @Nullable private String uploadId; @Nullable + private String uploadDomain; + @Nullable + private String token; + @Nullable + private String packageAssetId; + @Nullable + private Integer chunkSize; + @Nullable private Integer releaseId; @Nullable private SymbolUploadBeginRequest symbolUploadRequest; @@ -199,8 +202,11 @@ public Builder() { this.branchName = uploadRequest.branchName; // Expected to be nullable until they are added during UploadTask. - this.uploadUrl = uploadRequest.uploadUrl; this.uploadId = uploadRequest.uploadId; + this.uploadDomain = uploadRequest.uploadDomain; + this.token = uploadRequest.token; + this.packageAssetId = uploadRequest.packageAssetId; + this.chunkSize = uploadRequest.chunkSize; this.releaseId = uploadRequest.releaseId; this.symbolUploadRequest = uploadRequest.symbolUploadRequest; this.symbolUploadUrl = uploadRequest.symbolUploadUrl; @@ -260,14 +266,28 @@ public Builder setPathToDebugSymbols(@Nonnull String pathToDebugSymbols) { // Properties above this line are expected to be set by plugin configuration before a run. // Properties below this line are expected to be set during a run as these values will come // from AppCenter during execution they should be nullable prior to being set. + public Builder setUploadId(@Nonnull String uploadId) { + this.uploadId = uploadId; + return this; + } - public Builder setUploadUrl(@Nonnull String uploadUrl) { - this.uploadUrl = uploadUrl; + public Builder setUploadDomain(@Nonnull String uploadDomain) { + this.uploadDomain = uploadDomain; return this; } - public Builder setUploadId(@Nonnull String uploadId) { - this.uploadId = uploadId; + public Builder setToken(@Nonnull String token) { + this.token = token; + return this; + } + + public Builder setPackageAssetId(@Nonnull String packageAssetId) { + this.packageAssetId = packageAssetId; + return this; + } + + public Builder setChunkSize(@Nonnull Integer chunkSize) { + this.chunkSize = chunkSize; return this; } From 48126e9547bf684e2ce7d40a9946ad916ec6ab74 Mon Sep 17 00:00:00 2001 From: Mez Pahlan Date: Tue, 8 Dec 2020 16:53:31 +0000 Subject: [PATCH 03/24] [JENKINS-64388] Add SetMetadataTask --- .../appcenter/api/AppCenterService.java | 3 + .../model/appcenter/SetMetadataResponse.java | 33 ++++++ .../plugins/appcenter/task/UploadTask.java | 17 ++- .../task/internal/SetMetadataTask.java | 111 ++++++++++++++++++ 4 files changed, 158 insertions(+), 6 deletions(-) create mode 100644 src/main/java/io/jenkins/plugins/appcenter/model/appcenter/SetMetadataResponse.java create mode 100644 src/main/java/io/jenkins/plugins/appcenter/task/internal/SetMetadataTask.java diff --git a/src/main/java/io/jenkins/plugins/appcenter/api/AppCenterService.java b/src/main/java/io/jenkins/plugins/appcenter/api/AppCenterService.java index ce0ddb5..0b9f216 100644 --- a/src/main/java/io/jenkins/plugins/appcenter/api/AppCenterService.java +++ b/src/main/java/io/jenkins/plugins/appcenter/api/AppCenterService.java @@ -14,6 +14,9 @@ CompletableFuture releaseUploadsCreate( @Path("app_name") @Nonnull String appName, @Body @Nonnull ReleaseUploadBeginRequest releaseUploadBeginRequest); + @POST + CompletableFuture setMetaData(@Url @Nonnull String url); + @PATCH("v0.1/apps/{owner_name}/{app_name}/release_uploads/{upload_id}") CompletableFuture releaseUploadsComplete( @Path("owner_name") @Nonnull String user, diff --git a/src/main/java/io/jenkins/plugins/appcenter/model/appcenter/SetMetadataResponse.java b/src/main/java/io/jenkins/plugins/appcenter/model/appcenter/SetMetadataResponse.java new file mode 100644 index 0000000..3130da3 --- /dev/null +++ b/src/main/java/io/jenkins/plugins/appcenter/model/appcenter/SetMetadataResponse.java @@ -0,0 +1,33 @@ +package io.jenkins.plugins.appcenter.model.appcenter; + +import javax.annotation.Nonnull; +import java.util.Objects; + +public final class SetMetadataResponse { + @Nonnull + public final Integer chunk_size; + + public SetMetadataResponse(@Nonnull Integer chunkSize) { + this.chunk_size = chunkSize; + } + + @Override + public String toString() { + return "SetMetadataResponse{" + + "chunk_size=" + chunk_size + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + SetMetadataResponse that = (SetMetadataResponse) o; + return chunk_size.equals(that.chunk_size); + } + + @Override + public int hashCode() { + return Objects.hash(chunk_size); + } +} \ No newline at end of file diff --git a/src/main/java/io/jenkins/plugins/appcenter/task/UploadTask.java b/src/main/java/io/jenkins/plugins/appcenter/task/UploadTask.java index 0a879b6..58f72cf 100644 --- a/src/main/java/io/jenkins/plugins/appcenter/task/UploadTask.java +++ b/src/main/java/io/jenkins/plugins/appcenter/task/UploadTask.java @@ -1,11 +1,7 @@ package io.jenkins.plugins.appcenter.task; import io.jenkins.plugins.appcenter.AppCenterException; -import io.jenkins.plugins.appcenter.task.internal.CommitUploadResourceTask; -import io.jenkins.plugins.appcenter.task.internal.CreateUploadResourceTask; -import io.jenkins.plugins.appcenter.task.internal.DistributeResourceTask; -import io.jenkins.plugins.appcenter.task.internal.PrerequisitesTask; -import io.jenkins.plugins.appcenter.task.internal.UploadAppToResourceTask; +import io.jenkins.plugins.appcenter.task.internal.*; import io.jenkins.plugins.appcenter.task.request.UploadRequest; import jenkins.security.MasterToSlaveCallable; @@ -18,15 +14,23 @@ public final class UploadTask extends MasterToSlaveCallable, AppCenterLogger { + + private static final long serialVersionUID = 1L; + + @Nonnull + private final TaskListener taskListener; + @Nonnull + private final AppCenterServiceFactory factory; + + @Inject + SetMetadataTask(@Nonnull final TaskListener taskListener, + @Nonnull final AppCenterServiceFactory factory) { + this.taskListener = taskListener; + this.factory = factory; + } + + @Nonnull + @Override + public CompletableFuture execute(@Nonnull UploadRequest request) { + return setMetadata(request); + } + + @Nonnull + private CompletableFuture setMetadata(@Nonnull UploadRequest request) { + log("Setting metadata."); + + final CompletableFuture future = new CompletableFuture<>(); + + final String url = getUrl(request); + + factory.createAppCenterService() + .setMetaData(url) + .whenComplete((setMetadataResponse, throwable) -> { + if (throwable != null) { + final AppCenterException exception = logFailure("Setting metadata unsuccessful", throwable); + future.completeExceptionally(exception); + } else { + log("Setting metadata successful."); + + final UploadRequest uploadRequest = request.newBuilder() + .setChunkSize(setMetadataResponse.chunk_size) + .build(); + future.complete(uploadRequest); + } + }); + + return future; + } + + @Nonnull + private String getUrl(@Nonnull UploadRequest request) { + final File file = new File(request.pathToApp); + final String fileName = getFileName(file); + final long fileSize = getFileSize(file); + final String contentType = getContentType(request.pathToApp); + + 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", request.uploadDomain, request.packageAssetId, fileName, fileSize, request.token, contentType); + } + + @Nonnull + private String getFileName(@Nonnull File file) { + // TODO: Move to Prerequisite Task + return file.getName(); + } + + private long getFileSize(@Nonnull File file) { + // TODO: Move to Prerequisite Task + return file.length(); + } + + @Nonnull + private String getContentType(@Nonnull String pathToApp) { + // TODO: Move to Prerequisite Task + if (pathToApp.endsWith(".apk") || pathToApp.endsWith(".aab")) return "application/vnd.android.package-archive"; + if (pathToApp.endsWith(".msi")) return "application/x-msi"; + if (pathToApp.endsWith(".plist")) return "application/xml"; + if (pathToApp.endsWith(".aetx")) return "application/c-x509-ca-cert"; + if (pathToApp.endsWith(".cer")) return "application/pkix-cert"; + if (pathToApp.endsWith("xap")) return "application/x-silverlight-app"; + if (pathToApp.endsWith(".appx")) return "application/x-appx"; + if (pathToApp.endsWith(".appxbundle")) return "application/x-appxbundle"; + if (pathToApp.endsWith(".appxupload") || pathToApp.endsWith(".appxsym")) return "application/x-appxupload"; + if (pathToApp.endsWith(".msix")) return "application/x-msix"; + if (pathToApp.endsWith(".msixbundle")) return "application/x-msixbundle"; + if (pathToApp.endsWith(".msixupload") || pathToApp.endsWith(".msixsym")) return "application/x-msixupload"; + + // Otherwise + return "application/octet-stream"; + } + + @Override + public PrintStream getLogger() { + return taskListener.getLogger(); + } +} \ No newline at end of file From c499d64627fd9ae00a5a27a73bbd7b7c7a2e9e77 Mon Sep 17 00:00:00 2001 From: Mez Pahlan Date: Wed, 9 Dec 2020 17:15:48 +0000 Subject: [PATCH 04/24] [JENKINS-64388] Add new service definitions for AppCenter --- .../appcenter/api/AppCenterService.java | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/main/java/io/jenkins/plugins/appcenter/api/AppCenterService.java b/src/main/java/io/jenkins/plugins/appcenter/api/AppCenterService.java index 0b9f216..42b5d2b 100644 --- a/src/main/java/io/jenkins/plugins/appcenter/api/AppCenterService.java +++ b/src/main/java/io/jenkins/plugins/appcenter/api/AppCenterService.java @@ -1,6 +1,8 @@ package io.jenkins.plugins.appcenter.api; import io.jenkins.plugins.appcenter.model.appcenter.*; +import okhttp3.RequestBody; +import okhttp3.ResponseBody; import retrofit2.http.*; import javax.annotation.Nonnull; @@ -17,6 +19,26 @@ CompletableFuture releaseUploadsCreate( @POST CompletableFuture setMetaData(@Url @Nonnull String url); + @Headers("Content-Type: application/octet-stream") + @POST + CompletableFuture uploadApp(@Url @Nonnull String url, @Body @Nonnull RequestBody file); + + @POST + CompletableFuture finishRelease(@Url @Nonnull String url); + + @PATCH("v0.1/apps/{owner_name}/{app_name}/uploads/releases/{upload_id}") + CompletableFuture updateReleaseUpload( + @Path("owner_name") @Nonnull String user, + @Path("app_name") @Nonnull String appName, + @Path("upload_id") @Nonnull String uploadId, + @Body @Nonnull UpdateReleaseUploadRequest updateReleaseUploadRequest); + + @GET("v0.1/apps/{owner_name}/{app_name}/uploads/releases/{upload_id}") + CompletableFuture pollForRelease( + @Path("owner_name") @Nonnull String user, + @Path("app_name") @Nonnull String appName, + @Path("upload_id") @Nonnull String uploadId); + @PATCH("v0.1/apps/{owner_name}/{app_name}/release_uploads/{upload_id}") CompletableFuture releaseUploadsComplete( @Path("owner_name") @Nonnull String user, From 8496e824129b63fa9ffc8a31f85892b59a22967e Mon Sep 17 00:00:00 2001 From: Mez Pahlan Date: Wed, 9 Dec 2020 17:16:21 +0000 Subject: [PATCH 05/24] [JENKINS-64388] Disable tests temporarily --- .../CreateUploadResourceTaskTest.java | 128 +++++++++--------- .../internal/UploadAppToResourceTaskTest.java | 4 +- 2 files changed, 69 insertions(+), 63 deletions(-) diff --git a/src/test/java/io/jenkins/plugins/appcenter/task/internal/CreateUploadResourceTaskTest.java b/src/test/java/io/jenkins/plugins/appcenter/task/internal/CreateUploadResourceTaskTest.java index 6183916..caf4870 100644 --- a/src/test/java/io/jenkins/plugins/appcenter/task/internal/CreateUploadResourceTaskTest.java +++ b/src/test/java/io/jenkins/plugins/appcenter/task/internal/CreateUploadResourceTaskTest.java @@ -5,11 +5,11 @@ import hudson.util.Secret; import io.jenkins.plugins.appcenter.AppCenterException; import io.jenkins.plugins.appcenter.api.AppCenterServiceFactory; -import io.jenkins.plugins.appcenter.model.appcenter.SymbolUploadBeginRequest; import io.jenkins.plugins.appcenter.task.request.UploadRequest; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; import org.junit.Before; +import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.function.ThrowingRunnable; @@ -25,6 +25,7 @@ import static org.mockito.BDDMockito.given; @RunWith(MockitoJUnitRunner.class) +@Ignore public class CreateUploadResourceTaskTest { @Rule @@ -56,75 +57,78 @@ public void setUp() { @Test public void should_ReturnResponse_When_RequestIsSuccessful() throws Exception { - // Given - final UploadRequest expected = baseRequest.newBuilder().setUploadId("string").setUploadUrl("string").build(); - mockWebServer.enqueue(new MockResponse().setResponseCode(201).setBody("{\n" + - " \"upload_id\": \"string\",\n" + - " \"upload_url\": \"string\",\n" + - " \"asset_id\": \"string\",\n" + - " \"asset_domain\": \"string\",\n" + - " \"asset_token\": \"string\"\n" + - "}")); - - // When - final UploadRequest actual = task.execute(baseRequest).get(); - - // Then - assertThat(actual) - .isEqualTo(expected); + // TODO: Fix Me +// // Given +// final UploadRequest expected = baseRequest.newBuilder().setUploadId("string").setUploadUrl("string").build(); +// mockWebServer.enqueue(new MockResponse().setResponseCode(201).setBody("{\n" + +// " \"upload_id\": \"string\",\n" + +// " \"upload_url\": \"string\",\n" + +// " \"asset_id\": \"string\",\n" + +// " \"asset_domain\": \"string\",\n" + +// " \"asset_token\": \"string\"\n" + +// "}")); +// +// // When +// final UploadRequest actual = task.execute(baseRequest).get(); +// +// // Then +// assertThat(actual) +// .isEqualTo(expected); } @Test public void should_ReturnResponse_When_DebugSymbolsAreFound() throws Exception { - // Given - final UploadRequest request = baseRequest.newBuilder() - .setPathToDebugSymbols("path/to/mappings.txt") - .setSymbolUploadRequest(new SymbolUploadBeginRequest(SymbolUploadBeginRequest.SymbolTypeEnum.AndroidProguard, null, "mappings.txt", "1", "1.0.0")) - .build(); - final UploadRequest expected = request.newBuilder() - .setUploadId("string").setUploadUrl("string") - .setSymbolUploadId("string").setSymbolUploadUrl("string") - .build(); - mockWebServer.enqueue(new MockResponse().setResponseCode(201).setBody("{\n" + - " \"upload_id\": \"string\",\n" + - " \"upload_url\": \"string\",\n" + - " \"asset_id\": \"string\",\n" + - " \"asset_domain\": \"string\",\n" + - " \"asset_token\": \"string\"\n" + - "}")); - mockWebServer.enqueue(new MockResponse().setResponseCode(200).setBody("{\n" + - " \"symbol_upload_id\": \"string\",\n" + - " \"upload_url\": \"string\",\n" + - " \"expiration_date\": \"2019-11-17T12:01:43.953Z\"\n" + - "}")); - - // When - final UploadRequest actual = task.execute(request).get(); - - // Then - assertThat(actual) - .isEqualTo(expected); + // TODO: Fix Me +// // Given +// final UploadRequest request = baseRequest.newBuilder() +// .setPathToDebugSymbols("path/to/mappings.txt") +// .setSymbolUploadRequest(new SymbolUploadBeginRequest(SymbolUploadBeginRequest.SymbolTypeEnum.AndroidProguard, null, "mappings.txt", "1", "1.0.0")) +// .build(); +// final UploadRequest expected = request.newBuilder() +// .setUploadId("string").setUploadUrl("string") +// .setSymbolUploadId("string").setSymbolUploadUrl("string") +// .build(); +// mockWebServer.enqueue(new MockResponse().setResponseCode(201).setBody("{\n" + +// " \"upload_id\": \"string\",\n" + +// " \"upload_url\": \"string\",\n" + +// " \"asset_id\": \"string\",\n" + +// " \"asset_domain\": \"string\",\n" + +// " \"asset_token\": \"string\"\n" + +// "}")); +// mockWebServer.enqueue(new MockResponse().setResponseCode(200).setBody("{\n" + +// " \"symbol_upload_id\": \"string\",\n" + +// " \"upload_url\": \"string\",\n" + +// " \"expiration_date\": \"2019-11-17T12:01:43.953Z\"\n" + +// "}")); +// +// // When +// final UploadRequest actual = task.execute(request).get(); +// +// // Then +// assertThat(actual) +// .isEqualTo(expected); } @Test public void should_ReturnResponse_When_RequestIsSuccessful_NonAsciiCharactersInFileName() throws Exception { - // Given - final UploadRequest request = baseRequest.newBuilder().setAppName("åþþ ñåmë").build(); - final UploadRequest expected = request.newBuilder().setUploadId("string").setUploadUrl("string").build(); - mockWebServer.enqueue(new MockResponse().setResponseCode(201).setBody("{\n" + - " \"upload_id\": \"string\",\n" + - " \"upload_url\": \"string\",\n" + - " \"asset_id\": \"string\",\n" + - " \"asset_domain\": \"string\",\n" + - " \"asset_token\": \"string\"\n" + - "}")); - - // When - final UploadRequest actual = task.execute(request).get(); - - // Then - assertThat(actual) - .isEqualTo(expected); + // TODO: Fix Me +// // Given +// final UploadRequest request = baseRequest.newBuilder().setAppName("åþþ ñåmë").build(); +// final UploadRequest expected = request.newBuilder().setUploadId("string").setUploadUrl("string").build(); +// mockWebServer.enqueue(new MockResponse().setResponseCode(201).setBody("{\n" + +// " \"upload_id\": \"string\",\n" + +// " \"upload_url\": \"string\",\n" + +// " \"asset_id\": \"string\",\n" + +// " \"asset_domain\": \"string\",\n" + +// " \"asset_token\": \"string\"\n" + +// "}")); +// +// // When +// final UploadRequest actual = task.execute(request).get(); +// +// // Then +// assertThat(actual) +// .isEqualTo(expected); } @Test diff --git a/src/test/java/io/jenkins/plugins/appcenter/task/internal/UploadAppToResourceTaskTest.java b/src/test/java/io/jenkins/plugins/appcenter/task/internal/UploadAppToResourceTaskTest.java index fc0eb72..da93883 100644 --- a/src/test/java/io/jenkins/plugins/appcenter/task/internal/UploadAppToResourceTaskTest.java +++ b/src/test/java/io/jenkins/plugins/appcenter/task/internal/UploadAppToResourceTaskTest.java @@ -13,6 +13,7 @@ import okhttp3.mockwebserver.MockWebServer; import okhttp3.mockwebserver.RecordedRequest; import org.junit.Before; +import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.function.ThrowingRunnable; @@ -29,6 +30,7 @@ import static org.mockito.BDDMockito.given; @RunWith(MockitoJUnitRunner.class) +@Ignore public class UploadAppToResourceTaskTest { @Rule @@ -53,7 +55,7 @@ public class UploadAppToResourceTaskTest { @Before public void setUp() { baseRequest = new UploadRequest.Builder() - .setUploadUrl(mockWebServer.url("upload").toString()) +// .setUploadUrl(mockWebServer.url("upload").toString()) // TODO: Fix Me .setUploadId("upload-id") .setPathToApp("three/days/xiola.apk") .build(); From f6e5a0876712d1aa2141bd476707567266d4660a Mon Sep 17 00:00:00 2001 From: Mez Pahlan Date: Wed, 9 Dec 2020 17:16:51 +0000 Subject: [PATCH 06/24] [JENKINS-64388] Add additional API models --- .../appcenter/PollForReleaseResponse.java | 63 +++++++++++++++++++ .../appcenter/UpdateReleaseUploadRequest.java | 38 +++++++++++ .../UpdateReleaseUploadResponse.java | 48 ++++++++++++++ 3 files changed, 149 insertions(+) create mode 100644 src/main/java/io/jenkins/plugins/appcenter/model/appcenter/PollForReleaseResponse.java create mode 100644 src/main/java/io/jenkins/plugins/appcenter/model/appcenter/UpdateReleaseUploadRequest.java create mode 100644 src/main/java/io/jenkins/plugins/appcenter/model/appcenter/UpdateReleaseUploadResponse.java diff --git a/src/main/java/io/jenkins/plugins/appcenter/model/appcenter/PollForReleaseResponse.java b/src/main/java/io/jenkins/plugins/appcenter/model/appcenter/PollForReleaseResponse.java new file mode 100644 index 0000000..5a91be2 --- /dev/null +++ b/src/main/java/io/jenkins/plugins/appcenter/model/appcenter/PollForReleaseResponse.java @@ -0,0 +1,63 @@ +package io.jenkins.plugins.appcenter.model.appcenter; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.Objects; + +public final class PollForReleaseResponse { + @Nonnull + public final String id; + @Nonnull + public final StatusEnum upload_status; + @Nullable + public final String error_details; + @Nullable + public final Integer release_distinct_id; + @Nullable + public final String release_url; + + public PollForReleaseResponse(@Nonnull String id, + @Nonnull StatusEnum upload_status, + @Nullable String error_details, + @Nullable Integer release_distinct_id, + @Nullable String release_url) { + + this.id = id; + this.upload_status = upload_status; + this.error_details = error_details; + this.release_distinct_id = release_distinct_id; + this.release_url = release_url; + } + + @Override + public String toString() { + return "PollForReleaseResponse{" + + "id='" + id + '\'' + + ", upload_status=" + upload_status + + ", error_details='" + error_details + '\'' + + ", release_distinct_id=" + release_distinct_id + + ", release_url='" + release_url + '\'' + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + PollForReleaseResponse that = (PollForReleaseResponse) o; + 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); + } + + @Override + public int hashCode() { + return Objects.hash(id, upload_status, error_details, release_distinct_id, release_url); + } + + public enum StatusEnum { + uploadStarted, + uploadFinished, + readyToBePublished, + malwareDetected, + error + } +} \ No newline at end of file diff --git a/src/main/java/io/jenkins/plugins/appcenter/model/appcenter/UpdateReleaseUploadRequest.java b/src/main/java/io/jenkins/plugins/appcenter/model/appcenter/UpdateReleaseUploadRequest.java new file mode 100644 index 0000000..5a2a56d --- /dev/null +++ b/src/main/java/io/jenkins/plugins/appcenter/model/appcenter/UpdateReleaseUploadRequest.java @@ -0,0 +1,38 @@ +package io.jenkins.plugins.appcenter.model.appcenter; + +import javax.annotation.Nonnull; +import java.util.Objects; + +public final class UpdateReleaseUploadRequest { + @Nonnull + public final StatusEnum upload_status; + + public UpdateReleaseUploadRequest(@Nonnull StatusEnum upload_status) { + this.upload_status = upload_status; + } + + @Override + public String toString() { + return "UpdateReleaseUploadRequest{" + + "upload_status=" + upload_status + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + UpdateReleaseUploadRequest that = (UpdateReleaseUploadRequest) o; + return upload_status == that.upload_status; + } + + @Override + public int hashCode() { + return Objects.hash(upload_status); + } + + public enum StatusEnum { + uploadFinished, + uploadCanceled + } +} \ No newline at end of file diff --git a/src/main/java/io/jenkins/plugins/appcenter/model/appcenter/UpdateReleaseUploadResponse.java b/src/main/java/io/jenkins/plugins/appcenter/model/appcenter/UpdateReleaseUploadResponse.java new file mode 100644 index 0000000..1f5de58 --- /dev/null +++ b/src/main/java/io/jenkins/plugins/appcenter/model/appcenter/UpdateReleaseUploadResponse.java @@ -0,0 +1,48 @@ +package io.jenkins.plugins.appcenter.model.appcenter; + +import javax.annotation.Nonnull; +import java.util.Objects; + +public final class UpdateReleaseUploadResponse { + @Nonnull + public final String id; + @Nonnull + public final StatusEnum upload_status; + + public UpdateReleaseUploadResponse(@Nonnull String id, + @Nonnull StatusEnum upload_status) { + + this.id = id; + this.upload_status = upload_status; + } + + @Override + public String toString() { + return "UpdateReleaseUploadResponse{" + + "id='" + id + '\'' + + ", upload_status=" + upload_status + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + UpdateReleaseUploadResponse that = (UpdateReleaseUploadResponse) o; + return id.equals(that.id) && upload_status == that.upload_status; + } + + @Override + public int hashCode() { + return Objects.hash(id, upload_status); + } + + public enum StatusEnum { + uploadStarted, + uploadFinished, + uploadCanceled, + readyToBePublished, + malwareDetected, + error + } +} \ No newline at end of file From 3ca084873f886c27d9c8a65246022f985a7249dc Mon Sep 17 00:00:00 2001 From: Mez Pahlan Date: Wed, 9 Dec 2020 17:17:22 +0000 Subject: [PATCH 07/24] [JENKINS-64388] Add additional tasks --- .../task/internal/FinishReleaseTask.java | 70 +++++++++++++ .../task/internal/PollForReleaseTask.java | 99 +++++++++++++++++++ .../task/internal/UpdateReleaseTask.java | 70 +++++++++++++ 3 files changed, 239 insertions(+) create mode 100644 src/main/java/io/jenkins/plugins/appcenter/task/internal/FinishReleaseTask.java create mode 100644 src/main/java/io/jenkins/plugins/appcenter/task/internal/PollForReleaseTask.java create mode 100644 src/main/java/io/jenkins/plugins/appcenter/task/internal/UpdateReleaseTask.java diff --git a/src/main/java/io/jenkins/plugins/appcenter/task/internal/FinishReleaseTask.java b/src/main/java/io/jenkins/plugins/appcenter/task/internal/FinishReleaseTask.java new file mode 100644 index 0000000..02024a6 --- /dev/null +++ b/src/main/java/io/jenkins/plugins/appcenter/task/internal/FinishReleaseTask.java @@ -0,0 +1,70 @@ +package io.jenkins.plugins.appcenter.task.internal; + +import hudson.model.TaskListener; +import io.jenkins.plugins.appcenter.AppCenterException; +import io.jenkins.plugins.appcenter.AppCenterLogger; +import io.jenkins.plugins.appcenter.api.AppCenterServiceFactory; +import io.jenkins.plugins.appcenter.task.request.UploadRequest; + +import javax.annotation.Nonnull; +import javax.inject.Inject; +import javax.inject.Singleton; +import java.io.PrintStream; +import java.util.concurrent.CompletableFuture; + +@Singleton +public final class FinishReleaseTask implements AppCenterTask, AppCenterLogger { + + private static final long serialVersionUID = 1L; + + @Nonnull + private final TaskListener taskListener; + @Nonnull + private final AppCenterServiceFactory factory; + + @Inject + FinishReleaseTask(@Nonnull final TaskListener taskListener, + @Nonnull final AppCenterServiceFactory factory) { + this.taskListener = taskListener; + this.factory = factory; + } + + @Nonnull + @Override + public CompletableFuture execute(@Nonnull UploadRequest request) { + return finishRelease(request); + } + + @Nonnull + private CompletableFuture finishRelease(@Nonnull UploadRequest request) { + log("Finishing release."); + + final CompletableFuture future = new CompletableFuture<>(); + + final String url = getUrl(request); + + factory.createAppCenterService() + .finishRelease(url) + .whenComplete((finishReleaseResponse, throwable) -> { + if (throwable != null) { + final AppCenterException exception = logFailure("Finishing release unsuccessful", throwable); + future.completeExceptionally(exception); + } else { + log("Finishing release successful."); + future.complete(request); + } + }); + + return future; + } + + @Nonnull + private String getUrl(@Nonnull UploadRequest request) { + return String.format("%1$s/upload/finished/%2$s?token=%3$s", request.uploadDomain, request.packageAssetId, request.token); + } + + @Override + public PrintStream getLogger() { + return taskListener.getLogger(); + } +} \ No newline at end of file diff --git a/src/main/java/io/jenkins/plugins/appcenter/task/internal/PollForReleaseTask.java b/src/main/java/io/jenkins/plugins/appcenter/task/internal/PollForReleaseTask.java new file mode 100644 index 0000000..9e344c0 --- /dev/null +++ b/src/main/java/io/jenkins/plugins/appcenter/task/internal/PollForReleaseTask.java @@ -0,0 +1,99 @@ +package io.jenkins.plugins.appcenter.task.internal; + +import hudson.model.TaskListener; +import io.jenkins.plugins.appcenter.AppCenterException; +import io.jenkins.plugins.appcenter.AppCenterLogger; +import io.jenkins.plugins.appcenter.api.AppCenterServiceFactory; +import io.jenkins.plugins.appcenter.task.request.UploadRequest; + +import javax.annotation.Nonnull; +import javax.inject.Inject; +import javax.inject.Singleton; +import java.io.PrintStream; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +import static java.util.Objects.requireNonNull; + +@Singleton +public final class PollForReleaseTask implements AppCenterTask, AppCenterLogger { + + private static final long serialVersionUID = 1L; + + @Nonnull + private final TaskListener taskListener; + @Nonnull + private final AppCenterServiceFactory factory; + + @Inject + PollForReleaseTask(@Nonnull final TaskListener taskListener, + @Nonnull final AppCenterServiceFactory factory) { + this.taskListener = taskListener; + this.factory = factory; + } + + @Nonnull + @Override + public CompletableFuture execute(@Nonnull UploadRequest request) { + return pollForRelease(request); + } + + @Nonnull + private CompletableFuture pollForRelease(@Nonnull UploadRequest request) { + final String uploadId = requireNonNull(request.uploadId, "uploadId cannot be null"); + + log("Polling for app release."); + + final CompletableFuture future = new CompletableFuture<>(); + + poll(request, uploadId, future); + + return future; + } + + private void poll(@Nonnull UploadRequest request, @Nonnull String uploadId, @Nonnull CompletableFuture future) { + factory.createAppCenterService() + .pollForRelease(request.ownerName, request.appName, uploadId) + .whenComplete((pollForReleaseResponse, throwable) -> { + if (throwable != null) { + final AppCenterException exception = logFailure("Polling for app release unsuccessful", throwable); + future.completeExceptionally(exception); + } else { + switch (pollForReleaseResponse.upload_status) { + case uploadStarted: + case uploadFinished: + log("Polling for app release successful however not yet ready will try again."); + retryPolling(request, uploadId, future); + break; + case readyToBePublished: + log("Polling for app release successful."); + final UploadRequest uploadRequest = request.newBuilder() + .setReleaseId(pollForReleaseResponse.release_distinct_id) + .build(); + future.complete(uploadRequest); + break; + case malwareDetected: + case error: + future.completeExceptionally(logFailure("Polling for app release successful however was rejected by server: " + pollForReleaseResponse.error_details)); + break; + default: + future.completeExceptionally(logFailure("Polling for app release successful however unexpected enum returned from server: " + pollForReleaseResponse.upload_status)); + } + } + }); + } + + private void retryPolling(@Nonnull UploadRequest request, @Nonnull String uploadId, @Nonnull CompletableFuture future) { + try { + TimeUnit.SECONDS.sleep(1L); + poll(request, uploadId, future); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + @Override + public PrintStream getLogger() { + return taskListener.getLogger(); + } +} \ No newline at end of file diff --git a/src/main/java/io/jenkins/plugins/appcenter/task/internal/UpdateReleaseTask.java b/src/main/java/io/jenkins/plugins/appcenter/task/internal/UpdateReleaseTask.java new file mode 100644 index 0000000..a7b3fbc --- /dev/null +++ b/src/main/java/io/jenkins/plugins/appcenter/task/internal/UpdateReleaseTask.java @@ -0,0 +1,70 @@ +package io.jenkins.plugins.appcenter.task.internal; + +import hudson.model.TaskListener; +import io.jenkins.plugins.appcenter.AppCenterException; +import io.jenkins.plugins.appcenter.AppCenterLogger; +import io.jenkins.plugins.appcenter.api.AppCenterServiceFactory; +import io.jenkins.plugins.appcenter.model.appcenter.UpdateReleaseUploadRequest; +import io.jenkins.plugins.appcenter.task.request.UploadRequest; + +import javax.annotation.Nonnull; +import javax.inject.Inject; +import javax.inject.Singleton; +import java.io.PrintStream; +import java.util.concurrent.CompletableFuture; + +import static java.util.Objects.requireNonNull; + +@Singleton +public final class UpdateReleaseTask implements AppCenterTask, AppCenterLogger { + + private static final long serialVersionUID = 1L; + + @Nonnull + private final TaskListener taskListener; + @Nonnull + private final AppCenterServiceFactory factory; + + @Inject + UpdateReleaseTask(@Nonnull final TaskListener taskListener, + @Nonnull final AppCenterServiceFactory factory) { + this.taskListener = taskListener; + this.factory = factory; + } + + @Nonnull + @Override + public CompletableFuture execute(@Nonnull UploadRequest request) { + return updateRelease(request); + } + + @Nonnull + private CompletableFuture updateRelease(@Nonnull UploadRequest request) { + final String uploadId = requireNonNull(request.uploadId, "uploadId cannot be null"); + + log("Updating release."); + + final CompletableFuture future = new CompletableFuture<>(); + + final UpdateReleaseUploadRequest updateReleaseUploadRequest = new UpdateReleaseUploadRequest(UpdateReleaseUploadRequest.StatusEnum.uploadFinished); + + factory.createAppCenterService() + .updateReleaseUpload(request.ownerName, request.appName, uploadId, updateReleaseUploadRequest) + .whenComplete((updateReleaseUploadResponse, throwable) -> { + if (throwable != null) { + final AppCenterException exception = logFailure("Updating release unsuccessful", throwable); + future.completeExceptionally(exception); + } else { + log("Updating release release successful."); + future.complete(request); + } + }); + + return future; + } + + @Override + public PrintStream getLogger() { + return taskListener.getLogger(); + } +} \ No newline at end of file From bfeb376da722a53dddee6da66e44027f8f7c166e Mon Sep 17 00:00:00 2001 From: Mez Pahlan Date: Wed, 9 Dec 2020 17:19:02 +0000 Subject: [PATCH 08/24] [JENKINS-64388] Add new internal tasks to public task --- .../plugins/appcenter/task/UploadTask.java | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/main/java/io/jenkins/plugins/appcenter/task/UploadTask.java b/src/main/java/io/jenkins/plugins/appcenter/task/UploadTask.java index 58f72cf..b330f3f 100644 --- a/src/main/java/io/jenkins/plugins/appcenter/task/UploadTask.java +++ b/src/main/java/io/jenkins/plugins/appcenter/task/UploadTask.java @@ -16,7 +16,9 @@ public final class UploadTask extends MasterToSlaveCallable { if (throwable != null) { From d704ac9dcdb3fd3d0f7ba84165182443ff8a9920 Mon Sep 17 00:00:00 2001 From: Mez Pahlan Date: Wed, 9 Dec 2020 17:19:36 +0000 Subject: [PATCH 09/24] [JENKINS-64388] Add chunked support for app uploads --- .../internal/UploadAppToResourceTask.java | 68 +++++++++++++++---- 1 file changed, 55 insertions(+), 13 deletions(-) diff --git a/src/main/java/io/jenkins/plugins/appcenter/task/internal/UploadAppToResourceTask.java b/src/main/java/io/jenkins/plugins/appcenter/task/internal/UploadAppToResourceTask.java index 4c32f36..b0172f1 100644 --- a/src/main/java/io/jenkins/plugins/appcenter/task/internal/UploadAppToResourceTask.java +++ b/src/main/java/io/jenkins/plugins/appcenter/task/internal/UploadAppToResourceTask.java @@ -6,14 +6,14 @@ import io.jenkins.plugins.appcenter.api.AppCenterServiceFactory; import io.jenkins.plugins.appcenter.task.request.UploadRequest; import io.jenkins.plugins.appcenter.util.RemoteFileUtils; -import okhttp3.MediaType; -import okhttp3.MultipartBody; import okhttp3.RequestBody; +import org.apache.commons.io.FileUtils; import javax.annotation.Nonnull; import javax.inject.Inject; import javax.inject.Singleton; import java.io.File; +import java.io.IOException; import java.io.PrintStream; import java.util.concurrent.CompletableFuture; @@ -54,30 +54,72 @@ public CompletableFuture execute(@Nonnull UploadRequest request) @Nonnull private CompletableFuture uploadApp(@Nonnull UploadRequest request) { - final String pathToApp = request.pathToApp; - final String uploadUrl = requireNonNull(request.uploadUrl, "uploadUrl cannot be null"); + requireNonNull(request.uploadDomain, "uploadDomain cannot be null"); + requireNonNull(request.packageAssetId, "packageAssetId cannot be null"); + requireNonNull(request.token, "token cannot be null"); + final Integer chunkSize = requireNonNull(request.chunkSize, "chunkSize cannot be null"); log("Uploading app to resource."); final CompletableFuture future = new CompletableFuture<>(); - final File file = remoteFileUtils.getRemoteFile(pathToApp); - final RequestBody requestFile = RequestBody.create(MediaType.parse("multipart/form-data"), file); - final MultipartBody.Part body = MultipartBody.Part.createFormData("ipa", file.getName(), requestFile); + int offset = 0; + int blockNumber = 1; + calculateChunks(request, offset, chunkSize, blockNumber, future); - factory.createUploadService(uploadUrl) - .uploadApp(uploadUrl, body) + return future; + } + + private void calculateChunks(@Nonnull UploadRequest request, int offset, int chunkSize, int blockNumber, @Nonnull CompletableFuture future) { + // TODO: Retrofit (via OkHttp) is supposed to be able to do this natively if you set the contentLength to -1. Investigate + final String url = getUrl(request); + final File file = remoteFileUtils.getRemoteFile(request.pathToApp); + + try { + final byte[] bytes = FileUtils.readFileToByteArray(file); + final int size = bytes.length; + final int remainingSize = size - offset; + final boolean canChunk = remainingSize > chunkSize; + + int byteCount; + if (canChunk) { + // We can safely take a whole chunk from what remains given the offset. + byteCount = chunkSize; + } else { + // We can only safely take what remains and not the full chunk given the offset. + byteCount = remainingSize; + } + final RequestBody requestFile = RequestBody.create(bytes, null, offset, byteCount); + upload(request, offset, chunkSize, url, requestFile, blockNumber, future, !canChunk); + } catch (IOException e) { + final AppCenterException exception = logFailure("Upload app to resource unsuccessful", e); + future.completeExceptionally(exception); + } + } + + private void upload(@Nonnull UploadRequest request, int offset, int chunkSize, @Nonnull String url, @Nonnull RequestBody requestFile, int blockNumber, @Nonnull CompletableFuture future, boolean isFinal) { + factory.createAppCenterService() + .uploadApp(url + "&block_number=" + blockNumber, requestFile) .whenComplete((responseBody, throwable) -> { if (throwable != null) { final AppCenterException exception = logFailure("Upload app to resource unsuccessful", throwable); future.completeExceptionally(exception); } else { - log("Upload app to resource successful."); - future.complete(request); + if (isFinal) { + log(String.format("Upload app to resource chunk %1$d and final successful.", blockNumber)); + future.complete(request); + return; + } + + log(String.format("Upload app to resource chunk %1$d successful.", blockNumber)); + calculateChunks(request, offset + chunkSize, chunkSize, blockNumber + 1, future); } }); + } - return future; + @Nonnull + private String getUrl(@Nonnull UploadRequest request) { + return String.format("%1$s/upload/upload_chunk/%2$s?token=%3$s", request.uploadDomain, request.packageAssetId, request.token); } @Nonnull @@ -118,7 +160,7 @@ private CompletableFuture uploadSymbolsComplete(@Nonnull UploadRe log("Uploading all symbols at once to resource."); final CompletableFuture future = new CompletableFuture<>(); - final RequestBody requestFile = RequestBody.create(null, file); + final RequestBody requestFile = RequestBody.create(file, null); factory.createUploadService(symbolUploadUrl) .uploadSymbols(symbolUploadUrl, requestFile) From 57325b961efb4f59d7b44119b7419ce8453fdba1 Mon Sep 17 00:00:00 2001 From: Mez Pahlan Date: Wed, 9 Dec 2020 22:15:06 +0000 Subject: [PATCH 10/24] [JENKINS-64388] Fix integration tests --- .../appcenter/EnvInterpolationTest.java | 6 +- .../jenkins/plugins/appcenter/ProxyTest.java | 25 ++----- .../appcenter/util/MockWebServerUtil.java | 68 ++++++++++++++----- 3 files changed, 61 insertions(+), 38 deletions(-) diff --git a/src/test/java/io/jenkins/plugins/appcenter/EnvInterpolationTest.java b/src/test/java/io/jenkins/plugins/appcenter/EnvInterpolationTest.java index 2cea090..d6b6f9e 100644 --- a/src/test/java/io/jenkins/plugins/appcenter/EnvInterpolationTest.java +++ b/src/test/java/io/jenkins/plugins/appcenter/EnvInterpolationTest.java @@ -106,7 +106,7 @@ public void should_InterpolateEnv_InAppPath() throws Exception { mockWebServer.takeRequest(); mockWebServer.takeRequest(); final RecordedRequest recordedRequest = mockWebServer.takeRequest(); - assertThat(recordedRequest.getBody().readUtf8()).contains("filename=\"xiola.ipa\""); + assertThat(recordedRequest.getPath().contains("file_name=xiola.ipa")); } @Test @@ -140,6 +140,8 @@ public void should_InterpolateEnv_InDestinationGroups() throws Exception { mockWebServer.takeRequest(); mockWebServer.takeRequest(); mockWebServer.takeRequest(); + mockWebServer.takeRequest(); + mockWebServer.takeRequest(); final RecordedRequest recordedRequest = mockWebServer.takeRequest(); assertThat(recordedRequest.getBody().readUtf8()).contains("[{\"name\":\"casey\"},{\"name\":\"niccoli\"}]"); } @@ -160,6 +162,8 @@ public void should_InterpolateEnv_InReleaseNotes() throws Exception { mockWebServer.takeRequest(); mockWebServer.takeRequest(); mockWebServer.takeRequest(); + mockWebServer.takeRequest(); + mockWebServer.takeRequest(); final RecordedRequest recordedRequest = mockWebServer.takeRequest(); assertThat(recordedRequest.getBody().readUtf8()).contains("\"release_notes\":\"I miss you my dear Xiola\\n\\nI prepared the room tonight with Christmas lights.\""); } diff --git a/src/test/java/io/jenkins/plugins/appcenter/ProxyTest.java b/src/test/java/io/jenkins/plugins/appcenter/ProxyTest.java index eb6a320..e856979 100644 --- a/src/test/java/io/jenkins/plugins/appcenter/ProxyTest.java +++ b/src/test/java/io/jenkins/plugins/appcenter/ProxyTest.java @@ -54,7 +54,7 @@ public void should_SendRequestsDirectly_When_NoProxyConfigurationFound() throws // Then jenkinsRule.assertBuildStatus(Result.SUCCESS, freeStyleBuild); assertThat(proxyWebServer.getRequestCount()).isEqualTo(0); - assertThat(mockWebServer.getRequestCount()).isEqualTo(4); + assertThat(mockWebServer.getRequestCount()).isEqualTo(7); } @Test @@ -68,7 +68,7 @@ public void should_SendRequestsToProxy_When_ProxyConfigurationFound() throws Exc // Then jenkinsRule.assertBuildStatus(Result.SUCCESS, freeStyleBuild); - assertThat(proxyWebServer.getRequestCount()).isEqualTo(4); + assertThat(proxyWebServer.getRequestCount()).isEqualTo(7); assertThat(mockWebServer.getRequestCount()).isEqualTo(0); } @@ -88,7 +88,7 @@ public void should_SendProxyAuthorizationHeader_When_ProxyCredentialsConfigured( // Then jenkinsRule.assertBuildStatus(Result.SUCCESS, freeStyleBuild); - assertThat(proxyWebServer.getRequestCount()).isEqualTo(5); + assertThat(proxyWebServer.getRequestCount()).isEqualTo(8); assertThat(mockWebServer.getRequestCount()).isEqualTo(0); // proxy auth is performed on second request assertThat(proxyWebServer.takeRequest().getHeader(HttpHeaders.PROXY_AUTHORIZATION)).isNull(); @@ -109,23 +109,6 @@ public void should_SendAllRequestsDirectly_When_NoProxyHostConfigured() throws E // Then jenkinsRule.assertBuildStatus(Result.SUCCESS, freeStyleBuild); assertThat(proxyWebServer.getRequestCount()).isEqualTo(0); - assertThat(mockWebServer.getRequestCount()).isEqualTo(4); - } - - @Test - public void should_SendUploadRequestsDirectly_When_NoProxyHostConfiguredForAppCenterAPI() throws Exception { - // Given - final String noProxyHost = mockWebServer.url("/").url().getHost(); - jenkinsRule.jenkins.proxy = new ProxyConfiguration(proxyWebServer.getHostName(), proxyWebServer.getPort(), null, null, noProxyHost); - - MockWebServerUtil.enqueueUploadViaProxy(mockWebServer, proxyWebServer); - - // When - final FreeStyleBuild freeStyleBuild = freeStyleProject.scheduleBuild2(0).get(); - - // Then - jenkinsRule.assertBuildStatus(Result.SUCCESS, freeStyleBuild); - assertThat(proxyWebServer.getRequestCount()).isEqualTo(1); - assertThat(mockWebServer.getRequestCount()).isEqualTo(3); + assertThat(mockWebServer.getRequestCount()).isEqualTo(7); } } \ No newline at end of file diff --git a/src/test/java/io/jenkins/plugins/appcenter/util/MockWebServerUtil.java b/src/test/java/io/jenkins/plugins/appcenter/util/MockWebServerUtil.java index 98c816e..d28e734 100644 --- a/src/test/java/io/jenkins/plugins/appcenter/util/MockWebServerUtil.java +++ b/src/test/java/io/jenkins/plugins/appcenter/util/MockWebServerUtil.java @@ -5,10 +5,7 @@ import javax.annotation.Nonnull; -import static java.net.HttpURLConnection.HTTP_CREATED; -import static java.net.HttpURLConnection.HTTP_INTERNAL_ERROR; -import static java.net.HttpURLConnection.HTTP_OK; -import static java.net.HttpURLConnection.HTTP_PROXY_AUTH; +import static java.net.HttpURLConnection.*; public class MockWebServerUtil { @@ -25,18 +22,41 @@ public static void enqueueAppCenterViaProxy(final @Nonnull MockWebServer mockWeb } private static void enqueueSuccess(final @Nonnull MockWebServer mockAppCenterServer, final @Nonnull MockWebServer mockUploadServer) { + // Create upload resource for app mockAppCenterServer.enqueue(new MockResponse().setResponseCode(HTTP_CREATED).setBody("{\n" + - " \"upload_id\": \"string\",\n" + - " \"upload_url\": \"" + mockUploadServer.url("/").toString() + "\",\n" + - " \"asset_id\": \"string\",\n" + + " \"id\": \"string\",\n" + + " \"upload_domain\": \"" + mockUploadServer.url("/").toString() + "\",\n" + " \"asset_domain\": \"string\",\n" + - " \"asset_token\": \"string\"\n" + + " \"url_encoded_token\": \"string\",\n" + + " \"package_asset_id\": \"string\"\n" + + "}")); + + // Set Metadata + mockAppCenterServer.enqueue(new MockResponse().setResponseCode(HTTP_OK).setBody("{\n" + + " \"chunk_size\": 1234\n" + "}")); - mockUploadServer.enqueue(new MockResponse().setResponseCode(HTTP_OK)); + + // Upload app + mockAppCenterServer.enqueue(new MockResponse().setResponseCode(HTTP_OK)); + + // Finish Release + mockAppCenterServer.enqueue(new MockResponse().setResponseCode(HTTP_OK)); + + // Update Release mockAppCenterServer.enqueue(new MockResponse().setResponseCode(HTTP_OK).setBody("{\n" + - " \"release_id\": 0,\n" + + " \"id\": \"1234\",\n" + + " \"upload_status\": \"uploadFinished\"\n" + + "}")); + + // Poll For Release + mockAppCenterServer.enqueue(new MockResponse().setResponseCode(HTTP_OK).setBody("{\n" + + " \"id\": \"1234\",\n" + + " \"upload_status\": \"readyToBePublished\",\n" + + " \"release_distinct_id\": \"4321\",\n" + " \"release_url\": \"string\"\n" + "}")); + + // Distribute Resource mockAppCenterServer.enqueue(new MockResponse().setResponseCode(HTTP_OK).setBody("{\n" + " \"release_notes\": \"string\"\n" + "}")); @@ -45,11 +65,11 @@ private static void enqueueSuccess(final @Nonnull MockWebServer mockAppCenterSer public static void enqueueSuccessWithSymbols(final @Nonnull MockWebServer mockAppCenterServer) { // Create upload resource for app mockAppCenterServer.enqueue(new MockResponse().setResponseCode(HTTP_CREATED).setBody("{\n" + - " \"upload_id\": \"string\",\n" + - " \"upload_url\": \"" + mockAppCenterServer.url("/").toString() + "\",\n" + - " \"asset_id\": \"string\",\n" + + " \"id\": \"string\",\n" + + " \"upload_domain\": \"" + mockAppCenterServer.url("/").toString() + "\",\n" + " \"asset_domain\": \"string\",\n" + - " \"asset_token\": \"string\"\n" + + " \"url_encoded_token\": \"string\",\n" + + " \"package_asset_id\": \"string\"\n" + "}")); // Create upload resource for debug symbols @@ -59,15 +79,31 @@ public static void enqueueSuccessWithSymbols(final @Nonnull MockWebServer mockAp " \"expiration_date\": \"2020-03-18T21:16:22.188Z\"\n" + "}")); + // Set Metadata + mockAppCenterServer.enqueue(new MockResponse().setResponseCode(HTTP_OK).setBody("{\n" + + " \"chunk_size\": 1234\n" + + "}")); + // Upload app mockAppCenterServer.enqueue(new MockResponse().setResponseCode(HTTP_OK)); // Upload debug symbols mockAppCenterServer.enqueue(new MockResponse().setResponseCode(HTTP_OK)); - // Commit app + // Finish Release + mockAppCenterServer.enqueue(new MockResponse().setResponseCode(HTTP_OK)); + + // Update Release + mockAppCenterServer.enqueue(new MockResponse().setResponseCode(HTTP_OK).setBody("{\n" + + " \"id\": \"1234\",\n" + + " \"upload_status\": \"uploadFinished\"\n" + + "}")); + + // Poll For Release mockAppCenterServer.enqueue(new MockResponse().setResponseCode(HTTP_OK).setBody("{\n" + - " \"release_id\": 0,\n" + + " \"id\": \"1234\",\n" + + " \"upload_status\": \"readyToBePublished\",\n" + + " \"release_distinct_id\": \"4321\",\n" + " \"release_url\": \"string\"\n" + "}")); From 9f236ef540db06e13b00f98ae3e18503be2b8e5c Mon Sep 17 00:00:00 2001 From: Mez Pahlan Date: Thu, 10 Dec 2020 14:38:19 +0000 Subject: [PATCH 11/24] [JENKINS-64388] Add tests for FinishReleaseTask --- .../task/internal/FinishReleaseTask.java | 12 +- .../task/internal/FinishReleaseTaskTest.java | 140 ++++++++++++++++++ 2 files changed, 149 insertions(+), 3 deletions(-) create mode 100644 src/test/java/io/jenkins/plugins/appcenter/task/internal/FinishReleaseTaskTest.java diff --git a/src/main/java/io/jenkins/plugins/appcenter/task/internal/FinishReleaseTask.java b/src/main/java/io/jenkins/plugins/appcenter/task/internal/FinishReleaseTask.java index 02024a6..95e1436 100644 --- a/src/main/java/io/jenkins/plugins/appcenter/task/internal/FinishReleaseTask.java +++ b/src/main/java/io/jenkins/plugins/appcenter/task/internal/FinishReleaseTask.java @@ -12,6 +12,8 @@ import java.io.PrintStream; import java.util.concurrent.CompletableFuture; +import static java.util.Objects.requireNonNull; + @Singleton public final class FinishReleaseTask implements AppCenterTask, AppCenterLogger { @@ -37,11 +39,15 @@ public CompletableFuture execute(@Nonnull UploadRequest request) @Nonnull private CompletableFuture finishRelease(@Nonnull UploadRequest request) { + final String uploadDomain = requireNonNull(request.uploadDomain, "uploadDomain cannot be null"); + final String packageAssetId = requireNonNull(request.packageAssetId, "packageAssetId cannot be null"); + final String token = requireNonNull(request.token, "token cannot be null"); + log("Finishing release."); final CompletableFuture future = new CompletableFuture<>(); - final String url = getUrl(request); + final String url = getUrl(uploadDomain, packageAssetId, token); factory.createAppCenterService() .finishRelease(url) @@ -59,8 +65,8 @@ private CompletableFuture finishRelease(@Nonnull UploadRequest re } @Nonnull - private String getUrl(@Nonnull UploadRequest request) { - return String.format("%1$s/upload/finished/%2$s?token=%3$s", request.uploadDomain, request.packageAssetId, request.token); + private String getUrl(@Nonnull String uploadDomain, @Nonnull String packageAssetId, @Nonnull String token) { + return String.format("%1$s/upload/finished/%2$s?token=%3$s", uploadDomain, packageAssetId, token); } @Override diff --git a/src/test/java/io/jenkins/plugins/appcenter/task/internal/FinishReleaseTaskTest.java b/src/test/java/io/jenkins/plugins/appcenter/task/internal/FinishReleaseTaskTest.java new file mode 100644 index 0000000..d1a8ce4 --- /dev/null +++ b/src/test/java/io/jenkins/plugins/appcenter/task/internal/FinishReleaseTaskTest.java @@ -0,0 +1,140 @@ +package io.jenkins.plugins.appcenter.task.internal; + +import hudson.ProxyConfiguration; +import hudson.model.TaskListener; +import hudson.util.Secret; +import io.jenkins.plugins.appcenter.AppCenterException; +import io.jenkins.plugins.appcenter.api.AppCenterServiceFactory; +import io.jenkins.plugins.appcenter.task.request.UploadRequest; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.function.ThrowingRunnable; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.io.PrintStream; +import java.util.concurrent.ExecutionException; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; +import static org.mockito.BDDMockito.given; + +@RunWith(MockitoJUnitRunner.class) +public class FinishReleaseTaskTest { + + @Rule + public MockWebServer mockWebServer = new MockWebServer(); + + @Mock + TaskListener mockTaskListener; + + @Mock + PrintStream mockLogger; + + @Mock + ProxyConfiguration mockProxyConfig; + + private UploadRequest baseRequest; + + private FinishReleaseTask task; + + @Before + public void setUp() { + baseRequest = new UploadRequest.Builder() + .setOwnerName("owner-name") + .setAppName("app-name") + .build(); + given(mockTaskListener.getLogger()).willReturn(mockLogger); + final AppCenterServiceFactory factory = new AppCenterServiceFactory(Secret.fromString("secret-token"), mockWebServer.url("/").toString(), mockProxyConfig); + task = new FinishReleaseTask(mockTaskListener, factory); + } + + @Test + public void should_ReturnException_When_UploadDomainIsMissing() { + // Given + final UploadRequest uploadRequest = baseRequest.newBuilder() + .build(); + mockWebServer.enqueue(new MockResponse().setResponseCode(200)); + + // When + final ThrowingRunnable throwingRunnable = () -> task.execute(uploadRequest).get(); + + // Then + final NullPointerException exception = assertThrows(NullPointerException.class, throwingRunnable); + assertThat(exception).hasMessageThat().contains("uploadDomain cannot be null"); + } + + @Test + public void should_ReturnException_When_PackageAssetIdIsMissing() { + // Given + final UploadRequest uploadRequest = baseRequest.newBuilder() + .setUploadDomain("upload-domain") + .build(); + mockWebServer.enqueue(new MockResponse().setResponseCode(200)); + + // When + final ThrowingRunnable throwingRunnable = () -> task.execute(uploadRequest).get(); + + // Then + final NullPointerException exception = assertThrows(NullPointerException.class, throwingRunnable); + assertThat(exception).hasMessageThat().contains("packageAssetId cannot be null"); + } + + @Test + public void should_ReturnException_When_TokenIsMissing() { + // Given + final UploadRequest uploadRequest = baseRequest.newBuilder() + .setUploadDomain("upload-domain") + .setPackageAssetId("package_asset_id") + .build(); + mockWebServer.enqueue(new MockResponse().setResponseCode(200)); + + // When + final ThrowingRunnable throwingRunnable = () -> task.execute(uploadRequest).get(); + + // Then + final NullPointerException exception = assertThrows(NullPointerException.class, throwingRunnable); + assertThat(exception).hasMessageThat().contains("token cannot be null"); + } + + @Test + public void should_ReturnResponse_When_RequestIsSuccessful() throws Exception { + // Given + final UploadRequest uploadRequest = baseRequest.newBuilder() + .setUploadDomain("upload-domain") + .setPackageAssetId("package_asset_id") + .setToken("token") + .build(); + mockWebServer.enqueue(new MockResponse().setResponseCode(200)); + + // When + final UploadRequest actual = task.execute(uploadRequest).get(); + + // Then + assertThat(actual) + .isEqualTo(uploadRequest); + } + + @Test + public void should_ReturnException_When_RequestIsUnSuccessful() { + // Given + final UploadRequest uploadRequest = baseRequest.newBuilder() + .setUploadDomain("upload-domain") + .setPackageAssetId("package_asset_id") + .setToken("token") + .build(); + mockWebServer.enqueue(new MockResponse().setResponseCode(400)); + + // When + final ThrowingRunnable throwingRunnable = () -> task.execute(uploadRequest).get(); + + // Then + final ExecutionException exception = assertThrows(ExecutionException.class, throwingRunnable); + assertThat(exception).hasCauseThat().isInstanceOf(AppCenterException.class); + assertThat(exception).hasCauseThat().hasMessageThat().contains("Finishing release unsuccessful: HTTP 400 Client Error: "); + } +} \ No newline at end of file From 97243e38e85cb31d6404fbed1142682c215575e1 Mon Sep 17 00:00:00 2001 From: Mez Pahlan Date: Thu, 10 Dec 2020 14:41:49 +0000 Subject: [PATCH 12/24] [JENKINS-64388] Remove CommitUploadResourceTask.java This is not needed after the API changes of 8 Dec 2020. --- .../internal/CommitUploadResourceTask.java | 103 -------------- .../CommitUploadResourceTaskTest.java | 131 ------------------ 2 files changed, 234 deletions(-) delete mode 100644 src/main/java/io/jenkins/plugins/appcenter/task/internal/CommitUploadResourceTask.java delete mode 100644 src/test/java/io/jenkins/plugins/appcenter/task/internal/CommitUploadResourceTaskTest.java diff --git a/src/main/java/io/jenkins/plugins/appcenter/task/internal/CommitUploadResourceTask.java b/src/main/java/io/jenkins/plugins/appcenter/task/internal/CommitUploadResourceTask.java deleted file mode 100644 index 9ff6ed0..0000000 --- a/src/main/java/io/jenkins/plugins/appcenter/task/internal/CommitUploadResourceTask.java +++ /dev/null @@ -1,103 +0,0 @@ -package io.jenkins.plugins.appcenter.task.internal; - -import hudson.model.TaskListener; -import io.jenkins.plugins.appcenter.AppCenterException; -import io.jenkins.plugins.appcenter.AppCenterLogger; -import io.jenkins.plugins.appcenter.api.AppCenterServiceFactory; -import io.jenkins.plugins.appcenter.model.appcenter.ReleaseUploadEndRequest; -import io.jenkins.plugins.appcenter.model.appcenter.SymbolUploadEndRequest; -import io.jenkins.plugins.appcenter.task.request.UploadRequest; - -import javax.annotation.Nonnull; -import javax.inject.Inject; -import javax.inject.Singleton; -import java.io.PrintStream; -import java.util.concurrent.CompletableFuture; - -import static java.util.Objects.requireNonNull; - -@Singleton -public final class CommitUploadResourceTask implements AppCenterTask, AppCenterLogger { - - private static final long serialVersionUID = 1L; - - @Nonnull - private final TaskListener taskListener; - @Nonnull - private final AppCenterServiceFactory factory; - - @Inject - CommitUploadResourceTask(@Nonnull final TaskListener taskListener, - @Nonnull final AppCenterServiceFactory factory) { - this.taskListener = taskListener; - this.factory = factory; - } - - @Nonnull - @Override - public CompletableFuture execute(@Nonnull UploadRequest request) { - if (request.symbolUploadId == null) { - return commitAppUpload(request); - } else { - return commitAppUpload(request) - .thenCompose(this::commitSymbolsUpload); - } - } - - - @Nonnull - private CompletableFuture commitAppUpload(@Nonnull UploadRequest request) { - final String uploadId = requireNonNull(request.uploadId, "uploadId cannot be null"); - - log("Committing app resource."); - - final CompletableFuture future = new CompletableFuture<>(); - final ReleaseUploadEndRequest releaseUploadEndRequest = new ReleaseUploadEndRequest(ReleaseUploadEndRequest.StatusEnum.committed); - - factory.createAppCenterService() - .releaseUploadsComplete(request.ownerName, request.appName, uploadId, releaseUploadEndRequest) - .whenComplete((releaseUploadBeginResponse, throwable) -> { - if (throwable != null) { - final AppCenterException exception = logFailure("Committing app resource unsuccessful", throwable); - future.completeExceptionally(exception); - } else { - log("Committing app resource successful."); - final UploadRequest uploadRequest = request.newBuilder() - .setReleaseId(releaseUploadBeginResponse.release_id) - .build(); - future.complete(uploadRequest); - } - }); - - return future; - } - - @Nonnull - private CompletableFuture commitSymbolsUpload(@Nonnull UploadRequest request) { - final String symbolUploadId = requireNonNull(request.symbolUploadId, "symbolUploadId cannot be null"); - - log("Committing symbol resource."); - - final CompletableFuture future = new CompletableFuture<>(); - final SymbolUploadEndRequest symbolUploadEndRequest = new SymbolUploadEndRequest(SymbolUploadEndRequest.StatusEnum.committed); - - factory.createAppCenterService() - .symbolUploadsComplete(request.ownerName, request.appName, symbolUploadId, symbolUploadEndRequest) - .whenComplete((symbolUploadEndResponse, throwable) -> { - if (throwable != null) { - final AppCenterException exception = logFailure("Committing symbol resource unsuccessful: ", throwable); - future.completeExceptionally(exception); - } else { - log("Committing symbol resource successful."); - future.complete(request); - } - }); - - return future; - } - - @Override - public PrintStream getLogger() { - return taskListener.getLogger(); - } -} \ No newline at end of file diff --git a/src/test/java/io/jenkins/plugins/appcenter/task/internal/CommitUploadResourceTaskTest.java b/src/test/java/io/jenkins/plugins/appcenter/task/internal/CommitUploadResourceTaskTest.java deleted file mode 100644 index 77d70a0..0000000 --- a/src/test/java/io/jenkins/plugins/appcenter/task/internal/CommitUploadResourceTaskTest.java +++ /dev/null @@ -1,131 +0,0 @@ -package io.jenkins.plugins.appcenter.task.internal; - -import hudson.ProxyConfiguration; -import hudson.model.TaskListener; -import hudson.util.Secret; -import io.jenkins.plugins.appcenter.AppCenterException; -import io.jenkins.plugins.appcenter.api.AppCenterServiceFactory; -import io.jenkins.plugins.appcenter.model.appcenter.SymbolUploadBeginRequest; -import io.jenkins.plugins.appcenter.task.request.UploadRequest; -import okhttp3.mockwebserver.MockResponse; -import okhttp3.mockwebserver.MockWebServer; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.function.ThrowingRunnable; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.junit.MockitoJUnitRunner; - -import java.io.PrintStream; -import java.util.concurrent.ExecutionException; - -import static com.google.common.truth.Truth.assertThat; -import static org.junit.Assert.assertThrows; -import static org.mockito.BDDMockito.given; - -@RunWith(MockitoJUnitRunner.class) -public class CommitUploadResourceTaskTest { - - @Rule - public MockWebServer mockWebServer = new MockWebServer(); - - @Mock - TaskListener mockTaskListener; - - @Mock - PrintStream mockLogger; - - @Mock - ProxyConfiguration mockProxyConfig; - - private UploadRequest baseRequest; - - private CommitUploadResourceTask task; - - @Before - public void setUp() { - baseRequest = new UploadRequest.Builder() - .setOwnerName("owner-name") - .setAppName("app-name") - .setUploadId("upload-id") - .build(); - given(mockTaskListener.getLogger()).willReturn(mockLogger); - final AppCenterServiceFactory factory = new AppCenterServiceFactory(Secret.fromString("secret-token"), mockWebServer.url("/").toString(), mockProxyConfig); - task = new CommitUploadResourceTask(mockTaskListener, factory); - } - - @Test - public void should_ReturnResponse_When_RequestIsSuccessful() throws Exception { - // Given - final UploadRequest expected = baseRequest.newBuilder().setReleaseId(0).build(); - mockWebServer.enqueue(new MockResponse().setResponseCode(200).setBody("{\n" + - " \"release_id\": 0,\n" + - " \"release_url\": \"string\"\n" + - "}")); - - // When - final UploadRequest actual = task.execute(baseRequest).get(); - - // Then - assertThat(actual) - .isEqualTo(expected); - } - - @Test - public void should_ReturnResponse_When_DebugSymbolsAreFound() throws Exception { - // Given - final UploadRequest request = baseRequest.newBuilder() - .setPathToDebugSymbols("path/to/mappings.txt") - .setSymbolUploadRequest(new SymbolUploadBeginRequest(SymbolUploadBeginRequest.SymbolTypeEnum.AndroidProguard, null, "mappings.txt", "1", "1.0.0")) - .setSymbolUploadId("string") - .setSymbolUploadUrl("string") - .build(); - final UploadRequest expected = request.newBuilder().setReleaseId(0).build(); - mockWebServer.enqueue(new MockResponse().setResponseCode(200).setBody("{\n" + - " \"release_id\": 0,\n" + - " \"release_url\": \"string\"\n" + - "}")); - mockWebServer.enqueue(new MockResponse().setResponseCode(200).setBody("{\n" + - " \"symbol_upload_id\": \"string\",\n" + - " \"app_id\": \"string\",\n" + - " \"user\": {\n" + - " \"email\": \"string\",\n" + - " \"display_name\": \"string\"\n" + - " },\n" + - " \"status\": \"created\",\n" + - " \"symbol_type\": \"AndroidProguard\",\n" + - " \"symbols_uploaded\": [\n" + - " {\n" + - " \"symbol_id\": \"string\",\n" + - " \"platform\": \"string\"\n" + - " }\n" + - " ],\n" + - " \"origin\": \"User\",\n" + - " \"file_name\": \"string\",\n" + - " \"file_size\": 0,\n" + - " \"timestamp\": \"2019-11-17T12:12:06.701Z\"\n" + - "}")); - - // When - final UploadRequest actual = task.execute(request).get(); - - // Then - assertThat(actual) - .isEqualTo(expected); - } - - @Test - public void should_ReturnException_When_RequestIsUnSuccessful() { - // Given - mockWebServer.enqueue(new MockResponse().setResponseCode(400)); - - // When - final ThrowingRunnable throwingRunnable = () -> task.execute(baseRequest).get(); - - // Then - final ExecutionException exception = assertThrows(ExecutionException.class, throwingRunnable); - assertThat(exception).hasCauseThat().isInstanceOf(AppCenterException.class); - assertThat(exception).hasCauseThat().hasMessageThat().contains("Committing app resource unsuccessful: HTTP 400 Client Error: "); - } -} \ No newline at end of file From b2e5fc4e25ff128a14132a4435babd457a43cc1c Mon Sep 17 00:00:00 2001 From: Mez Pahlan Date: Thu, 10 Dec 2020 15:33:29 +0000 Subject: [PATCH 13/24] [JENKINS-64388] Remove redundant statements in test --- .../plugins/appcenter/task/internal/FinishReleaseTaskTest.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/test/java/io/jenkins/plugins/appcenter/task/internal/FinishReleaseTaskTest.java b/src/test/java/io/jenkins/plugins/appcenter/task/internal/FinishReleaseTaskTest.java index d1a8ce4..5b00c9e 100644 --- a/src/test/java/io/jenkins/plugins/appcenter/task/internal/FinishReleaseTaskTest.java +++ b/src/test/java/io/jenkins/plugins/appcenter/task/internal/FinishReleaseTaskTest.java @@ -58,7 +58,6 @@ public void should_ReturnException_When_UploadDomainIsMissing() { // Given final UploadRequest uploadRequest = baseRequest.newBuilder() .build(); - mockWebServer.enqueue(new MockResponse().setResponseCode(200)); // When final ThrowingRunnable throwingRunnable = () -> task.execute(uploadRequest).get(); @@ -74,7 +73,6 @@ public void should_ReturnException_When_PackageAssetIdIsMissing() { final UploadRequest uploadRequest = baseRequest.newBuilder() .setUploadDomain("upload-domain") .build(); - mockWebServer.enqueue(new MockResponse().setResponseCode(200)); // When final ThrowingRunnable throwingRunnable = () -> task.execute(uploadRequest).get(); @@ -91,7 +89,6 @@ public void should_ReturnException_When_TokenIsMissing() { .setUploadDomain("upload-domain") .setPackageAssetId("package_asset_id") .build(); - mockWebServer.enqueue(new MockResponse().setResponseCode(200)); // When final ThrowingRunnable throwingRunnable = () -> task.execute(uploadRequest).get(); From 87a3bd11883d29c84cf8a7cbe1bb43ed39968dce Mon Sep 17 00:00:00 2001 From: Mez Pahlan Date: Thu, 10 Dec 2020 15:44:38 +0000 Subject: [PATCH 14/24] [JENKINS-64388] Add test for PollForReleaseTaskTest --- .../task/internal/PollForReleaseTaskTest.java | 191 ++++++++++++++++++ 1 file changed, 191 insertions(+) create mode 100644 src/test/java/io/jenkins/plugins/appcenter/task/internal/PollForReleaseTaskTest.java diff --git a/src/test/java/io/jenkins/plugins/appcenter/task/internal/PollForReleaseTaskTest.java b/src/test/java/io/jenkins/plugins/appcenter/task/internal/PollForReleaseTaskTest.java new file mode 100644 index 0000000..7e1b908 --- /dev/null +++ b/src/test/java/io/jenkins/plugins/appcenter/task/internal/PollForReleaseTaskTest.java @@ -0,0 +1,191 @@ +package io.jenkins.plugins.appcenter.task.internal; + +import hudson.ProxyConfiguration; +import hudson.model.TaskListener; +import hudson.util.Secret; +import io.jenkins.plugins.appcenter.AppCenterException; +import io.jenkins.plugins.appcenter.api.AppCenterServiceFactory; +import io.jenkins.plugins.appcenter.task.request.UploadRequest; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.function.ThrowingRunnable; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.io.PrintStream; +import java.util.concurrent.ExecutionException; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; +import static org.mockito.BDDMockito.given; + +@RunWith(MockitoJUnitRunner.class) +public class PollForReleaseTaskTest { + + @Rule + public MockWebServer mockWebServer = new MockWebServer(); + + @Mock + TaskListener mockTaskListener; + + @Mock + PrintStream mockLogger; + + @Mock + ProxyConfiguration mockProxyConfig; + + private UploadRequest baseRequest; + + private PollForReleaseTask task; + + @Before + public void setUp() { + baseRequest = new UploadRequest.Builder() + .setOwnerName("owner-name") + .setAppName("app-name") + .build(); + given(mockTaskListener.getLogger()).willReturn(mockLogger); + final AppCenterServiceFactory factory = new AppCenterServiceFactory(Secret.fromString("secret-token"), mockWebServer.url("/").toString(), mockProxyConfig); + task = new PollForReleaseTask(mockTaskListener, factory); + } + + @Test + public void should_ReturnException_When_UploadIdIsMissing() { + // Given + final UploadRequest uploadRequest = baseRequest.newBuilder() + .build(); + + // When + final ThrowingRunnable throwingRunnable = () -> task.execute(uploadRequest).get(); + + // Then + final NullPointerException exception = assertThrows(NullPointerException.class, throwingRunnable); + assertThat(exception).hasMessageThat().contains("uploadId cannot be null"); + } + + @Test + public void should_RetryPolling_When_StatusIsStartedOrFinished() throws Exception { + // Given + final UploadRequest uploadRequest = baseRequest.newBuilder() + .setUploadId("upload_id") + .build(); + mockWebServer.enqueue(new MockResponse().setResponseCode(200).setBody("{\n" + + " \"upload_status\": \"uploadStarted\" \n" + + "}")); + mockWebServer.enqueue(new MockResponse().setResponseCode(200).setBody("{\n" + + " \"upload_status\": \"uploadFinished\" \n" + + "}")); + mockWebServer.enqueue(new MockResponse().setResponseCode(200).setBody("{\n" + + " \"upload_status\": \"readyToBePublished\",\n" + + " \"release_distinct_id\": 1234\n" + + "}")); + + // When + task.execute(uploadRequest).get(); + + // Then + assertThat(mockWebServer.getRequestCount()) + .isEqualTo(3); + } + + @Test + public void should_ReturnResponse_When_RequestIsSuccessful() throws Exception { + // Given + final UploadRequest uploadRequest = baseRequest.newBuilder() + .setUploadId("upload_id") + .build(); + final UploadRequest expected = uploadRequest.newBuilder().setReleaseId(1234).build(); + mockWebServer.enqueue(new MockResponse().setResponseCode(200).setBody("{\n" + + " \"upload_status\": \"readyToBePublished\",\n" + + " \"release_distinct_id\": 1234\n" + + "}")); + + // When + final UploadRequest actual = task.execute(uploadRequest).get(); + + // Then + assertThat(actual) + .isEqualTo(expected); + } + + @Test + public void should_ReturnException_When_StatusIsMalware() { + // Given + final UploadRequest uploadRequest = baseRequest.newBuilder() + .setUploadId("upload_id") + .build(); + mockWebServer.enqueue(new MockResponse().setResponseCode(200).setBody("{\n" + + " \"upload_status\": \"malwareDetected\",\n" + + " \"error_details\": \"we found this does it belong to you?\"\n" + + "}")); + + // When + final ThrowingRunnable throwingRunnable = () -> task.execute(uploadRequest).get(); + + // Then + final ExecutionException exception = assertThrows(ExecutionException.class, throwingRunnable); + assertThat(exception).hasCauseThat().isInstanceOf(AppCenterException.class); + assertThat(exception).hasCauseThat().hasMessageThat().contains("Polling for app release successful however was rejected by server: we found this does it belong to you?"); + } + + @Test + public void should_ReturnException_When_StatusIsError() { + // Given + final UploadRequest uploadRequest = baseRequest.newBuilder() + .setUploadId("upload_id") + .build(); + mockWebServer.enqueue(new MockResponse().setResponseCode(200).setBody("{\n" + + " \"upload_status\": \"error\",\n" + + " \"error_details\": \"error error error\"\n" + + "}")); + + // When + final ThrowingRunnable throwingRunnable = () -> task.execute(uploadRequest).get(); + + // Then + final ExecutionException exception = assertThrows(ExecutionException.class, throwingRunnable); + assertThat(exception).hasCauseThat().isInstanceOf(AppCenterException.class); + assertThat(exception).hasCauseThat().hasMessageThat().contains("Polling for app release successful however was rejected by server: error error error"); + } + + @Test + public void should_ReturnException_When_StatusIsUnknown() { + // Given + final UploadRequest uploadRequest = baseRequest.newBuilder() + .setUploadId("upload_id") + .build(); + mockWebServer.enqueue(new MockResponse().setResponseCode(200).setBody("{\n" + + " \"upload_status\": \"huggobilly\",\n" + + " \"error_details\": \"hubally goobally hobbilly goobilly\"\n" + + "}")); + + // When + final ThrowingRunnable throwingRunnable = () -> task.execute(uploadRequest).get(); + + // Then + final ExecutionException exception = assertThrows(ExecutionException.class, throwingRunnable); + assertThat(exception).hasCauseThat().isInstanceOf(AppCenterException.class); + assertThat(exception).hasCauseThat().hasMessageThat().contains("Polling for app release unsuccessful"); + } + + @Test + public void should_ReturnException_When_RequestIsUnSuccessful() { + // Given + final UploadRequest uploadRequest = baseRequest.newBuilder() + .setUploadId("upload_id") + .build(); + mockWebServer.enqueue(new MockResponse().setResponseCode(400)); + + // When + final ThrowingRunnable throwingRunnable = () -> task.execute(uploadRequest).get(); + + // Then + final ExecutionException exception = assertThrows(ExecutionException.class, throwingRunnable); + assertThat(exception).hasCauseThat().isInstanceOf(AppCenterException.class); + assertThat(exception).hasCauseThat().hasMessageThat().contains("Polling for app release unsuccessful"); + } +} \ No newline at end of file From 0fb27603126056f75d20246f45582ee650db4152 Mon Sep 17 00:00:00 2001 From: Mez Pahlan Date: Thu, 10 Dec 2020 17:49:27 +0000 Subject: [PATCH 15/24] [JENKINS-64388] Add test for RemoteFileUtils --- .../appcenter/util/RemoteFileUtils.java | 38 ++- .../appcenter/util/RemoteFileUtilsTest.java | 259 ++++++++++++++++++ .../plugins/appcenter/util/TestFileUtil.java | 6 +- 3 files changed, 300 insertions(+), 3 deletions(-) create mode 100644 src/test/java/io/jenkins/plugins/appcenter/util/RemoteFileUtilsTest.java diff --git a/src/main/java/io/jenkins/plugins/appcenter/util/RemoteFileUtils.java b/src/main/java/io/jenkins/plugins/appcenter/util/RemoteFileUtils.java index 39c5541..5492fca 100644 --- a/src/main/java/io/jenkins/plugins/appcenter/util/RemoteFileUtils.java +++ b/src/main/java/io/jenkins/plugins/appcenter/util/RemoteFileUtils.java @@ -3,6 +3,7 @@ import hudson.FilePath; import javax.annotation.Nonnull; +import javax.annotation.Nullable; import javax.inject.Inject; import java.io.File; import java.io.Serializable; @@ -14,6 +15,9 @@ public class RemoteFileUtils implements Serializable { @Nonnull private final FilePath filePath; + @Nullable + private File file; + @Inject RemoteFileUtils(@Nonnull final FilePath filePath) { this.filePath = filePath; @@ -21,6 +25,38 @@ public class RemoteFileUtils implements Serializable { @Nonnull public File getRemoteFile(@Nonnull String pathToRemoteFile) { - return new File(filePath.child(pathToRemoteFile).getRemote()); + if (file == null) { + file = new File(filePath.child(pathToRemoteFile).getRemote()); + } + + return file; + } + + @Nonnull + public String getFileName(@Nonnull String pathToRemoveFile) { + return getRemoteFile(pathToRemoveFile).getName(); + } + + public long getFileSize(@Nonnull String pathToRemoveFile) { + return getRemoteFile(pathToRemoveFile).length(); + } + + @Nonnull + public String getContentType(@Nonnull String pathToApp) { + if (pathToApp.endsWith(".apk") || pathToApp.endsWith(".aab")) return "application/vnd.android.package-archive"; + if (pathToApp.endsWith(".msi")) return "application/x-msi"; + if (pathToApp.endsWith(".plist")) return "application/xml"; + if (pathToApp.endsWith(".aetx")) return "application/c-x509-ca-cert"; + if (pathToApp.endsWith(".cer")) return "application/pkix-cert"; + if (pathToApp.endsWith("xap")) return "application/x-silverlight-app"; + if (pathToApp.endsWith(".appx")) return "application/x-appx"; + if (pathToApp.endsWith(".appxbundle")) return "application/x-appxbundle"; + if (pathToApp.endsWith(".appxupload") || pathToApp.endsWith(".appxsym")) return "application/x-appxupload"; + if (pathToApp.endsWith(".msix")) return "application/x-msix"; + if (pathToApp.endsWith(".msixbundle")) return "application/x-msixbundle"; + if (pathToApp.endsWith(".msixupload") || pathToApp.endsWith(".msixsym")) return "application/x-msixupload"; + + // Otherwise + return "application/octet-stream"; } } \ No newline at end of file diff --git a/src/test/java/io/jenkins/plugins/appcenter/util/RemoteFileUtilsTest.java b/src/test/java/io/jenkins/plugins/appcenter/util/RemoteFileUtilsTest.java new file mode 100644 index 0000000..ecc6ca0 --- /dev/null +++ b/src/test/java/io/jenkins/plugins/appcenter/util/RemoteFileUtilsTest.java @@ -0,0 +1,259 @@ +package io.jenkins.plugins.appcenter.util; + +import hudson.FilePath; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.io.File; + +import static com.google.common.truth.Truth.assertThat; +import static io.jenkins.plugins.appcenter.util.TestFileUtil.TEST_FILE_PATH; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; + +@RunWith(MockitoJUnitRunner.class) +public class RemoteFileUtilsTest { + + @Mock + FilePath filePath; + + private RemoteFileUtils remoteFileUtils; + + @Before + public void setUp() { + given(filePath.child(anyString())).willReturn(filePath); + remoteFileUtils = new RemoteFileUtils(filePath); + } + + @Test + public void should_ReturnFile_When_FileExists() { + // Given + final File expected = new File(TEST_FILE_PATH); + given(filePath.getRemote()).willReturn(TEST_FILE_PATH); + + // When + final File remoteFile = remoteFileUtils.getRemoteFile(TEST_FILE_PATH); + + // Then + assertThat(remoteFile).isEqualTo(expected); + } + + @Test + public void should_ReturnFileName_When_FileExists() { + // Given + given(filePath.getRemote()).willReturn(TEST_FILE_PATH); + + // When + final String fileName = remoteFileUtils.getFileName(TEST_FILE_PATH); + + // Then + assertThat(fileName).isEqualTo("xiola.apk"); + } + + @Test + public void should_ReturnFileSize_When_FileExists() { + // Given + given(filePath.getRemote()).willReturn(TEST_FILE_PATH); + + // When + final long fileSize = remoteFileUtils.getFileSize(TEST_FILE_PATH); + + // Then + assertThat(fileSize).isEqualTo(41); + } + + @Test + public void should_ReturnContentType_When_APK() { + // Given + final String pathToFile = "test.apk"; + + // When + final String contentType = remoteFileUtils.getContentType(pathToFile); + + // Then + assertThat(contentType).isEqualTo("application/vnd.android.package-archive"); + } + + @Test + public void should_ReturnContentType_When_AAB() { + // Given + final String pathToFile = "test.aab"; + + // When + final String contentType = remoteFileUtils.getContentType(pathToFile); + + // Then + assertThat(contentType).isEqualTo("application/vnd.android.package-archive"); + } + + @Test + public void should_ReturnContentType_When_MSI() { + // Given + final String pathToFile = "test.msi"; + + // When + final String contentType = remoteFileUtils.getContentType(pathToFile); + + // Then + assertThat(contentType).isEqualTo("application/x-msi"); + } + + @Test + public void should_ReturnContentType_When_PLIST() { + // Given + final String pathToFile = "test.plist"; + + // When + final String contentType = remoteFileUtils.getContentType(pathToFile); + + // Then + assertThat(contentType).isEqualTo("application/xml"); + } + + @Test + public void should_ReturnContentType_When_AETX() { + // Given + final String pathToFile = "test.aetx"; + + // When + final String contentType = remoteFileUtils.getContentType(pathToFile); + + // Then + assertThat(contentType).isEqualTo("application/c-x509-ca-cert"); + } + + @Test + public void should_ReturnContentType_When_CER() { + // Given + final String pathToFile = "test.cer"; + + // When + final String contentType = remoteFileUtils.getContentType(pathToFile); + + // Then + assertThat(contentType).isEqualTo("application/pkix-cert"); + } + + @Test + public void should_ReturnContentType_When_XAP() { + // Given + final String pathToFile = "test.xap"; + + // When + final String contentType = remoteFileUtils.getContentType(pathToFile); + + // Then + assertThat(contentType).isEqualTo("application/x-silverlight-app"); + } + + @Test + public void should_ReturnContentType_When_APPX() { + // Given + final String pathToFile = "test.appx"; + + // When + final String contentType = remoteFileUtils.getContentType(pathToFile); + + // Then + assertThat(contentType).isEqualTo("application/x-appx"); + } + + @Test + public void should_ReturnContentType_When_APPXBUNDLE() { + // Given + final String pathToFile = "test.appxbundle"; + + // When + final String contentType = remoteFileUtils.getContentType(pathToFile); + + // Then + assertThat(contentType).isEqualTo("application/x-appxbundle"); + } + + @Test + public void should_ReturnContentType_When_APPXUPLOAD() { + // Given + final String pathToFile = "test.appxupload"; + + // When + final String contentType = remoteFileUtils.getContentType(pathToFile); + + // Then + assertThat(contentType).isEqualTo("application/x-appxupload"); + } + + @Test + public void should_ReturnContentType_When_APPXSYM() { + // Given + final String pathToFile = "test.appxsym"; + + // When + final String contentType = remoteFileUtils.getContentType(pathToFile); + + // Then + assertThat(contentType).isEqualTo("application/x-appxupload"); + } + + @Test + public void should_ReturnContentType_When_MSIX() { + // Given + final String pathToFile = "test.msix"; + + // When + final String contentType = remoteFileUtils.getContentType(pathToFile); + + // Then + assertThat(contentType).isEqualTo("application/x-msix"); + } + + @Test + public void should_ReturnContentType_When_MSIXBUNDLE() { + // Given + final String pathToFile = "test.msixbundle"; + + // When + final String contentType = remoteFileUtils.getContentType(pathToFile); + + // Then + assertThat(contentType).isEqualTo("application/x-msixbundle"); + } + + @Test + public void should_ReturnContentType_When_MSIXUPLOAD() { + // Given + final String pathToFile = "test.msixupload"; + + // When + final String contentType = remoteFileUtils.getContentType(pathToFile); + + // Then + assertThat(contentType).isEqualTo("application/x-msixupload"); + } + + @Test + public void should_ReturnContentType_When_MSIXSYM() { + // Given + final String pathToFile = "test.msixsym"; + + // When + final String contentType = remoteFileUtils.getContentType(pathToFile); + + // Then + assertThat(contentType).isEqualTo("application/x-msixupload"); + } + + @Test + public void should_ReturnContentType_When_UNKNOWN() { + // Given + final String pathToFile = "test.foo"; + + // When + final String contentType = remoteFileUtils.getContentType(pathToFile); + + // Then + assertThat(contentType).isEqualTo("application/octet-stream"); + } +} \ No newline at end of file diff --git a/src/test/java/io/jenkins/plugins/appcenter/util/TestFileUtil.java b/src/test/java/io/jenkins/plugins/appcenter/util/TestFileUtil.java index 062ad16..9346562 100644 --- a/src/test/java/io/jenkins/plugins/appcenter/util/TestFileUtil.java +++ b/src/test/java/io/jenkins/plugins/appcenter/util/TestFileUtil.java @@ -5,14 +5,16 @@ public final class TestFileUtil { + public static final String TEST_FILE_PATH = "src/test/resources/three/days/xiola.apk"; + @Nonnull public static File createFileForTesting() { - return new File("src/test/resources/three/days/xiola.apk"); + return new File(TEST_FILE_PATH); } @Nonnull public static File createLargeFileForTesting() { - return new File("src/test/resources/three/days/xiola.apk") { + return new File(TEST_FILE_PATH) { @Override public long length() { return (1024 * 1024) * 512; // Double the max size allowed to upload. From 86052a4be43b14f15ff688c78ee708f4daeb0c6f Mon Sep 17 00:00:00 2001 From: Mez Pahlan Date: Sun, 13 Dec 2020 12:31:40 +0000 Subject: [PATCH 16/24] [JENKINS-64388] Add test for SetMetadataTask --- .../task/internal/SetMetadataTask.java | 57 +++---- .../task/internal/SetMetadataTaskTest.java | 145 ++++++++++++++++++ 2 files changed, 163 insertions(+), 39 deletions(-) create mode 100644 src/test/java/io/jenkins/plugins/appcenter/task/internal/SetMetadataTaskTest.java diff --git a/src/main/java/io/jenkins/plugins/appcenter/task/internal/SetMetadataTask.java b/src/main/java/io/jenkins/plugins/appcenter/task/internal/SetMetadataTask.java index 211588d..2d49112 100644 --- a/src/main/java/io/jenkins/plugins/appcenter/task/internal/SetMetadataTask.java +++ b/src/main/java/io/jenkins/plugins/appcenter/task/internal/SetMetadataTask.java @@ -5,14 +5,16 @@ import io.jenkins.plugins.appcenter.AppCenterLogger; import io.jenkins.plugins.appcenter.api.AppCenterServiceFactory; import io.jenkins.plugins.appcenter.task.request.UploadRequest; +import io.jenkins.plugins.appcenter.util.RemoteFileUtils; import javax.annotation.Nonnull; import javax.inject.Inject; import javax.inject.Singleton; -import java.io.File; import java.io.PrintStream; import java.util.concurrent.CompletableFuture; +import static java.util.Objects.requireNonNull; + @Singleton public final class SetMetadataTask implements AppCenterTask, AppCenterLogger { @@ -22,12 +24,16 @@ public final class SetMetadataTask implements AppCenterTask, AppC private final TaskListener taskListener; @Nonnull private final AppCenterServiceFactory factory; + @Nonnull + private final RemoteFileUtils remoteFileUtils; @Inject SetMetadataTask(@Nonnull final TaskListener taskListener, - @Nonnull final AppCenterServiceFactory factory) { + @Nonnull final AppCenterServiceFactory factory, + @Nonnull final RemoteFileUtils remoteFileUtils) { this.taskListener = taskListener; this.factory = factory; + this.remoteFileUtils = remoteFileUtils; } @Nonnull @@ -38,11 +44,15 @@ public CompletableFuture execute(@Nonnull UploadRequest request) @Nonnull private CompletableFuture setMetadata(@Nonnull UploadRequest request) { + final String uploadDomain = requireNonNull(request.uploadDomain, "uploadDomain cannot be null"); + final String packageAssetId = requireNonNull(request.packageAssetId, "packageAssetId cannot be null"); + final String token = requireNonNull(request.token, "token cannot be null"); + log("Setting metadata."); final CompletableFuture future = new CompletableFuture<>(); - final String url = getUrl(request); + final String url = getUrl(request.pathToApp, uploadDomain, packageAssetId, token); factory.createAppCenterService() .setMetaData(url) @@ -63,45 +73,14 @@ private CompletableFuture setMetadata(@Nonnull UploadRequest requ return future; } - @Nonnull - private String getUrl(@Nonnull UploadRequest request) { - final File file = new File(request.pathToApp); - final String fileName = getFileName(file); - final long fileSize = getFileSize(file); - final String contentType = getContentType(request.pathToApp); - - 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", request.uploadDomain, request.packageAssetId, fileName, fileSize, request.token, contentType); - } @Nonnull - private String getFileName(@Nonnull File file) { - // TODO: Move to Prerequisite Task - return file.getName(); - } + private String getUrl(@Nonnull String pathToApp, @Nonnull String uploadDomain, @Nonnull String packageAssetId, @Nonnull String token) { + final String fileName = remoteFileUtils.getFileName(pathToApp); + final long fileSize = remoteFileUtils.getFileSize(pathToApp); + final String contentType = remoteFileUtils.getContentType(pathToApp); - private long getFileSize(@Nonnull File file) { - // TODO: Move to Prerequisite Task - return file.length(); - } - - @Nonnull - private String getContentType(@Nonnull String pathToApp) { - // TODO: Move to Prerequisite Task - if (pathToApp.endsWith(".apk") || pathToApp.endsWith(".aab")) return "application/vnd.android.package-archive"; - if (pathToApp.endsWith(".msi")) return "application/x-msi"; - if (pathToApp.endsWith(".plist")) return "application/xml"; - if (pathToApp.endsWith(".aetx")) return "application/c-x509-ca-cert"; - if (pathToApp.endsWith(".cer")) return "application/pkix-cert"; - if (pathToApp.endsWith("xap")) return "application/x-silverlight-app"; - if (pathToApp.endsWith(".appx")) return "application/x-appx"; - if (pathToApp.endsWith(".appxbundle")) return "application/x-appxbundle"; - if (pathToApp.endsWith(".appxupload") || pathToApp.endsWith(".appxsym")) return "application/x-appxupload"; - if (pathToApp.endsWith(".msix")) return "application/x-msix"; - if (pathToApp.endsWith(".msixbundle")) return "application/x-msixbundle"; - if (pathToApp.endsWith(".msixupload") || pathToApp.endsWith(".msixsym")) return "application/x-msixupload"; - - // Otherwise - return "application/octet-stream"; + 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); } @Override diff --git a/src/test/java/io/jenkins/plugins/appcenter/task/internal/SetMetadataTaskTest.java b/src/test/java/io/jenkins/plugins/appcenter/task/internal/SetMetadataTaskTest.java new file mode 100644 index 0000000..bd0037a --- /dev/null +++ b/src/test/java/io/jenkins/plugins/appcenter/task/internal/SetMetadataTaskTest.java @@ -0,0 +1,145 @@ +package io.jenkins.plugins.appcenter.task.internal; + +import hudson.ProxyConfiguration; +import hudson.model.TaskListener; +import hudson.util.Secret; +import io.jenkins.plugins.appcenter.AppCenterException; +import io.jenkins.plugins.appcenter.api.AppCenterServiceFactory; +import io.jenkins.plugins.appcenter.task.request.UploadRequest; +import io.jenkins.plugins.appcenter.util.RemoteFileUtils; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.function.ThrowingRunnable; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.io.PrintStream; +import java.util.concurrent.ExecutionException; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; +import static org.mockito.BDDMockito.given; + +@RunWith(MockitoJUnitRunner.class) +public class SetMetadataTaskTest { + + @Rule + public MockWebServer mockWebServer = new MockWebServer(); + + @Mock + TaskListener mockTaskListener; + + @Mock + RemoteFileUtils remoteFileUtils; + + @Mock + PrintStream mockLogger; + + @Mock + ProxyConfiguration mockProxyConfig; + + private UploadRequest baseRequest; + + private SetMetadataTask task; + + @Before + public void setUp() { + baseRequest = new UploadRequest.Builder() + .setOwnerName("owner-name") + .setAppName("app-name") + .setPathToApp("path-to-app") + .build(); + given(mockTaskListener.getLogger()).willReturn(mockLogger); + final AppCenterServiceFactory factory = new AppCenterServiceFactory(Secret.fromString("secret-token"), mockWebServer.url("/").toString(), mockProxyConfig); + task = new SetMetadataTask(mockTaskListener, factory, remoteFileUtils); + } + + @Test + public void should_ReturnException_When_UploadDomainIsMissing() { + // Given + final UploadRequest uploadRequest = baseRequest.newBuilder() + .build(); + + // When + final ThrowingRunnable throwingRunnable = () -> task.execute(uploadRequest).get(); + + // Then + final NullPointerException exception = assertThrows(NullPointerException.class, throwingRunnable); + assertThat(exception).hasMessageThat().contains("uploadDomain cannot be null"); + } + + @Test + public void should_ReturnException_When_PackageAssetIdIsMissing() { + // Given + final UploadRequest uploadRequest = baseRequest.newBuilder() + .setUploadDomain("upload-domain") + .build(); + + // When + final ThrowingRunnable throwingRunnable = () -> task.execute(uploadRequest).get(); + + // Then + final NullPointerException exception = assertThrows(NullPointerException.class, throwingRunnable); + assertThat(exception).hasMessageThat().contains("packageAssetId cannot be null"); + } + + @Test + public void should_ReturnException_When_TokenIsMissing() { + // Given + final UploadRequest uploadRequest = baseRequest.newBuilder() + .setUploadDomain("upload-domain") + .setPackageAssetId("package_asset_id") + .build(); + + // When + final ThrowingRunnable throwingRunnable = () -> task.execute(uploadRequest).get(); + + // Then + final NullPointerException exception = assertThrows(NullPointerException.class, throwingRunnable); + assertThat(exception).hasMessageThat().contains("token cannot be null"); + } + + @Test + public void should_ReturnResponse_When_RequestIsSuccessful() throws Exception { + // Given + final UploadRequest uploadRequest = baseRequest.newBuilder() + .setUploadDomain("upload-domain") + .setPackageAssetId("package_asset_id") + .setToken("token") + .build(); + final UploadRequest expected = uploadRequest.newBuilder().setChunkSize(4098).build(); + mockWebServer.enqueue(new MockResponse().setResponseCode(200).setBody("{\n" + + " \"chunk_size\": 4098\n" + + "}")); + + // When + final UploadRequest actual = task.execute(uploadRequest).get(); + + // Then + assertThat(actual) + .isEqualTo(expected); + } + + @Test + public void should_ReturnException_When_RequestIsUnSuccessful() { + // Given + final UploadRequest uploadRequest = baseRequest.newBuilder() + .setUploadDomain("upload-domain") + .setPackageAssetId("package_asset_id") + .setToken("token") + .build(); + mockWebServer.enqueue(new MockResponse().setResponseCode(400)); + + // When + final ThrowingRunnable throwingRunnable = () -> task.execute(uploadRequest).get(); + + // Then + final ExecutionException exception = assertThrows(ExecutionException.class, throwingRunnable); + assertThat(exception).hasCauseThat().isInstanceOf(AppCenterException.class); + assertThat(exception).hasCauseThat().hasMessageThat().contains("Setting metadata unsuccessful"); + } +} \ No newline at end of file From e0afbdce99dd51539f60ef55de2c476d61eb4f92 Mon Sep 17 00:00:00 2001 From: Mez Pahlan Date: Sun, 13 Dec 2020 12:58:12 +0000 Subject: [PATCH 17/24] [JENKINS-64388] Add test for UpdateReleaseTask --- .../task/internal/UpdateReleaseTaskTest.java | 107 ++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 src/test/java/io/jenkins/plugins/appcenter/task/internal/UpdateReleaseTaskTest.java diff --git a/src/test/java/io/jenkins/plugins/appcenter/task/internal/UpdateReleaseTaskTest.java b/src/test/java/io/jenkins/plugins/appcenter/task/internal/UpdateReleaseTaskTest.java new file mode 100644 index 0000000..3b8eae6 --- /dev/null +++ b/src/test/java/io/jenkins/plugins/appcenter/task/internal/UpdateReleaseTaskTest.java @@ -0,0 +1,107 @@ +package io.jenkins.plugins.appcenter.task.internal; + +import hudson.ProxyConfiguration; +import hudson.model.TaskListener; +import hudson.util.Secret; +import io.jenkins.plugins.appcenter.AppCenterException; +import io.jenkins.plugins.appcenter.api.AppCenterServiceFactory; +import io.jenkins.plugins.appcenter.task.request.UploadRequest; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.function.ThrowingRunnable; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.io.PrintStream; +import java.util.concurrent.ExecutionException; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; +import static org.mockito.BDDMockito.given; + +@RunWith(MockitoJUnitRunner.class) +public class UpdateReleaseTaskTest { + + @Rule + public MockWebServer mockWebServer = new MockWebServer(); + + @Mock + TaskListener mockTaskListener; + + @Mock + PrintStream mockLogger; + + @Mock + ProxyConfiguration mockProxyConfig; + + private UploadRequest baseRequest; + + private UpdateReleaseTask task; + + @Before + public void setUp() { + baseRequest = new UploadRequest.Builder() + .setOwnerName("owner-name") + .setAppName("app-name") + .setPathToApp("path-to-app") + .build(); + given(mockTaskListener.getLogger()).willReturn(mockLogger); + final AppCenterServiceFactory factory = new AppCenterServiceFactory(Secret.fromString("secret-token"), mockWebServer.url("/").toString(), mockProxyConfig); + task = new UpdateReleaseTask(mockTaskListener, factory); + } + + @Test + public void should_ReturnException_When_UploadIdIsMissing() { + // Given + final UploadRequest uploadRequest = baseRequest.newBuilder() + .build(); + + // When + final ThrowingRunnable throwingRunnable = () -> task.execute(uploadRequest).get(); + + // Then + final NullPointerException exception = assertThrows(NullPointerException.class, throwingRunnable); + assertThat(exception).hasMessageThat().contains("uploadId cannot be null"); + } + + @Test + public void should_ReturnResponse_When_RequestIsSuccessful() throws Exception { + // Given + final UploadRequest uploadRequest = baseRequest.newBuilder() + .setUploadId("upload_id") + .build(); + mockWebServer.enqueue(new MockResponse().setResponseCode(200).setBody("{" + + "\"id\": \"upload_id\",\n" + + "\"upload_status\": \"uploadFinished\"\n" + + "}") + ); + + // When + final UploadRequest actual = task.execute(uploadRequest).get(); + + // Then + assertThat(actual) + .isEqualTo(uploadRequest); + } + + @Test + public void should_ReturnException_When_RequestIsUnSuccessful() { + // Given + final UploadRequest uploadRequest = baseRequest.newBuilder() + .setUploadId("upload_id") + .build(); + mockWebServer.enqueue(new MockResponse().setResponseCode(400)); + + // When + final ThrowingRunnable throwingRunnable = () -> task.execute(uploadRequest).get(); + + // Then + final ExecutionException exception = assertThrows(ExecutionException.class, throwingRunnable); + assertThat(exception).hasCauseThat().isInstanceOf(AppCenterException.class); + assertThat(exception).hasCauseThat().hasMessageThat().contains("Updating release unsuccessful"); + } +} \ No newline at end of file From 3727e5def74422f0daa8c569063323c999ee2299 Mon Sep 17 00:00:00 2001 From: Mez Pahlan Date: Sun, 13 Dec 2020 15:19:19 +0000 Subject: [PATCH 18/24] [JENKINS-64388] Fix test for CreateUploadResourceTaskTest --- .../CreateUploadResourceTaskTest.java | 120 ++++++++---------- 1 file changed, 56 insertions(+), 64 deletions(-) diff --git a/src/test/java/io/jenkins/plugins/appcenter/task/internal/CreateUploadResourceTaskTest.java b/src/test/java/io/jenkins/plugins/appcenter/task/internal/CreateUploadResourceTaskTest.java index caf4870..00d34c3 100644 --- a/src/test/java/io/jenkins/plugins/appcenter/task/internal/CreateUploadResourceTaskTest.java +++ b/src/test/java/io/jenkins/plugins/appcenter/task/internal/CreateUploadResourceTaskTest.java @@ -5,6 +5,7 @@ import hudson.util.Secret; import io.jenkins.plugins.appcenter.AppCenterException; import io.jenkins.plugins.appcenter.api.AppCenterServiceFactory; +import io.jenkins.plugins.appcenter.model.appcenter.SymbolUploadBeginRequest; import io.jenkins.plugins.appcenter.task.request.UploadRequest; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; @@ -57,78 +58,69 @@ public void setUp() { @Test public void should_ReturnResponse_When_RequestIsSuccessful() throws Exception { - // TODO: Fix Me -// // Given -// final UploadRequest expected = baseRequest.newBuilder().setUploadId("string").setUploadUrl("string").build(); -// mockWebServer.enqueue(new MockResponse().setResponseCode(201).setBody("{\n" + -// " \"upload_id\": \"string\",\n" + -// " \"upload_url\": \"string\",\n" + -// " \"asset_id\": \"string\",\n" + -// " \"asset_domain\": \"string\",\n" + -// " \"asset_token\": \"string\"\n" + -// "}")); -// -// // When -// final UploadRequest actual = task.execute(baseRequest).get(); -// -// // Then -// assertThat(actual) -// .isEqualTo(expected); + // Given + final UploadRequest expected = baseRequest.newBuilder().setUploadId("string").setUploadDomain("string").setToken("string").setPackageAssetId("string").build(); + mockWebServer.enqueue(new MockResponse().setResponseCode(201).setBody("{\n" + + " \"id\": \"string\",\n" + + " \"upload_domain\": \"string\",\n" + + " \"url_encoded_token\": \"string\",\n" + + " \"package_asset_id\": \"string\"\n" + + "}")); + + // When + final UploadRequest actual = task.execute(baseRequest).get(); + + // Then + assertThat(actual) + .isEqualTo(expected); } @Test public void should_ReturnResponse_When_DebugSymbolsAreFound() throws Exception { - // TODO: Fix Me -// // Given -// final UploadRequest request = baseRequest.newBuilder() -// .setPathToDebugSymbols("path/to/mappings.txt") -// .setSymbolUploadRequest(new SymbolUploadBeginRequest(SymbolUploadBeginRequest.SymbolTypeEnum.AndroidProguard, null, "mappings.txt", "1", "1.0.0")) -// .build(); -// final UploadRequest expected = request.newBuilder() -// .setUploadId("string").setUploadUrl("string") -// .setSymbolUploadId("string").setSymbolUploadUrl("string") -// .build(); -// mockWebServer.enqueue(new MockResponse().setResponseCode(201).setBody("{\n" + -// " \"upload_id\": \"string\",\n" + -// " \"upload_url\": \"string\",\n" + -// " \"asset_id\": \"string\",\n" + -// " \"asset_domain\": \"string\",\n" + -// " \"asset_token\": \"string\"\n" + -// "}")); -// mockWebServer.enqueue(new MockResponse().setResponseCode(200).setBody("{\n" + -// " \"symbol_upload_id\": \"string\",\n" + -// " \"upload_url\": \"string\",\n" + -// " \"expiration_date\": \"2019-11-17T12:01:43.953Z\"\n" + -// "}")); -// -// // When -// final UploadRequest actual = task.execute(request).get(); -// -// // Then -// assertThat(actual) -// .isEqualTo(expected); + // Given + final UploadRequest request = baseRequest.newBuilder() + .setPathToDebugSymbols("path/to/mappings.txt") + .setSymbolUploadRequest(new SymbolUploadBeginRequest(SymbolUploadBeginRequest.SymbolTypeEnum.AndroidProguard, null, "mappings.txt", "1", "1.0.0")) + .build(); + final UploadRequest expected = request.newBuilder().setUploadId("string").setUploadDomain("string").setToken("string").setPackageAssetId("string").setSymbolUploadId("string").setSymbolUploadUrl("string").build(); + mockWebServer.enqueue(new MockResponse().setResponseCode(201).setBody("{\n" + + " \"id\": \"string\",\n" + + " \"upload_domain\": \"string\",\n" + + " \"url_encoded_token\": \"string\",\n" + + " \"package_asset_id\": \"string\"\n" + + "}")); + mockWebServer.enqueue(new MockResponse().setResponseCode(200).setBody("{\n" + + " \"symbol_upload_id\": \"string\",\n" + + " \"upload_url\": \"string\",\n" + + " \"expiration_date\": \"2019-11-17T12:01:43.953Z\"\n" + + "}")); + + // When + final UploadRequest actual = task.execute(request).get(); + + // Then + assertThat(actual) + .isEqualTo(expected); } @Test public void should_ReturnResponse_When_RequestIsSuccessful_NonAsciiCharactersInFileName() throws Exception { - // TODO: Fix Me -// // Given -// final UploadRequest request = baseRequest.newBuilder().setAppName("åþþ ñåmë").build(); -// final UploadRequest expected = request.newBuilder().setUploadId("string").setUploadUrl("string").build(); -// mockWebServer.enqueue(new MockResponse().setResponseCode(201).setBody("{\n" + -// " \"upload_id\": \"string\",\n" + -// " \"upload_url\": \"string\",\n" + -// " \"asset_id\": \"string\",\n" + -// " \"asset_domain\": \"string\",\n" + -// " \"asset_token\": \"string\"\n" + -// "}")); -// -// // When -// final UploadRequest actual = task.execute(request).get(); -// -// // Then -// assertThat(actual) -// .isEqualTo(expected); + // Given + final UploadRequest request = baseRequest.newBuilder().setAppName("åþþ ñåmë").build(); + final UploadRequest expected = request.newBuilder().setUploadId("string").setUploadDomain("string").setToken("string").setPackageAssetId("string").build(); + mockWebServer.enqueue(new MockResponse().setResponseCode(201).setBody("{\n" + + " \"id\": \"string\",\n" + + " \"upload_domain\": \"string\",\n" + + " \"url_encoded_token\": \"string\",\n" + + " \"package_asset_id\": \"string\"\n" + + "}")); + + // When + final UploadRequest actual = task.execute(request).get(); + + // Then + assertThat(actual) + .isEqualTo(expected); } @Test From fd99e3012d4d8dacebd432a78e5af09aad1c61c4 Mon Sep 17 00:00:00 2001 From: Mez Pahlan Date: Sun, 13 Dec 2020 15:49:12 +0000 Subject: [PATCH 19/24] [JENKINS-64388] Fix test for UploadAppToResourceTaskTest --- .../task/internal/UploadAppToResourceTaskTest.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/test/java/io/jenkins/plugins/appcenter/task/internal/UploadAppToResourceTaskTest.java b/src/test/java/io/jenkins/plugins/appcenter/task/internal/UploadAppToResourceTaskTest.java index da93883..76c707e 100644 --- a/src/test/java/io/jenkins/plugins/appcenter/task/internal/UploadAppToResourceTaskTest.java +++ b/src/test/java/io/jenkins/plugins/appcenter/task/internal/UploadAppToResourceTaskTest.java @@ -55,7 +55,10 @@ public class UploadAppToResourceTaskTest { @Before public void setUp() { baseRequest = new UploadRequest.Builder() -// .setUploadUrl(mockWebServer.url("upload").toString()) // TODO: Fix Me + .setUploadDomain(mockWebServer.url("upload").toString()) + .setPackageAssetId("package-asset-id") + .setToken("token") + .setChunkSize(4098) .setUploadId("upload-id") .setPathToApp("three/days/xiola.apk") .build(); @@ -154,7 +157,7 @@ public void should_SendRequestToUploadUrl() throws Exception { // Then assertThat(recordedRequest.getPath()) - .isEqualTo("/upload"); + .isEqualTo("/upload/upload/upload_chunk/package-asset-id?token=token&block_number=1"); } @Test From 8895c9bb1abc4172be8a4e59f475b70de04147d6 Mon Sep 17 00:00:00 2001 From: Mez Pahlan Date: Sun, 13 Dec 2020 15:50:30 +0000 Subject: [PATCH 20/24] [JENKINS-64388] Remove ignore annotation --- .../appcenter/task/internal/CreateUploadResourceTaskTest.java | 2 -- .../appcenter/task/internal/UploadAppToResourceTaskTest.java | 2 -- 2 files changed, 4 deletions(-) diff --git a/src/test/java/io/jenkins/plugins/appcenter/task/internal/CreateUploadResourceTaskTest.java b/src/test/java/io/jenkins/plugins/appcenter/task/internal/CreateUploadResourceTaskTest.java index 00d34c3..86f6451 100644 --- a/src/test/java/io/jenkins/plugins/appcenter/task/internal/CreateUploadResourceTaskTest.java +++ b/src/test/java/io/jenkins/plugins/appcenter/task/internal/CreateUploadResourceTaskTest.java @@ -10,7 +10,6 @@ import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; import org.junit.Before; -import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.function.ThrowingRunnable; @@ -26,7 +25,6 @@ import static org.mockito.BDDMockito.given; @RunWith(MockitoJUnitRunner.class) -@Ignore public class CreateUploadResourceTaskTest { @Rule diff --git a/src/test/java/io/jenkins/plugins/appcenter/task/internal/UploadAppToResourceTaskTest.java b/src/test/java/io/jenkins/plugins/appcenter/task/internal/UploadAppToResourceTaskTest.java index 76c707e..88635fc 100644 --- a/src/test/java/io/jenkins/plugins/appcenter/task/internal/UploadAppToResourceTaskTest.java +++ b/src/test/java/io/jenkins/plugins/appcenter/task/internal/UploadAppToResourceTaskTest.java @@ -13,7 +13,6 @@ import okhttp3.mockwebserver.MockWebServer; import okhttp3.mockwebserver.RecordedRequest; import org.junit.Before; -import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.function.ThrowingRunnable; @@ -30,7 +29,6 @@ import static org.mockito.BDDMockito.given; @RunWith(MockitoJUnitRunner.class) -@Ignore public class UploadAppToResourceTaskTest { @Rule From 25f56ef2ff3290e1055742b6d36c538177bd0034 Mon Sep 17 00:00:00 2001 From: Mez Pahlan Date: Sun, 13 Dec 2020 16:23:32 +0000 Subject: [PATCH 21/24] [JENKINS-64388] Add OS check for file size So that this changeset passing on the CI server. --- .../jenkins/plugins/appcenter/util/RemoteFileUtilsTest.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/test/java/io/jenkins/plugins/appcenter/util/RemoteFileUtilsTest.java b/src/test/java/io/jenkins/plugins/appcenter/util/RemoteFileUtilsTest.java index ecc6ca0..c6d2256 100644 --- a/src/test/java/io/jenkins/plugins/appcenter/util/RemoteFileUtilsTest.java +++ b/src/test/java/io/jenkins/plugins/appcenter/util/RemoteFileUtilsTest.java @@ -1,6 +1,7 @@ package io.jenkins.plugins.appcenter.util; import hudson.FilePath; +import org.apache.commons.lang3.SystemUtils; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -62,7 +63,8 @@ public void should_ReturnFileSize_When_FileExists() { final long fileSize = remoteFileUtils.getFileSize(TEST_FILE_PATH); // Then - assertThat(fileSize).isEqualTo(41); + // Windows reports the file size differently so for the sake of simplicity we adjust our assertion here. + assertThat(fileSize).isEqualTo(SystemUtils.IS_OS_UNIX ? 41 : 42); } @Test From 780c3adec54150f099baaaeec283562aa9e84b34 Mon Sep 17 00:00:00 2001 From: Mez Pahlan Date: Sun, 20 Dec 2020 18:15:51 +0000 Subject: [PATCH 22/24] [JENKINS-64388] Read file in chunks more efficiently using Okio --- .../internal/UploadAppToResourceTask.java | 27 +++++++------------ 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/src/main/java/io/jenkins/plugins/appcenter/task/internal/UploadAppToResourceTask.java b/src/main/java/io/jenkins/plugins/appcenter/task/internal/UploadAppToResourceTask.java index b0172f1..00958c6 100644 --- a/src/main/java/io/jenkins/plugins/appcenter/task/internal/UploadAppToResourceTask.java +++ b/src/main/java/io/jenkins/plugins/appcenter/task/internal/UploadAppToResourceTask.java @@ -7,7 +7,9 @@ import io.jenkins.plugins.appcenter.task.request.UploadRequest; import io.jenkins.plugins.appcenter.util.RemoteFileUtils; import okhttp3.RequestBody; -import org.apache.commons.io.FileUtils; +import okio.Buffer; +import okio.BufferedSource; +import okio.Okio; import javax.annotation.Nonnull; import javax.inject.Inject; @@ -75,22 +77,13 @@ private void calculateChunks(@Nonnull UploadRequest request, int offset, int chu final String url = getUrl(request); final File file = remoteFileUtils.getRemoteFile(request.pathToApp); - try { - final byte[] bytes = FileUtils.readFileToByteArray(file); - final int size = bytes.length; - final int remainingSize = size - offset; - final boolean canChunk = remainingSize > chunkSize; - - int byteCount; - if (canChunk) { - // We can safely take a whole chunk from what remains given the offset. - byteCount = chunkSize; - } else { - // We can only safely take what remains and not the full chunk given the offset. - byteCount = remainingSize; - } - final RequestBody requestFile = RequestBody.create(bytes, null, offset, byteCount); - upload(request, offset, chunkSize, url, requestFile, blockNumber, future, !canChunk); + try (final BufferedSource bufferedSource = Okio.buffer(Okio.source(file))) { + bufferedSource.skip(offset); + final Buffer buffer = new Buffer(); + final boolean isFinal = !bufferedSource.request(chunkSize); + bufferedSource.read(buffer, chunkSize); + final RequestBody requestFile = RequestBody.create(buffer.readByteArray(), null); + upload(request, offset, chunkSize, url, requestFile, blockNumber, future, isFinal); } catch (IOException e) { final AppCenterException exception = logFailure("Upload app to resource unsuccessful", e); future.completeExceptionally(exception); From 6fe3f96b63c513345ac703d125e2d165d1abcedb Mon Sep 17 00:00:00 2001 From: Mez Pahlan Date: Sun, 20 Dec 2020 22:56:03 +0000 Subject: [PATCH 23/24] [JENKINS-64388] Implement an exponential backoff for polling. --- .../task/internal/PollForReleaseTask.java | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/main/java/io/jenkins/plugins/appcenter/task/internal/PollForReleaseTask.java b/src/main/java/io/jenkins/plugins/appcenter/task/internal/PollForReleaseTask.java index 9e344c0..4b9866b 100644 --- a/src/main/java/io/jenkins/plugins/appcenter/task/internal/PollForReleaseTask.java +++ b/src/main/java/io/jenkins/plugins/appcenter/task/internal/PollForReleaseTask.java @@ -52,6 +52,10 @@ private CompletableFuture pollForRelease(@Nonnull UploadRequest r } private void poll(@Nonnull UploadRequest request, @Nonnull String uploadId, @Nonnull CompletableFuture future) { + poll(request, uploadId, future, 0); + } + + private void poll(@Nonnull UploadRequest request, @Nonnull String uploadId, @Nonnull CompletableFuture future, int timeoutExponent) { factory.createAppCenterService() .pollForRelease(request.ownerName, request.appName, uploadId) .whenComplete((pollForReleaseResponse, throwable) -> { @@ -62,8 +66,7 @@ private void poll(@Nonnull UploadRequest request, @Nonnull String uploadId, @Non switch (pollForReleaseResponse.upload_status) { case uploadStarted: case uploadFinished: - log("Polling for app release successful however not yet ready will try again."); - retryPolling(request, uploadId, future); + retryPolling(request, uploadId, future, timeoutExponent); break; case readyToBePublished: log("Polling for app release successful."); @@ -83,10 +86,13 @@ private void poll(@Nonnull UploadRequest request, @Nonnull String uploadId, @Non }); } - private void retryPolling(@Nonnull UploadRequest request, @Nonnull String uploadId, @Nonnull CompletableFuture future) { + private void retryPolling(@Nonnull UploadRequest request, @Nonnull String uploadId, @Nonnull CompletableFuture future, int timeoutExponent) { try { - TimeUnit.SECONDS.sleep(1L); - poll(request, uploadId, future); + final double pow = Math.pow(2, timeoutExponent); + final long timeout = Double.valueOf(pow).longValue(); + log(String.format("Polling for app release successful however not yet ready will try again in %d seconds.", timeout)); + TimeUnit.SECONDS.sleep(timeout); + poll(request, uploadId, future, timeoutExponent + 1); } catch (InterruptedException e) { e.printStackTrace(); } From 38e2033cbe629dda40553df7e6392a7f8691a893 Mon Sep 17 00:00:00 2001 From: Mez Pahlan Date: Sun, 20 Dec 2020 23:32:22 +0000 Subject: [PATCH 24/24] [JENKINS-64388] Add count of number of blocks to log --- .../task/internal/UploadAppToResourceTask.java | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/main/java/io/jenkins/plugins/appcenter/task/internal/UploadAppToResourceTask.java b/src/main/java/io/jenkins/plugins/appcenter/task/internal/UploadAppToResourceTask.java index 00958c6..a9ce5b2c 100644 --- a/src/main/java/io/jenkins/plugins/appcenter/task/internal/UploadAppToResourceTask.java +++ b/src/main/java/io/jenkins/plugins/appcenter/task/internal/UploadAppToResourceTask.java @@ -76,21 +76,23 @@ private void calculateChunks(@Nonnull UploadRequest request, int offset, int chu // TODO: Retrofit (via OkHttp) is supposed to be able to do this natively if you set the contentLength to -1. Investigate final String url = getUrl(request); final File file = remoteFileUtils.getRemoteFile(request.pathToApp); + final long fileSize = file.length(); + final int noOfBlocks = (int) Math.ceil((double) fileSize / chunkSize); try (final BufferedSource bufferedSource = Okio.buffer(Okio.source(file))) { bufferedSource.skip(offset); final Buffer buffer = new Buffer(); - final boolean isFinal = !bufferedSource.request(chunkSize); + bufferedSource.request(chunkSize); bufferedSource.read(buffer, chunkSize); final RequestBody requestFile = RequestBody.create(buffer.readByteArray(), null); - upload(request, offset, chunkSize, url, requestFile, blockNumber, future, isFinal); + upload(request, offset, chunkSize, url, requestFile, blockNumber, noOfBlocks, future); } catch (IOException e) { final AppCenterException exception = logFailure("Upload app to resource unsuccessful", e); future.completeExceptionally(exception); } } - private void upload(@Nonnull UploadRequest request, int offset, int chunkSize, @Nonnull String url, @Nonnull RequestBody requestFile, int blockNumber, @Nonnull CompletableFuture future, boolean isFinal) { + private void upload(@Nonnull UploadRequest request, int offset, int chunkSize, @Nonnull String url, @Nonnull RequestBody requestFile, int blockNumber, int noOfBlocks, @Nonnull CompletableFuture future) { factory.createAppCenterService() .uploadApp(url + "&block_number=" + blockNumber, requestFile) .whenComplete((responseBody, throwable) -> { @@ -98,14 +100,13 @@ private void upload(@Nonnull UploadRequest request, int offset, int chunkSize, @ final AppCenterException exception = logFailure("Upload app to resource unsuccessful", throwable); future.completeExceptionally(exception); } else { - if (isFinal) { - log(String.format("Upload app to resource chunk %1$d and final successful.", blockNumber)); + log(String.format("Upload app to resource chunk %1$d / %2$d successful.", blockNumber, noOfBlocks)); + if (blockNumber == noOfBlocks) { future.complete(request); - return; + } else { + calculateChunks(request, offset + chunkSize, chunkSize, blockNumber + 1, future); } - log(String.format("Upload app to resource chunk %1$d successful.", blockNumber)); - calculateChunks(request, offset + chunkSize, chunkSize, blockNumber + 1, future); } }); }