Commit 144699d7 authored by Kostis Trantzas's avatar Kostis Trantzas
Browse files

Merge branch '81-implement-the-product-hubapi' into 'develop'

Resolve "Implement the Product HubAPI"

See merge request !78
parents 620982a1 4ff27b5d
Loading
Loading
Loading
Loading
Loading
+50 −1
Original line number Diff line number Diff line
@@ -22,21 +22,38 @@ package org.etsi.osl.tmf.pcm620.api;
import java.util.Optional;

import com.fasterxml.jackson.databind.ObjectMapper;

import org.etsi.osl.tmf.pcm620.model.EventSubscription;
import org.etsi.osl.tmf.pcm620.model.EventSubscriptionInput;
import org.etsi.osl.tmf.pcm620.reposervices.EventSubscriptionRepoService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;

import io.swagger.v3.oas.annotations.Parameter;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid;
@jakarta.annotation.Generated(value = "io.swagger.codegen.languages.SpringCodegen", date = "2019-10-19T00:15:57.249+03:00")

@Controller("HubApiController620")
@RequestMapping("/productCatalogManagement/v4/")
public class HubApiController implements HubApi {

    private static final Logger log = LoggerFactory.getLogger(HubApiController.class);

    private final ObjectMapper objectMapper;

    private final HttpServletRequest request;

    @Autowired
    EventSubscriptionRepoService eventSubscriptionRepoService;

    @org.springframework.beans.factory.annotation.Autowired
    public HubApiController(ObjectMapper objectMapper, HttpServletRequest request) {
        this.objectMapper = objectMapper;
@@ -53,4 +70,36 @@ public class HubApiController implements HubApi {
        return Optional.ofNullable(request);
    }

    @Override
    @PreAuthorize("hasAnyAuthority('ROLE_ADMIN')")
    public ResponseEntity<EventSubscription> registerListener(@Parameter(description = "Data containing the callback endpoint to deliver the information", required = true) @Valid @RequestBody EventSubscriptionInput data) {
        try {
            EventSubscription eventSubscription = eventSubscriptionRepoService.addEventSubscription(data);
            return new ResponseEntity<>(eventSubscription, HttpStatus.CREATED);
        } catch (IllegalArgumentException e) {
            log.error("Invalid input for listener registration: {}", e.getMessage());
            return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
        } catch (Exception e) {
            log.error("Error registering listener", e);
            return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }

    @Override
    @PreAuthorize("hasAnyAuthority('ROLE_ADMIN')")
    public ResponseEntity<Void> unregisterListener(@Parameter(description = "The id of the registered listener", required = true) @PathVariable("id") String id) {
        try {
            EventSubscription existing = eventSubscriptionRepoService.findById(id);
            if (existing == null) {
                return new ResponseEntity<>(HttpStatus.NOT_FOUND);
            }
            
            eventSubscriptionRepoService.deleteById(id);
            return new ResponseEntity<>(HttpStatus.NO_CONTENT);
        } catch (Exception e) {
            log.error("Error unregistering listener with id: " + id, e);
            return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }

}
+32 −0
Original line number Diff line number Diff line
/*-
 * ========================LICENSE_START=================================
 * org.etsi.osl.tmf.api
 * %%
 * Copyright (C) 2019 - 2021 openslice.io
 * %%
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * =========================LICENSE_END==================================
 */
package org.etsi.osl.tmf.pcm620.repo;

import java.util.Optional;
import org.etsi.osl.tmf.pcm620.model.EventSubscription;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface EventSubscriptionRepository extends CrudRepository<EventSubscription, String>, PagingAndSortingRepository<EventSubscription, String> {

    Optional<EventSubscription> findById(String id);
}
 No newline at end of file
+65 −0
Original line number Diff line number Diff line
/*-
 * ========================LICENSE_START=================================
 * org.etsi.osl.tmf.api
 * %%
 * Copyright (C) 2019 - 2021 openslice.io
 * %%
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * =========================LICENSE_END==================================
 */
package org.etsi.osl.tmf.pcm620.reposervices;

import java.util.Optional;
import java.util.UUID;

import org.etsi.osl.tmf.pcm620.model.EventSubscription;
import org.etsi.osl.tmf.pcm620.model.EventSubscriptionInput;
import org.etsi.osl.tmf.pcm620.repo.EventSubscriptionRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import jakarta.validation.Valid;

@Service
@Transactional
public class EventSubscriptionRepoService {

    @Autowired
    EventSubscriptionRepository eventSubscriptionRepo;

    public EventSubscription addEventSubscription(@Valid EventSubscriptionInput eventSubscriptionInput) {
        if (eventSubscriptionInput.getCallback() == null || eventSubscriptionInput.getCallback().trim().isEmpty()) {
            throw new IllegalArgumentException("Callback URL is required and cannot be empty");
        }
        
        EventSubscription eventSubscription = new EventSubscription();
        eventSubscription.setId(UUID.randomUUID().toString());
        eventSubscription.setCallback(eventSubscriptionInput.getCallback());
        eventSubscription.setQuery(eventSubscriptionInput.getQuery());
        
        return this.eventSubscriptionRepo.save(eventSubscription);
    }

    public EventSubscription findById(String id) {
        Optional<EventSubscription> optionalEventSubscription = this.eventSubscriptionRepo.findById(id);
        return optionalEventSubscription.orElse(null);
    }

    public void deleteById(String id) {
        Optional<EventSubscription> optionalEventSubscription = this.eventSubscriptionRepo.findById(id);
        if (optionalEventSubscription.isPresent()) {
            this.eventSubscriptionRepo.delete(optionalEventSubscription.get());
        }
    }
}
 No newline at end of file
+4 −0
Original line number Diff line number Diff line
package org.etsi.osl.tmf.pim637.api;

import java.io.IOException;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;

import jakarta.servlet.http.HttpServletRequest;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.etsi.osl.tmf.pim637.model.EventSubscription;
+245 −0
Original line number Diff line number Diff line
package org.etsi.osl.services.api.pcm620;

import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;

import org.apache.commons.io.IOUtils;
import org.etsi.osl.tmf.JsonUtils;
import org.etsi.osl.tmf.OpenAPISpringBoot;
import org.etsi.osl.tmf.pcm620.model.EventSubscription;
import org.etsi.osl.tmf.pcm620.model.EventSubscriptionInput;
import org.etsi.osl.tmf.pcm620.reposervices.EventSubscriptionRepoService;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.context.WebApplicationContext;

import com.fasterxml.jackson.databind.ObjectMapper;

@RunWith(SpringRunner.class)
@Transactional
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK, classes = OpenAPISpringBoot.class)
@AutoConfigureMockMvc
@ActiveProfiles("testing")
@AutoConfigureTestDatabase
public class HubApiControllerTest {

    @Autowired
    private MockMvc mvc;

    @Autowired
    private WebApplicationContext context;

    @Autowired
    private EventSubscriptionRepoService eventSubscriptionRepoService;

    @Autowired
    private ObjectMapper objectMapper;

    @Before
    public void setup() {
        mvc = MockMvcBuilders
                .webAppContextSetup(context)
                .apply(springSecurity())
                .build();
    }

    @WithMockUser(username = "osadmin", roles = {"ADMIN"})
    @Test
    public void testRegisterListener() throws Exception {
        File resourceSpecFile = new File("src/test/resources/testPCM620EventSubscriptionInput.json");
        InputStream in = new FileInputStream(resourceSpecFile);
        String eventSubscriptionInputString = IOUtils.toString(in, "UTF-8");
        EventSubscriptionInput eventSubscriptionInput = JsonUtils.toJsonObj(eventSubscriptionInputString, EventSubscriptionInput.class);

        MvcResult result = mvc.perform(MockMvcRequestBuilders.post("/productCatalogManagement/v4/hub")
                        .with(SecurityMockMvcRequestPostProcessors.csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .accept(MediaType.APPLICATION_JSON)
                        .content(JsonUtils.toJson(eventSubscriptionInput)))
                .andExpect(status().isCreated())
                .andExpect(content().contentType("application/json;charset=utf-8"))
                .andExpect(jsonPath("$.callback").value("http://localhost:8080/callback"))
                .andExpect(jsonPath("$.query").value("productOffering.create,productOffering.delete"))
                .andExpect(jsonPath("$.id").exists())
                .andReturn();

        String responseBody = result.getResponse().getContentAsString();
        EventSubscription createdSubscription = objectMapper.readValue(responseBody, EventSubscription.class);
        
        // Verify the subscription was actually saved to the database
        EventSubscription retrievedSubscription = eventSubscriptionRepoService.findById(createdSubscription.getId());
        assert retrievedSubscription != null;
        assert retrievedSubscription.getCallback().equals("http://localhost:8080/callback");
        assert retrievedSubscription.getQuery().equals("productOffering.create,productOffering.delete");
    }

    @WithMockUser(username = "osadmin", roles = {"ADMIN"})
    @Test
    public void testRegisterListenerWithoutAcceptHeader() throws Exception {
        File resourceSpecFile = new File("src/test/resources/testPCM620EventSubscriptionInput.json");
        InputStream in = new FileInputStream(resourceSpecFile);
        String eventSubscriptionInputString = IOUtils.toString(in, "UTF-8");
        EventSubscriptionInput eventSubscriptionInput = JsonUtils.toJsonObj(eventSubscriptionInputString, EventSubscriptionInput.class);

        mvc.perform(MockMvcRequestBuilders.post("/productCatalogManagement/v4/hub")
                        .with(SecurityMockMvcRequestPostProcessors.csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(JsonUtils.toJson(eventSubscriptionInput)))
                .andExpect(status().isCreated())
                .andExpect(jsonPath("$.callback").value("http://localhost:8080/callback"))
                .andExpect(jsonPath("$.query").value("productOffering.create,productOffering.delete"))
                .andExpect(jsonPath("$.id").exists());
    }

    @WithMockUser(username = "osadmin", roles = {"ADMIN"})
    @Test
    public void testUnregisterListener() throws Exception {
        // First, create a subscription
        EventSubscriptionInput input = new EventSubscriptionInput();
        input.setCallback("http://localhost:8080/callback");
        input.setQuery("test.event");
        
        EventSubscription created = eventSubscriptionRepoService.addEventSubscription(input);
        String subscriptionId = created.getId();

        // Verify the subscription exists
        assert eventSubscriptionRepoService.findById(subscriptionId) != null;

        // Delete the subscription
        mvc.perform(MockMvcRequestBuilders.delete("/productCatalogManagement/v4/hub/" + subscriptionId)
                        .with(SecurityMockMvcRequestPostProcessors.csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .accept(MediaType.APPLICATION_JSON))
                .andExpect(status().isNoContent());

        // Verify the subscription was deleted
        assert eventSubscriptionRepoService.findById(subscriptionId) == null;
    }

    @WithMockUser(username = "osadmin", roles = {"ADMIN"})
    @Test
    public void testUnregisterNonExistentListener() throws Exception {
        String nonExistentId = "non-existent-id";

        mvc.perform(MockMvcRequestBuilders.delete("/productCatalogManagement/v4/hub/" + nonExistentId)
                        .with(SecurityMockMvcRequestPostProcessors.csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .accept(MediaType.APPLICATION_JSON))
                .andExpect(status().isNotFound());
    }

    @WithMockUser(username = "osadmin", roles = {"ADMIN"})
    @Test
    public void testRegisterListenerWithInvalidData() throws Exception {
        // Test with missing required callback field
        EventSubscriptionInput invalidInput = new EventSubscriptionInput();
        invalidInput.setQuery("test.event");
        // callback is missing, which is required

        mvc.perform(MockMvcRequestBuilders.post("/productCatalogManagement/v4/hub")
                        .with(SecurityMockMvcRequestPostProcessors.csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .accept(MediaType.APPLICATION_JSON)
                        .content(JsonUtils.toJson(invalidInput)))
                .andExpect(status().isBadRequest());
    }

    @WithMockUser(username = "osadmin", roles = {"ADMIN"})
    @Test
    public void testRegisterListenerWithEmptyCallback() throws Exception {
        EventSubscriptionInput invalidInput = new EventSubscriptionInput();
        invalidInput.setCallback("");
        invalidInput.setQuery("test.event");

        mvc.perform(MockMvcRequestBuilders.post("/productCatalogManagement/v4/hub")
                        .with(SecurityMockMvcRequestPostProcessors.csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .accept(MediaType.APPLICATION_JSON)
                        .content(JsonUtils.toJson(invalidInput)))
                .andExpect(status().isBadRequest());
    }

    @WithMockUser(username = "user", roles = {"USER"})
    @Test
    public void testRegisterListenerUnauthorized() throws Exception {
        File resourceSpecFile = new File("src/test/resources/testPCM620EventSubscriptionInput.json");
        InputStream in = new FileInputStream(resourceSpecFile);
        String eventSubscriptionInputString = IOUtils.toString(in, "UTF-8");
        EventSubscriptionInput eventSubscriptionInput = JsonUtils.toJsonObj(eventSubscriptionInputString, EventSubscriptionInput.class);

        mvc.perform(MockMvcRequestBuilders.post("/productCatalogManagement/v4/hub")
                        .with(SecurityMockMvcRequestPostProcessors.csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .accept(MediaType.APPLICATION_JSON)
                        .content(JsonUtils.toJson(eventSubscriptionInput)))
                .andExpect(status().isForbidden());
    }

    @WithMockUser(username = "user", roles = {"USER"})
    @Test
    public void testUnregisterListenerUnauthorized() throws Exception {
        // First create a subscription as admin
        EventSubscriptionInput input = new EventSubscriptionInput();
        input.setCallback("http://localhost:8080/callback");
        input.setQuery("test.event");
        EventSubscription created = eventSubscriptionRepoService.addEventSubscription(input);
        String subscriptionId = created.getId();

        // Try to delete as regular user (should be forbidden)
        mvc.perform(MockMvcRequestBuilders.delete("/productCatalogManagement/v4/hub/" + subscriptionId)
                        .with(SecurityMockMvcRequestPostProcessors.csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .accept(MediaType.APPLICATION_JSON))
                .andExpect(status().isForbidden());

        // Verify the subscription still exists
        assert eventSubscriptionRepoService.findById(subscriptionId) != null;
    }

    @Test
    public void testRegisterListenerUnauthenticated() throws Exception {
        File resourceSpecFile = new File("src/test/resources/testPCM620EventSubscriptionInput.json");
        InputStream in = new FileInputStream(resourceSpecFile);
        String eventSubscriptionInputString = IOUtils.toString(in, "UTF-8");
        EventSubscriptionInput eventSubscriptionInput = JsonUtils.toJsonObj(eventSubscriptionInputString, EventSubscriptionInput.class);

        mvc.perform(MockMvcRequestBuilders.post("/productCatalogManagement/v4/hub")
                        .with(SecurityMockMvcRequestPostProcessors.csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .accept(MediaType.APPLICATION_JSON)
                        .content(JsonUtils.toJson(eventSubscriptionInput)))
                .andExpect(status().isUnauthorized());
    }

    @Test
    public void testUnregisterListenerUnauthenticated() throws Exception {
        String testId = "test-subscription-id";

        mvc.perform(MockMvcRequestBuilders.delete("/productCatalogManagement/v4/hub/" + testId)
                        .with(SecurityMockMvcRequestPostProcessors.csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .accept(MediaType.APPLICATION_JSON))
                .andExpect(status().isUnauthorized());
    }
}
 No newline at end of file
Loading