Skip to content

Commit 186c6ed

Browse files
feat: add mock tweet generation and handlers for API interactions
1 parent fbd051f commit 186c6ed

7 files changed

Lines changed: 312 additions & 74 deletions

File tree

.gitignore

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,8 @@ logs
2828
.vscode/*
2929

3030
# ignore testing coverage folder
31-
/coverage
31+
/coverage
32+
33+
# ignore mock data folder
34+
/mocks/data/*
35+
!/mocks/data/.gitkeep

mocks/data/.gitkeep

Whitespace-only changes.

mocks/generators/genMockTweets.ts

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { faker } from '@faker-js/faker';
2+
import { mkdirSync, writeFileSync } from 'fs';
3+
import path from 'path';
4+
import { fileURLToPath } from 'url';
5+
import type { Tweet } from '../../types/tweets';
6+
7+
const __filename = fileURLToPath(import.meta.url);
8+
const __dirname = path.dirname(__filename);
9+
10+
function makeAuthor(index: number) {
11+
const username = faker.internet.userName().toLowerCase() + index;
12+
return {
13+
username,
14+
displayName: faker.person.fullName(),
15+
avatarUrl: `/avatars/${username}.png`,
16+
isFollowing: faker.datatype.boolean(),
17+
isFollower: faker.datatype.boolean(),
18+
};
19+
}
20+
21+
function randomEntities() {
22+
const mentionsCount = faker.number.int({ min: 0, max: 2 });
23+
const hashtagsCount = faker.number.int({ min: 0, max: 2 });
24+
const mentions = Array.from({ length: mentionsCount }, () => ({
25+
username: faker.internet.userName().toLowerCase(),
26+
startPosition: faker.number.int({ min: 0, max: 20 }),
27+
}));
28+
const hashtags = Array.from({ length: hashtagsCount }, () => ({
29+
hashtag: faker.hacker.noun().replace(/\s+/g, ''),
30+
startPosition: faker.number.int({ min: 0, max: 20 }),
31+
}));
32+
return { mentions, hashtags };
33+
}
34+
35+
function randomMedia() {
36+
const shouldHaveMedia = faker.datatype.boolean(0.3);
37+
if (!shouldHaveMedia) return [] as Tweet['media'];
38+
const types = ['IMAGE', 'GIF', 'VIDEO'] as const;
39+
const type = faker.helpers.arrayElement(types);
40+
return [
41+
{
42+
type,
43+
url: faker.internet.url(),
44+
altText: faker.lorem.sentence(),
45+
width: faker.number.int({ min: 320, max: 1920 }),
46+
height: faker.number.int({ min: 240, max: 1080 }),
47+
},
48+
] as Tweet['media'];
49+
}
50+
51+
function makeBaseTweet(id: string, content?: string): Tweet {
52+
return {
53+
id,
54+
content: content ?? faker.lorem.sentences({ min: 1, max: 3 }),
55+
createdAt: new Date().toISOString(),
56+
author: makeAuthor(faker.number.int({ min: 1, max: 999 })),
57+
replyCount: 0,
58+
retweetCount: faker.number.int({ min: 0, max: 100 }),
59+
likeCount: faker.number.int({ min: 0, max: 500 }),
60+
isLiked: faker.datatype.boolean(),
61+
isRetweeted: faker.datatype.boolean(),
62+
entities: randomEntities(),
63+
media: randomMedia(),
64+
};
65+
}
66+
67+
function makeData() {
68+
const t1: Tweet = makeBaseTweet('tw-' + faker.string.nanoid(6));
69+
70+
const t2: Tweet = makeBaseTweet('tw-' + faker.string.nanoid(6));
71+
const t3: Tweet = {
72+
...makeBaseTweet('tw-' + faker.string.nanoid(6), faker.lorem.sentences({ min: 1, max: 2 })),
73+
isReplyToTweetId: t1.id,
74+
};
75+
t1.replyCount += 1;
76+
77+
const quotedLight: Tweet = {
78+
...t1,
79+
quotedTweet: undefined,
80+
quotedTweetId: undefined,
81+
isReplyToTweetId: undefined,
82+
};
83+
const t4: Tweet = {
84+
...makeBaseTweet('tw-' + faker.string.nanoid(6), faker.lorem.sentences({ min: 1, max: 2 })),
85+
quotedTweetId: t1.id,
86+
quotedTweet: quotedLight,
87+
};
88+
89+
const tweets: Tweet[] = [t1, t2, t3, t4];
90+
return tweets;
91+
}
92+
93+
function main() {
94+
const data = makeData();
95+
const outDir = path.resolve(__dirname, '../data');
96+
const outFile = path.join(outDir, 'tweet.json');
97+
mkdirSync(outDir, { recursive: true });
98+
writeFileSync(outFile, JSON.stringify(data, null, 2), 'utf-8');
99+
// eslint-disable-next-line no-console
100+
console.log(`Wrote ${data.length} tweets to ${path.relative(process.cwd(), outFile)}`);
101+
}
102+
103+
main();

mocks/handler.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { handlers as userHandlers } from './handlers/user';
2+
import { handlers as tweetHandlers } from './handlers/tweet';
23
// Import more handlers as needed
34

45
export const handlers = [
56
...userHandlers,
7+
...tweetHandlers,
68
// Add More handlers here
79
];

mocks/handlers/tweet.ts

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import { http, HttpResponse } from 'msw';
2+
import type { Tweet } from '../../types/tweets';
3+
import tweetsData from '../data/tweet.json' assert { type: 'json' };
4+
5+
const API_URL = '';
6+
const initialTweets = (tweetsData as unknown as Tweet[]) || [];
7+
const tweets = new Map<string, Tweet>(initialTweets.map((t) => [t.id, { ...t }]));
8+
9+
const getTweet = (id: string) => tweets.get(id);
10+
11+
const likeTweet = (t: Tweet) => {
12+
if (!t.isLiked) {
13+
t.isLiked = true;
14+
t.likeCount += 1;
15+
}
16+
};
17+
18+
const unlikeTweet = (t: Tweet) => {
19+
if (t.isLiked) {
20+
t.isLiked = false;
21+
t.likeCount = Math.max(0, t.likeCount - 1);
22+
}
23+
};
24+
25+
const retweet = (t: Tweet) => {
26+
if (!t.isRetweeted) {
27+
t.isRetweeted = true;
28+
t.retweetCount += 1;
29+
}
30+
};
31+
32+
const unretweet = (t: Tweet) => {
33+
if (t.isRetweeted) {
34+
t.isRetweeted = false;
35+
t.retweetCount = Math.max(0, t.retweetCount - 1);
36+
}
37+
};
38+
39+
export const handlers = [
40+
// GET /tweets/:id — fetch a tweet by id
41+
http.get(`${API_URL}/tweets/:id`, ({ params }) => {
42+
const { id } = params as { id: string };
43+
const tweet = getTweet(id);
44+
if (!tweet) {
45+
return HttpResponse.json({ message: `Tweet "${id}" not found` }, { status: 404 });
46+
}
47+
return HttpResponse.json(
48+
{
49+
success: true,
50+
message: 'Tweet fetched successfully.',
51+
data: tweet,
52+
},
53+
{ status: 200 },
54+
);
55+
}),
56+
57+
// POST /tweets/:id/like — like a tweet
58+
http.post(`${API_URL}/tweets/:id/like`, ({ params }) => {
59+
const { id } = params as { id: string };
60+
const tweet = getTweet(id);
61+
if (!tweet) {
62+
return HttpResponse.json({ message: `Tweet "${id}" not found` }, { status: 404 });
63+
}
64+
if (tweet.isLiked) {
65+
return HttpResponse.json({ message: 'Tweet already liked.' }, { status: 409 });
66+
}
67+
likeTweet(tweet);
68+
return HttpResponse.json(
69+
{ success: true, message: 'Tweet liked successfully.' },
70+
{ status: 200 },
71+
);
72+
}),
73+
74+
// DELETE /tweets/:id/like — unlike a tweet
75+
http.delete(`${API_URL}/tweets/:id/like`, ({ params }) => {
76+
const { id } = params as { id: string };
77+
const tweet = getTweet(id);
78+
if (!tweet) {
79+
return HttpResponse.json({ message: `Tweet "${id}" not found` }, { status: 404 });
80+
}
81+
if (!tweet.isLiked) {
82+
return HttpResponse.json({ message: 'Tweet is not liked.' }, { status: 400 });
83+
}
84+
unlikeTweet(tweet);
85+
return HttpResponse.json(
86+
{ success: true, message: 'Tweet unliked successfully.' },
87+
{ status: 200 },
88+
);
89+
}),
90+
91+
// POST /tweets/:id/retweet — retweet a tweet
92+
http.post(`${API_URL}/tweets/:id/retweet`, ({ params }) => {
93+
const { id } = params as { id: string };
94+
const tweet = getTweet(id);
95+
if (!tweet) {
96+
return HttpResponse.json({ message: `Tweet "${id}" not found` }, { status: 404 });
97+
}
98+
if (tweet.isRetweeted) {
99+
return HttpResponse.json({ message: 'Tweet already retweeted.' }, { status: 409 });
100+
}
101+
retweet(tweet);
102+
return HttpResponse.json(
103+
{ success: true, message: 'Tweet retweeted successfully.' },
104+
{ status: 200 },
105+
);
106+
}),
107+
108+
// DELETE /tweets/:id/retweet — undo retweet
109+
http.delete(`${API_URL}/tweets/:id/retweet`, ({ params }) => {
110+
const { id } = params as { id: string };
111+
const tweet = getTweet(id);
112+
if (!tweet) {
113+
return HttpResponse.json({ message: `Tweet "${id}" not found` }, { status: 404 });
114+
}
115+
if (!tweet.isRetweeted) {
116+
return HttpResponse.json({ message: 'Tweet is not retweeted.' }, { status: 400 });
117+
}
118+
unretweet(tweet);
119+
return HttpResponse.json(
120+
{ success: true, message: 'Retweet undone successfully.' },
121+
{ status: 200 },
122+
);
123+
}),
124+
];

0 commit comments

Comments
 (0)