Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save musketyr/f491e099baf719f0b9f78201e78e4950 to your computer and use it in GitHub Desktop.

Select an option

Save musketyr/f491e099baf719f0b9f78201e78e4950 to your computer and use it in GitHub Desktop.
Spock → JUnit 5 Migration Diff: RoiNotificationServiceSpec (sc185896)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Spock → JUnit 5 Migration: RoiNotificationServiceSpec</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; padding: 20px; line-height: 1.6; }
h1 { text-align: center; margin-bottom: 20px; color: #333; }
.section { background: white; border-radius: 8px; margin-bottom: 20px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); overflow: hidden; }
.section-header { background: #2d3748; color: white; padding: 12px 16px; cursor: pointer; display: flex; justify-content: space-between; align-items: center; }
.section-header:hover { background: #4a5568; }
.section-header h2 { font-size: 16px; font-weight: 600; }
.section-header .toggle { font-size: 12px; }
.section-content { display: block; }
.section-content.collapsed { display: none; }
.diff-container { display: grid; grid-template-columns: 1fr 1fr; }
.diff-panel { padding: 16px; overflow-x: auto; }
.diff-panel.left { background: #fff5f5; border-right: 1px solid #e2e8f0; }
.diff-panel.right { background: #f0fff4; }
.diff-panel h3 { font-size: 12px; text-transform: uppercase; color: #718096; margin-bottom: 12px; letter-spacing: 0.5px; }
pre { font-family: 'SF Mono', Monaco, 'Courier New', monospace; font-size: 13px; white-space: pre-wrap; word-wrap: break-word; }
.left pre { color: #c53030; }
.right pre { color: #276749; }
.summary { background: #c6f6d5; border-left: 4px solid #38a169; padding: 12px 16px; margin: 0; }
.summary p { color: #276749; font-size: 14px; }
.summary strong { color: #22543d; }
@media (max-width: 768px) { .diff-container { grid-template-columns: 1fr; } .diff-panel.left { border-right: none; border-bottom: 1px solid #e2e8f0; } }
</style>
</head>
<body>
<h1>🦀 Spock → JUnit 5 Migration: RoiNotificationServiceSpec</h1>
<p style="text-align: center; color: #666; margin-bottom: 20px;">Story: <a href="https://app.shortcut.com/agorapulse/story/185896">sc185896</a> | PR: <a href="https://github.com/agorapulse/platform/pull/73550">#73550</a></p>
<!-- Section: Imports -->
<div class="section">
<div class="section-header" onclick="toggleSection(this)">
<h2>📦 Imports</h2>
<span class="toggle">▼</span>
</div>
<div class="section-content">
<div class="diff-container">
<div class="diff-panel left">
<h3>Spock (Groovy)</h3>
<pre>package agorapulse.roi.notification
import agorapulse.commons.messagesource.interpolation.InterpolatingMessageSource
import agorapulse.notification.sender.NotificationSender
import agorapulse.organization.client.internal.OrganizationClient
import agorapulse.organization.client.model.ManagerSummary
import agorapulse.organization.client.model.OrganizationManagerSummary
import agorapulse.organization.client.model.OrganizationSummary
import agorapulse.roi.core.models.export.RoiEmailParameters
import agorapulse.roi.core.models.parameters.RoiExportParametersBuilder
import com.agorapulse.security.jwt.JsonWebTokenService
import spock.lang.Specification
import java.time.LocalDate</pre>
</div>
<div class="diff-panel right">
<h3>JUnit 5 (Java)</h3>
<pre>package agorapulse.roi.notification;
import agorapulse.commons.messagesource.interpolation.InterpolatingMessageSource;
import agorapulse.notification.sender.NotificationSender;
import agorapulse.notification.templates.NotificationTemplateBundle;
import agorapulse.organization.client.internal.OrganizationClient;
import agorapulse.organization.client.model.ManagerSummary;
import agorapulse.organization.client.model.OrganizationManagerSummary;
import agorapulse.organization.client.model.OrganizationSummary;
import agorapulse.roi.core.models.export.RoiEmailParameters;
import agorapulse.roi.core.models.parameters.RoiExportParametersBuilder;
import com.agorapulse.security.jwt.JsonWebTokenService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.io.File;
import java.time.LocalDate;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.function.Consumer;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyMap;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;</pre>
</div>
</div>
<div class="summary">
<p><strong>Changes:</strong> Added JUnit 5 annotations (@Test, @BeforeEach, @ExtendWith). Replaced Spock mocking with Mockito (@Mock, MockitoExtension). Added AssertJ assertions. Added explicit Java imports for List, Map, Locale, Consumer, File.</p>
</div>
</div>
</div>
<!-- Section: Class & Fields -->
<div class="section">
<div class="section-header" onclick="toggleSection(this)">
<h2>🏗️ Class & Fields</h2>
<span class="toggle">▼</span>
</div>
<div class="section-content">
<div class="diff-container">
<div class="diff-panel left">
<h3>Spock (Groovy)</h3>
<pre>class RoiNotificationServiceSpec extends Specification {
RoiNotificationService roiNotificationService
InterpolatingMessageSource interpolatingMessageSource = Mock(InterpolatingMessageSource)
JsonWebTokenService tokenService = Mock(JsonWebTokenService)
NotificationSender notificationSender
OrganizationClient organizationClient = Mock(OrganizationClient)</pre>
</div>
<div class="diff-panel right">
<h3>JUnit 5 (Java)</h3>
<pre>@ExtendWith(MockitoExtension.class)
class RoiNotificationServiceTest {
@Mock
private InterpolatingMessageSource interpolatingMessageSource;
@Mock
private JsonWebTokenService tokenService;
@Mock
private NotificationSender notificationSender;
@Mock
private OrganizationClient organizationClient;
private RoiNotificationService roiNotificationService;</pre>
</div>
</div>
<div class="summary">
<p><strong>Changes:</strong> Replaced "extends Specification" with @ExtendWith(MockitoExtension.class). Replaced Spock Mock() inline initialization with @Mock annotations. Added explicit private visibility and semicolons.</p>
</div>
</div>
</div>
<!-- Section: Setup -->
<div class="section">
<div class="section-header" onclick="toggleSection(this)">
<h2>🔧 Setup</h2>
<span class="toggle">▼</span>
</div>
<div class="section-content">
<div class="diff-container">
<div class="diff-panel left">
<h3>Spock (Groovy)</h3>
<pre>void setup() {
tokenService = Mock(JsonWebTokenService)
notificationSender = Mock(NotificationSender)
organizationClient = Mock(OrganizationClient)
roiNotificationService = new RoiNotificationService('bucket',
interpolatingMessageSource,
tokenService,
notificationSender,
organizationClient,
'url'
)
}</pre>
</div>
<div class="diff-panel right">
<h3>JUnit 5 (Java)</h3>
<pre>@BeforeEach
void setUp() {
roiNotificationService = new RoiNotificationService(
"bucket",
interpolatingMessageSource,
tokenService,
notificationSender,
organizationClient,
"url"
);
}</pre>
</div>
</div>
<div class="summary">
<p><strong>Changes:</strong> Replaced Spock setup() with @BeforeEach setUp(). Removed redundant mock re-initialization (handled by @Mock). Changed single quotes to double quotes for Java strings.</p>
</div>
</div>
</div>
<!-- Section: Test 1 -->
<div class="section">
<div class="section-header" onclick="toggleSection(this)">
<h2>🧪 should not send notification if no recipient</h2>
<span class="toggle">▼</span>
</div>
<div class="section-content">
<div class="diff-container">
<div class="diff-panel left">
<h3>Spock (Groovy)</h3>
<pre>void 'should not send notification if no recipient'() {
given:
RoiEmailParameters roiEmailParameters = new RoiEmailParameters()
roiEmailParameters.mimeType = 'mime'
roiEmailParameters.roiExportParameters = new RoiExportParametersBuilder(
'1', new ManagerSummary(identityId: '38'), LocalDate.now(),
LocalDate.now(), 1, '2', 3).build()
when:
boolean value = roiNotificationService.sendExportCompletedNotification(
[:], null, roiEmailParameters)
then:
!value
0 * notificationSender.send(_)
}</pre>
</div>
<div class="diff-panel right">
<h3>JUnit 5 (Java)</h3>
<pre>@Test
void shouldNotSendNotificationIfNoRecipient() {
// given
RoiEmailParameters roiEmailParameters = new RoiEmailParameters();
roiEmailParameters.setMimeType("mime");
ManagerSummary manager = new ManagerSummary();
manager.setIdentityId("38");
roiEmailParameters.setRoiExportParameters(
new RoiExportParametersBuilder("1", manager, LocalDate.now(),
LocalDate.now(), 1L, "2", 3L).build()
);
// when
boolean value = roiNotificationService.sendExportCompletedNotification(
Map.of(), null, roiEmailParameters);
// then
assertThat(value).isFalse();
verify(notificationSender, never()).send(
any(NotificationTemplateBundle.class), anyMap(), any(Consumer.class));
}</pre>
</div>
</div>
<div class="summary">
<p><strong>Changes:</strong> Added @Test annotation. Replaced Groovy property access with Java setters. Replaced Groovy map constructor syntax with explicit setter calls. Replaced [:] with Map.of(). Replaced Spock !value with AssertJ assertThat().isFalse(). Replaced 0 * mock.method() with verify(mock, never()).method().</p>
</div>
</div>
</div>
<!-- Section: Test 2 -->
<div class="section">
<div class="section-header" onclick="toggleSection(this)">
<h2>🧪 should not send notification if recipient id is not in org manager ids</h2>
<span class="toggle">▼</span>
</div>
<div class="section-content">
<div class="diff-container">
<div class="diff-panel left">
<h3>Spock (Groovy)</h3>
<pre>void 'should not send notification if recipient id is not in org manager ids'() {
given:
RoiEmailParameters roiEmailParameters = new RoiEmailParameters()
roiEmailParameters.mimeType = 'mime'
roiEmailParameters.roiExportParameters = new RoiExportParametersBuilder(
'1', new ManagerSummary(identityId: '38'), LocalDate.now(),
LocalDate.now(), 1, '2', 3).build()
ManagerSummary recipient = new ManagerSummary(
identityId: '38', administratorEnabled: false)
organizationClient.getOrganization(1) >>
new OrganizationSummary(organizationManagers: [])
when:
boolean value = roiNotificationService.sendExportCompletedNotification(
[:], recipient, roiEmailParameters)
then:
!value
0 * notificationSender.send(_, _, _)
}</pre>
</div>
<div class="diff-panel right">
<h3>JUnit 5 (Java)</h3>
<pre>@Test
void shouldNotSendNotificationIfRecipientIdIsNotInOrgManagerIds() {
// given
RoiEmailParameters roiEmailParameters = new RoiEmailParameters();
roiEmailParameters.setMimeType("mime");
ManagerSummary manager = new ManagerSummary();
manager.setIdentityId("38");
roiEmailParameters.setRoiExportParameters(
new RoiExportParametersBuilder("1", manager, LocalDate.now(),
LocalDate.now(), 1L, "2", 3L).build()
);
ManagerSummary recipient = new ManagerSummary();
recipient.setIdentityId("38");
recipient.setAdministratorEnabled(false);
OrganizationSummary orgSummary = new OrganizationSummary();
orgSummary.setOrganizationManagers(List.of());
when(organizationClient.getOrganization(1L)).thenReturn(orgSummary);
// when
boolean value = roiNotificationService.sendExportCompletedNotification(
Map.of(), recipient, roiEmailParameters);
// then
assertThat(value).isFalse();
verify(notificationSender, never()).send(
any(NotificationTemplateBundle.class), anyMap(), any(Consumer.class));
}</pre>
</div>
</div>
<div class="summary">
<p><strong>Changes:</strong> Replaced Groovy map constructor with explicit setters. Replaced Spock >> stubbing with Mockito when().thenReturn(). Replaced empty list [] with List.of(). Added explicit Long types (1L, 3L).</p>
</div>
</div>
</div>
<!-- Section: Test 3 -->
<div class="section">
<div class="section-header" onclick="toggleSection(this)">
<h2>🧪 should send notification if recipient id is not in org manager ids and is admin</h2>
<span class="toggle">▼</span>
</div>
<div class="section-content">
<div class="diff-container">
<div class="diff-panel left">
<h3>Spock (Groovy)</h3>
<pre>void 'should send notification if recipient id is not in org manager ids and is admin'() {
given:
RoiEmailParameters roiEmailParameters = new RoiEmailParameters()
roiEmailParameters.mimeType = 'mime'
roiEmailParameters.roiExportParameters = new RoiExportParametersBuilder(
'1', new ManagerSummary(identityId: '38'), LocalDate.now(),
LocalDate.now(), 1, '2', 3).build()
ManagerSummary recipient = new ManagerSummary(id: 38,
administratorEnabled: true, timeZone: 'Europe/Paris',
locale: Locale.FRANCE)
organizationClient.getOrganization(1) >>
new OrganizationSummary(organizationManagers: [])
when:
boolean value = roiNotificationService.sendExportCompletedNotification(
[:], recipient, roiEmailParameters)
then:
value
1 * notificationSender.send(_, _, _)
1 * tokenService.sign(_)
}</pre>
</div>
<div class="diff-panel right">
<h3>JUnit 5 (Java)</h3>
<pre>@Test
void shouldSendNotificationIfRecipientIdIsNotInOrgManagerIdsAndIsAdmin() {
// given
RoiEmailParameters roiEmailParameters = new RoiEmailParameters();
roiEmailParameters.setMimeType("mime");
ManagerSummary manager = new ManagerSummary();
manager.setIdentityId("38");
roiEmailParameters.setRoiExportParameters(
new RoiExportParametersBuilder("1", manager, LocalDate.now(),
LocalDate.now(), 1L, "2", 3L).build()
);
ManagerSummary recipient = new ManagerSummary();
recipient.setId(38L);
recipient.setAdministratorEnabled(true);
recipient.setTimeZone("Europe/Paris");
recipient.setLocale(Locale.FRANCE);
OrganizationSummary orgSummary = new OrganizationSummary();
orgSummary.setOrganizationManagers(List.of());
when(organizationClient.getOrganization(1L)).thenReturn(orgSummary);
// when
boolean value = roiNotificationService.sendExportCompletedNotification(
Map.of(), recipient, roiEmailParameters);
// then
assertThat(value).isTrue();
verify(notificationSender).send(
any(NotificationTemplateBundle.class), anyMap(), any(Consumer.class));
verify(tokenService).sign(any());
}</pre>
</div>
</div>
<div class="summary">
<p><strong>Changes:</strong> Replaced Spock value assertion with assertThat().isTrue(). Replaced 1 * mock.method() with verify(mock).method(). Converted id: 38 to setId(38L) with explicit Long type.</p>
</div>
</div>
</div>
<!-- Section: Test 4 -->
<div class="section">
<div class="section-header" onclick="toggleSection(this)">
<h2>🧪 should send notification if recipient id is in org manager ids and is not admin</h2>
<span class="toggle">▼</span>
</div>
<div class="section-content">
<div class="diff-container">
<div class="diff-panel left">
<h3>Spock (Groovy)</h3>
<pre>void 'should send notification if recipient id is in org manager ids and is not admin'() {
given:
RoiEmailParameters roiEmailParameters = new RoiEmailParameters()
roiEmailParameters.mimeType = 'mime'
roiEmailParameters.roiExportParameters = new RoiExportParametersBuilder(
'1', new ManagerSummary(identityId: '38'), LocalDate.now(),
LocalDate.now(), 1, '2', 3).build()
ManagerSummary recipient = new ManagerSummary(id: 38,
administratorEnabled: true, timeZone: 'Europe/Paris',
locale: Locale.FRANCE)
organizationClient.getOrganization(1) >> new OrganizationSummary(
organizationManagers: [new OrganizationManagerSummary(managerId: 38)])
when:
boolean value = roiNotificationService.sendExportCompletedNotification(
[:], recipient, roiEmailParameters)
then:
value
1 * notificationSender.send(_, _, _)
1 * tokenService.sign(_)
}</pre>
</div>
<div class="diff-panel right">
<h3>JUnit 5 (Java)</h3>
<pre>@Test
void shouldSendNotificationIfRecipientIdIsInOrgManagerIdsAndIsNotAdmin() {
// given
RoiEmailParameters roiEmailParameters = new RoiEmailParameters();
roiEmailParameters.setMimeType("mime");
ManagerSummary manager = new ManagerSummary();
manager.setIdentityId("38");
roiEmailParameters.setRoiExportParameters(
new RoiExportParametersBuilder("1", manager, LocalDate.now(),
LocalDate.now(), 1L, "2", 3L).build()
);
ManagerSummary recipient = new ManagerSummary();
recipient.setId(38L);
recipient.setAdministratorEnabled(true);
recipient.setTimeZone("Europe/Paris");
recipient.setLocale(Locale.FRANCE);
OrganizationManagerSummary orgManager = new OrganizationManagerSummary();
orgManager.setManagerId(38L);
OrganizationSummary orgSummary = new OrganizationSummary();
orgSummary.setOrganizationManagers(List.of(orgManager));
when(organizationClient.getOrganization(1L)).thenReturn(orgSummary);
// when
boolean value = roiNotificationService.sendExportCompletedNotification(
Map.of(), recipient, roiEmailParameters);
// then
assertThat(value).isTrue();
verify(notificationSender).send(
any(NotificationTemplateBundle.class), anyMap(), any(Consumer.class));
verify(tokenService).sign(any());
}</pre>
</div>
</div>
<div class="summary">
<p><strong>Changes:</strong> Replaced Groovy list literal [new OrganizationManagerSummary(...)] with List.of(orgManager). Expanded inline object construction to explicit instantiation and setter calls.</p>
</div>
</div>
</div>
<!-- Section: Test 5 -->
<div class="section">
<div class="section-header" onclick="toggleSection(this)">
<h2>🧪 should send notification with file messageSource</h2>
<span class="toggle">▼</span>
</div>
<div class="section-content">
<div class="diff-container">
<div class="diff-panel left">
<h3>Spock (Groovy)</h3>
<pre>void 'should send notification with file messageSource'() {
given:
RoiEmailParameters roiEmailParameters = new RoiEmailParameters()
roiEmailParameters.mimeType = 'mime'
roiEmailParameters.roiExportParameters = new RoiExportParametersBuilder(
'1', new ManagerSummary(identityId: '38'), LocalDate.now(),
LocalDate.now(), 1, '2', 3).build()
ManagerSummary recipient = new ManagerSummary(id: 38,
administratorEnabled: true, timeZone: 'Europe/Paris',
locale: Locale.FRANCE)
organizationClient.getOrganization(1) >> new OrganizationSummary(
organizationManagers: [new OrganizationManagerSummary(managerId: 38)])
Map fileData = [bucket: 'bucket', key: 'key', file: new File('test')]
when:
boolean value = roiNotificationService.sendExportCompletedNotification(
fileData, recipient, roiEmailParameters)
then:
value
1 * notificationSender.send(_, _, _)
1 * interpolatingMessageSource.get(
'email.roi-export-completed.attachment.label', Locale.FRANCE)
1 * tokenService.sign(_)
}</pre>
</div>
<div class="diff-panel right">
<h3>JUnit 5 (Java)</h3>
<pre>@Test
void shouldSendNotificationWithFileMessageSource() {
// given
RoiEmailParameters roiEmailParameters = new RoiEmailParameters();
roiEmailParameters.setMimeType("mime");
ManagerSummary manager = new ManagerSummary();
manager.setIdentityId("38");
roiEmailParameters.setRoiExportParameters(
new RoiExportParametersBuilder("1", manager, LocalDate.now(),
LocalDate.now(), 1L, "2", 3L).build()
);
ManagerSummary recipient = new ManagerSummary();
recipient.setId(38L);
recipient.setAdministratorEnabled(true);
recipient.setTimeZone("Europe/Paris");
recipient.setLocale(Locale.FRANCE);
OrganizationManagerSummary orgManager = new OrganizationManagerSummary();
orgManager.setManagerId(38L);
OrganizationSummary orgSummary = new OrganizationSummary();
orgSummary.setOrganizationManagers(List.of(orgManager));
when(organizationClient.getOrganization(1L)).thenReturn(orgSummary);
Map&lt;String, Object&gt; fileData = Map.of(
"bucket", "bucket", "key", "key", "file", new File("test"));
// when
boolean value = roiNotificationService.sendExportCompletedNotification(
fileData, recipient, roiEmailParameters);
// then
assertThat(value).isTrue();
verify(notificationSender).send(
any(NotificationTemplateBundle.class), anyMap(), any(Consumer.class));
verify(interpolatingMessageSource).get(
"email.roi-export-completed.attachment.label", Locale.FRANCE);
verify(tokenService).sign(any());
}</pre>
</div>
</div>
<div class="summary">
<p><strong>Changes:</strong> Replaced Groovy map literal [bucket: 'bucket', ...] with Java Map.of("bucket", "bucket", ...). Added generic types Map&lt;String, Object&gt;. Replaced Spock interaction verification with Mockito verify().</p>
</div>
</div>
</div>
<!-- Section: Test 6 -->
<div class="section">
<div class="section-header" onclick="toggleSection(this)">
<h2>🧪 should send notification with no file messageSource</h2>
<span class="toggle">▼</span>
</div>
<div class="section-content">
<div class="diff-container">
<div class="diff-panel left">
<h3>Spock (Groovy)</h3>
<pre>void 'should send notification with no file messageSource'() {
given:
RoiEmailParameters roiEmailParameters = new RoiEmailParameters()
roiEmailParameters.mimeType = 'mime'
roiEmailParameters.roiExportParameters = new RoiExportParametersBuilder(
'1', new ManagerSummary(identityId: '38'), LocalDate.now(),
LocalDate.now(), 1, '2', 3).build()
ManagerSummary recipient = new ManagerSummary(id: 38,
administratorEnabled: true, timeZone: 'Europe/Paris',
locale: Locale.FRANCE)
organizationClient.getOrganization(1) >> new OrganizationSummary(
organizationManagers: [new OrganizationManagerSummary(managerId: 38)])
when:
boolean value = roiNotificationService.sendExportCompletedNotification(
null, recipient, roiEmailParameters)
then:
value
1 * notificationSender.send(_, _, _)
1 * interpolatingMessageSource.get(
'email.roi-export-completed.download.label', Locale.FRANCE)
1 * tokenService.sign(_)
}</pre>
</div>
<div class="diff-panel right">
<h3>JUnit 5 (Java)</h3>
<pre>@Test
void shouldSendNotificationWithNoFileMessageSource() {
// given
RoiEmailParameters roiEmailParameters = new RoiEmailParameters();
roiEmailParameters.setMimeType("mime");
ManagerSummary manager = new ManagerSummary();
manager.setIdentityId("38");
roiEmailParameters.setRoiExportParameters(
new RoiExportParametersBuilder("1", manager, LocalDate.now(),
LocalDate.now(), 1L, "2", 3L).build()
);
ManagerSummary recipient = new ManagerSummary();
recipient.setId(38L);
recipient.setAdministratorEnabled(true);
recipient.setTimeZone("Europe/Paris");
recipient.setLocale(Locale.FRANCE);
OrganizationManagerSummary orgManager = new OrganizationManagerSummary();
orgManager.setManagerId(38L);
OrganizationSummary orgSummary = new OrganizationSummary();
orgSummary.setOrganizationManagers(List.of(orgManager));
when(organizationClient.getOrganization(1L)).thenReturn(orgSummary);
// when
boolean value = roiNotificationService.sendExportCompletedNotification(
null, recipient, roiEmailParameters);
// then
assertThat(value).isTrue();
verify(notificationSender).send(
any(NotificationTemplateBundle.class), anyMap(), any(Consumer.class));
verify(interpolatingMessageSource).get(
"email.roi-export-completed.download.label", Locale.FRANCE);
verify(tokenService).sign(any());
}</pre>
</div>
</div>
<div class="summary">
<p><strong>Changes:</strong> Verifies download.label message key (vs attachment.label when file exists). Same pattern conversions: Spock blocks → comments, Groovy syntax → Java syntax, Spock mocking → Mockito.</p>
</div>
</div>
</div>
<script>
function toggleSection(header) {
const content = header.nextElementSibling;
const toggle = header.querySelector('.toggle');
content.classList.toggle('collapsed');
toggle.textContent = content.classList.contains('collapsed') ? '▶' : '▼';
}
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment