Skip to content

Commit

Permalink
Configuration API (#1249)
Browse files Browse the repository at this point in the history
* add configuration API

* fix potential NPE
  • Loading branch information
cbellone committed Jun 16, 2023
1 parent 51c2eda commit d39c847
Show file tree
Hide file tree
Showing 3 changed files with 284 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
/**
* This file is part of alf.io.
*
* alf.io is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* alf.io is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with alf.io. If not, see <http://www.gnu.org/licenses/>.
*/
package alfio.controller.api.v1.admin;

import alfio.manager.EventManager;
import alfio.manager.system.ConfigurationManager;
import alfio.manager.user.UserManager;
import alfio.model.modification.ConfigurationModification;
import alfio.model.system.Configuration;
import alfio.model.system.ConfigurationKeys;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.security.Principal;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

@RestController
@RequestMapping("/api/v1/admin/configuration")
public class ConfigurationApiV1Controller {

private final ConfigurationManager configurationManager;
private final EventManager eventManager;
private final UserManager userManager;

public ConfigurationApiV1Controller(ConfigurationManager configurationManager,
EventManager eventManager,
UserManager userManager) {
this.configurationManager = configurationManager;
this.eventManager = eventManager;
this.userManager = userManager;
}

@PutMapping("/organization/{organizationId}")
public ResponseEntity<String> saveConfigurationForOrganization(@PathVariable("organizationId") int organizationId,
@RequestBody Map<String, String> configurationKeyValues,
Principal principal) {
var configurationKeys = configurationKeyValues.keySet().stream()
.map(ConfigurationKeys::safeValueOf)
.collect(Collectors.toSet());
var validationErrorOptional = validateInput(organizationId, principal, configurationKeyValues, configurationKeys);
if (validationErrorOptional.isPresent()) {
return validationErrorOptional.get();
}
var existingIds = configurationManager.loadOrganizationConfig(organizationId, principal.getName()).values().stream()
.flatMap(List::stream)
.filter(c -> configurationKeys.contains(c.getConfigurationKey()))
.collect(Collectors.toMap(Configuration::getKey, Configuration::getId));
var toSave = configurationKeyValues.entrySet().stream()
.map(ckv -> new ConfigurationModification(existingIds.get(ckv.getKey()), ckv.getKey(), ckv.getValue()))
.collect(Collectors.toList());
configurationManager.saveAllOrganizationConfiguration(organizationId, toSave, principal.getName());
return ResponseEntity.ok().body("OK");
}

@PutMapping("/organization/{organizationId}/event/{slug}")
public ResponseEntity<String> saveConfigurationForEvent(@PathVariable("organizationId") int organizationId,
@PathVariable("slug") String eventSlug,
@RequestBody Map<String, String> configurationKeyValues,
Principal principal) {
var configurationKeys = configurationKeyValues.keySet().stream()
.map(ConfigurationKeys::safeValueOf)
.collect(Collectors.toSet());

var validationErrorOptional = validateInput(organizationId, principal, configurationKeyValues, configurationKeys);

if (validationErrorOptional.isPresent()) {
return validationErrorOptional.get();
}

var eventAndOrgId = eventManager.getEventAndOrganizationId(eventSlug, principal.getName());
if (eventAndOrgId.getOrganizationId() != organizationId) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
}
int eventId = eventAndOrgId.getId();
var existingIds = configurationManager.loadEventConfig(eventId, principal.getName()).values().stream()
.flatMap(List::stream)
.filter(c -> configurationKeys.contains(c.getConfigurationKey()))
.collect(Collectors.toMap(Configuration::getKey, Configuration::getId));
var toSave = configurationKeyValues.entrySet().stream()
.map(ckv -> new ConfigurationModification(existingIds.get(ckv.getKey()), ckv.getKey(), ckv.getValue()))
.collect(Collectors.toList());
configurationManager.saveAllEventConfiguration(eventId, organizationId, toSave, principal.getName());
return ResponseEntity.ok().body("OK");
}

private boolean checkUserIsMemberOfOrganization(int organizationId, Principal principal) {
return userManager.findUserOrganizations(principal.getName()).stream().noneMatch(o -> o.getId() == organizationId);
}

static class ConfigurationKeyValue {

private final ConfigurationKeys key;
private final String value;

@JsonCreator
ConfigurationKeyValue(@JsonProperty("key") ConfigurationKeys key, @JsonProperty("value") String value) {
this.key = key;
this.value = value;
}

public ConfigurationKeys getKey() {
return key;
}

public String getValue() {
return value;
}
}

private Optional<ResponseEntity<String>> validateInput(int organizationId,
Principal principal,
Map<String, String> configurationKeyValues,
Set<ConfigurationKeys> configurationKeys) {
if (checkUserIsMemberOfOrganization(organizationId, principal)) {
return Optional.of(ResponseEntity.status(HttpStatus.FORBIDDEN).build());
}

if (configurationKeys.size() != configurationKeyValues.size()) {
return Optional.of(ResponseEntity.badRequest().body("Request contains duplicate keys"));
}

if (configurationKeys.contains(ConfigurationKeys.NOT_RECOGNIZED)) {
return Optional.of(ResponseEntity.badRequest().body("Request contains unrecognized keys"));
}

return Optional.empty();
}
}
2 changes: 1 addition & 1 deletion src/main/java/alfio/util/ItalianTaxIdValidator.java
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ private static FiscalCodeParts parseFiscalCodePart(String part) {

public static boolean validateVatId(String vatId) {
var nr = StringUtils.trimToNull(vatId);
if(length(nr) != COMPANY_TAX_ID_LENGTH && !StringUtils.isNumeric(nr)) {
if(nr == null || (length(nr) != COMPANY_TAX_ID_LENGTH && !StringUtils.isNumeric(nr))) {
return false;
}
int sumEven = 0;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/**
* This file is part of alf.io.
*
* alf.io is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* alf.io is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with alf.io. If not, see <http://www.gnu.org/licenses/>.
*/
package alfio.controller.api.v1;

import alfio.TestConfiguration;
import alfio.config.DataSourceConfiguration;
import alfio.config.Initializer;
import alfio.controller.api.ControllerConfiguration;
import alfio.controller.api.v1.admin.ConfigurationApiV1Controller;
import alfio.controller.api.v1.admin.EventApiV1Controller;
import alfio.manager.user.UserManager;
import alfio.model.modification.OrganizationModification;
import alfio.model.system.ConfigurationKeyValuePathLevel;
import alfio.model.user.Organization;
import alfio.model.user.Role;
import alfio.model.user.User;
import alfio.repository.EventRepository;
import alfio.repository.system.ConfigurationRepository;
import alfio.repository.user.OrganizationRepository;
import alfio.test.util.AlfioIntegrationTest;
import alfio.test.util.IntegrationTestUtil;
import alfio.util.BaseIntegrationTest;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.ContextConfiguration;

import java.security.Principal;
import java.util.List;
import java.util.Map;
import java.util.UUID;

import static alfio.controller.api.v1.EventApiV1IntegrationTest.creationRequest;
import static alfio.model.system.ConfigurationKeys.*;
import static java.util.Map.entry;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;


@AlfioIntegrationTest
@ContextConfiguration(classes = {DataSourceConfiguration.class, TestConfiguration.class, ControllerConfiguration.class})
@ActiveProfiles({Initializer.PROFILE_DEV, Initializer.PROFILE_DISABLE_JOBS, Initializer.PROFILE_INTEGRATION_TEST})
class ConfigurationApiV1IntegrationTest extends BaseIntegrationTest {

public static final List<String> OPTIONS_TO_MODIFY = List.of(GENERATE_ONLY_INVOICE.name(), USE_INVOICE_NUMBER_AS_ID.name(), VAT_NUMBER_IS_REQUIRED.name());
@Autowired
private ConfigurationRepository configurationRepository;
@Autowired
private EventRepository eventRepository;
@Autowired
private UserManager userManager;
@Autowired
private OrganizationRepository organizationRepository;
@Autowired
private EventApiV1Controller eventApiController;
@Autowired
private ConfigurationApiV1Controller controller;

private Principal mockPrincipal;
private Organization organization;

@BeforeEach
public void ensureConfiguration() {
IntegrationTestUtil.ensureMinimalConfiguration(configurationRepository);

String organizationName = UUID.randomUUID().toString();
String username = UUID.randomUUID().toString();

var organizationModification = new OrganizationModification(null, organizationName, "email@example.com", "org", null, null);
userManager.createOrganization(organizationModification, null);
organization = organizationRepository.findByName(organizationName).orElseThrow();
userManager.insertUser(organization.getId(), username, "test", "test", "test@example.com", Role.API_CONSUMER, User.Type.INTERNAL, null);

this.mockPrincipal = Mockito.mock(Principal.class);
Mockito.when(mockPrincipal.getName()).thenReturn(username);
}

@Test
void addEventConfiguration() {
String slug = "test";
eventApiController.create(creationRequest(slug), mockPrincipal);
int eventId = eventRepository.findOptionalEventAndOrganizationIdByShortName(slug).orElseThrow().getId();
var existing = configurationRepository.findByEventAndKeys(organization.getId(), eventId, OPTIONS_TO_MODIFY);
assertTrue(existing.isEmpty());
assertEquals(1, configurationRepository.insertEventLevel(organization.getId(), eventId, GENERATE_ONLY_INVOICE.name(), "true", ""));
var payload = Map.ofEntries(
entry(GENERATE_ONLY_INVOICE.name(), "false"),
entry(USE_INVOICE_NUMBER_AS_ID.name(), "true"),
entry(VAT_NUMBER_IS_REQUIRED.name(), "true")
);
var response = controller.saveConfigurationForEvent(organization.getId(), slug, payload, mockPrincipal);
assertTrue(response.getStatusCode().is2xxSuccessful());
var modified = configurationRepository.findByEventAndKeys(organization.getId(), eventId, OPTIONS_TO_MODIFY);
assertEquals(3, modified.size());
for (ConfigurationKeyValuePathLevel kv : modified) {
assertEquals(kv.getConfigurationKey() == GENERATE_ONLY_INVOICE ? "false" : "true", kv.getValue());
}
}

@Test
void addOrganizationConfiguration() {
var existing = configurationRepository.findByOrganizationAndKeys(organization.getId(), OPTIONS_TO_MODIFY);
assertTrue(existing.isEmpty());
assertEquals(1, configurationRepository.insertOrganizationLevel(organization.getId(), GENERATE_ONLY_INVOICE.name(), "true", ""));
var payload = Map.ofEntries(
entry(GENERATE_ONLY_INVOICE.name(), "false"),
entry(USE_INVOICE_NUMBER_AS_ID.name(), "true"),
entry(VAT_NUMBER_IS_REQUIRED.name(), "true")
);
var response = controller.saveConfigurationForOrganization(organization.getId(), payload, mockPrincipal);
assertTrue(response.getStatusCode().is2xxSuccessful());
var modified = configurationRepository.findByOrganizationAndKeys(organization.getId(), OPTIONS_TO_MODIFY);
assertEquals(3, modified.size());
for (ConfigurationKeyValuePathLevel kv : modified) {
assertEquals(kv.getConfigurationKey() == GENERATE_ONLY_INVOICE ? "false" : "true", kv.getValue());
}
}
}

0 comments on commit d39c847

Please sign in to comment.