Skip to content

Commit f9fb22f

Browse files
authored
Add ReactController for serving React app and static assets (#114)
* Add ReactController for serving React app and static assets Introduce a dedicated `ReactController` to serve the React app, handle client-side routing, and deliver static resources like favicon and assets. Remove redundant logic from `StaticResource` and adjust `application.yml` by removing the unused webapp mapping. * Replace StaticResourceTest with ReactControllerTest Migrated tests from `StaticResourceTest` to a new `ReactControllerTest` to better align with updated resource handling logic. The new tests cover scenarios for serving the React index.html file and handling missing resources.
1 parent a1db842 commit f9fb22f

File tree

5 files changed

+151
-66
lines changed

5 files changed

+151
-66
lines changed
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package com.jongsoft.finance.rest;
2+
3+
import io.micronaut.core.io.ResourceResolver;
4+
import io.micronaut.http.HttpResponse;
5+
import io.micronaut.http.MediaType;
6+
import io.micronaut.http.annotation.Controller;
7+
import io.micronaut.http.annotation.Get;
8+
import io.micronaut.http.server.types.files.StreamedFile;
9+
import io.micronaut.security.annotation.Secured;
10+
import io.micronaut.security.rules.SecurityRule;
11+
12+
import java.io.InputStream;
13+
import java.nio.charset.StandardCharsets;
14+
import java.util.Optional;
15+
16+
@Controller("/ui")
17+
@Secured(SecurityRule.IS_ANONYMOUS)
18+
public class ReactController {
19+
20+
private final ResourceResolver resourceResolver;
21+
22+
public ReactController(ResourceResolver resourceResolver) {
23+
this.resourceResolver = resourceResolver;
24+
}
25+
26+
/**
27+
* Serves the React app's index.html file.
28+
*
29+
* @return The index.html file
30+
*/
31+
@Get(produces = MediaType.TEXT_HTML)
32+
public HttpResponse<?> index() {
33+
Optional<InputStream> indexHtml = resourceResolver.getResourceAsStream("classpath:public/index.html");
34+
if (indexHtml.isPresent()) {
35+
return HttpResponse.ok(new StreamedFile(indexHtml.get(), MediaType.TEXT_HTML_TYPE));
36+
} else {
37+
return HttpResponse.notFound("React app not found");
38+
}
39+
}
40+
41+
/**
42+
* Catch-all route to serve the React app for any path under /react/.
43+
* This allows the React Router to handle client-side routing properly.
44+
*
45+
* @return The index.html file
46+
*/
47+
@Get(uri = "/{path:.*}", produces = MediaType.TEXT_HTML)
48+
public HttpResponse<?>catchAll(String path) {
49+
return index();
50+
}
51+
52+
@Get(uri = "/favicon.ico")
53+
public HttpResponse<?> favicon() {
54+
return loadResource("favicon.ico");
55+
}
56+
57+
@Get(uri = "/manifest.json")
58+
public HttpResponse<?> manifest() {
59+
return loadResource("manifest.json");
60+
}
61+
62+
@Get(uri = "/logo192.png")
63+
public HttpResponse<?> logo() {
64+
return loadResource("logo192.png");
65+
}
66+
67+
@Get(uri = "/logo512.png")
68+
public HttpResponse<?> logo_512() {
69+
return loadResource("logo512.png");
70+
}
71+
72+
@Get(uri = "/assets/{path:.*}")
73+
public HttpResponse<?> getAsset(String path) {
74+
return loadResource("assets/" + path);
75+
}
76+
77+
@Get(uri = "/images/{path:.*}")
78+
public HttpResponse<?> getImage(String path) {
79+
return loadResource("images/" + path);
80+
}
81+
82+
private HttpResponse<?> loadResource(String path) {
83+
var assetFile = resourceResolver.getResource("classpath:public/" + path);
84+
if (assetFile.isPresent()) {
85+
var streamedFile = new StreamedFile(assetFile.get());
86+
return HttpResponse.ok(streamedFile.getInputStream())
87+
.contentType(streamedFile.getMediaType())
88+
.characterEncoding(StandardCharsets.UTF_8);
89+
} else {
90+
return HttpResponse.notFound("React app not found");
91+
}
92+
}
93+
}

fintrack-api/src/main/java/com/jongsoft/finance/rest/StaticResource.java

Lines changed: 8 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
11
package com.jongsoft.finance.rest;
22

3-
import com.jongsoft.finance.core.exception.StatusException;
43
import io.micronaut.core.io.ResourceResolver;
54
import io.micronaut.http.HttpResponse;
65
import io.micronaut.http.MediaType;
76
import io.micronaut.http.annotation.Controller;
87
import io.micronaut.http.annotation.Get;
9-
import io.micronaut.http.annotation.PathVariable;
10-
import io.micronaut.http.annotation.Produces;
118
import io.micronaut.http.server.types.files.StreamedFile;
129
import io.micronaut.security.annotation.Secured;
1310
import io.micronaut.security.rules.SecurityRule;
@@ -16,18 +13,18 @@
1613
import org.slf4j.Logger;
1714
import org.slf4j.LoggerFactory;
1815

19-
import java.io.IOException;
16+
import java.io.InputStream;
2017
import java.net.URI;
2118
import java.net.URISyntaxException;
22-
import java.net.URL;
19+
import java.util.Optional;
2320

2421
@Controller
2522
@Secured(SecurityRule.IS_ANONYMOUS)
2623
public class StaticResource {
2724
private final Logger log = LoggerFactory.getLogger(StaticResource.class);
2825

2926
@Inject
30-
ResourceResolver res;
27+
ResourceResolver resourceResolver;
3128

3229
@Get
3330
@Operation(hidden = true)
@@ -38,39 +35,11 @@ public HttpResponse<?> index() throws URISyntaxException {
3835
@Get("/favicon.ico")
3936
@Operation(hidden = true)
4037
public HttpResponse<?> favicon() {
41-
return resource("assets/favicon.ico");
42-
}
43-
44-
@Operation(hidden = true)
45-
@Get("/ui/{path:([^\\.]+)$}")
46-
@Produces(MediaType.TEXT_HTML)
47-
public HttpResponse<?> refresh(@PathVariable String path) {
48-
return resource("index.html");
49-
}
50-
51-
@Operation(hidden = true)
52-
@Get("/ui/{path:(.+)\\.[\\w]+$}")
53-
public HttpResponse<?> resource(@PathVariable String path) {
54-
log.info("Loading static resource: {}", path);
55-
56-
var resource = res.getResource("classpath:public/" + path);
57-
if (resource.isEmpty()) {
58-
return HttpResponse.notFound("Resource at path " + path + " is not found on the server");
59-
}
60-
61-
return loadFromUri(resource.get());
62-
}
63-
64-
private HttpResponse<?> loadFromUri(URL uri) {
65-
var streamedFile = new StreamedFile(uri);
66-
67-
try {
68-
return HttpResponse.ok(
69-
streamedFile.getInputStream().readAllBytes())
70-
.contentType(streamedFile.getMediaType())
71-
.characterEncoding("utf-8");
72-
} catch (IOException e) {
73-
throw StatusException.internalError(e.getMessage());
38+
Optional<InputStream> indexHtml = resourceResolver.getResourceAsStream("classpath:public/assets/favicon.ico");
39+
if (indexHtml.isPresent()) {
40+
return HttpResponse.ok(new StreamedFile(indexHtml.get(), MediaType.IMAGE_X_ICON_TYPE));
41+
} else {
42+
return HttpResponse.notFound("Favicon not found");
7443
}
7544
}
7645

fintrack-api/src/main/resources/application.yml

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,7 @@ micronaut:
3232
swagger:
3333
paths: classpath:META-INF/swagger
3434
mapping: /spec/**
35-
webapp:
36-
paths: classpath:/public
37-
mapping: /ui/**
35+
3836
server:
3937
cors:
4038
enabled: true
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package com.jongsoft.finance.rest;
2+
3+
import io.micronaut.core.io.ResourceResolver;
4+
import io.micronaut.http.HttpResponse;
5+
import io.micronaut.http.MediaType;
6+
import io.micronaut.http.server.types.files.StreamedFile;
7+
import org.junit.jupiter.api.Test;
8+
9+
import java.io.ByteArrayInputStream;
10+
import java.io.InputStream;
11+
import java.util.Optional;
12+
13+
import static org.junit.jupiter.api.Assertions.assertEquals;
14+
import static org.mockito.Mockito.*;
15+
16+
class ReactControllerTest {
17+
18+
@Test
19+
void testIndexReturnsStreamedFileWhenResourceExists() {
20+
// Arrange
21+
ResourceResolver resourceResolver = mock(ResourceResolver.class);
22+
ReactController reactController = new ReactController(resourceResolver);
23+
InputStream mockStream = new ByteArrayInputStream("<html>Mock index.html</html>".getBytes());
24+
when(resourceResolver.getResourceAsStream("classpath:public/index.html")).thenReturn(Optional.of(mockStream));
25+
26+
// Act
27+
HttpResponse<?> response = reactController.index();
28+
29+
// Assert
30+
assertEquals(HttpResponse.ok().body(new StreamedFile(mockStream, MediaType.TEXT_HTML_TYPE)).getStatus(), response.getStatus());
31+
verify(resourceResolver, times(1)).getResourceAsStream("classpath:public/index.html");
32+
}
33+
34+
@Test
35+
void testIndexReturnsNotFoundWhenResourceDoesNotExist() {
36+
// Arrange
37+
ResourceResolver resourceResolver = mock(ResourceResolver.class);
38+
ReactController reactController = new ReactController(resourceResolver);
39+
when(resourceResolver.getResourceAsStream("classpath:public/index.html")).thenReturn(Optional.empty());
40+
41+
// Act
42+
HttpResponse<?> response = reactController.index();
43+
44+
// Assert
45+
assertEquals(HttpResponse.notFound("React app not found").getStatus(), response.getStatus());
46+
verify(resourceResolver, times(1)).getResourceAsStream("classpath:public/index.html");
47+
}
48+
}

fintrack-api/src/test/java/com/jongsoft/finance/rest/StaticResourceTest.java

Lines changed: 1 addition & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,14 @@
44
import io.micronaut.core.io.scan.DefaultClassPathResourceLoader;
55
import org.junit.jupiter.api.BeforeEach;
66
import org.junit.jupiter.api.Test;
7-
import org.junit.jupiter.api.extension.ExtendWith;
87
import org.mockito.InjectMocks;
98
import org.mockito.MockitoAnnotations;
109
import org.mockito.Spy;
1110

12-
import java.io.IOException;
1311
import java.net.URISyntaxException;
1412
import java.util.List;
1513

1614
import static org.assertj.core.api.Assertions.assertThat;
17-
import static org.junit.jupiter.api.Assertions.*;
1815

1916
class StaticResourceTest {
2017

@@ -37,24 +34,4 @@ void index() throws URISyntaxException {
3734
assertThat(response.getHeaders().get("Location")).isEqualTo("/ui/dashboard");
3835
}
3936

40-
@Test
41-
void refresh() {
42-
var response = subject.refresh("");
43-
44-
assertThat(new String((byte[]) response.body())).isEqualTo("It works!!!!");
45-
}
46-
47-
@Test
48-
void resource() {
49-
var response = subject.resource("css/style.css");
50-
51-
assertThat(new String((byte[]) response.body())).isEqualTo("Style works");
52-
}
53-
54-
@Test
55-
void resource_notFound() {
56-
var response = subject.resource("css/style-2.css");
57-
58-
assertThat(response.getStatus().getCode()).isEqualTo(404);
59-
}
60-
}
37+
}

0 commit comments

Comments
 (0)