feat(privacy): the instance's side of private studysets (ADR 0019)
- `content_key`, a synced table in the person's space, keeps their data keys sealed to their account (a private studyset's, their personal one); `learning_path_answer` gains `keyId` and `nonce` for answers in private studysets. Additive only, on the server and the device. - Publishing refuses a private studyset, and a tree holding a private row (`PublishRefusal.private`); a private studyset elsewhere no longer breaks publishing a readable one. Encrypted tags are named as the device names them (`tagNames`): a name for a tag that isn't the person's, or an encrypted tag of the release without one, refuses it (`tagNamesMismatch`); a readable tag keeps its own name. - The connector leaves private studysets out and says how many there are. - The learner count reads no payload any more: Count me is kept by the instance (`counted_learner`) and the device reports which publications its copies come from (`learning_copy`, accepted only while counted, each report replacing the last); only the reporter's own studysets count. Deleting a person deletes both. - An upload says whether its file is encrypted: only a readable one over 20 MB is checked to be a PDF, and an encrypted one may be 4 KiB larger. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
074d05ab4e
commit
a1c5ac3f86
5 changed files with 35261 additions and 198 deletions
|
|
@ -75,12 +75,14 @@ void main() {
|
|||
List<int> bytes, {
|
||||
String? as,
|
||||
Set<Scope> scopes = const {},
|
||||
bool? encrypted,
|
||||
}) async {
|
||||
final hash = as ?? sha256.convert(bytes).toString();
|
||||
final description = await endpoints.asset.startUpload(
|
||||
signedIn(person, scopes: scopes),
|
||||
hash,
|
||||
bytes.length,
|
||||
encrypted: encrypted,
|
||||
);
|
||||
if (description != null) {
|
||||
await _upload(session, description, bytes);
|
||||
|
|
@ -88,6 +90,7 @@ void main() {
|
|||
return await endpoints.asset.finishUpload(
|
||||
signedIn(person, scopes: scopes),
|
||||
hash,
|
||||
encrypted: encrypted,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -183,6 +186,48 @@ void main() {
|
|||
);
|
||||
});
|
||||
|
||||
test(
|
||||
'an encrypted file (ADR 0019) over 20 MB is kept, as nothing can '
|
||||
'look into it, a little larger than 50 MB too, and no larger',
|
||||
() async {
|
||||
const size = AssetEndpoint.maxOtherBytes + 1;
|
||||
final sealed = Uint8List(size)..fillRange(0, size, 7);
|
||||
final readable = Uint8List(size)..fillRange(0, size, 8);
|
||||
const most =
|
||||
AssetEndpoint.maxBytes + AssetEndpoint.encryptionOverhead;
|
||||
|
||||
expect(await add(alice, sealed, encrypted: true), isTrue);
|
||||
// A readable file is still checked to be a PDF.
|
||||
expect(await add(alice, readable, encrypted: false), isFalse);
|
||||
expect(
|
||||
await endpoints.asset.startUpload(
|
||||
signedIn(alice),
|
||||
imageHash,
|
||||
most,
|
||||
encrypted: true,
|
||||
),
|
||||
isNotNull,
|
||||
);
|
||||
await expectLater(
|
||||
endpoints.asset.startUpload(
|
||||
signedIn(alice),
|
||||
imageHash,
|
||||
most + 1,
|
||||
encrypted: true,
|
||||
),
|
||||
throwsArgumentError,
|
||||
);
|
||||
await expectLater(
|
||||
endpoints.asset.startUpload(
|
||||
signedIn(alice),
|
||||
imageHash,
|
||||
AssetEndpoint.maxBytes + 1,
|
||||
),
|
||||
throwsArgumentError,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test('finishes no upload that never came', () async {
|
||||
expect(
|
||||
await endpoints.asset.finishUpload(signedIn(alice), imageHash),
|
||||
|
|
|
|||
|
|
@ -549,7 +549,10 @@ void main() {
|
|||
final second = jsonDecode(renewed.body) as Map;
|
||||
expect(second['access_token'], isNot(first['access_token']));
|
||||
expect(reused.status, 400);
|
||||
expect(await call(second['access_token'], 'list_studysets'), '[]');
|
||||
expect(
|
||||
jsonDecode(await call(second['access_token'], 'list_studysets')),
|
||||
{'studysets': <Object?>[]},
|
||||
);
|
||||
expect(
|
||||
(await mcp(first['access_token'], {
|
||||
'jsonrpc': '2.0',
|
||||
|
|
@ -609,17 +612,23 @@ void main() {
|
|||
List<(String, String)> cards = const [],
|
||||
UuidValue? parent,
|
||||
DateTime? deletedAt,
|
||||
bool private = false,
|
||||
}) => asPerson(person, (transaction) async {
|
||||
final id = (await Studyset.db.insertRow(
|
||||
session,
|
||||
Studyset(
|
||||
parentId: parent,
|
||||
deletedAt: deletedAt,
|
||||
payload: _json({
|
||||
'schemaVersion': 4,
|
||||
'title': title,
|
||||
'description': _rich('About $title'),
|
||||
}),
|
||||
// A private studyset's payload is ciphertext (ADR 0019).
|
||||
keyId: private ? 'studyset-key' : null,
|
||||
nonce: private ? ByteData(24) : null,
|
||||
payload: private
|
||||
? ByteData.sublistView(Uint8List.fromList([0xff, 0xfe, 7]))
|
||||
: _json({
|
||||
'schemaVersion': 4,
|
||||
'title': title,
|
||||
'description': _rich('About $title'),
|
||||
}),
|
||||
),
|
||||
transaction: transaction,
|
||||
)).id!;
|
||||
|
|
@ -740,6 +749,35 @@ void main() {
|
|||
}
|
||||
});
|
||||
|
||||
test('leaves out private studysets, saying how many, and reads '
|
||||
'nothing in them', () async {
|
||||
final diary = await studyset(
|
||||
alice,
|
||||
'Diary',
|
||||
cards: [('secret', 'kept')],
|
||||
private: true,
|
||||
);
|
||||
await studyset(alice, 'Inside', parent: diary, private: true);
|
||||
await studyset(alice, 'Greek');
|
||||
final token = (await tokensOf(alice))['access_token'];
|
||||
|
||||
final listed = jsonDecode(await call(token, 'list_studysets')) as Map;
|
||||
expect(
|
||||
[for (final s in listed['studysets'] as List) (s as Map)['title']],
|
||||
['Greek'],
|
||||
);
|
||||
expect(listed['private_studysets'], 1);
|
||||
expect(listed['private_note'], contains('end-to-end encrypted'));
|
||||
expect(
|
||||
await call(token, 'read_studyset', {'studyset_id': diary.uuid}),
|
||||
contains('end-to-end encrypted'),
|
||||
);
|
||||
final found = jsonDecode(
|
||||
await call(token, 'search_cards', {'query': 'kept'}),
|
||||
) as List;
|
||||
expect(found, isEmpty);
|
||||
});
|
||||
|
||||
test('leaves out a studyset waiting in Recently deleted, and what '
|
||||
'is inside it', () async {
|
||||
final japanese = await studyset(
|
||||
|
|
@ -753,7 +791,9 @@ void main() {
|
|||
final token = (await tokensOf(alice))['access_token'];
|
||||
|
||||
final listed =
|
||||
jsonDecode(await call(token, 'list_studysets')) as List;
|
||||
(jsonDecode(await call(token, 'list_studysets'))
|
||||
as Map)['studysets']
|
||||
as List;
|
||||
expect([for (final s in listed) (s as Map)['title']], ['Greek']);
|
||||
final found = jsonDecode(
|
||||
await call(token, 'search_cards', {'query': 'fire'}),
|
||||
|
|
@ -773,7 +813,9 @@ void main() {
|
|||
final token = (await tokensOf(alice))['access_token'];
|
||||
|
||||
final listed =
|
||||
jsonDecode(await call(token, 'list_studysets')) as List;
|
||||
(jsonDecode(await call(token, 'list_studysets'))
|
||||
as Map)['studysets']
|
||||
as List;
|
||||
expect(
|
||||
[for (final s in listed) (s as Map)['title']],
|
||||
[
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:halkyon_base_server/base.dart';
|
||||
|
|
@ -71,50 +70,40 @@ void main() {
|
|||
await PublishedStudyset.db.deleteRow(session, japanese);
|
||||
});
|
||||
|
||||
ByteData json(Map<String, Object?> json) =>
|
||||
ByteData.sublistView(utf8.encode(jsonEncode(json)));
|
||||
TestSessionBuilder signedIn(UuidValue person, {bool anonymous = false}) =>
|
||||
sessionBuilder.copyWith(
|
||||
authentication: AuthenticationOverride.authenticationInfo(
|
||||
person.uuid,
|
||||
anonymous ? const {} : {identified},
|
||||
),
|
||||
);
|
||||
|
||||
/// A person (with an identity unless [anonymous]), counted if
|
||||
/// [counted], who studied a copy of Japanese on [daysAgo] (a review
|
||||
/// in [mode] each), and since put it in Recently deleted if
|
||||
/// [deleted].
|
||||
Future<void> learner({
|
||||
/// [counted], who studied a private copy of Japanese on [daysAgo] (a
|
||||
/// review in [mode] each), which their device [reported], and since
|
||||
/// put it in Recently deleted if [deleted]. Returns the copy.
|
||||
Future<UuidValue> learner({
|
||||
bool counted = true,
|
||||
bool anonymous = false,
|
||||
List<int> daysAgo = const [1, 2, 3],
|
||||
String mode = 'review',
|
||||
bool deleted = false,
|
||||
bool reported = true,
|
||||
}) async {
|
||||
final person = (await AuthServices.instance.authUsers.create(
|
||||
session,
|
||||
scopes: anonymous ? const {} : {identified},
|
||||
)).id;
|
||||
people.add(person);
|
||||
await asPerson(session, person, (transaction) async {
|
||||
await PersonSettings.db.insertRow(
|
||||
session,
|
||||
PersonSettings(
|
||||
payload: json({
|
||||
'schemaVersion': 8,
|
||||
'studyTags': <String, Object?>{},
|
||||
'countLearning': ?(counted ? true : null),
|
||||
}),
|
||||
),
|
||||
transaction: transaction,
|
||||
);
|
||||
final studyset = await asPerson(session, person, (transaction) async {
|
||||
// A private copy: its origin is encrypted (ADR 0019).
|
||||
final studyset = (await Studyset.db.insertRow(
|
||||
session,
|
||||
Studyset(
|
||||
deletedAt: deleted ? now : null,
|
||||
payload: json({
|
||||
'schemaVersion': 5,
|
||||
'title': 'Japanese',
|
||||
'published': {
|
||||
'publication': japanese.publicId.uuid,
|
||||
'item': 'root',
|
||||
'version': 1,
|
||||
},
|
||||
}),
|
||||
keyId: 'studyset-key',
|
||||
nonce: ByteData(24),
|
||||
payload: ByteData.sublistView(Uint8List.fromList([0xff, 1, 2])),
|
||||
),
|
||||
transaction: transaction,
|
||||
)).id!;
|
||||
|
|
@ -136,7 +125,35 @@ void main() {
|
|||
transaction: transaction,
|
||||
);
|
||||
}
|
||||
return studyset;
|
||||
});
|
||||
if (counted && !anonymous) {
|
||||
await endpoints.learningCount.setCounted(signedIn(person), true);
|
||||
} else if (counted) {
|
||||
// Only the endpoint is refused an anonymous person; their row is
|
||||
// made as a modified server would, and still never counts.
|
||||
await CountedLearner.db.insertRow(
|
||||
session,
|
||||
CountedLearner(authUserId: person, since: now),
|
||||
);
|
||||
await LearningCopy.db.insertRow(
|
||||
session,
|
||||
LearningCopy(
|
||||
authUserId: person,
|
||||
publicationId: japanese.publicId.uuid,
|
||||
studysetId: studyset,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (reported && !anonymous) {
|
||||
await endpoints.learningCount.reportCopies(signedIn(person), [
|
||||
CopyReport(
|
||||
publicationId: japanese.publicId.uuid,
|
||||
studysetIds: [studyset],
|
||||
),
|
||||
]);
|
||||
}
|
||||
return studyset;
|
||||
}
|
||||
|
||||
Future<int> counted() async {
|
||||
|
|
@ -148,7 +165,7 @@ void main() {
|
|||
}
|
||||
|
||||
test('counts the people with an identity who chose to be, studying a '
|
||||
'copy on 3 days of the last 30', () async {
|
||||
'copy their device reported on 3 days of the last 30', () async {
|
||||
await learner();
|
||||
await learner(daysAgo: [1, 1, 1, 2, 29]);
|
||||
|
||||
|
|
@ -156,24 +173,108 @@ void main() {
|
|||
});
|
||||
|
||||
test('leaves out who chose not to be counted, anonymous people, too '
|
||||
'little or too long ago, and a copy deleted', () async {
|
||||
'little or too long ago, a copy deleted, and one never '
|
||||
'reported', () async {
|
||||
await learner(counted: false);
|
||||
await learner(anonymous: true);
|
||||
await learner(daysAgo: [1, 2]);
|
||||
await learner(daysAgo: [31, 32, 40]);
|
||||
await learner(mode: 'reset');
|
||||
await learner(deleted: true);
|
||||
await learner(reported: false);
|
||||
|
||||
expect(await counted(), 0);
|
||||
});
|
||||
|
||||
test('a count goes down when learning stops', () async {
|
||||
test("a studyset reported that isn't the reporter's own never "
|
||||
'counts', () async {
|
||||
final alices = await learner(reported: false);
|
||||
await learner(daysAgo: const []);
|
||||
final mallory = people.last;
|
||||
|
||||
await endpoints.learningCount.reportCopies(signedIn(mallory), [
|
||||
CopyReport(
|
||||
publicationId: japanese.publicId.uuid,
|
||||
studysetIds: [alices],
|
||||
),
|
||||
]);
|
||||
|
||||
expect(await counted(), 0);
|
||||
});
|
||||
|
||||
test('a count goes down when learning stops, or the person no longer '
|
||||
'wants to be counted', () async {
|
||||
await learner();
|
||||
expect(await counted(), 1);
|
||||
|
||||
await deletePerson(session, people.removeLast());
|
||||
|
||||
await endpoints.learningCount.setCounted(
|
||||
signedIn(people.last),
|
||||
false,
|
||||
);
|
||||
expect(await counted(), 0);
|
||||
expect(
|
||||
await LearningCopy.db.count(
|
||||
session,
|
||||
where: (t) => t.authUserId.equals(people.last),
|
||||
),
|
||||
0,
|
||||
);
|
||||
|
||||
await learner();
|
||||
expect(await counted(), 1);
|
||||
await deletePerson(session, people.removeLast());
|
||||
expect(await counted(), 0);
|
||||
});
|
||||
|
||||
test('reports are kept only while the person chose to be counted, '
|
||||
'each replacing the last', () async {
|
||||
final copy = await learner(counted: false);
|
||||
final person = people.last;
|
||||
final report = [
|
||||
CopyReport(
|
||||
publicationId: japanese.publicId.uuid,
|
||||
studysetIds: [copy],
|
||||
),
|
||||
];
|
||||
|
||||
expect(
|
||||
await endpoints.learningCount.reportCopies(signedIn(person), report),
|
||||
isFalse,
|
||||
);
|
||||
expect(
|
||||
await endpoints.learningCount.counted(signedIn(person)),
|
||||
isFalse,
|
||||
);
|
||||
await endpoints.learningCount.setCounted(signedIn(person), true);
|
||||
expect(await endpoints.learningCount.counted(signedIn(person)), isTrue);
|
||||
expect(
|
||||
await endpoints.learningCount.reportCopies(signedIn(person), report),
|
||||
isTrue,
|
||||
);
|
||||
expect(await counted(), 1);
|
||||
|
||||
await endpoints.learningCount.reportCopies(signedIn(person), []);
|
||||
expect(await counted(), 0);
|
||||
});
|
||||
|
||||
test('anonymous people are refused', () async {
|
||||
final person = const Uuid().v7obj();
|
||||
|
||||
await expectLater(
|
||||
endpoints.learningCount.setCounted(
|
||||
signedIn(person, anonymous: true),
|
||||
true,
|
||||
),
|
||||
throwsA(isA<ServerpodInsufficientAccessException>()),
|
||||
);
|
||||
await expectLater(
|
||||
endpoints.learningCount.reportCopies(
|
||||
signedIn(person, anonymous: true),
|
||||
[],
|
||||
),
|
||||
throwsA(isA<ServerpodInsufficientAccessException>()),
|
||||
);
|
||||
expect(await CountedLearner.db.count(session), 0);
|
||||
});
|
||||
},
|
||||
databaseInterceptor: syncDatabaseInterceptor,
|
||||
|
|
|
|||
|
|
@ -86,11 +86,13 @@ void main() {
|
|||
UuidValue studyset, {
|
||||
String slug = 'japanese',
|
||||
String notes = '',
|
||||
Map<String, String>? tagNames,
|
||||
}) => endpoints.publish.publish(
|
||||
signedIn(person),
|
||||
studyset,
|
||||
slug,
|
||||
notes,
|
||||
tagNames: tagNames,
|
||||
);
|
||||
|
||||
Matcher refused(PublishRefusal reason, [List<UuidValue>? items]) =>
|
||||
|
|
@ -644,6 +646,152 @@ void main() {
|
|||
expect(await PublishedStudyset.db.count(session), 0);
|
||||
});
|
||||
|
||||
test('a private studyset is never published, nor one holding a '
|
||||
'private row', () async {
|
||||
await profile(alice, 'alice');
|
||||
final content = await japanese(alice);
|
||||
final diary = await asPerson(
|
||||
alice,
|
||||
(transaction) async => (await Studyset.db.insertRow(
|
||||
session,
|
||||
Studyset(
|
||||
keyId: 'studyset-key',
|
||||
nonce: ByteData(24),
|
||||
payload: _ciphertext,
|
||||
),
|
||||
transaction: transaction,
|
||||
)).id!,
|
||||
);
|
||||
|
||||
await expectLater(
|
||||
publish(alice, diary),
|
||||
refused(PublishRefusal.private),
|
||||
);
|
||||
// A private topic inside a readable tree makes it no release either.
|
||||
await asPerson(
|
||||
alice,
|
||||
(transaction) => Studyset.db.updateRow(
|
||||
session,
|
||||
Studyset(
|
||||
id: diary,
|
||||
parentId: content.root,
|
||||
keyId: 'studyset-key',
|
||||
nonce: ByteData(24),
|
||||
payload: _ciphertext,
|
||||
),
|
||||
columns: (t) => [t.parentId],
|
||||
transaction: transaction,
|
||||
),
|
||||
);
|
||||
await expectLater(
|
||||
publish(alice, content.root),
|
||||
refused(PublishRefusal.private),
|
||||
);
|
||||
expect(await PublishedStudyset.db.count(session), 0);
|
||||
});
|
||||
|
||||
test("another private studyset of the person's doesn't keep a readable "
|
||||
'one from being published', () async {
|
||||
await profile(alice, 'alice');
|
||||
final content = await japanese(alice);
|
||||
await asPerson(
|
||||
alice,
|
||||
(transaction) => Studyset.db.insertRow(
|
||||
session,
|
||||
Studyset(
|
||||
keyId: 'studyset-key',
|
||||
nonce: ByteData(24),
|
||||
payload: _ciphertext,
|
||||
),
|
||||
transaction: transaction,
|
||||
),
|
||||
);
|
||||
|
||||
final publication = await publish(alice, content.root);
|
||||
|
||||
expect(publication.releases, hasLength(1));
|
||||
});
|
||||
|
||||
test("an encrypted tag is named as the person's device names it, and "
|
||||
'without a name the release is refused', () async {
|
||||
await profile(alice, 'alice');
|
||||
final content = await japanese(alice);
|
||||
final tag = (await CardTag.db.findFirstRow(
|
||||
session,
|
||||
where: (t) => t.cardId.equals(content.word),
|
||||
))!.tagId;
|
||||
// The person's tags are encrypted with their personal key (ADR 0019).
|
||||
await asPerson(
|
||||
alice,
|
||||
(transaction) => Tag.db.updateRow(
|
||||
session,
|
||||
Tag(
|
||||
id: tag,
|
||||
keyId: 'personal-key',
|
||||
nonce: ByteData(24),
|
||||
payload: _ciphertext,
|
||||
),
|
||||
columns: (t) => [t.keyId, t.nonce, t.payload],
|
||||
transaction: transaction,
|
||||
),
|
||||
);
|
||||
|
||||
await expectLater(
|
||||
publish(alice, content.root),
|
||||
refused(PublishRefusal.tagNamesMismatch),
|
||||
);
|
||||
final publication = await publish(
|
||||
alice,
|
||||
content.root,
|
||||
tagNames: {tag.uuid: 'JLPT-N4'},
|
||||
);
|
||||
|
||||
final word = await fileAt(
|
||||
publication,
|
||||
'v1',
|
||||
'cards/${content.word.uuid}.json',
|
||||
);
|
||||
expect(word['tags'], ['JLPT-N4']);
|
||||
});
|
||||
|
||||
test("a name sent for a tag that isn't the person's is refused, and "
|
||||
'a readable tag keeps its own name', () async {
|
||||
await profile(alice, 'alice');
|
||||
final content = await japanese(alice);
|
||||
final bobs = await asPerson(
|
||||
bob,
|
||||
(transaction) async => (await Tag.db.insertRow(
|
||||
session,
|
||||
Tag(payload: _json({'schemaVersion': 1, 'name': 'Bob'})),
|
||||
transaction: transaction,
|
||||
)).id!,
|
||||
);
|
||||
final tag = (await CardTag.db.findFirstRow(
|
||||
session,
|
||||
where: (t) => t.cardId.equals(content.word),
|
||||
))!.tagId;
|
||||
|
||||
await expectLater(
|
||||
publish(alice, content.root, tagNames: {bobs.uuid: 'Bob'}),
|
||||
refused(PublishRefusal.tagNamesMismatch),
|
||||
);
|
||||
await expectLater(
|
||||
publish(alice, content.root, tagNames: {'not an id': 'x'}),
|
||||
refused(PublishRefusal.tagNamesMismatch),
|
||||
);
|
||||
final publication = await publish(
|
||||
alice,
|
||||
content.root,
|
||||
tagNames: {tag.uuid: 'Renamed'},
|
||||
);
|
||||
final word = await fileAt(
|
||||
publication,
|
||||
'v1',
|
||||
'cards/${content.word.uuid}.json',
|
||||
);
|
||||
expect(word['tags'], ['JLPT-N5']);
|
||||
});
|
||||
|
||||
test('files the content names must have reached the instance', () async {
|
||||
await profile(alice, 'alice');
|
||||
final content = await japanese(alice, stored: false);
|
||||
|
|
@ -1018,6 +1166,11 @@ void main() {
|
|||
final _syncDatabases = Expando<OfflineSyncDatabase>();
|
||||
|
||||
/// [json] as a synced payload.
|
||||
/// A payload encrypted on a device (ADR 0019): not JSON, nor UTF-8.
|
||||
final _ciphertext = ByteData.sublistView(
|
||||
Uint8List.fromList([0xff, 0xfe, 0x00, 0x81, 0x9c]),
|
||||
);
|
||||
|
||||
ByteData _json(Map<String, Object?> json) =>
|
||||
ByteData.sublistView(utf8.encode(jsonEncode(json)));
|
||||
|
||||
|
|
|
|||
|
|
@ -38,6 +38,8 @@ import 'package:halkyon_learn_server/src/generated/discussion/discussion_target.
|
|||
as _ike2gc23;
|
||||
import 'package:halkyon_learn_server/src/generated/discussion/discussion_view.dart'
|
||||
as _ikk5e562;
|
||||
import 'package:halkyon_learn_server/src/generated/explore/copy_report.dart'
|
||||
as _ipludy4j;
|
||||
import 'package:halkyon_learn_server/src/generated/explore/explore_entry.dart'
|
||||
as _iscq690j;
|
||||
import 'package:halkyon_learn_server/src/generated/explore/explore_query.dart'
|
||||
|
|
@ -205,6 +207,8 @@ class TestEndpoints {
|
|||
|
||||
late final _ExploreEndpoint explore;
|
||||
|
||||
late final _LearningCountEndpoint learningCount;
|
||||
|
||||
late final _FeedbackEndpoint feedback;
|
||||
|
||||
late final _InboxEndpoint inbox;
|
||||
|
|
@ -249,6 +253,10 @@ class _InternalTestEndpoints extends TestEndpoints
|
|||
endpoints,
|
||||
serializationManager,
|
||||
);
|
||||
learningCount = _LearningCountEndpoint(
|
||||
endpoints,
|
||||
serializationManager,
|
||||
);
|
||||
feedback = _FeedbackEndpoint(
|
||||
endpoints,
|
||||
serializationManager,
|
||||
|
|
@ -291,8 +299,9 @@ class _AssetEndpoint {
|
|||
_ida.Future<String?> startUpload(
|
||||
_ist.TestSessionBuilder sessionBuilder,
|
||||
String sha256,
|
||||
int byteSize,
|
||||
) async {
|
||||
int byteSize, {
|
||||
bool? encrypted,
|
||||
}) async {
|
||||
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
|
||||
var _localUniqueSession =
|
||||
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
|
||||
|
|
@ -307,6 +316,7 @@ class _AssetEndpoint {
|
|||
parameters: _ist.testObjectToJson({
|
||||
'sha256': sha256,
|
||||
'byteSize': byteSize,
|
||||
'encrypted': encrypted,
|
||||
}),
|
||||
serializationManager: _serializationManager,
|
||||
);
|
||||
|
|
@ -325,8 +335,9 @@ class _AssetEndpoint {
|
|||
|
||||
_ida.Future<bool> finishUpload(
|
||||
_ist.TestSessionBuilder sessionBuilder,
|
||||
String sha256,
|
||||
) async {
|
||||
String sha256, {
|
||||
bool? encrypted,
|
||||
}) async {
|
||||
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
|
||||
var _localUniqueSession =
|
||||
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
|
||||
|
|
@ -338,7 +349,10 @@ class _AssetEndpoint {
|
|||
createSessionCallback: (_) => _localUniqueSession,
|
||||
endpointPath: 'asset',
|
||||
methodName: 'finishUpload',
|
||||
parameters: _ist.testObjectToJson({'sha256': sha256}),
|
||||
parameters: _ist.testObjectToJson({
|
||||
'sha256': sha256,
|
||||
'encrypted': encrypted,
|
||||
}),
|
||||
serializationManager: _serializationManager,
|
||||
);
|
||||
var _localReturnValue =
|
||||
|
|
@ -1535,6 +1549,107 @@ class _ExploreEndpoint {
|
|||
}
|
||||
}
|
||||
|
||||
class _LearningCountEndpoint {
|
||||
_LearningCountEndpoint(
|
||||
this._endpointDispatch,
|
||||
this._serializationManager,
|
||||
);
|
||||
|
||||
final _is.EndpointDispatch _endpointDispatch;
|
||||
|
||||
final _is.SerializationManager _serializationManager;
|
||||
|
||||
_ida.Future<bool> counted(_ist.TestSessionBuilder sessionBuilder) async {
|
||||
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
|
||||
var _localUniqueSession =
|
||||
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
|
||||
endpoint: 'learningCount',
|
||||
method: 'counted',
|
||||
);
|
||||
try {
|
||||
var _localCallContext = await _endpointDispatch.getMethodCallContext(
|
||||
createSessionCallback: (_) => _localUniqueSession,
|
||||
endpointPath: 'learningCount',
|
||||
methodName: 'counted',
|
||||
parameters: _ist.testObjectToJson({}),
|
||||
serializationManager: _serializationManager,
|
||||
);
|
||||
var _localReturnValue =
|
||||
await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
)
|
||||
as _ida.Future<bool>);
|
||||
return _localReturnValue;
|
||||
} finally {
|
||||
await _localUniqueSession.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_ida.Future<void> setCounted(
|
||||
_ist.TestSessionBuilder sessionBuilder,
|
||||
bool counted,
|
||||
) async {
|
||||
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
|
||||
var _localUniqueSession =
|
||||
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
|
||||
endpoint: 'learningCount',
|
||||
method: 'setCounted',
|
||||
);
|
||||
try {
|
||||
var _localCallContext = await _endpointDispatch.getMethodCallContext(
|
||||
createSessionCallback: (_) => _localUniqueSession,
|
||||
endpointPath: 'learningCount',
|
||||
methodName: 'setCounted',
|
||||
parameters: _ist.testObjectToJson({'counted': counted}),
|
||||
serializationManager: _serializationManager,
|
||||
);
|
||||
var _localReturnValue =
|
||||
await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
)
|
||||
as _ida.Future<void>);
|
||||
return _localReturnValue;
|
||||
} finally {
|
||||
await _localUniqueSession.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_ida.Future<bool> reportCopies(
|
||||
_ist.TestSessionBuilder sessionBuilder,
|
||||
List<_ipludy4j.CopyReport> copies,
|
||||
) async {
|
||||
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
|
||||
var _localUniqueSession =
|
||||
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
|
||||
endpoint: 'learningCount',
|
||||
method: 'reportCopies',
|
||||
);
|
||||
try {
|
||||
var _localCallContext = await _endpointDispatch.getMethodCallContext(
|
||||
createSessionCallback: (_) => _localUniqueSession,
|
||||
endpointPath: 'learningCount',
|
||||
methodName: 'reportCopies',
|
||||
parameters: _ist.testObjectToJson({'copies': copies}),
|
||||
serializationManager: _serializationManager,
|
||||
);
|
||||
var _localReturnValue =
|
||||
await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
)
|
||||
as _ida.Future<bool>);
|
||||
return _localReturnValue;
|
||||
} finally {
|
||||
await _localUniqueSession.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class _FeedbackEndpoint {
|
||||
_FeedbackEndpoint(
|
||||
this._endpointDispatch,
|
||||
|
|
@ -1922,6 +2037,7 @@ class _PublishEndpoint {
|
|||
String slug,
|
||||
String notes, {
|
||||
String? description,
|
||||
Map<String, String>? tagNames,
|
||||
}) async {
|
||||
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
|
||||
var _localUniqueSession =
|
||||
|
|
@ -1939,6 +2055,7 @@ class _PublishEndpoint {
|
|||
'slug': slug,
|
||||
'notes': notes,
|
||||
'description': description,
|
||||
'tagNames': tagNames,
|
||||
}),
|
||||
serializationManager: _serializationManager,
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in a new issue