Compare commits

...

8 Commits

12 changed files with 274 additions and 318 deletions

View File

@ -1,5 +1,6 @@
{ {
"conventionalCommits.scopes": [ "conventionalCommits.scopes": [
"Cypress" "Cypress",
"Cypress-Gherkin"
] ]
} }

View File

@ -1,13 +1,25 @@
# Lifen Autotest Hangout with Simon Priet # Lifen Autotest Hangout with Simon Priet
Description Please find in this project my hangout of your technical test. The test is performed with `cypress`, and include `cypress-cucumber-preprocessor`. This module process `.feature` files (they carry the test case in Gherkin syntax) and find matching `.js` files to run them by cypress runner. I initially coded cypress to do the part 2 only, but took time to include the part one as well. Now cypress run the api to generate the communication with the document, then connect the user to send the mail.
# Installation Currently, cypress is still challenged with a SMS code, and I have not implemented a workaround. Cypress wait 20 for me to put the code in the GUI, then continue. Please feel free to reconfigure your environment with your own phone number to try out. **However**, this automate can't be industrialized into a CI/CD pipeline.
The code has been written with VSC in mind, so a launch.json is available to help run and debug the softwares. No helpers are available for other IDE. I fond 2 tools that can help us more:
* [`cypress-recurse` 🌐](https://github.com/bahmutov/cypress-recurse) can retry several time a request until a certain condition is meet. This can help us wait for Irène to process the CommunicationRequest (`draft``suspended`), before going any further.
* [`cypress-mailosaur` 🌐](https://mailosaur.com/docs/frameworks-and-tools/cypress/) is a mail and text provider that can help us read texts content sent by the server to a predefined phone number, and work around the text code challenge.
Please note the existance of a 'pure cypress' branch on this repo that doesn't rely on BDD.
## Installation
The code has been written with VSC in mind, on a Windows 10 machine, so a `launch.json` configuration file is available to help run and debug the software. No helpers are available for other IDE.
1. Clone the repo to your local computer. You will need the following requierement: 1. Clone the repo to your local computer. You will need the following requierement:
* Node.js 16 * Node.js 16
* Git * Git
2. In VS code, run `Test Cypress via NPM`. The `Launch.json` file will trigger the `test` script configured in the `package.json`. 2. Open a terminal in your project folder
5. Then, type `npm run test` to launch the script via CLI. 2. Purge the `node_module` folder ( `rm -rf ./node_modules/`)
3. Run `npm install` to reinstall cypress and it's dependences according to your OS's environment. npm will use the `package.json` to find what to do.
2. Type `npm run test` to launch the script. In VS code, run `Test Cypress via NPM` from the *Run and Debug* tab. The `Launch.json` file will trigger the `test` script configured in the `package.json`.
6. On the cypress GUI, select `sendDocument` for the throughout test, and `inbox` for some subtests in the Inbox website.

View File

@ -2,5 +2,7 @@
"baseUrl": "https://app.post-prod.lifen.fr", "baseUrl": "https://app.post-prod.lifen.fr",
"env": { "env": {
"MAILOSAUR_API_KEY": "your-key-here" "MAILOSAUR_API_KEY": "your-key-here"
} },
"testFiles": "**/*.feature",
"ignoreTestFiles": "**/*.spec.js"
} }

View File

@ -1,5 +0,0 @@
{
"login": "candidature-qa@example.org",
"mdp": "kHwWawhH5ADNuFb",
"phone": "0643779588"
}

View File

@ -1,208 +0,0 @@
/// <reference types="cypress" />
describe("it test the api", () => {
let jsonWebToken = "";
const apiUrl = "https://fhir-api.public.post-prod.lifen.fr";
let documentReferenceId, communicationRequestId, patientId = "";
it("I have a JWT", () => {
cy.request({
url: "https://lifen-post-prod.eu.auth0.com/oauth/token",
method: "POST",
body: {
client_id: 'szVsMPaDPUdwqngGiLoHfXFT5XCYPFcy',
client_secret: 'sMp2PB-QWBZupCB4IXJrWDJ-7tlpUl09vQ2tMKdy049He7-g93ofbXz7ESlAc82B',
audience: 'post-prod-apis',
grant_type: 'client_credentials',
}
}).its('body').then((body) => {
jsonWebToken = body.access_token;
//cy.log(json_web_token);
expect(jsonWebToken).to.not.be.empty;
});
});
it("I have a the document #1615660", () => {
cy.request({
url:`${apiUrl}/fhir/Binary/1615660`,
method:'GET',
headers: {
"authorization" : `Bearer ${jsonWebToken}`,
"Prefer": "return=representation"
}
}).then((response) => {
expect(response.status).to.eq(200);
expect(response.headers["content-type"]).to.eq("application/pdf");
})
});
it("I prepare the DocumentReference", () => {
cy.request({
url:`${apiUrl}/fhir/DocumentReference`,
method:'POST',
headers: {
authorization : `Bearer ${jsonWebToken}`,
'content-type': 'application/json',
"Prefer": "return=representation"
},
json: true,
body: {
"status": "current",
"docStatus": "final",
"type": {
"coding": [
{
"system": "http://loinc.org",
"code": "34109-9",
"display": "Document médical"
}
]
},
"indexed": "2021-02-11T09:25:39Z",
"description": "aelgain-copiepatient.pdf",
"content": [
{
"attachment": {
"contentType": "application/pdf",
"url": "Binary/1615660",
"title": "aelgain-copiepatient.pdf"
}
}
],
"resourceType": "DocumentReference"
}
}).then((response) => {
expect(response.status).to.eq(201);
expect(response.headers["content-type"]).to.eq("application/fhir+json;charset=UTF-8");
expect(response.body).to.have.property("resourceType", "DocumentReference");
expect(response.body).to.have.property('id');
documentReferenceId = response.body.id
});
//cy.log(documentReferenceId);
});
it("I prepare the CommunicationRequest", () => {
cy.request({
url: `${apiUrl}/fhir/CommunicationRequest`,
method: 'POST',
headers: {
authorization: `Bearer ${jsonWebToken}`,
'content-type': 'application/json',
"Prefer": "return=representation"
},
json: true,
body: {
"meta": {
"tag": [
{
"system": "http://lifen.fr/fhir/tag/verified/sender",
"code": "SENDER_VERIFIED",
"display": "Sender is verified and should not be changed."
},
{
"system": "http://lifen.fr/fhir/tag/processing/mode",
"code": "IMMEDIATE_MODE",
"display": "request should be treated in immediate mode"
},
{
"system": "http://lifen.fr/fhir/CodeSystem/Resource/Tag/PatientAutomaticResending",
"code": "NONE",
"display": "Patient communication should not be resent"
}
]
},
"status": "draft",
"category": [
{
"coding": [
{
"system": "http://lifen.fr/fhir/CodeSystem/communication-category",
"code": "MEDICAL_REPORT"
}
]
}
],
"priority": "routine",
"payload": [
{
"contentReference": {
"reference": `DocumentReference/${documentReferenceId}`
}
}
],
"sender": {
"extension": [
{
"url": "http://lifen.fr/fhir/StructureDefinition/Resource/Extension/Source",
"valueCode": "USER"
}
],
"reference": "Organization/2"
},
"requester": {
"agent": {
"extension": [
{
"url": "http://lifen.fr/fhir/StructureDefinition/communicationrequest-requester-user-uuid",
"valueString": "ea07d7c6-ff4b-43fc-9765-e4235886d30c"
}
],
"reference": "Organization/2"
}
},
"resourceType": "CommunicationRequest"
}
}).then((response) => {
expect(response.status).to.eq(201);
expect(response.headers["content-type"]).to.eq("application/fhir+json;charset=UTF-8");
expect(response.body).to.have.property("resourceType", "CommunicationRequest");
expect(response.body).to.have.property('id');
expect(response.body).to.have.property('status', 'draft');
communicationRequestId = response.body.id
});
});
it("I fetch the CommunicationRequest's PatientId after waiting 10 sec for the IA to process it", () => {
cy.wait(10000);
// avec cypress-recurse, on surveille jusqu'a ce que l'IA ai finit son traitement ; plutot que d'attendre un temps arbitraire.
// import { recurse } from 'cypress-recurse' // a placer en début de fichier.
// recurse(() => cy.request(...), (response) => { expect(response.body).to.have.property('status', 'suspended') } ).then((response) => {...})
cy.request({
url: `${apiUrl}/fhir/CommunicationRequest/${communicationRequestId}`,
method: 'GET',
headers: {
authorization: `Bearer ${jsonWebToken}`
}
}).then((response) => {
expect(response.status).to.eq(200);
expect(response.headers["content-type"]).to.eq("application/fhir+json;charset=UTF-8");
expect(response.body).to.have.property("resourceType", "CommunicationRequest");
expect(response.body).to.have.property('id');
expect(response.body).to.have.property('status', 'suspended');
expect(response.body).to.have.property('subject');
expect(response.body).to.have.property('recipient');
expect(response.body.subject).to.have.property('reference');
let patient = response.body.subject.reference;
patientId = patient.split("/")[1];
});
});
it("I get the Patient", () => {
cy.request({
url: `${apiUrl}/fhir/Patient/${patientId}`,
method: 'GET',
headers: {
authorization: `Bearer ${jsonWebToken}`
}
}).then((response) => {
expect(response.status).to.eq(200);
expect(response.headers["content-type"]).to.eq("application/fhir+json;charset=UTF-8");
expect(response.body).to.have.property("resourceType", "Patient");
expect(response.body).to.have.property('id', patientId);
});
});
});

