Fully test the new stats module
This commit is contained in:
parent
83b2a627c3
commit
34f3cc8d24
|
@ -47,7 +47,7 @@ export class DittoDB {
|
|||
provider: new FileMigrationProvider({
|
||||
fs,
|
||||
path,
|
||||
migrationFolder: new URL(import.meta.resolve('../db/migrations')).pathname,
|
||||
migrationFolder: new URL(import.meta.resolve('./migrations')).pathname,
|
||||
}),
|
||||
});
|
||||
|
||||
|
|
37
src/test.ts
37
src/test.ts
|
@ -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 { DittoTables } from '@/db/DittoTables.ts';
|
||||
import { purifyEvent } from '@/storages/hydrate.ts';
|
||||
|
||||
/** Import an event fixture by name in tests. */
|
||||
|
@ -21,3 +28,31 @@ export function genEvent(t: Partial<NostrEvent> = {}, sk: Uint8Array = generateS
|
|||
|
||||
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(),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
|
@ -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. */
|
||||
export async function updateAuthorStats(
|
||||
kysely: Kysely<DittoTables>,
|
||||
|
@ -108,11 +120,7 @@ export async function updateAuthorStats(
|
|||
notes_count: 0,
|
||||
};
|
||||
|
||||
const prev = await kysely
|
||||
.selectFrom('author_stats')
|
||||
.selectAll()
|
||||
.where('pubkey', '=', pubkey)
|
||||
.executeTakeFirst();
|
||||
const prev = await getAuthorStats(kysely, pubkey);
|
||||
|
||||
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. */
|
||||
export async function updateEventStats(
|
||||
kysely: Kysely<DittoTables>,
|
||||
|
@ -141,11 +161,7 @@ export async function updateEventStats(
|
|||
reactions_count: 0,
|
||||
};
|
||||
|
||||
const prev = await kysely
|
||||
.selectFrom('event_stats')
|
||||
.selectAll()
|
||||
.where('event_id', '=', eventId)
|
||||
.executeTakeFirst();
|
||||
const prev = await getEventStats(kysely, eventId);
|
||||
|
||||
const stats = fn(prev ?? empty);
|
||||
|
||||
|
|
Loading…
Reference in New Issue