Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ migrate_working_dir/
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
/coverage/
/build
/coverage
/devtools_options.yaml

# Symbolication related
Expand All @@ -49,4 +49,5 @@ app.*.map.json
# Generated files
**/generated/*
*.freezed.dart
*.mocks.dart
*.g.dart
10 changes: 9 additions & 1 deletion lib/providers/bootstrap_nodes.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,17 @@ import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'bootstrap_nodes.g.dart';

@Riverpod(keepAlive: true)
http.Client httpClient(Ref ref) {
final client = http.Client();
ref.onDispose(client.close);
return client;
}

@Riverpod(keepAlive: true)
Future<BootstrapNodeList> bootstrapNodes(Ref ref) async {
final response = await http.get(Uri.parse('https://nodes.tox.chat/json'));
final client = ref.watch(httpClientProvider);
final response = await client.get(Uri.parse('https://nodes.tox.chat/json'));
if (response.statusCode == 200) {
return BootstrapNodeList.fromJson(jsonDecode(response.body));
} else {
Expand Down
1 change: 1 addition & 0 deletions lib/providers/tox.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ Future<Tox> tox(Ref ref, SecretKey secretKey, ToxAddressNospam nospam) async {
savedataType: ffi.Tox_Savedata_Type.TOX_SAVEDATA_TYPE_SECRET_KEY,
);

// ignore: argument_type_not_assignable
final tox = ffi.Toxcore(await ref.read(ffi.toxFfiProvider.future), options);
ref.onDispose(tox.kill);
tox.nospam = nospam;
Expand Down
6 changes: 3 additions & 3 deletions pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -926,7 +926,7 @@ packages:
source: hosted
version: "2.0.0"
mockito:
dependency: transitive
dependency: "direct dev"
description:
name: mockito
sha256: a45d1aa065b796922db7b9e7e7e45f921aed17adf3a8318a1f47097e7e695566
Expand Down Expand Up @@ -1022,7 +1022,7 @@ packages:
source: hosted
version: "2.2.1"
path_provider_platform_interface:
dependency: transitive
dependency: "direct dev"
description:
name: path_provider_platform_interface
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
Expand Down Expand Up @@ -1054,7 +1054,7 @@ packages:
source: hosted
version: "3.1.6"
plugin_platform_interface:
dependency: transitive
dependency: "direct dev"
description:
name: plugin_platform_interface
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
Expand Down
3 changes: 3 additions & 0 deletions pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@ dev_dependencies:
freezed: ^3.2.0
glados: ^1.1.7
json_serializable: ^6.11.0
mockito: ^5.6.3
path_provider_platform_interface: ^2.1.2
plugin_platform_interface: ^2.1.8
riverpod_generator: ^4.0.0
riverpod_lint: ^3.1.0

Expand Down
52 changes: 52 additions & 0 deletions test/api/toxcore/tox_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import 'package:btox/api/toxcore/tox.dart';
import 'package:btox/ffi/generated/toxcore.ffi.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
group('ApiException', () {
test('toString() with functionName and args', () {
const exn = ApiException(Tox_Err_New.TOX_ERR_NEW_MALLOC, 'tox_new', [
'arg1',
123,
]);
expect(
exn.toString(),
'ApiException: Tox_Err_New.TOX_ERR_NEW_MALLOC in tox_new with args [arg1, 123]',
);
});

test('toString() without functionName', () {
const exn = ApiException(Tox_Err_New.TOX_ERR_NEW_MALLOC);
expect(exn.toString(), 'ApiException: Tox_Err_New.TOX_ERR_NEW_MALLOC');
});
});

test('ToxConstants constructor', () {
const constants = ToxConstants(
addressSize: 38,
conferenceIdSize: 32,
fileIdLength: 32,
groupChatIdSize: 32,
groupMaxCustomLosslessPacketLength: 100,
groupMaxCustomLossyPacketLength: 100,
groupMaxGroupNameLength: 100,
groupMaxMessageLength: 100,
groupMaxPartLength: 100,
groupMaxPasswordSize: 100,
groupMaxTopicLength: 100,
groupPeerPublicKeySize: 32,
hashLength: 32,
maxCustomPacketSize: 100,
maxFilenameLength: 100,
maxFriendRequestLength: 100,
maxHostnameLength: 100,
maxMessageLength: 100,
maxNameLength: 100,
maxStatusMessageLength: 100,
nospamSize: 4,
publicKeySize: 32,
secretKeySize: 32,
);
expect(constants.addressSize, 38);
});
}
132 changes: 132 additions & 0 deletions test/db/database_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import 'dart:typed_data';
import 'package:btox/db/database.dart';
import 'package:btox/models/content.dart';
import 'package:btox/models/crypto.dart';
import 'package:btox/models/profile_settings.dart';
import 'package:drift/native.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
late Database db;

setUp(() {
db = Database(NativeDatabase.memory());
});

tearDown(() async {
await db.close();
});

group('Database', () {
test('profiles can be added and watched', () async {
final p1 = ProfilesCompanion.insert(
settings: const ProfileSettings(
nickname: 'Test',
statusMessage: 'Available',
),
secretKey: SecretKey(Uint8List(32)),
publicKey: PublicKey(Uint8List(32)),
nospam: ToxAddressNospam(0),
);

final id = await db.addProfile(p1);
final profiles = await db.watchProfiles().first;

expect(profiles, hasLength(1));
expect(profiles.first.id, id);
});

test('activateProfile toggles active flag correctly', () async {
final p1 = ProfilesCompanion.insert(
active: const Value(true),
settings: const ProfileSettings(nickname: 'P1', statusMessage: ''),
secretKey: SecretKey(Uint8List(32)),
publicKey: PublicKey(Uint8List(32)),
nospam: ToxAddressNospam(1),
);
final p2 = ProfilesCompanion.insert(
active: const Value(false),
settings: const ProfileSettings(nickname: 'P2', statusMessage: ''),
secretKey: SecretKey(Uint8List(32)),
publicKey: PublicKey(Uint8List(32)),
nospam: ToxAddressNospam(2),
);

final id1 = await db.addProfile(p1);
final id2 = await db.addProfile(p2);

await db.activateProfile(id2);

final profile1 = await db.watchProfile(id1).first;
final profile2 = await db.watchProfile(id2).first;

expect(profile1.active, isFalse);
expect(profile2.active, isTrue);
});

test('deleteProfile cleans up contacts and messages', () async {
final pId = await db.addProfile(
ProfilesCompanion.insert(
settings: const ProfileSettings(
nickname: 'Profile',
statusMessage: '',
),
secretKey: SecretKey(Uint8List(32)),
publicKey: PublicKey(Uint8List(32)),
nospam: ToxAddressNospam(0),
),
);

final cId = await db.addContact(
ContactsCompanion.insert(
profileId: pId,
publicKey: PublicKey(Uint8List(32)),
name: const Value('Test Contact'),
),
);

await db.addMessage(
MessagesCompanion.insert(
contactId: cId,
author: PublicKey(Uint8List(32)),
sha: Sha256(Uint8List(32)),
timestamp: DateTime.now(),
content: const TextContent(text: 'Hello'),
),
);

await db.deleteProfile(pId);

final profiles = await db.watchProfiles().first;
expect(profiles, isEmpty);

// Verify contacts for that profile are gone.
final contacts = await db.watchContactsFor(pId).first;
expect(contacts, isEmpty);
});

test('updateProfileSettings updates only requested profile', () async {
final id = await db.addProfile(
ProfilesCompanion.insert(
settings: const ProfileSettings(
nickname: 'Old Name',
statusMessage: '',
),
secretKey: SecretKey(Uint8List(32)),
publicKey: PublicKey(Uint8List(32)),
nospam: ToxAddressNospam(0),
),
);

const newSettings = ProfileSettings(
nickname: 'New Name',
statusMessage: 'Busy',
);
await db.updateProfileSettings(id, newSettings);

final profile = await db.watchProfile(id).first;
expect(profile.settings.nickname, 'New Name');
expect(profile.settings.statusMessage, 'Busy');
});
});
}
66 changes: 66 additions & 0 deletions test/db/native_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import 'dart:io';

import 'package:btox/db/native.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:path_provider_platform_interface/path_provider_platform_interface.dart';
import 'package:plugin_platform_interface/plugin_platform_interface.dart';

class MockPathProvider extends Fake
with MockPlatformInterfaceMixin
implements PathProviderPlatform {
final String _path;
MockPathProvider(this._path);

@override
Future<String?> getApplicationDocumentsPath() async {
return _path;
}
}

void main() {
TestWidgetsFlutterBinding.ensureInitialized();

late Directory tempDir;

setUp(() {
tempDir = Directory.systemTemp.createTempSync('btox_db_test');
PathProviderPlatform.instance = MockPathProvider(tempDir.path);
});

tearDown(() {
if (tempDir.existsSync()) {
tempDir.deleteSync(recursive: true);
}
});

test('constructDb creates a database file', () async {
try {
final db = await constructDb('test-key');
expect(db, isNotNull);

// We need to trigger a database opening to see the file.
await db.customSelect('SELECT 1').get();

final file = File('${tempDir.path}/bTox.sqlite');
expect(file.existsSync(), isTrue);

await db.close();
} on Exception catch (e) {
if (e.toString().contains('Failed to get cipher version') ||
e.toString().contains('SqliteException')) {
// SQLCipher or SQLite might not be fully functional in this environment.
markTestSkipped('SQL/Cipher not fully available on this host: $e');
return;
}
rethrow;
} catch (e) {
// Catch other errors that might happen due to native loading failures.
markTestSkipped('Native loading failure on this host: $e');
}
});
}

void markTestSkipped(String message) {
// ignore: avoid_print
print('SKIPPED: $message');
}
Loading
Loading