feat(core): person-to-person ratings transport (kind 30778)
Wallapop-style reputation primitive next to trust: Rating (1..5 stars + optional comment), RatingTransport (rate/retract/ratingsOf/myRatingOf) and a Nostr implementation on the shared channel. Addressable kind 30778, d = subject: one live rating per (rater, subject) — re-rating replaces (a single author can never pile up reviews) and retracting overwrites in place. Client-side dedupe mirrors NIP-01 replacement order for relays that keep history.
This commit is contained in:
parent
dc7b2eee27
commit
acdacf1821
6 changed files with 297 additions and 0 deletions
|
|
@ -237,4 +237,75 @@ void main() {
|
|||
await trust.close();
|
||||
});
|
||||
});
|
||||
|
||||
group('ratings (custom kind)', () {
|
||||
test('published by raters, read back with stars and comments', () async {
|
||||
final alice = await idFor(1);
|
||||
final bob = await idFor(2);
|
||||
final carol = await idFor(3);
|
||||
|
||||
final aliceR = NostrRatingTransport(await connFor(alice));
|
||||
await aliceR.rate(
|
||||
subjectPubkey: carol.publicKeyHex,
|
||||
stars: 5,
|
||||
comment: 'Lovely seeds',
|
||||
);
|
||||
await aliceR.close();
|
||||
final bobR = NostrRatingTransport(await connFor(bob));
|
||||
await bobR.rate(subjectPubkey: carol.publicKeyHex, stars: 3);
|
||||
await bobR.close();
|
||||
|
||||
final reader = NostrRatingTransport(await connFor(await idFor(99)));
|
||||
final ratings = await reader.ratingsOf(carol.publicKeyHex);
|
||||
expect(ratings, hasLength(2));
|
||||
final byRater = {for (final r in ratings) r.rater: r};
|
||||
expect(byRater[alice.publicKeyHex]!.stars, 5);
|
||||
expect(byRater[alice.publicKeyHex]!.comment, 'Lovely seeds');
|
||||
expect(byRater[bob.publicKeyHex]!.stars, 3);
|
||||
await reader.close();
|
||||
});
|
||||
|
||||
test('re-rating replaces (one live rating per pair); retract removes',
|
||||
() async {
|
||||
final alice = await idFor(1);
|
||||
final carol = await idFor(3);
|
||||
|
||||
final aliceR = NostrRatingTransport(await connFor(alice));
|
||||
await aliceR.rate(subjectPubkey: carol.publicKeyHex, stars: 2);
|
||||
// createdAt has second granularity; cross the boundary so the
|
||||
// replacement is unambiguously newer.
|
||||
await Future<void>.delayed(const Duration(milliseconds: 1100));
|
||||
await aliceR.rate(
|
||||
subjectPubkey: carol.publicKeyHex,
|
||||
stars: 4,
|
||||
comment: 'Better this season',
|
||||
);
|
||||
|
||||
final mine = await aliceR.myRatingOf(carol.publicKeyHex);
|
||||
expect(mine!.stars, 4);
|
||||
final all = await aliceR.ratingsOf(carol.publicKeyHex);
|
||||
expect(all, hasLength(1));
|
||||
expect(all.single.stars, 4);
|
||||
|
||||
await Future<void>.delayed(const Duration(milliseconds: 1100));
|
||||
await aliceR.retract(subjectPubkey: carol.publicKeyHex);
|
||||
expect(await aliceR.myRatingOf(carol.publicKeyHex), isNull);
|
||||
expect(await aliceR.ratingsOf(carol.publicKeyHex), isEmpty);
|
||||
await aliceR.close();
|
||||
});
|
||||
|
||||
test('rejects out-of-range stars before touching the relay', () async {
|
||||
final alice = await idFor(1);
|
||||
final aliceR = NostrRatingTransport(await connFor(alice));
|
||||
expect(
|
||||
() => aliceR.rate(subjectPubkey: 'ff' * 32, stars: 0),
|
||||
throwsArgumentError,
|
||||
);
|
||||
expect(
|
||||
() => aliceR.rate(subjectPubkey: 'ff' * 32, stars: 6),
|
||||
throwsArgumentError,
|
||||
);
|
||||
await aliceR.close();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
29
packages/commons_core/test/social/rating_test.dart
Normal file
29
packages/commons_core/test/social/rating_test.dart
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import 'package:commons_core/commons_core.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
Rating make(int stars, {bool retracted = false}) => Rating(
|
||||
rater: 'a',
|
||||
subject: 'b',
|
||||
stars: stars,
|
||||
ratedAt: DateTime(2026),
|
||||
retracted: retracted,
|
||||
);
|
||||
|
||||
test('accepts 1..5 stars', () {
|
||||
for (var s = Rating.minStars; s <= Rating.maxStars; s++) {
|
||||
expect(make(s).stars, s);
|
||||
}
|
||||
});
|
||||
|
||||
test('rejects out-of-range stars', () {
|
||||
expect(() => make(0), throwsArgumentError);
|
||||
expect(() => make(6), throwsArgumentError);
|
||||
expect(() => make(-1), throwsArgumentError);
|
||||
});
|
||||
|
||||
test('a retracted rating is not active', () {
|
||||
expect(make(5).isActive, isTrue);
|
||||
expect(make(5, retracted: true).isActive, isFalse);
|
||||
});
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue