Skip to content

Commit

Permalink
Ajoute des tests end-to-end pour les audits et rapports (#894)
Browse files Browse the repository at this point in the history
* add basic test

* test audit edition

* test audit deletion

* improve seeds generation and document it

* add notes test

* add statement test

* move tests documentation in cypress folder

* add copy audit, criteria search and hide references tests

* add tests commands

* make getbylabel accept regex

* test filters, download, transverse status, finish audit, na on topic and reset filters features

* add some report tests

* seed an audit before each test

* seed audits for report tests

* use object options in create test audit and fix wait tests

* document tests script and command

* add todo tests

* fix regex in test to catch finish date on audit overview page

* add account creation tests

* handle error where user login with wrong token format

* test user login and settings actions

* temp: to reset

* remove unecessary create test account function

* use debug api endpoints to generate test audits

* test audits list page

* update tests doc

* lint

* only import debug controller on certain condition

* remove faker

* remove cypress boilerplate files

---------

Co-authored-by: Adrien Boutigny <[email protected]>
  • Loading branch information
bellangerq and hissalht authored Dec 13, 2024
1 parent b6391eb commit 9435124
Show file tree
Hide file tree
Showing 40 changed files with 1,502 additions and 2,341 deletions.
9 changes: 8 additions & 1 deletion confiture-rest-api/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { MailModule } from "./mail/mail.module";
import { AuthModule } from "./auth/auth.module";
import { ProfileModule } from "./profile/profile.module";
import { UserMiddleware } from "./auth/user.middleware";
import { DebugController } from "./debug.controller";
import { PrismaService } from "./prisma.service";

@Module({
imports: [
Expand All @@ -21,7 +23,12 @@ import { UserMiddleware } from "./auth/user.middleware";
AuthModule,
ProfileModule
],
controllers: [HealthCheckController]
providers: process.env.DEBUG_ENDPOINTS ? [PrismaService] : [],
controllers: [
HealthCheckController,
// enable debug enpoints only when the DEBUG_ENDPOINTS variable is set
...(process.env.DEBUG_ENDPOINTS ? [DebugController] : [])
]
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
Expand Down
3 changes: 2 additions & 1 deletion confiture-rest-api/src/auth/auth.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { CreateAccountController } from "./create-account.controller";
}),
FeedbackModule,
AuditsModule
]
],
exports: [AuthService]
})
export class AuthModule {}
199 changes: 199 additions & 0 deletions confiture-rest-api/src/debug.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
import { Body, Controller, Post } from "@nestjs/common";
import { PrismaService } from "./prisma.service";
import { AuthService } from "./auth/auth.service";
import {
CriterionResultStatus,
CriterionResultUserImpact
} from "@prisma/client";
import { nanoid } from "nanoid";
import { CRITERIA } from "./audits/criteria";

