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..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,31 +1,44 @@ 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 okhttp3.RequestBody; +import okhttp3.ResponseBody; +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, @Body @Nonnull ReleaseUploadBeginRequest releaseUploadBeginRequest); + @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, 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/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/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/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 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..b330f3f 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,17 +14,31 @@ public final class UploadTask extends MasterToSlaveCallable { if (throwable != null) { 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/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/internal/FinishReleaseTask.java b/src/main/java/io/jenkins/plugins/appcenter/task/internal/FinishReleaseTask.java new file mode 100644 index 0000000..95e1436 --- /dev/null +++ b/src/main/java/io/jenkins/plugins/appcenter/task/internal/FinishReleaseTask.java @@ -0,0 +1,76 @@ +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 static java.util.Objects.requireNonNull; + +@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) { + 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(uploadDomain, packageAssetId, token); + + 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 String uploadDomain, @Nonnull String packageAssetId, @Nonnull String token) { + return String.format("%1$s/upload/finished/%2$s?token=%3$s", uploadDomain, packageAssetId, 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..4b9866b --- /dev/null +++ b/src/main/java/io/jenkins/plugins/appcenter/task/internal/PollForReleaseTask.java @@ -0,0 +1,105 @@ +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) { + 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) -> { + 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: + retryPolling(request, uploadId, future, timeoutExponent); + 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, int timeoutExponent) { + try { + 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(); + } + } + + @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/SetMetadataTask.java b/src/main/java/io/jenkins/plugins/appcenter/task/internal/SetMetadataTask.java new file mode 100644 index 0000000..2d49112 --- /dev/null +++ b/src/main/java/io/jenkins/plugins/appcenter/task/internal/SetMetadataTask.java @@ -0,0 +1,90 @@ +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 io.jenkins.plugins.appcenter.util.RemoteFileUtils; + +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 SetMetadataTask implements AppCenterTask, AppCenterLogger { + + private static final long serialVersionUID = 1L; + + @Nonnull + 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 RemoteFileUtils remoteFileUtils) { + this.taskListener = taskListener; + this.factory = factory; + this.remoteFileUtils = remoteFileUtils; + } + + @Nonnull + @Override + public CompletableFuture execute(@Nonnull UploadRequest request) { + return setMetadata(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.pathToApp, uploadDomain, packageAssetId, token); + + 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 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); + + 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 + 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 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..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 @@ -6,14 +6,16 @@ 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 okio.Buffer; +import okio.BufferedSource; +import okio.Okio; 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 +56,64 @@ 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); + 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(); + bufferedSource.request(chunkSize); + bufferedSource.read(buffer, chunkSize); + final RequestBody requestFile = RequestBody.create(buffer.readByteArray(), null); + 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, int noOfBlocks, @Nonnull CompletableFuture future) { + 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); + log(String.format("Upload app to resource chunk %1$d / %2$d successful.", blockNumber, noOfBlocks)); + if (blockNumber == noOfBlocks) { + future.complete(request); + } else { + 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 +154,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) 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..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; @@ -195,16 +198,19 @@ 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; 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; this.symbolUploadId = uploadRequest.symbolUploadId; - this.commitHash = uploadRequest.commitHash; - this.branchName = uploadRequest.branchName; } public Builder setOwnerName(@Nonnull String ownerName) { @@ -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; } 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/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/task/internal/CreateUploadResourceTaskTest.java b/src/test/java/io/jenkins/plugins/appcenter/task/internal/CreateUploadResourceTaskTest.java index 6183916..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 @@ -57,13 +57,12 @@ public void setUp() { @Test public void should_ReturnResponse_When_RequestIsSuccessful() throws Exception { // Given - final UploadRequest expected = baseRequest.newBuilder().setUploadId("string").setUploadUrl("string").build(); + final UploadRequest expected = baseRequest.newBuilder().setUploadId("string").setUploadDomain("string").setToken("string").setPackageAssetId("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" + + " \"id\": \"string\",\n" + + " \"upload_domain\": \"string\",\n" + + " \"url_encoded_token\": \"string\",\n" + + " \"package_asset_id\": \"string\"\n" + "}")); // When @@ -81,16 +80,12 @@ public void should_ReturnResponse_When_DebugSymbolsAreFound() throws Exception { .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(); + 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" + - " \"upload_id\": \"string\",\n" + - " \"upload_url\": \"string\",\n" + - " \"asset_id\": \"string\",\n" + - " \"asset_domain\": \"string\",\n" + - " \"asset_token\": \"string\"\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" + @@ -110,13 +105,12 @@ public void should_ReturnResponse_When_DebugSymbolsAreFound() throws Exception { 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(); + final UploadRequest expected = request.newBuilder().setUploadId("string").setUploadDomain("string").setToken("string").setPackageAssetId("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" + + " \"id\": \"string\",\n" + + " \"upload_domain\": \"string\",\n" + + " \"url_encoded_token\": \"string\",\n" + + " \"package_asset_id\": \"string\"\n" + "}")); // When diff --git a/src/test/java/io/jenkins/plugins/appcenter/task/internal/CommitUploadResourceTaskTest.java b/src/test/java/io/jenkins/plugins/appcenter/task/internal/FinishReleaseTaskTest.java similarity index 50% rename from src/test/java/io/jenkins/plugins/appcenter/task/internal/CommitUploadResourceTaskTest.java rename to src/test/java/io/jenkins/plugins/appcenter/task/internal/FinishReleaseTaskTest.java index 77d70a0..5b00c9e 100644 --- a/src/test/java/io/jenkins/plugins/appcenter/task/internal/CommitUploadResourceTaskTest.java +++ b/src/test/java/io/jenkins/plugins/appcenter/task/internal/FinishReleaseTaskTest.java @@ -5,7 +5,6 @@ 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; @@ -25,7 +24,7 @@ import static org.mockito.BDDMockito.given; @RunWith(MockitoJUnitRunner.class) -public class CommitUploadResourceTaskTest { +public class FinishReleaseTaskTest { @Rule public MockWebServer mockWebServer = new MockWebServer(); @@ -41,91 +40,98 @@ public class CommitUploadResourceTaskTest { private UploadRequest baseRequest; - private CommitUploadResourceTask task; + private FinishReleaseTask 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); + task = new FinishReleaseTask(mockTaskListener, factory); } @Test - public void should_ReturnResponse_When_RequestIsSuccessful() throws Exception { + public void should_ReturnException_When_UploadDomainIsMissing() { // 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" + - "}")); + final UploadRequest uploadRequest = baseRequest.newBuilder() + .build(); // When - final UploadRequest actual = task.execute(baseRequest).get(); + final ThrowingRunnable throwingRunnable = () -> task.execute(uploadRequest).get(); // Then - assertThat(actual) - .isEqualTo(expected); + 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_ReturnResponse_When_DebugSymbolsAreFound() throws Exception { + public void should_ReturnException_When_TokenIsMissing() { // 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") + final UploadRequest uploadRequest = baseRequest.newBuilder() + .setUploadDomain("upload-domain") + .setPackageAssetId("package_asset_id") .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(); + 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(expected); + .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(baseRequest).get(); + 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("Committing app resource unsuccessful: HTTP 400 Client Error: "); + assertThat(exception).hasCauseThat().hasMessageThat().contains("Finishing release unsuccessful: HTTP 400 Client Error: "); } } \ No newline at end of file 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 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 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 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..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 @@ -53,7 +53,10 @@ public class UploadAppToResourceTaskTest { @Before public void setUp() { baseRequest = new UploadRequest.Builder() - .setUploadUrl(mockWebServer.url("upload").toString()) + .setUploadDomain(mockWebServer.url("upload").toString()) + .setPackageAssetId("package-asset-id") + .setToken("token") + .setChunkSize(4098) .setUploadId("upload-id") .setPathToApp("three/days/xiola.apk") .build(); @@ -152,7 +155,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 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" + "}")); 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..c6d2256 --- /dev/null +++ b/src/test/java/io/jenkins/plugins/appcenter/util/RemoteFileUtilsTest.java @@ -0,0 +1,261 @@ +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; +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 + // 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 + 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.