Skip to content

Commit

Permalink
Merge pull request #1222 from atsign-foundation/enroll_list_client_2
Browse files Browse the repository at this point in the history
feat: add new method to fetch enrollment requests
  • Loading branch information
murali-shris authored Jan 29, 2024
2 parents 302bd74 + a68033f commit 5fa927d
Show file tree
Hide file tree
Showing 12 changed files with 251 additions and 17 deletions.
4 changes: 3 additions & 1 deletion packages/at_client/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
## 3.0.75
- feat: Introduce feature to fetch enrollment requests from the server
## 3.0.74
- build[deps]: Upgraded dependencies for the following packages:
- at_chops to v2.0.0
Expand All @@ -9,7 +11,7 @@
- at_lookup to v3.0.44
- at_chops to v1.0.7
- at_persistence_secondary_server to v3.0.60
- feat: Replace encryption methods from EncryptionUtils with AtChops method
- feat: Replace encryption methods from EncryptionUtils with AtChops method
## 3.0.72
- chore: Minor change to allow us to support dart
versions both before and after 3.2.0 specifically for this
Expand Down
2 changes: 2 additions & 0 deletions packages/at_client/lib/at_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ export 'package:at_client/src/preference/at_client_preference.dart';
export 'package:at_client/src/response/at_notification.dart';
export 'package:at_client/src/util/at_client_util.dart';
export 'package:at_client/src/util/encryption_util.dart';
export 'package:at_client/src/util/enrollment_request.dart';
export 'package:at_client/src/util/enroll_list_request_param.dart';
export 'package:at_client/src/service/notification_service.dart';
export 'package:at_client/src/service/sync_service.dart';
export 'package:at_client/src/service/sync/sync_result.dart';
Expand Down
30 changes: 30 additions & 0 deletions packages/at_client/lib/src/client/at_client_impl.dart
Original file line number Diff line number Diff line change
Expand Up @@ -649,6 +649,36 @@ class AtClientImpl implements AtClient, AtSignChangeListener {
return AtChopsImpl(atChopsKeys);
}

@override
Future<List<EnrollmentRequest>> fetchEnrollmentRequests(
EnrollListRequestParam enrollmentListRequestParams) async {
// enrollmentListRequestParams for now is not used
// A server side enhancement request is created. https://github.com/atsign-foundation/at_server/issues/1748
// On implementation of this enhancement/feature, the enrollListRequestParam object can be made use of
EnrollVerbBuilder enrollBuilder = EnrollVerbBuilder()
..operation = EnrollOperationEnum.list
..appName = enrollmentListRequestParams.appName
..deviceName = enrollmentListRequestParams.deviceName;

var response = await getRemoteSecondary()
?.executeCommand(enrollBuilder.buildCommand(), auth: true);

return _formatEnrollListResponse(response);
}

List<EnrollmentRequest> _formatEnrollListResponse(response) {
response = response?.replaceFirst('data:', '');
Map<String, dynamic> enrollRequests = jsonDecode(response!);
List<EnrollmentRequest> enrollRequestsFormatted = [];
EnrollmentRequest? enrollment;
for (var request in enrollRequests.entries) {
enrollment = EnrollmentRequest.fromJson(request.value);
enrollment.enrollmentKey = request.key;
enrollRequestsFormatted.add(enrollment);
}
return enrollRequestsFormatted;
}

