Skip to content

Commit 50cee5c

Browse files
authored
Merge pull request #88 from BUMETCS673/test/d4-unittest
#88
2 parents 45440d8 + 48443d3 commit 50cee5c

File tree

4 files changed

+300
-2
lines changed

4 files changed

+300
-2
lines changed
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
package com.bu.getactivecore.service.users;
2+
3+
import static org.junit.jupiter.api.Assertions.*;
4+
import static org.mockito.Mockito.*;
5+
6+
import java.time.LocalDateTime;
7+
import java.util.Optional;
8+
9+
import org.junit.jupiter.api.BeforeEach;
10+
import org.junit.jupiter.api.Test;
11+
import org.junit.jupiter.api.extension.ExtendWith;
12+
import org.mockito.Mock;
13+
import org.mockito.junit.jupiter.MockitoExtension;
14+
import org.springframework.security.authentication.AuthenticationManager;
15+
import org.springframework.security.core.Authentication;
16+
17+
import com.bu.getactivecore.model.users.UserPrincipal;
18+
import com.bu.getactivecore.model.users.Users;
19+
import com.bu.getactivecore.repository.UserRepository;
20+
import com.bu.getactivecore.service.jwt.api.JwtApi;
21+
import com.bu.getactivecore.service.users.api.UserInfoApi;
22+
import com.bu.getactivecore.service.users.entity.UpdateAvatarRequestDto;
23+
import com.bu.getactivecore.service.users.entity.UpdateAvatarResponseDto;
24+
import com.bu.getactivecore.shared.ApiErrorPayload;
25+
import com.bu.getactivecore.shared.ErrorCode;
26+
import com.bu.getactivecore.shared.exception.ApiException;
27+
import com.bu.getactivecore.shared.validation.AccountStateChecker;
28+
29+
@ExtendWith(MockitoExtension.class)
30+
class UsersServiceTest {
31+
32+
@Mock
33+
private AuthenticationManager authManager;
34+
35+
@Mock
36+
private JwtApi jwtApi;
37+
38+
@Mock
39+
private AccountStateChecker accountStateChecker;
40+
41+
@Mock
42+
private UserRepository userRepository;
43+
44+
private UsersService usersService;
45+
46+
@BeforeEach
47+
void setUp() {
48+
usersService = new UsersService(authManager, jwtApi, accountStateChecker, userRepository);
49+
}
50+
51+
@Test
52+
void updateAvatar_Success() {
53+
// Arrange
54+
String username = "testuser";
55+
String avatarData = "data:image/jpeg;base64," + "a".repeat(1000); // Small base64 string
56+
UpdateAvatarRequestDto requestDto = new UpdateAvatarRequestDto(avatarData);
57+
58+
Users user = new Users();
59+
user.setUsername(username);
60+
user.setAvatar(null);
61+
user.setAvatarUpdatedAt(null);
62+
63+
when(userRepository.findByUsername(username)).thenReturn(Optional.of(user));
64+
when(userRepository.save(any(Users.class))).thenAnswer(i -> i.getArguments()[0]);
65+
66+
// Act
67+
UpdateAvatarResponseDto response = usersService.updateAvatar(username, requestDto);
68+
69+
// Assert
70+
assertNotNull(response);
71+
assertEquals(avatarData, response.getAvatar());
72+
assertNotNull(response.getAvatarUpdatedAt());
73+
verify(userRepository).findByUsername(username);
74+
verify(userRepository).save(any(Users.class));
75+
}
76+
77+
@Test
78+
void updateAvatar_ExceedsSizeLimit() {
79+
// Arrange
80+
String username = "testuser";
81+
// Create a base64 string that exceeds 3MB when decoded
82+
String largeBase64 = "data:image/jpeg;base64," + "a".repeat(10 * 1024 * 1024);
83+
UpdateAvatarRequestDto requestDto = new UpdateAvatarRequestDto(largeBase64);
84+
85+
// Act & Assert
86+
ApiException exception = assertThrows(ApiException.class, () -> {
87+
usersService.updateAvatar(username, requestDto);
88+
});
89+
90+
ApiErrorPayload error = exception.getError();
91+
assertEquals(ErrorCode.AVATAR_SIZE_EXCEEDS_LIMIT, error.getErrorCode());
92+
assertEquals("Avatar size exceeds 3MB limit", error.getMessage());
93+
verify(userRepository, never()).findByUsername(anyString());
94+
verify(userRepository, never()).save(any(Users.class));
95+
}
96+
97+
@Test
98+
void updateAvatar_UserNotFound() {
99+
// Arrange
100+
String username = "nonexistentuser";
101+
String avatarData = "data:image/jpeg;base64," + "a".repeat(1000);
102+
UpdateAvatarRequestDto requestDto = new UpdateAvatarRequestDto(avatarData);
103+
104+
when(userRepository.findByUsername(username)).thenReturn(Optional.empty());
105+
106+
// Act & Assert
107+
ApiException exception = assertThrows(ApiException.class, () -> {
108+
usersService.updateAvatar(username, requestDto);
109+
});
110+
111+
ApiErrorPayload error = exception.getError();
112+
assertEquals(ErrorCode.WRONG_CREDENTIALS, error.getErrorCode());
113+
assertEquals("User not found", error.getMessage());
114+
verify(userRepository).findByUsername(username);
115+
verify(userRepository, never()).save(any(Users.class));
116+
}
117+
118+
@Test
119+
void updateAvatar_InvalidBase64Data() {
120+
// Arrange
121+
String username = "testuser";
122+
String invalidBase64 = "data:image/jpeg;base64,invalid-base64-data";
123+
UpdateAvatarRequestDto requestDto = new UpdateAvatarRequestDto(invalidBase64);
124+
125+
// Act & Assert
126+
assertThrows(IllegalArgumentException.class, () -> {
127+
usersService.updateAvatar(username, requestDto);
128+
});
129+
130+
verify(userRepository, never()).findByUsername(anyString());
131+
verify(userRepository, never()).save(any(Users.class));
132+
}
133+
}

