Files
marco/tests/unit/services/activity-test.js
T

274 lines
8.0 KiB
JavaScript

import { module, test } from 'qunit';
import { setupTest } from 'marco/tests/helpers';
import Service from '@ember/service';
import { tracked } from '@glimmer/tracking';
import { parseZapReceipt } from 'marco/utils/activity';
import {
setVerifyWrappedEventMethod,
fakeVerifyEvent,
} from 'applesauce-core/helpers/event';
setVerifyWrappedEventMethod(fakeVerifyEvent);
const USER_PUBKEY = 'a'.repeat(64);
const SENDER_PUBKEY = 'b'.repeat(64);
const PHOTO_EVENT_ID_1 = '1'.repeat(64);
const PHOTO_EVENT_ID_2 = '2'.repeat(64);
const PHOTO_EVENT_ID_OTHER = '3'.repeat(64);
const TEXT_EVENT_ID = '4'.repeat(64);
const RECEIPT_ID_1 = '5'.repeat(64);
const RECEIPT_ID_2 = '6'.repeat(64);
const ZAP_REQ_ID = '7'.repeat(64);
const BOLT11_INVOICE =
'lnbc20u1p3y0x3hpp5743k2g0fsqqxj7n8qzuhns5gmkk4djeejk3wkp64ppevgekvc0jsdqcve5kzar2v9nr5gpqd4hkuetesp5ez2g297jduwc20t6lmqlsg3man0vf2jfd8ar9fh8fhn2g8yttfkqxqy9gcqcqzys9qrsgqrzjqtx3k77yrrav9hye7zar2rtqlfkytl094dsp0ms5majzth6gt7ca6uhdkxl983uywgqqqqlgqqqvx5qqjqrzjqd98kxkpyw0l9tyy8r8q57k7zpy9zjmh6sez752wj6gcumqnj3yxzhdsmg6qq56utgqqqqqqqqqqqeqqjq7jd56882gtxhrjm03c93aacyfy306m4fq0tskf83c0nmet8zc2lxyyg3saz8x6vwcp26xnrlagf9semau3qm2glysp7sv95693fphvsp54l567';
function makeZapReceiptEvent(opts = {}) {
const recipient = opts.recipient || USER_PUBKEY;
const sender = opts.sender || SENDER_PUBKEY;
const zappedEventId = opts.zappedEventId || PHOTO_EVENT_ID_1;
const description = JSON.stringify({
kind: 9734,
pubkey: sender,
content: opts.message || '',
id: ZAP_REQ_ID,
created_at: 10000,
sig: 'fake',
tags: [
['p', recipient],
['relays', ['wss://relay.example.com']],
],
});
return {
id: opts.id || RECEIPT_ID_1,
kind: 9735,
pubkey: 'zap-service-pubkey',
created_at: opts.created_at || 10000,
tags: [
['p', recipient],
['P', sender],
['e', zappedEventId],
['bolt11', BOLT11_INVOICE],
['description', description],
],
content: '',
sig: 'sig',
};
}
function makePhotoEvent(opts = {}) {
return {
id: opts.id || PHOTO_EVENT_ID_1,
pubkey: opts.author || USER_PUBKEY,
kind: 360,
created_at: 5000,
tags: [
['i', opts.placeIdentifier || 'osm:node:123'],
['imeta', `url ${opts.url || 'https://x.com/photo.jpg'}`, 'dim 800x600'],
],
content: '',
sig: 'sig',
};
}
class MockNostrDataService extends Service {
@tracked profiles = {};
store = {
events: new Map(),
add(event) {
this.events.set(event.id, event);
},
getEvent(id) {
return this.events.get(id);
},
getReplaceable(kind, pubkey) {
// Find a replaceable event (kind 0 profile) by pubkey
for (const event of this.events.values()) {
if (event.kind === kind && event.pubkey === pubkey) {
return event;
}
}
return undefined;
},
timeline() {
return {
subscribe(callback) {
callback([]);
return { unsubscribe() {} };
},
};
},
model() {
return {
subscribe() {
return { unsubscribe() {} };
},
};
},
};
loadProfiles() {}
getProfile(pubkey) {
return this.profiles[pubkey];
}
async loadIncomingZaps() {}
}
module('Unit | Service | activity', function (hooks) {
setupTest(hooks);
hooks.beforeEach(function () {
this.owner.register('service:nostrData', MockNostrDataService);
});
test('_updateItems parses receipts and enriches with photos from the store', function (assert) {
const service = this.owner.lookup('service:activity');
const photoEvent = makePhotoEvent({
id: PHOTO_EVENT_ID_1,
placeIdentifier: 'osm:node:42',
});
service.nostrData.store.add(photoEvent);
const receipt = makeZapReceiptEvent({
zappedEventId: PHOTO_EVENT_ID_1,
message: 'Love it!',
});
service._updateItems([receipt], USER_PUBKEY);
assert.strictEqual(service.items.length, 1);
assert.strictEqual(service.items[0].type, 'zap');
assert.strictEqual(service.items[0].senderPubkey, SENDER_PUBKEY);
assert.strictEqual(service.items[0].amountSats, 2000);
assert.strictEqual(service.items[0].message, 'Love it!');
assert.strictEqual(service.items[0].placeIdentifier, 'osm:node:42');
assert.ok(service.items[0].photo, 'photo is populated');
});
test('_updateItems filters out zaps for non-photo events', function (assert) {
const service = this.owner.lookup('service:activity');
service.nostrData.store.add({
id: TEXT_EVENT_ID,
pubkey: USER_PUBKEY,
kind: 1,
created_at: 5000,
tags: [],
});
const receipt = makeZapReceiptEvent({ zappedEventId: TEXT_EVENT_ID });
service._updateItems([receipt], USER_PUBKEY);
assert.strictEqual(service.items.length, 0, 'non-photo zap filtered out');
});
test('_updateItems filters out zaps for photos not authored by the user', function (assert) {
const service = this.owner.lookup('service:activity');
const otherUserPhoto = makePhotoEvent({
id: PHOTO_EVENT_ID_OTHER,
author: 'z'.repeat(64),
});
service.nostrData.store.add(otherUserPhoto);
const receipt = makeZapReceiptEvent({
zappedEventId: PHOTO_EVENT_ID_OTHER,
});
service._updateItems([receipt], USER_PUBKEY);
assert.strictEqual(
service.items.length,
0,
"zap for someone else's photo filtered out"
);
});
test('_updateItems filters out zaps not directed at the user', function (assert) {
const service = this.owner.lookup('service:activity');
const photoEvent = makePhotoEvent({ id: PHOTO_EVENT_ID_1 });
service.nostrData.store.add(photoEvent);
const receipt = makeZapReceiptEvent({
recipient: 'c'.repeat(64),
zappedEventId: PHOTO_EVENT_ID_1,
});
service._updateItems([receipt], USER_PUBKEY);
assert.strictEqual(
service.items.length,
0,
'zap directed at someone else filtered out'
);
});
test('_updateItems sorts entries newest-first', function (assert) {
const service = this.owner.lookup('service:activity');
service.nostrData.store.add(makePhotoEvent({ id: PHOTO_EVENT_ID_1 }));
service.nostrData.store.add(makePhotoEvent({ id: PHOTO_EVENT_ID_2 }));
const oldReceipt = makeZapReceiptEvent({
id: RECEIPT_ID_1,
zappedEventId: PHOTO_EVENT_ID_1,
created_at: 1000,
});
const newReceipt = makeZapReceiptEvent({
id: RECEIPT_ID_2,
zappedEventId: PHOTO_EVENT_ID_2,
created_at: 9000,
});
service._updateItems([oldReceipt, newReceipt], USER_PUBKEY);
assert.strictEqual(service.items.length, 2);
assert.strictEqual(service.items[0].createdAt, 9000, 'newest first');
assert.strictEqual(service.items[1].createdAt, 1000, 'oldest second');
});
test('stop clears items and resets state', function (assert) {
const service = this.owner.lookup('service:activity');
service.nostrData.store.add(makePhotoEvent({ id: PHOTO_EVENT_ID_1 }));
service._updateItems(
[makeZapReceiptEvent({ zappedEventId: PHOTO_EVENT_ID_1 })],
USER_PUBKEY
);
assert.strictEqual(service.items.length, 1);
service.stop();
assert.strictEqual(service.items.length, 0, 'items cleared');
assert.strictEqual(service._userPubkey, null);
});
test('load with no pubkey clears items', async function (assert) {
const service = this.owner.lookup('service:activity');
service.items = [{ fake: true }];
await service.load(null);
assert.strictEqual(service.items.length, 0);
});
test('_resolveSender applies profile when available', function (assert) {
const service = this.owner.lookup('service:activity');
service.nostrData.profiles[SENDER_PUBKEY] = {
name: 'Alice',
picture: 'https://x.com/avatar.jpg',
};
const entry = parseZapReceipt(makeZapReceiptEvent(), USER_PUBKEY);
service._resolveSender(entry);
assert.strictEqual(entry.senderName, 'Alice');
assert.strictEqual(entry.senderAvatar, 'https://x.com/avatar.jpg');
assert.false(entry.senderProfileLoading);
});
});