import { test } from '@japa/runner' import db from '@adonisjs/lucid/services/db' import testUtils from '@adonisjs/core/services/test_utils' import { Spotter } from '#services/spotter' import { SpotInspector } from '#services/spot_inspector' import { releaseIdentity } from '#lib/release/index' import { parseSubject } from '#lib/headers/index' import { SOURCES } from '#lib/release/source' import { insertRelease } from '#tests/functional/helpers/releases' // Message ids whose sha1('') starts with 0000: self-signed spots (key 7) pass on that hashcash alone. const SPOT = 'spottest731@spot.net' const DISPOSE = 'spottest168100@spot.net' const OTHER = 'spottest173225@spot.net' // A poster's key longer than 50 characters: its spotter id makes a personal dispose count. const PUBKEY = 'A'.repeat(64) const STAMP = 1_760_000_000 /** A spot's raw HEAD: From with the spot header (category 1, key 7), the X-XML with title, description and NZB. */ function rawSpot(messageId: string, title: string, description: string, key = 7, stamp = STAMP) { return [ `From: Spotter <${PUBKEY}@1${key}a09z00.734003200.0.${stamp}.0.SIG>`, `Subject: ${title} | Tag`, `Message-ID: <${messageId}>`, `X-XML: ${key}${title}${description}`, `X-XML: 0101a09nzb1@spot.net734003200`, '', ].join('\r\n') } /** An NZB as spotters post them: the files' original subjects (buildNzb writes its own). */ function nzbXml(subjects: string[], poster: string, date: number, password?: string) { const esc = (t: string) => t.replace(/&/g, '&').replace(/"/g, '"').replace(//g, '>') const head = password ? `${esc(password)}` : '' const files = subjects .map((s, i) => `alt.binaries.test.spotsseg${i}@x`) .join('') return `${head}${files}` } test.group('Spots', (group) => { group.each.setup(() => testUtils.db().withGlobalTransaction()) test('the spotter lists a verified spot with the password its text names, and a dispose removes it', async ({ assert }) => { const spotter = new Spotter() assert.equal(await spotter.store(rawSpot(SPOT, 'Some.Movie.2024.1080p', 'Mooie film[br]Wachtwoord: geheim123'), SPOT, 10), 'listed') const spot = await db.from('spots').where('message_id', SPOT).firstOrFail() assert.equal(spot.title, 'Some.Movie.2024.1080p') assert.equal(spot.password, 'geheim123') assert.equal(spot.password_source, 'spot-text') assert.deepEqual(spot.nzb_segments, ['nzb1@spot.net']) // A {{pw}} in the title wins, and is said to come from the title. assert.equal(await spotter.store(rawSpot(OTHER, 'Other.Show{{t1tl3pw}}', 'Geen ww'), OTHER, 11), 'listed') assert.equal((await db.from('spots').where('message_id', OTHER).firstOrFail()).password_source, 'spot-title') // A message id without the hashcash doesn't verify. assert.equal(await spotter.store(rawSpot('nohash@spot.net', 'X', ''), 'nohash@spot.net', 12), 'unverified') // The poster's own dispose within 5 days. await db.table('release_spots').insert({ release_id: 1, spot_id: SPOT, files: 3 }) assert.equal(await spotter.store(rawSpot(DISPOSE, `dispose <${SPOT}>`, '', 2, STAMP + 3600), DISPOSE, 13), 'moderation') assert.equal((await db.from('spots').where('message_id', SPOT).firstOrFail()).status, 'removed') assert.equal((await db.from('release_spots').where('spot_id', SPOT).select('*')).length, 0) }) test("the inspector lists an NZB's files and links the releases they match, with the password apart", async ({ assert }) => { const [g] = await db.table('groups').insert({ name: 'alt.binaries.test.spots', priority: 1 }).returning('id') const groupId = Number(g.id) const poster = 'up ' const posted = new Date('2025-10-09T10:00:00Z') const subjects = [1, 2, 3].map((i) => `Some.Movie.2024.1080p [${i}/3] - "Some.Movie.2024.1080p.part0${i}.rar" yEnc (1/50)`) // The release the header pass built for these files: the same identity, in the group. const parsed = parseSubject(subjects[0])! const identity = releaseIdentity({ poster, subject: parsed.base, name: parsed.name, weak: false })! const r = { id: await insertRelease({ release_key: identity.key, identity_key: identity.key, name: identity.name, source: identity.source, poster, groups: '[]', group_id: groupId, files: 3, posted_at: posted, readable: true }) } const releaseId = Number(r.id) const nzb = nzbXml(subjects, poster, posted.getTime() / 1000, 'nzbpw') await new Spotter().store(rawSpot(SPOT, 'Some.Movie.2024.1080p', 'Wachtwoord: geheim123'), SPOT, 10) const spot = await db.from('spots').where('message_id', SPOT).firstOrFail() const outcome = await new SpotInspector().inspectNzb(spot, nzb) assert.deepEqual(outcome, { status: 'inspected', matched: 1 }) const files = await db.from('spot_files').where('spot_id', SPOT).orderBy('idx') assert.deepEqual(files.map((f) => f.name), ['Some.Movie.2024.1080p.part01.rar', 'Some.Movie.2024.1080p.part02.rar', 'Some.Movie.2024.1080p.part03.rar']) assert.deepEqual([...new Set(files.map((f) => Number(f.release_id)))], [releaseId]) assert.equal((await db.from('release_spots').where({ release_id: releaseId, spot_id: SPOT }).firstOrFail()).files, 3) // The NZB's own password over the one in the spot's text, kept apart from the release. const passwords = await db.from('release_passwords').where('release_id', releaseId).select('password', 'source') assert.deepEqual(passwords, [{ password: 'nzbpw', source: 'nzb' }]) assert.isTrue((await db.from('releases').where('id', releaseId).firstOrFail()).passworded) const after = await db.from('spots').where('message_id', SPOT).firstOrFail() assert.equal(after.status, 'inspected') assert.equal(Number(after.release_id), releaseId) assert.equal(after.matched, 3) }) test('a spot whose files match no release becomes one of its own, with its NZB', async ({ assert }) => { await db.table('groups').insert({ name: 'alt.binaries.test.spots', priority: 1 }) const nzb = nzbXml(['[1/1] - "lonely.file.mkv" yEnc (1/9)'], 'p', STAMP) await new Spotter().store(rawSpot(SPOT, 'Lonely File', ''), SPOT, 10) const inspector = new SpotInspector() assert.deepEqual(await inspector.inspectNzb(await db.from('spots').where('message_id', SPOT).firstOrFail(), nzb), { status: 'inspected', matched: 0 }) assert.isTrue(await inspector.standalone(SPOT)) const release = await db.from('releases').where('source', SOURCES.spot).firstOrFail() assert.equal(release.name, 'Lonely File') // Its title says nothing: the spot's category (01, a09: a movie, x264). assert.equal(release.category, 2040) assert.equal(release.files, 1) // Its poster (the spot's) by id, in posters for poster search. assert.isNotNull(release.poster_id) assert.isNotEmpty((await db.from('posters').where('id', release.poster_id).firstOrFail()).poster) const { default: NzbService } = await import('#services/nzb_service') assert.include((await NzbService.build(Number(release.id)))!.xml, 'lonely.file.mkv') // Not again while it has one; once the header pass builds the file's release, the spot moves there. assert.deepEqual(await inspector.rematch(), { matched: 0, standalone: 0 }) const group = await db.from('groups').where('name', 'alt.binaries.test.spots').firstOrFail() const parsed = parseSubject('[1/1] - "lonely.file.mkv" yEnc (1/9)')! const identity = releaseIdentity({ poster: 'p', subject: parsed.base, name: parsed.name, weak: false })! const h = { id: await insertRelease({ release_key: identity.key, identity_key: identity.key, name: identity.name, source: identity.source, poster: 'p', groups: '[]', group_id: group.id, files: 1, posted_at: new Date(STAMP * 1000), readable: true }) } assert.equal(await inspector.match(SPOT), 1) assert.isNull(await db.from('releases').where('id', release.id).first()) assert.equal(Number((await db.from('spots').where('message_id', SPOT).firstOrFail()).release_id), Number(h.id)) }) })