View File

@ -1,7 +1,8 @@
Feature: Send a document that is ready Feature: Send a document that is ready
As a practiciant, I can send mails that i have prepared As a practiciant, I can send mails that i have prepared
Scenario: Send a document
Scenario: Send a prepared document
Given I am connected to my mailbox Given I am connected to my mailbox
And I have a document ready to send And I have a document ready to send
When I send the mail When I send the mail

View File

@ -1,77 +0,0 @@
/// <reference types="cypress" />
context('The login page', () => {
describe.skip('Test the login page', () => {
it('Access the webpage', function() {
cy.visit('/login');
cy.get('#email').should("have.attr", "type", "email");
cy.get('#password').should("have.attr", "type", "password");
//cy.get('#password').type(mdp);
});
it('Log a malformed email', function() {
cy.visit('/login');
cy.get('#email').type("not an email");
cy.get('#password').type("somepasseword {enter}");
cy.url().should('include', 'login?');
});
it('Log an unexisting user', function() {
cy.visit('/login');
cy.get('#email').type("toto@yopmail.com");
cy.get('#password').type("somepasseword {enter}");
cy.get('#error-message').should("have.class", "alert alert-danger").should("be.visible");
});
});
/* describe("It send an SMS", function() {
it('Log the candidate user', function(){
cy.visit('/');
cy.get('#email').type("candidature-qa@example.org");
cy.get('#password').type("kHwWawhH5ADNuFb");
cy.get('#continueButton').click();
cy.mailosaurGetMessage(serverId, {
sentTo: smsNumber
}).as('sms');
cy.wait(10000).then(() => {
cy.get('#validateButton').click();
});
});
});
*/
});
context("The inbox page", function() {
describe("It check the inbox content", function() {
before(() => {
cy.visit('/');
cy.get('#email').type("candidature-qa@example.org");
cy.get('#password').type("kHwWawhH5ADNuFb");
cy.get('#continueButton').click();
cy.wait(20000).then(() => {
cy.get('#validateButton').click();
});
});
it("It load the inbox page of a valid user", ()=> {
cy.visit("/request");
cy.get("#dashboard-dropzone tr:first-child td").eq(1).click();
cy.get("#sending-button").click();
});
it("It checks for messages in the inbox", () => {
});
it("Open the message for data that has been injected previously", () => {
});
});
});

View File

@ -1,7 +1,7 @@
/* global Given, When, Then */ /* global Given, When, Then */
import { Given, When, Then, And, Before } from "cypress-cucumber-preprocessor/steps"; import { Given, When, Then, And, Before } from "cypress-cucumber-preprocessor/steps";
describe("Send a document", () => { describe("Send a prepared document", () => {
Given("I am connected to my mailbox", () => { Given("I am connected to my mailbox", () => {
cy.visit('/'); cy.visit('/');

View File

@ -0,0 +1,14 @@
Feature: Prepare and Send the document #1615660
As a practiciant, I can prepare a document to send to my colleague and be helped by Irène to fill contextual data.
Scenario: Send a document
Given I use the API
And I have the document #1615660
And I have a DocumentReference
And I have a CommunicationRequest
And Irène prepared the CommunicationRequest
And I have a Patient
And I am connected to my mailbox
When I send the mail
Then the mail is sent

View File

@ -0,0 +1,232 @@
/* global Given, When, Then */
import { Given, When, Then, And, Before } from "cypress-cucumber-preprocessor/steps";
describe("Send a document", () => {
const apiUrl = "https://fhir-api.public.post-prod.lifen.fr";
let jsonWebToken, documentReferenceId, communicationRequestId, patientId = "";
Given("I use the API", () => {
cy.request({
url: "https://lifen-post-prod.eu.auth0.com/oauth/token",
method: "POST",
body: {
client_id: 'szVsMPaDPUdwqngGiLoHfXFT5XCYPFcy',
client_secret: 'sMp2PB-QWBZupCB4IXJrWDJ-7tlpUl09vQ2tMKdy049He7-g93ofbXz7ESlAc82B',
audience: 'post-prod-apis',
grant_type: 'client_credentials',
}
}).its('body').then((body) => {
jsonWebToken = body.access_token;
//cy.log(json_web_token);
expect(jsonWebToken).to.not.be.empty;
});
})
And("I have the document #1615660", () => {
cy.request({
url: `${apiUrl}/fhir/Binary/1615660`,
method: 'GET',
headers: {
"authorization": `Bearer ${jsonWebToken}`,
"Prefer": "return=representation"
}
}).then((response) => {
expect(response.status).to.eq(200);
expect(response.headers["content-type"]).to.eq("application/pdf");
});
});
And("I have a DocumentReference", () => {
cy.request({
url: `${apiUrl}/fhir/DocumentReference`,
method: 'POST',
headers: {
authorization: `Bearer ${jsonWebToken}`,
'content-type': 'application/json',
"Prefer": "return=representation"
},
json: true,
body: {
"status": "current",
"docStatus": "final",
"type": {
"coding": [
{
"system": "http://loinc.org",
"code": "34109-9",
"display": "Document médical"
}
]
},
"indexed": "2021-02-11T09:25:39Z",
"description": "aelgain-copiepatient.pdf",
"content": [
{
"attachment": {
"contentType": "application/pdf",
"url": "Binary/1615660",
"title": "aelgain-copiepatient.pdf"
}
}
],
"resourceType": "DocumentReference"
}
}).then((response) => {
expect(response.status).to.eq(201);
expect(response.headers["content-type"]).to.eq("application/fhir+json;charset=UTF-8");
expect(response.body).to.have.property("resourceType", "DocumentReference");
expect(response.body).to.have.property('id');
documentReferenceId = response.body.id
});
});
And("I have a CommunicationRequest", () => {
cy.request({
url: `${apiUrl}/fhir/CommunicationRequest`,
method: 'POST',
headers: {
authorization: `Bearer ${jsonWebToken}`,
'content-type': 'application/json',
"Prefer": "return=representation"
},
json: true,
body: {
"meta": {
"tag": [
{
"system": "http://lifen.fr/fhir/tag/verified/sender",
"code": "SENDER_VERIFIED",
"display": "Sender is verified and should not be changed."
},
{
"system": "http://lifen.fr/fhir/tag/processing/mode",
"code": "IMMEDIATE_MODE",
"display": "request should be treated in immediate mode"
},
{
"system": "http://lifen.fr/fhir/CodeSystem/Resource/Tag/PatientAutomaticResending",
"code": "NONE",
"display": "Patient communication should not be resent"
}
]
},
"status": "draft",
"category": [
{
"coding": [
{
"system": "http://lifen.fr/fhir/CodeSystem/communication-category",
"code": "MEDICAL_REPORT"
}
]
}
],
"priority": "routine",
"payload": [
{
"contentReference": {
"reference": `DocumentReference/${documentReferenceId}`
}
}
],
"sender": {
"extension": [
{
"url": "http://lifen.fr/fhir/StructureDefinition/Resource/Extension/Source",
"valueCode": "USER"
}
],
"reference": "Organization/2"
},
"requester": {
"agent": {
"extension": [
{
"url": "http://lifen.fr/fhir/StructureDefinition/communicationrequest-requester-user-uuid",
"valueString": "ea07d7c6-ff4b-43fc-9765-e4235886d30c"
}
],
"reference": "Organization/2"
}
},
"resourceType": "CommunicationRequest"
}
}).then((response) => {
expect(response.status).to.eq(201);
expect(response.headers["content-type"]).to.eq("application/fhir+json;charset=UTF-8");
expect(response.body).to.have.property("resourceType", "CommunicationRequest");
expect(response.body).to.have.property('id');
expect(response.body).to.have.property('status', 'draft');
communicationRequestId = response.body.id
});
});
And("Irène prepared the CommunicationRequest", () => {
cy.wait(10000);
// avec cypress-recurse, on surveille jusqu'a ce que l'IA ai finit son traitement ; plutot que d'attendre un temps arbitraire.
// import { recurse } from 'cypress-recurse' // a placer en début de fichier.
// recurse(() => cy.request(...), (response) => { expect(response.body).to.have.property('status', 'suspended') } ).then((response) => {...})
cy.request({
url: `${apiUrl}/fhir/CommunicationRequest/${communicationRequestId}`,
method: 'GET',
headers: {
authorization: `Bearer ${jsonWebToken}`
}
}).then((response) => {
expect(response.status).to.eq(200);
expect(response.headers["content-type"]).to.eq("application/fhir+json;charset=UTF-8");
expect(response.body).to.have.property("resourceType", "CommunicationRequest");
expect(response.body).to.have.property('id');
expect(response.body).to.have.property('status', 'suspended');
expect(response.body).to.have.property('subject');
expect(response.body).to.have.property('recipient');
expect(response.body.subject).to.have.property('reference');
let patient = response.body.subject.reference;
patientId = patient.split("/")[1];
});
});
And("I have a Patient", () => {
cy.request({
url: `${apiUrl}/fhir/Patient/${patientId}`,
method: 'GET',
headers: {
authorization: `Bearer ${jsonWebToken}`
}
}).then((response) => {
expect(response.status).to.eq(200);
expect(response.headers["content-type"]).to.eq("application/fhir+json;charset=UTF-8");
expect(response.body).to.have.property("resourceType", "Patient");
expect(response.body).to.have.property('id', patientId);
});
});
And("I am connected to my mailbox", () => {
cy.visit('/');
cy.get('#email').type("candidature-qa@example.org");
cy.get('#password').type("kHwWawhH5ADNuFb");
cy.get('#continueButton').click();
cy.wait(20000).then(() => {
cy.get('#validateButton').click();
});
cy.visit("/request");
cy.get("#account-menu div[title]").should((elm) => {
expect(elm).to.contain("candidature-qa@example.org")
});
});
When("I send the mail", () => {
// Il n'y a pas de moyen fiable de lier la communicationRequestId que l'on a préparer avec l'une des entrée listée.
// Il y a bien un code LT-xxxxxxx, mais il faudrais un moyen d'atteindre de le modèle stoquée dans la vue
cy.get("#dashboard-dropzone tr:first-child td").eq(1).click();
cy.get("#sending-button").click();
});
Then("the mail is sent", () => {
//?
// On peux essayer d'attendre une requete de repondre un status
// On peux attendre la notification "Envoie du document réussit"
});
});

View File

@ -23,16 +23,3 @@
// //
// -- This will overwrite an existing command -- // -- This will overwrite an existing command --
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
import 'cypress-mailosaur';
Cypress.Commands.add('TokenLogin', (token) => {
const autorisation = `bearer ${ token }`;
cy.request({
method:'POST',
url:'/request',
headers: {
autorisation,
}
});
})

View File

@ -4,11 +4,8 @@
"description": "run cypress end-2-end tests against post-prod.lifen.fr", "description": "run cypress end-2-end tests against post-prod.lifen.fr",
"main": "index.js", "main": "index.js",
"dependencies": { "dependencies": {
"cypress": "^8.3.1" "cypress": "^8.3.1",
}, "cypress-cucumber-preprocessor": "^4.2.0"
"devDependencies": {
"cypress-cucumber-preprocessor": "^4.2.0",
"cypress-mailosaur": "^2.3.3"
}, },
"cypress-cucumber-preprocessor": { "cypress-cucumber-preprocessor": {
"nonGlobalStepDefinitions": true "nonGlobalStepDefinitions": true