Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions server/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export const users: DbUser[] = [
handle: "darklord",
avatar: "/cdn/avatars/darklord.jpeg",
info: "I am the dark lord, the root of all evil. 'Tis I who brought the world to its knees. In blood I was born, and in blood I shall have my vengeance.",
numShoutsPastDay: 3,
blockedUserIds: ["user-2"],
followsUserIds: ["user-3"],
},
Expand All @@ -17,6 +18,7 @@ export const users: DbUser[] = [
type: "user",
attributes: {
handle: "prettypinkpony",
numShoutsPastDay: 4,
avatar: "/cdn/avatars/prettypinkpony.jpeg",
info: "I like colors. I'm a colorful person (although I'm pretty white *giggles*). I'd like to make this world a better place. And sometimes I feel like the only one who can...",
blockedUserIds: ["user-1"],
Expand All @@ -29,6 +31,7 @@ export const users: DbUser[] = [
attributes: {
handle: "fcku",
avatar: "/cdn/avatars/fcku.jpeg",
numShoutsPastDay: 2,
blockedUserIds: [],
followsUserIds: ["user-1", "user-2"],
},
Expand Down
6 changes: 6 additions & 0 deletions server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,10 +237,16 @@ fastify.post(
reply
) {
await waitRandomTime();

if (req.body.message === "error") {
return reply.status(400).send({ error: "unkown error" });
}

const user = getUserFromCookie(req.cookies);
if (!user) {
return reply.status(401).send({ error: true });
}
user.attributes.numShoutsPastDay += 1;
const shout: DbShout = {
id: `shout-${shouts.length + 1}`,
type: "shout",
Expand Down
1 change: 1 addition & 0 deletions server/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export interface DbUser {
handle: string;
avatar: string;
info?: string;
numShoutsPastDay: number;
blockedUserIds: UserId[];
followsUserIds: UserId[];
};
Expand Down
76 changes: 76 additions & 0 deletions src/application/get-user-profile/get-user-profile.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { describe, expect, it, vitest } from "vitest";

import { getUserProfile } from "./get-user-profile";

const handle = "user-handle";

const mockGetUser = vitest.fn().mockResolvedValue({
id: "user-id",
handle: "user-handle",
avatar: "user-avatar",
info: "user-info",
followerIds: [],
});

const mockGetUserShouts = vitest.fn().mockResolvedValue({
shouts: [
{
id: "shout-id",
createdAt: 1234567890,
authorId: "user-id",
text: "shout-text",
likes: 0,
reshouts: 0,
imageId: "image-id",
replies: [],
},
],
images: [
{
id: "image-id",
url: "image-url",
},
],
});

const mockDependencies = {
getUser: mockGetUser,
getUserShouts: mockGetUserShouts,
};

describe("getUserProfile", () => {
it("should create a new shout with an image and reply to it", async () => {
const result = await getUserProfile({ handle }, mockDependencies);

expect(mockGetUser).toHaveBeenCalledWith(handle);
expect(mockGetUserShouts).toHaveBeenCalledWith(handle);

expect(result).toEqual({
user: {
id: "user-id",
handle: "user-handle",
avatar: "user-avatar",
info: "user-info",
followerIds: [],
},
shouts: [
{
id: "shout-id",
createdAt: 1234567890,
authorId: "user-id",
text: "shout-text",
likes: 0,
reshouts: 0,
imageId: "image-id",
replies: [],
},
],
images: [
{
id: "image-id",
url: "image-url",
},
],
});
});
});
30 changes: 30 additions & 0 deletions src/application/get-user-profile/get-user-profile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { useCallback } from "react";

import UserService from "@/infrastructure/user";

interface GetUserProfileInput {
handle: string;
}

const dependencies = {
getUser: UserService.getUser,
getUserShouts: UserService.getUserShouts,
};

export async function getUserProfile(
{ handle }: GetUserProfileInput,
{ getUser, getUserShouts }: typeof dependencies
) {
const [user, { shouts, images }] = await Promise.all([
getUser(handle),
getUserShouts(handle),
]);
return { user, shouts, images };
}

export function useGetUserProfile() {
return useCallback(
(input: GetUserProfileInput) => getUserProfile(input, dependencies),
[]
);
}
1 change: 1 addition & 0 deletions src/application/get-user-profile/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { useGetUserProfile } from "./get-user-profile";
1 change: 1 addition & 0 deletions src/application/reply-to-shout/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { useReplyToShout } from "./reply-to-shout";
121 changes: 121 additions & 0 deletions src/application/reply-to-shout/reply-to-shout.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { beforeEach, describe, expect, it, vitest } from "vitest";

import { createMockFile } from "@/test/create-mock-file";

import { ErrorMessages, replyToShout } from "./reply-to-shout";

const recipientHandle = "recipient-handle";
const shoutId = "shout-id";
const message = "Hello, world!";
const files = [createMockFile("image.png")];

const imageId = "image-id";
const newShoutId = "new-shout-id";

const mockMe = {
id: "user-1",
handle: "me",
avatar: "user-1.png",
numShoutsPastDay: 0,
blockedUserIds: [],
followerIds: [],
};

const mockRecipient = {
id: "user-2",
handle: recipientHandle,
avatar: "user-2.png",
numShoutsPastDay: 0,
blockedUserIds: [],
followerIds: [],
};

const mockGetMe = vitest.fn().mockResolvedValue(mockMe);
const mockGetUser = vitest.fn().mockResolvedValue(mockRecipient);
const mockSaveImage = vitest.fn().mockResolvedValue({ id: imageId });
const mockCreateShout = vitest.fn().mockResolvedValue({ id: newShoutId });
const mockCreateReply = vitest.fn();

const mockDependencies = {
getMe: mockGetMe,
getUser: mockGetUser,
saveImage: mockSaveImage,
createShout: mockCreateShout,
createReply: mockCreateReply,
};

describe("replyToShout", () => {
beforeEach(() => {
Object.values(mockDependencies).forEach((mock) => mock.mockClear());
});

it("should return an error if the user has made too many shouts", async () => {
mockGetMe.mockResolvedValueOnce({ ...mockMe, numShoutsPastDay: 5 });

const result = await replyToShout(
{ recipientHandle, shoutId, message, files },
mockDependencies
);

expect(result).toEqual({ error: ErrorMessages.TooManyShouts });
});

it("should return an error if the recipient does not exist", async () => {
mockGetUser.mockResolvedValueOnce(undefined);

const result = await replyToShout(
{ recipientHandle, shoutId, message, files },
mockDependencies
);

expect(result).toEqual({ error: ErrorMessages.RecipientNotFound });
});

it("should return an error if the recipient has blocked the author", async () => {
mockGetUser.mockResolvedValueOnce({
...mockRecipient,
blockedUserIds: [mockMe.id],
});

const result = await replyToShout(
{ recipientHandle, shoutId, message, files },
mockDependencies
);

expect(result).toEqual({ error: ErrorMessages.AuthorBlockedByRecipient });
});

it("should create a new shout with an image and reply to it", async () => {
await replyToShout(
{ recipientHandle, shoutId, message, files },
mockDependencies
);

expect(mockSaveImage).toHaveBeenCalledWith(files[0]);
expect(mockCreateShout).toHaveBeenCalledWith({
message,
imageId,
});
expect(mockCreateReply).toHaveBeenCalledWith({
shoutId,
replyId: newShoutId,
});
});

it("should create a new shout without an image and reply to it", async () => {
await replyToShout(
{ recipientHandle, shoutId, message, files: [] },
mockDependencies
);

expect(mockSaveImage).not.toHaveBeenCalled();
expect(mockCreateShout).toHaveBeenCalledWith({
message,
imageId: undefined,
});
expect(mockCreateReply).toHaveBeenCalledWith({
shoutId,
replyId: newShoutId,
});
});
});
75 changes: 75 additions & 0 deletions src/application/reply-to-shout/reply-to-shout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { useCallback } from "react";

import MediaService from "@/infrastructure/media";
import ShoutService from "@/infrastructure/shout";
import UserService from "@/infrastructure/user";

interface ReplyToShoutInput {
recipientHandle: string;
shoutId: string;
message: string;
files?: File[] | null;
}

export const ErrorMessages = {
TooManyShouts:
"You have reached the maximum number of shouts per day. Please try again tomorrow.",
RecipientNotFound: "The user you want to reply to does not exist.",
AuthorBlockedByRecipient:
"You can't reply to this user. They have blocked you.",
UnknownError: "An unknown error occurred. Please try again later.",
} as const;

const dependencies = {
getMe: UserService.getMe,
getUser: UserService.getUser,
saveImage: MediaService.saveImage,
createShout: ShoutService.createShout,
createReply: ShoutService.createReply,
};

export async function replyToShout(
{ recipientHandle, shoutId, message, files }: ReplyToShoutInput,
{ getMe, getUser, saveImage, createReply, createShout }: typeof dependencies
) {
const me = await getMe();
if (me.numShoutsPastDay >= 5) {
return { error: ErrorMessages.TooManyShouts };
}

const recipient = await getUser(recipientHandle);
if (!recipient) {
return { error: ErrorMessages.RecipientNotFound };
}
if (recipient.blockedUserIds.includes(me.id)) {
return { error: ErrorMessages.AuthorBlockedByRecipient };
}

try {
let image;
if (files?.length) {
image = await saveImage(files[0]);
}

const newShout = await createShout({
message,
imageId: image?.id,
});

await createReply({
shoutId,
replyId: newShout.id,
});

return { error: undefined };
} catch {
return { error: ErrorMessages.UnknownError };
}
}

export function useReplyToShout() {
return useCallback(
(input: ReplyToShoutInput) => replyToShout(input, dependencies),
[]
);
}
Loading