Skip to content

Commit 73793ba

Browse files
committed
add remove review feature
1 parent edcf0b7 commit 73793ba

15 files changed

Lines changed: 331 additions & 45 deletions

File tree

server/src/main/java/org/eclipse/openvsx/admin/AdminAPI.java

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,42 @@ public ResponseEntity<ResultJson> deleteExtension(
277277
}
278278
}
279279

280+
@PostMapping(
281+
path = "/admin/extension/{namespace}/{extension}/review/{reviewId}/delete",
282+
produces = MediaType.APPLICATION_JSON_VALUE
283+
)
284+
@CrossOrigin
285+
@Operation(summary = "Delete a review for an extension")
286+
@ApiResponse(
287+
responseCode = "200",
288+
description = "A success message is returned in JSON format",
289+
content = @Content(schema = @Schema(implementation = ResultJson.class))
290+
)
291+
@ApiResponse(
292+
responseCode = "404",
293+
description = "Extension not found",
294+
content = @Content()
295+
)
296+
@ApiResponse(
297+
responseCode = "404",
298+
description = "Review not found",
299+
content = @Content()
300+
)
301+
public ResponseEntity<ResultJson> deleteReview(
302+
@PathVariable String namespace,
303+
@PathVariable String extension,
304+
@PathVariable String reviewId
305+
) {
306+
try {
307+
var adminUser = admins.checkAdminUser();
308+
var result = admins.deleteReview(namespace, extension, reviewId);
309+
admins.logAdminAction(adminUser, result);
310+
return ResponseEntity.ok(result);
311+
} catch (ErrorResultException exc) {
312+
return exc.toResponseEntity();
313+
}
314+
}
315+
280316
@GetMapping(
281317
path = "/admin/namespace/{namespaceName}",
282318
produces = MediaType.APPLICATION_JSON_VALUE

server/src/main/java/org/eclipse/openvsx/admin/AdminService.java

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,35 @@ private String userNotFoundMessage(String user) {
249249
return "User not found: " + user;
250250
}
251251

252+
@Transactional(rollbackOn = ErrorResultException.class)
253+
public ResultJson deleteReview(String namespace, String extensionName, String reviewId) {
254+
var extension = repositories.findExtension(extensionName, namespace);
255+
if (extension == null || !extension.isActive()) {
256+
var message = "Extension not found: " + NamingUtil.toExtensionId(namespace, extensionName);
257+
throw new ErrorResultException(message, HttpStatus.NOT_FOUND);
258+
}
259+
260+
var review = repositories.findReview(Long.parseLong(reviewId));
261+
if (review.isEmpty()) {
262+
var message = "Review with id " + reviewId + " not found";
263+
throw new ErrorResultException(message, HttpStatus.NOT_FOUND);
264+
}
265+
266+
deleteReview(review.get());
267+
return ResultJson.success("Deleted review from " + review.get().getUser().getLoginName() + " for " + NamingUtil.toExtensionId(extension));
268+
}
269+
270+
private void deleteReview(ExtensionReview review) {
271+
review.setActive(false);
272+
273+
var extension = review.getExtension();
274+
extension.setAverageRating(repositories.getAverageReviewRating(extension));
275+
extension.setReviewCount(repositories.countActiveReviews(extension));
276+
search.updateSearchEntry(extension);
277+
cache.evictExtensionJsons(extension);
278+
cache.evictLatestExtensionVersion(extension);
279+
}
280+
252281
@Transactional(rollbackOn = ErrorResultException.class)
253282
public ResultJson editNamespaceMember(String namespaceName, String userName, String provider, String role,
254283
UserData admin) throws ErrorResultException {
@@ -361,6 +390,11 @@ public UserPublishInfoJson getUserPublishInfo(String provider, String loginName)
361390
)
362391
.toList());
363392

393+
var reviews = repositories.findActiveReviews(user);
394+
userPublishInfo.setReviews(reviews.stream()
395+
.map(ExtensionReview::toReviewJson)
396+
.toList());
397+
364398
return userPublishInfo;
365399
}
366400

@@ -401,8 +435,16 @@ public ResultJson revokePublisherContributions(String provider, String loginName
401435
extensions.updateExtension(extension);
402436
}
403437

