Просмотр исходного кода

RHL-009 feat(auth): implement change password functionality with corresponding tests

Code_Uwe 5 дней назад
Родитель
Сommit
3a8958c6d7
2 измененных файлов с 61 добавлено и 13 удалено
  1. 20 4
      lib/frontend/apiClient.js
  2. 41 9
      lib/frontend/apiClient.test.js

+ 20 - 4
lib/frontend/apiClient.js

@@ -280,6 +280,22 @@ export function getMe(options) {
 	return apiFetch("/api/auth/me", { method: "GET", ...options });
 }
 
+/**
+ * Change password (RHL-009):
+ * - Requires an active session cookie.
+ * - Body: { currentPassword, newPassword }
+ *
+ * @param {{ currentPassword: string, newPassword: string }} input
+ * @param {{ baseUrl?: string, fetchImpl?: typeof fetch }=} options
+ */
+export function changePassword(input, options) {
+	return apiFetch("/api/auth/change-password", {
+		method: "POST",
+		body: input,
+		...options,
+	});
+}
+
 /**
  * List branches visible to the current session (RBAC is enforced server-side).
  *
@@ -308,9 +324,9 @@ export function getYears(branch, options) {
 export function getMonths(branch, year, options) {
 	return apiFetch(
 		`/api/branches/${encodeURIComponent(branch)}/${encodeURIComponent(
-			year
+			year,
 		)}/months`,
-		{ method: "GET", ...options }
+		{ method: "GET", ...options },
 	);
 }
 
@@ -323,9 +339,9 @@ export function getMonths(branch, year, options) {
 export function getDays(branch, year, month, options) {
 	return apiFetch(
 		`/api/branches/${encodeURIComponent(branch)}/${encodeURIComponent(
-			year
+			year,
 		)}/${encodeURIComponent(month)}/days`,
-		{ method: "GET", ...options }
+		{ method: "GET", ...options },
 	);
 }
 

+ 41 - 9
lib/frontend/apiClient.test.js

@@ -8,6 +8,7 @@ import {
 	login,
 	getMe,
 	search,
+	changePassword,
 } from "./apiClient.js";
 
 beforeEach(() => {
@@ -28,7 +29,7 @@ describe("lib/frontend/apiClient", () => {
 			new Response(JSON.stringify({ ok: true }), {
 				status: 200,
 				headers: { "Content-Type": "application/json" },
-			})
+			}),
 		);
 
 		await apiFetch("/api/health");
@@ -46,7 +47,7 @@ describe("lib/frontend/apiClient", () => {
 			new Response(JSON.stringify({ ok: true }), {
 				status: 200,
 				headers: { "Content-Type": "application/json" },
-			})
+			}),
 		);
 
 		await login({ username: "u", password: "p" });
@@ -64,8 +65,8 @@ describe("lib/frontend/apiClient", () => {
 				JSON.stringify({
 					error: { message: "Unauthorized", code: "AUTH_UNAUTHENTICATED" },
 				}),
-				{ status: 401, headers: { "Content-Type": "application/json" } }
-			)
+				{ status: 401, headers: { "Content-Type": "application/json" } },
+			),
 		);
 
 		await expect(apiFetch("/api/branches")).rejects.toMatchObject({
@@ -99,8 +100,8 @@ describe("lib/frontend/apiClient", () => {
 					day: "23",
 					files: [],
 				}),
-				{ status: 200, headers: { "Content-Type": "application/json" } }
-			)
+				{ status: 200, headers: { "Content-Type": "application/json" } },
+			),
 		);
 
 		await getFiles("NL01", "2024", "10", "23");
@@ -124,7 +125,7 @@ describe("lib/frontend/apiClient", () => {
 			new Response(JSON.stringify({ user: null }), {
 				status: 200,
 				headers: { "Content-Type": "application/json" },
-			})
+			}),
 		);
 
 		const res = await getMe();
@@ -141,12 +142,43 @@ describe("lib/frontend/apiClient", () => {
 		expect(init.cache).toBe("no-store");
 	});
 
+	it("changePassword calls /api/auth/change-password with POST and JSON body", async () => {
+		fetch.mockResolvedValue(
+			new Response(JSON.stringify({ ok: true }), {
+				status: 200,
+				headers: { "Content-Type": "application/json" },
+			}),
+		);
+
+		await changePassword({
+			currentPassword: "OldPassword123",
+			newPassword: "StrongPassword123",
+		});
+
+		expect(fetch).toHaveBeenCalledTimes(1);
+		const [url, init] = fetch.mock.calls[0];
+
+		expect(url).toBe("/api/auth/change-password");
+		expect(init.method).toBe("POST");
+		expect(init.headers.Accept).toBe("application/json");
+		expect(init.headers["Content-Type"]).toBe("application/json");
+		expect(init.body).toBe(
+			JSON.stringify({
+				currentPassword: "OldPassword123",
+				newPassword: "StrongPassword123",
+			}),
+		);
+
+		expect(init.credentials).toBe("include");
+		expect(init.cache).toBe("no-store");
+	});
+
 	it("search builds the expected query string for branch scope", async () => {
 		fetch.mockResolvedValue(
 			new Response(JSON.stringify({ items: [], nextCursor: null }), {
 				status: 200,
 				headers: { "Content-Type": "application/json" },
-			})
+			}),
 		);
 
 		await search({ q: "bridgestone", branch: "NL01", limit: 100 });
@@ -170,7 +202,7 @@ describe("lib/frontend/apiClient", () => {
 			new Response(JSON.stringify({ items: [], nextCursor: null }), {
 				status: 200,
 				headers: { "Content-Type": "application/json" },
-			})
+			}),
 		);
 
 		await search({