All checks were successful
Depuis /admin/messages, un admin peut créer une issue Forgejo à partir
d'un message utilisateur : bouton « Créer un ticket » → modale pré-remplie
éditable → l'issue est créée via l'API Forgejo et le lien (#N) est
mémorisé sur le message. Première intégration Forgejo d'apps/api.
- Modèle : `forgejoIssueNumber` / `forgejoIssueUrl` sur `ContactMessage`
(migration `20260908130000_contact_message_forgejo_link`, 2 colonnes
nullables, aucun backfill).
- Config : `FORGEJO_BASE_URL` / `FORGEJO_TOKEN` / `FORGEJO_REPO`, toutes
`.optional()` — **fail-closed** (même posture que `ADMIN_JWT_SECRET`) :
bouton masqué + endpoint `503` tant que les trois ne sont pas là.
Documentées dans `.env.example` ; valeurs de test dans `ci.yml`.
- `ForgejoService` (`lib/forgejo/forgejo.service.ts`, singleton `forgejo`) :
`isConfigured()` + `createIssue({ title, body })` → `POST .../issues`,
en-tête `Authorization: token <pat>`, lit `{ number, html_url }` ;
erreur réseau / non-2xx / JSON invalide ⇒ `HttpError 502
FORGEJO_REQUEST_FAILED`.
- `admin-contact` : `POST /admin/api/contact/:id/forgejo-issue`
(`createForgejoIssueFromMessage` — ordre 503 → 404 → 409 → Forgejo →
écriture BDD ; un échec Forgejo ne touche pas la BDD) et
`GET /admin/api/contact/forgejo/status`.
- Contrat partagé : `createForgejoIssueSchema` (title 1–255, body 1–60000),
`ContactMessageAdminView` étendu, `ForgejoIntegrationStatus`, nouveaux
`ErrorCode` (`FORGEJO_NOT_CONFIGURED`=5001, `FORGEJO_REQUEST_FAILED`=5002,
`CONTACT_MESSAGE_ALREADY_LINKED`=4026, `CONTACT_MESSAGE_NOT_FOUND`=4055)
+ labels i18n.
- Front : `MessagesPage` — sonde `forgejo/status` au montage, colonne
« Ticket » (lien `#N` si lié, bouton sinon, « non configurée » sinon),
`CreateForgejoIssueModal` + helpers purs `messages.ts`
(`buildIssueTitle` / `buildIssueBody`).
- Tests : `forgejo.service.test.ts` (stub `globalThis.fetch`),
`admin-contact.test.ts` étendu (nominal / 503 / 404 / 409 / 502 +
ligne intacte / 400), `admin-messages.cy.ts` étendu (modale pré-remplie,
lien après création, masquage si non configuré).
- Docs : `backend-architecture.md`, `batch-cooking-modele.md`.
Closes #81.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
159 lines
5.6 KiB
TypeScript
159 lines
5.6 KiB
TypeScript
// Mocks the admin API via cy.intercept — no live backend (see
|
|
// .github/workflows/ci.yml); apps/api's Mocha suite covers
|
|
// `GET /admin/api/contact` and the Forgejo routes against a real database.
|
|
|
|
const adminBody = {
|
|
id: 1,
|
|
email: "ops@example.com",
|
|
name: "Ops",
|
|
createdAt: "2026-08-01T00:00:00.000Z",
|
|
lastLoginAt: "2026-08-28T09:00:00.000Z",
|
|
};
|
|
|
|
// Matches the list endpoint (`/contact`, `/contact?type=…`) but NOT the
|
|
// nested `/contact/forgejo/status` or `/contact/:id/forgejo-issue`.
|
|
const LIST_URL = /\/admin\/api\/contact(\?|$)/;
|
|
|
|
function messages() {
|
|
return [
|
|
{
|
|
id: 2,
|
|
type: "bug",
|
|
message: "Le bouton Envoyer ne réagit pas sur la page planning.",
|
|
pageUrl: "/",
|
|
userAgent: "Mozilla/5.0 (Linux; Android 14) BatchCookingApp/0.2.1",
|
|
appVersion: "0.2.1",
|
|
createdAt: "2026-09-07T10:00:00.000Z",
|
|
author: { id: 9, firstName: "Alice", lastName: "Martin", email: "alice@example.com" },
|
|
forgejoIssueNumber: null,
|
|
forgejoIssueUrl: null,
|
|
},
|
|
{
|
|
id: 1,
|
|
type: "suggestion",
|
|
message: "Ajoutez un mode hors-ligne pour la liste de courses.",
|
|
pageUrl: "/liste-de-courses",
|
|
userAgent: null,
|
|
appVersion: null,
|
|
createdAt: "2026-09-06T08:30:00.000Z",
|
|
author: null,
|
|
forgejoIssueNumber: null,
|
|
forgejoIssueUrl: null,
|
|
},
|
|
];
|
|
}
|
|
|
|
describe("Admin contact messages (/admin/messages)", () => {
|
|
beforeEach(() => {
|
|
cy.viewport(1400, 1000);
|
|
cy.intercept("GET", "**/admin/api/auth/me", { statusCode: 200, body: adminBody });
|
|
// Default: Forgejo integration off — individual tests override.
|
|
cy.intercept("GET", "**/admin/api/contact/forgejo/status", {
|
|
statusCode: 200,
|
|
body: { enabled: false },
|
|
}).as("forgejoStatus");
|
|
});
|
|
|
|
it("lists the messages read-only: sender or « Compte supprimé », type badge, message", () => {
|
|
cy.intercept("GET", LIST_URL, { statusCode: 200, body: messages() }).as("getMessages");
|
|
|
|
cy.visit("/admin/messages");
|
|
cy.wait("@getMessages");
|
|
|
|
cy.get(".messages-table tbody tr").should("have.length", 2);
|
|
cy.contains(".messages-table tr", "page planning").should("contain.text", "alice@example.com");
|
|
cy.contains(".messages-table tr", "mode hors-ligne").should("contain.text", "Compte supprimé");
|
|
// Integration off ⇒ no create button, and the "not configured" note shows.
|
|
cy.get(".messages-table__ticket-button").should("not.exist");
|
|
cy.contains(".messages-forgejo-note", "non configurée").should("be.visible");
|
|
});
|
|
|
|
it("filters by type through the query param", () => {
|
|
cy.intercept("GET", "**/admin/api/contact", { statusCode: 200, body: messages() }).as("all");
|
|
cy.intercept("GET", "**/admin/api/contact?type=suggestion", {
|
|
statusCode: 200,
|
|
body: [messages()[1]],
|
|
}).as("filtered");
|
|
|
|
cy.visit("/admin/messages");
|
|
cy.wait("@all");
|
|
|
|
cy.get(".messages-filters select").select("Suggestion");
|
|
cy.wait("@filtered");
|
|
cy.get(".messages-table tbody tr").should("have.length", 1);
|
|
cy.contains(".messages-table tr", "mode hors-ligne").should("be.visible");
|
|
});
|
|
|
|
it("creates a Forgejo issue from a message through the pre-filled modal", () => {
|
|
cy.intercept("GET", "**/admin/api/contact/forgejo/status", {
|
|
statusCode: 200,
|
|
body: { enabled: true },
|
|
}).as("forgejoStatus");
|
|
cy.intercept("GET", LIST_URL, { statusCode: 200, body: messages() }).as("getMessages");
|
|
cy.intercept("POST", "**/admin/api/contact/2/forgejo-issue", {
|
|
statusCode: 200,
|
|
body: {
|
|
...messages()[0],
|
|
forgejoIssueNumber: 101,
|
|
forgejoIssueUrl: "https://git.example/kyuno/batchCooking/issues/101",
|
|
},
|
|
}).as("createIssue");
|
|
|
|
cy.visit("/admin/messages");
|
|
cy.wait("@getMessages");
|
|
|
|
cy.contains(".messages-table tr", "page planning")
|
|
.find(".messages-table__ticket-button")
|
|
.click();
|
|
|
|
// Modal opens pre-filled from the message.
|
|
cy.get("#forgejoIssueTitle").should(
|
|
"have.value",
|
|
"[Bug] Le bouton Envoyer ne réagit pas sur la page planning.",
|
|
);
|
|
cy.get("#forgejoIssueBody").should("contain.value", "Signalé par");
|
|
|
|
cy.contains(".dialog-panel__footer button", "Créer le ticket").click();
|
|
cy.wait("@createIssue").its("request.body").should("have.keys", ["title", "body"]);
|
|
|
|
// Modal closed, the row now shows the link instead of the button.
|
|
cy.get("#forgejoIssueTitle").should("not.exist");
|
|
cy.contains(".messages-table tr", "page planning")
|
|
.find("a")
|
|
.should("have.attr", "href", "https://git.example/kyuno/batchCooking/issues/101")
|
|
.and("contain.text", "#101");
|
|
});
|
|
|
|
it("shows the existing link and no create button for an already-linked message", () => {
|
|
cy.intercept("GET", "**/admin/api/contact/forgejo/status", {
|
|
statusCode: 200,
|
|
body: { enabled: true },
|
|
});
|
|
const linked = messages().map((m) =>
|
|
m.id === 2
|
|
? {
|
|
...m,
|
|
forgejoIssueNumber: 55,
|
|
forgejoIssueUrl: "https://git.example/kyuno/batchCooking/issues/55",
|
|
}
|
|
: m,
|
|
);
|
|
cy.intercept("GET", LIST_URL, { statusCode: 200, body: linked }).as("getMessages");
|
|
|
|
cy.visit("/admin/messages");
|
|
cy.wait("@getMessages");
|
|
|
|
cy.contains(".messages-table tr", "page planning").within(() => {
|
|
cy.contains("a", "#55").should(
|
|
"have.attr",
|
|
"href",
|
|
"https://git.example/kyuno/batchCooking/issues/55",
|
|
);
|
|
cy.get(".messages-table__ticket-button").should("not.exist");
|
|
});
|
|
// The still-unlinked suggestion row keeps its button.
|
|
cy.contains(".messages-table tr", "mode hors-ligne")
|
|
.find(".messages-table__ticket-button")
|
|
.should("be.visible");
|
|
});
|
|
});
|