@override
Future<AtStreamResponse> stream(String sharedWith, String filePath,
{String? namespace}) async {
Expand Down
15 changes: 15 additions & 0 deletions packages/at_client/lib/src/client/at_client_spec.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'dart:io';

import 'package:at_client/at_client.dart';
import 'package:at_client/src/client/local_secondary.dart';
import 'package:at_client/src/client/remote_secondary.dart';
import 'package:at_client/src/manager/sync_manager.dart';
Expand Down Expand Up @@ -572,4 +573,18 @@ abstract class AtClient {
String? getCurrentAtSign();

EncryptionService? get encryptionService;

/// Fetches all enrollment requests from the corresponding atServer; Formats the requests into a
/// List<[EnrollmentResponse]>
///
/// Responses can be filtered using params provided through [EnrollListRequestParam]
/// ```
/// e.g.
/// List<EnrollmentRequest> enrollmentRequests = fetchEnrollmentRequests(EnrollRequestParams());
/// enrollmentRequests now contains all the enrollment requests fetched from the server in the for of
/// EnrollmentRequest objects
/// ```
@experimental
Future<List<EnrollmentRequest>> fetchEnrollmentRequests(
EnrollListRequestParam enrollmentListRequest);
}
14 changes: 4 additions & 10 deletions packages/at_client/lib/src/client/remote_secondary.dart
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,10 @@ class RemoteSecondary implements Secondary {
logger = AtSignLogger('RemoteSecondary ($_atSign)');
_preference = preference;
privateKey ??= preference.privateKey;
SecureSocketConfig secureSocketConfig = SecureSocketConfig();
secureSocketConfig.decryptPackets = preference.decryptPackets;
secureSocketConfig.pathToCerts = preference.pathToCerts;
secureSocketConfig.tlsKeysSavePath = preference.tlsKeysSavePath;
SecureSocketConfig secureSocketConfig = SecureSocketConfig()
..decryptPackets = preference.decryptPackets
..pathToCerts = preference.pathToCerts
..tlsKeysSavePath = preference.tlsKeysSavePath;
atLookUp = AtLookupImpl(atSign, preference.rootDomain, preference.rootPort,
privateKey: privateKey,
cramSecret: preference.cramSecret,
Expand Down Expand Up @@ -107,14 +107,8 @@ class RemoteSecondary implements Secondary {
// ignore: prefer_typing_uninitialized_variables
var verbResult;
try {
logger.finer(logger.getLogMessageWithClientParticulars(
_preference.atClientParticulars,
'Command sent to server: ${builder.buildCommand()}'));
verbResult = await executeVerb(builder);
verbResult = verbResult.replaceFirst('data:', '');
logger.finer(logger.getLogMessageWithClientParticulars(
_preference.atClientParticulars,
'Response from server: $verbResult'));
} on AtException catch (e) {
throw e
..stack(AtChainedException(Intent.fetchData,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ class AtClientConfig {

/// Represents the at_client version.
/// Must always be the same as the actual version in pubspec.yaml
final String atClientVersion = '3.0.74';
final String atClientVersion = '3.0.75';

/// Represents the client commit log compaction time interval
///
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/// class to store request parameters while fetching a list of enrollments
class EnrollListRequestParam {
String? appName;
String? deviceName;
String? namespace;
}
40 changes: 40 additions & 0 deletions packages/at_client/lib/src/util/enrollment_request.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import 'dart:collection';
import 'dart:convert';

class EnrollmentRequest {
late String enrollmentKey;
late String appName;
late String deviceName;
late Map<String, dynamic> namespace;

String get enrollmentId {
return extractEnrollmentId(enrollmentKey);
}

@override
String toString() {
return 'Enrollment Request: enrollmentKey: $enrollmentKey | appName: $appName | deviceName: $deviceName | namespace: ${namespace.toString()}';
}

Map<String, dynamic> toJson() {
Map<String, dynamic> jsonMap = HashMap();
jsonMap['enrollmentKey'] = enrollmentKey;
jsonMap['appName'] = appName;
jsonMap['deviceName'] = deviceName;
jsonMap['namespace'] = jsonEncode(namespace);

return jsonMap;
}

static EnrollmentRequest fromJson(Map<String, dynamic> json) {
EnrollmentRequest enrollmentRequest = EnrollmentRequest();
return enrollmentRequest
..appName = json['appName']
..deviceName = json['deviceName']
..namespace = json['namespace'];
}

static String extractEnrollmentId(String enrollmentKey) {
return enrollmentKey.split('.')[0];
}
}
4 changes: 2 additions & 2 deletions packages/at_client/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ description: The at_client library is the non-platform specific Client SDK which
##
##
## NB: When incrementing the version, please also increment the version in AtClientConfig file
version: 3.0.74
version: 3.0.75
## NB: When incrementing the version, please also increment the version in AtClientConfig file
##

Expand Down Expand Up @@ -46,4 +46,4 @@ dev_dependencies:
at_demo_data: ^1.0.1
coverage: ^1.5.0
mocktail: ^0.3.0
dart_code_metrics: ^4.17.0
dart_code_metrics: ^4.17.0
64 changes: 63 additions & 1 deletion packages/at_client/test/at_client_impl_test.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import 'dart:convert';

import 'package:at_client/at_client.dart';
import 'package:at_client/src/compaction/at_commit_log_compaction.dart';
import 'package:at_client/src/service/notification_service_impl.dart';
Expand All @@ -20,6 +22,8 @@ class MockAtCompactionJob extends Mock implements AtCompactionJob {
}
}

class MockRemoteSecondary extends Mock implements RemoteSecondary {}

void main() {
group('A group of at client impl create tests', () {
final String atSign = '@alice';
Expand Down Expand Up @@ -251,7 +255,7 @@ void main() {
});
});

group('A group of tests related to setting enrollmentId', () {
group('A group of tests related to apkam/enrollments', () {
test(
'A test to verify enrollmentId is set in atClient after calling setCurrentAtSign',
() async {
Expand All @@ -261,5 +265,63 @@ void main() {
enrollmentId: testEnrollmentId);
expect(atClientManager.atClient.enrollmentId, testEnrollmentId);
});

MockRemoteSecondary mockRemoteSecondary = MockRemoteSecondary();

test('verify behaviour of fetchEnrollmentRequests()', () async {
String currentAtsign = '@apkam';
String enrollKey1 =
'0acdeb4d-1a2e-43e4-93bd-378f1d366ea7.new.enrollments.__manage$currentAtsign';
String enrollValue1 =
'{"appName":"buzz","deviceName":"pixel","namespace":{"buzz":"rw"}}';
String enrollKey2 =
'9beefa26-3384-4f10-81a6-0deaa4332669.new.enrollments.__manage$currentAtsign';
String enrollValue2 =
'{"appName":"buzz","deviceName":"pixel","namespace":{"buzz":"rw"}}';
String enrollKey3 =
'a6bbef17-c7bf-46f4-a172-1ed7b3b443bc.new.enrollments.__manage$currentAtsign';
String enrollValue3 =
'{"appName":"buzz","deviceName":"pixel","namespace":{"buzz":"rw"}}';
when(() =>
mockRemoteSecondary.executeCommand('enroll:list\n',
auth: true)).thenAnswer((_) => Future.value('data:{"$enrollKey1":'
'$enrollValue1,"$enrollKey2":$enrollValue2,"$enrollKey3":$enrollValue3}'));

AtClient? client = await AtClientImpl.create(
currentAtsign, 'buzz', AtClientPreference(),
remoteSecondary: mockRemoteSecondary);
AtClientImpl? clientImpl = client as AtClientImpl;

List<EnrollmentRequest> requests =
await clientImpl.fetchEnrollmentRequests(EnrollListRequestParam());
expect(requests.length, 3);
expect(requests[0].enrollmentKey, enrollKey1);
expect(requests[0].appName, jsonDecode(enrollValue1)['appName']);
expect(requests[0].deviceName, jsonDecode(enrollValue1)['deviceName']);
expect(requests[0].namespace, jsonDecode(enrollValue1)['namespace']);
// the following statement asserts that the enrollment.enrollmentId getter fetches the correct enrollment id
expect(requests[0].enrollmentKey.contains(enrollKey1), true);

expect(requests[1].enrollmentKey, enrollKey2);
expect(requests[1].appName, jsonDecode(enrollValue2)['appName']);
expect(requests[1].deviceName, jsonDecode(enrollValue2)['deviceName']);
expect(requests[1].namespace, jsonDecode(enrollValue2)['namespace']);
expect(requests[1].enrollmentKey.contains(enrollKey2), true);

expect(requests[2].enrollmentKey, enrollKey3);
expect(requests[2].appName, jsonDecode(enrollValue3)['appName']);
expect(requests[2].deviceName, jsonDecode(enrollValue3)['deviceName']);
expect(requests[2].namespace, jsonDecode(enrollValue3)['namespace']);
expect(requests[2].enrollmentKey.contains(enrollKey3), true);
});

test('validate EnrollRequest.extractEnrollmentId()', () {
String enrollmentKey =
'0acdeb4d-1a2e-43e4-93bd-378f1d366ea7.new.enrollments.__manage@random';
String enrollmentId = '0acdeb4d-1a2e-43e4-93bd-378f1d366ea7';

expect(
EnrollmentRequest.extractEnrollmentId(enrollmentKey), enrollmentId);
});
});
}
74 changes: 72 additions & 2 deletions tests/at_functional_test/test/enrollment_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ void main() {
await setLastReceivedNotificationDateTime();
});

void _stopSubscriptions() {
void stopSubscriptions() {
atClientManager.atClient.notificationService.stopAllSubscriptions();
print('subscriptions stopped');
}
Expand Down Expand Up @@ -87,9 +87,75 @@ void main() {
print('got enrollment notification: $enrollNotification');
expect(enrollNotification.key,
'$enrollmentIdFromServer.new.enrollments.__manage');
_stopSubscriptions();
stopSubscriptions();
}, count: 1, max: 1));
});

test(
'validate client functionality to fetch pending enrollments on legacy pkam authenticated client',
() async {
atClientManager = await TestUtils.initAtClient(atSign, 'new_app');
AtClient? client = atClientManager.atClient;
// fetch first otp
String? otp =
await TestUtils.executeCommandAndParse(client, 'otp:get', auth: true);
expect(otp, isNotNull);
// create first enrollment request
RemoteSecondary? secondRemoteSecondary =
RemoteSecondary(atSign, getClient2Preferences());
var apkamPublicKey =
at_demos.pkamPublicKeyMap['@eve🛠']; // can be any random public key
var newEnrollRequest = TestUtils.formatCommand(
'enroll:request:{"appName":"new_app","deviceName":"pixel","namespaces":{"new_app":"rw"},"otp":"$otp","apkamPublicKey":"$apkamPublicKey"}');
var enrollResponse = await TestUtils.executeCommandAndParse(
null, newEnrollRequest,
remoteSecondary: secondRemoteSecondary);
Map<String, dynamic> enrollResponse1JsonDecoded =
jsonDecode(enrollResponse!);
expect(enrollResponse1JsonDecoded['enrollmentId'], isNotNull);
expect(enrollResponse1JsonDecoded['status'], 'pending');

// fetch second otp
otp = await TestUtils.executeCommandAndParse(client, 'otp:get', auth: true);
expect(otp, isNotNull);
// create second enrollment request
newEnrollRequest = TestUtils.formatCommand(
'enroll:request:{"appName":"new_app","deviceName":"pixel7","namespaces":{"new_app":"rw", "wavi":"r"},"otp":"$otp","apkamPublicKey":"$apkamPublicKey"}');
enrollResponse = await TestUtils.executeCommandAndParse(
null, newEnrollRequest,
remoteSecondary: secondRemoteSecondary);
var enrollResponse2JsonDecoded = jsonDecode(enrollResponse!);
expect(enrollResponse2JsonDecoded['enrollmentId'], isNotNull);
expect(enrollResponse2JsonDecoded['status'], 'pending');

// fetch enrollment requests through client
List<EnrollmentRequest> enrollmentRequests =
await client.fetchEnrollmentRequests(EnrollListRequestParam());

expect(enrollmentRequests.length, 4);
// 4 entries - 2 entries from this test
// + 2 entries from the other test in this file.

String firstEnrollmentKey =
getEnrollmentKey(enrollResponse1JsonDecoded['enrollmentId'], atSign);
String secondEnrollmentKey =
getEnrollmentKey(enrollResponse2JsonDecoded['enrollmentId'], atSign);
int matchCount = 0;
for (var request in enrollmentRequests) {
if (request.enrollmentKey == firstEnrollmentKey) {
expect(request.namespace['new_app'], 'rw');
expect(request.deviceName, 'pixel');
matchCount++;
} else if (request.enrollmentKey == secondEnrollmentKey) {
expect(request.namespace['new_app'], 'rw');
expect(request.namespace['wavi'], 'r');
expect(request.deviceName, 'pixel7');
matchCount++;
}
}
// this counter is to assert that the list of requests has exactly two request matches
expect(matchCount, 2);
});
}

Future<void> setLastReceivedNotificationDateTime() async {
Expand All @@ -115,6 +181,10 @@ Future<void> setLastReceivedNotificationDateTime() async {
.put(lastReceivedNotificationAtKey, jsonEncode(atNotification.toJson()));
}

String getEnrollmentKey(String enrollmentId, String atsign) {
return '$enrollmentId.new.enrollments.__manage$atsign';
}

AtClientPreference getClient2Preferences() {
return AtClientPreference()
..commitLogPath = 'test/hive/client_2/commit'
Expand Down
13 changes: 13 additions & 0 deletions tests/at_functional_test/test/test_utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import 'package:at_client/at_client.dart';
import 'package:at_functional_test/src/at_demo_credentials.dart'
as demo_credentials;


class TestUtils {
static AtClientPreference getPreference(String atsign) {
var preference = AtClientPreference();
Expand Down Expand Up @@ -52,4 +53,16 @@ class TestUtils {
atClientManager.atClient, currentAtSign);
return atClientManager;
}

static String formatCommand(String command){
if(!command.contains('\n')) return '$command\n';
return command;
}

static Future<String?> executeCommandAndParse(AtClient? client, command, {bool auth = false, RemoteSecondary? remoteSecondary}) async {
remoteSecondary ??= client?.getRemoteSecondary();
String? response = await remoteSecondary?.executeCommand(formatCommand(command), auth: auth);
print('Command: $command -> Response: $response');
return response?.replaceFirst('data:', '');
}
}

0 comments on commit 5fa927d

Please sign in to comment.