Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add /emails/{id} resource and corresponding test to part2.2-rest #58

Merged
merged 14 commits into from
Apr 26, 2024
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package pl.allegro.tech.workshops.testsparallelexecution


import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.web.client.TestRestTemplate
Expand Down
11 changes: 11 additions & 0 deletions part2.2-rest/.readme/sequence.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,15 @@ sequenceDiagram
REST API ->>+ external service: POST /external-api-service/emails <br/>{"subject": "New ...", "sender": "...", "recipient": "..."}
external service -->>- REST API: response
REST API -->>- User: response
Note over User, external service: read e-mail
User ->>+ REST API: GET /emails/{id}
REST API ->>+ external service: GET /external-api-service/emails/{id}
external service -->>- REST API: response <br/>{"subject": "New ...", "sender": "...", "recipient": "..."}
REST API -->>- User: response <br/>{"subject": "New ...", "sender": "...", "recipient": "..."}
```

Convert to svg format

```shell
npx @mermaid-js/mermaid-cli mmdc -i part2.2-rest/.readme/sequence.md -o part2.2-rest/.readme/sequence.svg -t dark -b "" --cssFile .readme/diagrams.css
```
2 changes: 1 addition & 1 deletion part2.2-rest/.readme/sequence.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 2 additions & 1 deletion part2.2-rest/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ Check [`tests`](src/test/groovy).
4. Run tests `./gradlew --rerun-tasks :part2.2-rest:test :part2.2-rest:createTestsExecutionReport --continue`
5. Temporarily disable test `retry email sending after error response ...` - add `@Ignore` annotation to method
containing this test.
6. Determine and remove shared state.
6. Temporarily disable tests in `SendEmailResourceTest` class - add `@Ignore` annotation this class.
7. Determine and remove shared state.

#### Shared state

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package pl.allegro.tech.workshops.testsparallelexecution.email.rest;


import jakarta.validation.constraints.NotBlank;

public record Email(String subject, @NotBlank String sender, String recipient) {

static public Email of(String subject, String sender, String recipient) {
return new Email(subject, sender, recipient);
}

}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package pl.allegro.tech.workshops.testsparallelexecution.email.rest;

public interface EmailClient {
void send(EmailRequest email);
void send(Email email);

Email read(String id);
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package pl.allegro.tech.workshops.testsparallelexecution.email.rest;

import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
Expand All @@ -15,8 +17,13 @@ public EmailController(EmailService emailService) {
this.emailService = emailService;
}

@GetMapping("/{id}")
public Email readEmail(@PathVariable String id) {
return emailService.readEmail(id);
}

@PostMapping()
public void createEmail(@Valid @RequestBody EmailRequest email) {
public void createEmail(@Valid @RequestBody Email email) {
emailService.sendEmail(email);
}

Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public EmailService(EmailClient emailClient) {
this.emailClient = emailClient;
}

public void sendEmail(EmailRequest email) {
public void sendEmail(Email email) {
try {
emailClient.send(email);
} catch (Exception e) {
Expand All @@ -26,4 +26,16 @@ public void sendEmail(EmailRequest email) {
e);
}
}

public Email readEmail(String id) {
try {
return emailClient.read(id);
} catch (Exception e) {
throw new ErrorResponseException(
HttpStatus.INTERNAL_SERVER_ERROR,
ProblemDetail.forStatusAndDetail(HttpStatus.INTERNAL_SERVER_ERROR, "Email service communication error. " + e.getMessage()),
e);
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,13 @@ public ExternalEmailClient(RestTemplateBuilder builder, @Value("${application.se
}

@Override
public void send(EmailRequest email) {
public void send(Email email) {
retryTemplate.execute(context -> restTemplate.postForEntity("/external-api-service/emails", email, Void.class));
}

@Override
public Email read(String id) {
return retryTemplate.execute(context -> restTemplate.getForEntity("/external-api-service/emails/" + id, Email.class).getBody());
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package pl.allegro.tech.workshops.testsparallelexecution.email.rest

import com.github.tomakehurst.wiremock.WireMockServer
import com.github.tomakehurst.wiremock.stubbing.Scenario
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.annotation.Import
import org.springframework.http.ProblemDetail
import pl.allegro.tech.workshops.testsparallelexecution.BaseTestWithRest

import static com.github.tomakehurst.wiremock.client.WireMock.aResponse
import static com.github.tomakehurst.wiremock.client.WireMock.get
import static com.github.tomakehurst.wiremock.client.WireMock.urlPathMatching
import static com.github.tomakehurst.wiremock.http.Fault.CONNECTION_RESET_BY_PEER
import static com.github.tomakehurst.wiremock.http.Fault.EMPTY_RESPONSE
import static org.springframework.http.HttpHeaders.CONTENT_TYPE
import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR
import static org.springframework.http.HttpStatus.OK
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE

@Import(WiremockConfig)
class GetEmailResourceTest extends BaseTestWithRest implements WiremockPortSupport {

@Autowired
private WireMockServer wiremockServer

private static final String VALID_BODY = """{"subject": "test subject", "sender": "[email protected]", "recipient": "[email protected]"}"""

private String emailId = "2"

def cleanup() {
wiremockServer.resetAll()
wiremockServer.resetScenarios()
}

def "get e-mail"() {
given:
wiremockServer.stubFor(get(urlPathMatching("/external-api-service/emails/.*"))
.willReturn(aResponse()
.withHeader(CONTENT_TYPE, APPLICATION_JSON_VALUE)
.withStatus(200)
.withBody("""{"subject": "test subject", "sender": "[email protected]", "recipient": "[email protected]"}""")
)
)

when:
def result = restClient.get("/emails/$emailId", Email)

then:
result.statusCode == OK
result.body == Email.of("test subject", "[email protected]", "[email protected]")
}

def "handle email service errors (status=#errorResponse.status, fault=#errorResponse.fault, delay=#errorResponse.fixedDelayMilliseconds)"() {
given:
wiremockServer.stubFor(get(urlPathMatching("/external-api-service/emails/.*"))
.willReturn(errorResponse)
)

when:
def result = restClient.get("/emails/$emailId", ProblemDetail)

then:
result.statusCode == INTERNAL_SERVER_ERROR
result.body.detail.contains expectedDetail

where:
errorResponse || expectedDetail
aResponse().withStatus(400) || "400 Bad Request"
aResponse().withStatus(500) || "500 Server Error"
aResponse().withFault(EMPTY_RESPONSE) || "Unexpected end of file from server"
aResponse().withFault(CONNECTION_RESET_BY_PEER) || "Connection reset"
aResponse().withFixedDelay(1000)
.withHeader(CONTENT_TYPE, APPLICATION_JSON_VALUE)
.withStatus(200)
.withBody(VALID_BODY) || "Read timed out"
}

def "retry email fetching after error response (status=#errorResponse.status, fault=#errorResponse.fault, delay=#errorResponse.fixedDelayMilliseconds)"() {
given:
wiremockServer.stubFor(get(urlPathMatching("/external-api-service/emails/.*"))
.willReturn(errorResponse)
.inScenario("retry scenario")
.whenScenarioStateIs(Scenario.STARTED)
.willSetStateTo('after error')
)
wiremockServer.stubFor(get(urlPathMatching("/external-api-service/emails/.*"))
.willReturn(aResponse()
.withHeader(CONTENT_TYPE, APPLICATION_JSON_VALUE)
.withStatus(200)
.withBody("""{"subject": "test subject", "sender": "[email protected]", "recipient": "[email protected]"}""")
)
.inScenario("retry scenario")
.whenScenarioStateIs('after error')
.willSetStateTo('after ok')
)

when:
def result = restClient.get("/emails/$emailId", Email)

then:
result.statusCode == OK
result.body == Email.of("test subject", "[email protected]", "[email protected]")

where:
errorResponse << [
aResponse().withStatus(400),
aResponse().withStatus(500),
aResponse().withFault(EMPTY_RESPONSE),
aResponse().withFault(CONNECTION_RESET_BY_PEER),
aResponse().withFixedDelay(1000)
.withHeader(CONTENT_TYPE, APPLICATION_JSON_VALUE)
.withStatus(200)
.withBody(VALID_BODY)
]
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,16 @@ package pl.allegro.tech.workshops.testsparallelexecution.email.rest

import com.github.tomakehurst.wiremock.WireMockServer
import com.github.tomakehurst.wiremock.stubbing.Scenario
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.annotation.Import
import org.springframework.http.ProblemDetail
import pl.allegro.tech.workshops.testsparallelexecution.BaseTestWithRest
import spock.lang.Shared

import static com.github.tomakehurst.wiremock.client.WireMock.aResponse
import static com.github.tomakehurst.wiremock.client.WireMock.equalTo
import static com.github.tomakehurst.wiremock.client.WireMock.matchingJsonPath
import static com.github.tomakehurst.wiremock.client.WireMock.post
import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor
import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo
import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo
import static com.github.tomakehurst.wiremock.http.Fault.CONNECTION_RESET_BY_PEER
import static com.github.tomakehurst.wiremock.http.Fault.EMPTY_RESPONSE
import static org.springframework.http.HttpHeaders.ACCEPT
Expand Down Expand Up @@ -39,31 +39,23 @@ import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE
}
</pre>
*/
class EmailsByRestResourceTest extends BaseTestWithRest {
@Import(WiremockConfig)
class SendEmailResourceTest extends BaseTestWithRest implements WiremockPortSupport {

@Shared
WireMockServer wiremockServer
@Autowired
private WireMockServer wiremockServer

private String subject = "New workshops!"

def setupSpec() {
wiremockServer = new WireMockServer(8099)
wiremockServer.start()
}

def cleanupSpec() {
wiremockServer.stop()
}

def cleanup() {
wiremockServer.resetAll()
wiremockServer.resetScenarios()
}

def "send e-mail"() {
given:
def email = EmailRequest.of(subject, "[email protected]", "[email protected]")
wiremockServer.stubFor(post(urlEqualTo("/external-api-service/emails"))
def email = Email.of(subject, "[email protected]", "[email protected]")
wiremockServer.stubFor(post(urlPathEqualTo("/external-api-service/emails"))
.willReturn(aResponse()
.withHeader(CONTENT_TYPE, APPLICATION_JSON_VALUE)
.withStatus(200)
Expand All @@ -75,15 +67,15 @@ class EmailsByRestResourceTest extends BaseTestWithRest {

then:
result.statusCode == OK
wiremockServer.verify(1, postRequestedFor(urlEqualTo("/external-api-service/emails"))
wiremockServer.verify(1, postRequestedFor(urlPathEqualTo("/external-api-service/emails"))
.withHeader(ACCEPT, equalTo("application/json, application/*+json"))
)
}

def "do not sent email without sender"() {
given:
def email = EmailRequest.of(subject, sender, "[email protected]")
wiremockServer.stubFor(post(urlEqualTo("/external-api-service/emails"))
def email = Email.of(subject, sender, "[email protected]")
wiremockServer.stubFor(post(urlPathEqualTo("/external-api-service/emails"))
.willReturn(aResponse()
.withHeader(CONTENT_TYPE, APPLICATION_JSON_VALUE)
.withStatus(200)
Expand All @@ -97,7 +89,7 @@ class EmailsByRestResourceTest extends BaseTestWithRest {

then:
result.statusCode == BAD_REQUEST
wiremockServer.verify(0, postRequestedFor(urlEqualTo("/external-api-service/emails"))
wiremockServer.verify(0, postRequestedFor(urlPathEqualTo("/external-api-service/emails"))
.withHeader(ACCEPT, equalTo("application/json, application/*+json"))
)

Expand All @@ -106,8 +98,9 @@ class EmailsByRestResourceTest extends BaseTestWithRest {
}

def "handle email service errors (status=#errorResponse.status, fault=#errorResponse.fault, delay=#errorResponse.fixedDelayMilliseconds)"() {
def email = EmailRequest.of(subject, "[email protected]", "[email protected]")
wiremockServer.stubFor(post(urlEqualTo("/external-api-service/emails"))
given:
def email = Email.of(subject, "[email protected]", "[email protected]")
wiremockServer.stubFor(post(urlPathEqualTo("/external-api-service/emails"))
.willReturn(errorResponse)
)

Expand All @@ -130,14 +123,15 @@ class EmailsByRestResourceTest extends BaseTestWithRest {
}

def "retry email sending after error response (status=#errorResponse.status, fault=#errorResponse.fault, delay=#errorResponse.fixedDelayMilliseconds)"() {
def email = EmailRequest.of(subject, "[email protected]", "[email protected]")
wiremockServer.stubFor(post(urlEqualTo("/external-api-service/emails"))
given:
def email = Email.of(subject, "[email protected]", "[email protected]")
wiremockServer.stubFor(post(urlPathEqualTo("/external-api-service/emails"))
.willReturn(errorResponse)
.inScenario("retry scenario")
.whenScenarioStateIs(Scenario.STARTED)
.willSetStateTo('after error')
)
wiremockServer.stubFor(post(urlEqualTo("/external-api-service/emails"))
wiremockServer.stubFor(post(urlPathEqualTo("/external-api-service/emails"))
.willReturn(aResponse()
.withHeader(CONTENT_TYPE, APPLICATION_JSON_VALUE)
.withStatus(200)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package pl.allegro.tech.workshops.testsparallelexecution.email.rest

import com.github.tomakehurst.wiremock.WireMockServer
import org.springframework.beans.factory.annotation.Value
import org.springframework.boot.test.context.TestConfiguration
import org.springframework.context.annotation.Bean

@TestConfiguration
class WiremockConfig {

@Bean
WireMockServer getWiremockServer(@Value('${wiremock.port}') int wiremockPort) {
return new WireMockServer(wiremockPort).tap {
it.start()
}
}
}
Loading
Loading