Skip to content

Commit f327e0d

Browse files
committed
fix: manage @Profile filter at method level ONLY for legacy @Changeset
1 parent be057af commit f327e0d

3 files changed

Lines changed: 205 additions & 5 deletions

File tree

platform-plugins/flamingock-springboot-integration/src/main/java/io/flamingock/springboot/SpringbootProfileFilter.java

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import io.flamingock.internal.core.change.loaded.CodeLoadedChange;
2323
import org.springframework.context.annotation.Profile;
2424

25+
import java.lang.reflect.Method;
2526
import java.util.Arrays;
2627
import java.util.Collections;
2728
import java.util.List;
@@ -71,11 +72,26 @@ private boolean filterTemplateChange(AbstractTemplateLoadedChange reflectionDesc
7172

7273

7374
private boolean filterCodeChange(CodeLoadedChange change) {
74-
// Profiles are declared at change (class) level only. The change is the atomic unit, so
75-
// its profile gate lives on the @Change-annotated class. Method-level @Profile (on @Apply
76-
// or @Rollback) is intentionally NOT honored: a per-method gate is incoherent for a
77-
// change-level inclusion decision and would risk applying a change while silently skipping
78-
// its rollback under a different active profile, breaking recovery semantics.
75+
// Legacy Mongock @ChangeSet changes: each @ChangeSet method is its own atomic change,
76+
// so method-level @Profile is the natural gate. We detect the annotation by FQCN.
77+
//
78+
// This is intentionally narrow: only methods annotated with the legacy Mongock
79+
// @ChangeSet annotation (com.github.cloudyrock.mongock.ChangeSet) qualify.
80+
// Mongock @ChangeUnit / @Execution / @BeforeExecution flows are excluded.
81+
// Native Flamingock @Apply method-level @Profile is also excluded.
82+
if (hasChangeSetAnnotation(change.getApplyMethod())) {
83+
Method applyMethod = change.getApplyMethod();
84+
if (applyMethod != null && applyMethod.isAnnotationPresent(Profile.class)) {
85+
return filterProfiles(Arrays.asList(applyMethod.getAnnotation(Profile.class).value()));
86+
}
87+
}
88+
89+
// Native Flamingock changes (and legacy fallback): profiles are declared at the change
90+
// (class) level only. The change is the atomic unit, so its profile gate lives on the
91+
// @Change-annotated class. Method-level @Profile (on @Apply or @Rollback) is
92+
// intentionally NOT honored: a per-method gate is incoherent for a change-level inclusion
93+
// decision and would risk applying a change while silently skipping its rollback under a
94+
// different active profile, breaking recovery semantics.
7995
Class<?> sourceClass = change.getImplementationClass();
8096
if (!sourceClass.isAnnotationPresent(Profile.class)) {
8197
return true; // no-profiled changeset always matches
@@ -84,6 +100,22 @@ private boolean filterCodeChange(CodeLoadedChange change) {
84100
return filterProfiles(changeProfile);
85101
}
86102

103+
/**
104+
* Checks whether the given method carries the legacy Mongock {@code @ChangeSet} annotation,
105+
* using its fully qualified name to avoid a compile-time dependency on the legacy module.
106+
*/
107+
private static boolean hasChangeSetAnnotation(Method method) {
108+
if (method == null) {
109+
return false;
110+
}
111+
for (java.lang.annotation.Annotation ann : method.getDeclaredAnnotations()) {
112+
if ("com.github.cloudyrock.mongock.ChangeSet".equals(ann.annotationType().getName())) {
113+
return true;
114+
}
115+
}
116+
return false;
117+
}
118+
87119
private boolean filterProfiles(List<String> changeProfile) {
88120
boolean changeHasAtLeastOneProfileApplied = false;
89121
for (String profile : changeProfile) {
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
* Copyright 2025 Flamingock (https://www.flamingock.io)
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.github.cloudyrock.mongock;
17+
18+
import java.lang.annotation.ElementType;
19+
import java.lang.annotation.Retention;
20+
import java.lang.annotation.RetentionPolicy;
21+
import java.lang.annotation.Target;
22+
23+
/**
24+
* Test-only minimal replica of the legacy Mongock {@code @ChangeSet} annotation.
25+
* <p>Replaces the {@code :legacy:mongock-support} test dependency so that
26+
* {@link io.flamingock.springboot.SpringbootProfileFilter} FQCN-based detection
27+
* of {@code com.github.cloudyrock.mongock.ChangeSet} can be verified without
28+
* pulling the full legacy module into the test classpath.</p>
29+
*/
30+
@Target(ElementType.METHOD)
31+
@Retention(RetentionPolicy.RUNTIME)
32+
public @interface ChangeSet {
33+
34+
String author();
35+
36+
String id();
37+
38+
String order();
39+
}

platform-plugins/flamingock-springboot-integration/src/test/java/io/flamingock/springboot/SpringProfileFilterCodeChangeTest.java

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
package io.flamingock.springboot;
1717

18+
import com.github.cloudyrock.mongock.ChangeSet;
1819
import io.flamingock.api.annotations.Apply;
1920
import io.flamingock.api.annotations.Change;
2021
import io.flamingock.internal.core.change.loaded.CodeLoadedChange;
@@ -92,6 +93,92 @@ private CodeLoadedChange getCodeLoadedChange(Class<?> sourceClass) {
9293
return LoadedChangeBuilder.getCodeBuilderInstance(sourceClass).build();
9394
}
9495

96+
private CodeLoadedChange getLegacyCodeLoadedChange(Class<?> sourceClass) {
97+
return LoadedChangeBuilder.getCodeBuilderInstance(sourceClass).setLegacy(true).build();
98+
}
99+
100+
// -----------------------------------------------------------------------
101+
// Legacy method @Profile tests — @ChangeSet vs ChangeUnit distinction
102+
// -----------------------------------------------------------------------
103+
104+
// _010__LegacyWithMethodProfile: legacy change setLegacy(true), method has @Profile but
105+
// does NOT carry @ChangeSet (simulates a ChangeUnit/@Execution-style change).
106+
// Method-level @Profile must be IGNORED — fall through to class-level (no class @Profile → unfiltered).
107+
108+
@Test
109+
@DisplayName("SHOULD return true WHEN legacy non-@ChangeSet method has @Profile(P1) and activeProfiles=[P1] — method @Profile is IGNORED")
110+
void legacyMethodProfileIgnoredWhenActiveProfileMatches() {
111+
assertTrue(new SpringbootProfileFilter("P1").filter(getLegacyCodeLoadedChange(_010__LegacyWithMethodProfile.class)));
112+
}
113+
114+
@Test
115+
@DisplayName("SHOULD return true WHEN legacy non-@ChangeSet method has @Profile(P1) and activeProfiles=[P2] — method @Profile is IGNORED")
116+
void legacyMethodProfileIgnoredWhenActiveProfileDoesNotMatch() {
117+
// Method-level @Profile is not honored for non-@ChangeSet methods (ChangeUnit-style),
118+
// so fall through to class-level: no class @Profile → unfiltered → true.
119+
assertTrue(new SpringbootProfileFilter("P2").filter(getLegacyCodeLoadedChange(_010__LegacyWithMethodProfile.class)));
120+
}
121+
122+
@Test
123+
@DisplayName("SHOULD return true WHEN legacy class has @Profile(P1) and method has no @Profile, activeProfiles=[P1]")
124+
void legacyClassProfileFallbackWhenNoMethodProfile() {
125+
assertTrue(new SpringbootProfileFilter("P1").filter(getLegacyCodeLoadedChange(_011__LegacyClassProfileNoMethod.class)));
126+
}
127+
128+
@Test
129+
@DisplayName("SHOULD return false WHEN legacy class has @Profile(P1) and method has no @Profile, activeProfiles=[P2]")
130+
void legacyClassProfileFallbackWhenNoMethodProfileNotMatching() {
131+
assertFalse(new SpringbootProfileFilter("P2").filter(getLegacyCodeLoadedChange(_011__LegacyClassProfileNoMethod.class)));
132+
}
133+
134+
// -----------------------------------------------------------------------
135+
// Legacy @ChangeSet method @Profile tests — @Profile must be HONORED
136+
// -----------------------------------------------------------------------
137+
138+
@Test
139+
@DisplayName("SHOULD return true WHEN legacy @ChangeSet method has @Profile(P1) and activeProfiles=[P1]")
140+
void changeSetMethodProfileHonoredWhenMatches() {
141+
assertTrue(new SpringbootProfileFilter("P1").filter(getLegacyCodeLoadedChange(_013__ChangeSetWithMethodProfile.class)));
142+
}
143+
144+
@Test
145+
@DisplayName("SHOULD return false WHEN legacy @ChangeSet method has @Profile(P1) and activeProfiles=[P2]")
146+
void changeSetMethodProfileNotHonoredWhenNotMatches() {
147+
assertFalse(new SpringbootProfileFilter("P2").filter(getLegacyCodeLoadedChange(_013__ChangeSetWithMethodProfile.class)));
148+
}
149+
150+
// -----------------------------------------------------------------------
151+
// Native method @Profile tests — method-level @Profile must be IGNORED
152+
// -----------------------------------------------------------------------
153+
154+
@Test
155+
@DisplayName("SHOULD return true WHEN native @Apply method has @Profile(P1) but no class @Profile, activeProfiles=[]")
156+
void nativeMethodProfileNotHonoredWhenActiveProfileEmpty() {
157+
// No class-level @Profile means the change is always unfiltered.
158+
// Method-level @Profile on native @Apply is NOT honored.
159+
assertTrue(new SpringbootProfileFilter().filter(getCodeLoadedChange(_012__NativeWithMethodProfile.class)));
160+
}
161+
162+
@Test
163+
@DisplayName("SHOULD return true WHEN native @Apply method has @Profile(P1) but no class @Profile, activeProfiles=[P1]")
164+
void nativeMethodProfileNotHonoredWhenActiveProfileMatches() {
165+
// Even when active profiles happen to match, the method-level @Profile is
166+
// not checked — it must not gate execution for native changes.
167+
assertTrue(new SpringbootProfileFilter("P1").filter(getCodeLoadedChange(_012__NativeWithMethodProfile.class)));
168+
}
169+
170+
@Test
171+
@DisplayName("SHOULD return true WHEN native @Apply method has @Profile(P1) but no class @Profile, activeProfiles=[P2]")
172+
void nativeMethodProfileNotHonoredWhenActiveProfileDoesNotMatch() {
173+
// Method-level @Profile must be entirely ignored for native changes.
174+
// Without class-level @Profile the change is unfiltered even when P2 is active.
175+
assertTrue(new SpringbootProfileFilter("P2").filter(getCodeLoadedChange(_012__NativeWithMethodProfile.class)));
176+
}
177+
178+
// =======================================================================
179+
// Test change classes
180+
// =======================================================================
181+
95182
@Change(id="not-annotated", author = "aperezdieppa")
96183
public static class _000__NotAnnotated {
97184
@Apply
@@ -126,4 +213,46 @@ public void apply() {
126213
// testing purpose
127214
}
128215
}
216+
217+
// Legacy-style change: method-level @Profile on the apply method
218+
@Change(id="legacy-method-profile", author = "test")
219+
public static class _010__LegacyWithMethodProfile {
220+
@Apply
221+
@Profile("P1")
222+
public void apply() {
223+
// testing purpose
224+
}
225+
}
226+
227+
// Legacy-style change: only class-level @Profile, method has none
228+
@Profile("P1")
229+
@Change(id="legacy-class-fallback", author = "test")
230+
public static class _011__LegacyClassProfileNoMethod {
231+
@Apply
232+
public void apply() {
233+
// testing purpose
234+
}
235+
}
236+
237+
// Native Flamingock change: method-level @Profile should be IGNORED by the filter
238+
@Change(id="native-method-profile", author = "test")
239+
public static class _012__NativeWithMethodProfile {
240+
@Apply
241+
@Profile("P1")
242+
public void apply() {
243+
// testing purpose
244+
}
245+
}
246+
247+
// Legacy @ChangeSet change: method-level @Profile should be HONORED by the filter
248+
@SuppressWarnings("deprecation")
249+
@Change(id="legacy-changeset-method-profile", author = "test")
250+
public static class _013__ChangeSetWithMethodProfile {
251+
@Apply
252+
@ChangeSet(author = "test", id = "test-id", order = "1")
253+
@Profile("P1")
254+
public void apply() {
255+
// testing purpose
256+
}
257+
}
129258
}

0 commit comments

Comments
 (0)