Skip to content

Commit

Permalink
add REST controller for reading transactions (#910)
Browse files Browse the repository at this point in the history
  • Loading branch information
goekay committed Sep 30, 2022
1 parent bbd9d30 commit cf716b4
Show file tree
Hide file tree
Showing 4 changed files with 269 additions and 4 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
* SteVe - SteckdosenVerwaltung - https://github.com/steve-community/steve
* Copyright (C) 2013-2019 RWTH Aachen University - Information Systems - Intelligent Distributed Systems Group (IDSG).
* All Rights Reserved.
*
* This program 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.
*
* This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.rwth.idsg.steve.web.api;

import de.rwth.idsg.steve.repository.TransactionRepository;
import de.rwth.idsg.steve.repository.dto.Transaction;
import de.rwth.idsg.steve.web.api.ApiControllerAdvice.ApiErrorResponse;
import de.rwth.idsg.steve.web.dto.TransactionQueryForm;
import io.swagger.annotations.ApiResponse;
import io.swagger.annotations.ApiResponses;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException;

import javax.validation.Valid;
import java.util.List;

/**
* @author Sevket Goekay <sevketgokay@gmail.com>
* @since 13.09.2022
*/
@Slf4j
@RestController
@RequestMapping(value = "/api/v1/transactions", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
@RequiredArgsConstructor
public class TransactionsRestController {

private final TransactionRepository transactionRepository;

@ApiResponses(value = {
@ApiResponse(code = 200, message = "OK"),
@ApiResponse(code = 400, message = "Bad Request", response = ApiErrorResponse.class),
@ApiResponse(code = 401, message = "Unauthorized", response = ApiErrorResponse.class),
@ApiResponse(code = 403, message = "Forbidden", response = ApiErrorResponse.class),
@ApiResponse(code = 500, message = "Internal Server Error", response = ApiErrorResponse.class)}
)
@GetMapping(value = "")
@ResponseBody
public List<Transaction> get(@Valid TransactionQueryForm params) {
log.debug("Read request for query: {}", params);

if (params.isReturnCSV()) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "returnCSV=true is not supported for API calls");
}

var response = transactionRepository.getTransactions(params);
log.debug("Read response for query: {}", response);
return response;
}
}
11 changes: 11 additions & 0 deletions src/main/java/de/rwth/idsg/steve/web/dto/QueryForm.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
*/
package de.rwth.idsg.steve.web.dto;

import io.swagger.annotations.ApiModelProperty;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
Expand All @@ -36,25 +37,35 @@
@ToString
public abstract class QueryForm {

@ApiModelProperty(value = "The identifier of the chargebox (i.e. charging station)")
private String chargeBoxId;

@ApiModelProperty(value = "The OCPP tag")
private String ocppIdTag;

@ApiModelProperty(value = "A date/time without timezone. Example: 2022-10-10 09:00")
private LocalDateTime from;

@ApiModelProperty(value = "A date/time without timezone. Example: 2022-10-10 12:00")
private LocalDateTime to;

@ApiModelProperty(hidden = true)
@AssertTrue(message = "'To' must be after 'From'")
public boolean isFromToValid() {
return !isFromToSet() || to.isAfter(from);
}

@ApiModelProperty(hidden = true)
boolean isFromToSet() {
return from != null && to != null;
}

@ApiModelProperty(hidden = true)
public boolean isChargeBoxIdSet() {
return chargeBoxId != null;
}

@ApiModelProperty(hidden = true)
public boolean isOcppIdTagSet() {
return ocppIdTag != null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,13 @@
*/
package de.rwth.idsg.steve.web.dto;

import io.swagger.annotations.ApiModelProperty;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import lombok.ToString;

import javax.validation.constraints.AssertTrue;
import javax.validation.constraints.NotNull;
import java.util.Objects;

/**
Expand All @@ -37,20 +37,25 @@
public class TransactionQueryForm extends QueryForm {

// Internal database Id
@ApiModelProperty(value = "Database primary key of the transaction")
private Integer transactionPk;

/**
* Init with sensible default values
*/
@ApiModelProperty(value = "Disabled for the Web APIs. Do not use and set", hidden = true)
private boolean returnCSV = false;

@ApiModelProperty(value = "Return active or all transactions? Defaults to ALL")
private QueryType type = QueryType.ACTIVE;

@ApiModelProperty(value = "Return the time period of the transactions. If FROM_TO, 'from' and 'to' must be set. Additionally, 'to' must be after 'from'")
private QueryPeriodType periodType = QueryPeriodType.ALL;

@ApiModelProperty(hidden = true)
@AssertTrue(message = "The values 'From' and 'To' must be both set")
public boolean isPeriodFromToCorrect() {
return periodType != QueryPeriodType.FROM_TO || isFromToSet();
}

@ApiModelProperty(hidden = true)
public boolean isTransactionPkSet() {
return transactionPk != null;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
package de.rwth.idsg.steve.web.api;

import de.rwth.idsg.steve.repository.TransactionRepository;
import de.rwth.idsg.steve.repository.dto.Transaction;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultMatcher;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;

import java.util.Collections;
import java.util.List;

import static org.hamcrest.Matchers.hasSize;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
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;

/**
* @author Sevket Goekay <sevketgokay@gmail.com>
* @since 17.09.2022
*/
@ExtendWith(MockitoExtension.class)
public class TransactionRestControllerTest {

@Mock
private TransactionRepository transactionRepository;

private MockMvc mockMvc;

@BeforeEach
public void setup() {
mockMvc = MockMvcBuilders.standaloneSetup(new TransactionsRestController(transactionRepository))
.setControllerAdvice(new ApiControllerAdvice())
.setMessageConverters(new MappingJackson2HttpMessageConverter())
.alwaysExpect(content().contentType("application/json;charset=UTF-8"))
.build();
}

@Test
@DisplayName("Test with empty results, expected 200")
public void test1() throws Exception {
// given
List<Transaction> results = Collections.emptyList();

// when
when(transactionRepository.getTransactions(any())).thenReturn(results);

// then
mockMvc.perform(get("/api/v1/transactions"))
.andExpect(status().isOk())
.andExpect(jsonPath("$", hasSize(0)));
}

@Test
@DisplayName("Test with one result, expected 200")
public void test2() throws Exception {
// given
List<Transaction> results = List.of(Transaction.builder().id(234).build());

// when
when(transactionRepository.getTransactions(any())).thenReturn(results);

// then
mockMvc.perform(get("/api/v1/transactions"))
.andExpect(status().isOk())
.andExpect(jsonPath("$", hasSize(1)))
.andExpect(jsonPath("$[0].id").value("234"));
}


@Test
@DisplayName("Downstream bean throws exception, expected 500")
public void test3() throws Exception {
// when
when(transactionRepository.getTransactions(any())).thenThrow(new RuntimeException("failed"));

// then
mockMvc.perform(get("/api/v1/transactions"))
.andExpect(status().isInternalServerError())
.andExpectAll(errorJsonMatchers());
}


@Test
@DisplayName("Hidden param used, expected 400")
public void test4() throws Exception {
mockMvc.perform(get("/api/v1/transactions")
.param("returnCSV", "true")
)
.andExpect(status().isBadRequest())
.andExpectAll(errorJsonMatchers());
}


@Test
@DisplayName("Typo in param makes validation fail, expected 400")
public void test5() throws Exception {
mockMvc.perform(get("/api/v1/transactions")
.param("periodType", "TODAYZZZ")
)
.andExpect(status().isBadRequest())
.andExpectAll(errorJsonMatchers());
}

@Test
@DisplayName("Param requires other params which are not set, expected 400")
public void test6() throws Exception {
mockMvc.perform(get("/api/v1/transactions")
.param("periodType", "FROM_TO")
)
.andExpect(status().isBadRequest())
.andExpectAll(errorJsonMatchers());
}

@Test
@DisplayName("to is before from when using FROM_TO, expected 400")
public void test7() throws Exception {
mockMvc.perform(get("/api/v1/transactions")
.param("periodType", "FROM_TO")
.param("from", "2022-10-01 00:00")
.param("to", "2022-09-01 00:00")
)
.andExpect(status().isBadRequest())
.andExpectAll(errorJsonMatchers());
}

@Test
@DisplayName("Sets all valid params, expected 200")
public void test8() throws Exception {
// given
Transaction transaction = Transaction
.builder()
.id(1)
.chargeBoxId("cb-2")
.ocppIdTag("id-3")
.build();

// when
when(transactionRepository.getTransactions(any())).thenReturn(List.of(transaction));

// then
mockMvc.perform(get("/api/v1/transactions")
.param("transactionPk", String.valueOf(transaction.getId()))
.param("type", "ACTIVE")
.param("periodType", "FROM_TO")
.param("chargeBoxId", transaction.getChargeBoxId())
.param("ocppIdTag", transaction.getOcppIdTag())
.param("from", "2022-10-01 00:00")
.param("to", "2022-10-08 00:00")
)
.andExpect(status().isOk())
.andExpect(jsonPath("$", hasSize(1)))
.andExpect(jsonPath("$[0].id").value("1"))
.andExpect(jsonPath("$[0].chargeBoxId").value("cb-2"))
.andExpect(jsonPath("$[0].ocppIdTag").value("id-3"));
}


private static ResultMatcher[] errorJsonMatchers() {
return new ResultMatcher[] {
jsonPath("$.timestamp").exists(),
jsonPath("$.status").exists(),
jsonPath("$.error").exists(),
jsonPath("$.message").exists(),
jsonPath("$.path").exists()
};
}
}

0 comments on commit cf716b4

Please sign in to comment.