From dcec738667f35f1f3131da49fa2f97288dae05b8 Mon Sep 17 00:00:00 2001 From: Felix Angelov Date: Tue, 28 Mar 2023 13:14:07 -0500 Subject: [PATCH] feat(code_push_client): add `createReleaseArtifact` (#185) --- .../example/main.dart | 9 ++ .../lib/src/code_push_client.dart | 29 ++++ .../test/src/code_push_client_test.dart | 129 ++++++++++++++++++ 3 files changed, 167 insertions(+) diff --git a/packages/shorebird_code_push_client/example/main.dart b/packages/shorebird_code_push_client/example/main.dart index 762e5610..3b69ec42 100644 --- a/packages/shorebird_code_push_client/example/main.dart +++ b/packages/shorebird_code_push_client/example/main.dart @@ -31,6 +31,15 @@ Future main() async { displayName: '', // e.g. 'v1.0.0' ); + // Create a release artifact. + final releaseArtifact = await client.createReleaseArtifact( + releaseId: release.id, + artifactPath: '', // e.g. 'libapp.so' + platform: '', // e.g. 'android' + arch: '', // e.g. 'aarch64' + hash: '', // 'sha256 hash of the artifact' + ); + // Create a new patch. final patch = await client.createPatch(releaseId: release.id); 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 a4a6835b..d6c6f758 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 @@ -75,6 +75,35 @@ class CodePushClient { return PatchArtifact.fromJson(json.decode(body) as Map); } + /// Create a new artifact for a specific [releaseId]. + Future createReleaseArtifact({ + required String artifactPath, + required int releaseId, + required String arch, + required String platform, + required String hash, + }) async { + final request = http.MultipartRequest( + 'POST', + Uri.parse('$hostedUri/api/v1/releases/$releaseId/artifacts'), + ); + final file = await http.MultipartFile.fromPath('file', artifactPath); + request.files.add(file); + request.fields.addAll({ + 'arch': arch, + 'platform': platform, + 'hash': hash, + 'size': '${file.length}', + }); + request.headers.addAll(_apiKeyHeader); + final response = await _httpClient.send(request); + final body = await response.stream.bytesToString(); + + if (response.statusCode != HttpStatus.ok) throw _parseErrorResponse(body); + + return ReleaseArtifact.fromJson(json.decode(body) as Map); + } + /// Create a new app with the provided [displayName]. /// Returns the newly created app. Future createApp({required String displayName}) async { 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 e1f1b9b9..caa2ab01 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 @@ -185,6 +185,135 @@ void main() { }); }); + group('createReleaseArtifact', () { + const releaseId = 0; + const arch = 'aarch64'; + const platform = 'android'; + const hash = 'test-hash'; + const size = 42; + + test('throws an exception if the http request fails (unknown)', () async { + when(() => httpClient.send(any())).thenAnswer((_) async { + return http.StreamedResponse( + Stream.empty(), + HttpStatus.failedDependency, + ); + }); + + final tempDir = Directory.systemTemp.createTempSync(); + final fixture = File(path.join(tempDir.path, 'release.txt')) + ..createSync(); + + expect( + codePushClient.createReleaseArtifact( + artifactPath: fixture.path, + releaseId: releaseId, + arch: arch, + platform: platform, + hash: hash, + ), + throwsA( + isA().having( + (e) => e.message, + 'message', + CodePushClient.unknownErrorMessage, + ), + ), + ); + }); + + test('throws an exception if the http request fails', () async { + when(() => httpClient.send(any())).thenAnswer((_) async { + return http.StreamedResponse( + Stream.value(utf8.encode(json.encode(errorResponse.toJson()))), + HttpStatus.failedDependency, + ); + }); + + final tempDir = Directory.systemTemp.createTempSync(); + final fixture = File(path.join(tempDir.path, 'release.txt')) + ..createSync(); + + expect( + codePushClient.createReleaseArtifact( + artifactPath: fixture.path, + releaseId: releaseId, + arch: arch, + platform: platform, + hash: hash, + ), + throwsA( + isA().having( + (e) => e.message, + 'message', + errorResponse.message, + ), + ), + ); + }); + + test('completes when request succeeds', () async { + const artifactId = 0; + const artifactUrl = '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, + arch: arch, + platform: platform, + hash: hash, + size: size, + ), + ), + ), + ), + 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, + ), + 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), + ), + ), + ); + + final request = verify(() => httpClient.send(captureAny())) + .captured + .single as http.MultipartRequest; + + expect( + request.url, + codePushClient.hostedUri.replace( + path: '/api/v1/releases/$releaseId/artifacts', + ), + ); + }); + }); + group('createApp', () { test('throws an exception if the http request fails (unknown)', () async { when(