438+
var deactivatedReviewCount = 0;
439+
var reviews = repositories.findActiveReviews(user);
440+
for (var review : reviews) {
441+
deleteReview(review);
442+
deactivatedReviewCount++;
443+
}
444+
404445
var result = ResultJson.success("Deactivated " + deactivatedTokenCount
405-
+ " tokens and deactivated " + deactivatedExtensionCount + " extensions of user "
446+
+ " tokens, deactivated " + deactivatedExtensionCount + " extensions and"
447+
+ " deactivated " + deactivatedReviewCount + " reviews of user "
406448
+ provider + "/" + loginName + ".");
407449
logAdminAction(admin, result);
408450
return result;

server/src/main/java/org/eclipse/openvsx/entities/ExtensionReview.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515

1616
import org.eclipse.openvsx.json.ReviewJson;
1717
import org.eclipse.openvsx.util.TimeUtil;
18+
import org.eclipse.openvsx.util.UrlUtil;
19+
20+
import static org.eclipse.openvsx.util.UrlUtil.createApiUrl;
1821

1922
@Entity
2023
public class ExtensionReview {
@@ -47,11 +50,17 @@ public class ExtensionReview {
4750
*/
4851
public ReviewJson toReviewJson() {
4952
var json = new ReviewJson();
53+
json.setId(this.getId());
5054
json.setTimestamp(TimeUtil.toUTCString(this.getTimestamp()));
5155
json.setUser(this.getUser().toUserJson());
5256
json.setTitle(this.getTitle());
5357
json.setComment(this.getComment());
5458
json.setRating(this.getRating());
59+
60+
var deleteUrl =
61+
createApiUrl(UrlUtil.getBaseUrl(), "admin", "extension", extension.getNamespace().getName(), extension.getName(), "review", Long.toString(this.getId()), "delete");
62+
json.setDeleteUrl(deleteUrl);
63+
5564
return json;
5665
}
5766

server/src/main/java/org/eclipse/openvsx/json/ReviewJson.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@
2626
@JsonInclude(Include.NON_NULL)
2727
public class ReviewJson {
2828

29+
@Schema(hidden = true)
30+
private long id;
31+
2932
@Schema(description = "Data of the user who posted this review")
3033
@NotNull
3134
private UserJson user;
@@ -45,6 +48,17 @@ public class ReviewJson {
4548
@Max(5)
4649
private int rating;
4750

51+
@Schema(hidden = true)
52+
private String deleteUrl;
53+
54+
public long getId() {
55+
return id;
56+
}
57+
58+
public void setId(long id) {
59+
this.id = id;
60+
}
61+
4862
public UserJson getUser() {
4963
return user;
5064
}
@@ -85,6 +99,14 @@ public void setRating(int rating) {
8599
this.rating = rating;
86100
}
87101

102+
public String getDeleteUrl() {
103+
return deleteUrl;
104+
}
105+
106+
public void setDeleteUrl(String deleteUrl) {
107+
this.deleteUrl = deleteUrl;
108+
}
109+
88110
@Override
89111
public boolean equals(Object o) {
90112
if (this == o) return true;

server/src/main/java/org/eclipse/openvsx/json/UserPublishInfoJson.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ public static UserPublishInfoJson error(String message) {
3131
@NotNull
3232
private List<ExtensionJson> extensions;
3333

34+
@NotNull
35+
private List<ReviewJson> reviews;
36+
3437
@NotNull
3538
private Integer activeAccessTokenNum;
3639

@@ -50,6 +53,14 @@ public void setExtensions(List<ExtensionJson> extensions) {
5053
this.extensions = extensions;
5154
}
5255

56+
public List<ReviewJson> getReviews() {
57+
return reviews;
58+
}
59+
60+
public void setReviews(List<ReviewJson> reviews) {
61+
this.reviews = reviews;
62+
}
63+
5364
public Integer getActiveAccessTokenNum() {
5465
return activeAccessTokenNum;
5566
}

server/src/main/java/org/eclipse/openvsx/repositories/ExtensionReviewRepository.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,22 @@
1717
import org.springframework.data.repository.Repository;
1818
import org.springframework.data.util.Streamable;
1919

20+
import java.util.Optional;
21+
2022
import static org.eclipse.openvsx.cache.CacheService.CACHE_AVERAGE_REVIEW_RATING;
2123

2224
public interface ExtensionReviewRepository extends Repository<ExtensionReview, Long> {
2325

2426
Streamable<ExtensionReview> findByExtension(Extension extension);
2527

28+
Streamable<ExtensionReview> findByUserAndActiveTrue(UserData user);
29+
2630
Streamable<ExtensionReview> findByExtensionAndActiveTrue(Extension extension);
2731

2832
Streamable<ExtensionReview> findByExtensionAndUserAndActiveTrue(Extension extension, UserData user);
2933

34+
Optional<ExtensionReview> findById(Long id);
35+
3036
long countByExtensionAndActiveTrue(Extension extension);
3137

3238
@Cacheable(CACHE_AVERAGE_REVIEW_RATING)

server/src/main/java/org/eclipse/openvsx/repositories/RepositoryService.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import java.util.Collection;
2525
import java.util.List;
2626
import java.util.Map;
27+
import java.util.Optional;
2728

2829
import static org.eclipse.openvsx.entities.FileResource.*;
2930

@@ -279,6 +280,14 @@ public Streamable<ExtensionReview> findActiveReviews(Extension extension, UserDa
279280
return extensionReviewRepo.findByExtensionAndUserAndActiveTrue(extension, user);
280281
}
281282

283+
public Streamable<ExtensionReview> findActiveReviews(UserData user) {
284+
return extensionReviewRepo.findByUserAndActiveTrue(user);
285+
}
286+
287+
public Optional<ExtensionReview> findReview(long reviewId) {
288+
return extensionReviewRepo.findById(reviewId);
289+
}
290+
282291
public long countActiveReviews(Extension extension) {
283292
return extensionReviewRepo.countByExtensionAndActiveTrue(extension);
284293
}

server/src/test/java/org/eclipse/openvsx/admin/AdminAPITest.java

Lines changed: 99 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -52,16 +52,12 @@
5252
import org.springframework.transaction.support.TransactionTemplate;
5353

5454
import java.time.LocalDateTime;
55-
import java.util.ArrayList;
56-
import java.util.Collections;
57-
import java.util.List;
58-
import java.util.Map;
55+
import java.util.*;
5956
import java.util.function.Consumer;
6057
import java.util.stream.Collectors;
6158

6259
import static org.assertj.core.api.Assertions.assertThat;
63-
import static org.mockito.ArgumentMatchers.any;
64-
import static org.mockito.ArgumentMatchers.anyCollection;
60+
import static org.mockito.ArgumentMatchers.*;
6561
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
6662
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
6763
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
@@ -582,6 +578,7 @@ void testGetUserPublishInfo() throws Exception {
582578
Mockito.when(repositories.findUserByLoginName("github", "test")).thenReturn(user);
583579
Mockito.when(repositories.countActiveAccessTokens(user)).thenReturn(1L);
584580
Mockito.when(repositories.findLatestVersions(user)).thenReturn(versions);
581+
Mockito.when(repositories.findActiveReviews(user)).thenReturn(Streamable.empty());
585582

586583
mockMvc.perform(get("/admin/publisher/{provider}/{loginName}", "github", "test")
587584
.with(user("admin_user").authorities(new SimpleGrantedAuthority(("ROLE_ADMIN"))))
@@ -634,11 +631,14 @@ void testRevokePublisherAgreement() throws Exception {
634631
Mockito.when(repositories.findVersionsByUser(user, true))
635632
.thenReturn(Streamable.of(versions));
636633

634+
Mockito.when(repositories.findActiveReviews(user))
635+
.thenReturn(Streamable.empty());
636+
637637
mockMvc.perform(post("/admin/publisher/{provider}/{loginName}/revoke", "github", "test")
638638
.with(user("admin_user").authorities(new SimpleGrantedAuthority(("ROLE_ADMIN"))))
639639
.with(csrf().asHeader()))
640640
.andExpect(status().isOk())
641-
.andExpect(content().json(successJson("Deactivated 1 tokens and deactivated 1 extensions of user github/test.")));
641+
.andExpect(content().json(successJson("Deactivated 1 tokens, deactivated 1 extensions and deactivated 0 reviews of user github/test.")));
642642

643643
assertThat(token.isActive()).isFalse();
644644
assertThat(versions.get(0).isActive()).isFalse();
@@ -1145,6 +1145,58 @@ void testChangeNamespaceAbortOnNewNamespaceExists() throws Exception {
11451145
.andExpect(content().json(errorJson("New namespace already exists: bar")));
11461146
}
11471147

1148+
@Test
1149+
void testDeleteReview() throws Exception {
1150+
mockAdminUser();
1151+
var reviews = mockReviews();
1152+
1153+
mockMvc.perform(post("/admin/extension/{namespace}/{extension}/review/{reviewId}/delete", "foobar", "baz", "1")
1154+
.with(user("admin_user").authorities(new SimpleGrantedAuthority(("ROLE_ADMIN"))))
1155+
.with(csrf().asHeader()))
1156+
.andExpect(status().isOk())
1157+
.andExpect(content().json(successJson("Deleted review from user1 for foobar.baz")));
1158+
1159+
assertThat(reviews.get(0).isActive()).isFalse();
1160+
}
1161+
1162+
@Test
1163+
void testDeleteReviewNotLoggedIn() throws Exception {
1164+
mockMvc.perform(post("/admin/extension/{namespace}/{extension}/review/{reviewId}/delete", "foo", "bar", "1").with(csrf()))
1165+
.andExpect(status().isForbidden());
1166+
}
1167+
1168+
@Test
1169+
void testDeleteReviewNormalUser() throws Exception {
1170+
mockNormalUser();
1171+
1172+
mockMvc.perform(post("/admin/extension/{namespace}/{extension}/review/{reviewId}/delete", "foo", "bar", "1")
1173+
.with(user("test_user"))
1174+
.with(csrf().asHeader()))
1175+
.andExpect(status().isForbidden());
1176+
}
1177+
1178+
@Test
1179+
void testDeleteReviewUnknownExtension() throws Exception {
1180+
mockAdminUser();
1181+
mockMvc.perform(post("/admin/extension/{namespace}/{extension}/review/{reviewId}/delete", "foo", "bar", "1")
1182+
.with(user("admin_user").authorities(new SimpleGrantedAuthority(("ROLE_ADMIN"))))
1183+
.with(csrf().asHeader()))
1184+
.andExpect(status().isNotFound())
1185+
.andExpect(content().json(errorJson("Extension not found: foo.bar")));
1186+
}
1187+
1188+
@Test
1189+
void testDeleteReviewNonExistingReview() throws Exception {
1190+
mockAdminUser();
1191+
mockReviews();
1192+
1193+
mockMvc.perform(post("/admin/extension/{namespace}/{extension}/review/{reviewId}/delete", "foobar", "baz", "3")
1194+
.with(user("admin_user").authorities(new SimpleGrantedAuthority(("ROLE_ADMIN"))))
1195+
.with(csrf().asHeader()))
1196+
.andExpect(status().isNotFound())
1197+
.andExpect(content().json(errorJson("Review with id 3 not found")));
1198+
}
1199+
11481200
//---------- UTILITY ----------//
11491201

11501202
private PersonalAccessToken mockAdminToken() {
@@ -1290,6 +1342,46 @@ private List<ExtensionVersion> mockExtension(int numberOfVersions, int numberOfB
12901342
return versions;
12911343
}
12921344

1345+
private List<ExtensionReview> mockReviews() {
1346+
var extVersions = mockExtension(1, 0, 0);
1347+
var extVersion = extVersions.get(0);
1348+
var extension = extVersion.getExtension();
1349+
1350+
var user1 = new UserData();
1351+
user1.setLoginName("user1");
1352+
var review1 = new ExtensionReview();
1353+
review1.setId(1);
1354+
review1.setExtension(extension);
1355+
review1.setUser(user1);
1356+
review1.setRating(3);
1357+
review1.setComment("Somewhat ok");
1358+
review1.setTimestamp(LocalDateTime.parse("2000-01-01T10:00"));
1359+
review1.setActive(true);
1360+
1361+
var user2 = new UserData();
1362+
user2.setLoginName("user2");
1363+
var review2 = new ExtensionReview();
1364+
review2.setId(2);
1365+
review2.setExtension(extension);
1366+
review2.setUser(user2);
1367+
review2.setRating(4);
1368+
review2.setComment("Quite good");
1369+
review2.setTimestamp(LocalDateTime.parse("2000-01-01T10:00"));
1370+
review2.setActive(true);
1371+
1372+
Mockito.when(repositories.findReview(anyLong()))
1373+
.thenReturn(Optional.empty());
1374+
Mockito.when(repositories.findReview(1))
1375+
.thenReturn(Optional.of(review1));
1376+
Mockito.when(repositories.findReview(2))
1377+
.thenReturn(Optional.of(review2));
1378+
1379+
Mockito.when(repositories.findActiveReviews(extension))
1380+
.thenReturn(Streamable.of(review1, review2));
1381+
1382+
return List.of(review1, review2);
1383+
}
1384+
12931385
private String createVersion(int major) {
12941386
return major + ".0.0";
12951387
}

0 commit comments

Comments
 (0)