Skip to content

Commit

Permalink
refactor: break up monolithic jobs into smaller pieces
Browse files Browse the repository at this point in the history
  • Loading branch information
treipatru committed Jul 15, 2024
1 parent 1f2cde9 commit 0d009b7
Show file tree
Hide file tree
Showing 11 changed files with 421 additions and 145 deletions.
35 changes: 35 additions & 0 deletions src/actions/movie/get-discovery-movies.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { getDiscoveryMovies } from '@/actions/movie/get-discovery-movies.js';
import { ENDPOINTS } from '@/test/msw/handlers.js';
import { server } from '@/test/msw/server.js';
import { http, HttpResponse } from 'msw';

describe('getDiscoveryMovies', () => {
server.listen();

it('returns a list of movies with the expected properties', async () => {
const movies = await getDiscoveryMovies();

const movie = movies[0];
expect(movie).toHaveProperty('id');
expect(movie).toHaveProperty('title');
expect(movie).toHaveProperty('overview');
expect(movie).toHaveProperty('release_date');
expect(movie).toHaveProperty('poster_path');
});

it('throws an error if any of the requests fail', async () => {
server.use(
http.get(
ENDPOINTS.movieDb.discover,
() => {
return HttpResponse.error();
},
{ once: true },
),
);

await expect(getDiscoveryMovies())
.rejects
.toThrow();
});
});
66 changes: 66 additions & 0 deletions src/actions/movie/get-discovery-movies.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { getDiscoverMovie } from '@/api/moviedb.js';
import { type Movie } from '@/types/moviedb.js';
import { appLogger } from '@/utils/logger.js';
import { getAdjacentDayInYear } from './get-adjacent-year-date.js';
import { getYearsRange } from './get-years-range.js';

export async function getDiscoveryMovies(): Promise<Movie[]> {
let allMovies: Movie[] = [];
const years = getYearsRange();

const promises = years
.map(async (releaseYear) => {
/**
* The Movie Database API does not have a method to request movies
* released on a particular day so we have to specify a range.
* If we're interested in movies released on a specific day, we need
* to specify the day before and after.
*/
const dayBefore = getAdjacentDayInYear({
date: new Date(),
direction: 'previous',
year: releaseYear,
});
const dayAfter = getAdjacentDayInYear({
date: new Date(),
direction: 'next',
year: releaseYear,
});

/**
* Get an array of movies released on today's month and day for the
* current release year.
*/
const discoverResponse = await getDiscoverMovie({
'primary_release_date.gte': dayBefore,
'primary_release_date.lte': dayAfter,
'sort_by': 'popularity.desc',
'with_runtime.gte': '80',
'without_genres': '99,10402', // Exclude and musicals
});

appLogger.info({
message: `Got ${discoverResponse.results.length} movies for ${releaseYear}.`,
service: 'tmdb',
});

return discoverResponse.results;
});

try {
const movies = await Promise.all(promises);

movies.forEach((yearMovies) => {
allMovies = [...allMovies, ...yearMovies];
});
} catch (error) {
appLogger.error({
error,
message: `Failed to get movies for one or more years`,
service: 'tmdb',
});
throw error;
}

return allMovies;
}
31 changes: 31 additions & 0 deletions src/actions/movie/get-movie-poster.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { getMoviePoster } from '@/actions/movie/get-movie-poster.js';
import { ENDPOINTS } from '@/test/msw/handlers.js';
import { server } from '@/test/msw/server.js';
import { http, HttpResponse } from 'msw';

describe('getMoviePoster', () => {
server.listen();

it('fetches a movie poster', async () => {
const url = 'https://image.test/image.jpg';
const poster = await getMoviePoster(url);

expect(poster).toBeInstanceOf(Response);
});

it('throws an error if the request fails', async () => {
server.use(
http.get(
ENDPOINTS.image,
() => {
return HttpResponse.error();
},
{ once: true },
),
);

await expect(getMoviePoster('https://image.test/image.jpg'))
.rejects
.toThrow();
});
});
22 changes: 22 additions & 0 deletions src/actions/movie/get-movie-poster.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { appLogger } from '@/utils/logger.js';

export async function getMoviePoster(url: string) {
try {
const posterFile = await fetch(url);

appLogger.info({
message: `Fetched movie poster ${url}.`,
service: 'tmdb',
});

return posterFile;
} catch (error) {
appLogger.error({
error,
message: `Failed to fetch movie poster ${url} .`,
service: 'tmdb',
});

throw error;
}
}
46 changes: 46 additions & 0 deletions src/actions/post/create-post-media.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { createPostMedia } from '@/actions/post/create-post-media.js';
import { generateMediaAttachment } from '@/test/fakers/mastodon.js';
import { ENDPOINTS } from '@/test/msw/handlers.js';
import { server } from '@/test/msw/server.js';
import { http, HttpResponse } from 'msw';

