Files
usenet-indexer/tests/functional/spots.spec.ts
T
Daan MeijerandClaude Fable 5.1 2580f20a43
Test and deploy / test (push) Successful in 51s
Test and deploy / edge (push) Successful in 1m48s
Test and deploy / deploy (push) Skipped
voyager 3: the batch store writes releases once, by id references and bytea keys
Each release chunk is a release inserted once: release_key the SHA-1 of
the identity, the owner group and the first article (deterministic; no
numbering under a group lock), identity_key the identity's SHA-1 bytes,
poster_id and group_set_id through catalog_refs (cached per process),
source as a code (weak-stem added), the name into release_texts. A
finished release's candidates are claimable at once; a provisional tail
release is finished by a later chunk of its key, a finished one sent again
is left alone. Gone: joins into earlier releases and across groups, key
numbering, the group lock, the fragment merger. Interim: chunks, counts
and samples keyed by ranges.block_id, a range without a block gets one.

Readers follow: search (posters and group_sets joined, words through
release_texts, posters by id, source by code), the par2 merger, the
sidecar folder, the judge's scraped rule, the spot inspector, the spotter,
the NZB service, releases:regroup, edge:bench. Tests insert fixtures
through tests/functional/helpers/releases.ts; the suites pass again.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-25 21:04:03 +01:00

123 lines
8.3 KiB
TypeScript

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('<id>') 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: <Spotnet><Posting><Key>${key}</Key><Title>${title}</Title><Description>${description}</Description>`,
`X-XML: <Category>01<Sub>01a09</Sub></Category><NZB><Segment>nzb1@spot.net</Segment></NZB><Size>734003200</Size></Posting></Spotnet>`,
'',
].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, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
const head = password ? `<head><meta type="password">${esc(password)}</meta></head>` : ''
const files = subjects
.map((s, i) => `<file poster="${esc(poster)}" date="${date}" subject="${esc(s)}"><groups><group>alt.binaries.test.spots</group></groups><segments><segment bytes="400000000" number="1">seg${i}@x</segment></segments></file>`)
.join('')
return `<?xml version="1.0" encoding="UTF-8"?><nzb xmlns="http://www.newzbin.com/DTD/2003/nzb">${head}${files}</nzb>`
}
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 <up@load.er>'
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))
})
})