├── .gitignore ├── .gitattributes ├── src ├── test │ ├── resources │ │ ├── application-test.properties │ │ └── features │ │ │ └── Resiliency-Scenarios.feature │ └── java │ │ ├── cucumber │ │ └── Runner.java │ │ ├── toxiproxy │ │ └── metric │ │ │ ├── MetricClient.java │ │ │ ├── Metric.java │ │ │ └── Metrics.java │ │ └── stepdefs │ │ ├── ToxiProxyHelper.java │ │ ├── ServiceHttpSteps.java │ │ └── ToxiProxySteps.java └── main │ ├── java │ └── com │ │ └── qabound │ │ └── spring │ │ ├── repository │ │ └── QuoteRepository.java │ │ ├── exception │ │ └── QuoteNotFoundException.java │ │ ├── model │ │ └── Quote.java │ │ ├── core │ │ └── CrudApplication.java │ │ ├── service │ │ └── QuoteService.java │ │ └── controller │ │ └── QuoteController.java │ └── resources │ ├── application.properties │ └── text ├── toxiproxyConfig.json ├── Docker-Compose.yaml ├── ReadMe.md ├── dump.sql ├── pom.xml └── ToxiProxyDemo.iml /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | /target/ 3 | target 4 | /.idea/ 5 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /src/test/resources/application-test.properties: -------------------------------------------------------------------------------- 1 | rest.svc=localhost:8080 2 | rest.endpoint.allQuotes=api/v1/quotes -------------------------------------------------------------------------------- /toxiproxyConfig.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "mysql", 4 | "listen": "[::]:3306", 5 | "upstream": "mysql:3306", 6 | "enabled": true, 7 | "toxics": [] 8 | } 9 | ] -------------------------------------------------------------------------------- /src/test/java/cucumber/Runner.java: -------------------------------------------------------------------------------- 1 | package cucumber; 2 | 3 | import io.cucumber.junit.Cucumber; 4 | import io.cucumber.junit.CucumberOptions; 5 | import org.junit.runner.RunWith; 6 | 7 | @RunWith(Cucumber.class) 8 | @CucumberOptions( 9 | features = {"src/test/resources/features"}, 10 | glue = {"stepdefs"} 11 | ) 12 | public class Runner { 13 | 14 | } -------------------------------------------------------------------------------- /src/main/java/com/qabound/spring/repository/QuoteRepository.java: -------------------------------------------------------------------------------- 1 | package com.qabound.spring.repository; 2 | 3 | 4 | import com.qabound.spring.model.Quote; 5 | import org.springframework.data.repository.CrudRepository; 6 | import org.springframework.stereotype.Repository; 7 | 8 | @Repository 9 | public interface QuoteRepository extends CrudRepository { 10 | } 11 | -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.jpa.hibernate.ddl-auto=update 2 | spring.datasource.url=jdbc:mysql://localhost:3306/${MYSQL_DATABASE:crud-application} 3 | spring.datasource.username=${MYSQL_USER:root} 4 | spring.datasource.password=${MYSQL_PASSWORD:password} 5 | spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect 6 | spring.jpa.hibernate.use-new-id-generator-mappings=false 7 | logging.level.org.hibernate.SQL=DEBUG 8 | 9 | -------------------------------------------------------------------------------- /src/main/java/com/qabound/spring/exception/QuoteNotFoundException.java: -------------------------------------------------------------------------------- 1 | package com.qabound.spring.exception; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.web.bind.annotation.ResponseStatus; 5 | 6 | @ResponseStatus(code = HttpStatus.NOT_FOUND, reason = "Quote Not Found") 7 | public class QuoteNotFoundException extends Exception { 8 | 9 | public QuoteNotFoundException(String message) { 10 | super(message); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/resources/text: -------------------------------------------------------------------------------- 1 | toxiproxy_proxy_received_bytes_total{direction="downstream",listener="[::]:3306",proxy="mysql",upstream="mysql:3306"} 15618 2 | toxiproxy_proxy_received_bytes_total{direction="upstream",listener="[::]:3306",proxy="mysql",upstream="mysql:3306"} 14291 3 | # HELP toxiproxy_proxy_sent_bytes_total 4 | # TYPE toxiproxy_proxy_sent_bytes_total counter 5 | toxiproxy_proxy_sent_bytes_total{direction="downstream",listener="[::]:3306",proxy="mysql",upstream="mysql:3306"} 14916 6 | toxiproxy_proxy_sent_bytes_total{direction="upstream",listener="[::]:3306",proxy="mysql",upstream="mysql:3306"} 14291 -------------------------------------------------------------------------------- /Docker-Compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | toxiproxy: 4 | image: "ghcr.io/shopify/toxiproxy" 5 | command: 6 | - -host=0.0.0.0 7 | - -proxy-metrics 8 | - -config=toxiproxyConfig.json 9 | volumes: 10 | - ./toxiproxyConfig.json:/toxiproxyConfig.json 11 | ports: 12 | - "8474:8474" 13 | - "3306:3306" 14 | mysql: 15 | image: mysql:5.6 16 | ports: 17 | - "3307:3306" 18 | environment: 19 | MYSQL_ROOT_PASSWORD: 'password' 20 | MYSQL_DATABASE: 'crud-application' 21 | volumes: 22 | - ./dump.sql:/docker-entrypoint-initdb.d/dump.sql -------------------------------------------------------------------------------- /src/main/java/com/qabound/spring/model/Quote.java: -------------------------------------------------------------------------------- 1 | package com.qabound.spring.model; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Builder; 5 | import lombok.Getter; 6 | import lombok.NoArgsConstructor; 7 | import lombok.Setter; 8 | 9 | import javax.persistence.Entity; 10 | import javax.persistence.GeneratedValue; 11 | import javax.persistence.GenerationType; 12 | import javax.persistence.Id; 13 | import java.time.Instant; 14 | 15 | @Getter 16 | @Setter 17 | @Builder 18 | @AllArgsConstructor 19 | @NoArgsConstructor 20 | @Entity 21 | public class Quote { 22 | @Id 23 | @GeneratedValue(strategy = GenerationType.AUTO) 24 | private Integer id; 25 | private String quote; 26 | private String source; 27 | @Builder.Default private long date = Instant.now().getEpochSecond(); 28 | } 29 | -------------------------------------------------------------------------------- /src/test/resources/features/Resiliency-Scenarios.feature: -------------------------------------------------------------------------------- 1 | Feature: Testing how the system responds under adverse conditions 2 | 3 | Scenario: A request sent while the database is down should result in a 500 4 | Given The SQL service timeouts after 2 millisecond 5 | When A request GET all request is sent to the service 6 | Then A 500 error should be returned 7 | And bytes have been received by proxy 8 | And bytes have not been sent to database 9 | 10 | 11 | Scenario: A post request sent while the connection timeouts during the response from the SQL server 12 | Given The SQL service timeouts after 2 millisecond upstream 13 | When A post request is sent to the service 14 | Then A 500 error should be returned 15 | And bytes have been received by proxy 16 | And bytes have been sent to database -------------------------------------------------------------------------------- /src/main/java/com/qabound/spring/core/CrudApplication.java: -------------------------------------------------------------------------------- 1 | package com.qabound.spring.core; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.boot.autoconfigure.domain.EntityScan; 6 | import org.springframework.context.annotation.ComponentScan; 7 | import org.springframework.data.jpa.repository.config.EnableJpaRepositories; 8 | 9 | @SpringBootApplication(scanBasePackages = "com.qabound.spring") 10 | @ComponentScan("com.qabound.spring") 11 | @EnableJpaRepositories("com.qabound.spring.repository") 12 | @EntityScan("com.qabound.spring.model") 13 | public class CrudApplication { 14 | 15 | public static void main(String[] args) { 16 | SpringApplication.run(CrudApplication.class, args); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/test/java/toxiproxy/metric/MetricClient.java: -------------------------------------------------------------------------------- 1 | package toxiproxy.metric; 2 | 3 | import io.restassured.RestAssured; 4 | import io.restassured.filter.log.RequestLoggingFilter; 5 | import io.restassured.filter.log.ResponseLoggingFilter; 6 | 7 | import java.util.ArrayList; 8 | import java.util.HashMap; 9 | import java.util.List; 10 | import java.util.Map; 11 | 12 | public class MetricClient { 13 | 14 | 15 | public static Metrics getMetric(String toxiUrl) { 16 | String response = RestAssured.given().filters(new RequestLoggingFilter(), new ResponseLoggingFilter()) 17 | .baseUri(toxiUrl).get("/metrics").body().asString(); 18 | String[] splitLine = response.split("\n"); 19 | 20 | Map> metricMap = new HashMap<>(); 21 | 22 | for (String line : splitLine) { 23 | if (line.startsWith(Metric.ToxiDirection.PROXY_RECIEVED.getLinePrefix()) || line.startsWith(Metric.ToxiDirection.PROXY_SENT.getLinePrefix())) { 24 | Metric metric = new Metric(line); 25 | if (!metricMap.containsKey(metric.getProxy())) { 26 | metricMap.put(metric.getProxy(), new ArrayList<>(List.of(metric))); 27 | } else { 28 | metricMap.get(metric.getProxy()).add(metric); 29 | } 30 | } 31 | } 32 | return new Metrics(metricMap); 33 | } 34 | 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/test/java/stepdefs/ToxiProxyHelper.java: -------------------------------------------------------------------------------- 1 | package stepdefs; 2 | 3 | import eu.rekawek.toxiproxy.Proxy; 4 | import eu.rekawek.toxiproxy.ToxiproxyClient; 5 | import lombok.Getter; 6 | import toxiproxy.metric.MetricClient; 7 | import toxiproxy.metric.Metrics; 8 | 9 | import java.io.IOException; 10 | 11 | public class ToxiProxyHelper { 12 | 13 | 14 | private final ToxiproxyClient client; 15 | @Getter 16 | private final Proxy mySqlProxy; 17 | 18 | public ToxiProxyHelper() { 19 | this.client = new ToxiproxyClient(); //defaults url to localhost:8474, but can be overriden 20 | this.mySqlProxy = createProxy("mysql", "localhost:3306", "mysql:3306"); 21 | } 22 | 23 | public Metrics getMetric() { 24 | return MetricClient.getMetric("http://localhost:8474"); 25 | } 26 | 27 | 28 | public void reset() { 29 | try { 30 | client.reset(); 31 | } catch (IOException e) { 32 | e.printStackTrace(); 33 | } 34 | } 35 | 36 | private Proxy createProxy(String name, String listenURl, String upStreamURL) { 37 | try { 38 | return client.createProxy(name, listenURl, upStreamURL); 39 | } catch (IOException e) { 40 | try { 41 | return client.getProxy(name); 42 | } catch (IOException ex) { 43 | ex.printStackTrace(); 44 | } 45 | } 46 | return null; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/com/qabound/spring/service/QuoteService.java: -------------------------------------------------------------------------------- 1 | package com.qabound.spring.service; 2 | 3 | 4 | import com.qabound.spring.exception.QuoteNotFoundException; 5 | import com.qabound.spring.model.Quote; 6 | import com.qabound.spring.repository.QuoteRepository; 7 | import lombok.AllArgsConstructor; 8 | import lombok.NoArgsConstructor; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.stereotype.Service; 11 | 12 | import java.util.Optional; 13 | 14 | @Service 15 | @AllArgsConstructor 16 | @NoArgsConstructor 17 | public class QuoteService { 18 | 19 | @Autowired 20 | private QuoteRepository quoteRepository; 21 | 22 | public Iterable getAll() { 23 | return quoteRepository.findAll(); 24 | } 25 | 26 | public Optional getById(String quoteId) { 27 | return quoteRepository.findById(Integer.parseInt(quoteId)); 28 | } 29 | 30 | public Quote save(Quote quote) { 31 | return quoteRepository.save(quote); 32 | } 33 | 34 | public Quote update(String quoteId, Quote quote) throws QuoteNotFoundException { 35 | Optional retrievedQuote = quoteRepository.findById(Integer.parseInt(quoteId)); 36 | if (retrievedQuote.isPresent()) { 37 | retrievedQuote.get().setQuote(quote.getQuote()); 38 | retrievedQuote.get().setSource(quote.getSource()); 39 | return quoteRepository.save(retrievedQuote.get()); 40 | } else { 41 | throw new QuoteNotFoundException("Quote Not Found"); 42 | } 43 | } 44 | 45 | public void delete(String quoteId) { 46 | quoteRepository.deleteById(Integer.parseInt(quoteId)); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/test/java/toxiproxy/metric/Metric.java: -------------------------------------------------------------------------------- 1 | package toxiproxy.metric; 2 | 3 | import lombok.Getter; 4 | import lombok.ToString; 5 | 6 | @Getter 7 | @ToString 8 | public class Metric { 9 | 10 | private String direction; 11 | private String listener; 12 | private String proxy; 13 | private String upstream; 14 | private int bytes; 15 | private ToxiDirection toxiDirection; 16 | 17 | 18 | public enum ToxiDirection { 19 | PROXY_RECIEVED("toxiproxy_proxy_received_bytes_total"), 20 | PROXY_SENT("toxiproxy_proxy_sent_bytes_total"); 21 | 22 | @Getter 23 | private final String linePrefix; 24 | 25 | ToxiDirection(String linePrefix) { 26 | this.linePrefix = linePrefix; 27 | 28 | } 29 | } 30 | 31 | private void populate(String line) { 32 | if (line.startsWith(ToxiDirection.PROXY_RECIEVED.getLinePrefix())) { 33 | this.toxiDirection = ToxiDirection.PROXY_RECIEVED; 34 | } else if (line.startsWith(ToxiDirection.PROXY_SENT.getLinePrefix())) { 35 | this.toxiDirection = ToxiDirection.PROXY_SENT; 36 | } else { 37 | throw new IllegalArgumentException(String.format("Start of string not as expected, string =%s", line)); 38 | } 39 | this.direction = line.split("direction=\"")[1].split("\"")[0]; 40 | this.listener = line.split("listener=\"")[1].split("\"")[0]; 41 | this.proxy = line.split("proxy=\"")[1].split("\"")[0]; 42 | this.upstream = line.split("upstream=\"")[1].split("\"")[0]; 43 | this.bytes = Integer.parseInt(line.split(" ")[1]); 44 | } 45 | 46 | public Metric(String line) { 47 | populate(line); 48 | } 49 | 50 | 51 | } 52 | -------------------------------------------------------------------------------- /ReadMe.md: -------------------------------------------------------------------------------- 1 | #Star Wars Quote Application - For ToxiProxy Demo 2 | (Built by cannibalising the project: https://github.com/emreozcan3320/star-wars-quote-web-application) 3 | 4 | This project is a simple SpringBoot project that interfaces with a MySQL database. 5 | 6 | The project has 5 endpoints 7 | 8 | * GET /api/v1/quotes - Get All Quotes 9 | * GET /{quoteId} - Get A singular Quote back if the ID matches 10 | * POST /api/v1/quotes - Add a Quote 11 | * PUT /{quoteId} - Edit a Quote 12 | * DELETE /{quoteId} - Delete a Quote 13 | 14 | The purpose of this repo is to demonstrate how ToxiProxy works in action. 15 | 16 | To start the dependencies navigate to the project directory and run 17 | 18 | `docker-compose down` 19 | then 20 | `docker-compose up` 21 | 22 | * Make note that in application.properties the MySQL url is localhost:3306, 23 | which is the port the ToxiProxy is configured on in the docker compose, 24 | to start up the application without any errors the proxy must be in present linking the localhost:3306 port to the localhost:3307 port. 25 | 26 | `curl --location --request POST 'http://localhost:8474/proxies' \ 27 | --header 'Content-Type: application/json' \ 28 | --data-raw '{ 29 | "name": "mysql", 30 | "listen": "[::]:3306", 31 | "upstream": "mysql:3306", 32 | "enabled": true, 33 | "toxics": [] 34 | }'` 35 | 36 | ^The above is always created on the 'docker-compose up' command as the toxiproxyConfig.json is mounted - 37 | To make changes to the proxy setup make changes in the toxiProxy config. 38 | 39 | 40 | Then run the class 41 | `src/main/java/com/qabound/spring/core/CrudApplication.java` 42 | 43 | Now you can run the feature file: 44 | `src/test/resources/features/Resiliency-Scenarios.feature` 45 | 46 | 47 | -------------------------------------------------------------------------------- /src/main/java/com/qabound/spring/controller/QuoteController.java: -------------------------------------------------------------------------------- 1 | package com.qabound.spring.controller; 2 | 3 | import com.qabound.spring.exception.QuoteNotFoundException; 4 | import com.qabound.spring.model.Quote; 5 | import com.qabound.spring.service.QuoteService; 6 | import lombok.AllArgsConstructor; 7 | import lombok.extern.log4j.Log4j2; 8 | import org.springframework.web.bind.annotation.*; 9 | 10 | import java.util.Optional; 11 | 12 | @AllArgsConstructor 13 | @RestController 14 | @CrossOrigin(origins = {"*"}) 15 | @RequestMapping("api/v1/quotes") 16 | @Log4j2 17 | public class QuoteController { 18 | 19 | private final QuoteService quoteService; 20 | 21 | @GetMapping 22 | public Iterable getAllQuotes() { 23 | log.info("/request recieved GET-ALL"); 24 | Iterable all = quoteService.getAll(); 25 | log.info("/response sent GET-ALL"); 26 | return all; 27 | } 28 | 29 | @GetMapping("/{quoteId}") 30 | public Quote getQuoteById(@PathVariable String quoteId) throws QuoteNotFoundException { 31 | Optional responseQuote = quoteService.getById(quoteId); 32 | if (responseQuote.isPresent()) { 33 | return responseQuote.get(); 34 | } else { 35 | throw new QuoteNotFoundException("Quote not found"); 36 | } 37 | } 38 | 39 | @PostMapping 40 | public Quote saveQuote(@RequestBody Quote quote) { 41 | return quoteService.save(quote); 42 | } 43 | 44 | @PutMapping("/{quoteId}") 45 | public Quote updateQuote(@PathVariable String quoteId, @RequestBody Quote quote) throws QuoteNotFoundException { 46 | return quoteService.update(quoteId, quote); 47 | } 48 | 49 | @DeleteMapping("/{quoteId}") 50 | public void deleteQuote(@PathVariable String quoteId) { 51 | quoteService.delete(quoteId); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/test/java/stepdefs/ServiceHttpSteps.java: -------------------------------------------------------------------------------- 1 | package stepdefs; 2 | 3 | import com.qabound.spring.model.Quote; 4 | import io.cucumber.java.en.Then; 5 | import io.cucumber.java.en.When; 6 | import io.restassured.RestAssured; 7 | import io.restassured.filter.log.RequestLoggingFilter; 8 | import io.restassured.filter.log.ResponseLoggingFilter; 9 | import io.restassured.http.ContentType; 10 | import io.restassured.response.Response; 11 | import io.restassured.specification.RequestSpecification; 12 | import org.junit.Assert; 13 | 14 | import java.time.Instant; 15 | 16 | public class ServiceHttpSteps { 17 | 18 | private final RequestSpecification restAssuredSpec; 19 | private String restSvcUrl = "http://localhost:8080"; 20 | private String getAllEndpoint = "api/v1/quotes"; 21 | private Response response; 22 | 23 | public ServiceHttpSteps() { 24 | this.restAssuredSpec = RestAssured.given().filters(new RequestLoggingFilter(), new ResponseLoggingFilter()).given(); 25 | } 26 | 27 | @When("A request GET all request is sent to the service") 28 | public void sentGetAllRequest() { 29 | this.response = RestAssured.given().filters(new RequestLoggingFilter(), new ResponseLoggingFilter()) 30 | .given().baseUri(restSvcUrl).get(getAllEndpoint); 31 | } 32 | 33 | @When("A post request is sent to the service") 34 | public void sentAddQuoteRequest() { 35 | var quote = Quote.builder() 36 | .quote("I have the high ground") 37 | .date(Instant.now().toEpochMilli()) 38 | .source("Star Wars: Episode III -- Revenge of the Sith") 39 | .build(); 40 | 41 | this.response = RestAssured 42 | .given() 43 | .filters(new RequestLoggingFilter(), new ResponseLoggingFilter()) 44 | .contentType(ContentType.JSON) 45 | .baseUri(restSvcUrl) 46 | .body(quote) 47 | .post(getAllEndpoint); 48 | } 49 | 50 | @Then("A {int} error should be returned") 51 | public void aErrorShouldBeReturned(int expectedStatusCode) { 52 | int respStatusCode = response.andReturn().statusCode(); 53 | Assert.assertEquals(expectedStatusCode, respStatusCode); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /dump.sql: -------------------------------------------------------------------------------- 1 | -- phpMyAdmin SQL Dump 2 | -- version 5.0.4 3 | -- https://www.phpmyadmin.net/ 4 | -- 5 | -- Host: db 6 | -- Generation Time: Nov 18, 2020 at 02:59 PM 7 | -- Server version: 8.0.22 8 | -- PHP Version: 7.4.12 9 | 10 | SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO"; 11 | START TRANSACTION; 12 | SET time_zone = "+00:00"; 13 | 14 | 15 | /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; 16 | /*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; 17 | /*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; 18 | /*!40101 SET NAMES utf8mb4 */; 19 | 20 | -- 21 | -- Database: `crud-application` 22 | -- 23 | 24 | -- -------------------------------------------------------- 25 | 26 | -- 27 | -- Table structure for table `quote` 28 | -- 29 | 30 | CREATE TABLE `quote` ( 31 | `id` int NOT NULL AUTO_INCREMENT PRIMARY KEY, 32 | `date` varchar(255) DEFAULT NULL , 33 | `quote` varchar(255) DEFAULT NULL, 34 | `source` varchar(255) DEFAULT NULL 35 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci; 36 | 37 | -- 38 | -- Dumping data for table `quote` 39 | -- 40 | 41 | INSERT INTO `quote` (`id`, `date`, `quote`, `source`) VALUES 42 | (7, '1605646652', 'Great leaders inspire greatness in others', 'Star Wars: The Clone Wars 01×01 – Ambush'), 43 | (8, '1605652409', 'Belief is not a matter of choice, but of conviction.', 'Star Wars: The Clone Wars 01×02 – Rising Malevolence'), 44 | (9, '1605652433', 'Easy is the path to wisdom for those not blinded by ego.', 'Star Wars: The Clone Wars 01×03 – Shadow of Malevolence'), 45 | (10, '1605652447', 'A plan is only as good as those who see it through.', 'Star Wars: The Clone Wars 01×04 – Destroy Malevolence'), 46 | (11, '1605652458', 'The best confidence builder is experience.', 'Star Wars: The Clone Wars 01×05 – Rookies'), 47 | (12, '1605652468', 'Trust in your friends, and they’ll have reason to trust in you.', 'Star Wars: The Clone Wars 01×06 – Downfall of a Droid'), 48 | (22, '1605711409', 'You hold onto friends by keeping your heart a little softer than your head.', 'Star Wars: The Clone Wars 01×07 – Duel of the Droids'), 49 | (23, '1605711453', 'Heroes are made by the times.', 'Star Wars: The Clone Wars 01×08 – Bombad Jedi'), 50 | (24, '1605711485', 'Ignore your instincts at your peril.', 'Star Wars: The Clone Wars 01×09 – Cloak of Darkness'), 51 | (25, '1605711520', 'Most powerful is he who controls his own power.', 'Star Wars: The Clone Wars 01×10 – Lair of Grievous'); 52 | 53 | -- 54 | -- Indexes for dumped tables 55 | -- 56 | 57 | -- 58 | -- Indexes for table `quote` 59 | 60 | /*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; 61 | /*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; 62 | /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; -------------------------------------------------------------------------------- /src/test/java/stepdefs/ToxiProxySteps.java: -------------------------------------------------------------------------------- 1 | package stepdefs; 2 | 3 | import eu.rekawek.toxiproxy.model.ToxicDirection; 4 | import io.cucumber.java.After; 5 | import io.cucumber.java.Before; 6 | import io.cucumber.java.en.And; 7 | import io.cucumber.java.en.Given; 8 | import lombok.SneakyThrows; 9 | import org.assertj.core.api.Assertions; 10 | import toxiproxy.metric.Metrics; 11 | 12 | public class ToxiProxySteps { 13 | 14 | private final ToxiProxyHelper toxiProxyHelper = new ToxiProxyHelper(); 15 | 16 | private Metrics initialMetrics; 17 | 18 | private Metrics latestMetrics; 19 | 20 | @Before() 21 | public void init() { 22 | toxiProxyHelper.reset(); 23 | } 24 | 25 | @After() 26 | public void clean() { 27 | toxiProxyHelper.reset(); 28 | } 29 | 30 | @SneakyThrows 31 | @Given("The SQL service timeouts after {int} millisecond") 32 | public void SQLServiceIsDown(int timeoutMillis) { 33 | this.initialMetrics = toxiProxyHelper.getMetric(); 34 | toxiProxyHelper.getMySqlProxy().toxics().timeout("sql-timeout", ToxicDirection.DOWNSTREAM, timeoutMillis); 35 | } 36 | 37 | @SneakyThrows 38 | @Given("The SQL service timeouts after {int} millisecond upstream") 39 | public void SQLServiceIsDownUpstream(int timeoutMillis) { 40 | this.initialMetrics = toxiProxyHelper.getMetric(); 41 | toxiProxyHelper.getMySqlProxy().toxics().timeout("sql-timeout", ToxicDirection.UPSTREAM, timeoutMillis); 42 | } 43 | 44 | @And("bytes have been received by proxy") 45 | public void bytesHaveBeenReceivedByProxy() { 46 | this.latestMetrics = toxiProxyHelper.getMetric(); 47 | boolean forwardedBytesAreLessThanSent = latestMetrics.forwardedBytesAreLessThanSent("mysql", initialMetrics); 48 | Assertions.assertThat(forwardedBytesAreLessThanSent) 49 | .as("Bytes that have been sent from application to ToxiProxyServer are less than the Bytes forwarded from ToxiProxyServer") 50 | .isTrue(); 51 | } 52 | 53 | @And("bytes have not been sent to database") 54 | public void bytesHaveNotBeenSentToDatabase() { 55 | boolean anyForwarded = latestMetrics.noBytesForwardedFromProxyServer("mysql", initialMetrics); 56 | Assertions.assertThat(anyForwarded) 57 | .as("NO Bytes have been forwarded from ToxiProxyServer to actual service") 58 | .isTrue(); 59 | } 60 | 61 | @And("bytes have been sent to database") 62 | public void bytesHaveBeenSentToDatabase() { 63 | boolean anyForwarded = latestMetrics.noBytesForwardedFromProxyServer("mysql", initialMetrics); 64 | Assertions.assertThat(anyForwarded) 65 | .as("NO Bytes have been forwarded from ToxiProxyServer to actual service") 66 | .isFalse(); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/test/java/toxiproxy/metric/Metrics.java: -------------------------------------------------------------------------------- 1 | package toxiproxy.metric; 2 | 3 | import lombok.Getter; 4 | 5 | import java.util.ArrayList; 6 | import java.util.Map; 7 | 8 | public class Metrics { 9 | 10 | private final Map> metrics; 11 | 12 | public Metrics(Map> metrics) { 13 | this.metrics = metrics; 14 | } 15 | 16 | public Metric getMetric(String proxyName, 17 | Metric.ToxiDirection toxiDirection, 18 | StreamDirection streamDirection) { 19 | var metricList = metrics.get(proxyName); 20 | if (metricList == null) { 21 | return null; 22 | } 23 | return metricList.stream() 24 | .filter(m -> m.getToxiDirection() == toxiDirection 25 | && m.getDirection().equals(streamDirection.getDirection())) 26 | .findFirst() 27 | .orElseThrow(() -> 28 | new IllegalArgumentException("")); 29 | } 30 | 31 | private int getDifference(Metrics initialMetrics, 32 | String proxyName, 33 | Metric.ToxiDirection toxiDirection, 34 | StreamDirection streamDirection) { 35 | Metric initMetricSentByApp = initialMetrics. 36 | getMetric(proxyName, toxiDirection, streamDirection); 37 | Metric finalMetricSentByApp = this. 38 | getMetric(proxyName, toxiDirection, streamDirection); 39 | 40 | int initBytesSent; 41 | if (initMetricSentByApp == null) { 42 | initBytesSent = 0; 43 | } else { 44 | initBytesSent = initMetricSentByApp.getBytes(); 45 | } 46 | 47 | return finalMetricSentByApp.getBytes() - initBytesSent; 48 | 49 | } 50 | 51 | public boolean forwardedBytesAreLessThanSent(String proxyName, Metrics initialMetrics) { 52 | int bytesSentToServer = getDifference(initialMetrics, proxyName, Metric.ToxiDirection.PROXY_RECIEVED, StreamDirection.DOWN); 53 | int bytesForwardedFromServer = getDifference(initialMetrics, proxyName, Metric.ToxiDirection.PROXY_SENT, StreamDirection.UP); 54 | return bytesSentToServer > bytesForwardedFromServer; 55 | } 56 | 57 | public boolean noBytesForwardedFromProxyServer(String proxyName, Metrics initialMetrics) { 58 | int bytesForwardedFromServer = getDifference(initialMetrics, proxyName, Metric.ToxiDirection.PROXY_SENT, StreamDirection.UP); 59 | return bytesForwardedFromServer == 0; 60 | } 61 | 62 | public enum StreamDirection { 63 | UP("upstream"), 64 | DOWN("downstream"); 65 | 66 | @Getter 67 | private final String direction; 68 | 69 | StreamDirection(String direction) { 70 | this.direction = direction; 71 | } 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | org.springframework.boot 8 | spring-boot-starter-parent 9 | 2.7.3 10 | 11 | 12 | com.example 13 | demo 14 | 0.0.1-SNAPSHOT 15 | demo 16 | Demo project for Spring Boot 17 | 18 | 19 | 20 | org.springframework.boot 21 | spring-boot-starter-web 22 | 23 | 24 | org.springframework.boot 25 | spring-boot-starter-test 26 | test 27 | 28 | 29 | org.springframework.boot 30 | spring-boot-starter-data-jpa 31 | 32 | 33 | mysql 34 | mysql-connector-java 35 | runtime 36 | 37 | 38 | 39 | io.rest-assured 40 | rest-assured 41 | 4.0.0 42 | test 43 | 44 | 45 | org.projectlombok 46 | lombok 47 | 1.18.24 48 | 49 | 50 | eu.rekawek.toxiproxy 51 | toxiproxy-java 52 | 2.1.7 53 | 54 | 55 | io.cucumber 56 | cucumber-java 57 | 7.5.0 58 | test 59 | 60 | 61 | io.cucumber 62 | cucumber-junit 63 | 7.5.0 64 | test 65 | 66 | 67 | 68 | 69 | 70 | 71 | org.springframework.boot 72 | spring-boot-maven-plugin 73 | 74 | 75 | org.apache.maven.plugins 76 | maven-compiler-plugin 77 | 78 | 10 79 | 10 80 | 81 | 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /ToxiProxyDemo.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | --------------------------------------------------------------------------------