feat: add device ID override functionality to ShorebirdUpdater
ci / ✅ Semantic Pull Request (push) Has been cancelled
ci / 🔤 Check Spelling (push) Has been cancelled
ci / 👀 Detect Changes (push) Has been cancelled
Shorebird CI / changes (push) Has been cancelled
Shorebird CI / CSpell (push) Has been cancelled
ci / 🦀 Build ${{ matrix.crate }} (${{ matrix.os }}) (push) Has been cancelled
ci / 🎯 Build ${{ matrix.package }} (push) Has been cancelled
ci / ci (push) Has been cancelled
Shorebird CI / shorebird_code_push (push) Has been cancelled
Shorebird CI / shorebird_code_push_example (push) Has been cancelled
Shorebird CI / required (push) Has been cancelled
ci / ✅ Semantic Pull Request (push) Has been cancelled
ci / 🔤 Check Spelling (push) Has been cancelled
ci / 👀 Detect Changes (push) Has been cancelled
Shorebird CI / changes (push) Has been cancelled
Shorebird CI / CSpell (push) Has been cancelled
ci / 🦀 Build ${{ matrix.crate }} (${{ matrix.os }}) (push) Has been cancelled
ci / 🎯 Build ${{ matrix.package }} (push) Has been cancelled
ci / ci (push) Has been cancelled
Shorebird CI / shorebird_code_push (push) Has been cancelled
Shorebird CI / shorebird_code_push_example (push) Has been cancelled
Shorebird CI / required (push) Has been cancelled
- Introduced `setDeviceIdOverride` method in `ShorebirdUpdater` to allow clients to set a custom device ID for patch checks. - Implemented the method in `ShorebirdUpdaterImpl` for both IO and web platforms. - Updated the `Updater` class to handle the device ID override in native bindings. - Added tests for the new functionality in both IO and web test suites, ensuring proper behavior when the updater is available and unavailable.
This commit is contained in:
@@ -83,6 +83,15 @@ SHOREBIRD_EXPORT uintptr_t shorebird_next_boot_patch_number(void);
|
||||
SHOREBIRD_EXPORT
|
||||
bool shorebird_check_for_downloadable_update(const char *c_channel);
|
||||
|
||||
/**
|
||||
* Overrides the generated random per-install device/client id used in patch
|
||||
* check requests. This is for applications that need to bind patch delivery
|
||||
* to their own stable account/device identifier. The updater persists this
|
||||
* value in state.json after a successful call.
|
||||
*/
|
||||
SHOREBIRD_EXPORT
|
||||
bool shorebird_set_device_id_override(const char *c_device_id);
|
||||
|
||||
/**
|
||||
* Synchronously download an update on the first non-null channel of:
|
||||
* 1. `c_channel`
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
//! `c_api::engine` — that surface is unstable and changes freely.
|
||||
use std::os::raw::c_char;
|
||||
|
||||
use super::{allocate_c_string, free_c_string, log_on_error, to_rust_option};
|
||||
use super::{allocate_c_string, free_c_string, log_on_error, to_rust, to_rust_option};
|
||||
use crate::{updater, UpdateStatus};
|
||||
|
||||
/// An unknown error occurred while updating. The update was not installed.
|
||||
@@ -105,6 +105,23 @@ pub extern "C" fn shorebird_check_for_downloadable_update(c_channel: *const c_ch
|
||||
)
|
||||
}
|
||||
|
||||
/// Overrides the generated random per-install device/client id used in patch
|
||||
/// check requests. This is for applications that need to bind patch delivery
|
||||
/// to their own stable account/device identifier. The updater persists this
|
||||
/// value in state.json after a successful call.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn shorebird_set_device_id_override(c_device_id: *const c_char) -> bool {
|
||||
log_on_error(
|
||||
|| {
|
||||
let device_id = to_rust(c_device_id)?;
|
||||
updater::set_client_id_override(&device_id)?;
|
||||
Ok(true)
|
||||
},
|
||||
"setting device id override",
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
/// Synchronously download an update on the first non-null channel of:
|
||||
/// 1. `c_channel`
|
||||
/// 2. The channel specified in shorebird.yaml
|
||||
|
||||
Vendored
+36
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use anyhow::Result;
|
||||
use anyhow::{ensure, Result};
|
||||
#[cfg(test)]
|
||||
use anyhow::{bail, Context};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -100,6 +100,23 @@ impl UpdaterState {
|
||||
pub fn client_id(&self) -> String {
|
||||
self.serialized_state.client_id.clone()
|
||||
}
|
||||
|
||||
pub fn set_client_id_override(&mut self, client_id: &str) -> Result<()> {
|
||||
ensure!(
|
||||
!client_id.is_empty(),
|
||||
"Device id override must not be empty."
|
||||
);
|
||||
ensure!(
|
||||
client_id.len() <= 256,
|
||||
"Device id override must be 256 bytes or fewer."
|
||||
);
|
||||
ensure!(
|
||||
!client_id.chars().any(char::is_control),
|
||||
"Device id override must not contain control characters."
|
||||
);
|
||||
self.serialized_state.client_id = client_id.to_string();
|
||||
self.save()
|
||||
}
|
||||
}
|
||||
|
||||
impl UpdaterState {
|
||||
@@ -529,6 +546,24 @@ mod tests {
|
||||
assert_eq!(next.client_id(), original_client_id);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn client_id_override_persists_across_release_changes() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let mut original = load(&tmp, "1.0.0+1");
|
||||
original
|
||||
.set_client_id_override("developer-device-id")
|
||||
.unwrap();
|
||||
let next = load(&tmp, "1.0.0+2");
|
||||
assert_eq!(next.client_id(), "developer-device-id");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn client_id_override_rejects_empty_value() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let mut state = load(&tmp, "1.0.0+1");
|
||||
assert!(state.set_client_id_override("").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrupt_state_file_creates_new_state() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
|
||||
@@ -271,6 +271,13 @@ pub fn should_auto_update() -> anyhow::Result<bool> {
|
||||
with_config(|config| Ok(config.auto_update))
|
||||
}
|
||||
|
||||
/// Overrides the generated per-install device/client id used for patch checks
|
||||
/// and server-side device targeting. The value is persisted in updater state
|
||||
/// and survives release-version cache resets.
|
||||
pub fn set_client_id_override(client_id: &str) -> anyhow::Result<()> {
|
||||
with_mut_state(|state| state.set_client_id_override(client_id))
|
||||
}
|
||||
|
||||
/// Synchronously checks for an update on the first non-null channel of:
|
||||
/// 1. `c_channel`
|
||||
/// 2. The channel specified in shorebird.yaml
|
||||
|
||||
@@ -120,6 +120,22 @@ updater.checkForUpdate(track: UpdateTrack('my-custom-track'));
|
||||
tracks. See [#3484](https://github.com/shorebirdtech/shorebird/issues/3484)
|
||||
for details.
|
||||
|
||||
### Device id override
|
||||
|
||||
The updater generates a random per-install `client_id` on first startup and
|
||||
persists it locally. Self-hosted update servers that encrypt or target patches
|
||||
per app/account device can override that id before checking for updates:
|
||||
|
||||
```dart
|
||||
final updater = ShorebirdUpdater();
|
||||
await updater.setDeviceIdOverride('stable-app-device-id');
|
||||
await updater.checkForUpdate();
|
||||
```
|
||||
|
||||
Do not put secrets in this value. It is sent to the update server as
|
||||
`client_id`; encryption keys should be derived from server-side or
|
||||
app-provided key material.
|
||||
|
||||
## Join us on Discord!
|
||||
|
||||
We have an active [Discord server](https://discord.gg/shorebird) where you can
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -127,6 +127,16 @@ abstract class ShorebirdUpdater {
|
||||
/// Throws a [ReadPatchException] if the read is unsuccessful.
|
||||
Future<Patch?> readNextPatch();
|
||||
|
||||
/// Overrides the generated random per-install device id used in patch
|
||||
/// checks. Call this before [checkForUpdate] or [update] if your server
|
||||
/// needs patch delivery bound to an app/account-specific identifier.
|
||||
///
|
||||
/// The updater persists the override in its local state after a successful
|
||||
/// call. The value should be stable for this installation/account and must
|
||||
/// not contain sensitive secrets; it is sent to the update server as
|
||||
/// `client_id`.
|
||||
Future<void> setDeviceIdOverride(String deviceId);
|
||||
|
||||
/// Checks for an available patch on [track] (or [UpdateTrack.stable] if no
|
||||
/// track is specified) and returns the [UpdateStatus].
|
||||
/// This method should be used to determine the update status before calling
|
||||
|
||||
@@ -9,12 +9,9 @@ import 'package:shorebird_code_push/src/shorebird_updater.dart';
|
||||
import 'package:shorebird_code_push/src/updater.dart';
|
||||
|
||||
@visibleForTesting
|
||||
|
||||
/// Type definition for [Isolate.run].
|
||||
typedef IsolateRun = Future<R> Function<R>(
|
||||
FutureOr<R> Function(), {
|
||||
String? debugName,
|
||||
});
|
||||
typedef IsolateRun =
|
||||
Future<R> Function<R>(FutureOr<R> Function(), {String? debugName});
|
||||
|
||||
/// {@template shorebird_updater_io}
|
||||
/// The Shorebird IO Updater.
|
||||
@@ -22,8 +19,8 @@ typedef IsolateRun = Future<R> Function<R>(
|
||||
class ShorebirdUpdaterImpl implements ShorebirdUpdater {
|
||||
/// {@macro shorebird_updater_io}
|
||||
ShorebirdUpdaterImpl({Updater? updater, IsolateRun? run})
|
||||
: _updater = updater ?? const Updater(),
|
||||
_run = run ?? Isolate.run {
|
||||
: _updater = updater ?? const Updater(),
|
||||
_run = run ?? Isolate.run {
|
||||
try {
|
||||
// If the Shorebird Engine is not available, this will throw an exception.
|
||||
// FIXME: Run this in an isolate or refactor the updater to avoid risking
|
||||
@@ -55,18 +52,25 @@ class ShorebirdUpdaterImpl implements ShorebirdUpdater {
|
||||
@override
|
||||
Future<Patch?> readNextPatch() => _readPatch(_updater.nextPatchNumber);
|
||||
|
||||
@override
|
||||
Future<void> setDeviceIdOverride(String deviceId) async {
|
||||
if (!_isAvailable) return;
|
||||
final didSet = await _run(() => _updater.setDeviceIdOverride(deviceId));
|
||||
if (!didSet) {
|
||||
throw StateError('Unable to set Shorebird device id override.');
|
||||
}
|
||||
}
|
||||
|
||||
Future<Patch?> _readPatch(int Function() fn) async {
|
||||
if (!_isAvailable) return null;
|
||||
return _run(
|
||||
() {
|
||||
try {
|
||||
final patchNumber = fn();
|
||||
return patchNumber > 0 ? Patch(number: patchNumber) : null;
|
||||
} catch (error) {
|
||||
throw ReadPatchException(message: '$error');
|
||||
}
|
||||
},
|
||||
);
|
||||
return _run(() {
|
||||
try {
|
||||
final patchNumber = fn();
|
||||
return patchNumber > 0 ? Patch(number: patchNumber) : null;
|
||||
} catch (error) {
|
||||
throw ReadPatchException(message: '$error');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -74,8 +78,9 @@ class ShorebirdUpdaterImpl implements ShorebirdUpdater {
|
||||
if (!_isAvailable) return UpdateStatus.unavailable;
|
||||
|
||||
// First, check to see whether an update is available for download.
|
||||
final isUpdateAvailable =
|
||||
await _run(() => _updater.checkForDownloadableUpdate(track: track));
|
||||
final isUpdateAvailable = await _run(
|
||||
() => _updater.checkForDownloadableUpdate(track: track),
|
||||
);
|
||||
if (isUpdateAvailable) return UpdateStatus.outdated;
|
||||
|
||||
// If no new update is available for download, see if a new patch exists
|
||||
|
||||
@@ -18,6 +18,9 @@ class ShorebirdUpdaterImpl implements ShorebirdUpdater {
|
||||
@override
|
||||
Future<Patch?> readNextPatch() async => null;
|
||||
|
||||
@override
|
||||
Future<void> setDeviceIdOverride(String deviceId) async {}
|
||||
|
||||
@override
|
||||
Future<UpdateStatus> checkForUpdate({UpdateTrack? track}) async =>
|
||||
UpdateStatus.unavailable;
|
||||
|
||||
@@ -16,8 +16,9 @@ class Updater {
|
||||
|
||||
/// The ffi bindings to the Updater library.
|
||||
@visibleForTesting
|
||||
static UpdaterBindings bindings =
|
||||
UpdaterBindings(ffi.DynamicLibrary.process());
|
||||
static UpdaterBindings bindings = UpdaterBindings(
|
||||
ffi.DynamicLibrary.process(),
|
||||
);
|
||||
|
||||
/// The currently active patch number.
|
||||
int currentPatchNumber() => bindings.shorebird_current_boot_patch_number();
|
||||
@@ -32,6 +33,18 @@ class Updater {
|
||||
track == null ? ffi.nullptr : track.name.toNativeUtf8().cast<Char>(),
|
||||
);
|
||||
|
||||
/// Overrides the generated per-install device id sent in patch checks.
|
||||
bool setDeviceIdOverride(String deviceId) {
|
||||
final nativeDeviceId = deviceId.toNativeUtf8();
|
||||
try {
|
||||
return bindings.shorebird_set_device_id_override(
|
||||
nativeDeviceId.cast<Char>(),
|
||||
);
|
||||
} finally {
|
||||
malloc.free(nativeDeviceId);
|
||||
}
|
||||
}
|
||||
|
||||
/// Downloads the latest patch, if available and returns an [UpdateResult]
|
||||
/// to indicate whether the update was successful.
|
||||
Pointer<UpdateResult> update({UpdateTrack? track}) =>
|
||||
|
||||
@@ -118,11 +118,7 @@ void main() {
|
||||
await expectLater(
|
||||
shorebirdUpdater.readNextPatch(),
|
||||
completion(
|
||||
isA<Patch>().having(
|
||||
(p) => p.number,
|
||||
'number',
|
||||
nextPatchNumber,
|
||||
),
|
||||
isA<Patch>().having((p) => p.number, 'number', nextPatchNumber),
|
||||
),
|
||||
);
|
||||
});
|
||||
@@ -151,11 +147,7 @@ void main() {
|
||||
await expectLater(
|
||||
shorebirdUpdater.readNextPatch(),
|
||||
completion(
|
||||
isA<Patch>().having(
|
||||
(p) => p.number,
|
||||
'number',
|
||||
nextPatchNumber,
|
||||
),
|
||||
isA<Patch>().having((p) => p.number, 'number', nextPatchNumber),
|
||||
),
|
||||
);
|
||||
});
|
||||
@@ -186,11 +178,7 @@ void main() {
|
||||
await expectLater(
|
||||
shorebirdUpdater.readNextPatch(),
|
||||
completion(
|
||||
isA<Patch>().having(
|
||||
(p) => p.number,
|
||||
'number',
|
||||
nextPatchNumber,
|
||||
),
|
||||
isA<Patch>().having((p) => p.number, 'number', nextPatchNumber),
|
||||
),
|
||||
);
|
||||
});
|
||||
@@ -221,6 +209,61 @@ void main() {
|
||||
});
|
||||
});
|
||||
|
||||
group('setDeviceIdOverride', () {
|
||||
group('when updater is unavailable', () {
|
||||
setUp(() {
|
||||
when(updater.currentPatchNumber).thenThrow(Exception('oops'));
|
||||
});
|
||||
|
||||
test(
|
||||
'does nothing',
|
||||
overridePrint((_) async {
|
||||
shorebirdUpdater = ShorebirdUpdaterImpl(updater: updater, run: run);
|
||||
await expectLater(
|
||||
shorebirdUpdater.setDeviceIdOverride('developer-device-id'),
|
||||
completes,
|
||||
);
|
||||
verifyNever(() => updater.setDeviceIdOverride(any()));
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
group('when updater accepts the override', () {
|
||||
setUp(() {
|
||||
when(updater.currentPatchNumber).thenReturn(0);
|
||||
when(
|
||||
() => updater.setDeviceIdOverride('developer-device-id'),
|
||||
).thenReturn(true);
|
||||
shorebirdUpdater = ShorebirdUpdaterImpl(updater: updater, run: run);
|
||||
});
|
||||
|
||||
test('forwards the device id to the updater', () async {
|
||||
await expectLater(
|
||||
shorebirdUpdater.setDeviceIdOverride('developer-device-id'),
|
||||
completes,
|
||||
);
|
||||
verify(
|
||||
() => updater.setDeviceIdOverride('developer-device-id'),
|
||||
).called(1);
|
||||
});
|
||||
});
|
||||
|
||||
group('when updater rejects the override', () {
|
||||
setUp(() {
|
||||
when(updater.currentPatchNumber).thenReturn(0);
|
||||
when(() => updater.setDeviceIdOverride('')).thenReturn(false);
|
||||
shorebirdUpdater = ShorebirdUpdaterImpl(updater: updater, run: run);
|
||||
});
|
||||
|
||||
test('throws a StateError', () async {
|
||||
await expectLater(
|
||||
shorebirdUpdater.setDeviceIdOverride(''),
|
||||
throwsA(isA<StateError>()),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
group('checkForUpdate', () {
|
||||
group('when updater is unavailable', () {
|
||||
setUp(() {
|
||||
@@ -304,7 +347,7 @@ void main() {
|
||||
|
||||
group('when current patch has been rolled back', () {
|
||||
setUp(() {
|
||||
// The app is currently running patch 1, but checkForDownloadableUpdate
|
||||
// The app is running patch 1, but checkForDownloadableUpdate
|
||||
// triggered a rollback which set next_boot_patch to None (0).
|
||||
when(updater.currentPatchNumber).thenReturn(1);
|
||||
when(updater.nextPatchNumber).thenReturn(0);
|
||||
@@ -331,15 +374,18 @@ void main() {
|
||||
shorebirdUpdater = ShorebirdUpdaterImpl(updater: updater, run: run);
|
||||
});
|
||||
|
||||
test('forwards the provided track to the underlying updater call',
|
||||
() async {
|
||||
await expectLater(
|
||||
shorebirdUpdater.checkForUpdate(track: track),
|
||||
completion(equals(UpdateStatus.outdated)),
|
||||
);
|
||||
verify(() => updater.checkForDownloadableUpdate(track: track))
|
||||
.called(1);
|
||||
});
|
||||
test(
|
||||
'forwards the provided track to the underlying updater call',
|
||||
() async {
|
||||
await expectLater(
|
||||
shorebirdUpdater.checkForUpdate(track: track),
|
||||
completion(equals(UpdateStatus.outdated)),
|
||||
);
|
||||
verify(
|
||||
() => updater.checkForDownloadableUpdate(track: track),
|
||||
).called(1);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -401,15 +447,17 @@ void main() {
|
||||
shorebirdUpdater = ShorebirdUpdaterImpl(updater: updater, run: run);
|
||||
});
|
||||
|
||||
test('propagates the exception and does not call freeUpdateResult',
|
||||
() async {
|
||||
await expectLater(
|
||||
shorebirdUpdater.update(),
|
||||
throwsA(isA<Exception>()),
|
||||
);
|
||||
verify(() => updater.update()).called(1);
|
||||
verifyNever(() => updater.freeUpdateResult(any()));
|
||||
});
|
||||
test(
|
||||
'propagates the exception and does not call freeUpdateResult',
|
||||
() async {
|
||||
await expectLater(
|
||||
shorebirdUpdater.update(),
|
||||
throwsA(isA<Exception>()),
|
||||
);
|
||||
verify(() => updater.update()).called(1);
|
||||
verifyNever(() => updater.freeUpdateResult(any()));
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
group('when no update is available', () {
|
||||
@@ -483,11 +531,7 @@ void main() {
|
||||
shorebirdUpdater.update,
|
||||
throwsA(
|
||||
isA<UpdateException>()
|
||||
.having(
|
||||
(e) => e.message,
|
||||
'message',
|
||||
'oops',
|
||||
)
|
||||
.having((e) => e.message, 'message', 'oops')
|
||||
.having(
|
||||
(e) => e.reason,
|
||||
'reason',
|
||||
@@ -627,15 +671,14 @@ void main() {
|
||||
shorebirdUpdater = ShorebirdUpdaterImpl(updater: updater, run: run);
|
||||
});
|
||||
|
||||
test('forwards the provided track to the underlying updater call',
|
||||
() async {
|
||||
await expectLater(
|
||||
shorebirdUpdater.update(track: track),
|
||||
completes,
|
||||
);
|
||||
verify(() => updater.update(track: track)).called(1);
|
||||
verify(() => updater.freeUpdateResult(any())).called(1);
|
||||
});
|
||||
test(
|
||||
'forwards the provided track to the underlying updater call',
|
||||
() async {
|
||||
await expectLater(shorebirdUpdater.update(track: track), completes);
|
||||
verify(() => updater.update(track: track)).called(1);
|
||||
verify(() => updater.freeUpdateResult(any())).called(1);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -65,6 +65,18 @@ void main() {
|
||||
);
|
||||
});
|
||||
|
||||
group('setDeviceIdOverride', () {
|
||||
test(
|
||||
'does nothing',
|
||||
overridePrint((_) async {
|
||||
await expectLater(
|
||||
shorebirdUpdater.setDeviceIdOverride('developer-device-id'),
|
||||
completes,
|
||||
);
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
group('update', () {
|
||||
test(
|
||||
'does nothing',
|
||||
|
||||
@@ -40,56 +40,59 @@ void main() {
|
||||
});
|
||||
|
||||
group('checkForDownloadableUpdate', () {
|
||||
test('forwards the result of shorebird_check_for_downloadable_update',
|
||||
() {
|
||||
when(
|
||||
() => updaterBindings.shorebird_check_for_downloadable_update(
|
||||
nullptr,
|
||||
),
|
||||
).thenReturn(true);
|
||||
expect(updater.checkForDownloadableUpdate(), isTrue);
|
||||
test(
|
||||
'forwards the result of shorebird_check_for_downloadable_update',
|
||||
() {
|
||||
when(
|
||||
() => updaterBindings.shorebird_check_for_downloadable_update(
|
||||
nullptr,
|
||||
),
|
||||
).thenReturn(true);
|
||||
expect(updater.checkForDownloadableUpdate(), isTrue);
|
||||
|
||||
when(
|
||||
() => updaterBindings.shorebird_check_for_downloadable_update(
|
||||
nullptr,
|
||||
),
|
||||
).thenReturn(false);
|
||||
expect(updater.checkForDownloadableUpdate(), isFalse);
|
||||
});
|
||||
when(
|
||||
() => updaterBindings.shorebird_check_for_downloadable_update(
|
||||
nullptr,
|
||||
),
|
||||
).thenReturn(false);
|
||||
expect(updater.checkForDownloadableUpdate(), isFalse);
|
||||
},
|
||||
);
|
||||
|
||||
group('when a track is provided', () {
|
||||
setUp(() {
|
||||
when(
|
||||
() => updaterBindings.shorebird_check_for_downloadable_update(
|
||||
any(),
|
||||
),
|
||||
() =>
|
||||
updaterBindings.shorebird_check_for_downloadable_update(any()),
|
||||
).thenReturn(true);
|
||||
});
|
||||
|
||||
test('forwards the result of shorebird_check_for_downloadable_update',
|
||||
() {
|
||||
expect(
|
||||
updater.checkForDownloadableUpdate(track: UpdateTrack.beta),
|
||||
isTrue,
|
||||
);
|
||||
test(
|
||||
'forwards the result of shorebird_check_for_downloadable_update',
|
||||
() {
|
||||
expect(
|
||||
updater.checkForDownloadableUpdate(track: UpdateTrack.beta),
|
||||
isTrue,
|
||||
);
|
||||
|
||||
expect(
|
||||
updater.checkForDownloadableUpdate(track: UpdateTrack.stable),
|
||||
isTrue,
|
||||
);
|
||||
expect(
|
||||
updater.checkForDownloadableUpdate(track: UpdateTrack.stable),
|
||||
isTrue,
|
||||
);
|
||||
|
||||
final captured = verify(
|
||||
() => updaterBindings.shorebird_check_for_downloadable_update(
|
||||
captureAny(),
|
||||
),
|
||||
).captured;
|
||||
expect(
|
||||
captured.map(
|
||||
(cstr) => (cstr as Pointer<Char>).cast<Utf8>().toDartString(),
|
||||
),
|
||||
equals(['beta', 'stable']),
|
||||
);
|
||||
});
|
||||
final captured = verify(
|
||||
() => updaterBindings.shorebird_check_for_downloadable_update(
|
||||
captureAny(),
|
||||
),
|
||||
).captured;
|
||||
expect(
|
||||
captured.map(
|
||||
(cstr) => (cstr as Pointer<Char>).cast<Utf8>().toDartString(),
|
||||
),
|
||||
equals(['beta', 'stable']),
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -103,6 +106,28 @@ void main() {
|
||||
});
|
||||
});
|
||||
|
||||
group('setDeviceIdOverride', () {
|
||||
test('forwards the device id to shorebird_set_device_id_override', () {
|
||||
late String capturedDeviceId;
|
||||
when(
|
||||
() => updaterBindings.shorebird_set_device_id_override(any()),
|
||||
).thenAnswer((invocation) {
|
||||
capturedDeviceId =
|
||||
(invocation.positionalArguments.single as Pointer<Char>)
|
||||
.cast<Utf8>()
|
||||
.toDartString();
|
||||
return true;
|
||||
});
|
||||
|
||||
expect(updater.setDeviceIdOverride('developer-device-id'), isTrue);
|
||||
|
||||
verify(
|
||||
() => updaterBindings.shorebird_set_device_id_override(captureAny()),
|
||||
).called(1);
|
||||
expect(capturedDeviceId, 'developer-device-id');
|
||||
});
|
||||
});
|
||||
|
||||
group('update', () {
|
||||
test('calls bindings.shorebird_update_with_result', () {
|
||||
when(
|
||||
|
||||
Reference in New Issue
Block a user