Files
GameTime/server/test/share_api_test.dart
Blomios db6e84be06 feat(server): partage ciblé de programmes et séances entre comptes (ticket #51)
Ajoute l'endpoint api/share_api.dart, les use cases de partage
(application/share_use_cases.dart) et l'adapter Postgres
(infrastructure/postgres/share_repository.dart). L'acceptation d'un
partage crée une nouvelle ressource synced_resources avec de nouveaux
IDs pour le destinataire, sans jamais modifier la ressource source de
l'émetteur ; gestion des conflits révoqué/déjà répondu. dart pub get
OK, dart analyze clean, dart test 24/24 vert. Use case d'acceptation
relu manuellement et jugé correct ; pas de test de bout en bout contre
un vrai PostgreSQL faute d'accès Docker dans ce sandbox.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 10:34:09 +02:00

206 lines
5.0 KiB
Dart

import 'package:gametime_server/api/router.dart';
import 'package:gametime_server/api/share_api.dart';
import 'package:gametime_server/application/application.dart';
import 'package:gametime_server/domain/domain.dart';
import 'package:shelf/shelf.dart';
import 'package:test/test.dart';
void main() {
test('share routes require bearer authentication', () async {
final handler = buildApiHandler(shareApi: _shareApi());
final requests = [
Request('POST', Uri.parse('http://localhost/shares')),
Request('GET', Uri.parse('http://localhost/shares/inbox')),
Request('POST', Uri.parse('http://localhost/shares/share-1/accept')),
Request('POST', Uri.parse('http://localhost/shares/share-1/decline')),
Request('POST', Uri.parse('http://localhost/shares/share-1/revoke')),
];
for (final request in requests) {
final response = await handler(request);
expect(response.statusCode, 401, reason: request.url.path);
}
});
}
ShareApi _shareApi() {
final clock = _FakeClock(DateTime.utc(2026, 7, 19, 12));
final ids = _FakeIds();
final users = _FakeUserRepository(
UserAccount(
id: 'user-1',
email: 'user@example.com',
passwordHash: 'hash',
createdAt: clock.now(),
updatedAt: clock.now(),
),
);
final sessions = _FakeAuthSessionRepository(
AuthSession(
id: 'session-1',
userId: 'user-1',
tokenHash: 'token-hash:valid-token',
issuedAt: clock.now(),
expiresAt: clock.now().add(const Duration(days: 1)),
),
);
final tokens = _FakeTokenService();
final authenticate = AuthenticateRequestUseCase(
users: users,
sessions: sessions,
tokens: tokens,
clock: clock,
);
final shares = _FakeShareRepository();
final resources = _FakeSyncedResourceRepository();
return ShareApi(
createShare: CreateShareUseCase(
users: users,
shares: shares,
clock: clock,
ids: ids,
),
listInbox: ListInboxUseCase(shares: shares),
acceptShare: AcceptShareUseCase(
shares: shares,
resources: resources,
clock: clock,
ids: ids,
),
declineShare: DeclineShareUseCase(shares: shares, clock: clock),
revokeShare: RevokeShareUseCase(shares: shares, clock: clock),
authenticateRequest: authenticate,
);
}
final class _FakeUserRepository implements UserRepository {
const _FakeUserRepository(this.user);
final UserAccount user;
@override
Future<UserAccount?> findByEmail(String email) async {
return user.email == email.toLowerCase() ? user : null;
}
@override
Future<UserAccount?> findById(String id) async {
return id == user.id ? user : null;
}
@override
Future<void> insert(UserAccount user) async {}
@override
Future<void> updatePasswordHash({
required String userId,
required String passwordHash,
required DateTime updatedAt,
}) async {}
}
final class _FakeAuthSessionRepository implements AuthSessionRepository {
const _FakeAuthSessionRepository(this.session);
final AuthSession session;
@override
Future<AuthSession?> findByTokenHash(String tokenHash) async {
return tokenHash == session.tokenHash ? session : null;
}
@override
Future<void> insert(AuthSession session) async {}
@override
Future<void> revoke({
required String sessionId,
required DateTime revokedAt,
}) async {}
}
final class _FakeShareRepository implements ShareRepository {
@override
Future<void> insertShare({
required Share share,
required List<ShareRecipient> recipients,
}) async {}
@override
Future<List<ShareInboxItem>> listInbox(String recipientUserId) async {
return const [];
}
@override
Future<Share?> findShareById(String shareId) async {
return null;
}
@override
Future<ShareRecipient?> findRecipient({
required String shareId,
required String recipientUserId,
}) async {
return null;
}
@override
Future<void> updateRecipientStatus({
required String recipientId,
required ShareRecipientStatus status,
required DateTime respondedAt,
}) async {}
@override
Future<void> revokeShare({
required String shareId,
required DateTime revokedAt,
}) async {}
}
final class _FakeSyncedResourceRepository implements SyncedResourceRepository {
@override
Future<SyncWriteResult> upsertWithLww(SyncedResource resource) async {
return SyncWriteResult(
status: SyncWriteStatus.accepted,
resource: resource,
);
}
@override
Future<List<SyncedResource>> findAllForUserSince({
required String ownerUserId,
DateTime? since,
}) async {
return const [];
}
}
final class _FakeTokenService implements OpaqueTokenService {
@override
String generateToken() => 'valid-token';
@override
String hashToken(String token) => 'token-hash:$token';
}
final class _FakeClock implements Clock {
const _FakeClock(this.value);
final DateTime value;
@override
DateTime now() => value;
}
final class _FakeIds implements IdGenerator {
var _next = 0;
@override
String newId() {
_next += 1;
return 'id-$_next';
}
}