194 lines
11 KiB
TypeScript
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=""a.part1.rar" 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')
|
|
})
|
|
})
|