diff --git a/.github/workflows/unit-tests-push.yml b/.github/workflows/unit-tests-push.yml new file mode 100644 index 00000000..11deb24d --- /dev/null +++ b/.github/workflows/unit-tests-push.yml @@ -0,0 +1,61 @@ +name: Application tests + +on: + push: + branches: + - master + - unit-tests + - develop + pull_request: + types: [opened, synchronize, reopened] + +jobs: + app-tests-analyze: + runs-on: ubuntu-latest + services: + mysql: + image: mysql:latest + env: + MYSQL_ROOT_PASSWORD: root + MYSQL_DATABASE: test_database + ports: + - 3306:3306 + options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 + + steps: + - uses: actions/checkout@v2 + + - name: Cache Maven packages + uses: actions/cache@v2 + with: + path: | + ~/.m2 + key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} + restore-keys: | + ${{ runner.os }}-m2 + + - name: Set up JDK 17 + uses: actions/setup-java@v1 + with: + java-version: '17' + + - name: Add exec permission to mvnw + run: chmod +x mvnw + + - name: Compile the application + run: ./mvnw -B clean install -DskipTests=true + + - name: Start the application + run: ./mvnw spring-boot:run & + env: + SPRING_DATASOURCE_URL: jdbc:mysql://localhost:3306/test_database + SPRING_DATASOURCE_USERNAME: root + SPRING_DATASOURCE_PASSWORD: root + SPRING_DATASOURCE_DRIVER_CLASS_NAME: com.mysql.cj.jdbc.Driver + + - name: Run all tests with sonar analysis + run: | + ./mvnw -B verify sonar:sonar -Dsonar.projectKey=Arquisoft_wiq_es04b -Dsonar.organization=arquisoft -Dsonar.branch.name=${{ github.ref }} -Dsonar.host.url=https://sonarcloud.io -Dsonar.login=${{ secrets.SONAR_TOKEN }} -Dspring.profiles.active=test -Dspring.datasource.url=jdbc:mysql://localhost:3306/test_database -Dspring.datasource.username=root -Dspring.datasource.password=root -Dspring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver + env: + SPRING_PROFILES_ACTIVE: test + headless: true \ No newline at end of file diff --git a/README.md b/README.md index 061b7c03..fd46ddc7 100644 --- a/README.md +++ b/README.md @@ -4,128 +4,6 @@ [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=Arquisoft_wiq_es04b&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=Arquisoft_wiq_es04b) [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=Arquisoft_wiq_es04b&metric=coverage)](https://sonarcloud.io/summary/new_code?id=Arquisoft_wiq_es04b) -This is a base repo for the [Software Architecture course](http://arquisoft.github.io/) -in [2023/2024 edition](https://arquisoft.github.io/course2324.html). - -This repo is a basic application composed of several components. - -- **Gateway service**. Express service that is exposed to the public and serves as a proxy to the two previous ones. -- **User service**. Express service that handles the insertion of new users in the system. -- **Auth service**. Express service that handles the authentication of users. -- **Webapp**. React web application that uses the gateway service to allow basic login and new user features. - -Both the user and auth service share a Mongo database that is accessed with mongoose. - -## Quick start guide - -### Using docker - -The fastest way for launching this sample project is using docker. Just clone the project: - -```sh -git clone https://github.com/Arquisoft/wiq_es04b.git -``` - -and launch it with docker compose: - -```sh -docker compose --profile dev up --build -``` - -### Starting Component by component - -First, start the database. Either install and run Mongo or run it using docker: - -```docker run -d -p 27017:27017 --name=my-mongo mongo:latest``` - -You can also use services like Mongo Altas for running a Mongo database in the cloud. - -Now, launch the auth, user and gateway services. Just go to each directory and run `npm install` followed -by `npm start`. - -Lastly, go to the webapp directory and launch this component with `npm install` followed by `npm start`. - -After all the components are launched, the app should be available in localhost in port 3000. - -## Deployment - -For the deployment, we have several options. - -The first and more flexible is to deploy to a virtual machine using SSH. This will work with any cloud service (or with -our own server). - -Other options include using the container services that most cloud services provide. This means, deploying our Docker -containers directly. - -We are going to use the first approach, creating a virtual machine in a cloud service and after installing docker and -docker-compose, deploy our containers there using GitHub Actions and SSH. - -### Machine requirements for deployment - -The machine for deployment can be created in services like Microsoft Azure or Amazon AWS. These are in general the -settings that it must have: - -- Linux machine with Ubuntu > 20.04. -- Docker and docker-compose installed. -- Open ports for the applications installed (in this case, ports 3000 for the webapp and 8000 for the gateway service). - -Once you have the virtual machine created, you can install **docker** and **docker-compose** using the following -instructions: - -```ssh -sudo apt update -sudo apt install apt-transport-https ca-certificates curl software-properties-common -curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add - -sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu focal stable" -sudo apt update -sudo apt install docker-ce -sudo usermod -aG docker ${USER} -sudo curl -L "https://github.com/docker/compose/releases/download/1.28.5/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose -sudo chmod +x /usr/local/bin/docker-compose -``` - -### Continuous delivery (GitHub Actions) - -Once we have our machine ready, we could deploy by hand the application, taking our docker-compose file and executing it -in the remote machine. - -In this repository, this process is done automatically using **GitHub Actions**. The idea is to trigger a series of -actions when some condition is met in the repository. - -As you can see, unitary tests of each module and e2e tests are executed before pushing the docker images and deploying -them. Using this approach we avoid deploying versions that do not pass the tests. - -The deploy action is the following: - -```yml -deploy: - name: Deploy over SSH - runs-on: ubuntu-latest - needs: [docker-push-userservice,docker-push-authservice,docker-push-gatewayservice,docker-push-webapp] - steps: - - name: Deploy over SSH - uses: fifsky/ssh-action@master - with: - host: ${{ secrets.DEPLOY_HOST }} - user: ${{ secrets.DEPLOY_USER }} - key: ${{ secrets.DEPLOY_KEY }} - command: | - wget https://raw.githubusercontent.com/arquisoft/wiq_es04b/master/docker-compose-deploy.yml -O docker-compose.yml - wget https://raw.githubusercontent.com/arquisoft/wiq_es04b/master/.env -O .env - docker compose down - docker compose --profile prod up -d -``` - -This action uses three secrets that must be configured in the repository: - -- DEPLOY_HOST: IP of the remote machine. -- DEPLOY_USER: user with permission to execute the commands in the remote machine. -- DEPLOY_KEY: key to authenticate the user in the remote machine. - -Note that this action logs in the remote machine and downloads the docker-compose file from the repository and launches -it. Obviously, previous actions have been executed which have uploaded the docker images to the GitHub Packages -repository. - ### 🚀 TEAM - **Daniel Alvarez Blanco** diff --git a/pom.xml b/pom.xml index 86487ac8..e604b79b 100644 --- a/pom.xml +++ b/pom.xml @@ -72,7 +72,22 @@ commons-validator 1.7 - + + org.seleniumhq.selenium + selenium-java + 4.1.0 + + + + io.github.bonigarcia + webdrivermanager + 5.0.3 + + + org.apache.httpcomponents.client5 + httpclient5 + 5.1 + diff --git a/src/main/java/com/uniovi/services/InsertSampleDataService.java b/src/main/java/com/uniovi/services/InsertSampleDataService.java index 71253e4e..9248c2fb 100644 --- a/src/main/java/com/uniovi/services/InsertSampleDataService.java +++ b/src/main/java/com/uniovi/services/InsertSampleDataService.java @@ -19,6 +19,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.context.event.EventListener; +import org.springframework.core.env.Environment; import org.springframework.stereotype.Service; import java.time.LocalDateTime; @@ -31,22 +32,29 @@ public class InsertSampleDataService { private final CategoryService categoryService; private final QuestionRepository questionRepository; private final GameSessionRepository gameSessionRepository; + private Environment environment; private Logger log = LoggerFactory.getLogger(InsertSampleDataService.class);; public InsertSampleDataService(PlayerService playerService, QuestionService questionService, CategoryService categoryService, QuestionRepository questionRepository, - GameSessionRepository gameSessionRepository) { + GameSessionRepository gameSessionRepository, Environment environment) { this.playerService = playerService; this.questionService = questionService; this.categoryService = categoryService; this.questionRepository = questionRepository; this.gameSessionRepository = gameSessionRepository; + this.environment = environment; } @Transactional @EventListener(ApplicationReadyEvent.class) // Uncomment this line to insert sample data on startup public void insertSampleQuestions() { + if (Arrays.stream(environment.getActiveProfiles()).anyMatch(env -> (env.equalsIgnoreCase("test")))) { + log.info("Test profile active, skipping sample data insertion"); + return; + } + if (!playerService.getUserByEmail("test@test.com").isPresent()) { PlayerDto player = new PlayerDto(); player.setEmail("test@test.com"); diff --git a/src/test/java/com/uniovi/WiqEs04bApplicationTests.java b/src/test/java/com/uniovi/WiqEs04bApplicationTests.java deleted file mode 100644 index 35eacaa0..00000000 --- a/src/test/java/com/uniovi/WiqEs04bApplicationTests.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.uniovi; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -class WiqEs04bApplicationTests { - - @Test - void contextLoads() { - } - -} diff --git a/src/test/java/com/uniovi/Wiq_IntegrationTests.java b/src/test/java/com/uniovi/Wiq_IntegrationTests.java new file mode 100644 index 00000000..074e43e3 --- /dev/null +++ b/src/test/java/com/uniovi/Wiq_IntegrationTests.java @@ -0,0 +1,61 @@ +package com.uniovi; + +import io.github.bonigarcia.wdm.WebDriverManager; +import org.junit.jupiter.api.*; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.chrome.ChromeDriver; +import org.openqa.selenium.firefox.FirefoxDriver; +import org.openqa.selenium.firefox.FirefoxOptions; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.core.env.Environment; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ActiveProfiles; + +import static org.junit.jupiter.api.Assertions.fail; + +@SpringBootTest +@Tag("integration") +@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +@ActiveProfiles("test") +class Wiq_IntegrationTests { + static final String URL = "http://localhost:3000/"; + + static WebDriver driver; + + @Autowired + Environment env; + + @BeforeEach + public void begin() { + if (driver == null) { + WebDriverManager.firefoxdriver().setup(); + if (env.getProperty("headless") != null && env.getProperty("headless").equals("true")) { + FirefoxOptions options = new FirefoxOptions(); + options.addArguments("--headless"); + driver = new FirefoxDriver(options); + } else { + driver = new FirefoxDriver(); + } + } + driver.navigate().to(URL); + } + + @AfterEach + void tearDown() { + driver.manage().deleteAllCookies(); + } + + @AfterAll + public static void end() { + driver.quit(); + } + + @Test + @Order(1) + void testHome() { + // Check the title + Assertions.assertEquals("Wikigame", driver.getTitle()); + } +} diff --git a/src/test/java/com/uniovi/Wiq_UnitTests.java b/src/test/java/com/uniovi/Wiq_UnitTests.java new file mode 100644 index 00000000..69e128cc --- /dev/null +++ b/src/test/java/com/uniovi/Wiq_UnitTests.java @@ -0,0 +1,22 @@ +package com.uniovi; + +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ActiveProfiles; + +@SpringBootTest +@Tag("unit") +@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +@ActiveProfiles("test") +class Wiq_UnitTests { + + @Test + void contextLoads() { + } + +} diff --git a/src/test/java/com/uniovi/util/SeleniumUtils.java b/src/test/java/com/uniovi/util/SeleniumUtils.java new file mode 100644 index 00000000..21723d3b --- /dev/null +++ b/src/test/java/com/uniovi/util/SeleniumUtils.java @@ -0,0 +1,119 @@ +package com.uniovi.util; + + +import org.junit.jupiter.api.Assertions; +import org.openqa.selenium.By; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.support.ui.ExpectedConditions; +import org.openqa.selenium.support.ui.WebDriverWait; + +import java.time.Duration; +import java.util.List; + +public class SeleniumUtils { + /** + * Aborta si el "texto" no está presente en la página actual + * @param driver: apuntando al navegador abierto actualmente. + * @param text: texto a buscar + */ + static public void textIsPresentOnPage(WebDriver driver, String text) + { + List list = driver.findElements(By.xpath("//*[contains(text(),'" + text + "')]")); + Assertions.assertTrue(list.size() > 0, "Texto " + text + " no localizado!"); + } + + /** + * Aborta si el "texto" está presente en la página actual + * @param driver: apuntando al navegador abierto actualmente. + * @param text: texto a buscar + */ + static public void textIsNotPresentOnPage(WebDriver driver, String text) + { + List list = driver.findElements(By.xpath("//*[contains(text(),'" + text + "')]")); + Assertions.assertEquals(0, list.size(), "Texto " + text + " no está presente !"); + } + + /** + * Aborta si el "texto" está presente en la página actual tras timeout segundos. + * @param driver: apuntando al navegador abierto actualmente. + * @param text: texto a buscar + * @param timeout: el tiempo máximo que se esperará por la aparición del texto a buscar + */ + static public void waitTextIsNotPresentOnPage(WebDriver driver, String text, int timeout) + { + Boolean resultado = + (new WebDriverWait(driver, Duration.ofSeconds(timeout))).until(ExpectedConditions.invisibilityOfElementLocated(By.xpath("//*[contains(text(),'" + text + "')]"))); + + Assertions.assertTrue(resultado); + } + + + /** + * Espera por la visibilidad de un elemento/s en la vista actualmente cargandose en driver. Para ello se empleará una consulta xpath. + * @param driver: apuntando al navegador abierto actualmente. + * @param xpath: consulta xpath. + * @param timeout: el tiempo máximo que se esperará por la aparición del elemento a buscar con xpath + * @return Se retornará la lista de elementos resultantes de la búsqueda con xpath. + */ + static public List waitLoadElementsByXpath(WebDriver driver, String xpath, int timeout) + { + WebElement result = + (new WebDriverWait(driver, Duration.ofSeconds(timeout))).until(ExpectedConditions.visibilityOfElementLocated(By.xpath(xpath))); + Assertions.assertNotNull(result); + return driver.findElements(By.xpath(xpath)); + } + + /** + * Espera por la visibilidad de un elemento/s en la vista actualmente cargandose en driver. Para ello se empleará una consulta xpath + * según varios criterios.. + * + * @param driver: apuntando al navegador abierto actualmente. + * @param criterio: "id" or "class" or "text" or "@attribute" or "free". Si el valor de criterio es free es una expresion xpath completa. + * @param text: texto correspondiente al criterio. + * @param timeout: el tiempo máximo que se esperará por la apareción del elemento a buscar con criterio/text. + * @return Se retornará la lista de elementos resultantes de la búsqueda. + */ + static public List waitLoadElementsBy(WebDriver driver, String criterio, String text, int timeout) + { + String searchCriterio; + switch (criterio) { + case "id": + searchCriterio = "//*[contains(@id,'" + text + "')]"; + break; + case "class": + searchCriterio = "//*[contains(@class,'" + text + "')]"; + break; + case "text": + searchCriterio = "//*[contains(text(),'" + text + "')]"; + break; + case "free": + searchCriterio = text; + break; + default: + searchCriterio = "//*[contains(" + criterio + ",'" + text + "')]"; + break; + } + + return waitLoadElementsByXpath(driver, searchCriterio, timeout); + } + + + /** + * PROHIBIDO USARLO PARA VERSIÓN FINAL. + * Esperar "segundos" durante la ejecucion del navegador + * @param driver: apuntando al navegador abierto actualmente. + * @param seconds: Segundos de bloqueo de la ejecución en el navegador. + */ + static public void waitSeconds(WebDriver driver, int seconds){ + + //noinspection SynchronizationOnLocalVariableOrMethodParameter + synchronized(driver){ + try { + driver.wait(seconds * 1000L); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } +}