mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +01:00 
			
		
		
		
	Test BookFinder.js using mocha
This commit is contained in:
		
							parent
							
								
									d1671f0ddc
								
							
						
					
					
						commit
						e8c14dbb58
					
				
							
								
								
									
										6385
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										6385
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										11
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								package.json
									
									
									
									
									
								
							| @ -16,7 +16,7 @@ | |||||||
|     "docker-arm64-local": "docker buildx build --platform linux/arm64 --load .  -t advplyr/audiobookshelf-arm64-local", |     "docker-arm64-local": "docker buildx build --platform linux/arm64 --load .  -t advplyr/audiobookshelf-arm64-local", | ||||||
|     "docker-armv7-local": "docker buildx build --platform linux/arm/v7 --load .  -t advplyr/audiobookshelf-armv7-local", |     "docker-armv7-local": "docker buildx build --platform linux/arm/v7 --load .  -t advplyr/audiobookshelf-armv7-local", | ||||||
|     "deploy-linux": "node deploy/linux", |     "deploy-linux": "node deploy/linux", | ||||||
|     "test": "jest" |     "test": "mocha" | ||||||
|   }, |   }, | ||||||
|   "bin": "prod.js", |   "bin": "prod.js", | ||||||
|   "pkg": { |   "pkg": { | ||||||
| @ -29,6 +29,9 @@ | |||||||
|       "server/**/*.js" |       "server/**/*.js" | ||||||
|     ] |     ] | ||||||
|   }, |   }, | ||||||
|  |   "mocha": { | ||||||
|  |     "recursive": true | ||||||
|  |   }, | ||||||
|   "author": "advplyr", |   "author": "advplyr", | ||||||
|   "license": "GPL-3.0", |   "license": "GPL-3.0", | ||||||
|   "dependencies": { |   "dependencies": { | ||||||
| @ -45,7 +48,9 @@ | |||||||
|     "xml2js": "^0.5.0" |     "xml2js": "^0.5.0" | ||||||
|   }, |   }, | ||||||
|   "devDependencies": { |   "devDependencies": { | ||||||
|     "jest": "^29.7.0", |     "chai": "^4.3.10", | ||||||
|     "nodemon": "^2.0.20" |     "mocha": "^10.2.0", | ||||||
|  |     "nodemon": "^2.0.20", | ||||||
|  |     "sinon": "^17.0.1" | ||||||
|   } |   } | ||||||
| } | } | ||||||
| @ -1,315 +0,0 @@ | |||||||
| const bookFinder = require('./BookFinder') |  | ||||||
| const Audnexus = require('../providers/Audnexus') |  | ||||||
| const { LogLevel } = require('../utils/constants') |  | ||||||
| const Logger = require('../Logger') |  | ||||||
| jest.mock('../providers/Audnexus') |  | ||||||
| 
 |  | ||||||
| Logger.setLogLevel(LogLevel.INFO) |  | ||||||
| 
 |  | ||||||
| describe('TitleCandidates', () => { |  | ||||||
|   describe('cleanAuthor non-empty', () => { |  | ||||||
|     let titleCandidates |  | ||||||
|     let cleanAuthor = 'leo tolstoy' |  | ||||||
| 
 |  | ||||||
|     beforeEach(() => { |  | ||||||
|       titleCandidates = new bookFinder.constructor.TitleCandidates(cleanAuthor) |  | ||||||
|     }) |  | ||||||
| 
 |  | ||||||
|     describe('single add', () => { |  | ||||||
|       it.each([ |  | ||||||
|         ['adds a clean title to candidates', 'anna karenina', ['anna karenina']], |  | ||||||
|         ['lowercases candidate title', 'ANNA KARENINA', ['anna karenina']], |  | ||||||
|         ['removes author name from title', `anna karenina by ${cleanAuthor}`, ['anna karenina']], |  | ||||||
|         ['removes author name title', cleanAuthor, []], |  | ||||||
|         ['cleans subtitle from title', 'anna karenina: subtitle', ['anna karenina']], |  | ||||||
|         ['removes "by ..." from title', 'anna karenina by arnold schwarzenegger', ['anna karenina', 'anna karenina by arnold schwarzenegger']], |  | ||||||
|         ['removes bitrate from title', 'anna karenina 64kbps', ['anna karenina', 'anna karenina 64kbps']], |  | ||||||
|         ['removes edition from title 1', 'anna karenina 2nd edition', ['anna karenina', 'anna karenina 2nd edition']], |  | ||||||
|         ['removes edition from title 2', 'anna karenina 4th ed.', ['anna karenina', 'anna karenina 4th ed.']], |  | ||||||
|         ['removes file-type from title', 'anna karenina.mp3', ['anna karenina', 'anna karenina.mp3']], |  | ||||||
|         ['removes "a novel" from title', 'anna karenina a novel', ['anna karenina', 'anna karenina a novel']], |  | ||||||
|         ['removes preceding/trailing numbers from title', '1 anna karenina 2', ['anna karenina', '1 anna karenina 2']], |  | ||||||
|         ['does not add empty title', '', []], |  | ||||||
|         ['does not add title with only spaces', '   ', []], |  | ||||||
|         ['adds digit-only title, but not its empty string transformation', '1984', ['1984']], |  | ||||||
|       ])('%s', (_, title, expected) => { |  | ||||||
|         titleCandidates.add(title) |  | ||||||
|         expect(titleCandidates.getCandidates()).toEqual(expected) |  | ||||||
|       }) |  | ||||||
|     }) |  | ||||||
| 
 |  | ||||||
|     describe('multi add', () => { |  | ||||||
|       it.each([ |  | ||||||
|         ['digits-only candidates get lower priority', ['01', 'anna karenina'], ['anna karenina', '01']], |  | ||||||
|         ['transformed candidates get higher priority', ['title1 1', 'title2 1'], ['title1', 'title2', 'title1 1', 'title2 1']], |  | ||||||
|         ['other candidates are ordered by position', ['title1', 'title2'], ['title1', 'title2']], |  | ||||||
|         ['author candidate is removed', ['title1', cleanAuthor], ['title1']], |  | ||||||
|       ])('%s', (_, titles, expected) => { |  | ||||||
|         for (const title of titles) titleCandidates.add(title) |  | ||||||
|         expect(titleCandidates.getCandidates()).toEqual(expected) |  | ||||||
|       }) |  | ||||||
|     }) |  | ||||||
|   }) |  | ||||||
| 
 |  | ||||||
|   describe('cleanAuthor empty', () => { |  | ||||||
|     let titleCandidates |  | ||||||
|     let cleanAuthor = '' |  | ||||||
|    |  | ||||||
|     beforeEach(() => { |  | ||||||
|       titleCandidates = new bookFinder.constructor.TitleCandidates(cleanAuthor) |  | ||||||
|     }) |  | ||||||
|    |  | ||||||
|     describe('single add', () => { |  | ||||||
|       it.each([ |  | ||||||
|         ['does not remove author name', 'leo tolstoy', ['leo tolstoy']], |  | ||||||
|       ])('%s', (_, title, expected) => { |  | ||||||
|         titleCandidates.add(title) |  | ||||||
|         expect(titleCandidates.getCandidates()).toEqual(expected) |  | ||||||
|       }) |  | ||||||
|     }) |  | ||||||
|   })   |  | ||||||
| }) |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| describe('AuthorCandidates', () => { |  | ||||||
|   let authorCandidates |  | ||||||
|   const audnexus = new Audnexus() |  | ||||||
|   audnexus.authorASINsRequest.mockResolvedValue([  |  | ||||||
|     { name: 'Leo Tolstoy' },  |  | ||||||
|     { name: 'Nikolai Gogol' }, |  | ||||||
|     { name: 'J. K. Rowling' }, |  | ||||||
|   ]) |  | ||||||
| 
 |  | ||||||
|   describe('cleanAuthor is null', () => { |  | ||||||
|     beforeEach(() => { |  | ||||||
|       authorCandidates = new bookFinder.constructor.AuthorCandidates(null, audnexus) |  | ||||||
|     }) |  | ||||||
| 
 |  | ||||||
|     describe('no add', () => { |  | ||||||
|       it.each([ |  | ||||||
|         ['returns empty author', []], |  | ||||||
|       ])('%s', async (_,  expected) => { |  | ||||||
|         expect(await authorCandidates.getCandidates()).toEqual([...expected, '']) |  | ||||||
|       }) |  | ||||||
|     }) |  | ||||||
| 
 |  | ||||||
|     describe('single add', () => { |  | ||||||
|       it.each([ |  | ||||||
|         ['returns valid author', 'nikolai gogol', ['nikolai gogol']],         |  | ||||||
|         ['does not return invalid author (not in list)', 'fyodor dostoevsky', []], |  | ||||||
|         ['returns valid author (valid is a substring of added)', 'dr. nikolai gogol', ['nikolai gogol']], |  | ||||||
|         ['returns added author (added is a substring of valid)', 'gogol', ['gogol']], |  | ||||||
|         ['returns valid author (added is similar to valid)', 'nicolai gogol', ['nikolai gogol']], |  | ||||||
|         ['does not return invalid author (added too distant)', 'nikolai google', []], |  | ||||||
|         ['returns valid author (contains redundant spaces)', 'nikolai    gogol', ['nikolai gogol']], |  | ||||||
|         ['returns valid author (normalized initials)', 'j.k. rowling', ['j. k. rowling']], |  | ||||||
|       ])('%s', async (_, author, expected) => { |  | ||||||
|         authorCandidates.add(author) |  | ||||||
|         expect(await authorCandidates.getCandidates()).toEqual([...expected, '']) |  | ||||||
|       }) |  | ||||||
|     }) |  | ||||||
| 
 |  | ||||||
|     describe('multi add', () => { |  | ||||||
|       it.each([ |  | ||||||
|         ['returns valid authors', ['nikolai gogol', 'leo tolstoy'], ['nikolai gogol', 'leo tolstoy']], |  | ||||||
|         ['returns deduped valid authors', ['nikolai gogol', 'nikolai gogol'], ['nikolai gogol']], |  | ||||||
|       ])('%s', async (_, authors, expected) => { |  | ||||||
|         for (const author of authors) authorCandidates.add(author) |  | ||||||
|         expect(await authorCandidates.getCandidates()).toEqual([...expected, '']) |  | ||||||
|       }) |  | ||||||
|     }) |  | ||||||
|   }) |  | ||||||
| 
 |  | ||||||
|   describe('cleanAuthor is valid', () => { |  | ||||||
|     const cleanAuthor = 'leo tolstoy' |  | ||||||
| 
 |  | ||||||
|     beforeEach(() => { |  | ||||||
|       authorCandidates = new bookFinder.constructor.AuthorCandidates(cleanAuthor, audnexus) |  | ||||||
|     }) |  | ||||||
| 
 |  | ||||||
|     describe('no add', () => { |  | ||||||
|       it.each([ |  | ||||||
|         ['returns clean author from constructor', [cleanAuthor]], |  | ||||||
|       ])('%s', async (_,  expected) => { |  | ||||||
|         expect(await authorCandidates.getCandidates()).toEqual([...expected, '']) |  | ||||||
|       }) |  | ||||||
|     }) |  | ||||||
| 
 |  | ||||||
|     describe('single add', () => { |  | ||||||
|       it.each([ |  | ||||||
|         ['returns cleanAuthor + valid author', 'nikolai gogol', [cleanAuthor, 'nikolai gogol']], |  | ||||||
|         ['returns deduplicated author', cleanAuthor, [cleanAuthor]],         |  | ||||||
|       ])('%s', async (_, author, expected) => { |  | ||||||
|         authorCandidates.add(author) |  | ||||||
|         expect(await authorCandidates.getCandidates()).toEqual([...expected, '']) |  | ||||||
|       }) |  | ||||||
|     }) |  | ||||||
|   }) |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|   describe('cleanAuthor is invalid', () => { |  | ||||||
|     const cleanAuthor = 'fyodor dostoevsky' |  | ||||||
| 
 |  | ||||||
|     beforeEach(() => { |  | ||||||
|       authorCandidates = new bookFinder.constructor.AuthorCandidates(cleanAuthor, audnexus) |  | ||||||
|     }) |  | ||||||
| 
 |  | ||||||
|     describe('no add', () => { |  | ||||||
|       it.each([ |  | ||||||
|         ['returns invalid clean author from constructor', [cleanAuthor]], |  | ||||||
|       ])('%s', async (_,  expected) => { |  | ||||||
|         expect(await authorCandidates.getCandidates()).toEqual([...expected, '']) |  | ||||||
|       }) |  | ||||||
|     }) |  | ||||||
| 
 |  | ||||||
|     describe('single add', () => { |  | ||||||
|       it.each([ |  | ||||||
|         ['returns only valid author', 'nikolai gogol', ['nikolai gogol']], |  | ||||||
|       ])('%s', async (_, author, expected) => { |  | ||||||
|         authorCandidates.add(author) |  | ||||||
|         expect(await authorCandidates.getCandidates()).toEqual([...expected, '']) |  | ||||||
|       }) |  | ||||||
|     }) |  | ||||||
|   }) |  | ||||||
| 
 |  | ||||||
|   describe('cleanAuthor is invalid and dirty', () => { |  | ||||||
|     describe('no add', () => { |  | ||||||
|       it.each([ |  | ||||||
|         ['returns invalid aggressively cleanAuthor from constructor', 'fyodor dostoevsky, translated by jackie chan', ['fyodor dostoevsky']], |  | ||||||
|         ['returns invalid cleanAuthor from constructor (empty after aggressive ckean)', ', jackie chan', [', jackie chan']], |  | ||||||
|       ])('%s', async (_,  cleanAuthor, expected) => { |  | ||||||
|         authorCandidates = new bookFinder.constructor.AuthorCandidates(cleanAuthor, audnexus) |  | ||||||
|         expect(await authorCandidates.getCandidates()).toEqual([...expected, '']) |  | ||||||
|       }) |  | ||||||
|     }) |  | ||||||
|   }) |  | ||||||
| }) |  | ||||||
| 
 |  | ||||||
| describe('search', () => { |  | ||||||
|   const t = 'title' |  | ||||||
|   const a = 'author' |  | ||||||
|   const u = 'unknown' |  | ||||||
|   const r = ['book'] |  | ||||||
| 
 |  | ||||||
|   bookFinder.runSearch = jest.fn((searchTitle, searchAuthor) => { |  | ||||||
|     return new Promise((resolve) => { |  | ||||||
|       resolve(searchTitle == t && (searchAuthor == a || searchAuthor == u) ? r : []) |  | ||||||
|     }) |  | ||||||
|   }) |  | ||||||
|    |  | ||||||
|   const audnexus = new Audnexus() |  | ||||||
|   audnexus.authorASINsRequest.mockResolvedValue([  |  | ||||||
|     { name: a },  |  | ||||||
|   ]) |  | ||||||
|   bookFinder.audnexus = audnexus |  | ||||||
| 
 |  | ||||||
|   beforeEach(() => { |  | ||||||
|     bookFinder.runSearch.mockClear() |  | ||||||
|   }) |  | ||||||
| 
 |  | ||||||
|   describe('no or empty title', () => { |  | ||||||
|     it('returns empty result', async () => { |  | ||||||
|       expect(await bookFinder.search('', '', a)).toEqual([]) |  | ||||||
|       expect(bookFinder.runSearch).toHaveBeenCalledTimes(0) |  | ||||||
|     }) |  | ||||||
|   }) |  | ||||||
| 
 |  | ||||||
|   describe('exact valid title and exact valid author', () => { |  | ||||||
|     it('returns result (no fuzzy searches)', async () => { |  | ||||||
|       expect(await bookFinder.search('', t, a)).toEqual(r) |  | ||||||
|       expect(bookFinder.runSearch).toHaveBeenCalledTimes(1) |  | ||||||
|     }) |  | ||||||
|   }) |  | ||||||
| 
 |  | ||||||
|   describe('contains valid title and exact valid author', () => { |  | ||||||
|     it.each([ |  | ||||||
|       [`${t} -`], |  | ||||||
|       [`${t} - ${a}`], |  | ||||||
|       [`${a} - ${t}`], |  | ||||||
|       [`${t}- ${a}`], |  | ||||||
|       [`${t} -${a}`], |  | ||||||
|       [`${t} ${a}`], |  | ||||||
|       [`${a} - ${t} (unabridged)`], |  | ||||||
|       [`${a} - ${t} (subtitle) - mp3`], |  | ||||||
|       [`${t} {narrator} - series-01 64kbps 10:00:00`], |  | ||||||
|       [`${a} - ${t} (2006) narrated by narrator [unabridged]`], |  | ||||||
|       [`${t} - ${a} 2022 mp3`], |  | ||||||
|       [`01 ${t}`], |  | ||||||
|       [`2022_${t}_HQ`], |  | ||||||
|     ])(`returns result ('%s', '${a}') (1 fuzzy search)` , async (searchTitle) => { |  | ||||||
|       expect(await bookFinder.search('', searchTitle, a)).toEqual(r) |  | ||||||
|       expect(bookFinder.runSearch).toHaveBeenCalledTimes(2) |  | ||||||
|     }) |  | ||||||
| 
 |  | ||||||
|      |  | ||||||
|     it.each([ |  | ||||||
|       [`s-01 - ${t} (narrator) 64kbps 10:00:00`], |  | ||||||
|       [`${a} - series 01 - ${t}`], |  | ||||||
|     ])(`returns result ('%s', '${a}') (2 fuzzy searches)` , async (searchTitle) => { |  | ||||||
|       expect(await bookFinder.search('', searchTitle, a)).toEqual(r) |  | ||||||
|       expect(bookFinder.runSearch).toHaveBeenCalledTimes(3) |  | ||||||
|     }) |  | ||||||
| 
 |  | ||||||
|     it.each([ |  | ||||||
|       [`${t}-${a}`], |  | ||||||
|       [`${t} junk`], |  | ||||||
|     ])(`returns empty result ('%s', '${a}')`, async (searchTitle) => {  |  | ||||||
|       expect(await bookFinder.search('', searchTitle, a)).toEqual([]) |  | ||||||
|     }) |  | ||||||
| 
 |  | ||||||
|     describe('maxFuzzySearches = 0', () => { |  | ||||||
|       it.each([ |  | ||||||
|         [`${t} - ${a}`], |  | ||||||
|       ])(`returns empty result ('%s', '${a}') (no fuzzy search)` , async (searchTitle) => { |  | ||||||
|         expect(await bookFinder.search('', searchTitle, a, null, null, { maxFuzzySearches: 0 })).toEqual([]) |  | ||||||
|         expect(bookFinder.runSearch).toHaveBeenCalledTimes(1) |  | ||||||
|       })   |  | ||||||
|     }) |  | ||||||
| 
 |  | ||||||
|     describe('maxFuzzySearches = 1', () => { |  | ||||||
|       it.each([ |  | ||||||
|         [`s-01 - ${t} (narrator) 64kbps 10:00:00`], |  | ||||||
|         [`${a} - series 01 - ${t}`], |  | ||||||
|         ])(`returns empty result ('%s', '${a}') (1 fuzzy search)` , async (searchTitle) => { |  | ||||||
|         expect(await bookFinder.search('', searchTitle, a, null, null, { maxFuzzySearches: 1 })).toEqual([]) |  | ||||||
|         expect(bookFinder.runSearch).toHaveBeenCalledTimes(2) |  | ||||||
|       })   |  | ||||||
|     })   |  | ||||||
|   }) |  | ||||||
| 
 |  | ||||||
|   describe('contains valid title and no author', () => { |  | ||||||
|     it.each([ |  | ||||||
|       [`${t} - ${a}`], |  | ||||||
|       [`${a} - ${t}`], |  | ||||||
|     ])(`returns result ('%s', '') (1 fuzzy search)` , async (searchTitle) => { |  | ||||||
|       expect(await bookFinder.search('', searchTitle, '')).toEqual(r) |  | ||||||
|       expect(bookFinder.runSearch).toHaveBeenCalledTimes(2) |  | ||||||
|     }) |  | ||||||
| 
 |  | ||||||
|     it.each([ |  | ||||||
|       [`${t}`], |  | ||||||
|       [`${t} - ${u}`], |  | ||||||
|       [`${u} - ${t}`], |  | ||||||
|     ])(`returns empty result ('%s', '') (no fuzzy search)` , async (searchTitle) => { |  | ||||||
|       expect(await bookFinder.search('', searchTitle, '')).toEqual([]) |  | ||||||
|     }) |  | ||||||
|   }) |  | ||||||
| 
 |  | ||||||
|   describe('contains valid title and unknown author', () => { |  | ||||||
|     it.each([ |  | ||||||
|       [`${t} - ${u}`], |  | ||||||
|       [`${u} - ${t}`], |  | ||||||
|     ])(`returns result ('%s', '') (1 fuzzy search)` , async (searchTitle) => { |  | ||||||
|       expect(await bookFinder.search('', searchTitle, u)).toEqual(r) |  | ||||||
|       expect(bookFinder.runSearch).toHaveBeenCalledTimes(2) |  | ||||||
|     }) |  | ||||||
| 
 |  | ||||||
|     it.each([ |  | ||||||
|       [`${t}`], |  | ||||||
|     ])(`returns result ('%s', '') (no fuzzy search)` , async (searchTitle) => { |  | ||||||
|       expect(await bookFinder.search('', searchTitle, u)).toEqual(r) |  | ||||||
|       expect(bookFinder.runSearch).toHaveBeenCalledTimes(1) |  | ||||||
|     }) |  | ||||||
|   }) |  | ||||||
| 
 |  | ||||||
| }) |  | ||||||
							
								
								
									
										344
									
								
								test/server/finders/BookFinder.test.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										344
									
								
								test/server/finders/BookFinder.test.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,344 @@ | |||||||
|  | const sinon = require('sinon'); | ||||||
|  | const chai = require('chai'); | ||||||
|  | const expect = chai.expect; | ||||||
|  | const bookFinder = require('../../../server/finders/BookFinder'); | ||||||
|  | const { LogLevel } = require('../../../server/utils/constants') | ||||||
|  | const Logger = require('../../../server/Logger') | ||||||
|  | Logger.setLogLevel(LogLevel.INFO) | ||||||
|  | 
 | ||||||
|  |   describe('TitleCandidates', () => { | ||||||
|  |     describe('cleanAuthor non-empty', () => { | ||||||
|  |       let titleCandidates; | ||||||
|  |       const cleanAuthor = 'leo tolstoy'; | ||||||
|  | 
 | ||||||
|  |       beforeEach(() => { | ||||||
|  |         titleCandidates = new bookFinder.constructor.TitleCandidates(cleanAuthor); | ||||||
|  |       }); | ||||||
|  | 
 | ||||||
|  |       describe('no adds', () => { | ||||||
|  |         it('returns no candidates', () => { | ||||||
|  |           expect(titleCandidates.getCandidates()).to.deep.equal([]); | ||||||
|  |         }) | ||||||
|  |       }) | ||||||
|  | 
 | ||||||
|  |       describe('single add', () => { | ||||||
|  |         [ | ||||||
|  |           ['adds candidate', 'anna karenina', ['anna karenina']], | ||||||
|  |           ['adds lowercased candidate', 'ANNA KARENINA', ['anna karenina']], | ||||||
|  |           ['adds candidate, removing redundant spaces', 'anna    karenina', ['anna karenina']], | ||||||
|  |           ['adds candidate, removing author', `anna karenina by ${cleanAuthor}`, ['anna karenina']], | ||||||
|  |           ['does not add empty candidate after removing author', cleanAuthor, []], | ||||||
|  |           ['adds candidate, removing subtitle', 'anna karenina: subtitle', ['anna karenina']], | ||||||
|  |           ['adds candidate + variant, removing "by ..."', 'anna karenina by arnold schwarzenegger', ['anna karenina', 'anna karenina by arnold schwarzenegger']], | ||||||
|  |           ['adds candidate + variant, removing bitrate', 'anna karenina 64kbps', ['anna karenina', 'anna karenina 64kbps']], | ||||||
|  |           ['adds candidate + variant, removing edition 1', 'anna karenina 2nd edition', ['anna karenina', 'anna karenina 2nd edition']], | ||||||
|  |           ['adds candidate + variant, removing edition 2', 'anna karenina 4th ed.', ['anna karenina', 'anna karenina 4th ed.']], | ||||||
|  |           ['adds candidate + variant, removing fie type', 'anna karenina.mp3', ['anna karenina', 'anna karenina.mp3']], | ||||||
|  |           ['adds candidate + variant, removing "a novel"', 'anna karenina a novel', ['anna karenina', 'anna karenina a novel']], | ||||||
|  |           ['adds candidate + variant, removing preceding/trailing numbers', '1 anna karenina 2', ['anna karenina', '1 anna karenina 2']], | ||||||
|  |           ['does not add empty candidate', '', []], | ||||||
|  |           ['does not add spaces-only candidate', '   ', []], | ||||||
|  |           ['does not add empty variant', '1984', ['1984']], | ||||||
|  |         ].forEach(([name, title, expected]) => it(name, () => { | ||||||
|  |           titleCandidates.add(title); | ||||||
|  |           expect(titleCandidates.getCandidates()).to.deep.equal(expected); | ||||||
|  |         })); | ||||||
|  |       }) | ||||||
|  | 
 | ||||||
|  |       describe('multiple adds', () => { | ||||||
|  |         [ | ||||||
|  |           ['demotes digits-only candidates', ['01', 'anna karenina'], ['anna karenina', '01']], | ||||||
|  |           ['promotes transformed variants', ['title1 1', 'title2 1'], ['title1', 'title2', 'title1 1', 'title2 1']], | ||||||
|  |           ['orders by position', ['title2', 'title1'], ['title2', 'title1']], | ||||||
|  |           ['dedupes candidates', ['title1', 'title1'], ['title1']], | ||||||
|  |         ].forEach(([name, titles, expected]) => it(name, () => { | ||||||
|  |           for (const title of titles) titleCandidates.add(title) | ||||||
|  |           expect(titleCandidates.getCandidates()).to.deep.equal(expected); | ||||||
|  |         })); | ||||||
|  |       }) | ||||||
|  |     }) | ||||||
|  | 
 | ||||||
|  |     describe('cleanAuthor empty', () => { | ||||||
|  |       let titleCandidates | ||||||
|  |       let cleanAuthor = '' | ||||||
|  |      | ||||||
|  |       beforeEach(() => { | ||||||
|  |         titleCandidates = new bookFinder.constructor.TitleCandidates(cleanAuthor) | ||||||
|  |       }) | ||||||
|  |      | ||||||
|  |       describe('single add', () => { | ||||||
|  |         [ | ||||||
|  |           ['adds a candidate', 'leo tolstoy', ['leo tolstoy']], | ||||||
|  |         ].forEach(([name, title, expected]) => it(name, () => { | ||||||
|  |           titleCandidates.add(title) | ||||||
|  |           expect(titleCandidates.getCandidates()).to.deep.equal(expected); | ||||||
|  |         })) | ||||||
|  |       }) | ||||||
|  |     }) | ||||||
|  |   }) | ||||||
|  | 
 | ||||||