code/frontend/src/components/Avator.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ export default function AvatarUpload({ user }) {
104104
<Tooltip title="Click to update avatar" placement="top">
105105
<AvatarContainer onClick={handleClick}>
106106
<AvatarImage>{user.avatar ? <img src={user.avatar} /> : user.username.charAt(0).toUpperCase()}</AvatarImage>
107-
<HiddenInput type="file" ref={fileInputRef} accept="image/jpeg,image/png" onChange={handleFileSelect} />
107+
<HiddenInput data-testid="avatar-input" type="file" ref={fileInputRef} accept="image/jpeg,image/png" onChange={handleFileSelect} />
108108
</AvatarContainer>
109109
</Tooltip>
110110
<Snackbar
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
2+
import { describe, expect, vi, beforeEach, afterEach, test } from "vitest";
3+
import AvatarUpload from "../../components/Avator";
4+
5+
const mockUpdateAvatar = vi.fn();
6+
vi.mock("../../contexts/AuthContext", () => {
7+
return {
8+
useAuth: () => ({
9+
updateAvatar: mockUpdateAvatar,
10+
}),
11+
AuthProvider: ({ children }) => <div data-testid="MockAuthProvider">{children}</div>,
12+
};
13+
});
14+
15+
// Mock image compression library
16+
vi.mock("browser-image-compression", () => ({
17+
default: vi.fn().mockImplementation((file) => Promise.resolve(file)),
18+
}));
19+
20+
// Mock FileReader
21+
class MockFileReader {
22+
onload = null;
23+
readAsDataURL() {
24+
this.onload({ target: { result: "data:image/jpeg;base64,mockBase64Data" } });
25+
}
26+
}
27+
28+
import { AuthProvider } from "../../contexts/AuthContext";
29+
30+
describe("AvatarUpload Component Tests", () => {
31+
const mockUser = {
32+
username: "testuser",
33+
userEmail: "test@example.com",
34+
};
35+
36+
beforeEach(() => {
37+
vi.clearAllMocks();
38+
mockUpdateAvatar.mockResolvedValue({ success: true, avatarResponse: { avatar: "newAvatarUrl" } });
39+
});
40+
41+
afterEach(() => {
42+
vi.clearAllMocks();
43+
});
44+
45+
test("renders avatar with username initial when no avatar exists", () => {
46+
render(
47+
<AuthProvider>
48+
<AvatarUpload user={mockUser} />
49+
</AuthProvider>
50+
);
51+
expect(screen.getByText("T")).toBeInTheDocument();
52+
});
53+
54+
test("renders avatar image when avatar exists", () => {
55+
const userWithAvatar = { ...mockUser, avatar: "avatarUrl" };
56+
render(
57+
<AuthProvider>
58+
<AvatarUpload user={userWithAvatar} />
59+
</AuthProvider>
60+
);
61+
const avatarImage = screen.getByRole("img");
62+
expect(avatarImage).toHaveAttribute("src", "avatarUrl");
63+
});
64+
65+
test("shows success message when avatar is updated successfully", async () => {
66+
render(
67+
<AuthProvider>
68+
<AvatarUpload user={mockUser} />
69+
</AuthProvider>
70+
);
71+
72+
const file = new File(["test"], "test.jpg", { type: "image/jpeg" });
73+
const input = screen.getByTestId("avatar-input");
74+
75+
fireEvent.change(input, { target: { files: [file] } });
76+
77+
await waitFor(() => {
78+
expect(screen.getByText("success to update avatar")).toBeInTheDocument();
79+
});
80+
});
81+
82+
test("shows error message when uploading unsupported file type", async () => {
83+
render(
84+
<AuthProvider>
85+
<AvatarUpload user={mockUser} />
86+
</AuthProvider>
87+
);
88+
89+
const file = new File(["test"], "test.gif", { type: "image/gif" });
90+
const input = screen.getByTestId("avatar-input");
91+
92+
fireEvent.change(input, { target: { files: [file] } });
93+
94+
await waitFor(() => {
95+
expect(screen.getByText("only jpeg and png are supported")).toBeInTheDocument();
96+
});
97+
});
98+
99+
test("shows error message when avatar update fails", async () => {
100+
mockUpdateAvatar.mockRejectedValue(new Error("Update failed"));
101+
102+
render(
103+
<AuthProvider>
104+
<AvatarUpload user={mockUser} />
105+
</AuthProvider>
106+
);
107+
108+
const file = new File(["test"], "test.jpg", { type: "image/jpeg" });
109+
const input = screen.getByTestId("avatar-input");
110+
111+
fireEvent.change(input, { target: { files: [file] } });
112+
113+
await waitFor(() => {
114+
expect(screen.getByText("Update failed")).toBeInTheDocument();
115+
});
116+
});
117+
118+
test("compresses image before upload", async () => {
119+
const { default: imageCompression } = await import("browser-image-compression");
120+
121+
render(
122+
<AuthProvider>
123+
<AvatarUpload user={mockUser} />
124+
</AuthProvider>
125+
);
126+
127+
const file = new File(["test"], "test.jpg", { type: "image/jpeg" });
128+
const input = screen.getByTestId("avatar-input");
129+
130+
fireEvent.change(input, { target: { files: [file] } });
131+
132+
await waitFor(() => {
133+
expect(imageCompression).toHaveBeenCalledWith(file, {
134+
maxSizeMB: 3,
135+
maxWidthOrHeight: 1024,
136+
useWebWorker: true,
137+
});
138+
});
139+
});
140+
});

code/frontend/src/test/pages/Home.test.jsx

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,15 @@ import Home from "../../pages/Home";
44
import { describe, expect, vi, beforeEach, afterEach, test } from "vitest";
55
import { MemoryRouter } from "react-router-dom";
66

7+
const mockNavigate = vi.fn();
8+
vi.mock("react-router-dom", async (importOriginal) => {
9+
const actual = await importOriginal();
10+
return {
11+
...actual,
12+
useNavigate: () => mockNavigate,
13+
};
14+
});
15+
716
vi.mock("../../contexts/AuthContext", () => {
817
return {
918
useAuth: () => ({
@@ -12,6 +21,7 @@ vi.mock("../../contexts/AuthContext", () => {
1221
username: "testuser",
1322
userEmail: "test@example.com",
1423
},
24+
logout: vi.fn(),
1525
}),
1626
AuthProvider: ({ children }) => <div data-testid="MockAuthProvider">{children}</div>,
1727
};
@@ -167,4 +177,19 @@ describe("Home Page Unit Test", () => {
167177
});
168178
expect(screen.queryByText("Joined 1")).toBeNull();
169179
});
170-
});
180+
181+
test("logout", async () => {
182+
render(
183+
<AuthProvider>
184+
<MemoryRouter>
185+
<Home />
186+
</MemoryRouter>
187+
</AuthProvider>
188+
);
189+
fireEvent.click(screen.getByLabelText("logout"));
190+
await waitFor(() => {
191+
expect(mockNavigate).toHaveBeenCalledTimes(1);
192+
expect(mockNavigate).toHaveBeenCalledWith("/login");
193+
});
194+
});
195+
});

0 commit comments

Comments
 (0)