import 'dart:convert'; import 'dart:typed_data'; import 'package:block2_spike/block2_spike.dart'; import 'package:test/test.dart'; /// Q2 — the `OfferTransport` seam must not leak inventory or an exact location. /// The offer carries only a chosen summary; the wire event is checked byte-wise. void main() { final key = NostrKey.deriveFromSeed( Uint8List.fromList(List.generate(32, (i) => i)), ); final codec = Nip99Codec(); // A realistic private-ish context: the caller ACCIDENTALLY passes a very // precise geohash and there is secret inventory the offer must never expose. const secretInventoryMarker = 'SECRET-LOT-12-KG-IN-SHED'; const preciseGeohash = 'u09tunqmabc'; // ~sub-metre precision Offer sampleOffer() => Offer( id: 'offer-1', authorPubkeyHex: key.publicKeyHex, summary: 'Tomate rosa de Barbastro', type: OfferType.gift, category: 'Solanaceae', approxGeohash: preciseGeohash, ); test('exact location is coarsened on the wire (never full precision)', () { final event = codec.encode(sampleOffer(), createdAt: 1000); final gTags = event.tags.where((t) => t[0] == 'g').map((t) => t[1]); // No geohash tag exceeds the coarse cap... expect(gTags.every((g) => g.length <= Nip99Codec.maxGeohashChars), isTrue); // ...and the precise fix never appears anywhere in the serialised event. final wire = jsonEncode(event.toJson()); expect(wire, isNot(contains(preciseGeohash))); expect(wire, contains('u09t')); // coarse prefix is present }); test('no inventory / address field can ride along (seam has none)', () { final event = codec.encode(sampleOffer(), createdAt: 1000); final wire = jsonEncode(event.toJson()); expect(wire, isNot(contains(secretInventoryMarker))); // The event has no "location", "address" or "geo-exact" tag by construction. final tagNames = event.tags.map((t) => t[0]).toSet(); expect(tagNames, isNot(contains('location'))); expect(tagNames, isNot(contains('address'))); }); test('geohash ladder lets an area query match by exact tag', () { final event = codec.encode(sampleOffer(), createdAt: 1000); final gTags = event.tags.where((t) => t[0] == 'g').map((t) => t[1]).toList(); expect(gTags, containsAll(['u', 'u0', 'u09', 'u09t'])); }); test('round-trips Offer ↔ NIP-99 event, only chosen fields survive', () { final original = Offer( id: 'offer-2', authorPubkeyHex: key.publicKeyHex, summary: 'Judía del ganxet', type: OfferType.sale, priceAmount: 3, priceCurrency: 'G1', category: 'Fabaceae', approxGeohash: 'sp3e9', ); final event = codec.encode(original, createdAt: 1000) ..signWith(key.privateKeyHex); expect(event.verify(), isTrue); final decoded = codec.decode(event); expect(decoded.id, original.id); expect(decoded.summary, original.summary); expect(decoded.type, OfferType.sale); expect(decoded.priceAmount, 3); expect(decoded.priceCurrency, 'G1'); expect(decoded.category, 'Fabaceae'); expect(decoded.approxGeohash, 'sp3e9'); }); }