From 157b807d9bd86ba76c1a544f2c8c68e4e73e138b Mon Sep 17 00:00:00 2001 From: hazycora Date: Tue, 30 Jul 2024 17:32:21 -0500 Subject: [PATCH] add tests --- .forgejo/workflows/check.yml | 26 ++++++ .gitignore | 3 + package.json | 4 + src/index.ts | 17 +++- src/streamers/deezer/main.ts | 15 ++- src/streamers/qobuz/main.ts | 10 ++ src/streamers/soundcloud/main.ts | 10 ++ src/streamers/spotify/main.ts | 15 +++ src/streamers/tidal/main.ts | 14 +++ src/test.ts | 152 +++++++++++++++++++++++++++++++ src/types.ts | 8 ++ 11 files changed, 270 insertions(+), 4 deletions(-) create mode 100644 .forgejo/workflows/check.yml create mode 100644 src/test.ts diff --git a/.forgejo/workflows/check.yml b/.forgejo/workflows/check.yml new file mode 100644 index 0000000..9457a1e --- /dev/null +++ b/.forgejo/workflows/check.yml @@ -0,0 +1,26 @@ +on: + pull_request: + +jobs: + check: + runs-on: docker + steps: + - name: Checkout repository + uses: https://code.forgejo.org/actions/checkout@v4 + - uses: https://github.com/pnpm/action-setup@v4 + name: Install pnpm + with: + version: 9 + run_install: false + - name: Install Node.js + uses: https://github.com/actions/setup-node@v4 + with: + node-version: 'latest' + cache: 'pnpm' + - run: pnpm install + - run: pnpm run build + - run: pnpm run test + - name: Comment test results + uses: https://github.com/thollander/actions-comment-pull-request@v2 + with: + filePath: '.test-results.md' diff --git a/.gitignore b/.gitignore index 8aaf309..564c870 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,7 @@ node_modules /tmp /run.js +.test-options.json +.test-results.* + .pnpm-store \ No newline at end of file diff --git a/package.json b/package.json index 6b1d9a7..fe1aff3 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,9 @@ "types": "build/index.d.ts", "scripts": { "tsc": "tsc", + "build": "tsc", + "watch": "tsc -w", + "test": "node build/test.js", "lint": "prettier --check . && eslint .", "format": "prettier --write .", "prepare": "husky" @@ -17,6 +20,7 @@ "import": "./build/types.js", "types": "./build/types.d.ts" }, + "./streamers/*": "./build/streamers/*/main.js", "./streamers/*.js": "./build/streamers/*.js" }, "files": [ diff --git a/src/index.ts b/src/index.ts index 039ecd1..9c4b6fd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,7 +7,7 @@ import { StreamerAccount } from './types.js' -interface LucidaOptions { +export interface LucidaOptions { modules: { [key: string]: Streamer | StreamerWithLogin } logins?: { [key: string]: { @@ -28,13 +28,24 @@ class Lucida { .flat() if (options.logins) this.logins = options.logins } - async login() { + async login(ignoreFailures = false) { if (!this.logins) throw new Error('No logins specified') for (const i in this.modules) { const credentials = this.logins[i] const module = this.modules[i] if (module && 'login' in module) { - await module.login?.(credentials?.username, credentials?.password) + try { + await module.login?.(credentials?.username, credentials?.password) + } catch (error) { + console.error(error) + if (!ignoreFailures) { + throw new Error(`Failed to login to ${i}`) + } else { + await module.disconnect?.() + console.error(`ignoreFailures is on, removing ${i} module...`) + delete this.modules[i] + } + } } } } diff --git a/src/streamers/deezer/main.ts b/src/streamers/deezer/main.ts index df1c3b4..b808295 100644 --- a/src/streamers/deezer/main.ts +++ b/src/streamers/deezer/main.ts @@ -53,7 +53,20 @@ interface APIMethod { export default class Deezer implements StreamerWithLogin { hostnames = ['deezer.com', 'www.deezer.com', 'deezer.page.link'] - + testData = { + 'https://www.deezer.com/us/artist/1194083': { + type: 'artist', + title: 'Tyler, The Creator' + }, + 'https://www.deezer.com/us/track/559711712': { + type: 'track', + title: 'Potato Salad' + }, + 'https://www.deezer.com/us/album/97140952': { + type: 'album', + title: 'IGOR' + } + } as const headers: { [header: string]: string } = { Accept: '*/*', 'User-Agent': diff --git a/src/streamers/qobuz/main.ts b/src/streamers/qobuz/main.ts index ef7020c..e6e7f8b 100644 --- a/src/streamers/qobuz/main.ts +++ b/src/streamers/qobuz/main.ts @@ -52,6 +52,16 @@ interface LoginResponse { export default class Qobuz implements StreamerWithLogin { hostnames = ['play.qobuz.com', 'open.qobuz.com', 'www.qobuz.com', 'qobuz.com'] + testData = { + 'https://www.qobuz.com/us-en/interpreter/tyler-the-creator/589771': { + title: 'Tyler, The Creator', + type: 'artist' + }, + 'https://www.qobuz.com/us-en/album/igor-tyler-the-creator/qtz65tw2of0ha': { + title: 'IGOR', + type: 'album' + } + } as const token?: string appSecret: string appId: string diff --git a/src/streamers/soundcloud/main.ts b/src/streamers/soundcloud/main.ts index 4ff2b6f..e302e7a 100644 --- a/src/streamers/soundcloud/main.ts +++ b/src/streamers/soundcloud/main.ts @@ -45,6 +45,16 @@ interface SoundcloudTranscoding { export default class Soundcloud implements Streamer { hostnames = ['soundcloud.com', 'm.soundcloud.com', 'www.soundcloud.com'] + testData = { + 'https://soundcloud.com/saoirsedream/charlikartlanparty': { + type: 'track', + title: 'Charli Kart LAN Party' + }, + 'https://soundcloud.com/saoirsedream/sets/star': { + type: 'album', + title: 'star★☆' + } + } as const oauthToken?: string client?: ScClient constructor(options: SoundcloudOptions) { diff --git a/src/streamers/spotify/main.ts b/src/streamers/spotify/main.ts index d601021..7c5f517 100644 --- a/src/streamers/spotify/main.ts +++ b/src/streamers/spotify/main.ts @@ -11,6 +11,21 @@ import { class Spotify implements StreamerWithLogin { client: Librespot hostnames = ['open.spotify.com'] + testData = { + 'https://open.spotify.com/track/1jzIJcHCXneHw7ojC6LXiF': { + type: 'track', + title: 'Potato Salad' + }, + 'https://open.spotify.com/album/5zi7WsKlIiUXv09tbGLKsE': { + type: 'album', + title: 'IGOR' + }, + 'https://open.spotify.com/artist/4V8LLVI7PbaPR0K2TGSxFF': { + type: 'artist', + title: 'Tyler, The Creator' + } + } as const + constructor(options: LibrespotOptions) { this.client = new Librespot(options) } diff --git a/src/streamers/tidal/main.ts b/src/streamers/tidal/main.ts index e306616..f8af92d 100644 --- a/src/streamers/tidal/main.ts +++ b/src/streamers/tidal/main.ts @@ -70,6 +70,20 @@ export default class Tidal implements Streamer { countryCode: string userId: number | undefined hostnames = ['tidal.com', 'www.tidal.com', 'listen.tidal.com'] + testData = { + 'https://tidal.com/browse/artist/3908662': { + type: 'artist', + title: 'Tyler, The Creator' + }, + 'https://tidal.com/browse/album/109485854': { + type: 'album', + title: 'IGOR' + }, + 'https://tidal.com/browse/track/95691774': { + type: 'track', + title: 'Potato Salad' + } + } as const failedAuth = false constructor(options: TidalOptions) { this.tvToken = options.tvToken diff --git a/src/test.ts b/src/test.ts new file mode 100644 index 0000000..cc97e34 --- /dev/null +++ b/src/test.ts @@ -0,0 +1,152 @@ +import Lucida, { LucidaOptions } from 'lucida' +import { GetByUrlResponse, Streamer } from 'lucida/types' + +import fs from 'node:fs' + +let testOptions: { + [module: string]: { + username?: string + password?: string + [key: string]: string | number | boolean | undefined + } +} + +if (!process.env.TEST_OPTIONS) { + console.error( + 'No test options found in environment variables. Trying to read from .test-options.json' + ) + try { + const testOptionsFile = fs.readFileSync('.test-options.json', 'utf-8') + testOptions = JSON.parse(testOptionsFile) + } catch (error) { + console.error( + 'No test options found in environment variables or .test-options.json. Exiting...' + ) + process.exit(1) + } +} else { + testOptions = JSON.parse(process.env.TEST_OPTIONS) +} + +async function start() { + const lucidaOptions: LucidaOptions = { + modules: {} + } + + for (const [module, options] of Object.entries(testOptions)) { + const username = options.username + const password = options.password + delete options.username + delete options.password + const { default: Streamer } = await import(`lucida/streamers/${module}`) + lucidaOptions.modules[module] = new Streamer(options) + if (username && password) { + lucidaOptions.logins ??= {} + lucidaOptions.logins[module] = { username, password } + } + } + + const lucida = new Lucida(lucidaOptions) + await lucida.login(true) + + const testResults: { + [module: string]: { + loginSuccess: boolean + searchSuccess: boolean + urlTestsSuccess?: number + } + } = {} + + for (const moduleName of Object.keys(testOptions)) { + const module = lucida.modules[moduleName] + console.log(`Testing ${moduleName}...`) + testResults[moduleName] ??= { + loginSuccess: module ? true : false, + searchSuccess: false + } + if (!module) { + console.error(`Module ${moduleName} not found in Lucida instance. Skipping...`) + continue + } + try { + await Promise.race([ + module.search('test', 5), + new Promise((_, reject) => setTimeout(() => reject('Search timed out'), 10000)) + ]) + console.log(`Searching with the ${moduleName} streamer succeeded`) + testResults[moduleName].searchSuccess = true + } catch (error) { + console.error(`Searching with the ${moduleName} streamer failed:`, error) + } + + let urlTestsSuccess = 0 + if (module.testData) { + for (const url in module.testData) { + const expected = module.testData[url] + let getResult: GetByUrlResponse + try { + getResult = ( + await Promise.race([ + module.getByUrl(url), + new Promise((_, reject) => setTimeout(() => reject('Search timed out'), 10000)) + ]) + ) + } catch (error) { + console.error(`Getting data from ${url} with the ${moduleName} streamer failed:`, error) + continue + } + const actual = { + title: getResult.type != 'artist' ? getResult.metadata.title : getResult.metadata.name, + type: getResult.type + } + const success = { + title: actual.title.toLowerCase() == expected.title.toLowerCase(), + type: actual.type == expected.type + } + if (!success.type) { + console.error( + `Type mismatch for ${url} with the ${moduleName} streamer. Expected: ${expected.title}, got: ${actual.title}` + ) + } + if (!success.title) { + console.error( + `Title mismatch for ${url} with the ${moduleName} streamer. Expected: ${expected.title}, got: ${actual.title}` + ) + } + if (success.title && success.type) { + console.log(`getByUrl test succeeded`) + urlTestsSuccess++ + } + } + + testResults[moduleName].urlTestsSuccess = + urlTestsSuccess / Object.keys(module.testData).length + } else { + console.error(`No test data found for ${moduleName}. Skipping URL tests...`) + } + } + + lucida.disconnect() + + console.log('Testing complete. Results:', testResults) + + const readableResults = Object.entries(testResults).map((result) => { + const moduleName = result[0] + const moduleResults = result[1] + const score = Object.values(moduleResults).filter( + (result) => result === 1 || result != false + ).length + + const percentage = (score / Object.keys(moduleResults).length) * 100 + return `- ${moduleName}: ${percentage.toFixed(2)}%` + }) + + fs.writeFileSync( + '.test-results.md', + `## Testing complete\n\n${readableResults.join('\n')}\n\n
Results\n\n\`\`\`json\n` + + JSON.stringify(testResults, null, '\t') + + '\n```\n\n
' + ) +} + +start() diff --git a/src/types.ts b/src/types.ts index 593f6e0..8ea2c9e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -138,8 +138,16 @@ export interface StreamerAccount { explicit?: boolean } +export interface StreamerTestData { + [url: string]: { + title: string + type: ItemType + } +} + export interface Streamer { hostnames: string[] + testData?: StreamerTestData search(query: string, limit: number): Promise getTypeFromUrl(url: string): Promise getByUrl: