Skip to content

Commit 2cb5584

Browse files
committed
test: add tests for Errors, ffprobe, and Emby routes
- Add tests for Error classes - Add tests for ffprobe submodule - Add tests for Emby shows, users, and items routes to improve coverage
1 parent 285b32e commit 2cb5584

File tree

5 files changed

+954
-0
lines changed

5 files changed

+954
-0
lines changed

tests/mocha/Errors.spec.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
2+
import expect from 'expect.js';
3+
import ExtendableError from '../../src/lib/errors/ExtendableError.js';
4+
import DebugExtendableError from '../../src/lib/errors/DebugExtendableError.js';
5+
import InfoExtendableError from '../../src/lib/errors/InfoExtendableError.js';
6+
import WarnExtendableError from '../../src/lib/errors/WarnExtendableError.js';
7+
8+
describe('Errors', function () {
9+
describe('ExtendableError', function () {
10+
it('should have default level ERROR', function () {
11+
const error = new ExtendableError('test message');
12+
expect(error.message).to.be('test message');
13+
expect(error.name).to.be('ExtendableError');
14+
expect(error.level).to.be('ERROR');
15+
expect(error.stack).to.be.ok();
16+
});
17+
18+
it('should capture stack trace', function () {
19+
const error = new ExtendableError('stack test');
20+
expect(error.stack).to.contain('Errors.spec.ts');
21+
});
22+
});
23+
24+
describe('DebugExtendableError', function () {
25+
it('should have level DEBUG', function () {
26+
const error = new DebugExtendableError('debug message');
27+
expect(error.message).to.be('debug message');
28+
expect(error.name).to.be('DebugExtendableError');
29+
expect(error.level).to.be('DEBUG');
30+
});
31+
});
32+
33+
describe('InfoExtendableError', function () {
34+
it('should have level INFO', function () {
35+
const error = new InfoExtendableError('info message');
36+
expect(error.message).to.be('info message');
37+
expect(error.name).to.be('InfoExtendableError');
38+
expect(error.level).to.be('INFO');
39+
});
40+
});
41+
42+
describe('WarnExtendableError', function () {
43+
it('should have level WARN', function () {
44+
const error = new WarnExtendableError('warn message');
45+
expect(error.message).to.be('warn message');
46+
expect(error.name).to.be('WarnExtendableError');
47+
expect(error.level).to.be('WARN');
48+
});
49+
});
50+
});

tests/mocha/embyItems.spec.ts

