diff --git a/.github/workflows/shorebird_code_push_api.yaml b/.github/workflows/shorebird_code_push_api.yaml index 52d4d726..7fdbd69a 100644 --- a/.github/workflows/shorebird_code_push_api.yaml +++ b/.github/workflows/shorebird_code_push_api.yaml @@ -20,6 +20,11 @@ on: - "packages/shorebird_code_push_api/test/**" - "packages/shorebird_code_push_api/pubspec.yaml" +env: + PROJECT_ID: shorebird-code-push-api + SERVICE: shorebird-code-push-api + REGION: us-central1 + jobs: semantic-pull-request: uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/semantic_pull_request.yml@v1 @@ -33,3 +38,46 @@ jobs: uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/pana.yml@v1 with: working_directory: packages/shorebird_code_push_api + + deploy: + needs: build + + defaults: + run: + working-directory: packages/shorebird_code_push_api + + runs-on: ubuntu-latest + + if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} + + steps: + - uses: actions/checkout@v2 + + - name: Setup Cloud SDK + uses: google-github-actions/setup-gcloud@v0.2.0 + with: + project_id: ${{ env.PROJECT_ID }} + service_account_key: ${{ secrets.CLOUD_RUN_SA }} + export_default_credentials: true + + - name: Authorize Docker Push + run: gcloud auth configure-docker + + - name: Build and Push Container + run: |- + docker build -t gcr.io/${{ env.PROJECT_ID }}/${{ env.SERVICE }}:${{ github.sha }} . + docker push gcr.io/${{ env.PROJECT_ID }}/${{ env.SERVICE }}:${{ github.sha }} + + - name: Deploy to Cloud Run + id: deploy + uses: google-github-actions/deploy-cloudrun@v0.4.0 + with: + service: ${{ env.SERVICE }} + image: gcr.io/${{ env.PROJECT_ID }}/${{ env.SERVICE }}:${{ github.sha }} + region: ${{ env.REGION }} + + - name: Show Output + run: echo ${{ steps.deploy.outputs.url }} + + - name: Ping + run: curl "${{ steps.deploy.outputs.url }}" diff --git a/packages/shorebird_code_push_api/.gitignore b/packages/shorebird_code_push_api/.gitignore index 526da158..a4385834 100644 --- a/packages/shorebird_code_push_api/.gitignore +++ b/packages/shorebird_code_push_api/.gitignore @@ -4,4 +4,6 @@ .dart_tool/ .packages build/ -pubspec.lock \ No newline at end of file +pubspec.lock +coverage/ +cache/ \ No newline at end of file diff --git a/packages/shorebird_code_push_api/analysis_options.yaml b/packages/shorebird_code_push_api/analysis_options.yaml index 84e34fba..d767e5d3 100644 --- a/packages/shorebird_code_push_api/analysis_options.yaml +++ b/packages/shorebird_code_push_api/analysis_options.yaml @@ -1 +1,4 @@ include: package:very_good_analysis/analysis_options.4.0.0.yaml +linter: + rules: + public_member_api_docs: false diff --git a/packages/shorebird_code_push_api/bin/server.dart b/packages/shorebird_code_push_api/bin/server.dart index 1fa9db67..46548c45 100644 --- a/packages/shorebird_code_push_api/bin/server.dart +++ b/packages/shorebird_code_push_api/bin/server.dart @@ -3,15 +3,24 @@ import 'dart:io'; import 'package:shelf/shelf.dart'; import 'package:shelf/shelf_io.dart' as shelf_io; import 'package:shelf_router/shelf_router.dart' as shelf_router; +import 'package:shorebird_code_push_api/src/middleware/middleware.dart'; +import 'package:shorebird_code_push_api/src/routes/routes.dart'; Future main() async { final port = int.parse(Platform.environment['PORT'] ?? '8080'); final router = shelf_router.Router() - ..all('/', (_) => Response.ok('Hello, world!')); + ..all('/', (_) => Response(HttpStatus.noContent)) + ..post('/api/v1/updates', checkForUpdatesHandler) + ..get('/api/v1/releases/', downloadReleaseHandler) + ..post('/api/v1/releases', uploadReleaseHandler); + + final handler = const Pipeline() + .addMiddleware(versionStoreProvider) + .addHandler(router.call); final server = await shelf_io.serve( - logRequests().addHandler(router.call), - InternetAddress.anyIPv6, + logRequests().addHandler(handler), + InternetAddress.anyIPv4, port, ); diff --git a/packages/shorebird_code_push_api/lib/shorebird_code_push_api.dart b/packages/shorebird_code_push_api/lib/shorebird_code_push_api.dart deleted file mode 100644 index 17127c20..00000000 --- a/packages/shorebird_code_push_api/lib/shorebird_code_push_api.dart +++ /dev/null @@ -1,4 +0,0 @@ -/// The Shorebird CodePush API -library shorebird_code_push_api; - -export 'src/shorebird_code_push_api.dart'; diff --git a/packages/shorebird_code_push_api/lib/src/config.dart b/packages/shorebird_code_push_api/lib/src/config.dart new file mode 100644 index 00000000..bf28939f --- /dev/null +++ b/packages/shorebird_code_push_api/lib/src/config.dart @@ -0,0 +1 @@ +const cachePath = 'cache'; diff --git a/packages/shorebird_code_push_api/lib/src/middleware/middleware.dart b/packages/shorebird_code_push_api/lib/src/middleware/middleware.dart new file mode 100644 index 00000000..036aa574 --- /dev/null +++ b/packages/shorebird_code_push_api/lib/src/middleware/middleware.dart @@ -0,0 +1 @@ +export 'version_store_provider.dart'; diff --git a/packages/shorebird_code_push_api/lib/src/middleware/version_store_provider.dart b/packages/shorebird_code_push_api/lib/src/middleware/version_store_provider.dart new file mode 100644 index 00000000..50f52335 --- /dev/null +++ b/packages/shorebird_code_push_api/lib/src/middleware/version_store_provider.dart @@ -0,0 +1,7 @@ +import 'package:shorebird_code_push_api/src/config.dart' as config; +import 'package:shorebird_code_push_api/src/provider.dart'; +import 'package:shorebird_code_push_api/src/version_store.dart'; + +const _versionStore = VersionStore(cachePath: config.cachePath); + +final versionStoreProvider = provider((_) => _versionStore); diff --git a/packages/shorebird_code_push_api/lib/src/provider.dart b/packages/shorebird_code_push_api/lib/src/provider.dart new file mode 100644 index 00000000..3ff5c799 --- /dev/null +++ b/packages/shorebird_code_push_api/lib/src/provider.dart @@ -0,0 +1,25 @@ +import 'package:shelf/shelf.dart'; + +extension ProvideExtension on Request { + Request provide(T Function() create) { + return change(context: {...context, '$T': create}); + } + + T lookup() { + final value = context['$T']; + if (value == null) { + throw StateError( + ''' +request.lookup<$T>() called with a request request that does not contain a $T. +''', + ); + } + return (value as T Function())(); + } +} + +Middleware provider(T Function(Request request) create) { + return (handler) { + return (req) => handler(req.provide(() => create(req))); + }; +} diff --git a/packages/shorebird_code_push_api/lib/src/routes/check_for_updates/check_for_updates.dart b/packages/shorebird_code_push_api/lib/src/routes/check_for_updates/check_for_updates.dart new file mode 100644 index 00000000..63a00823 --- /dev/null +++ b/packages/shorebird_code_push_api/lib/src/routes/check_for_updates/check_for_updates.dart @@ -0,0 +1,2 @@ +export 'check_for_updates_handler.dart'; +export 'models/models.dart'; diff --git a/packages/shorebird_code_push_api/lib/src/routes/check_for_updates/check_for_updates_handler.dart b/packages/shorebird_code_push_api/lib/src/routes/check_for_updates/check_for_updates_handler.dart new file mode 100644 index 00000000..1028a1f6 --- /dev/null +++ b/packages/shorebird_code_push_api/lib/src/routes/check_for_updates/check_for_updates_handler.dart @@ -0,0 +1,39 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:shelf/shelf.dart'; +import 'package:shorebird_code_push_api/src/provider.dart'; +import 'package:shorebird_code_push_api/src/routes/check_for_updates/check_for_updates.dart'; +import 'package:shorebird_code_push_api/src/version_store.dart'; + +Future checkForUpdatesHandler(Request request) async { + late final CheckForUpdatesRequest checkForUpdatesRequest; + try { + checkForUpdatesRequest = CheckForUpdatesRequest.fromJson( + jsonDecode(await request.readAsString()) as Map, + ); + } catch (_) { + return Response.badRequest( + body: 'Invalid request body', + headers: {HttpHeaders.contentTypeHeader: ContentType.text.value}, + ); + } + + final store = request.lookup(); + final latestVersion = store.latestVersionForClient( + checkForUpdatesRequest.clientId, + currentVersion: checkForUpdatesRequest.version, + ); + + final response = latestVersion == null + ? const CheckForUpdatesResponse() + : CheckForUpdatesResponse( + updateAvailable: true, + update: Update(version: latestVersion, hash: ''), + ); + + return Response.ok( + json.encode(response.toJson()), + headers: {HttpHeaders.contentTypeHeader: ContentType.json.value}, + ); +} diff --git a/packages/shorebird_code_push_api/lib/src/routes/check_for_updates/models/check_for_updates_request.dart b/packages/shorebird_code_push_api/lib/src/routes/check_for_updates/models/check_for_updates_request.dart new file mode 100644 index 00000000..0578b68a --- /dev/null +++ b/packages/shorebird_code_push_api/lib/src/routes/check_for_updates/models/check_for_updates_request.dart @@ -0,0 +1,31 @@ +class CheckForUpdatesRequest { + const CheckForUpdatesRequest({ + required this.version, + required this.platform, + required this.arch, + required this.clientId, + }); + + factory CheckForUpdatesRequest.fromJson(Map json) { + return CheckForUpdatesRequest( + version: (json['version'] ?? '') as String, + platform: json['platform'] as String, + arch: json['arch'] as String, + clientId: json['client_id'] as String, + ); + } + + Map toJson() { + return { + 'version': version, + 'platform': platform, + 'arch': arch, + 'client_id': clientId, + }; + } + + final String version; + final String platform; + final String arch; + final String clientId; +} diff --git a/packages/shorebird_code_push_api/lib/src/routes/check_for_updates/models/check_for_updates_response.dart b/packages/shorebird_code_push_api/lib/src/routes/check_for_updates/models/check_for_updates_response.dart new file mode 100644 index 00000000..a64f65c8 --- /dev/null +++ b/packages/shorebird_code_push_api/lib/src/routes/check_for_updates/models/check_for_updates_response.dart @@ -0,0 +1,35 @@ +class CheckForUpdatesResponse { + const CheckForUpdatesResponse({ + this.updateAvailable = false, + this.update, + }); + + final bool updateAvailable; + final Update? update; + + Map toJson() { + return { + 'update_available': updateAvailable, + if (update != null) 'update': update!.toJson(), + }; + } +} + +class Update { + const Update({required this.version, required this.hash}); + + final String version; + final String hash; + + Map toJson() { + return { + 'version': version, + 'hash': hash, + 'download_url': downloadUrlForVersion(version), + }; + } + + String downloadUrlForVersion(String version) { + return 'http://localhost:8080/releases/$version.txt'; + } +} diff --git a/packages/shorebird_code_push_api/lib/src/routes/check_for_updates/models/models.dart b/packages/shorebird_code_push_api/lib/src/routes/check_for_updates/models/models.dart new file mode 100644 index 00000000..030bbd45 --- /dev/null +++ b/packages/shorebird_code_push_api/lib/src/routes/check_for_updates/models/models.dart @@ -0,0 +1,2 @@ +export 'check_for_updates_request.dart'; +export 'check_for_updates_response.dart'; diff --git a/packages/shorebird_code_push_api/lib/src/routes/download_release/download_release.dart b/packages/shorebird_code_push_api/lib/src/routes/download_release/download_release.dart new file mode 100644 index 00000000..d4a93c91 --- /dev/null +++ b/packages/shorebird_code_push_api/lib/src/routes/download_release/download_release.dart @@ -0,0 +1 @@ +export 'download_release_handler.dart'; diff --git a/packages/shorebird_code_push_api/lib/src/routes/download_release/download_release_handler.dart b/packages/shorebird_code_push_api/lib/src/routes/download_release/download_release_handler.dart new file mode 100644 index 00000000..d198d46b --- /dev/null +++ b/packages/shorebird_code_push_api/lib/src/routes/download_release/download_release_handler.dart @@ -0,0 +1,19 @@ +import 'dart:io'; + +import 'package:path/path.dart' as path; +import 'package:shelf/shelf.dart'; +import 'package:shorebird_code_push_api/src/provider.dart'; +import 'package:shorebird_code_push_api/src/version_store.dart'; + +Future downloadReleaseHandler( + Request request, + String versionWithExtension, +) async { + final version = path.withoutExtension(versionWithExtension); + final releasePath = + request.lookup().filePathForVersion(version); + final file = File(releasePath); + if (!file.existsSync()) return Response.notFound('Release not found'); + final bytes = file.openRead(); + return Response.ok(bytes); +} diff --git a/packages/shorebird_code_push_api/lib/src/routes/routes.dart b/packages/shorebird_code_push_api/lib/src/routes/routes.dart new file mode 100644 index 00000000..7fb726d7 --- /dev/null +++ b/packages/shorebird_code_push_api/lib/src/routes/routes.dart @@ -0,0 +1,3 @@ +export 'check_for_updates/check_for_updates.dart'; +export 'download_release/download_release.dart'; +export 'upload_release/upload_release.dart'; diff --git a/packages/shorebird_code_push_api/lib/src/routes/upload_release/upload_release.dart b/packages/shorebird_code_push_api/lib/src/routes/upload_release/upload_release.dart new file mode 100644 index 00000000..f2ad5914 --- /dev/null +++ b/packages/shorebird_code_push_api/lib/src/routes/upload_release/upload_release.dart @@ -0,0 +1 @@ +export 'upload_release_handler.dart'; diff --git a/packages/shorebird_code_push_api/lib/src/routes/upload_release/upload_release_handler.dart b/packages/shorebird_code_push_api/lib/src/routes/upload_release/upload_release_handler.dart new file mode 100644 index 00000000..a998638a --- /dev/null +++ b/packages/shorebird_code_push_api/lib/src/routes/upload_release/upload_release_handler.dart @@ -0,0 +1,45 @@ +import 'dart:io'; + +import 'package:shelf/shelf.dart'; +import 'package:shelf_multipart/form_data.dart'; +import 'package:shelf_multipart/multipart.dart'; +import 'package:shorebird_code_push_api/src/provider.dart'; +import 'package:shorebird_code_push_api/src/version_store.dart'; + +Future uploadReleaseHandler(Request request) async { + if (!request.isMultipart || !request.isMultipartForm) { + return Response.badRequest(body: 'Expected multipart form request'); + } + + final store = request.lookup(); + + store.cacheDir.createSync(recursive: true); + + final nextVersion = store.getNextVersion(); + final path = store.filePathForVersion(nextVersion); + + var foundFile = false; + + try { + await for (final formData in request.multipartFormData) { + // 'file' is just the name of the field we used in this form. + if (formData.name == 'file') { + if (foundFile) { + throw Exception('Unexpected form data: ${formData.name}'); + } + final file = File(path); + await file.create(); + await file.writeAsBytes(await formData.part.readBytes(), flush: true); + foundFile = true; + continue; + } else { + throw Exception('Unexpected form data: ${formData.name}'); + } + } + if (!foundFile) throw Exception('Missing file'); + } catch (error) { + return Response.badRequest(body: error.toString()); + } + + return Response(HttpStatus.created, body: 'OK'); +} diff --git a/packages/shorebird_code_push_api/lib/src/shorebird_code_push_api.dart b/packages/shorebird_code_push_api/lib/src/shorebird_code_push_api.dart deleted file mode 100644 index f1065833..00000000 --- a/packages/shorebird_code_push_api/lib/src/shorebird_code_push_api.dart +++ /dev/null @@ -1,7 +0,0 @@ -/// {@template shorebird_code_push_api} -/// The Shorebird CodePush API -/// {@endtemplate} -class ShorebirdCodePushApi { - /// {@macro shorebird_code_push_api} - const ShorebirdCodePushApi(); -} diff --git a/packages/shorebird_code_push_api/lib/src/version_store.dart b/packages/shorebird_code_push_api/lib/src/version_store.dart new file mode 100644 index 00000000..dcbd1148 --- /dev/null +++ b/packages/shorebird_code_push_api/lib/src/version_store.dart @@ -0,0 +1,56 @@ +import 'dart:io'; + +import 'package:path/path.dart' as p; +import 'package:version/version.dart'; + +int _compareVersions(String a, String b) { + return Version.parse(a).compareTo(Version.parse(b)); +} + +class VersionStore { + const VersionStore({required this.cachePath}); + + final String cachePath; + + Directory get cacheDir { + return Directory(p.join(Directory.current.path, cachePath)); + } + + // Should take an api key/product name, etc. + String getNextVersion() { + final latest = latestVersionForClient('client') ?? '0.0.0'; + final next = Version.parse(latest).incrementPatch().toString(); + return next; + } + + void addVersion(String version, List bytes) { + cacheDir.createSync(recursive: true); + final path = filePathForVersion(version); + File(path).writeAsBytesSync(bytes); + } + + String? latestVersionForClient(String clientId, {String? currentVersion}) { + final versions = _versionsForClientId(clientId).toList() + ..sort(_compareVersions); + if (versions.isEmpty) return null; + if (versions.last == currentVersion) return null; + + return versions.last; + } + + String filePathForVersion(String version) { + return p.join(cacheDir.path, '$version.txt'); + } + + Iterable _versionsForClientId(String clientId) { + // This should use the clientId to get a productId and look up the versions + // based on productId/architecture, etc. + try { + final dir = cacheDir; + final files = dir.listSync(); + return files.map((e) => p.basenameWithoutExtension(e.path)); + } catch (e) { + return []; + } + } +} diff --git a/packages/shorebird_code_push_api/pubspec.yaml b/packages/shorebird_code_push_api/pubspec.yaml index 32c2c027..b8540cad 100644 --- a/packages/shorebird_code_push_api/pubspec.yaml +++ b/packages/shorebird_code_push_api/pubspec.yaml @@ -7,8 +7,11 @@ environment: sdk: ">=2.19.0 <3.0.0" dependencies: + path: ^1.8.3 shelf: ^1.4.0 + shelf_multipart: ^1.0.0 shelf_router: ^1.1.3 + version: ^3.0.2 dev_dependencies: mocktail: ^0.3.0 diff --git a/packages/shorebird_code_push_api/test/fixtures/release.txt b/packages/shorebird_code_push_api/test/fixtures/release.txt new file mode 100644 index 00000000..e69de29b diff --git a/packages/shorebird_code_push_api/test/src/middleware/version_store_provider_test.dart b/packages/shorebird_code_push_api/test/src/middleware/version_store_provider_test.dart new file mode 100644 index 00000000..7f6ded73 --- /dev/null +++ b/packages/shorebird_code_push_api/test/src/middleware/version_store_provider_test.dart @@ -0,0 +1,24 @@ +import 'package:shelf/shelf.dart'; +import 'package:shorebird_code_push_api/src/middleware/middleware.dart'; +import 'package:shorebird_code_push_api/src/provider.dart'; +import 'package:shorebird_code_push_api/src/version_store.dart'; +import 'package:test/test.dart'; + +void main() { + group('versionStoreProvider', () { + test('provides a version store instance', () async { + VersionStore? store; + + final handler = versionStoreProvider( + (req) { + store = req.lookup(); + return Response.ok(''); + }, + ); + final request = Request('GET', Uri.parse('http://localhost/')); + + await handler(request); + expect(store, isNotNull); + }); + }); +} diff --git a/packages/shorebird_code_push_api/test/src/provider_test.dart b/packages/shorebird_code_push_api/test/src/provider_test.dart new file mode 100644 index 00000000..73993d4f --- /dev/null +++ b/packages/shorebird_code_push_api/test/src/provider_test.dart @@ -0,0 +1,45 @@ +import 'dart:io'; + +import 'package:shelf/shelf.dart'; +import 'package:shorebird_code_push_api/src/provider.dart'; +import 'package:test/test.dart'; + +void main() { + test('values can be provided and read via middleware', () async { + const value = '__test_value__'; + Handler middleware(Handler handler) { + return (request) { + return handler(request.provide(() => value)); + }; + } + + Response onRequest(Request request) { + final value = request.lookup(); + return Response.ok(value); + } + + final handler = + const Pipeline().addMiddleware(middleware).addHandler(onRequest); + + final request = Request('GET', Uri.parse('http://localhost/')); + final response = await handler(request); + + await expectLater(response.statusCode, equals(HttpStatus.ok)); + await expectLater(await response.readAsString(), equals(value)); + }); + + test('A StateError is thrown when reading an un-provided value', () async { + Response onRequest(Request request) { + request.lookup(); + return Response.ok(''); + } + + final handler = const Pipeline() + .addMiddleware((handler) => handler) + .addHandler(onRequest); + + final request = Request('GET', Uri.parse('http://localhost/')); + + await expectLater(() => handler(request), throwsStateError); + }); +} diff --git a/packages/shorebird_code_push_api/test/src/routes/check_for_updates/check_for_updates_handler_test.dart b/packages/shorebird_code_push_api/test/src/routes/check_for_updates/check_for_updates_handler_test.dart new file mode 100644 index 00000000..b2e0251d --- /dev/null +++ b/packages/shorebird_code_push_api/test/src/routes/check_for_updates/check_for_updates_handler_test.dart @@ -0,0 +1,95 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:mocktail/mocktail.dart'; +import 'package:shelf/shelf.dart'; +import 'package:shorebird_code_push_api/src/provider.dart'; +import 'package:shorebird_code_push_api/src/routes/check_for_updates/check_for_updates.dart'; +import 'package:shorebird_code_push_api/src/version_store.dart'; +import 'package:test/test.dart'; + +class _MockVersionStore extends Mock implements VersionStore {} + +void main() { + group('checkForUpdatesHandler', () { + final uri = Uri.parse('http://localhost/'); + + late VersionStore store; + + setUp(() { + store = _MockVersionStore(); + }); + + test('returns 400 if request body is invalid', () async { + final request = Request('POST', uri); + final response = await checkForUpdatesHandler(request); + + expect(response.statusCode, HttpStatus.badRequest); + }); + + test( + 'returns 200 no update available ' + 'when unable to get latest version', () async { + const payload = CheckForUpdatesRequest( + version: '1.0.0', + platform: 'android', + arch: 'arm64', + clientId: 'client-id', + ); + + when( + () => store.latestVersionForClient( + any(), + currentVersion: any(named: 'currentVersion'), + ), + ).thenReturn(null); + + final request = Request( + 'POST', + uri, + body: json.encode(payload.toJson()), + ).provide(() => store); + + final response = await checkForUpdatesHandler(request); + + expect(response.statusCode, HttpStatus.ok); + + final body = await response.readAsString(); + expect(body, equals('{"update_available":false}')); + }); + + test('returns 200 update available when version is not latest', () async { + const payload = CheckForUpdatesRequest( + version: '1.0.0', + platform: 'android', + arch: 'arm64', + clientId: 'client-id', + ); + + when( + () => store.latestVersionForClient( + any(), + currentVersion: any(named: 'currentVersion'), + ), + ).thenReturn('1.0.1'); + + final request = Request( + 'POST', + uri, + body: json.encode(payload.toJson()), + ).provide(() => store); + + final response = await checkForUpdatesHandler(request); + + expect(response.statusCode, HttpStatus.ok); + + final body = await response.readAsString(); + expect( + body, + equals( + '{"update_available":true,"update":{"version":"1.0.1","hash":"","download_url":"http://localhost:8080/releases/1.0.1.txt"}}', + ), + ); + }); + }); +} diff --git a/packages/shorebird_code_push_api/test/src/routes/download_release/download_release_handler_test.dart b/packages/shorebird_code_push_api/test/src/routes/download_release/download_release_handler_test.dart new file mode 100644 index 00000000..9c7b55be --- /dev/null +++ b/packages/shorebird_code_push_api/test/src/routes/download_release/download_release_handler_test.dart @@ -0,0 +1,42 @@ +import 'dart:io'; + +import 'package:mocktail/mocktail.dart'; +import 'package:path/path.dart' as path; +import 'package:shelf/shelf.dart'; +import 'package:shorebird_code_push_api/src/provider.dart'; +import 'package:shorebird_code_push_api/src/routes/download_release/download_release.dart'; +import 'package:shorebird_code_push_api/src/version_store.dart'; +import 'package:test/test.dart'; + +class _MockVersionStore extends Mock implements VersionStore {} + +void main() { + group('downloadReleaseHandler', () { + final uri = Uri.parse('http://localhost/'); + late VersionStore store; + + setUp(() { + store = _MockVersionStore(); + }); + + test('returns 404 if release not found', () async { + when(() => store.filePathForVersion('1.0.0')).thenReturn('not-found'); + + final request = Request('GET', uri).provide(() => store); + final response = await downloadReleaseHandler(request, '1.0.0.txt'); + + expect(response.statusCode, HttpStatus.notFound); + }); + + test('returns 200 if release found', () async { + when( + () => store.filePathForVersion('1.0.0'), + ).thenReturn(path.join('test', 'fixtures', 'release.txt')); + + final request = Request('GET', uri).provide(() => store); + final response = await downloadReleaseHandler(request, '1.0.0.txt'); + + expect(response.statusCode, HttpStatus.ok); + }); + }); +} diff --git a/packages/shorebird_code_push_api/test/src/shorebird_code_push_api_test.dart b/packages/shorebird_code_push_api/test/src/shorebird_code_push_api_test.dart deleted file mode 100644 index c4ec44cc..00000000 --- a/packages/shorebird_code_push_api/test/src/shorebird_code_push_api_test.dart +++ /dev/null @@ -1,11 +0,0 @@ -// ignore_for_file: prefer_const_constructors -import 'package:shorebird_code_push_api/shorebird_code_push_api.dart'; -import 'package:test/test.dart'; - -void main() { - group('ShorebirdCodePushApi', () { - test('can be instantiated', () { - expect(ShorebirdCodePushApi(), isNotNull); - }); - }); -} diff --git a/packages/shorebird_code_push_api/test/src/version_store_test.dart b/packages/shorebird_code_push_api/test/src/version_store_test.dart new file mode 100644 index 00000000..aa6fda9f --- /dev/null +++ b/packages/shorebird_code_push_api/test/src/version_store_test.dart @@ -0,0 +1,47 @@ +import 'dart:io'; + +import 'package:shorebird_code_push_api/src/version_store.dart'; +import 'package:test/test.dart'; + +void main() { + group('VersionStore', () { + group('getNextVersion', () { + test('returns 0.0.1 when no versions exist', () { + final tempDir = Directory.systemTemp.createTempSync(); + final store = VersionStore(cachePath: tempDir.path); + expect(store.getNextVersion(), equals('0.0.1')); + }); + + test('returns 0.0.2 when 0.0.1 exists', () { + final tempDir = Directory.systemTemp.createTempSync(); + final store = VersionStore(cachePath: tempDir.path) + ..addVersion('0.0.1', []); + expect(store.getNextVersion(), equals('0.0.2')); + }); + }); + + group('latestVersionForClient', () { + test('returns null when cache does not exist', () { + const store = VersionStore(cachePath: 'invalid-path'); + expect(store.latestVersionForClient('empty-client-id'), isNull); + }); + + test('returns null when no versions exist', () { + final tempDir = Directory.systemTemp.createTempSync(); + final store = VersionStore(cachePath: tempDir.path); + expect(store.latestVersionForClient('empty-client-id'), isNull); + }); + + test('returns latest version when multiple versions exist', () { + final tempDir = Directory.systemTemp.createTempSync(); + final store = VersionStore(cachePath: tempDir.path) + ..addVersion('0.0.1', []) + ..addVersion('0.0.2', []); + expect( + store.latestVersionForClient('empty-client-id'), + equals('0.0.2'), + ); + }); + }); + }); +}