import { test } from '@japa/runner' import { DateTime } from 'luxon' import db from '@adonisjs/lucid/services/db' import testUtils from '@adonisjs/core/services/test_utils' import User from '#models/user' import BatchStore from '#services/batch_store' import AccessService from '#services/access_service' import { TIERS } from '#config/access' import { RangeIndexer } from '#lib/edge/index' import { DEFAULT_RULES } from '#lib/rules/index' import type { Overview } from '#lib/nntp/index' const GROUP = 'alt.binaries.test' function file(name: string, counter: string, parts: number, at: number): Overview[] { return Array.from({ length: parts }, (_, i) => [at + i, { 'subject': `${counter} - "${name}" yEnc (${i + 1}/${parts})`, 'from': 'p ', 'date': 'Mon, 05 Oct 2026 10:00:00 +0000', 'message-id': `<${name}-${i + 1}@test>`, ':bytes': String(700000 + i), 'xref': `news.example ${GROUP}:${at + i}`, }] as Overview) } /** Two releases: a show (recent) and a movie posted two years ago. */ async function catalog() { const [g] = await db.table('groups').insert({ name: GROUP, priority: 1 }).returning('id') const [n] = await db.table('nodes').insert({ name: 'access-node' }).returning('id') const [r] = await db.table('ranges').insert({ group_id: g.id, start_id: 1, end_id: 1000, next_id: 1, status: 'leased', node_id: n.id }).returning('id') const indexer = new RangeIndexer({ group: GROUP, start: 1, end: 1000, rules: { ...DEFAULT_RULES, samplePercent: 0, droppedSignatures: [], priorities: { [GROUP]: 1 } } }) indexer.add([...file('Some.Show.S01E01.1080p.WEB.mkv', 'Some.Show.S01E01.1080p.WEB [1/1]', 2, 100), ...file('Old.Movie.2024.1080p.BluRay.mkv', 'Old.Movie.2024.1080p.BluRay [1/1]', 1, 300)], 1001) indexer.finish() await BatchStore.store(Number(r.id), Number(n.id), indexer.take()) const show = await db.from('releases').where('name', 'ilike', '%show%').firstOrFail() const movie = await db.from('releases').where('name', 'ilike', '%movie%').firstOrFail() await db.from('releases').where('id', movie.id).update({ posted_at: DateTime.now().minus({ years: 2 }).toJSDate() }) return { show: Number(show.id), movie: Number(movie.id) } } const user = (email = 'someone@example.com') => User.create({ email, password: 'secret-password' }) test.group('Access with accounts', (group) => { const free = { ...TIERS.free } group.each.setup(() => testUtils.db().withGlobalTransaction()) group.each.setup(() => { AccessService.mode = 'accounts' AccessService.signup = 'invite' Object.assign(TIERS.free, { apiHits: 3, grabs: 1, retentionDays: 365 }) return () => { AccessService.mode = 'open' AccessService.signup = 'open' Object.assign(TIERS.free, free) } }) test("Newznab takes a user's key, counts searches and grabs per day and keeps to the tier's retention", async ({ client, assert }) => { const { show, movie } = await catalog() const someone = await user() assert.include((await client.get('/api?t=search&q=')).text(), 'code="100"') assert.include((await client.get('/api?t=search&q=&apikey=0123456789abcdef0123456789abcdef')).text(), 'code="100"') // caps isn't counted. assert.include((await client.get(`/api?t=caps&apikey=${someone.apiKey}`)).text(), '') const feed = (await client.get(`/api?t=search&q=&apikey=${someone.apiKey}`)).text() // The free tier sees a year back: the movie isn't found. assert.include(feed, 'Some Show S01E01') assert.notInclude(feed, 'Old Movie') assert.include(feed, `t=get&id=${show}&apikey=${someone.apiKey}`) assert.include(feed, '') // Past the retention: refused, and not counted as a grab. assert.include((await client.get(`/api?t=get&id=${movie}&apikey=${someone.apiKey}`)).text(), 'code="102"') assert.include((await client.get(`/api?t=get&id=999999&apikey=${someone.apiKey}`)).text(), 'code="300"') const nzb = await client.get(`/api?t=get&id=${show}&apikey=${someone.apiKey}`) assert.include(nzb.header('content-type'), 'application/x-nzb') assert.include((await client.get(`/api?t=get&id=${show}&apikey=${someone.apiKey}`)).text(), 'code="501"') await client.get(`/api?t=search&q=&apikey=${someone.apiKey}`) await client.get(`/api?t=search&q=&apikey=${someone.apiKey}`) assert.include((await client.get(`/api?t=search&q=&apikey=${someone.apiKey}`)).text(), 'code="500"') assert.deepEqual(await AccessService.usage(someone.id), { apiHits: 3, grabs: 1 }) // VIP sees everything; a tier that ran out is free again. AccessService.grantTier(someone, 'vip', 30) await someone.save() assert.include((await client.get(`/api?t=movie&q=old movie&apikey=${someone.apiKey}`)).text(), 'Old Movie 2024') someone.tierUntil = DateTime.now().minus({ days: 1 }) await someone.save() assert.include((await client.get(`/api?t=movie&q=old movie&apikey=${someone.apiKey}`)).text(), 'code="500"') someone.suspended = true await someone.save() assert.include((await client.get(`/api?t=caps&apikey=${someone.apiKey}`)).text(), 'code="101"') }) test('the catalog needs a token or a key, and counts as Newznab does', async ({ client, assert }) => { const { show, movie } = await catalog() const someone = await user() ;(await client.get('/api/v1/releases')).assertStatus(401) ;(await client.get(`/api/v1/releases/${show}/nzb`)).assertStatus(401) // Stats and how to sign in stay open. ;(await client.get('/api/v1/stats')).assertStatus(200) assert.deepEqual((await client.get('/api/v1/access')).body(), { mode: 'accounts', signup: 'invite' }) const found = await client.get('/api/v1/releases').loginAs(someone) found.assertStatus(200) assert.deepEqual((found.body() as { releases: { id: number }[] }).releases.map((r) => r.id), [show]) ;(await client.get(`/api/v1/releases/${movie}`).loginAs(someone)).assertStatus(403) ;(await client.get(`/api/v1/releases/${show}`).loginAs(someone)).assertStatus(200) // A plain link (the web interface's download button) carries the key. const nzb = await client.get(`/api/v1/releases/${show}/nzb?apikey=${someone.apiKey}`) nzb.assertStatus(200) const over = await client.get(`/api/v1/releases/${show}/nzb?apikey=${someone.apiKey}`) over.assertStatus(429) assert.deepEqual(over.body(), { error: 'Download limit reached', code: 501 }) // The refused movie wasn't counted: a third search is left. ;(await client.get('/api/v1/releases').loginAs(someone)).assertStatus(200) ;(await client.get('/api/v1/releases').loginAs(someone)).assertStatus(429) const profile = ((await client.get('/api/v1/account/profile').loginAs(someone)).body() as any).data assert.equal(profile.access.tier, 'free') assert.equal(profile.access.apiKey, someone.apiKey) assert.deepEqual(profile.access.usage, { apiHits: 3, grabs: 1 }) const reset = (await client.post('/api/v1/account/api-key').loginAs(someone)).body() as { apiKey: string } assert.notEqual(reset.apiKey, someone.apiKey) assert.include((await client.get(`/api?t=caps&apikey=${someone.apiKey}`)).text(), 'code="100"') }) test('signup takes an unused invite, which may give a tier', async ({ client, assert }) => { const form = (email: string, invite?: string) => ({ email, password: 'secret-password', passwordConfirmation: 'secret-password', fullName: null, invite }) await db.table('invites').insert([{ code: 'plain-invite' }, { code: 'vip-invite', tier: 'vip', tier_days: 365 }]) ;(await client.post('/api/v1/auth/signup').json(form('a@example.com'))).assertStatus(403) ;(await client.post('/api/v1/auth/signup').json(form('a@example.com', 'no-such-code'))).assertStatus(403) // A refused invite leaves no account behind. assert.isNull(await User.findBy('email', 'a@example.com')) ;(await client.post('/api/v1/auth/signup').json(form('a@example.com', 'plain-invite'))).assertStatus(200) ;(await client.post('/api/v1/auth/signup').json(form('b@example.com', 'plain-invite'))).assertStatus(403) const vip = await client.post('/api/v1/auth/signup').json(form('c@example.com', 'vip-invite')) vip.assertStatus(200) const c = await User.findByOrFail('email', 'c@example.com') assert.equal(c.tier, 'vip') assert.closeTo(c.tierUntil!.diffNow('days').days, 365, 1) assert.match(c.apiKey, /^[0-9a-f]{32}$/) // Renewed early, the year is added to what's left. AccessService.grantTier(c, 'vip', 365) assert.closeTo(c.tierUntil!.diffNow('days').days, 730, 1) AccessService.signup = 'closed' ;(await client.post('/api/v1/auth/signup').json(form('d@example.com', 'vip-invite'))).assertStatus(403) }) })