mirror of
https://github.com/block/buzz.git
synced 2026-10-28 04:51:40 +00:00
Make the push gateway origin configurable across the relay, gateway, mobile client, and deployment chart. This is a fresh deployment. The gateway has only run on personal development infrastructure, and push has no existing enabled users. Legacy migration, configured-to-unconfigured mobile transitions, and rolling upgrades from pre-launch gateway binaries are not supported. Populated pre-launch databases are rejected with an informative error before migrations run; their data is untouched. Mobile builds may omit the gateway URL, leaving push unavailable. Once push is enabled, subsequent builds must retain that configuration. ### Validation - All applicable CI checks pass at `5f546cfaf403f0386f146d213adfb4392f4ec29c`, including Rust, mobile/Swift, desktop/integration, security, image builds, and Helm checks. An unrelated agent cancellation test passed on retry. - PostgreSQL CI passed 384 tests, including the production-entry-point regression for refusing populated pre-launch databases before migrations, fresh initialization, and subsequent initialized deployments. - Focused local validation passed all 10 gateway PostgreSQL tests under the CI discovery filter and all 3 unconfigured-mobile tests. Repository-wide PostgreSQL discovery validation and the complete pre-push hooks passed. - Physical-device APNs/App Attest behavior and deployment to a live cluster were not verified in this work. --------- Signed-off-by: Tom Brow <tomb@block.xyz> Co-authored-by: Codex <noreply@openai.com>
241 lines
7.0 KiB
Dart
241 lines
7.0 KiB
Dart
import 'package:buzz/shared/push/dev_push_lease.dart';
|
|
import 'package:buzz/shared/community/community.dart';
|
|
import 'package:buzz/shared/push/push_bootstrap.dart';
|
|
import 'package:buzz/shared/push/push_subscription.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:flutter/widgets.dart';
|
|
|
|
void main() {
|
|
testWidgets('first opt-in starts registration without switching community', (
|
|
tester,
|
|
) async {
|
|
var registrations = 0;
|
|
Widget bootstrap(bool enabled) => BuzzPushRegistrationBootstrap(
|
|
shouldRegister: enabled,
|
|
attemptKey: 'same-community|same-relay',
|
|
startRegistration: () async {
|
|
registrations += 1;
|
|
},
|
|
child: const SizedBox(),
|
|
);
|
|
await tester.pumpWidget(bootstrap(false));
|
|
expect(registrations, 0);
|
|
await tester.pumpWidget(bootstrap(true));
|
|
expect(registrations, 1);
|
|
await tester.pumpWidget(bootstrap(true));
|
|
expect(registrations, 1);
|
|
});
|
|
|
|
test(
|
|
'superseded lease acceptance is a retryable publication failure',
|
|
() async {
|
|
await expectLater(
|
|
publishBuzzPushLeaseRecoverably(
|
|
reserveGeneration: () async => 1,
|
|
publish: (_) async {},
|
|
markAccepted: (_) async => false,
|
|
),
|
|
throwsStateError,
|
|
);
|
|
},
|
|
);
|
|
|
|
test('failed bootstrap attempt becomes retryable after the delay', () async {
|
|
final gate = BuzzPushAttemptGate(retryDelay: Duration.zero);
|
|
addTearDown(gate.dispose);
|
|
var retries = 0;
|
|
|
|
expect(gate.tryBegin('attempt'), isTrue);
|
|
gate.failed('attempt', retry: () => retries += 1);
|
|
await Future<void>.delayed(Duration.zero);
|
|
|
|
expect(retries, 1);
|
|
expect(gate.tryBegin('attempt'), isTrue);
|
|
});
|
|
|
|
test('a new attempt cancels an obsolete scheduled retry', () async {
|
|
final gate = BuzzPushAttemptGate(retryDelay: Duration.zero);
|
|
addTearDown(gate.dispose);
|
|
var retries = 0;
|
|
|
|
expect(gate.tryBegin('old'), isTrue);
|
|
gate.failed('old', retry: () => retries += 1);
|
|
expect(gate.tryBegin('new'), isTrue);
|
|
await Future<void>.delayed(Duration.zero);
|
|
|
|
expect(retries, 0);
|
|
expect(gate.tryBegin('new'), isFalse);
|
|
});
|
|
|
|
test('successful bootstrap becomes retryable at renewal time', () async {
|
|
final gate = BuzzPushAttemptGate(retryDelay: Duration.zero);
|
|
addTearDown(gate.dispose);
|
|
var retries = 0;
|
|
|
|
expect(gate.tryBegin('attempt'), isTrue);
|
|
gate.retryAfter('attempt', delay: Duration.zero, retry: () => retries += 1);
|
|
await Future<void>.delayed(Duration.zero);
|
|
|
|
expect(retries, 1);
|
|
expect(gate.tryBegin('attempt'), isTrue);
|
|
});
|
|
|
|
test('completed bootstrap attempt can run again for later work', () {
|
|
final gate = BuzzPushAttemptGate();
|
|
addTearDown(gate.dispose);
|
|
|
|
expect(gate.tryBegin('attempt'), isTrue);
|
|
gate.complete('attempt');
|
|
expect(gate.tryBegin('attempt'), isTrue);
|
|
});
|
|
|
|
test('publication attempt changes when the relay executor rotates', () {
|
|
final subscription = BuzzPushSubscription(
|
|
filter: BuzzPushFilter(kinds: const [9], pTags: [_hex('a')]),
|
|
notificationClass: 'default',
|
|
);
|
|
final original = buzzPushPublicationAttemptKey(
|
|
communityId: 'community',
|
|
relayBaseUrl: 'https://relay.example',
|
|
token: 'token',
|
|
descriptor: _descriptor(keyId: 'relay-v1', pubkey: _hex('b')),
|
|
subscriptions: [subscription],
|
|
);
|
|
|
|
expect(
|
|
buzzPushPublicationAttemptKey(
|
|
communityId: 'community',
|
|
relayBaseUrl: 'https://relay.example',
|
|
token: 'token',
|
|
descriptor: _descriptor(keyId: 'relay-v2', pubkey: _hex('b')),
|
|
subscriptions: [subscription],
|
|
),
|
|
isNot(original),
|
|
);
|
|
expect(
|
|
buzzPushPublicationAttemptKey(
|
|
communityId: 'community',
|
|
relayBaseUrl: 'https://relay.example',
|
|
token: 'token',
|
|
descriptor: _descriptor(keyId: 'relay-v1', pubkey: _hex('c')),
|
|
subscriptions: [subscription],
|
|
),
|
|
isNot(original),
|
|
);
|
|
});
|
|
|
|
test('relay capability alone does not activate push without opt-in', () {
|
|
final disabled = Community.create(
|
|
name: 'Team',
|
|
relayUrl: 'wss://relay.example',
|
|
);
|
|
final enabled = disabled.copyWith(pushNotificationsEnabled: true);
|
|
|
|
expect(
|
|
buzzPushLifecycleEnabled(
|
|
community: disabled,
|
|
descriptor: _descriptor(keyId: 'relay-v1', pubkey: _hex('b')),
|
|
),
|
|
isFalse,
|
|
);
|
|
expect(
|
|
buzzPushLifecycleEnabled(
|
|
community: enabled,
|
|
descriptor: _descriptor(keyId: 'relay-v1', pubkey: _hex('b')),
|
|
),
|
|
isTrue,
|
|
);
|
|
expect(
|
|
buzzPushLifecycleEnabled(community: enabled, descriptor: null),
|
|
isFalse,
|
|
);
|
|
});
|
|
|
|
test('pending opt-out tombstone keeps active push lifecycle disabled', () {
|
|
final subscription = BuzzPushSubscription(
|
|
filter: BuzzPushFilter(kinds: const [9], pTags: [_hex('a')]),
|
|
notificationClass: 'default',
|
|
);
|
|
final community =
|
|
Community.create(
|
|
name: 'Team',
|
|
relayUrl: 'wss://relay.example',
|
|
).copyWith(
|
|
pushNotificationsEnabled: false,
|
|
pushSubscriptionState:
|
|
BuzzPushLeaseSubscriptionState.desired(desired: [subscription])
|
|
.withAccepted(subscriptions: [subscription], generation: 3)
|
|
.withPendingTombstone(4),
|
|
);
|
|
|
|
expect(
|
|
buzzPushLifecycleEnabled(
|
|
community: community,
|
|
descriptor: _descriptor(keyId: 'relay-v1', pubkey: _hex('b')),
|
|
),
|
|
isFalse,
|
|
);
|
|
});
|
|
|
|
test(
|
|
'relay commit followed by local failure retries at a newer generation',
|
|
() async {
|
|
var durableCursor = 0;
|
|
var relayGeneration = 0;
|
|
var acceptedGeneration = 0;
|
|
var failLocalSave = true;
|
|
|
|
Future<int> reserve() async => ++durableCursor;
|
|
Future<void> publish(int generation) async {
|
|
expect(generation, greaterThan(relayGeneration));
|
|
relayGeneration = generation;
|
|
}
|
|
|
|
Future<bool> markAccepted(int generation) async {
|
|
if (failLocalSave) {
|
|
failLocalSave = false;
|
|
throw StateError('injected local persistence failure');
|
|
}
|
|
acceptedGeneration = generation;
|
|
return true;
|
|
}
|
|
|
|
await expectLater(
|
|
publishBuzzPushLeaseRecoverably(
|
|
reserveGeneration: reserve,
|
|
publish: publish,
|
|
markAccepted: markAccepted,
|
|
),
|
|
throwsStateError,
|
|
);
|
|
expect(relayGeneration, 1);
|
|
expect(acceptedGeneration, 0);
|
|
|
|
await publishBuzzPushLeaseRecoverably(
|
|
reserveGeneration: reserve,
|
|
publish: publish,
|
|
markAccepted: markAccepted,
|
|
);
|
|
expect(relayGeneration, 2);
|
|
expect(acceptedGeneration, 2);
|
|
},
|
|
);
|
|
}
|
|
|
|
BuzzPushLeaseDescriptor _descriptor({
|
|
required String keyId,
|
|
required String pubkey,
|
|
}) => BuzzPushLeaseDescriptor(
|
|
origin: 'wss://relay.example',
|
|
executorKeyId: keyId,
|
|
executorPubkey: pubkey,
|
|
transport: 'apns',
|
|
maxLeaseTtlSeconds: 3600,
|
|
maxContentLength: 4096,
|
|
maxPlaintextLength: 4096,
|
|
maxEndpointLength: 2048,
|
|
maxStringLength: 512,
|
|
);
|
|
|
|
String _hex(String character) => List.filled(64, character).join();
|