@Controller("debug")
export class DebugController {
constructor(
private readonly prisma: PrismaService,
private readonly auth: AuthService
) {}

@Post("verification-token")
async getAccountVerificationToken(@Body() body: { username: string }) {
const token = this.auth.regenerateVerificationToken(body.username);
return token;
}

@Post("password-reset-verification-token")
async getPasswordResetVerificationToken(@Body() body: { username: string }) {
const token = this.auth.generatePasswordResetVerificationToken(
body.username
);

return token;
}

@Post("email-update-verification-token")
async getEmailUpdateVerificationToken(@Body() body: { uid: string }) {
const token = this.auth.regenerateEmailUpdateVerificationToken(body.uid);

return token;
}

@Post("create-verified-user")
async createVerifiedUser() {
const email = `john-doe${Math.random()}@example.com`;
const password = "pouetpouetpouet";

await this.auth.createUnverifiedUser(email, password);
const user = await this.prisma.user.update({
data: {
isVerified: true,
verificationJti: null
},
where: {
username: email
}
});

const authToken = await this.auth.signin(email, password);

return {
username: email,
password,
authToken,
uid: user.uid
};
}

@Post("create-audit")
async createAudit(
@Body()
body: {
isComplete: boolean;
isPristine: boolean;
noImprovements: boolean;
auditorEmail?: string;
}
) {
const editUniqueId = `edit-${nanoid()}`;
const reportUniqueId = `report-${nanoid()}`;

const completedAudit = await this.prisma.audit.create({
data: {
editUniqueId: editUniqueId,
consultUniqueId: reportUniqueId,
creationDate: new Date(),
publicationDate: body.isComplete ? new Date() : null,
auditTrace: {
create: {
auditConsultUniqueId: editUniqueId,
auditEditUniqueId: reportUniqueId
}
},
auditType: "FULL",
procedureName: "Audit de mon petit site",
auditorEmail: body.auditorEmail || "[email protected]",
auditorName: "Étienne Durand",
transverseElementsPage: {
create: {
name: "Éléments transverses",
url: ""
}
},
pages: {
createMany: {
data: [
{
name: "Accueil",
url: "https://example.com"
},
{
name: "Contact",
url: "https://example.com/contact"
},
{
name: "À propos",
url: "https://example.com/a-propos"
},
{
name: "Blog",
url: "https://example.com/blog"
},
{
name: "Article",
url: "https://example.com/blog/article"
},
{
name: "Connexion",
url: "https://example.com/connexion"
},
{
name: "Documentation",
url: "https://example.com/documentation"
},
{
name: "FAQ",
url: "https://example.com/faq"
}
]
}
}
},
include: {
transverseElementsPage: true
}
});

const auditPages = await this.prisma.auditedPage.findMany({
where: {
auditUniqueId: editUniqueId
}
});

if (!body.isPristine) {
await Promise.all(
[completedAudit.transverseElementsPage, ...auditPages].map(async (p) =>
this.prisma.criterionResult.createMany({
data: CRITERIA.map((c, i) => ({
status: [
CriterionResultStatus.COMPLIANT,
CriterionResultStatus.NOT_APPLICABLE,
CriterionResultStatus.NOT_COMPLIANT
][i % 3],
notCompliantComment: "Une erreur ici",
notApplicableComment: body.noImprovements
? null
: "Attention quand même si ça devient applicable",
compliantComment: body.noImprovements ? null : "Peut mieux faire",
quickWin: i % 7 === 0,
userImpact: [
CriterionResultUserImpact.MINOR,
CriterionResultUserImpact.MAJOR,
CriterionResultUserImpact.BLOCKING,
null
][i % 4],
topic: c.topic,
criterium: c.criterium,
pageId: p.id
}))
})
)
);
}

if (!body.isComplete && !body.isPristine) {
await this.prisma.criterionResult.delete({
where: {
pageId_topic_criterium: {
topic: 1,
criterium: 1,
pageId: auditPages[0].id
}
}
});
}

return {
editId: editUniqueId,
reportId: reportUniqueId
};
}
}
18 changes: 0 additions & 18 deletions confiture-web-app/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,24 +31,6 @@ Lancer le [serveur local sur le port 3000](http://localhost:3000) :
yarn dev
```

## Tests

[Cypress](https://www.cypress.io/) est utilisé pour lancer des tests end-to-end (e2e) dans un navigateur pour reproduire le comportement des utilisateurs.

Les tests peuvent être lancés de 2 manières :

- Via l’application Cypress avec :

```sh
yarn cypress open
```

- Via le terminal avec :

```sh
yarn cypress run
```

## Guidelines

- Utiliser les media queries en "desktop first" et avec la notation suivante avec les [valeurs de points de rupture du DSFR](https://www.systeme-de-design.gouv.fr/elements-d-interface/fondamentaux-techniques/grille-et-points-de-rupture) :
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ function copyAuditLink(uniqueId: string) {
}
function copyReportLink(uniqueId: string) {
const url = `${window.location.origin}/rapports/${uniqueId}`;
const url = `${window.location.origin}/rapport/${uniqueId}`;
navigator.clipboard.writeText(url).then(() => {
notify(
Expand Down Expand Up @@ -174,8 +174,8 @@ function copyStatementLink(uniqueId: string) {
'fr-badge--purple-glycine': isInProgress || isNotStarted
}"
>
<span class="fr-sr-only">Statut </span>
{{ isInProgress || isNotStarted ? "En cours" : "Terminé" }}
<span class="fr-sr-only">Statut </span
>{{ isInProgress || isNotStarted ? "En cours" : "Terminé" }}
</p>

<!-- Creation date -->
Expand Down Expand Up @@ -273,7 +273,7 @@ function copyStatementLink(uniqueId: string) {
? "Continuer l’audit"
: "Voir le rapport"
}}
<span v-if="isInProgress" class="fr-sr-only">
<span v-if="isInProgress || isNotStarted" class="fr-sr-only">
{{ audit.procedureName }}</span
>
<span v-else class="fr-sr-only"
Expand Down
3 changes: 1 addition & 2 deletions confiture-web-app/src/components/audit/AraTabs.vue
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,7 @@ watch(currentTab, (currentTab) => {
@keydown.home.prevent="selectFirstTab"
@keydown.end.prevent="selectLastTab"
>
<LayoutIcon v-if="i === 0" class="fr-mr-2v" />
{{ tab.label }}
<LayoutIcon v-if="i === 0" class="fr-mr-2v" />{{ tab.label }}
</button>
</li>
</ul>
Expand Down
1 change: 0 additions & 1 deletion confiture-web-app/src/components/audit/PagesSample.vue
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,6 @@ function updatePageOrder(startIndex: number, endIndex: number) {
class="fr-btn fr-btn--tertiary-no-outline"
type="button"
:disabled="pages.length === 1"
data-cy="delete"
@click="deletePage(i)"
>
Supprimer
Expand Down
4 changes: 3 additions & 1 deletion confiture-web-app/src/pages/report/ReportPage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@ const tabs = computed(() => [
const showCopyAlert = ref(false);
async function copyReportUrl() {
const url = `${window.location.origin}/rapports/${uniqueId}`;
const url =
window.location.origin +
router.resolve({ name: "report", params: { uniqueId } }).fullPath;
navigator.clipboard.writeText(url).then(() => {
showCopyAlert.value = true;
Expand Down
11 changes: 7 additions & 4 deletions confiture-web-app/src/store/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,14 @@ export const useAccountStore = defineStore("account", {
let authToken = localStorage.getItem(AUTH_TOKEN_STORAGE_KEY) ?? null;

if (authToken) {
const payload = jwtDecode(authToken) as AuthenticationJwtPayload;
try {
const payload = jwtDecode(authToken) as AuthenticationJwtPayload;

const isTokenExpired = payload.exp * 1000 < Date.now();

if (isTokenExpired) {
const isTokenExpired = payload.exp * 1000 < Date.now();
if (isTokenExpired) {
throw "tokenIsExpired";
}
} catch {
authToken = null;
localStorage.removeItem(AUTH_TOKEN_STORAGE_KEY);
}
Expand Down
1 change: 1 addition & 0 deletions cypress/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
downloads/
38 changes: 38 additions & 0 deletions cypress/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Tests end-to-end

[Cypress](https://www.cypress.io/) est utilisé pour lancer des tests end-to-end (e2e) dans un navigateur pour reproduire le comportement des utilisateurs.

En parallèle des tests, le serveur back-end doit être lancé avec la variable `DEBUG_ENDPOINTS` à `1` :

```sh
DEBUG_ENDPOINTS=1 yarn start:dev
```

Différentes données (audit, rapport ou compte) sont créées avant chaque test et de manière indépendante. Il est possible d’ajouter des options en fonction de la donnée souhaitée avec les fonctions [`createTestAccount` et `createTestAudit()`](/cypress/support/commands.ts).

Les tests peuvent être lancés de 2 manières, depuis la racine du projet :

- Via l’application Cypress avec :

```sh
yarn tests:open
```

- Via le terminal avec :

```sh
yarn tests:run
```

## Guidelines

- Cypress ne supporte pas la gestion des liens externes avec `target="_blank"`. Dans ce cas, il faut ajouter `.invoke("removeAttr", "target")` sur le lien avant de cliquer dessus. Exemple :

```js
cy.contains("a", "Compléter").invoke("removeAttr", "target").click();
```

- Il est préférable d'indique le type d’élément ciblé avec `contains()` pour s’assurer qu’il ne s’agisse pas d’un autre texte sur la page. Exemple :
```js
cy.contains("button", "Valider les paramètres").click();
```
Loading

0 comments on commit 9435124

Please sign in to comment.