halkyon/halkyon_learn_flutter/test/support/fake_profile_gateway.dart
Tizian 「ティツィアーン」 3a328d2bb8
feat(profile): a handle in a studyset opens its person's profile
The summary's first block now reads as a studyset's card: its secondary
title, its title, then by @handle; the blocks' second heading is
Material. A licence holder written as a handle of the instance
(@raisondetredev) reads © raisondetredev, the © standing for the @,
and is a link, apart from a name from elsewhere such as Tae Kim's; it
and the author open their public profile in a sheet: avatar, name,
address, bio and links, as the instance's profiles.byHandle gives them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-14 18:23:54 +02:00

142 lines
3.7 KiB
Dart

import 'dart:typed_data';
import 'package:halkyon_learn_flutter/src/feature/profile/data/profile_gateway.dart';
import 'package:halkyon_learn_flutter/src/feature/profile/model/public_profile.dart';
/// A [ProfileGateway] in memory, on an instance whose account domain is
/// `halkyon.eu`: one profile, refusing the handles others have (whatever
/// their case) and, once its handle is fixed, any other handle.
class FakeProfileGateway({
/// The person's profile; null while they have none.
var PublicProfile? profile,
/// Handles others have.
final Set<String> taken = const {},
/// Others' profiles, by their handles.
final Map<String, PublicProfile> others = const {},
}) implements ProfileGateway {
@override
Future<PublicProfile?> byHandle(String handle) async {
if (fail case final error?) {
throw error;
}
return others[handle.toLowerCase()];
}
/// When set, every request fails with it.
Exception? fail;
@override
Future<PublicProfile?> mine() async {
if (fail case final error?) {
throw error;
}
return profile;
}
/// The handles checked, in order.
final checked = <String>[];
@override
Future<ProfileProblem?> checkHandle(String handle) async {
if (fail case final error?) {
throw error;
}
checked.add(handle);
return _problemOf(handle);
}
ProfileProblem? _problemOf(String handle) {
final current = profile;
if (!RegExp(r'^[A-Za-z0-9_]{1,30}$').hasMatch(handle)) {
return ProfileProblem.handleInvalid;
}
if (handle.toLowerCase() == 'admin') {
return ProfileProblem.handleReserved;
}
if (taken.any((other) => other.toLowerCase() == handle.toLowerCase())) {
return ProfileProblem.handleTaken;
}
if (current != null &&
current.handleFixed &&
current.handle.toLowerCase() != handle.toLowerCase()) {
return ProfileProblem.published;
}
return null;
}
/// Every profile saved, in order.
final saves = <PublicProfile>[];
@override
Future<PublicProfile> save({
required String handle,
required String displayName,
required String bio,
required List<String> links,
}) async {
if (fail case final error?) {
throw error;
}
final current = profile;
if (_problemOf(handle) case final problem?) {
throw ProfileRefused(problem);
}
final saved = profile = PublicProfile(
handle: handle,
address: '$handle@halkyon.eu',
displayName: displayName,
bio: bio,
links: links,
handleFixed: current?.handleFixed ?? false,
);
saves.add(saved);
return saved;
}
/// The avatars set, in order.
final avatars = <Uint8List>[];
@override
Future<PublicProfile> setAvatar(Uint8List png) async {
if (fail case final error?) {
throw error;
}
avatars.add(png);
final current = profile!;
return profile = PublicProfile(
handle: current.handle,
address: current.address,
displayName: current.displayName,
bio: current.bio,
links: current.links,
avatarUrl: 'https://learn.halkyon.eu/avatars/${avatars.length}.png',
handleFixed: current.handleFixed,
);
}
@override
Future<PublicProfile> removeAvatar() async {
final current = profile!;
return profile = PublicProfile(
handle: current.handle,
address: current.address,
displayName: current.displayName,
bio: current.bio,
links: current.links,
handleFixed: current.handleFixed,
);
}
@override
Future<void> remove() async {
if (fail case final error?) {
throw error;
}
if (profile?.handleFixed ?? false) {
throw ProfileRefused(ProfileProblem.published);
}
profile = null;
}
}