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

PP-13030: Use CreateServiceRequest pojo instead of JsonNode #2653

Merged
merged 5 commits into from
Sep 9, 2024

Conversation

oswaldquek
Copy link
Contributor

@oswaldquek oswaldquek commented Sep 6, 2024

Following the general pattern in most of our Dropwizard codebases, we want to avoid JsonNode as the POST request body where possible so we can advantage of using javax Validation constraints.

It is also generally agreed that using hard coded URL strings makes for easier-to-read code.

Added a few more test cases in ServiceResourceCreateIT to reflect the pact tests between selfservice and adminusers:

@oswaldquek oswaldquek force-pushed the PP-13030-use-CreateServiceRequest-pojo branch from 4a4de0e to a3ed16d Compare September 6, 2024 16:32
.body(Map.of("service_name", Map.of("fr", "Service name")))
.post("v1/api/services")
.then()
.statusCode(400)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Returning a 422 might be more correct here, but getting Jersey to do this from within a custom deserialiser is very hard to do. We return a 400 in other places so I think this is acceptable.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree this is fine for an internal API.

@oswaldquek oswaldquek force-pushed the PP-13030-use-CreateServiceRequest-pojo branch 2 times, most recently from da26a5e to bd365a8 Compare September 6, 2024 16:46
Comment on lines 17 to 21
Map<SupportedLanguage, String> supportedLanguageToServiceName = new HashMap<>();
jsonParser.getCodec().readValue(jsonParser, new TypeReference<Map<String, String>>() {})
.forEach((key, value) ->
supportedLanguageToServiceName.put(SupportedLanguage.fromIso639AlphaTwoCode(key), value));
return Collections.unmodifiableMap(supportedLanguageToServiceName);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return jsonParser.getCodec().readValue(jsonParser, new TypeReference<Map<String, String>>() {})
        .entrySet().stream()
        .collect(toUnmodifiableMap(entry ->
                SupportedLanguage.fromIso639AlphaTwoCode(entry.getKey()), Map.Entry::getValue));

😀

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, fixed.

Following the general pattern in most of our Dropwizard codebases, we want to avoid
JsonNode as the POST request body where possible so we can advantage of using
javax Validation constraints.

It is also generally agreed that using hard coded URL strings makes for
easier-to-read code.

