Skip to content

Commit e646663

Browse files
authored
Add recommended flag to maps and mods (#472)
1 parent 9d32008 commit e646663

12 files changed

Lines changed: 325 additions & 12 deletions

File tree

.github/workflows/build.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: Build
22
on: [ push, pull_request ]
33
env:
44
FLYWAY_VERSION: 7.5.4
5-
FAF_DB_VERSION: v116
5+
FAF_DB_VERSION: v117
66
jobs:
77
test:
88
runs-on: ubuntu-latest
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
package com.faforever.api.data;
2+
3+
import com.faforever.api.AbstractIntegrationTest;
4+
import com.faforever.api.data.domain.GroupPermission;
5+
import com.faforever.api.map.MapRepository;
6+
import com.faforever.api.security.OAuthScope;
7+
import org.junit.jupiter.api.Test;
8+
import org.springframework.beans.factory.annotation.Autowired;
9+
import org.springframework.http.HttpHeaders;
10+
import org.springframework.test.context.jdbc.Sql;
11+
import org.springframework.test.context.jdbc.Sql.ExecutionPhase;
12+
13+
import static com.faforever.api.data.JsonApiMediaType.JSON_API_MEDIA_TYPE;
14+
import static org.hamcrest.MatcherAssert.assertThat;
15+
import static org.hamcrest.Matchers.hasSize;
16+
import static org.hamcrest.Matchers.is;
17+
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
18+
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch;
19+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
20+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
21+
22+
@Sql(executionPhase = ExecutionPhase.BEFORE_TEST_METHOD, scripts = "classpath:sql/truncateTables.sql")
23+
@Sql(executionPhase = ExecutionPhase.BEFORE_TEST_METHOD, scripts = "classpath:sql/prepDefaultData.sql")
24+
@Sql(executionPhase = ExecutionPhase.BEFORE_TEST_METHOD, scripts = "classpath:sql/prepMapVersion.sql")
25+
public class MapElideTest extends AbstractIntegrationTest {
26+
27+
@Autowired
28+
MapRepository mapRepository;
29+
30+
private static final String MAP_RECOMMENDED_FALSE_ID_1 = """
31+
{
32+
"data": {
33+
"type": "map",
34+
"id": "1",
35+
"attributes": {
36+
"recommended": false
37+
}
38+
}
39+
}""";
40+
41+
private static final String MAP_RECOMMENDED_TRUE_ID_1 = """
42+
{
43+
"data": {
44+
"type": "map",
45+
"id": "1",
46+
"attributes": {
47+
"recommended": true
48+
}
49+
}
50+
}""";
51+
52+
53+
@Test
54+
public void canReadMapWithoutScopeAndRole() throws Exception {
55+
mockMvc.perform(get("/data/map")
56+
.with(getOAuthTokenWithTestUser(NO_SCOPE, NO_AUTHORITIES)))
57+
.andExpect(status().isOk())
58+
.andExpect(jsonPath("$.data", hasSize(1)));
59+
}
60+
61+
@Test
62+
public void canUpdateMapRecommendationToTrueWithScopeAndRole() throws Exception {
63+
mockMvc.perform(
64+
patch("/data/map/1")
65+
.with(getOAuthTokenWithTestUser(OAuthScope._MANAGE_VAULT, GroupPermission.ROLE_ADMIN_MAP))
66+
.header(HttpHeaders.CONTENT_TYPE, JSON_API_MEDIA_TYPE)
67+
// update recommended to true requires the vault permission, thus we test it here for the success case
68+
.content(MAP_RECOMMENDED_TRUE_ID_1))
69+
.andExpect(status().isNoContent());
70+
71+
assertThat(mapRepository.getOne(1).getRecommended(), is(true));
72+
}
73+
74+
@Test
75+
public void cannotUpdateMapRecommendationToTrueWithoutScope() throws Exception {
76+
mockMvc.perform(
77+
patch("/data/map/1")
78+
.with(getOAuthTokenWithTestUser(NO_SCOPE, GroupPermission.ROLE_ADMIN_MAP))
79+
.header(HttpHeaders.CONTENT_TYPE, JSON_API_MEDIA_TYPE)
80+
// update recommended to true requires the vault permission, thus we test it here for the failing case
81+
.content(MAP_RECOMMENDED_TRUE_ID_1))
82+
.andExpect(status().isForbidden());
83+
84+
assertThat(mapRepository.getOne(1).getRecommended(), is(false));
85+
}
86+
87+
@Test
88+
public void cannotUpdateMapRecommendationToTrueWithoutRole() throws Exception {
89+
mockMvc.perform(
90+
patch("/data/map/1")
91+
.with(getOAuthTokenWithTestUser(OAuthScope._MANAGE_VAULT, NO_AUTHORITIES))
92+
.header(HttpHeaders.CONTENT_TYPE, JSON_API_MEDIA_TYPE)
93+
// update recommended to true requires the vault permission, thus we test it here for the failing case
94+
.content(MAP_RECOMMENDED_TRUE_ID_1))
95+
.andExpect(status().isForbidden());
96+
97+
assertThat(mapRepository.getOne(1).getRecommended(), is(false));
98+
}
99+
100+
@Test
101+
public void canUpdateMapRecommendationToFalseWithScopeAndRole() throws Exception {
102+
mockMvc.perform(
103+
patch("/data/map/1")
104+
.with(getOAuthTokenWithTestUser(OAuthScope._MANAGE_VAULT, GroupPermission.ROLE_ADMIN_MAP))
105+
.header(HttpHeaders.CONTENT_TYPE, JSON_API_MEDIA_TYPE)
106+
// update recommended to false requires the vault permission, thus we test it here for the success case
107+
.content(MAP_RECOMMENDED_FALSE_ID_1))
108+
.andExpect(status().isNoContent());
109+
110+
assertThat(mapRepository.getOne(1).getRecommended(), is(false));
111+
}
112+
113+
@Test
114+
public void cannotUpdateMapRecommendationToFalseWithoutScope() throws Exception {
115+
mockMvc.perform(
116+
patch("/data/map/1")
117+
.with(getOAuthTokenWithTestUser(NO_SCOPE, GroupPermission.ROLE_ADMIN_MAP))
118+
.header(HttpHeaders.CONTENT_TYPE, JSON_API_MEDIA_TYPE)
119+
// update recommended to false requires the vault permission, thus we test it here for the failing case
120+
.content(MAP_RECOMMENDED_FALSE_ID_1))
121+
.andExpect(status().isForbidden());
122+
}
123+
124+
@Test
125+
public void cannotUpdateMapRecommendationToFalseWithoutRole() throws Exception {
126+
mockMvc.perform(
127+
patch("/data/map/1")
128+
.with(getOAuthTokenWithTestUser(OAuthScope._MANAGE_VAULT, NO_AUTHORITIES))
129+
.header(HttpHeaders.CONTENT_TYPE, JSON_API_MEDIA_TYPE)
130+
// update recommended to false requires the vault permission, thus we test it here for the failing case
131+
.content(MAP_RECOMMENDED_FALSE_ID_1))
132+
.andExpect(status().isForbidden());
133+
}
134+
}
135+
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
package com.faforever.api.data;
2+
3+
import com.faforever.api.AbstractIntegrationTest;
4+
import com.faforever.api.data.domain.GroupPermission;
5+
import com.faforever.api.mod.ModRepository;
6+
import com.faforever.api.security.OAuthScope;
7+
import org.junit.jupiter.api.Test;
8+
import org.springframework.beans.factory.annotation.Autowired;
9+
import org.springframework.http.HttpHeaders;
10+
import org.springframework.test.context.jdbc.Sql;
11+
import org.springframework.test.context.jdbc.Sql.ExecutionPhase;
12+
13+
import static com.faforever.api.data.JsonApiMediaType.JSON_API_MEDIA_TYPE;
14+
import static org.hamcrest.MatcherAssert.assertThat;
15+
import static org.hamcrest.Matchers.hasSize;
16+
import static org.hamcrest.Matchers.is;
17+
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
18+
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch;
19+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
20+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
21+
22+
@Sql(executionPhase = ExecutionPhase.BEFORE_TEST_METHOD, scripts = "classpath:sql/truncateTables.sql")
23+
@Sql(executionPhase = ExecutionPhase.BEFORE_TEST_METHOD, scripts = "classpath:sql/prepDefaultData.sql")
24+
@Sql(executionPhase = ExecutionPhase.BEFORE_TEST_METHOD, scripts = "classpath:sql/prepModData.sql")
25+
public class ModElideTest extends AbstractIntegrationTest {
26+
27+
@Autowired
28+
ModRepository modRepository;
29+
30+
private static final String MOD_RECOMMENDED_FALSE_ID_1 = """
31+
{
32+
"data": {
33+
"type": "mod",
34+
"id": "1",
35+
"attributes": {
36+
"recommended": false
37+
}
38+
}
39+
}""";
40+
41+
private static final String MOD_RECOMMENDED_TRUE_ID_1 = """
42+
{
43+
"data": {
44+
"type": "mod",
45+
"id": "1",
46+
"attributes": {
47+
"recommended": true
48+
}
49+
}
50+
}""";
51+
52+
53+
@Test
54+
public void canReadModWithoutScopeAndRole() throws Exception {
55+
mockMvc.perform(get("/data/mod")
56+
.with(getOAuthTokenWithTestUser(NO_SCOPE, NO_AUTHORITIES)))
57+
.andExpect(status().isOk())
58+
.andExpect(jsonPath("$.data", hasSize(2)));
59+
}
60+
61+
@Test
62+
public void canUpdateModRecommendationToTrueWithScopeAndRole() throws Exception {
63+
mockMvc.perform(
64+
patch("/data/mod/1")
65+
.with(getOAuthTokenWithTestUser(OAuthScope._MANAGE_VAULT, GroupPermission.ROLE_ADMIN_MOD))
66+
.header(HttpHeaders.CONTENT_TYPE, JSON_API_MEDIA_TYPE)
67+
// update recommended to true requires the vault permission, thus we test it here for the success case
68+
.content(MOD_RECOMMENDED_TRUE_ID_1))
69+
.andExpect(status().isNoContent());
70+
71+
assertThat(modRepository.getOne(1).getRecommended(), is(true));
72+
}
73+
74+
@Test
75+
public void cannotUpdateModRecommendationToTrueWithoutScope() throws Exception {
76+
mockMvc.perform(
77+
patch("/data/mod/1")
78+
.with(getOAuthTokenWithTestUser(NO_SCOPE, GroupPermission.ROLE_ADMIN_MOD))
79+
.header(HttpHeaders.CONTENT_TYPE, JSON_API_MEDIA_TYPE)
80+
// update recommended to true requires the vault permission, thus we test it here for the failing case
81+
.content(MOD_RECOMMENDED_TRUE_ID_1))
82+
.andExpect(status().isForbidden());
83+
84+
assertThat(modRepository.getOne(1).getRecommended(), is(false));
85+
}
86+
87+
@Test
88+
public void cannotUpdateModRecommendationToTrueWithoutRole() throws Exception {
89+
mockMvc.perform(
90+
patch("/data/mod/1")
91+
.with(getOAuthTokenWithTestUser(OAuthScope._MANAGE_VAULT, NO_AUTHORITIES))
92+
.header(HttpHeaders.CONTENT_TYPE, JSON_API_MEDIA_TYPE)
93+
// update recommended to true requires the vault permission, thus we test it here for the failing case
94+
.content(MOD_RECOMMENDED_TRUE_ID_1))
95+
.andExpect(status().isForbidden());
96+
97+
assertThat(modRepository.getOne(1).getRecommended(), is(false));
98+
}
99+
100+
@Test
101+
public void canUpdateModRecommendationToFalseWithScopeAndRole() throws Exception {
102+
mockMvc.perform(
103+
patch("/data/mod/1")
104+
.with(getOAuthTokenWithTestUser(OAuthScope._MANAGE_VAULT, GroupPermission.ROLE_ADMIN_MOD))
105+
.header(HttpHeaders.CONTENT_TYPE, JSON_API_MEDIA_TYPE)
106+
// update recommended to false requires the vault permission, thus we test it here for the success case
107+
.content(MOD_RECOMMENDED_FALSE_ID_1))
108+
.andExpect(status().isNoContent());
109+
110+
assertThat(modRepository.getOne(1).getRecommended(), is(false));
111+
}
112+
113+
@Test
114+
public void cannotUpdateModRecommendationToFalseWithoutScope() throws Exception {
115+
mockMvc.perform(
116+
patch("/data/mod/1")
117+
.with(getOAuthTokenWithTestUser(NO_SCOPE, GroupPermission.ROLE_ADMIN_MOD))
118+
.header(HttpHeaders.CONTENT_TYPE, JSON_API_MEDIA_TYPE)
119+
// update recommended to false requires the vault permission, thus we test it here for the failing case
120+
.content(MOD_RECOMMENDED_FALSE_ID_1))
121+
.andExpect(status().isForbidden());
122+
}
123+
124+
@Test
125+
public void cannotUpdateModRecommendationToFalseWithoutRole() throws Exception {
126+
mockMvc.perform(
127+
patch("/data/mod/1")
128+
.with(getOAuthTokenWithTestUser(OAuthScope._MANAGE_VAULT, NO_AUTHORITIES))
129+
.header(HttpHeaders.CONTENT_TYPE, JSON_API_MEDIA_TYPE)
130+
// update recommended to false requires the vault permission, thus we test it here for the failing case
131+
.content(MOD_RECOMMENDED_FALSE_ID_1))
132+
.andExpect(status().isForbidden());
133+
}
134+
}
135+
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
INSERT INTO map (id, display_name, map_type, battle_type, author) VALUES (1, 'display name', 'mtype', 'btype', 1);
1+
INSERT INTO map (id, display_name, map_type, battle_type, author, recommended) VALUES (1, 'display name', 'mtype', 'btype', 1, false);
22
INSERT INTO map_version (id, description, max_players, width, height, version, filename, map_id, hidden, ranked)
33
VALUES (1, 'des', 2, 2, 2, 1, 'map/ghb.zip', 1, 0, 1);

src/inttest/resources/sql/prepModData.sql

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
INSERT INTO `mod` (id, display_name, author, uploader)
2-
VALUES (1, 'MOD_001', 'some Author', 1),
3-
(2, 'MOD_002', 'some Author', 1);
1+
INSERT INTO `mod` (id, display_name, author, uploader, recommended)
2+
VALUES (1, 'MOD_001', 'some Author', 1, false),
3+
(2, 'MOD_002', 'some Author', 1, false);
44

55
INSERT INTO mod_version (id, description, type, uid, version, filename, hidden, mod_id)
66
VALUES (1, 'SCMP 001', 'SIM', '11111111-1111-1111-1111-111111111111', 1, 'maps/scmp_001.v0001.zip', false, 1),

src/main/java/com/faforever/api/data/domain/Map.java

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@
22

33
import com.faforever.api.data.checks.Prefab;
44
import com.faforever.api.data.listeners.MapChangeListener;
5+
import com.faforever.api.security.elide.permission.AdminMapCheck;
6+
import com.yahoo.elide.annotation.Audit;
57
import com.yahoo.elide.annotation.Include;
68
import com.yahoo.elide.annotation.UpdatePermission;
79
import lombok.Setter;
810
import org.hibernate.annotations.BatchSize;
9-
import org.hibernate.annotations.Immutable;
1011
import org.hibernate.annotations.JoinColumnOrFormula;
1112
import org.hibernate.annotations.JoinColumnsOrFormulas;
1213
import org.hibernate.annotations.JoinFormula;
@@ -34,12 +35,12 @@
3435
@Setter
3536
@Table(name = "map")
3637
@Include(type = Map.TYPE_NAME)
37-
@Immutable
3838
@EntityListeners(MapChangeListener.class)
3939
public class Map extends AbstractEntity implements OwnableEntity {
4040

4141
public static final String TYPE_NAME = "map";
4242

43+
private boolean recommended;
4344
private String displayName;
4445
private String mapType;
4546
private String battleType;
@@ -50,6 +51,14 @@ public class Map extends AbstractEntity implements OwnableEntity {
5051
private Integer gamesPlayed;
5152
private MapReviewsSummary reviewsSummary;
5253

54+
@Column(name = "recommended")
55+
@Audit(action = Audit.Action.UPDATE, logStatement = "Updated map `{0}` attribute recommended to: {1}", logExpressions = {"${map.id}", "${map.recommended}"})
56+
@NotNull
57+
@UpdatePermission(expression = AdminMapCheck.EXPRESSION)
58+
public boolean getRecommended() {
59+
return recommended;
60+
}
61+
5362
@Column(name = "display_name", unique = true)
5463
@Size(max = 100)
5564
@NotNull

src/main/java/com/faforever/api/data/domain/Mod.java

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
package com.faforever.api.data.domain;
22

33
import com.faforever.api.data.checks.Prefab;
4+
import com.faforever.api.data.listeners.ModChangeListener;
5+
import com.faforever.api.security.elide.permission.AdminModCheck;
46
import com.yahoo.elide.annotation.Include;
57
import com.yahoo.elide.annotation.UpdatePermission;
68
import lombok.Setter;
7-
import org.hibernate.annotations.Immutable;
89
import org.hibernate.annotations.JoinColumnOrFormula;
910
import org.hibernate.annotations.JoinColumnsOrFormulas;
1011
import org.hibernate.annotations.JoinFormula;
@@ -13,6 +14,7 @@
1314
import javax.persistence.CascadeType;
1415
import javax.persistence.Column;
1516
import javax.persistence.Entity;
17+
import javax.persistence.EntityListeners;
1618
import javax.persistence.JoinColumn;
1719
import javax.persistence.ManyToOne;
1820
import javax.persistence.OneToMany;
@@ -27,19 +29,27 @@
2729
@Entity
2830
@Table(name = "\"mod\"")
2931
@Include(type = Mod.TYPE_NAME)
30-
@Immutable
3132
@Setter
33+
@EntityListeners(ModChangeListener.class)
3234
public class Mod extends AbstractEntity implements OwnableEntity {
3335

3436
public static final String TYPE_NAME = "mod";
3537

38+
private boolean recommended;
3639
private String displayName;
3740
private String author;
3841
private List<ModVersion> versions;
3942
private ModVersion latestVersion;
4043
private Player uploader;
4144
private ModReviewsSummary reviewsSummary;
4245

46+
@Column(name = "recommended")
47+
@NotNull
48+
@UpdatePermission(expression = AdminModCheck.EXPRESSION)
49+
public boolean getRecommended() {
50+
return recommended;
51+
}
52+
4353
@Column(name = "display_name")
4454
@Size(max = 100)
4555
@NotNull

src/main/java/com/faforever/api/data/listeners/MapChangeListener.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,6 @@ public class MapChangeListener {
1717
@PostUpdate
1818
@PostRemove
1919
public void mapChanged(Map map) {
20-
log.debug("Map and MapVersion cache evicted, due to change on MapVersion with id: {}", map.getId());
20+
log.debug("Map and MapVersion cache evicted, due to change on Map with id: {}", map.getId());
2121
}
2222
}

0 commit comments

Comments
 (0)