From e0d19ee23b9f409879147a815b9dc23a0e3587f9 Mon Sep 17 00:00:00 2001 From: Felix Angelov Date: Fri, 2 Jun 2023 13:48:55 -0700 Subject: [PATCH] feat(code_push_client): adjust create artifact APIs to use `uploadUrl` (#586) --- .../example/main.dart | 4 +- .../lib/src/code_push_client.dart | 36 ++++- .../test/src/code_push_client_test.dart | 152 ++++++++++++++---- .../lib/src/messages/messages.dart | 2 +- 4 files changed, 153 insertions(+), 41 deletions(-) diff --git a/packages/shorebird_code_push_client/example/main.dart b/packages/shorebird_code_push_client/example/main.dart index 31a6d768..ddac510f 100644 --- a/packages/shorebird_code_push_client/example/main.dart +++ b/packages/shorebird_code_push_client/example/main.dart @@ -29,7 +29,7 @@ Future main() async { ); // Create a release artifact. - final releaseArtifact = await client.createReleaseArtifact( + await client.createReleaseArtifact( releaseId: release.id, artifactPath: '', // e.g. 'libapp.so' platform: '', // e.g. 'android' @@ -41,7 +41,7 @@ Future main() async { final patch = await client.createPatch(releaseId: release.id); // Create a patch artifact. - final patchArtifact = await client.createPatchArtifact( + await client.createPatchArtifact( patchId: patch.id, artifactPath: '', // e.g. 'libapp.so' platform: '', // e.g. 'android' diff --git a/packages/shorebird_code_push_client/lib/src/code_push_client.dart b/packages/shorebird_code_push_client/lib/src/code_push_client.dart index 41e3d356..525f98c3 100644 --- a/packages/shorebird_code_push_client/lib/src/code_push_client.dart +++ b/packages/shorebird_code_push_client/lib/src/code_push_client.dart @@ -82,7 +82,7 @@ class CodePushClient { } /// Create a new artifact for a specific [patchId]. - Future createPatchArtifact({ + Future createPatchArtifact({ required String artifactPath, required int patchId, required String arch, @@ -94,7 +94,6 @@ class CodePushClient { Uri.parse('$_v1/patches/$patchId/artifacts'), ); final file = await http.MultipartFile.fromPath('file', artifactPath); - request.files.add(file); request.fields.addAll({ 'arch': arch, 'platform': platform, @@ -108,7 +107,20 @@ class CodePushClient { throw _parseErrorResponse(response.statusCode, body); } - return PatchArtifact.fromJson(json.decode(body) as Map); + final decoded = CreatePatchArtifactResponse.fromJson( + json.decode(body) as Map, + ); + + final uploadResponse = await _httpClient.put( + Uri.parse(decoded.uploadUrl), + body: File(artifactPath).readAsBytesSync(), + ); + if (uploadResponse.statusCode != HttpStatus.ok) { + throw CodePushException( + message: + '''Failed to upload artifact (${uploadResponse.reasonPhrase} '${uploadResponse.statusCode})''', + ); + } } /// Generates a Stripe payment link for the current user. @@ -127,7 +139,7 @@ class CodePushClient { } /// Create a new artifact for a specific [releaseId]. - Future createReleaseArtifact({ + Future createReleaseArtifact({ required String artifactPath, required int releaseId, required String arch, @@ -139,7 +151,6 @@ class CodePushClient { Uri.parse('$_v1/releases/$releaseId/artifacts'), ); final file = await http.MultipartFile.fromPath('file', artifactPath); - request.files.add(file); request.fields.addAll({ 'arch': arch, 'platform': platform, @@ -153,7 +164,20 @@ class CodePushClient { throw _parseErrorResponse(response.statusCode, body); } - return ReleaseArtifact.fromJson(json.decode(body) as Map); + final decoded = CreateReleaseArtifactResponse.fromJson( + json.decode(body) as Map, + ); + + final uploadResponse = await _httpClient.put( + Uri.parse(decoded.uploadUrl), + body: File(artifactPath).readAsBytesSync(), + ); + if (uploadResponse.statusCode != HttpStatus.ok) { + throw CodePushException( + message: + '''Failed to upload artifact (${uploadResponse.reasonPhrase} '${uploadResponse.statusCode})''', + ); + } } /// Create a new app with the provided [displayName]. diff --git a/packages/shorebird_code_push_client/test/src/code_push_client_test.dart b/packages/shorebird_code_push_client/test/src/code_push_client_test.dart index 5bab712c..943fc1ff 100644 --- a/packages/shorebird_code_push_client/test/src/code_push_client_test.dart +++ b/packages/shorebird_code_push_client/test/src/code_push_client_test.dart @@ -244,22 +244,19 @@ void main() { ); }); - test('completes when request succeeds', () async { - const artifactId = 0; - const artifactUrl = 'https://example.com/artifact.zip'; + test('throws an exception if the upload fails', () async { + const uploadUrl = 'https://example.com/artifact.zip'; when(() => httpClient.send(any())).thenAnswer((_) async { return http.StreamedResponse( Stream.value( utf8.encode( json.encode( - PatchArtifact( - id: artifactId, - url: artifactUrl, - patchId: patchId, + CreatePatchArtifactResponse( arch: arch, platform: platform, hash: hash, size: size, + uploadUrl: uploadUrl, ), ), ), @@ -268,6 +265,10 @@ void main() { ); }); + when( + () => httpClient.put(any(), body: any(named: 'body')), + ).thenAnswer((_) async => http.Response('', HttpStatus.badRequest)); + final tempDir = Directory.systemTemp.createTempSync(); final fixture = File(path.join(tempDir.path, 'release.txt')) ..createSync(); @@ -280,18 +281,61 @@ void main() { platform: platform, hash: hash, ), - completion( - equals( - isA() - .having((a) => a.id, 'id', artifactId) - .having((a) => a.patchId, 'patchId', patchId) - .having((a) => a.arch, 'arch', arch) - .having((a) => a.platform, 'platform', platform) - .having((a) => a.hash, 'hash', hash) - .having((a) => a.url, 'artifactUrl', artifactUrl), + throwsA( + isA().having( + (e) => e.message, + 'message', + contains('Failed to upload artifact'), ), ), ); + verify( + () => httpClient.put( + Uri.parse(uploadUrl), + body: fixture.readAsBytesSync(), + ), + ).called(1); + }); + + test('completes when request succeeds', () async { + const uploadUrl = 'https://example.com/artifact.zip'; + when(() => httpClient.send(any())).thenAnswer((_) async { + return http.StreamedResponse( + Stream.value( + utf8.encode( + json.encode( + CreatePatchArtifactResponse( + arch: arch, + platform: platform, + hash: hash, + size: size, + uploadUrl: uploadUrl, + ), + ), + ), + ), + HttpStatus.ok, + ); + }); + + when( + () => httpClient.put(any(), body: any(named: 'body')), + ).thenAnswer((_) async => http.Response('', HttpStatus.ok)); + + final tempDir = Directory.systemTemp.createTempSync(); + final fixture = File(path.join(tempDir.path, 'release.txt')) + ..createSync(); + + await expectLater( + codePushClient.createPatchArtifact( + artifactPath: fixture.path, + patchId: patchId, + arch: arch, + platform: platform, + hash: hash, + ), + completes, + ); final request = verify(() => httpClient.send(captureAny())) .captured @@ -442,22 +486,19 @@ void main() { ); }); - test('completes when request succeeds', () async { - const artifactId = 0; - const artifactUrl = 'https://example.com/artifact.zip'; + test('throws an exception if the upload fails', () async { + const uploadUrl = 'https://example.com/artifact.zip'; when(() => httpClient.send(any())).thenAnswer((_) async { return http.StreamedResponse( Stream.value( utf8.encode( json.encode( - ReleaseArtifact( - id: artifactId, - url: artifactUrl, - releaseId: releaseId, + CreateReleaseArtifactResponse( arch: arch, platform: platform, hash: hash, size: size, + uploadUrl: uploadUrl, ), ), ), @@ -466,6 +507,10 @@ void main() { ); }); + when( + () => httpClient.put(any(), body: any(named: 'body')), + ).thenAnswer((_) async => http.Response('', HttpStatus.badRequest)); + final tempDir = Directory.systemTemp.createTempSync(); final fixture = File(path.join(tempDir.path, 'release.txt')) ..createSync(); @@ -478,18 +523,61 @@ void main() { platform: platform, hash: hash, ), - completion( - equals( - isA() - .having((a) => a.id, 'id', artifactId) - .having((a) => a.releaseId, 'releaseId', releaseId) - .having((a) => a.arch, 'arch', arch) - .having((a) => a.platform, 'platform', platform) - .having((a) => a.hash, 'hash', hash) - .having((a) => a.url, 'artifactUrl', artifactUrl), + throwsA( + isA().having( + (e) => e.message, + 'message', + contains('Failed to upload artifact'), ), ), ); + verify( + () => httpClient.put( + Uri.parse(uploadUrl), + body: fixture.readAsBytesSync(), + ), + ).called(1); + }); + + test('completes when request succeeds', () async { + const uploadUrl = 'https://example.com/artifact.zip'; + when(() => httpClient.send(any())).thenAnswer((_) async { + return http.StreamedResponse( + Stream.value( + utf8.encode( + json.encode( + CreateReleaseArtifactResponse( + arch: arch, + platform: platform, + hash: hash, + size: size, + uploadUrl: uploadUrl, + ), + ), + ), + ), + HttpStatus.ok, + ); + }); + + when( + () => httpClient.put(any(), body: any(named: 'body')), + ).thenAnswer((_) async => http.Response('', HttpStatus.ok)); + + final tempDir = Directory.systemTemp.createTempSync(); + final fixture = File(path.join(tempDir.path, 'release.txt')) + ..createSync(); + + await expectLater( + codePushClient.createReleaseArtifact( + artifactPath: fixture.path, + releaseId: releaseId, + arch: arch, + platform: platform, + hash: hash, + ), + completes, + ); final request = verify(() => httpClient.send(captureAny())) .captured diff --git a/packages/shorebird_code_push_protocol/lib/src/messages/messages.dart b/packages/shorebird_code_push_protocol/lib/src/messages/messages.dart index ab2c2b12..e091b7f9 100644 --- a/packages/shorebird_code_push_protocol/lib/src/messages/messages.dart +++ b/packages/shorebird_code_push_protocol/lib/src/messages/messages.dart @@ -2,9 +2,9 @@ export 'cancel_subscription/cancel_subscription_response.dart'; export 'check_for_patches/check_for_patches.dart'; export 'create_app/create_app.dart'; export 'create_app_collaborator/create_app_collaborator.dart'; -export 'create_patch_artifact/create_patch_artifact.dart'; export 'create_channel/create_channel.dart'; export 'create_patch/create_patch.dart'; +export 'create_patch_artifact/create_patch_artifact.dart'; export 'create_payment_link/create_payment_link.dart'; export 'create_release/create_release.dart'; export 'create_release_artifact/create_release_artifact.dart';