|  |   describe('AuthorCandidates', () => { | ||||||
|  |     let authorCandidates; | ||||||
|  |     const audnexus = { | ||||||
|  |       authorASINsRequest: sinon.stub().resolves([ | ||||||
|  |         { name: 'Leo Tolstoy' }, | ||||||
|  |         { name: 'Nikolai Gogol' }, | ||||||
|  |         { name: 'J. K. Rowling' }, | ||||||
|  |       ]), | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     describe('cleanAuthor is null', () => { | ||||||
|  |       beforeEach(() => { | ||||||
|  |         authorCandidates = new bookFinder.constructor.AuthorCandidates(null, audnexus); | ||||||
|  |       }); | ||||||
|  | 
 | ||||||
|  |       describe('no adds', () => { | ||||||
|  |         [ | ||||||
|  |           ['returns empty author candidate', []], | ||||||
|  |         ].forEach(([name, expected]) => it(name, async () => { | ||||||
|  |           expect(await authorCandidates.getCandidates()).to.deep.equal([...expected, '']) | ||||||
|  |         })) | ||||||
|  |       }); | ||||||
|  | 
 | ||||||
|  |       describe('single add', () => { | ||||||
|  |         [ | ||||||
|  |           ['adds recognized candidate', 'nikolai gogol', ['nikolai gogol']],         | ||||||
|  |           ['does not add unrecognized candidate', 'fyodor dostoevsky', []], | ||||||
|  |           ['adds recognized author if candidate is a superstring', 'dr. nikolai gogol', ['nikolai gogol']], | ||||||
|  |           ['adds candidate if it is a substring of recognized author', 'gogol', ['gogol']], | ||||||
|  |           ['adds recognized author if edit distance from candidate is small', 'nicolai gogol', ['nikolai gogol']], | ||||||
|  |           ['does not add candidate if edit distance from any recognized author is large', 'nikolai google', []], | ||||||
|  |           ['adds normalized recognized candidate (contains redundant spaces)', 'nikolai    gogol', ['nikolai gogol']], | ||||||
|  |           ['adds normalized recognized candidate (normalized initials)', 'j.k. rowling', ['j. k. rowling']], | ||||||
|  |         ].forEach(([name, author, expected]) => it(name, async () => { | ||||||
|  |           authorCandidates.add(author) | ||||||
|  |           expect(await authorCandidates.getCandidates()).to.deep.equal([...expected, '']) | ||||||
|  |         })); | ||||||
|  |       }) | ||||||
|  |    | ||||||
|  |       describe('multi add', () => { | ||||||
|  |         [ | ||||||
|  |           ['adds recognized author candidates', ['nikolai gogol', 'leo tolstoy'], ['nikolai gogol', 'leo tolstoy']], | ||||||
|  |           ['dedupes author candidates', ['nikolai gogol', 'nikolai gogol'], ['nikolai gogol']], | ||||||
|  |         ].forEach(([name, authors, expected]) => it(name, async () => { | ||||||
|  |           for (const author of authors) authorCandidates.add(author) | ||||||
|  |           expect(await authorCandidates.getCandidates()).to.deep.equal([...expected, '']) | ||||||
|  |         })) | ||||||
|  |       }) | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     describe('cleanAuthor is a recognized author', () => { | ||||||
|  |       const cleanAuthor = 'leo tolstoy'; | ||||||
|  | 
 | ||||||
|  |       beforeEach(() => { | ||||||
|  |         authorCandidates = new bookFinder.constructor.AuthorCandidates(cleanAuthor, audnexus); | ||||||
|  |       }); | ||||||
|  | 
 | ||||||
|  |       describe('no adds', () => { | ||||||
|  |         [ | ||||||
|  |           ['adds cleanAuthor as candidate', [cleanAuthor]], | ||||||
|  |         ].forEach(([name, expected]) => it(name, async () => { | ||||||
|  |           expect(await authorCandidates.getCandidates()).to.deep.equal([...expected, '']) | ||||||
|  |         })) | ||||||
|  |       }) | ||||||
|  |    | ||||||
|  |       describe('single add', () => { | ||||||
|  |         [ | ||||||
|  |           ['adds recognized candidate', 'nikolai gogol', [cleanAuthor, 'nikolai gogol']], | ||||||
|  |           ['does not add candidate if it is a dupe of cleanAuthor', cleanAuthor, [cleanAuthor]],         | ||||||
|  |         ].forEach(([name, author, expected]) => it(name, async () => { | ||||||
|  |           authorCandidates.add(author) | ||||||
|  |           expect(await authorCandidates.getCandidates()).to.deep.equal([...expected, '']) | ||||||
|  |         })) | ||||||
|  |       }) | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     describe('cleanAuthor is an unrecognized author', () => { | ||||||
|  |       const cleanAuthor = 'Fyodor Dostoevsky'; | ||||||
|  | 
 | ||||||
|  |       beforeEach(() => { | ||||||
|  |         authorCandidates = new bookFinder.constructor.AuthorCandidates(cleanAuthor, audnexus); | ||||||
|  |       }); | ||||||
|  | 
 | ||||||
|  |       describe('no adds', () => { | ||||||
|  |         [ | ||||||
|  |           ['adds cleanAuthor as candidate', [cleanAuthor]], | ||||||
|  |         ].forEach(([name, expected]) => it(name, async () => { | ||||||
|  |           expect(await authorCandidates.getCandidates()).to.deep.equal([...expected, '']) | ||||||
|  |         })) | ||||||
|  |       }) | ||||||
|  |    | ||||||
|  |       describe('single add', () => { | ||||||
|  |         [ | ||||||
|  |           ['adds recognized candidate and removes cleanAuthor', 'nikolai gogol', ['nikolai gogol']], | ||||||
|  |           ['does not add unrecognized candidate', 'jackie chan', [cleanAuthor]], | ||||||
|  |         ].forEach(([name, author, expected]) => it(name, async () => { | ||||||
|  |           authorCandidates.add(author) | ||||||
|  |           expect(await authorCandidates.getCandidates()).to.deep.equal([...expected, '']) | ||||||
|  |         })) | ||||||
|  |       }) | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     describe('cleanAuthor is unrecognized and dirty', () => { | ||||||
|  |       describe('no adds', () => { | ||||||
|  |         [ | ||||||
|  |           ['adds aggressively cleaned cleanAuthor', 'fyodor dostoevsky, translated by jackie chan', ['fyodor dostoevsky']], | ||||||
|  |           ['adds cleanAuthor if aggresively cleaned cleanAuthor is empty', ', jackie chan', [', jackie chan']], | ||||||
|  |         ].forEach(([name, cleanAuthor, expected]) => it(name, async () => { | ||||||
|  |           authorCandidates = new bookFinder.constructor.AuthorCandidates(cleanAuthor, audnexus) | ||||||
|  |           expect(await authorCandidates.getCandidates()).to.deep.equal([...expected, '']) | ||||||
|  |         })) | ||||||
|  |       }) | ||||||
|  | 
 | ||||||
|  |       describe('single add', () => { | ||||||
|  |         [ | ||||||
|  |           ['adds recognized candidate and removes cleanAuthor', 'fyodor dostoevsky, translated by jackie chan', 'nikolai gogol', ['nikolai gogol']], | ||||||
|  |         ].forEach(([name, cleanAuthor, author, expected]) => it(name, async () => { | ||||||
|  |           authorCandidates = new bookFinder.constructor.AuthorCandidates(cleanAuthor, audnexus) | ||||||
|  |           authorCandidates.add(author) | ||||||
|  |           expect(await authorCandidates.getCandidates()).to.deep.equal([...expected, '']) | ||||||
|  |         })) | ||||||
|  |       }) | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   describe('search', () => { | ||||||
|  |     const t = 'title'; | ||||||
|  |     const a = 'author'; | ||||||
|  |     const u = 'unrecognized'; | ||||||
|  |     const r = ['book']; | ||||||
|  | 
 | ||||||
|  |     const runSearchStub = sinon.stub(bookFinder, 'runSearch') | ||||||
|  |     runSearchStub.resolves([]) | ||||||
|  |     runSearchStub.withArgs(t, a).resolves(r); | ||||||
|  |     runSearchStub.withArgs(t, u).resolves(r); | ||||||
|  | 
 | ||||||
|  |     const audnexusStub = sinon.stub(bookFinder.audnexus, 'authorASINsRequest') | ||||||
|  |     audnexusStub.resolves([ { name: a } ]) | ||||||
|  | 
 | ||||||
|  |     beforeEach(() => { | ||||||
|  |       bookFinder.runSearch.resetHistory(); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     describe('search title is empty', () => { | ||||||
|  |       it('returns empty result', async () => { | ||||||
|  |         expect(await bookFinder.search('', '', a)).to.deep.equal([]); | ||||||
|  |         sinon.assert.callCount(bookFinder.runSearch, 0); | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     describe('search title is a recognized title and search author is a recognized author', () => { | ||||||
|  |       it('returns non-empty result (no fuzzy searches)', async () => { | ||||||
|  |         expect(await bookFinder.search('', t, a)).to.deep.equal(r); | ||||||
|  |         sinon.assert.callCount(bookFinder.runSearch, 1); | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     describe('search title contains recognized title and search author is a recognized author', () => { | ||||||
|  |       [ | ||||||
|  |         [`${t} -`], | ||||||
|  |         [`${t} - ${a}`], | ||||||
|  |         [`${a} - ${t}`], | ||||||
|  |         [`${t}- ${a}`], | ||||||
|  |         [`${t} -${a}`], | ||||||
|  |         [`${t} ${a}`], | ||||||
|  |         [`${a} - ${t} (unabridged)`], | ||||||
|  |         [`${a} - ${t} (subtitle) - mp3`], | ||||||
|  |         [`${t} {narrator} - series-01 64kbps 10:00:00`], | ||||||
|  |         [`${a} - ${t} (2006) narrated by narrator [unabridged]`], | ||||||
|  |         [`${t} - ${a} 2022 mp3`], | ||||||
|  |         [`01 ${t}`], | ||||||
|  |         [`2022_${t}_HQ`], | ||||||
|  |       ].forEach(([searchTitle]) => { | ||||||
|  |         it(`search('${searchTitle}', '${a}') returns non-empty result (with 1 fuzzy search)`, async () => { | ||||||
|  |           expect(await bookFinder.search('', searchTitle, a)).to.deep.equal(r); | ||||||
|  |           sinon.assert.callCount(bookFinder.runSearch, 2); | ||||||
|  |         }); | ||||||
|  |       }); | ||||||
|  | 
 | ||||||
|  |       [ | ||||||
|  |         [`s-01 - ${t} (narrator) 64kbps 10:00:00`], | ||||||
|  |         [`${a} - series 01 - ${t}`], | ||||||
|  |       ].forEach(([searchTitle]) => { | ||||||
|  |         it(`search('${searchTitle}', '${a}') returns non-empty result (with 2 fuzzy searches)`, async () => { | ||||||
|  |           expect(await bookFinder.search('', searchTitle, a)).to.deep.equal(r); | ||||||
|  |           sinon.assert.callCount(bookFinder.runSearch, 3); | ||||||
|  |         }); | ||||||
|  |       }); | ||||||
|  | 
 | ||||||
|  |       [ | ||||||
|  |         [`${t}-${a}`], | ||||||
|  |         [`${t} junk`], | ||||||
|  |       ].forEach(([searchTitle]) => { | ||||||
|  |         it(`search('${searchTitle}', '${a}') returns an empty result`, async () => { | ||||||
|  |           expect(await bookFinder.search('', searchTitle, a)).to.deep.equal([]); | ||||||
|  |         }); | ||||||
|  |       }); | ||||||
|  | 
 | ||||||
|  |       describe('maxFuzzySearches = 0', () => { | ||||||
|  |         [ | ||||||
|  |           [`${t} - ${a}`], | ||||||
|  |         ].forEach(([searchTitle]) => { | ||||||
|  |           it(`search('${searchTitle}', '${a}') returns an empty result (with no fuzzy searches)`, async () => { | ||||||
|  |             expect(await bookFinder.search('', searchTitle, a, null, null, { maxFuzzySearches: 0 })).to.deep.equal([]); | ||||||
|  |             sinon.assert.callCount(bookFinder.runSearch, 1); | ||||||
|  |           }); | ||||||
|  |         }); | ||||||
|  |       }); | ||||||
|  | 
 | ||||||
|  |       describe('maxFuzzySearches = 1', () => { | ||||||
|  |         [ | ||||||
|  |           [`s-01 - ${t} (narrator) 64kbps 10:00:00`], | ||||||
|  |           [`${a} - series 01 - ${t}`], | ||||||
|  |         ].forEach(([searchTitle]) => { | ||||||
|  |           it(`search('${searchTitle}', '${a}') returns an empty result (1 fuzzy search)`, async () => { | ||||||
|  |             expect(await bookFinder.search('', searchTitle, a, null, null, { maxFuzzySearches: 1 })).to.deep.equal([]); | ||||||
|  |             sinon.assert.callCount(bookFinder.runSearch, 2); | ||||||
|  |           }); | ||||||
|  |         }); | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     describe('search title contains recognized title and search author is empty', () => { | ||||||
|  |       [ | ||||||
|  |         [`${t} - ${a}`], | ||||||
|  |         [`${a} - ${t}`], | ||||||
|  |       ].forEach(([searchTitle]) => { | ||||||
|  |         it(`search('${searchTitle}', '') returns a non-empty result (1 fuzzy search)`, async () => { | ||||||
|  |           expect(await bookFinder.search('', searchTitle, '')).to.deep.equal(r); | ||||||
|  |           sinon.assert.callCount(bookFinder.runSearch, 2); | ||||||
|  |         }); | ||||||
|  |       }); | ||||||
|  | 
 | ||||||
|  |       [ | ||||||
|  |         [`${t}`], | ||||||
|  |         [`${t} - ${u}`], | ||||||
|  |         [`${u} - ${t}`] | ||||||
|  |       ].forEach(([searchTitle]) => { | ||||||
|  |         it(`search('${searchTitle}', '') returns an empty result`, async () => { | ||||||
|  |           expect(await bookFinder.search('', searchTitle, '')).to.deep.equal([]); | ||||||
|  |         }); | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     describe('search title contains recognized title and search author is an unrecognized author', () => { | ||||||
|  |       [ | ||||||
|  |         [`${t} - ${u}`], | ||||||
|  |         [`${u} - ${t}`] | ||||||
|  |       ].forEach(([searchTitle]) => { | ||||||
|  |         it(`search('${searchTitle}', '${u}') returns a non-empty result (1 fuzzy search)`, async () => { | ||||||
|  |           expect(await bookFinder.search('', searchTitle, u)).to.deep.equal(r); | ||||||
|  |           sinon.assert.callCount(bookFinder.runSearch, 2); | ||||||
|  |         }); | ||||||
|  |       }); | ||||||
|  | 
 | ||||||
|  |       [ | ||||||
|  |         [`${t}`] | ||||||
|  |       ].forEach(([searchTitle]) => { | ||||||
|  |         it(`search('${searchTitle}', '${u}') returns a non-empty result (no fuzzy search)`, async () => { | ||||||
|  |           expect(await bookFinder.search('', searchTitle, u)).to.deep.equal(r); | ||||||
|  |           sinon.assert.callCount(bookFinder.runSearch, 1); | ||||||
|  |         }); | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
		Loading…
	
		Reference in New Issue
	
	Block a user