describe('createPostMedia', () => {
server.listen();

test('it should create post media', async () => {
const file = new File([''], 'image.png', { type: 'image/png' });
const media = generateMediaAttachment();

server.use(
http.post(
ENDPOINTS.mastodon.media,
() => {
return HttpResponse.json(media);
},
{ once: true },
),
);

const response = await createPostMedia({ file, description: media.description });

expect(response).toEqual(media);
});

test('it should throw an error if the request fails', async () => {
const file = new File([''], 'image.png', { type: 'image/png' });

server.use(
http.post(
ENDPOINTS.mastodon.media,
() => {
return HttpResponse.error();
},
{ once: true },
),
);

await expect(createPostMedia({ file }))
.rejects
.toThrow();
});
});
21 changes: 21 additions & 0 deletions src/actions/post/create-post-media.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { createMedia } from '@/api/mastodon.js';
import { CreateMediaAttachmentParams } from '@/types/mastodon.js';
import { appLogger } from '@/utils/logger.js';

export async function createPostMedia({
file,
description,
}: CreateMediaAttachmentParams) {
try {
const media = await createMedia({ file, description });
return media;
} catch (error) {
appLogger.error({
error,
message: 'Failed to create post media on Mastodon.',
service: 'mastodon',
});

throw error;
}
}
71 changes: 71 additions & 0 deletions src/actions/post/create-post.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { createPost } from '@/actions/post/create-post.js';
import { generateMediaAttachment, generateStatus } from '@/test/fakers/mastodon.js';
import { ENDPOINTS } from '@/test/msw/handlers.js';
import { server } from '@/test/msw/server.js';
import { http, HttpResponse } from 'msw';

describe('createPost', () => {
server.listen();

test('it should create post without attachment', async () => {
const newStatus = generateStatus();

server.use(
http.post(
ENDPOINTS.mastodon.statuses,
() => {
return HttpResponse.json(newStatus);
},
{ once: true },
),
);

const createdStatus = await createPost({ status: 'In the mood for love' });
expect(createdStatus).toEqual(newStatus);
expect(createdStatus.mediaAttachments).toHaveLength(0);
});

test('it should create post with attachment', async () => {
const newStatus = generateStatus();
const media = generateMediaAttachment();

server.use(
http.post(
ENDPOINTS.mastodon.statuses,
() => {
return HttpResponse.json({
...newStatus,
media_attachments: [media],
});
},
{ once: true },
),
);

const createdStatus = await createPost({
status: 'In the mood for love',
statusAttachment: media,
});

expect(createdStatus).toEqual({
...newStatus,
mediaAttachments: [media],
});
});

test('it should throw an error if the request fails', async () => {
server.use(
http.post(
ENDPOINTS.mastodon.statuses,
() => {
return HttpResponse.error();
},
{ once: true },
),
);

await expect(createPost({ status: 'In the mood for love' }))
.rejects
.toThrow();
});
});
34 changes: 34 additions & 0 deletions src/actions/post/create-post.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { createStatus } from '@/api/mastodon.js';
import { MediaAttachment } from '@/types/mastodon.js';
import { appLogger } from '@/utils/logger.js';

export async function createPost({
status,
statusAttachment,
}: {
status: string;
statusAttachment?: MediaAttachment;
}) {
try {
const newStatus = await createStatus({
status,
mediaIds: statusAttachment ? [statusAttachment.id] : undefined,
visibility: 'public',
});

appLogger.info({
message: `Posted movie status\n${status}`,
service: 'mastodon',
});

return newStatus;
} catch (error) {
appLogger.error({
error,
message: 'Failed to post movie status',
service: 'mastodon',
});

throw error;
}
}
44 changes: 44 additions & 0 deletions src/actions/post/get-last-post.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { getLastPost } from '@/actions/post/get-last-post.js';
import { generateStatus } from '@/test/fakers/mastodon.js';
import { ENDPOINTS } from '@/test/msw/handlers.js';
import { server } from '@/test/msw/server.js';
import { http, HttpResponse } from 'msw';

describe('getLastPost', () => {
server.listen();

test('it should return the last post', async () => {
const statuses = [
generateStatus(),
generateStatus(),
generateStatus(),
].sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());

server.use(
http.get(
ENDPOINTS.mastodon.homeTimeline,
() => {
return HttpResponse.json(statuses);
},
{ once: true },
),
);

const response = await getLastPost();
expect(response).toEqual(statuses[0]);
});

test('it should throw an error if the request fails', async () => {
server.use(
http.post(
ENDPOINTS.mastodon.homeTimeline,
() => {
return HttpResponse.error();
},
{ once: true },
),
);

await expect(getLastPost()).rejects.toThrow();
});
});
17 changes: 17 additions & 0 deletions src/actions/post/get-last-post.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { getTimeline } from '@/api/mastodon.js';
import { appLogger } from '@/utils/logger.js';

export async function getLastPost() {
try {
const posts = await getTimeline({ limit: 1 });
return posts[0];
} catch (error) {
appLogger.error({
error,
message: 'Failed to get last post from Mastodon.',
service: 'mastodon',
});

throw error;
}
}
Loading

0 comments on commit 0d009b7

Please sign in to comment.