Lines changed: 307 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,307 @@
1+
2+
/* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call, @typescript-eslint/strict-boolean-expressions, @typescript-eslint/no-unsafe-return */
3+
import assert from 'node:assert/strict';
4+
import { Sequelize } from 'sequelize';
5+
import itemsRoutes from '../../src/lib/embyEmulation/ServerAPI/routes/items/index.js';
6+
import { Series, seriesColumns } from '../../src/models/series.js';
7+
import { Episode, episodeColumns } from '../../src/models/episode.js';
8+
import { Movie, movieColumns } from '../../src/models/movie.js';
9+
import { TrackEpisode, trackEpisodesColumns } from '../../src/models/trackEpisode.js';
10+
import { TrackMovie, trackMovieColumns } from '../../src/models/trackMovie.js';
11+
import { File, fileColumns } from '../../src/models/file.js';
12+
import { Stream, streamColumns } from '../../src/models/stream.js';
13+
import { User, userColumns } from '../../src/models/user.js';
14+
import { formatUuid, formatId } from '../../src/lib/embyEmulation/helpers.js';
15+
16+
const makeServer = () => {
17+
const handlers = new Map();
18+
19+
const register = (method: string) => (route: string, handler: any) => {
20+
handlers.set(`${method} ${route}`, handler);
21+
};
22+
23+
return {
24+
handlers,
25+
get: register('GET'),
26+
post: register('POST'),
27+
put: register('PUT'),
28+
delete: register('DELETE')
29+
};
30+
};
31+
32+
const makeRes = () => ({
33+
statusCode: 200,
34+
body: null as any,
35+
status(code: number) {
36+
this.statusCode = code;
37+
return this;
38+
},
39+
send(payload: any) {
40+
this.body = payload;
41+
return this;
42+
}
43+
});
44+
45+
describe('Emby items routes', () => {
46+
let sequelize: Sequelize;
47+
let series1: Series;
48+
let ep1_1: Episode;
49+
let movie1: Movie;
50+
let file1: File;
51+
const userId = 1;
52+
const userIdUuid = formatUuid(userId);
53+
54+
const mockArtworkUtils = {
55+
moviePosterPath: () => '/tmp/poster.jpg',
56+
movieFanartPath: () => '/tmp/fanart.jpg',
57+
seriesPosterPath: () => '/tmp/series.jpg',
58+
episodeBannerPath: () => '/tmp/episode.jpg'
59+
};
60+
61+
const mockEmbyEmulation: any = {
62+
serverId: 'test-server-id',
63+
sessions: {},
64+
oblecto: {
65+
artworkUtils: mockArtworkUtils,
66+
config: {
67+
artwork: {
68+
poster: {},
69+
fanart: {},
70+
banner: {}
71+
}
72+
}
73+
}
74+
};
75+
76+
before(async () => {
77+
sequelize = new Sequelize({
78+
dialect: 'sqlite', storage: ':memory:', logging: false
79+
});
80+
81+
// Initialize models
82+
Series.init(seriesColumns, { sequelize, modelName: 'Series' });
83+
Episode.init(episodeColumns, { sequelize, modelName: 'Episode' });
84+
Movie.init(movieColumns, { sequelize, modelName: 'Movie' });
85+
TrackEpisode.init(trackEpisodesColumns, { sequelize, modelName: 'TrackEpisode' });
86+
TrackMovie.init(trackMovieColumns, { sequelize, modelName: 'TrackMovie' });
87+
File.init(fileColumns, { sequelize, modelName: 'File' });
88+
Stream.init(streamColumns, { sequelize, modelName: 'Stream' });
89+
User.init(userColumns, { sequelize, modelName: 'User' });
90+
91+
// Associations
92+
Episode.belongsTo(Series);
93+
Series.hasMany(Episode);
94+
TrackEpisode.belongsTo(Episode, { foreignKey: 'episodeId' });
95+
Episode.hasMany(TrackEpisode, { foreignKey: 'episodeId' });
96+
TrackMovie.belongsTo(Movie, { foreignKey: 'movieId' });
97+
Movie.hasMany(TrackMovie, { foreignKey: 'movieId' });
98+
99+
Episode.hasMany(File);
100+
File.belongsTo(Episode);
101+
Movie.hasMany(File);
102+
File.belongsTo(Movie);
103+
104+
File.hasMany(Stream);
105+
Stream.belongsTo(File);
106+
107+
await sequelize.sync({ force: true });
108+
109+
series1 = await Series.create({ seriesName: 'Test Series', firstAired: '2021-01-01' });
110+
111+
ep1_1 = await Episode.create({
112+
episodeName: 'S1E1',
113+
airedSeason: 1,
114+
airedEpisodeNumber: 1,
115+
SeriesId: series1.id
116+
});
117+
118+
movie1 = await Movie.create({
119+
movieName: 'Test Movie',
120+
releaseDate: '2020-01-01'
121+
});
122+
123+
file1 = await File.create({ path: '/tmp/movie.mkv', MovieId: movie1.id });
124+
});
125+
126+
describe('GET /items', () => {
127+
it('should return all items', async () => {
128+
const server = makeServer();
129+
itemsRoutes(server as any, mockEmbyEmulation);
130+
131+
const handler = server.handlers.get('GET /items');
132+
const req = { query: { IncludeItemTypes: 'Movie,Series,Episode' } };
133+
const res = makeRes();
134+
135+
await handler(req, res);
136+
137+
assert.equal(res.body.TotalRecordCount, 3); // 1 movie, 1 series, 1 episode
138+
});
139+
140+
it('should filter by SearchTerm', async () => {
141+
const server = makeServer();
142+
itemsRoutes(server as any, mockEmbyEmulation);
143+
144+
const handler = server.handlers.get('GET /items');
145+
const req = { query: { IncludeItemTypes: 'Movie', SearchTerm: 'Test Movie' } };
146+
const res = makeRes();
147+
148+
await handler(req, res);
149+
150+
assert.equal(res.body.TotalRecordCount, 1);
151+
assert.equal(res.body.Items[0].Name, 'Test Movie');
152+
});
153+
});
154+
155+
describe('GET /items/:mediaid', () => {
156+
it('should return movie details', async () => {
157+
const server = makeServer();
158+
itemsRoutes(server as any, mockEmbyEmulation);
159+
160+
const handler = server.handlers.get('GET /items/:mediaid');
161+
const req = { params: { mediaid: formatId(movie1.id, 'movie') } };
162+
const res = makeRes();
163+
164+
await handler(req, res);
165+
166+
assert.equal(res.body.Name, 'Test Movie');
167+
});
168+
169+
it('should return "shows" folder info', async () => {
170+
const server = makeServer();
171+
itemsRoutes(server as any, mockEmbyEmulation);
172+
173+
const handler = server.handlers.get('GET /items/:mediaid');
174+
const req = { params: { mediaid: 'shows' } };
175+
const res = makeRes();
176+
177+
await handler(req, res);
178+
179+
assert.equal(res.body.Name, 'Shows');
180+
});
181+
});
182+
183+
describe('GET /search/hints', () => {
184+
it('should return hints matching search term', async () => {
185+
const server = makeServer();
186+
itemsRoutes(server as any, mockEmbyEmulation);
187+
188+
const handler = server.handlers.get('GET /search/hints');
189+
const req = { query: { SearchTerm: 'Test' } }; // Matches 'Test Series' and 'Test Movie'
190+
const res = makeRes();
191+
192+
await handler(req, res);
193+
194+
assert.ok(res.body.TotalRecordCount >= 2);
195+
assert.ok(res.body.SearchHints.some((h: any) => h.Name === 'Test Movie'));
196+
});
197+
});
198+
199+
describe('POST /items/:mediaid/playbackinfo', () => {
200+
it('should return playback info for movie', async () => {
201+
const server = makeServer();
202+
itemsRoutes(server as any, mockEmbyEmulation);
203+
204+
const handler = server.handlers.get('POST /items/:mediaid/playbackinfo');
205+
const req = {
206+
params: { mediaid: formatId(movie1.id, 'movie') },
207+
headers: { emby: { Token: 'test-token' } },
208+
query: {}
209+
};
210+
const res = makeRes();
211+
212+
// Mock session for token
213+
mockEmbyEmulation.sessions['test-token'] = { Id: userIdUuid };
214+
215+
await handler(req, res);
216+
217+
assert.ok(res.body.MediaSources);
218+
assert.equal(res.body.MediaSources.length, 1);
219+
});
220+
});
221+
222+
describe('Additional stub routes', () => {
223+
it('should return empty lists or defaults for stub routes', async () => {
224+
const server = makeServer();
225+
itemsRoutes(server as any, mockEmbyEmulation);
226+
227+
const routes = [
228+
'GET /items/filters',
229+
'GET /items/filters2',
230+
'GET /items/:itemid/images',
231+
'GET /items/:itemid/instantmix',
232+
'GET /items/:itemid/externalidinfos',
233+
'GET /items/:itemid/criticreviews',
234+
'GET /items/:itemid/themesongs',
235+
'GET /items/:itemid/themevideos',
236+
'GET /items/counts',
237+
'GET /items/:itemid/remoteimages',
238+
'GET /items/:itemid/remoteimages/providers',
239+
'GET /items/suggestions',
240+
'GET /items/:itemid/intros',
241+
'GET /items/:itemid/localtrailers',
242+
'GET /items/:itemid/specialfeatures',
243+
'GET /items/root',
244+
'GET /movies/:itemid/similar',
245+
'GET /movies/recommendations',
246+
'GET /shows/:itemid/similar',
247+
'GET /shows/upcoming',
248+
'GET /trailers',
249+
'GET /trailers/:itemid/similar'
250+
];
251+
252+
for (const route of routes) {
253+
const [method, path] = route.split(' ');
254+
const handler = server.handlers.get(route);
255+
256+
if (!handler) {
257+
throw new Error(`Handler not found for ${route}`);
258+
}
259+
260+
const req = { params: { itemid: '1', mediaid: '1' } };
261+
const res = makeRes();
262+
263+
await handler(req, res);
264+
assert.ok(res.body, `Response body should exist for ${route}`);
265+
}
266+
});
267+
268+
it('should return 404 for download/file routes', async () => {
269+
const server = makeServer();
270+
itemsRoutes(server as any, mockEmbyEmulation);
271+
272+
const routes = [
273+
'GET /items/:itemid/download',
274+
'GET /items/:itemid/file',
275+
'GET /items/:itemid/remoteimages/download'
276+
];
277+
278+
for (const route of routes) {
279+
const handler = server.handlers.get(route);
280+
const req = { params: { itemid: '1' } };
281+
const res = makeRes();
282+
283+
await handler(req, res);
284+
assert.equal(res.statusCode, 404, `Should be 404 for ${route}`);
285+
}
286+
});
287+
288+
it('should return 204 for post actions', async () => {
289+
const server = makeServer();
290+
itemsRoutes(server as any, mockEmbyEmulation);
291+
292+
const routes = [
293+
'POST /items/remotesearch/apply/:itemid',
294+
'POST /items/:itemid/refresh'
295+
];
296+
297+
for (const route of routes) {
298+
const handler = server.handlers.get(route);
299+
const req = { params: { itemid: '1' } };
300+
const res = makeRes();
301+
302+
await handler(req, res);
303+
assert.equal(res.statusCode, 204, `Should be 204 for ${route}`);
304+
}
305+
});
306+
});
307+
});

0 commit comments

Comments
 (0)