Added a few more test cases in ServiceResourceCreateIT to reflect the pact
tests between selfservice and adminusers:
* [a valid create service request with empty object](https://github.com/alphagov/pay-selfservice/blob/b2053a1049bc28813e8e7363245ad546ef19b675/test/pact/adminusers-client/service/create-service.pact.test.js#L50)
* [a valid create service request with gateway account ids](https://github.com/alphagov/pay-selfservice/blob/b2053a1049bc28813e8e7363245ad546ef19b675/test/pact/adminusers-client/service/create-service.pact.test.js#L85C31-L85C86)
* [a valid create service request with service name](https://github.com/alphagov/pay-selfservice/blob/b2053a1049bc28813e8e7363245ad546ef19b675/test/pact/adminusers-client/service/create-service.pact.test.js#L124)
@oswaldquek oswaldquek force-pushed the PP-13030-use-CreateServiceRequest-pojo branch from 148f7ae to 9ac402e Compare September 9, 2024 09:25
Copy link
Contributor

@alexbishop1 alexbishop1 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good! Just a few small suggestions.

@@ -227,28 +220,21 @@ public Response searchServices(JsonNode payload) {
@ApiResponse(responseCode = "201", description = "Created",
content = @Content(schema = @Schema(implementation = Service.class))),
@ApiResponse(responseCode = "409", description = "Gateway account IDs provided has already been assigned to another service"),
@ApiResponse(responseCode = "400", description = "Service names must be one of 'en' (English) or 'cy' (Welsh)"),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose really it’s the service name key that needs to be ‘en’ or ‘cy’.

Comment on lines +230 to +233
var nullableCreateServiceRequest = Optional.ofNullable(createServiceRequest);
Service service = serviceServicesFactory.serviceCreator().doCreate(
nullableCreateServiceRequest.map(CreateServiceRequest::gatewayAccountIds).orElse(List.of()),
nullableCreateServiceRequest.map(CreateServiceRequest::serviceName).orElse(Map.of()));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Presumably the entire createServiceRequest is not going to be null but the list of gateway account IDs or map of languages to service names might be, so perhaps this would be better:

Service service = serviceServicesFactory.serviceCreator().doCreate(
        Optional.ofNullable(createServiceRequest.gatewayAccountIds()).orElse(List.of()),
        Optional.ofNullable(createServiceRequest.serviceName()).orElse(Map.of()));

Copy link
Contributor Author

@oswaldquek oswaldquek Sep 9, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

createServiceRequest is null actually.... otherwise your suggestion would be the better option!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this would be easier:

Service service = serviceServicesFactory.serviceCreator().doCreate(
        createServiceRequest == null ? List.of() : createServiceRequest.gatewayAccountIds(),
        createServiceRequest == null ? Map.of() : createServiceRequest.serviceName());

But am not very fussed here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⬆️ Actually my suggestion wouldn't work 🙈

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this would work:

Service service = serviceServicesFactory.serviceCreator().doCreate(
        Optional.ofNullable(createServiceRequest).map(CreateServiceRequest::gatewayAccountIds).orElse(List.of()),
        Optional.ofNullable(createServiceRequest).map(CreateServiceRequest::serviceName).orElse(Map.of()));

It would get rid of the need to have the nullableCreateServiceRequest variable at the expense of an extra Optional.ofNullable(…) call (the cost of which is probably absolutely trivial).

This could be slightly rearranged to surface the actual data and types involved a bit more:

List<String> gatewayAccountIds = Optional.ofNullable(createServiceRequest).map(CreateServiceRequest::gatewayAccountIds).orElse(List.of());
Map<SupportedLanguage, String> serviceNames = Optional.ofNullable(createServiceRequest).map(CreateServiceRequest::serviceName).orElse(Map.of());

Service service = serviceServicesFactory.serviceCreator().doCreate(gatewayAccountIds, serviceNames);

Comment on lines 40 to 52
private void assertStandardFields(ValidatableResponse validatableResponse) {
validatableResponse
.body("current_psp_test_account_stage", is("NOT_STARTED"))
.body("current_go_live_stage", is("NOT_STARTED"))
.body("default_billing_address_country", is("GB"))
.body("agent_initiated_moto_enabled", is(false))
.body("takes_payments_over_phone", is(false))
.body("experimental_features_enabled", is(false))
.body("internal", is(false))
.body("archived", is(false))
.body("redirect_to_service_immediately_on_terminal_state", is(false))
.body("collect_billing_address", is(true));
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this method should go after all the tests that rely on it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do you mean? Like for other integration tests?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or do you mean moving this method to the end of the class...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, at the end of the class or at least after all the tests that rely on it.

.body(Map.of("service_name", Map.of("fr", "Service name")))
.post("v1/api/services")
.then()
.statusCode(400)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree this is fine for an internal API.

Copy link
Contributor

@alexbishop1 alexbishop1 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks fine. One take it or leave it suggestion.

Comment on lines +230 to +233
var nullableCreateServiceRequest = Optional.ofNullable(createServiceRequest);
Service service = serviceServicesFactory.serviceCreator().doCreate(
nullableCreateServiceRequest.map(CreateServiceRequest::gatewayAccountIds).orElse(List.of()),
nullableCreateServiceRequest.map(CreateServiceRequest::serviceName).orElse(Map.of()));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this would work:

Service service = serviceServicesFactory.serviceCreator().doCreate(
        Optional.ofNullable(createServiceRequest).map(CreateServiceRequest::gatewayAccountIds).orElse(List.of()),
        Optional.ofNullable(createServiceRequest).map(CreateServiceRequest::serviceName).orElse(Map.of()));

It would get rid of the need to have the nullableCreateServiceRequest variable at the expense of an extra Optional.ofNullable(…) call (the cost of which is probably absolutely trivial).

This could be slightly rearranged to surface the actual data and types involved a bit more:

List<String> gatewayAccountIds = Optional.ofNullable(createServiceRequest).map(CreateServiceRequest::gatewayAccountIds).orElse(List.of());
Map<SupportedLanguage, String> serviceNames = Optional.ofNullable(createServiceRequest).map(CreateServiceRequest::serviceName).orElse(Map.of());

Service service = serviceServicesFactory.serviceCreator().doCreate(gatewayAccountIds, serviceNames);

@oswaldquek oswaldquek merged commit 00a0f8a into master Sep 9, 2024
11 checks passed
@oswaldquek oswaldquek deleted the PP-13030-use-CreateServiceRequest-pojo branch September 9, 2024 12:27
@oswaldquek
Copy link
Contributor Author

Looks fine. One take it or leave it suggestion.

I'll take it, but will put it in the next PR. Have another PR incoming for this repo and would like to get this one merged into master.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants