Fully test the new stats module

This commit is contained in:
Alex Gleason 2024-05-24 15:52:30 -05:00
parent 83b2a627c3
commit 34f3cc8d24
No known key found for this signature in database
GPG Key ID: 7211D1F99744FBB7
4 changed files with 207 additions and 12 deletions

View File

@ -47,7 +47,7 @@ export class DittoDB {
provider: new FileMigrationProvider({ provider: new FileMigrationProvider({
fs, fs,
path, path,
migrationFolder: new URL(import.meta.resolve('../db/migrations')).pathname, migrationFolder: new URL(import.meta.resolve('./migrations')).pathname,
}), }),
}); });

View File

@ -1,6 +1,13 @@
import { NostrEvent } from '@nostrify/nostrify'; import fs from 'node:fs/promises';
import path from 'node:path';
import { Database as Sqlite } from '@db/sqlite';
import { NDatabase, NostrEvent } from '@nostrify/nostrify';
import { DenoSqlite3Dialect } from '@soapbox/kysely-deno-sqlite';
import { FileMigrationProvider, Kysely, Migrator } from 'kysely';
import { finalizeEvent, generateSecretKey } from 'nostr-tools'; import { finalizeEvent, generateSecretKey } from 'nostr-tools';
import { DittoTables } from '@/db/DittoTables.ts';
import { purifyEvent } from '@/storages/hydrate.ts'; import { purifyEvent } from '@/storages/hydrate.ts';
/** Import an event fixture by name in tests. */ /** Import an event fixture by name in tests. */
@ -21,3 +28,31 @@ export function genEvent(t: Partial<NostrEvent> = {}, sk: Uint8Array = generateS
return purifyEvent(event); return purifyEvent(event);
} }
/** Get an in-memory SQLite database to use for testing. It's automatically destroyed when it goes out of scope. */
export async function getTestDB() {
const kysely = new Kysely<DittoTables>({
dialect: new DenoSqlite3Dialect({
database: new Sqlite(':memory:'),
}),
});
const migrator = new Migrator({
db: kysely,
provider: new FileMigrationProvider({
fs,
path,
migrationFolder: new URL(import.meta.resolve('./db/migrations')).pathname,
}),
});
await migrator.migrateToLatest();
const store = new NDatabase(kysely);
return {
store,
kysely,
[Symbol.asyncDispose]: () => kysely.destroy(),
};
}

144
src/utils/stats.test.ts Normal file
View File

@ -0,0 +1,144 @@
import { assertEquals } from '@std/assert';
import { generateSecretKey, getPublicKey } from 'nostr-tools';
import { genEvent, getTestDB } from '@/test.ts';
import { getAuthorStats, getEventStats, getFollowDiff, updateStats } from '@/utils/stats.ts';
Deno.test('updateStats with kind 1 increments notes count', async () => {
await using db = await getTestDB();
const sk = generateSecretKey();
const pubkey = getPublicKey(sk);
await updateStats({ ...db, event: genEvent({ kind: 1 }, sk) });
const stats = await getAuthorStats(db.kysely, pubkey);
assertEquals(stats!.notes_count, 1);
});
Deno.test('updateStats with kind 5 decrements notes count', async () => {
await using db = await getTestDB();
const sk = generateSecretKey();
const pubkey = getPublicKey(sk);
const create = genEvent({ kind: 1 }, sk);
const remove = genEvent({ kind: 5, tags: [['e', create.id]] }, sk);
await updateStats({ ...db, event: create });
assertEquals((await getAuthorStats(db.kysely, pubkey))!.notes_count, 1);
await db.store.event(create);
await updateStats({ ...db, event: remove });
assertEquals((await getAuthorStats(db.kysely, pubkey))!.notes_count, 0);
await db.store.event(remove);
});
Deno.test('updateStats with kind 3 increments followers count', async () => {
await using db = await getTestDB();
await updateStats({ ...db, event: genEvent({ kind: 3, tags: [['p', 'alex']] }) });
await updateStats({ ...db, event: genEvent({ kind: 3, tags: [['p', 'alex']] }) });
await updateStats({ ...db, event: genEvent({ kind: 3, tags: [['p', 'alex']] }) });
const stats = await getAuthorStats(db.kysely, 'alex');
assertEquals(stats!.followers_count, 3);
});
Deno.test('updateStats with kind 3 decrements followers count', async () => {
await using db = await getTestDB();
const sk = generateSecretKey();
const follow = genEvent({ kind: 3, tags: [['p', 'alex']], created_at: 0 }, sk);
const remove = genEvent({ kind: 3, tags: [], created_at: 1 }, sk);
await updateStats({ ...db, event: follow });
assertEquals((await getAuthorStats(db.kysely, 'alex'))!.followers_count, 1);
await db.store.event(follow);
await updateStats({ ...db, event: remove });
assertEquals((await getAuthorStats(db.kysely, 'alex'))!.followers_count, 0);
await db.store.event(remove);
});
Deno.test('getFollowDiff returns added and removed followers', () => {
const prev = genEvent({ tags: [['p', 'alex'], ['p', 'bob']] });
const next = genEvent({ tags: [['p', 'alex'], ['p', 'carol']] });
const { added, removed } = getFollowDiff(next.tags, prev.tags);
assertEquals(added, new Set(['carol']));
assertEquals(removed, new Set(['bob']));
});
Deno.test('updateStats with kind 6 increments reposts count', async () => {
await using db = await getTestDB();
const note = genEvent({ kind: 1 });
await updateStats({ ...db, event: note });
await db.store.event(note);
const repost = genEvent({ kind: 6, tags: [['e', note.id]] });
await updateStats({ ...db, event: repost });
await db.store.event(repost);
const stats = await getEventStats(db.kysely, note.id);
assertEquals(stats!.reposts_count, 1);
});
Deno.test('updateStats with kind 5 decrements reposts count', async () => {
await using db = await getTestDB();
const note = genEvent({ kind: 1 });
await updateStats({ ...db, event: note });
await db.store.event(note);
const sk = generateSecretKey();
const repost = genEvent({ kind: 6, tags: [['e', note.id]] }, sk);
await updateStats({ ...db, event: repost });
await db.store.event(repost);
await updateStats({ ...db, event: genEvent({ kind: 5, tags: [['e', repost.id]] }, sk) });
const stats = await getEventStats(db.kysely, note.id);
assertEquals(stats!.reposts_count, 0);
});
Deno.test('updateStats with kind 7 increments reactions count', async () => {
await using db = await getTestDB();
const note = genEvent({ kind: 1 });
await updateStats({ ...db, event: note });
await db.store.event(note);
const reaction = genEvent({ kind: 7, tags: [['e', note.id]] });
await updateStats({ ...db, event: reaction });
await db.store.event(reaction);
const stats = await getEventStats(db.kysely, note.id);
assertEquals(stats!.reactions_count, 1);
});
Deno.test('updateStats with kind 5 decrements reactions count', async () => {
await using db = await getTestDB();
const note = genEvent({ kind: 1 });
await updateStats({ ...db, event: note });
await db.store.event(note);
const sk = generateSecretKey();
const reaction = genEvent({ kind: 7, tags: [['e', note.id]] }, sk);
await updateStats({ ...db, event: reaction });
await db.store.event(reaction);
await updateStats({ ...db, event: genEvent({ kind: 5, tags: [['e', reaction.id]] }, sk) });
const stats = await getEventStats(db.kysely, note.id);
assertEquals(stats!.reactions_count, 0);
});

View File

@ -95,6 +95,18 @@ export function getFollowDiff(
}; };
} }
/** Retrieve the author stats by the pubkey. */
export function getAuthorStats(
kysely: Kysely<DittoTables>,
pubkey: string,
): Promise<DittoTables['author_stats'] | undefined> {
return kysely
.selectFrom('author_stats')
.selectAll()
.where('pubkey', '=', pubkey)
.executeTakeFirst();
}
/** Retrieve the author stats by the pubkey, then call the callback to update it. */ /** Retrieve the author stats by the pubkey, then call the callback to update it. */
export async function updateAuthorStats( export async function updateAuthorStats(
kysely: Kysely<DittoTables>, kysely: Kysely<DittoTables>,
@ -108,11 +120,7 @@ export async function updateAuthorStats(
notes_count: 0, notes_count: 0,
}; };
const prev = await kysely const prev = await getAuthorStats(kysely, pubkey);
.selectFrom('author_stats')
.selectAll()
.where('pubkey', '=', pubkey)
.executeTakeFirst();
const stats = fn(prev ?? empty); const stats = fn(prev ?? empty);
@ -128,6 +136,18 @@ export async function updateAuthorStats(
} }
} }
/** Retrieve the event stats by the event ID. */
export function getEventStats(
kysely: Kysely<DittoTables>,
eventId: string,
): Promise<DittoTables['event_stats'] | undefined> {
return kysely
.selectFrom('event_stats')
.selectAll()
.where('event_id', '=', eventId)
.executeTakeFirst();
}
/** Retrieve the event stats by the event ID, then call the callback to update it. */ /** Retrieve the event stats by the event ID, then call the callback to update it. */
export async function updateEventStats( export async function updateEventStats(
kysely: Kysely<DittoTables>, kysely: Kysely<DittoTables>,
@ -141,11 +161,7 @@ export async function updateEventStats(
reactions_count: 0, reactions_count: 0,
}; };
const prev = await kysely const prev = await getEventStats(kysely, eventId);
.selectFrom('event_stats')
.selectAll()
.where('event_id', '=', eventId)
.executeTakeFirst();
const stats = fn(prev ?? empty); const stats = fn(prev ?? empty);