Files
usenet-indexer/tests/functional/spot_listing.spec.ts
T
Daan MeijerandClaude Opus 5.5 1520a8dc35
Test and deploy / test (push) Successful in 59s
Test and deploy / deploy (push) Successful in 1m29s
Spots: a corrupt NZB is a result, not a reason to read the listing again
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 15:23:24 +02:00

194 lines
11 KiB
TypeScript

import { test } from '@japa/runner'
import { createHash } from 'node:crypto'
import db from '@adonisjs/lucid/services/db'
import testUtils from '@adonisjs/core/services/test_utils'
import SpotService, { isDummyNzb, NzbUnreadable } from '#services/SpotService'
import SearchService from '#services/SearchService'
test.group('SpotService listings', (group) => {
group.each.setup(() => testUtils.db().withGlobalTransaction())
test('old verified spots become visible releases without files', async ({ assert }) => {
const now = new Date()
const old = new Date('2019-05-01T12:00:00Z')
const spot = {
message_id: 'old1@spot.net', group_name: 'free.pt', article_number: 42, title: 'Some.Show.S01E01.1080p.WEB-DL-GRP',
poster: 'Bart', key_id: 7, size: 1_500_000_000, posted_at: old, verified: true, created_at: now, status: 'pending',
}
await db.table('spots').insert(spot)
const [claimed] = await SpotService.claim(10)
assert.isTrue(SpotService.isListable(claimed))
assert.isFalse(SpotService.isListable({ ...claimed, title: 'DISPOSE abc@spot.net', key_id: 2 }))
assert.isFalse(SpotService.isListable({ ...claimed, verified: false }))
assert.equal(await SpotService.list([claimed]), 1)
const row = await db.from('spots').where('message_id', 'old1@spot.net').first()
assert.equal(row.status, 'listed')
const release = await db.from('releases').where('id', row.release_id).first()
assert.include(release, { name: 'Some.Show.S01E01.1080p.WEB-DL-GRP', source: 'spot', file_count: 0 })
assert.equal(Number(release.size), 1_500_000_000)
// The key an import of this spot's NZB uses, so it fills this release.
assert.equal(release.release_key, createHash('sha1').update('nzb\nold1@spot.net').digest('hex'))
const found = await SearchService.search({ q: 'Some Show S01E01' } as any)
assert.isTrue(found.releases.some((r: any) => r.id === Number(release.id) && r.listingOnly))
})
test('records a full XOVER batch of spots (more rows than one INSERT can bind)', async ({ assert }) => {
// A real spot's From line; the message ids differ, so these don't verify, which doesn't matter here.
const from = 'Bart <0RnzK11s9B4jqZdfc0UHj1qpSrgBw7nKaYBPckvJuk8sKchVXvdofY6qcKkZE-s9x@17a09b03c07c10d17z00.21651233204.20.1790619886.1.NL.Uyf3JHAAlVvgAg9HKIu3veFDGC4DX9>'
const overviews = Array.from({ length: 6000 }, (_, i) => [
i + 1,
{ 'from': from, 'subject': `Title ${i} | Bart`, 'message-id': `<bulk${i}@spot.net>`, 'date': 'Mon, 28 Sep 2026 18:24:45 GMT' },
]) as any
assert.equal(await SpotService.recordHeaders('free.pt', overviews), 6000)
const [{ count }] = await db.from('spots').where('message_id', 'like', 'bulk%@spot.net').count('* as count')
assert.equal(Number(count), 6000)
})
test('a release shows its spot: description, image and Spotweb categories', async ({ assert }) => {
const spot = {
message_id: 'details1@spot.net', group_name: 'free.pt', article_number: 43, title: 'Some.Movie.1981.1080p.BluRay-GRP',
poster: 'Bart', key_id: 7, size: 1_000_000, posted_at: new Date('2019-05-01T12:00:00Z'), verified: true,
created_at: new Date(), status: 'pending', category: 0, subcats: JSON.stringify({ a: [9], z: [0] }),
description: 'Plot[br]more', image: JSON.stringify({ segments: ['img@spot.net'], width: 185, height: 278 }),
details_at: new Date(),
}
await db.table('spots').insert(spot)
const [claimed] = await SpotService.claim(10)
await SpotService.list([claimed])
const { release_id: releaseId } = await db.from('spots').where('message_id', 'details1@spot.net').first()
const release = (await SearchService.release(Number(releaseId)))!
assert.equal(release.spot!.description, 'Plot[br]more')
assert.deepEqual(release.spot!.image, { url: `/api/v1/releases/${releaseId}/image`, width: 185, height: 278 })
assert.equal(release.spot!.categories.head, 'Image')
assert.deepEqual(release.spot!.categories.subcats, [{ type: 'Type', values: ['Movie'] }, { type: 'Format', values: ['x264'] }])
})
test('recognises placeholder NZBs (Dummy.rar) by their size against the spot', ({ assert }) => {
const nzb = (bytes: number[]) => ({ files: bytes.map((b) => ({ segments: [{ bytes: b }] })) })
assert.isTrue(isDummyNzb(nzb([1113, 1506]), 45_000_000_000))
assert.isFalse(isDummyNzb(nzb([700_000_000, 700_000_000]), 1_500_000_000))
// Text-only spots announce the dummy's own size.
assert.isTrue(isDummyNzb(nzb([1113, 1506]), 2619))
})
})
test.group('SpotService passwords', (group) => {
group.each.setup(() => testUtils.db().withGlobalTransaction())
test('a password in the description goes on the release, and shows as known', async ({ assert }) => {
const now = new Date()
await db.table('spots').insert({
message_id: 'pw1@spot.net', group_name: 'free.pt', article_number: 44, title: 'Some.Film.2020.1080p-GRP', poster: 'Bart',
key_id: 7, size: 1_000_000, posted_at: new Date('2019-05-01T12:00:00Z'), verified: true, created_at: now, status: 'pending',
description: 'Mooie film.[br][br]WW: p3IU5Rz9R', details_at: now,
})
const [claimed] = await SpotService.claim(10)
await SpotService.list([claimed])
assert.equal(await SpotService.applyPassword('pw1@spot.net'), 'p3IU5Rz9R')
const { release_id: releaseId } = await db.from('spots').where('message_id', 'pw1@spot.net').first()
await db.from('releases').where('id', releaseId).update({ encrypted: true })
const release = (await SearchService.release(Number(releaseId)))!
assert.include(release, { password: 'p3IU5Rz9R', passwordSource: 'spot', passwordState: 'known' })
// An NZB's own password is not replaced.
await db.from('releases').where('id', releaseId).update({ password: 'from-nzb', password_source: 'nzb' })
await SpotService.applyPassword('pw1@spot.net')
assert.equal((await db.from('releases').where('id', releaseId).first()).password, 'from-nzb')
})
test('password states', async ({ assert }) => {
const { passwordState } = await import('#services/SearchService')
assert.equal(passwordState(true, false), 'missing')
assert.equal(passwordState(true, true), 'known')
assert.equal(passwordState(null, true), 'known')
assert.equal(passwordState(false, false), 'none')
assert.equal(passwordState(null, false), 'none')
})
})
test.group('SpotService reading listings', (group) => {
group.each.setup(() => testUtils.db().withGlobalTransaction())
const nzb = (password: string | null, bytes = 700_000_000) => `<?xml version="1.0" encoding="UTF-8"?>
<nzb xmlns="http://www.newzbin.com/DTD/2003/nzb"><head>${password ? `<meta type="password">${password}</meta>` : ''}</head>
<file poster="p" date="1700000000" subject="&quot;a.part1.rar&quot; yEnc (1/1)"><groups><group>alt.binaries.test</group></groups>
<segments><segment bytes="${bytes}" number="1">a1@x</segment></segments></file></nzb>`
/** A listed spot, as worker:spots leaves an old one, with its article and NZB as given. */
async function listedSpot(id: string, read: { description?: string; nzb?: string | null; error?: string; nzbError?: Error }) {
await db.table('spots').insert({
message_id: id, group_name: 'free.pt', article_number: 1, title: `Listed ${id}`, poster: 'Bart', key_id: 7, size: 700_000_000,
posted_at: new Date('2019-05-01T12:00:00Z'), verified: true, created_at: new Date(), status: 'pending',
})
const [claimed] = await SpotService.claim(10)
await SpotService.list([claimed])
const service = SpotService as any
service.readHead = async () => {
if (read.error) throw new Error(read.error)
return { header: { size: 700_000_000 }, xml: { description: read.description ?? '', nzbSegments: ['n@x'] } }
}
service.readNzb = async () => {
if (read.nzbError) throw read.nzbError
return read.nzb ?? null
}
}
const restore = () => {
delete (SpotService as any).readHead
delete (SpotService as any).readNzb
}
group.each.teardown(restore)
const passwordOf = async (id: string) => {
const spot = await db.from('spots').where('message_id', id).first()
const release = await db.from('releases').where('id', spot.release_id).first()
return { spot, password: release.password, source: release.password_source }
}
test("reads the NZB's password and the details, without importing files", async ({ assert }) => {
await listedSpot('l1@spot.net', { description: 'Plot', nzb: nzb('fromnzb1') })
assert.deepEqual(await SpotService.readListings(10, 2), { 'nzb-password': 1 })
const { spot, password, source } = await passwordOf('l1@spot.net')
assert.deepEqual([password, source, spot.status, spot.description], ['fromnzb1', 'nzb', 'listed', 'Plot'])
assert.isNotNull(spot.nzb_read_at)
assert.equal((await db.from('releases').where('id', spot.release_id).first()).file_count, 0)
// Read once: nothing left.
assert.deepEqual(await SpotService.readListings(10, 2), {})
})
test("the spot's text when the NZB has none; the NZB's own password wins over it", async ({ assert }) => {
await listedSpot('l2@spot.net', { description: 'Wachtwoord: fromtext2', nzb: nzb(null) })
assert.deepEqual(await SpotService.readListings(10, 1), { 'spot-password': 1 })
assert.include(await passwordOf('l2@spot.net'), { password: 'fromtext2', source: 'spot' })
await listedSpot('l3@spot.net', { description: 'Wachtwoord: fromtext3', nzb: nzb('fromnzb3') })
await db.from('releases').where('id', (await db.from('spots').where('message_id', 'l3@spot.net').first()).release_id).update({ password: 'fromtext3', password_source: 'spot' })
assert.deepEqual(await SpotService.readListings(10, 1), { 'nzb-password': 1 })
assert.include(await passwordOf('l3@spot.net'), { password: 'fromnzb3', source: 'nzb' })
})
test('a placeholder NZB hides the listing; a lost connection gives the spot back, a gone article does not', async ({ assert }) => {
await listedSpot('l4@spot.net', { nzb: nzb(null, 1500) })
assert.deepEqual(await SpotService.readListings(10, 1), { dummy: 1 })
assert.equal((await db.from('spots').where('message_id', 'l4@spot.net').first()).status, 'dummy')
await listedSpot('l5@spot.net', { error: 'Connection reset' })
assert.deepEqual(await SpotService.readListings(10, 1), { retry: 1 })
assert.isNull((await db.from('spots').where('message_id', 'l5@spot.net').first()).nzb_read_at)
await db.from('spots').where('message_id', 'l5@spot.net').delete()
await listedSpot('l6@spot.net', { error: '430 No Such Article' })
assert.deepEqual(await SpotService.readListings(10, 1), { gone: 1 })
assert.isNotNull((await db.from('spots').where('message_id', 'l6@spot.net').first()).nzb_read_at)
// A corrupt NZB is a result, not something to read again.
await listedSpot('l7@spot.net', { nzbError: new NzbUnreadable('invalid code lengths set') })
assert.deepEqual(await SpotService.readListings(10, 1), { 'nzb-unreadable': 1 })
const l7 = await db.from('spots').where('message_id', 'l7@spot.net').first()
assert.isNotNull(l7.nzb_read_at)
assert.equal(l7.error, 'NZB unreadable: invalid code lengths set')
})
})