|
1 | 1 | package com.springqprobackend.springqpro.integration; |
2 | 2 |
|
3 | 3 | import java.time.Instant; |
| 4 | +import java.util.List; |
4 | 5 |
|
| 6 | +import com.springqprobackend.springqpro.security.dto.AuthResponse; |
5 | 7 | import org.junit.jupiter.api.BeforeEach; |
6 | 8 | import org.junit.jupiter.api.Test; |
7 | 9 | import org.springframework.beans.factory.annotation.Autowired; |
|
22 | 24 | import com.springqprobackend.springqpro.repository.UserRepository; |
23 | 25 | import com.springqprobackend.springqpro.security.JwtUtil; |
24 | 26 | import com.springqprobackend.springqpro.testcontainers.IntegrationTestBase; |
| 27 | +import java.util.concurrent.atomic.AtomicReference; |
| 28 | + |
| 29 | +import static org.assertj.core.api.Assertions.*; |
25 | 30 |
|
26 | 31 | /* 2025-11-17-NOTE(S)-TO-SELF: |
27 | 32 | - GraphQlTester is Spring's testing utility for GraphQL endpoints. |
@@ -59,222 +64,194 @@ Test completes (and succeeds) and after the Spring test context begins shutting |
59 | 64 | } |
60 | 65 | ) |
61 | 66 | @ActiveProfiles("test") |
62 | | -@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) |
63 | | -class TaskGraphQLIntegrationTest extends IntegrationTestBase { |
64 | | - |
65 | | - private GraphQlTester graphQlTester; |
66 | | - |
67 | | - @LocalServerPort |
68 | | - private int port; |
| 67 | +class TaskGraphQLIntegrationTest extends AbstractAuthenticatedIntegrationTest { |
69 | 68 |
|
70 | 69 | @Autowired |
71 | 70 | private TaskRepository taskRepository; |
72 | 71 |
|
73 | 72 | @Autowired |
74 | 73 | private UserRepository userRepository; |
75 | 74 |
|
76 | | - @Autowired |
77 | | - private JwtUtil jwtUtil; |
78 | | - |
79 | 75 | private static final String TEST_EMAIL = "graphql-test@example.com"; |
| 76 | + private static final String PASSWORD = "password"; |
80 | 77 |
|
81 | 78 | @BeforeEach |
82 | | - void init() { |
| 79 | + void setup() { |
83 | 80 | taskRepository.deleteAll(); |
84 | 81 | userRepository.deleteAll(); |
85 | | - // Create a user so SecurityContext + ownership is valid |
86 | | - userRepository.save(new UserEntity( |
87 | | - TEST_EMAIL, |
88 | | - "{noop}password" // password not used here; we just need a valid user record |
89 | | - )); |
90 | | - // Generate a valid JWT for that user |
91 | | - String token = jwtUtil.generateAccessToken(TEST_EMAIL); |
92 | | - |
93 | | - WebTestClient client = WebTestClient.bindToServer() |
94 | | - .baseUrl("http://localhost:" + port + "/graphql") |
95 | | - .exchangeStrategies( |
96 | | - ExchangeStrategies.builder() |
97 | | - .codecs(c -> c.defaultCodecs().maxInMemorySize(5_000_000)) |
98 | | - .build() |
99 | | - ) |
100 | | - .defaultHeader("Authorization", "Bearer " + token) |
101 | | - .build(); |
102 | | - this.graphQlTester = HttpGraphQlTester.create(client); |
103 | 82 | } |
104 | 83 |
|
105 | | - // Test #1 - Create Task, Query the Task, then verify its Status, Type, and Payload: |
| 84 | + // Test #1: createTask + fetch by id |
106 | 85 | @Test |
107 | | - void taskCreation_succeeds_isRetrieved() { |
108 | | - String mutation = """ |
| 86 | + void createTask_thenQueryById_returnsCorrectTask() { |
| 87 | + AuthResponse auth = registerAndLogin(TEST_EMAIL, PASSWORD); |
| 88 | + |
| 89 | + String createMutation = """ |
109 | 90 | mutation { |
110 | | - createTask(input:{ |
111 | | - payload:"send-an-email" |
112 | | - type:EMAIL |
113 | | - }) { |
114 | | - id |
115 | | - payload |
116 | | - type |
117 | | - status |
118 | | - attempts |
119 | | - maxRetries |
120 | | - } |
| 91 | + createTask(input: { |
| 92 | + payload: "send-an-email" |
| 93 | + type: EMAIL |
| 94 | + }) { |
| 95 | + id |
| 96 | + payload |
| 97 | + type |
| 98 | + status |
| 99 | + } |
121 | 100 | } |
122 | 101 | """; |
123 | | - /*2025 - 11 - 17 - NOTE(S) - TO - SELF:+DEBUG: |
124 | | - -graphQlTester.document(mutation) prepares a GraphQL mutation with String mutation, which is just a String |
125 | | - containing |
126 | | - a proper GraphQL mutation obv.So graphQlTester runs it by running my actual Spring Boot app(which runs my real |
127 | | - GraphQL controller, my real service layer, and uses the real database(Testcontainers in this |
128 | | - case)). |
129 | | - - .execute() clearly runs it and you can inspect the fields.*/ |
130 | | - var created = graphQlTester.document(mutation) |
131 | | - .execute() |
132 | | - .path("createTask.payload").entity(String.class).isEqualTo("send-an-email") |
133 | | - .path("createTask.type").entity(String.class).isEqualTo("EMAIL") |
134 | | - .path("createTask.status").entity(String.class).isEqualTo("QUEUED"); |
135 | | - // Extract the id from the same GraphQL mutation: |
136 | | - String id = graphQlTester.document(mutation) |
137 | | - .execute() |
138 | | - .path("createTask.id").entity(String.class).get(); |
139 | | - // Search for that Task w/ a GraphQL query: |
| 102 | + |
| 103 | + AtomicReference<String> taskIdRef = new AtomicReference<>(); |
| 104 | + |
| 105 | + graphQLWithToken(auth.accessToken(), createMutation) |
| 106 | + .expectStatus().isOk() |
| 107 | + .expectBody() |
| 108 | + .jsonPath("$.data.createTask.id") |
| 109 | + .value(id -> taskIdRef.set((String) id)) |
| 110 | + .jsonPath("$.data.createTask.payload").isEqualTo("send-an-email") |
| 111 | + .jsonPath("$.data.createTask.type").isEqualTo("EMAIL") |
| 112 | + .jsonPath("$.data.createTask.status").isEqualTo("QUEUED"); |
| 113 | + |
| 114 | + String taskId = taskIdRef.get(); |
| 115 | + |
140 | 116 | String query = """ |
141 | 117 | query { |
142 | | - task(id: "%s") { |
143 | | - id |
144 | | - payload |
145 | | - type |
146 | | - status |
147 | | - } |
| 118 | + task(id: "%s") { |
| 119 | + id |
| 120 | + payload |
| 121 | + type |
| 122 | + status |
| 123 | + } |
148 | 124 | } |
149 | | - """.formatted(id); |
150 | | - graphQlTester.document(query) |
151 | | - .execute() |
152 | | - .path("task.payload").entity(String.class).isEqualTo("send-an-email") |
153 | | - .path("task.type").entity(String.class).isEqualTo("EMAIL"); |
| 125 | + """.formatted(taskId); |
| 126 | + |
| 127 | + graphQLWithToken(auth.accessToken(), query) |
| 128 | + .expectStatus().isOk() |
| 129 | + .expectBody() |
| 130 | + .jsonPath("$.data.task.id").isEqualTo(taskId) |
| 131 | + .jsonPath("$.data.task.payload").isEqualTo("send-an-email") |
| 132 | + .jsonPath("$.data.task.type").isEqualTo("EMAIL"); |
154 | 133 | } |
155 | 134 |
|
156 | | - // Test #2 - Update Task (Partially), Verify Change in DB, Query Returns Updated: |
| 135 | + // Test #2: updateTask reflects in DB + query |
157 | 136 | @Test |
158 | | - void updateTask_verifyDBChange_retrieveByQuery() { |
159 | | - TaskEntity entity = new TaskEntity( |
160 | | - "Task-ArbitraryTaskId", |
161 | | - "Send an email", |
| 137 | + void updateTask_updatesDatabase_andQueryReflectsChange() { |
| 138 | + AuthResponse auth = registerAndLogin(TEST_EMAIL, PASSWORD); |
| 139 | + |
| 140 | + TaskEntity task = new TaskEntity( |
| 141 | + "Task-Update-1", |
| 142 | + "original payload", |
162 | 143 | TaskType.EMAIL, |
163 | 144 | TaskStatus.QUEUED, |
164 | 145 | 0, |
165 | 146 | 3, |
166 | 147 | Instant.now(), |
167 | 148 | TEST_EMAIL |
168 | 149 | ); |
169 | | - taskRepository.save(entity); |
170 | | - // Manually change the status of entity to COMPLETED and increment attempts: |
171 | | - // NOTE: When you want to specify integer in mutation, put %d -- "%d" will return you a integer wrapped in a String (aka just a String) |
| 150 | + taskRepository.save(task); |
| 151 | + |
172 | 152 | String mutation = """ |
173 | 153 | mutation { |
174 | | - updateTask(input:{ |
175 | | - id:"%s", |
176 | | - status:COMPLETED, |
177 | | - attempts:%d |
178 | | - }) { |
179 | | - id |
180 | | - status |
181 | | - attempts |
182 | | - } |
| 154 | + updateTask(input: { |
| 155 | + id: "%s" |
| 156 | + status: COMPLETED |
| 157 | + attempts: 1 |
| 158 | + }) { |
| 159 | + id |
| 160 | + status |
| 161 | + attempts |
| 162 | + } |
183 | 163 | } |
184 | | - """.formatted(entity.getId(), entity.getAttempts()+1); |
185 | | - graphQlTester.document(mutation).execute() |
186 | | - .path("updateTask.status").entity(String.class).isEqualTo("COMPLETED") |
187 | | - .path("updateTask.attempts").entity(Integer.class).isEqualTo(1); |
188 | | - // Verify that the change happened in the DataBase w/ taskRepository: |
189 | | - TaskEntity getEntity = taskRepository.findById(entity.getId()).orElseThrow(); |
190 | | - assert getEntity.getStatus() == TaskStatus.COMPLETED; |
191 | | - assert getEntity.getAttempts() == entity.getAttempts() + 1; |
| 164 | + """.formatted(task.getId()); |
| 165 | + |
| 166 | + graphQLWithToken(auth.accessToken(), mutation) |
| 167 | + .expectStatus().isOk() |
| 168 | + .expectBody() |
| 169 | + .jsonPath("$.data.updateTask.status").isEqualTo("COMPLETED") |
| 170 | + .jsonPath("$.data.updateTask.attempts").isEqualTo(1); |
| 171 | + |
| 172 | + TaskEntity updated = taskRepository.findById(task.getId()).orElseThrow(); |
| 173 | + assertThat(updated.getStatus()).isEqualTo(TaskStatus.COMPLETED); |
| 174 | + assertThat(updated.getAttempts()).isEqualTo(1); |
192 | 175 | } |
193 | 176 |
|
194 | | - // Test #3 - Delete Task, Verify Change in DB, Query Returns NULL (or whatever): |
| 177 | + // Test #3: deleteTask removes entity |
195 | 178 | @Test |
196 | | - void deleteTask_verifyDBChange_queryRetrieveNull() { |
197 | | - TaskEntity entity = new TaskEntity( |
198 | | - "Task-ArbitraryTaskId", |
199 | | - "Send an email", |
| 179 | + void deleteTask_removesTaskFromDatabase() { |
| 180 | + AuthResponse auth = registerAndLogin(TEST_EMAIL, PASSWORD); |
| 181 | + |
| 182 | + TaskEntity task = new TaskEntity( |
| 183 | + "Task-Delete-1", |
| 184 | + "to delete", |
200 | 185 | TaskType.EMAIL, |
201 | 186 | TaskStatus.QUEUED, |
202 | 187 | 0, |
203 | 188 | 3, |
204 | 189 | Instant.now(), |
205 | 190 | TEST_EMAIL |
206 | 191 | ); |
207 | | - taskRepository.save(entity); |
| 192 | + taskRepository.save(task); |
| 193 | + |
208 | 194 | String mutation = """ |
209 | 195 | mutation { |
210 | | - deleteTask(id:"%s") |
| 196 | + deleteTask(id: "%s") |
211 | 197 | } |
212 | | - """.formatted(entity.getId()); |
213 | | - graphQlTester.document(mutation) |
214 | | - .execute() |
215 | | - .path("deleteTask").entity(Boolean.class).isEqualTo(true); |
216 | | - assert taskRepository.findById(entity.getId()).isEmpty(); |
| 198 | + """.formatted(task.getId()); |
| 199 | + |
| 200 | + graphQLWithToken(auth.accessToken(), mutation) |
| 201 | + .expectStatus().isOk() |
| 202 | + .expectBody() |
| 203 | + .jsonPath("$.data.deleteTask").isEqualTo(true); |
| 204 | + |
| 205 | + assertThat(taskRepository.findById(task.getId())).isEmpty(); |
217 | 206 | } |
218 | 207 |
|
219 | | - // Test #4 - Being able to filter Tasks w/ Status: |
| 208 | + // Test #4: filter tasks by status |
220 | 209 | @Test |
221 | | - void filterTasks_byStatusQuery_returnsCorrectList() { |
222 | | - // Task #1: |
223 | | - taskRepository.save( |
224 | | - new TaskEntity("Task-ArbitraryTaskId-1","Send an email", TaskType.EMAIL, TaskStatus.QUEUED, 0, 3, Instant.now(), TEST_EMAIL) |
225 | | - ); |
226 | | - // Task #2: |
227 | | - taskRepository.save( |
228 | | - new TaskEntity("Task-ArbitraryTaskId-2", "Send an email 2", TaskType.EMAIL, TaskStatus.COMPLETED, 1, 3, Instant.now(), TEST_EMAIL) |
229 | | - ); |
230 | | - // Task #3: |
231 | | - taskRepository.save( |
232 | | - new TaskEntity("Task-ArbitaryTaskId-3", "Send an SMS or whatever", TaskType.SMS, TaskStatus.QUEUED, 0, 3, Instant.now(), TEST_EMAIL) |
233 | | - ); |
| 210 | + void tasksQuery_filtersByStatus() { |
| 211 | + AuthResponse auth = registerAndLogin(TEST_EMAIL, PASSWORD); |
| 212 | + |
| 213 | + taskRepository.saveAll(List.of( |
| 214 | + new TaskEntity("t1", "a", TaskType.EMAIL, TaskStatus.QUEUED, 0, 3, Instant.now(), TEST_EMAIL), |
| 215 | + new TaskEntity("t2", "b", TaskType.EMAIL, TaskStatus.COMPLETED, 1, 3, Instant.now(), TEST_EMAIL), |
| 216 | + new TaskEntity("t3", "c", TaskType.SMS, TaskStatus.QUEUED, 0, 3, Instant.now(), TEST_EMAIL) |
| 217 | + )); |
| 218 | + |
234 | 219 | String query = """ |
235 | 220 | query { |
236 | | - tasks(status: QUEUED) { |
237 | | - id |
238 | | - payload |
239 | | - status |
240 | | - } |
| 221 | + tasks(status: QUEUED) { |
| 222 | + id |
| 223 | + } |
241 | 224 | } |
242 | 225 | """; |
243 | | - graphQlTester.document(query) |
244 | | - .execute() |
245 | | - .path("tasks").entityList(TaskEntity.class).hasSize(2); |
| 226 | + |
| 227 | + graphQLWithToken(auth.accessToken(), query) |
| 228 | + .expectStatus().isOk() |
| 229 | + .expectBody() |
| 230 | + .jsonPath("$.data.tasks.length()").isEqualTo(2); |
246 | 231 | } |
247 | 232 |
|
248 | | - // Test #5 - Being able to filter Tasks w/ Type: |
| 233 | + // Test #5: filter tasks by type |
249 | 234 | @Test |
250 | | - void filterTasks_byTypeQuery_returnsCorrectList() { |
251 | | - // Task #1: |
252 | | - taskRepository.save( |
253 | | - new TaskEntity("Task-ArbitraryTaskId-1","Do something NEWSLETTER related, I don't know.", TaskType.NEWSLETTER, TaskStatus.QUEUED, 0, 3, Instant.now(), TEST_EMAIL) |
254 | | - ); |
255 | | - // Task #2: |
256 | | - taskRepository.save( |
257 | | - new TaskEntity("Task-ArbitraryTaskId-2", "Do something NEWSLETTER related, I don't know 2.", TaskType.NEWSLETTER, TaskStatus.COMPLETED, 1, 3, Instant.now(), TEST_EMAIL) |
258 | | - ); |
259 | | - // Task #3: |
260 | | - taskRepository.save( |
261 | | - new TaskEntity("Task-ArbitraryTaskId-3", "Send an SMS or whatever", TaskType.SMS, TaskStatus.QUEUED, 0, 3, Instant.now(), TEST_EMAIL) |
262 | | - ); |
263 | | - // Task #4: |
264 | | - taskRepository.save( |
265 | | - new TaskEntity("Task-ArbitraryTaskId-4", "Do something NEWSLETTER related, I don't know 3.", TaskType.NEWSLETTER, TaskStatus.QUEUED, 0, 3, Instant.now(), TEST_EMAIL) |
266 | | - ); |
| 235 | + void tasksTypeQuery_filtersByType() { |
| 236 | + AuthResponse auth = registerAndLogin(TEST_EMAIL, PASSWORD); |
| 237 | + |
| 238 | + taskRepository.saveAll(List.of( |
| 239 | + new TaskEntity("t1", "n1", TaskType.NEWSLETTER, TaskStatus.QUEUED, 0, 3, Instant.now(), TEST_EMAIL), |
| 240 | + new TaskEntity("t2", "n2", TaskType.NEWSLETTER, TaskStatus.COMPLETED, 1, 3, Instant.now(), TEST_EMAIL), |
| 241 | + new TaskEntity("t3", "s1", TaskType.SMS, TaskStatus.QUEUED, 0, 3, Instant.now(), TEST_EMAIL) |
| 242 | + )); |
| 243 | + |
267 | 244 | String query = """ |
268 | 245 | query { |
269 | | - tasksType(type: NEWSLETTER) { |
270 | | - id |
271 | | - payload |
272 | | - status |
273 | | - } |
| 246 | + tasksType(type: NEWSLETTER) { |
| 247 | + id |
| 248 | + } |
274 | 249 | } |
275 | 250 | """; |
276 | | - graphQlTester.document(query) |
277 | | - .execute() |
278 | | - .path("tasksType").entityList(TaskEntity.class).hasSize(3); |
| 251 | + |
| 252 | + graphQLWithToken(auth.accessToken(), query) |
| 253 | + .expectStatus().isOk() |
| 254 | + .expectBody() |
| 255 | + .jsonPath("$.data.tasksType.length()").isEqualTo(2); |
279 | 256 | } |
280 | 257 | } |
0 commit comments