mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 21:10:59 +00:00
fix(profile): localize the email-conflict message too
The 409 email-conflict branch had a safe, specific message already, but no errorCode - the account-section fallback would have had nothing to translate it through once it stopped echoing json.error verbatim. Give it profile.update.emailInUse so it renders in the active locale like every other profile-save outcome, and propagate errorCode through both route handlers even on the (non-issues) conflict path. Also updates the /api/user/profile route test for the reported scenario directly: a valid timeFormat now lands even when gender in the same PATCH is unsupported, instead of the whole request failing.
This commit is contained in:
1 parent
6f5935aeb4
commit
356888134d
10 files changed
+88
-25
No files matched your search
+2
-1
@@ -124,7 +124,8 @@
|
||||
"profile": {
|
||||
"update": {
|
||||
"nothingSaved": "Es wurde nichts gespeichert. Bitte das markierte Feld korrigieren und erneut versuchen.",
|
||||
"invalidBody": "Die Profilaktualisierung konnte nicht gelesen werden."
|
||||
"invalidBody": "Die Profilaktualisierung konnte nicht gelesen werden.",
|
||||
"emailInUse": "Diese E-Mail-Adresse wird bereits verwendet."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
+2
-1
@@ -124,7 +124,8 @@
|
||||
"profile": {
|
||||
"update": {
|
||||
"nothingSaved": "Nothing was saved. Check the highlighted field and try again.",
|
||||
"invalidBody": "The profile update could not be read."
|
||||
"invalidBody": "The profile update could not be read.",
|
||||
"emailInUse": "This email address is already in use."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
+2
-1
@@ -124,7 +124,8 @@
|
||||
"profile": {
|
||||
"update": {
|
||||
"nothingSaved": "No se guardó nada. Revisa el campo marcado e inténtalo de nuevo.",
|
||||
"invalidBody": "No se pudo leer la actualización del perfil."
|
||||
"invalidBody": "No se pudo leer la actualización del perfil.",
|
||||
"emailInUse": "Esta dirección de correo ya está en uso."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
+2
-1
@@ -124,7 +124,8 @@
|
||||
"profile": {
|
||||
"update": {
|
||||
"nothingSaved": "Rien n'a été enregistré. Corrigez le champ signalé et réessayez.",
|
||||
"invalidBody": "La mise à jour du profil n'a pas pu être lue."
|
||||
"invalidBody": "La mise à jour du profil n'a pas pu être lue.",
|
||||
"emailInUse": "Cette adresse e-mail est déjà utilisée."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
+2
-1
@@ -124,7 +124,8 @@
|
||||
"profile": {
|
||||
"update": {
|
||||
"nothingSaved": "Non è stato salvato nulla. Controlla il campo evidenziato e riprova.",
|
||||
"invalidBody": "Impossibile leggere l'aggiornamento del profilo."
|
||||
"invalidBody": "Impossibile leggere l'aggiornamento del profilo.",
|
||||
"emailInUse": "Questo indirizzo email è già in uso."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
+2
-1
@@ -124,7 +124,8 @@
|
||||
"profile": {
|
||||
"update": {
|
||||
"nothingSaved": "Nic nie zostało zapisane. Sprawdź oznaczone pole i spróbuj ponownie.",
|
||||
"invalidBody": "Nie udało się odczytać aktualizacji profilu."
|
||||
"invalidBody": "Nie udało się odczytać aktualizacji profilu.",
|
||||
"emailInUse": "Ten adres e-mail jest już używany."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -20,14 +20,10 @@ export const PUT = apiHandler(async (request: NextRequest) => {
|
||||
|
||||
const result = await applyProfileUpdate(user.id, body, getClientIp(request));
|
||||
if (!result.ok) {
|
||||
const meta = result.errorCode ? { errorCode: result.errorCode } : undefined;
|
||||
return result.issues
|
||||
? apiValidationError(
|
||||
result.message,
|
||||
result.issues,
|
||||
result.status,
|
||||
result.errorCode ? { errorCode: result.errorCode } : undefined,
|
||||
)
|
||||
: apiError(result.message, result.status);
|
||||
? apiValidationError(result.message, result.issues, result.status, meta)
|
||||
: apiError(result.message, result.status, meta);
|
||||
}
|
||||
|
||||
annotate({ action: { name: "auth.profile.update" } });
|
||||
|
||||
@@ -368,13 +368,73 @@ describe("PATCH /api/user/profile", () => {
|
||||
expect(body.data.gender).toBe("OTHER");
|
||||
});
|
||||
|
||||
it("still refuses an unsupported gender, with a message that names the field", async () => {
|
||||
it("still refuses an unsupported gender alone, with a message that names the field", async () => {
|
||||
vi.mocked(getSession).mockResolvedValue(SESSION_OK as never);
|
||||
const res = await PATCH(req({ timeFormat: "H24", gender: "diverse" }));
|
||||
const res = await PATCH(req({ gender: "diverse" }));
|
||||
expect(res.status).toBe(422);
|
||||
expect(prisma.user.update).not.toHaveBeenCalled();
|
||||
const body = (await res.json()) as { error: string };
|
||||
expect(body.error).toContain("Gender must be MALE, FEMALE or OTHER");
|
||||
const body = (await res.json()) as {
|
||||
error: string;
|
||||
details?: { issues?: Array<{ path: string; message: string }> };
|
||||
};
|
||||
// The top-level message is a sentence for a person — it names the
|
||||
// field but never repeats the validator's own enum-literal prose.
|
||||
expect(body.error).toMatch(/nothing was saved/i);
|
||||
expect(body.error).toContain("gender");
|
||||
expect(body.error).not.toContain("Invalid option");
|
||||
// The specific reason is relocated, not deleted.
|
||||
expect(body.details?.issues?.[0]).toMatchObject({ path: "gender" });
|
||||
});
|
||||
|
||||
// Reported regression: a rejected `gender` took a sibling `timeFormat`
|
||||
// edit down with it because the whole PATCH was one transaction. The
|
||||
// schema has no cross-field dependency, so the valid field must now
|
||||
// land even when `gender` is rejected in the same body.
|
||||
it("persists a valid sibling field even when gender in the same body is unsupported", async () => {
|
||||
vi.mocked(getSession).mockResolvedValue(SESSION_OK as never);
|
||||
vi.mocked(prisma.user.findUnique).mockResolvedValue(null);
|
||||
vi.mocked(prisma.user.update).mockResolvedValue({
|
||||
id: "user-1",
|
||||
username: "testuser",
|
||||
displayName: null,
|
||||
email: null,
|
||||
role: "USER",
|
||||
heightCm: null,
|
||||
dateOfBirth: null,
|
||||
gender: null,
|
||||
timezone: "Europe/Berlin",
|
||||
locale: null,
|
||||
timeFormat: "H24",
|
||||
dateFormat: "AUTO",
|
||||
moodReminderEnabled: false,
|
||||
fullName: null,
|
||||
insurerName: null,
|
||||
insurerIkNumber: null,
|
||||
insuranceNumberEncrypted: null,
|
||||
} as never);
|
||||
|
||||
const res = await PATCH(req({ timeFormat: "H24", gender: "diverse" }));
|
||||
expect(res.status).toBe(200);
|
||||
expect(prisma.user.update).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
data: expect.objectContaining({ timeFormat: "H24" }),
|
||||
}),
|
||||
);
|
||||
const arg = vi.mocked(prisma.user.update).mock.calls[0][0] as {
|
||||
data: Record<string, unknown>;
|
||||
};
|
||||
expect("gender" in arg.data).toBe(false);
|
||||
const body = (await res.json()) as {
|
||||
error: string | null;
|
||||
data: {
|
||||
timeFormat: string;
|
||||
rejectedFields?: Array<{ path: string }>;
|
||||
};
|
||||
};
|
||||
expect(body.error).toBeNull();
|
||||
expect(body.data.timeFormat).toBe("H24");
|
||||
expect(body.data.rejectedFields).toEqual([
|
||||
expect.objectContaining({ path: "gender" }),
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -102,14 +102,10 @@ export const PATCH = apiHandler(async (request: NextRequest) => {
|
||||
|
||||
const result = await applyProfileUpdate(user.id, body, getClientIp(request));
|
||||
if (!result.ok) {
|
||||
const meta = result.errorCode ? { errorCode: result.errorCode } : undefined;
|
||||
return result.issues
|
||||
? apiValidationError(
|
||||
result.message,
|
||||
result.issues,
|
||||
result.status,
|
||||
result.errorCode ? { errorCode: result.errorCode } : undefined,
|
||||
)
|
||||
: apiError(result.message, result.status);
|
||||
? apiValidationError(result.message, result.issues, result.status, meta)
|
||||
: apiError(result.message, result.status, meta);
|
||||
}
|
||||
|
||||
annotate({ action: { name: "user.profile.update" } });
|
||||
|
||||
@@ -210,7 +210,12 @@ export async function applyProfileUpdate(
|
||||
select: { id: true },
|
||||
});
|
||||
if (existing && existing.id !== userId) {
|
||||
return { ok: false, status: 409, message: "Email already in use" };
|
||||
return {
|
||||
ok: false,
|
||||
status: 409,
|
||||
message: "Email already in use",
|
||||
errorCode: "profile.update.emailInUse",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user