ajout de la connection via facebook et du captcha
This commit is contained in:
75
backend/tests/captcha.middleware.test.js
Normal file
75
backend/tests/captcha.middleware.test.js
Normal file
@@ -0,0 +1,75 @@
|
||||
const verifyCaptcha = require("../src/middlewares/captcha.middleware");
|
||||
|
||||
function mockRes() {
|
||||
const res = {};
|
||||
res.status = jest.fn().mockReturnValue(res);
|
||||
res.json = jest.fn().mockReturnValue(res);
|
||||
return res;
|
||||
}
|
||||
|
||||
describe("verifyCaptcha", () => {
|
||||
const originalFetch = global.fetch;
|
||||
|
||||
beforeEach(() => {
|
||||
process.env.TURNSTILE_SECRET_KEY = "secret-test";
|
||||
global.fetch = jest.fn();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.env.TURNSTILE_SECRET_KEY = "";
|
||||
global.fetch = originalFetch;
|
||||
});
|
||||
|
||||
it("laisse passer sans vérifier quand aucune clé n'est configurée", async () => {
|
||||
process.env.TURNSTILE_SECRET_KEY = "";
|
||||
const next = jest.fn();
|
||||
|
||||
await verifyCaptcha({ body: {} }, mockRes(), next);
|
||||
|
||||
expect(next).toHaveBeenCalled();
|
||||
expect(global.fetch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("refuse une requête sans jeton", async () => {
|
||||
const res = mockRes();
|
||||
const next = jest.fn();
|
||||
|
||||
await verifyCaptcha({ body: {} }, res, next);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(400);
|
||||
expect(next).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("appelle next quand Cloudflare valide le jeton", async () => {
|
||||
global.fetch.mockResolvedValue({ json: () => Promise.resolve({ success: true }) });
|
||||
const next = jest.fn();
|
||||
|
||||
await verifyCaptcha({ body: { captchaToken: "jeton" }, ip: "1.2.3.4" }, mockRes(), next);
|
||||
|
||||
expect(next).toHaveBeenCalled();
|
||||
const [, options] = global.fetch.mock.calls[0];
|
||||
expect(JSON.parse(options.body)).toEqual({ secret: "secret-test", response: "jeton", remoteip: "1.2.3.4" });
|
||||
});
|
||||
|
||||
it("refuse quand Cloudflare rejette le jeton", async () => {
|
||||
global.fetch.mockResolvedValue({ json: () => Promise.resolve({ success: false }) });
|
||||
const res = mockRes();
|
||||
const next = jest.fn();
|
||||
|
||||
await verifyCaptcha({ body: { captchaToken: "faux" } }, res, next);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(400);
|
||||
expect(next).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("répond 503 quand Cloudflare est injoignable", async () => {
|
||||
global.fetch.mockRejectedValue(new Error("réseau"));
|
||||
jest.spyOn(console, "error").mockImplementation(() => {});
|
||||
const res = mockRes();
|
||||
|
||||
await verifyCaptcha({ body: { captchaToken: "jeton" } }, res, jest.fn());
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(503);
|
||||
console.error.mockRestore();
|
||||
});
|
||||
});
|
||||
113
backend/tests/facebookLogin.test.js
Normal file
113
backend/tests/facebookLogin.test.js
Normal file
@@ -0,0 +1,113 @@
|
||||
// Identifiants factices posés avant le chargement de passport : la stratégie
|
||||
// Facebook est ainsi enregistrée même sans vraie app Meta configurée.
|
||||
process.env.FACEBOOK_APP_ID = "test-app-id";
|
||||
process.env.FACEBOOK_APP_SECRET = "test-app-secret";
|
||||
|
||||
const request = require("supertest");
|
||||
const app = require("../src/app");
|
||||
const prisma = require("../src/config/prisma");
|
||||
const mailer = require("../src/config/mailer");
|
||||
const { passport } = require("../src/config/passport");
|
||||
|
||||
jest.mock("../src/config/mailer", () => ({ sendMail: jest.fn() }));
|
||||
|
||||
const EMAIL = "facebook-login-test@test.local";
|
||||
const EMAIL_EXISTANT = "facebook-link-test@test.local";
|
||||
|
||||
function profil(overrides = {}) {
|
||||
return {
|
||||
id: "sim-facebook-id-987654",
|
||||
displayName: "Léa Durand",
|
||||
name: { givenName: "Léa", familyName: "Durand" },
|
||||
emails: [{ value: EMAIL }],
|
||||
photos: [{ value: "https://example.test/fb.png" }],
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
// Rejoue le callback de la strategie Facebook sans sortir sur le reseau.
|
||||
function verifierProfilFacebook(profile) {
|
||||
const strategy = passport._strategy("facebook");
|
||||
return new Promise((resolve, reject) => {
|
||||
strategy._verify("access-token", "refresh-token", profile, (err, user, info) => {
|
||||
if (err) reject(err);
|
||||
else resolve({ user, info });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function cleanup() {
|
||||
await prisma.user.deleteMany({ where: { email: { in: [EMAIL, EMAIL_EXISTANT] } } });
|
||||
}
|
||||
|
||||
beforeAll(cleanup);
|
||||
afterAll(async () => {
|
||||
await cleanup();
|
||||
await prisma.$disconnect();
|
||||
});
|
||||
|
||||
describe("Connexion Facebook", () => {
|
||||
beforeEach(() => jest.clearAllMocks());
|
||||
|
||||
test("la route de depart redirige vers Facebook", async () => {
|
||||
const res = await request(app).get("/api/auth/facebook");
|
||||
|
||||
expect(res.status).toBe(302);
|
||||
const url = new URL(res.headers.location);
|
||||
expect(url.hostname).toBe("www.facebook.com");
|
||||
expect(url.searchParams.get("client_id")).toBe("test-app-id");
|
||||
expect(url.searchParams.get("scope")).toBe("email");
|
||||
expect(url.searchParams.get("redirect_uri")).toMatch(/\/api\/auth\/facebook\/callback$/);
|
||||
});
|
||||
|
||||
test("un premier passage cree un compte FACEBOOK sans mot de passe", async () => {
|
||||
mailer.sendMail.mockResolvedValue(true);
|
||||
|
||||
const { user } = await verifierProfilFacebook(profil());
|
||||
|
||||
expect(user.provider).toBe("FACEBOOK");
|
||||
expect(user.providerId).toBe("sim-facebook-id-987654");
|
||||
expect(user.password).toBeNull();
|
||||
expect(user.isEmailVerified).toBe(true);
|
||||
expect(user.firstname).toBe("Léa");
|
||||
|
||||
expect(mailer.sendMail).toHaveBeenCalledTimes(1);
|
||||
expect(mailer.sendMail.mock.calls[0][0].html).toContain("créé avec Facebook");
|
||||
});
|
||||
|
||||
test("un second passage reconnait le compte sans renvoyer d email", async () => {
|
||||
const { user } = await verifierProfilFacebook(profil());
|
||||
|
||||
expect(user.email).toBe(EMAIL);
|
||||
expect(mailer.sendMail).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("un compte existant avec le meme email est rattache a Facebook", async () => {
|
||||
await prisma.user.create({
|
||||
data: { firstname: "Paul", lastname: "Martin", email: EMAIL_EXISTANT, password: "hash", isEmailVerified: false },
|
||||
});
|
||||
|
||||
const { user } = await verifierProfilFacebook(
|
||||
profil({ id: "sim-facebook-id-link", emails: [{ value: EMAIL_EXISTANT }] })
|
||||
);
|
||||
|
||||
expect(user.provider).toBe("FACEBOOK");
|
||||
expect(user.isEmailVerified).toBe(true);
|
||||
// Le mot de passe existant est conserve.
|
||||
expect(user.password).toBe("hash");
|
||||
});
|
||||
|
||||
test("un compte Facebook sans email est refuse", async () => {
|
||||
const { user, info } = await verifierProfilFacebook(profil({ id: "sim-sans-email", emails: undefined }));
|
||||
|
||||
expect(user).toBe(false);
|
||||
expect(info.message).toMatch(/Facebook/);
|
||||
});
|
||||
|
||||
test("la connexion par mot de passe oriente vers le bouton Facebook", async () => {
|
||||
const res = await request(app).post("/api/auth/login").send({ email: EMAIL, password: "PeuImporte123" });
|
||||
|
||||
expect(res.status).toBe(401);
|
||||
expect(res.body.message).toMatch(/connexion Facebook/);
|
||||
});
|
||||
});
|
||||
64
backend/tests/oauthCallback.test.js
Normal file
64
backend/tests/oauthCallback.test.js
Normal file
@@ -0,0 +1,64 @@
|
||||
// Ce fichier ne charge pas app.js, donc pas le .env : le JWT a besoin d'un secret.
|
||||
process.env.JWT_SECRET = process.env.JWT_SECRET || "test-secret";
|
||||
|
||||
const getFrontendUrl = require("../src/utils/frontendUrl");
|
||||
const { oauthCallback } = require("../src/controllers/auth.controller");
|
||||
|
||||
describe("getFrontendUrl", () => {
|
||||
const original = process.env.FRONTEND_URL;
|
||||
afterEach(() => {
|
||||
process.env.FRONTEND_URL = original;
|
||||
});
|
||||
|
||||
test("ne garde que la premiere origine d une liste (CORS prod)", () => {
|
||||
process.env.FRONTEND_URL = "https://site.fr, https://www.site.fr";
|
||||
expect(getFrontendUrl()).toBe("https://site.fr");
|
||||
});
|
||||
|
||||
test("retire le slash final", () => {
|
||||
process.env.FRONTEND_URL = "https://site.fr/";
|
||||
expect(getFrontendUrl()).toBe("https://site.fr");
|
||||
});
|
||||
|
||||
test("retombe sur localhost sans configuration", () => {
|
||||
delete process.env.FRONTEND_URL;
|
||||
expect(getFrontendUrl()).toBe("http://localhost:5173");
|
||||
});
|
||||
});
|
||||
|
||||
describe("oauthCallback", () => {
|
||||
const original = process.env.FRONTEND_URL;
|
||||
afterEach(() => {
|
||||
process.env.FRONTEND_URL = original;
|
||||
});
|
||||
|
||||
test("redirige vers une URL valide avec un profil lisible par le front", () => {
|
||||
process.env.FRONTEND_URL = "https://site.fr,https://www.site.fr";
|
||||
const res = { redirect: jest.fn() };
|
||||
// Accents (UTF-8) et « ~ » : ce profil produit un base64 contenant « + »,
|
||||
// qui deviendrait un espace sans encodage.
|
||||
const user = { id: 7, firstname: "Chloé", lastname: "Lefèvre", email: "ab~test@test.local", role: "CLIENT" };
|
||||
expect(Buffer.from(JSON.stringify(user)).toString("base64")).toContain("+");
|
||||
|
||||
oauthCallback({ user }, res);
|
||||
|
||||
const url = new URL(res.redirect.mock.calls[0][0]);
|
||||
expect(`${url.origin}${url.pathname}`).toBe("https://site.fr/auth/callback");
|
||||
expect(url.searchParams.get("token")).toBeTruthy();
|
||||
|
||||
// Même décodage que frontend/src/pages/AuthCallback.jsx.
|
||||
const bytes = Uint8Array.from(atob(url.searchParams.get("user")), (c) => c.charCodeAt(0));
|
||||
expect(JSON.parse(new TextDecoder().decode(bytes))).toEqual(user);
|
||||
});
|
||||
|
||||
test("renvoie une erreur au front quand passport n a pas d utilisateur", () => {
|
||||
process.env.FRONTEND_URL = "https://site.fr,https://www.site.fr";
|
||||
const res = { redirect: jest.fn() };
|
||||
|
||||
oauthCallback({}, res);
|
||||
|
||||
const url = new URL(res.redirect.mock.calls[0][0]);
|
||||
expect(url.origin).toBe("https://site.fr");
|
||||
expect(url.searchParams.get("error")).toMatch(/refusée/);
|
||||
});
|
||||
});
|
||||
4
backend/tests/setupEnv.js
Normal file
4
backend/tests/setupEnv.js
Normal file
@@ -0,0 +1,4 @@
|
||||
// Exécuté avant chaque fichier de test, donc avant le dotenv de src/app.js :
|
||||
// dotenv n'écrase pas une variable déjà définie, le captcha reste désactivé
|
||||
// pour les tests d'API (captcha.middleware.test.js le teste à part).
|
||||
process.env.TURNSTILE_SECRET_KEY = "";
|
||||
Reference in New Issue
Block a user