Skip to content

Commit ef40cd1

Browse files
committed
fix tests
1 parent 841b9d0 commit ef40cd1

4 files changed

Lines changed: 119 additions & 91 deletions

File tree

frontend/__tests__/Navbar/Navbar.test.tsx

Lines changed: 51 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,13 @@ vi.mock("@components/UseAuth", () => ({
1818
default: vi.fn(),
1919
}));
2020

21+
vi.mock("next/image", () => ({
22+
default: (props: any) => {
23+
// eslint-disable-next-line jsx-a11y/alt-text, @next/next/no-img-element
24+
return <img {...props} />;
25+
},
26+
}));
27+
2128
vi.mock(
2229
"@services/auth.service",
2330
async (
@@ -30,6 +37,14 @@ vi.mock(
3037
default: {
3138
...actual.default,
3239
logout: vi.fn(),
40+
getUser: vi.fn().mockImplementation(() => {
41+
return {
42+
id: 1,
43+
username: "test",
44+
refreshToken: "test",
45+
profileImage: "",
46+
};
47+
}),
3348
AuthStatus: {
3449
Unauthorized: "Unauthorized",
3550
Authorized: "Authorized",
@@ -152,28 +167,44 @@ describe("GlobalNavbar", () => {
152167
expect(mockPush).toHaveBeenCalledWith("/");
153168
});
154169

155-
it("renders default avatar when authorized and no profileImage", () => {
156-
(useAuth as MockedFunction<typeof useAuth>).mockReturnValue({
157-
status: AuthStatus.Authorized,
158-
userId: 1,
159-
profileImage: "",
160-
});
161-
render(<GlobalNavbar />);
162-
const avatar = screen.getByAltText("Profile") as HTMLImageElement;
163-
expect(avatar).toBeInTheDocument();
164-
expect(avatar.src).toContain("/img_avatar.png");
170+
it("renders default avatar when authorized and no profileImage", () => {
171+
(useAuth as MockedFunction<typeof useAuth>).mockReturnValue(
172+
AuthStatus.Authorized,
173+
);
174+
175+
const mock = vi.fn().mockImplementation(authService.getUser);
176+
mock.mockImplementationOnce(() => {
177+
return {
178+
id: 1,
179+
username: "test",
180+
refreshToken: "test",
181+
profileImage: "",
182+
};
165183
});
166184

167-
it("renders user avatar when authorized and profileImage exists", () => {
168-
(useAuth as MockedFunction<typeof useAuth>).mockReturnValue({
169-
status: AuthStatus.Authorized,
170-
userId: 1,
171-
profileImage: "avatars/1.png",
172-
});
173-
render(<GlobalNavbar />);
174-
const avatar = screen.getByAltText("Profile") as HTMLImageElement;
175-
expect(avatar).toBeInTheDocument();
176-
expect(avatar.src).toContain("/api/user/avatar?userId=1");
185+
render(<GlobalNavbar />);
186+
const avatar = screen.getByAltText("Profile") as HTMLImageElement;
187+
expect(avatar).toBeInTheDocument();
188+
expect(avatar.src).toContain("/img_avatar.png");
189+
});
190+
191+
it("renders user avatar when authorized and profileImage exists", () => {
192+
(useAuth as MockedFunction<typeof useAuth>).mockReturnValue(
193+
AuthStatus.Authorized,
194+
);
195+
196+
(authService.getUser as ReturnType<typeof vi.fn>).mockReturnValue({
197+
id: 1,
198+
username: "test",
199+
refreshToken: "test",
200+
profileImage: "avatars/1.png",
177201
});
178202

203+
// In your test setup file or at the top of the test file
204+
205+
render(<GlobalNavbar />);
206+
const avatar = screen.getByAltText("Profile") as HTMLImageElement;
207+
expect(avatar).toBeInTheDocument();
208+
expect(avatar.src).toContain("/api/user/avatar?userId=1");
209+
});
179210
});

frontend/components/Navbar/Navbar.tsx

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,11 @@ import navbarView from "styles/navbar.module.scss";
1212

1313
const GlobalNavbar: React.FC = () => {
1414
const userAuth = useAuth();
15+
const user = authService.getUser();
1516

1617
const router = useRouter();
1718
function authButton() {
18-
if (userAuth.status === AuthStatus.Unauthorized || userAuth.status === undefined) {
19+
if (userAuth == AuthStatus.Unauthorized || userAuth === undefined) {
1920
return (
2021
<ButtonGroup>
2122
<Button
@@ -49,6 +50,12 @@ const GlobalNavbar: React.FC = () => {
4950
router.push("/account/login");
5051
};
5152

53+
console.log(user?.profileImage && user?.profileImage.trim() !== "");
54+
console.log(
55+
user?.profileImage && user?.profileImage.trim() !== ""
56+
? `/api/user/avatar?userId=${user.id}`
57+
: "/img_avatar.png",
58+
);
5259
return (
5360
<Navbar
5461
expand="md"
@@ -69,33 +76,33 @@ const GlobalNavbar: React.FC = () => {
6976
/>
7077
FindFirst
7178
</Navbar.Brand>
72-
{userAuth.status === AuthStatus.Authorized ? <Searchbar /> : null}
79+
{userAuth === AuthStatus.Authorized ? <Searchbar /> : null}
7380
<div className={`btn-group ${navbarView.navBtns}`}>
74-
{userAuth.status === AuthStatus.Authorized ? (
81+
{userAuth === AuthStatus.Authorized ? (
7582
<ImportModal
7683
file={undefined}
7784
show={false}
7885
data-testid="import-modal"
7986
/>
8087
) : null}
81-
{userAuth.status === AuthStatus.Authorized ? (
88+
{userAuth === AuthStatus.Authorized ? (
8289
<Export data-testid="export-component" />
8390
) : null}
8491
<LightDarkToggle />
8592
{authButton()}
86-
{userAuth.status === AuthStatus.Authorized && (
93+
{userAuth === AuthStatus.Authorized && (
8794
<Image
88-
src={
89-
userAuth.profileImage && userAuth.profileImage.trim() !== ""
90-
? `/api/user/avatar?userId=${userAuth.userId}`
91-
: "/img_avatar.png"
92-
}
93-
alt="Profile"
94-
width={36}
95-
height={36}
96-
className="rounded-circle ms-6"
95+
src={
96+
user?.profileImage && user?.profileImage.trim() !== ""
97+
? `/api/user/avatar?userId=${user.id}`
98+
: "/img_avatar.png"
99+
}
100+
alt="Profile"
101+
width={36}
102+
height={36}
103+
className="rounded-circle ms-6"
97104
/>
98-
)}
105+
)}
99106
</div>
100107
</Container>
101108
</Navbar>

frontend/components/UseAuth.tsx

Lines changed: 14 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,21 @@
1-
"use client";
1+
import authService, { AuthObserver, AuthStatus } from "@services/auth.service";
22
import { useEffect, useState } from "react";
3-
import authService, { AuthStatus } from "@services/auth.service";
43

5-
interface UserAuth {
6-
status: AuthStatus;
7-
userId?: number | null;
8-
profileImage?: string | null;
9-
}
10-
11-
export default function useAuth(): UserAuth {
12-
const [auth, setAuth] = useState<UserAuth>({
13-
status: AuthStatus.Unauthorized,
14-
userId: null,
15-
profileImage: null,
16-
});
17-
18-
useEffect(() => {
19-
async function checkAuth() {
20-
const status = authService.getAuthorized();
21-
let userId: number | null = null;
22-
let profileImage: string | null = null;
4+
export default function UseAuth() {
5+
const [authorized, setAuthorized] = useState<AuthStatus>();
236

24-
if (status === AuthStatus.Authorized) {
25-
const user = authService.getUser(); // your backend returns { id, profileImage }
26-
userId = user?.id || null;
27-
profileImage = user?.profileImage || null;
28-
}
7+
const onAuthUpdated: AuthObserver = (authState: AuthStatus) => {
8+
setAuthorized(authState);
9+
};
2910

30-
setAuth({ status, userId, profileImage });
31-
}
11+
useEffect(() => {
12+
authService.attach(onAuthUpdated);
13+
return () => authService.detach(onAuthUpdated);
14+
}, []);
3215

33-
checkAuth();
34-
}, []);
16+
useEffect(() => {
17+
setAuthorized(authService.getAuthorized());
18+
}, []);
3519

36-
return auth;
20+
return authorized;
3721
}

frontend/pages/api/user/avatar.ts

Lines changed: 32 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -3,34 +3,40 @@ import axios from "axios";
33
import fs from "fs";
44
import path from "path";
55

6-
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
7-
const { userId } = req.query;
6+
export default async function handler(
7+
req: NextApiRequest,
8+
res: NextApiResponse,
9+
) {
10+
const { userId } = req.query;
811

9-
if (!userId) {
10-
return res.status(400).json({ error: "Missing userId" });
11-
}
12-
13-
try {
14-
const backendUrl = `${process.env.NEXT_PUBLIC_SERVER_URL}/user/profile-picture?userId=${userId}`;
15-
16-
const response = await axios.get(backendUrl, {
17-
responseType: "arraybuffer",
18-
});
12+
if (!userId) {
13+
return res.status(400).json({ error: "Missing userId" });
14+
}
1915

20-
res.setHeader("Content-Type", response.headers["content-type"] || "image/png");
21-
res.setHeader("Cache-Control", "public, max-age=3600");
22-
res.end(Buffer.from(response.data), "binary");
16+
try {
17+
const backendUrl = `${process.env.NEXT_PUBLIC_SERVER_URL}/user/profile-picture?userId=${userId}`;
2318

24-
} catch (err: any) {
25-
if (err.response?.status === 404) {
26-
// fallback to default avatar
27-
const fallbackPath = path.join(process.cwd(), "public", "img_avatar.png");
28-
const imageBuffer = fs.readFileSync(fallbackPath);
29-
res.setHeader("Content-Type", "image/png");
30-
res.setHeader("Cache-Control", "public, max-age=3600");
31-
res.end(imageBuffer);
32-
return;
33-
}
34-
res.status(err.response?.status || 500).json({ error: "Unable to fetch avatar" });
19+
const response = await axios.get(backendUrl, {
20+
responseType: "arraybuffer",
21+
});
22+
res.setHeader(
23+
"Content-Type",
24+
response.headers["content-type"] || "image/png",
25+
);
26+
res.setHeader("Cache-Control", "public, max-age=3600");
27+
res.end(Buffer.from(response.data), "binary");
28+
} catch (err: any) {
29+
if (err.response?.status === 404) {
30+
// fallback to default avatar
31+
const fallbackPath = path.join(process.cwd(), "public", "img_avatar.png");
32+
const imageBuffer = fs.readFileSync(fallbackPath);
33+
res.setHeader("Content-Type", "image/png");
34+
res.setHeader("Cache-Control", "public, max-age=3600");
35+
res.end(imageBuffer);
36+
return;
3537
}
38+
res
39+
.status(err.response?.status || 500)
40+
.json({ error: "Unable to fetch avatar" });
41+
}
3642
}

0 commit comments

Comments
 (0)