Add update and delete handlers for Extensions (#2172)

pull/2180/head
John Niang 2022-06-21 19:04:21 +08:00 committed by GitHub
parent b9e5ed2f4c
commit d556787b3a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 516 additions and 84 deletions

View File

@ -2,8 +2,12 @@ package run.halo.app.extension;
import static org.springframework.web.reactive.function.server.RouterFunctions.route;
import java.net.URI;
import java.time.Instant;
import java.util.Objects;
import org.springframework.http.MediaType;
import org.springframework.lang.NonNull;
import org.springframework.util.StringUtils;
import org.springframework.web.reactive.function.server.HandlerFunction;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerRequest;
@ -28,11 +32,15 @@ public class ExtensionRouterFunctionFactory {
var getHandler = new ExtensionGetHandler(scheme, client);
var listHandler = new ExtensionListHandler(scheme, client);
var createHandler = new ExtensionCreateHandler(scheme, client);
var updateHandler = new ExtensionUpdateHandler(scheme, client);
var deleteHandler = new ExtensionDeleteHandler(scheme, client);
// TODO More handlers here
return route()
.GET(getHandler.pathPattern(), getHandler)
.GET(listHandler.pathPattern(), listHandler)
.POST(createHandler.pathPattern(), createHandler)
.PUT(updateHandler.pathPattern(), updateHandler)
.DELETE(deleteHandler.pathPattern(), deleteHandler)
.build();
}
@ -65,6 +73,14 @@ public class ExtensionRouterFunctionFactory {
}
interface UpdateHandler extends HandlerFunction<ServerResponse>, PathPatternGenerator {
}
interface DeleteHandler extends HandlerFunction<ServerResponse>, PathPatternGenerator {
}
static class ExtensionCreateHandler implements CreateHandler {
private final Scheme scheme;
@ -82,16 +98,17 @@ public class ExtensionRouterFunctionFactory {
return request.bodyToMono(Unstructured.class)
.switchIfEmpty(Mono.error(() -> new ExtensionConvertException(
"Cannot read body to " + scheme.groupVersionKind())))
.doOnSuccess(client::create)
.map(unstructured ->
client.fetch(scheme.type(), unstructured.getMetadata().getName())
.flatMap(extToCreate -> Mono.fromCallable(() -> {
var name = extToCreate.getMetadata().getName();
client.create(extToCreate);
return client.fetch(scheme.type(), name)
.orElseThrow(() -> new ExtensionNotFoundException(
scheme.groupVersionKind() + " " + unstructured.getMetadata().getName()
+ "was not found")))
.flatMap(extension -> ServerResponse
.ok()
"Extension with name " + name + " was not found"));
}))
.flatMap(createdExt -> ServerResponse
.created(URI.create(pathPattern() + "/" + createdExt.getMetadata().getName()))
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(extension))
.bodyValue(createdExt))
.cast(ServerResponse.class);
}
@ -159,4 +176,81 @@ public class ExtensionRouterFunctionFactory {
}
}
static class ExtensionUpdateHandler implements UpdateHandler {
private final Scheme scheme;
private final ExtensionClient client;
ExtensionUpdateHandler(Scheme scheme, ExtensionClient client) {
this.scheme = scheme;
this.client = client;
}
@Override
public Mono<ServerResponse> handle(ServerRequest request) {
String name = request.pathVariable("name");
return request.bodyToMono(Unstructured.class)
.filter(unstructured -> unstructured.getMetadata() != null
&& StringUtils.hasText(unstructured.getMetadata().getName())
&& Objects.equals(unstructured.getMetadata().getName(), name))
.switchIfEmpty(Mono.error(() -> new ExtensionConvertException(
"Cannot read body to " + scheme.groupVersionKind())))
.flatMap(extToUpdate -> Mono.fromCallable(() -> {
client.update(extToUpdate);
return client.fetch(scheme.type(), name)
.orElseThrow(() -> new ExtensionNotFoundException(
"Extension with name " + name + " was not found"));
}))
.flatMap(updated -> ServerResponse
.ok()
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(updated));
}
@Override
public String pathPattern() {
return PathPatternGenerator.buildExtensionPathPattern(scheme) + "/{name}";
}
}
static class ExtensionDeleteHandler implements DeleteHandler {
private final Scheme scheme;
private final ExtensionClient client;
ExtensionDeleteHandler(Scheme scheme, ExtensionClient client) {
this.scheme = scheme;
this.client = client;
}
@Override
public Mono<ServerResponse> handle(ServerRequest request) {
String name = request.pathVariable("name");
return getExtension(name)
.flatMap(extension ->
Mono.fromRunnable(() -> {
extension.getMetadata().setDeletionTimestamp(Instant.now());
client.update(extension);
}).thenReturn(extension))
.flatMap(extension -> this.getExtension(name))
.flatMap(extension -> ServerResponse
.ok()
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(extension));
}
private Mono<? extends Extension> getExtension(String name) {
return Mono.justOrEmpty(client.fetch(scheme.type(), name))
.switchIfEmpty(Mono.error(() -> new ExtensionNotFoundException(
"Extension with name " + name + " was not found")));
}
@Override
public String pathPattern() {
return PathPatternGenerator.buildExtensionPathPattern(scheme) + "/{name}";
}
}
}

View File

@ -1,25 +1,30 @@
package run.halo.app.config;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.when;
import java.time.Instant;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.web.reactive.server.WebTestClient;
import run.halo.app.core.extension.Role;
import run.halo.app.core.extension.service.RoleService;
import run.halo.app.extension.ExtensionClient;
import run.halo.app.extension.FakeExtension;
import run.halo.app.extension.Metadata;
import run.halo.app.extension.Scheme;
@ -27,7 +32,6 @@ import run.halo.app.extension.SchemeManager;
@SpringBootTest
@AutoConfigureWebTestClient
@AutoConfigureTestDatabase
class ExtensionConfigurationTest {
@Autowired
@ -50,17 +54,18 @@ class ExtensionConfigurationTest {
var role = new Role();
role.setRules(List.of(rule));
when(roleService.getRole(anyString())).thenReturn(role);
}
@AfterEach
void cleanUp() {
schemeManager.fetch(Scheme.buildFromType(FakeExtension.class).groupVersionKind())
.ifPresent(schemeManager::unregister);
// register scheme
schemeManager.register(FakeExtension.class);
}
@Test
@WithMockUser
void shouldReturnNotFoundWhenSchemeNotRegistered() {
// unregister the Extension if necessary
schemeManager.fetch(Scheme.buildFromType(FakeExtension.class).groupVersionKind())
.ifPresent(schemeManager::unregister);
webClient.get()
.uri("/apis/fake.halo.run/v1alpha1/fakes")
.exchange()
@ -76,71 +81,158 @@ class ExtensionConfigurationTest {
.bodyValue(new FakeExtension())
.exchange()
.expectStatus().isNotFound();
}
@Test
@WithMockUser
void shouldListExtensionsWhenSchemeRegistered() {
schemeManager.register(FakeExtension.class);
webClient.get()
.uri("/apis/fake.halo.run/v1alpha1/fakes")
webClient.put()
.uri("/apis/fake.halo.run/v1alpha1/fakes/my-fake")
.bodyValue(new FakeExtension())
.exchange()
.expectStatus().isOk();
}
.expectStatus().isNotFound();
@Test
@WithMockUser
@DirtiesContext(methodMode = DirtiesContext.MethodMode.AFTER_METHOD)
void shouldCreateExtensionWhenSchemeRegistered() {
schemeManager.register(FakeExtension.class);
getCreateExtensionResponse()
.expectStatus().isOk()
.expectBody(FakeExtension.class)
.consumeWith(result -> {
var gotFake = result.getResponseBody();
assertNotNull(gotFake);
assertEquals("my-fake", gotFake.getMetadata().getName());
assertNotNull(gotFake.getMetadata().getVersion());
assertNotNull(gotFake.getMetadata().getCreationTimestamp());
});
}
@Test
@WithMockUser
@DirtiesContext(methodMode = DirtiesContext.MethodMode.AFTER_METHOD)
void shouldGetExtensionWhenSchemeRegistered() {
schemeManager.register(FakeExtension.class);
// create the Extension
getCreateExtensionResponse().expectStatus().isOk();
webClient.get()
webClient.delete()
.uri("/apis/fake.halo.run/v1alpha1/fakes/my-fake")
.exchange()
.expectStatus().isOk()
.expectBody(FakeExtension.class)
.consumeWith(result -> {
var gotFake = result.getResponseBody();
assertNotNull(gotFake);
assertEquals("my-fake", gotFake.getMetadata().getName());
assertNotNull(gotFake.getMetadata().getVersion());
assertNotNull(gotFake.getMetadata().getCreationTimestamp());
});
.expectStatus().isNotFound();
}
WebTestClient.ResponseSpec getCreateExtensionResponse() {
var metadata = new Metadata();
metadata.setName("my-fake");
var fake = new FakeExtension();
fake.setMetadata(metadata);
@Nested
@DisplayName("After creating extension")
class AfterCreatingExtension {
@Autowired
ExtensionClient extClient;
FakeExtension createdFake;
@BeforeEach
void setUp() {
var metadata = new Metadata();
metadata.setName("my-fake");
var fake = new FakeExtension();
fake.setMetadata(metadata);
createdFake = webClient.post()
.uri("/apis/fake.halo.run/v1alpha1/fakes")
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(fake)
.exchange()
.expectStatus().isCreated()
.expectHeader().location("/apis/fake.halo.run/v1alpha1/fakes/my-fake")
.expectBody(FakeExtension.class)
.consumeWith(result -> {
var gotFake = result.getResponseBody();
assertNotNull(gotFake);
assertEquals("my-fake", gotFake.getMetadata().getName());
assertNotNull(gotFake.getMetadata().getVersion());
assertNotNull(gotFake.getMetadata().getCreationTimestamp());
})
.returnResult()
.getResponseBody();
}
@AfterEach
void cleanUp() {
FakeExtension fakeToDelete = getFakeExtension(createdFake.getMetadata().getName());
extClient.delete(fakeToDelete);
}
@Test
@WithMockUser
void shouldDeleteExtensionWhenSchemeRegistered() {
webClient.delete()
.uri("/apis/fake.halo.run/v1alpha1/fakes/{name}",
createdFake.getMetadata().getName())
.exchange()
.expectStatus().isOk()
.expectHeader().contentType(MediaType.APPLICATION_JSON)
.expectBody(FakeExtension.class)
.consumeWith(result -> {
var deletedFake = result.getResponseBody();
assertNotNull(deletedFake);
assertNotNull(deletedFake.getMetadata().getDeletionTimestamp());
assertTrue(deletedFake.getMetadata().getDeletionTimestamp()
.isBefore(Instant.now()));
});
}
@Test
@WithMockUser
void shouldListExtensionsWhenSchemeRegistered() {
webClient.get()
.uri("/apis/fake.halo.run/v1alpha1/fakes")
.exchange()
.expectStatus().isOk()
.expectBodyList(FakeExtension.class)
.hasSize(1);
}
@Test
@WithMockUser
void shouldUpdateExtensionWhenSchemeRegistered() {
var name = createdFake.getMetadata().getName();
FakeExtension fakeToUpdate = getFakeExtension(name);
fakeToUpdate.getMetadata().setLabels(Map.of("updated", "true"));
webClient.put()
.uri("/apis/fake.halo.run/v1alpha1/fakes/{name}", name)
.bodyValue(fakeToUpdate)
.exchange()
.expectStatus().isOk()
.expectHeader().contentType(MediaType.APPLICATION_JSON)
.expectBody(FakeExtension.class)
.consumeWith(result -> {
var updatedFake = result.getResponseBody();
assertNotNull(updatedFake);
assertNotEquals(fakeToUpdate.getMetadata().getVersion(),
updatedFake.getMetadata().getVersion());
assertEquals(Map.of("updated", "true"),
updatedFake.getMetadata().getLabels());
});
}
@Test
@WithMockUser
void shouldGetExtensionWhenSchemeRegistered() {
var name = createdFake.getMetadata().getName();
webClient.get()
.uri("/apis/fake.halo.run/v1alpha1/fakes/{name}", name)
.exchange()
.expectStatus().isOk()
.expectBody(FakeExtension.class)
.consumeWith(result -> {
var gotFake = result.getResponseBody();
assertNotNull(gotFake);
assertEquals(name, gotFake.getMetadata().getName());
assertNotNull(gotFake.getMetadata().getVersion());
assertNotNull(gotFake.getMetadata().getCreationTimestamp());
});
}
FakeExtension getFakeExtension(String name) {
return webClient.get()
.uri("/apis/fake.halo.run/v1alpha1/fakes/{name}", name)
.exchange()
.expectStatus().isOk()
.expectBody(FakeExtension.class)
.returnResult()
.getResponseBody();
}
WebTestClient.ResponseSpec getCreateExtensionResponse() {
var metadata = new Metadata();
metadata.setName("my-fake");
var fake = new FakeExtension();
fake.setMetadata(metadata);
return webClient.post()
.uri("/apis/fake.halo.run/v1alpha1/fakes")
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(fake)
.exchange();
}
return webClient.post()
.uri("/apis/fake.halo.run/v1alpha1/fakes")
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(fake)
.exchange();
}
}

View File

@ -2,11 +2,16 @@ package run.halo.app.extension;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.util.Objects;
import java.util.Optional;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@ -31,8 +36,8 @@ class ExtensionCreateHandlerTest {
@Test
void shouldBuildPathPatternCorrectly() {
var scheme = Scheme.buildFromType(FakeExtension.class);
var getHandler = new ExtensionCreateHandler(scheme, client);
var pathPattern = getHandler.pathPattern();
var createHandler = new ExtensionCreateHandler(scheme, client);
var pathPattern = createHandler.pathPattern();
assertEquals("/apis/fake.halo.run/v1alpha1/fakes", pathPattern);
}
@ -58,7 +63,9 @@ class ExtensionCreateHandlerTest {
StepVerifier.create(responseMono)
.consumeNextWith(response -> {
assertEquals(HttpStatus.OK, response.statusCode());
assertEquals(HttpStatus.CREATED, response.statusCode());
assertEquals("/apis/fake.halo.run/v1alpha1/fakes/my-fake",
response.headers().getLocation().toString());
assertEquals(MediaType.APPLICATION_JSON, response.headers().getContentType());
assertTrue(response instanceof EntityResponse<?>);
assertEquals(fake, ((EntityResponse<?>) response).entity());
@ -90,13 +97,16 @@ class ExtensionCreateHandlerTest {
var serverRequest = MockServerRequest.builder()
.body(Mono.just(unstructured));
when(client.fetch(eq(FakeExtension.class), eq("my-fake"))).thenReturn(Optional.empty());
doThrow(ExtensionNotFoundException.class).when(client).create(any());
var scheme = Scheme.buildFromType(FakeExtension.class);
var getHandler = new ExtensionCreateHandler(scheme, client);
var responseMono = getHandler.handle(serverRequest);
var createHandler = new ExtensionCreateHandler(scheme, client);
var responseMono = createHandler.handle(serverRequest);
StepVerifier.create(responseMono)
.verifyError(ExtensionNotFoundException.class);
verify(client, times(1)).create(
argThat(extension -> Objects.equals("my-fake", extension.getMetadata().getName())));
verify(client, times(0)).fetch(any(), anyString());
}
}

View File

@ -0,0 +1,106 @@
package run.halo.app.extension;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.util.Optional;
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.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.mock.web.reactive.function.server.MockServerRequest;
import org.springframework.web.reactive.function.server.EntityResponse;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import run.halo.app.extension.ExtensionRouterFunctionFactory.ExtensionDeleteHandler;
import run.halo.app.extension.exception.ExtensionNotFoundException;
@ExtendWith(MockitoExtension.class)
class ExtensionDeleteHandlerTest {
@Mock
ExtensionClient client;
@Test
void shouldBuildPathPatternCorrectly() {
var scheme = Scheme.buildFromType(FakeExtension.class);
var deleteHandler = new ExtensionDeleteHandler(scheme, client);
var pathPattern = deleteHandler.pathPattern();
assertEquals("/apis/fake.halo.run/v1alpha1/fakes/{name}", pathPattern);
}
@Test
void shouldHandleCorrectly() {
final var fake = new FakeExtension();
var metadata = new Metadata();
metadata.setName("my-fake");
fake.setMetadata(metadata);
var unstructured = new Unstructured();
unstructured.setMetadata(metadata);
unstructured.setApiVersion("fake.halo.run/v1alpha1");
unstructured.setKind("Fake");
var serverRequest = MockServerRequest.builder()
.pathVariable("name", "my-fake")
.body(Mono.just(unstructured));
when(client.fetch(eq(FakeExtension.class), eq("my-fake"))).thenReturn(Optional.of(fake));
doNothing().when(client).update(any());
var scheme = Scheme.buildFromType(FakeExtension.class);
var deleteHandler = new ExtensionDeleteHandler(scheme, client);
var responseMono = deleteHandler.handle(serverRequest);
StepVerifier.create(responseMono)
.assertNext(response -> {
assertEquals(HttpStatus.OK, response.statusCode());
assertEquals(MediaType.APPLICATION_JSON, response.headers().getContentType());
assertTrue(response instanceof EntityResponse<?>);
assertEquals(fake, ((EntityResponse<?>) response).entity());
})
.verifyComplete();
verify(client, times(2)).fetch(eq(FakeExtension.class), eq("my-fake"));
verify(client, times(1)).update(
argThat(fakeToDelete -> fakeToDelete.getMetadata().getDeletionTimestamp() != null));
verify(client, times(0)).delete(any());
}
@Test
void shouldReturnErrorWhenNoNameProvided() {
var serverRequest = MockServerRequest.builder()
.body(Mono.empty());
var scheme = Scheme.buildFromType(FakeExtension.class);
var deleteHandler = new ExtensionDeleteHandler(scheme, client);
assertThrows(IllegalArgumentException.class, () -> deleteHandler.handle(serverRequest));
}
@Test
void shouldReturnErrorWhenExtensionNotFound() {
var serverRequest = MockServerRequest.builder()
.pathVariable("name", "my-fake")
.build();
when(client.fetch(FakeExtension.class, "my-fake")).thenReturn(Optional.empty());
var scheme = Scheme.buildFromType(FakeExtension.class);
var deleteHandler = new ExtensionDeleteHandler(scheme, client);
var responseMono = deleteHandler.handle(serverRequest);
StepVerifier.create(responseMono)
.verifyError(ExtensionNotFoundException.class);
verify(client, times(1)).fetch(any(), anyString());
verify(client, times(0)).update(any());
verify(client, times(0)).delete(any());
}
}

View File

@ -27,20 +27,20 @@ class ExtensionListHandlerTest {
@Test
void shouldBuildPathPatternCorrectly() {
var scheme = Scheme.buildFromType(FakeExtension.class);
var getHandler = new ExtensionListHandler(scheme, client);
var pathPattern = getHandler.pathPattern();
var listHandler = new ExtensionListHandler(scheme, client);
var pathPattern = listHandler.pathPattern();
assertEquals("/apis/fake.halo.run/v1alpha1/fakes", pathPattern);
}
@Test
void shouldHandleCorrectly() {
var scheme = Scheme.buildFromType(FakeExtension.class);
var getHandler = new ExtensionListHandler(scheme, client);
var listHandler = new ExtensionListHandler(scheme, client);
var serverRequest = MockServerRequest.builder().build();
final var fake = new FakeExtension();
when(client.list(eq(FakeExtension.class), any(), any())).thenReturn(List.of(fake));
var responseMono = getHandler.handle(serverRequest);
var responseMono = listHandler.handle(serverRequest);
StepVerifier.create(responseMono)
.consumeNextWith(response -> {

View File

@ -18,6 +18,7 @@ import org.springframework.web.server.ServerWebExchange;
import run.halo.app.extension.ExtensionRouterFunctionFactory.CreateHandler;
import run.halo.app.extension.ExtensionRouterFunctionFactory.GetHandler;
import run.halo.app.extension.ExtensionRouterFunctionFactory.ListHandler;
import run.halo.app.extension.ExtensionRouterFunctionFactory.UpdateHandler;
@ExtendWith(MockitoExtension.class)
class ExtensionRouterFunctionFactoryTest {
@ -53,10 +54,15 @@ class ExtensionRouterFunctionFactoryTest {
MockServerHttpRequest.post("/apis/fake.halo.run/v1alpha1/fakes").body("{}")
);
var updateWebExchange = MockServerWebExchange.from(
MockServerHttpRequest.put("/apis/fake.halo.run/v1alpha1/fakes/my-fake").body("{}")
);
return List.of(
new TestCase(listWebExchange, ListHandler.class),
new TestCase(getWebExchange, GetHandler.class),
new TestCase(createWebExchange, CreateHandler.class)
new TestCase(createWebExchange, CreateHandler.class),
new TestCase(updateWebExchange, UpdateHandler.class)
);
}

View File

@ -0,0 +1,124 @@
package run.halo.app.extension;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.util.Objects;
import java.util.Optional;
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.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.mock.web.reactive.function.server.MockServerRequest;
import org.springframework.web.reactive.function.server.EntityResponse;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import run.halo.app.extension.ExtensionRouterFunctionFactory.ExtensionUpdateHandler;
import run.halo.app.extension.exception.ExtensionConvertException;
import run.halo.app.extension.exception.ExtensionNotFoundException;
@ExtendWith(MockitoExtension.class)
class ExtensionUpdateHandlerTest {
@Mock
ExtensionClient client;
@Test
void shouldBuildPathPatternCorrectly() {
var scheme = Scheme.buildFromType(FakeExtension.class);
var updateHandler = new ExtensionUpdateHandler(scheme, client);
var pathPattern = updateHandler.pathPattern();
assertEquals("/apis/fake.halo.run/v1alpha1/fakes/{name}", pathPattern);
}
@Test
void shouldHandleCorrectly() {
final var fake = new FakeExtension();
var metadata = new Metadata();
metadata.setName("my-fake");
fake.setMetadata(metadata);
var unstructured = new Unstructured();
unstructured.setMetadata(metadata);
unstructured.setApiVersion("fake.halo.run/v1alpha1");
unstructured.setKind("Fake");
var serverRequest = MockServerRequest.builder()
.pathVariable("name", "my-fake")
.body(Mono.just(unstructured));
when(client.fetch(eq(FakeExtension.class), eq("my-fake"))).thenReturn(Optional.of(fake));
var scheme = Scheme.buildFromType(FakeExtension.class);
var updateHandler = new ExtensionUpdateHandler(scheme, client);
var responseMono = updateHandler.handle(serverRequest);
StepVerifier.create(responseMono)
.assertNext(response -> {
assertEquals(HttpStatus.OK, response.statusCode());
assertEquals(MediaType.APPLICATION_JSON, response.headers().getContentType());
assertTrue(response instanceof EntityResponse<?>);
assertEquals(fake, ((EntityResponse<?>) response).entity());
})
.verifyComplete();
verify(client, times(1)).fetch(eq(FakeExtension.class), eq("my-fake"));
verify(client, times(1)).update(eq(unstructured));
}
@Test
void shouldReturnErrorWhenNoBodyProvided() {
var serverRequest = MockServerRequest.builder()
.pathVariable("name", "my-fake")
.body(Mono.empty());
var scheme = Scheme.buildFromType(FakeExtension.class);
var updateHandler = new ExtensionUpdateHandler(scheme, client);
var responseMono = updateHandler.handle(serverRequest);
StepVerifier.create(responseMono)
.verifyError(ExtensionConvertException.class);
}
@Test
void shouldReturnErrorWhenNoNameProvided() {
var serverRequest = MockServerRequest.builder()
.body(Mono.empty());
var scheme = Scheme.buildFromType(FakeExtension.class);
var updateHandler = new ExtensionUpdateHandler(scheme, client);
assertThrows(IllegalArgumentException.class, () -> updateHandler.handle(serverRequest));
}
@Test
void shouldReturnErrorWhenExtensionNotFound() {
final var unstructured = new Unstructured();
var metadata = new Metadata();
metadata.setName("my-fake");
unstructured.setMetadata(metadata);
unstructured.setApiVersion("fake.halo.run/v1alpha1");
unstructured.setKind("Fake");
var serverRequest = MockServerRequest.builder()
.pathVariable("name", "my-fake")
.body(Mono.just(unstructured));
doThrow(ExtensionNotFoundException.class).when(client).update(any());
var scheme = Scheme.buildFromType(FakeExtension.class);
var updateHandler = new ExtensionUpdateHandler(scheme, client);
var responseMono = updateHandler.handle(serverRequest);
StepVerifier.create(responseMono)
.verifyError(ExtensionNotFoundException.class);
verify(client, times(1)).update(
argThat(extension -> Objects.equals("my-fake", extension.getMetadata().getName())));
verify(client, times(0)).fetch(any(), anyString());
}
}