Skip to content

Commit b32721f

Browse files
salevineclaude
andauthored
feat(server): stop CE startup when database was previously EE (#41906)
## Summary Running Appsmith **Community Edition (CE)** against a MongoDB that was previously operated by Appsmith **Enterprise Edition (EE)** is an unsupported EE→CE downgrade — CE migrations and CE code then run against EE-shaped data and can corrupt it. Today this happens silently. This PR adds a startup guard that detects the condition **before any Mongock migration runs** and **hard-stops CE startup** with a clear, operator-facing log message. ## How it works - **Where:** inside `MongoConfig.mongockInitializingBeanRunner`, immediately before `buildInitializingBeanRunner()` — guaranteed to run before any migration, and where the existing `checkForbiddenIds` already throws-to-abort. - **CE-only, no EE change:** the guard call is gated on `DeploymentProperties.getEdition() == "CE"`. An EE build already reports edition `"EE"` (hardcoded, binary-determined — not read from config), so EE skips the guard automatically and can never self-block against its own database. The edition value is shared via `DeploymentPropertiesCE.EDITION_CE`. - **Signal (verified against a live EE→CE database and the EE repo):** query the Mongock audit collection `mongockChangeLog`. EE records its `@ChangeUnit` migrations in the **parent** package `com.appsmith.server.migrations.db.` with an `EE` token in the class name (e.g. `Migration003EE01...`, `Migration042EE01AddWorkflowPlugin`), plus a legacy `com.appsmith.server.migrations.DatabaseChangelogEE`. CE migrations live in the `com.appsmith.server.migrations.db.ce.` subpackage, and CE has **zero** classes directly in the parent `db` package. So the DB was used by EE iff `mongockChangeLog` holds a `changeLogClass` under `...migrations.db.` that is **not** under `...migrations.db.ce.`, or the legacy `DatabaseChangelogEE`. This keys on the package layout (the same split Mongock scans), not an `EE` name token, so a CE class like `db.ce.EeThing` is not misread as EE. Prefixes are anchored and `Pattern.quote`-escaped. - **On detection (fail-closed):** log an ERROR banner — stable token `APPSMITH-EE-DOWNGRADE-BLOCKED`, with `changeId`/`changeLogClass` passed as CR/LF-stripped SLF4J parameters — then throw to abort startup. - **On query/infrastructure error (fail-open):** log a warning and proceed, in a catch that does **not** wrap the abort, so a flaky/locked-down Mongo never bricks a healthy CE instance. > Detection was corrected mid-branch: an earlier revision used a `...migrations.db.ee.` prefix, which does not exist in EE and so never fired. Verified against a real EE→CE database (84 EE records matched, 0 CE false positives; old rule matched 0). ## Testing - `EnterpriseDowngradeGuardCEImplTest` (embedded Mongo) — **8/8**: real EE class present → abort; legacy `DatabaseChangelogEE` → abort; CE-only (db.ce + legacy `DatabaseChangelog0/2`) → pass; empty collection → pass; missing collection → pass; `FAILED`-state EE record → abort; CE-package near-misses (`db.ce.EeThing`) → pass; mixed CE+EE database → abort. - `EnterpriseDowngradeGuardCEImplUnitTest` (Mockito) — **2/2**: fail-open when the query throws. - `spotless:check` and `test-compile` clean. Verified live: CE startup against an EE database now aborts. ## Follow-ups (not in this PR) 1. **CE invariant guard (recommended).** Add a CE test/ArchUnit assertion that CE migrations only ever live in `...migrations.db.ce.` (never directly in the parent `db` package), since the guard treats any parent-`db` class as EE. 2. **Docs:** replace the placeholder runbook URL behind the `APPSMITH-EE-DOWNGRADE-BLOCKED` token (`// TODO(docs)`). 3. **Optional EE-side test:** assert EE `DeploymentProperties.getEdition() == "EE"` so an accidentally-dropped override is caught in CI rather than at startup. > No EE-repo code change is required — the edition gate keeps the whole feature in CE. 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Release Notes * **New Features** * Community Edition startup now performs a pre-check for existing Enterprise Edition migration history to prevent incompatible downgrades. * If Enterprise migration evidence is detected, startup is stopped with a clear error message/banner. * **Bug Fixes** * Detection includes both current and legacy Enterprise migration markers and handles mixed scenarios. * The check is fail-open when audit data can’t be read, avoiding unnecessary startup blocks. * **Tests** * Added unit and integration tests covering blocking vs non-blocking outcomes, including empty or missing audit data. <!-- end of auto-generated comment: release notes by coderabbit.ai --> <!-- This is an auto-generated comment: Cypress test results --> > [!TIP] > 🟢 🟢 🟢 All cypress tests have passed! 🎉 🎉 🎉 > Workflow run: <https://github.com/appsmithorg/appsmith/actions/runs/27768419055> > Commit: 57f2657 > <a href="https://internal.appsmith.com/app/cypress-dashboard/rundetails-65890b3c81d7400d08fa9ee5?branch=master&workflowId=27768419055&attempt=1" target="_blank">Cypress dashboard</a>. > Tags: `@tag.All` > Spec: > <hr>Thu, 18 Jun 2026 15:59:26 UTC <!-- end of auto-generated comment: Cypress test results --> ## Automation /ok-to-test tags="@tag.All" --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 057cc61 commit b32721f

8 files changed

Lines changed: 412 additions & 2 deletions

File tree

app/server/appsmith-server/src/main/java/com/appsmith/server/configurations/DeploymentPropertiesCE.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@
1414
@Slf4j
1515
public class DeploymentPropertiesCE {
1616

17+
// The edition reported by a Community Edition build. EE overrides getEdition() to return "EE".
18+
// Shared so callers that branch on edition (e.g. MongoConfig's EE-downgrade guard) cannot drift.
19+
public static final String EDITION_CE = "CE";
20+
1721
private final String INFO_JSON_PATH = "/tmp/appsmith/infra.json";
1822
private String cloudProvider;
1923
private String tool;
@@ -22,7 +26,7 @@ public class DeploymentPropertiesCE {
2226
private String deployedAt;
2327

2428
public String getEdition() {
25-
return "CE";
29+
return EDITION_CE;
2630
}
2731

2832
public DeploymentPropertiesCE(ObjectMapper objectMapper) {

app/server/appsmith-server/src/main/java/com/appsmith/server/configurations/MongoConfig.java

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import com.appsmith.external.models.AuthenticationDTO;
66
import com.appsmith.server.configurations.mongo.SoftDeleteMongoRepositoryFactoryBean;
77
import com.appsmith.server.converters.StringToInstantConverter;
8+
import com.appsmith.server.helpers.EnterpriseDowngradeGuard;
89
import com.appsmith.server.repositories.BaseRepositoryImpl;
910
import com.github.cloudyrock.mongock.ChangeLog;
1011
import com.github.cloudyrock.mongock.ChangeSet;
@@ -170,7 +171,11 @@ public class MongoConfig {
170171
*/
171172
@Bean
172173
public MongockInitializingBeanRunner mongockInitializingBeanRunner(
173-
ApplicationContext springContext, MongoClient mongoClient, MongoProperties mongoProperties) {
174+
ApplicationContext springContext,
175+
MongoClient mongoClient,
176+
MongoProperties mongoProperties,
177+
EnterpriseDowngradeGuard enterpriseDowngradeGuard,
178+
DeploymentProperties deploymentProperties) {
174179
MongoReactiveDriver driver =
175180
MongoReactiveDriver.withDefaultLock(mongoClient, mongoProperties.getMongoClientDatabase());
176181
driver.setWriteConcern(WriteConcern.JOURNALED.withJournal(false));
@@ -184,6 +189,14 @@ public MongockInitializingBeanRunner mongockInitializingBeanRunner(
184189

185190
checkForbiddenIds(runnerBuilder);
186191

192+
// Only a CE build must refuse to start on an EE database. An EE build legitimately runs
193+
// against EE data, and DeploymentProperties#getEdition() already returns "EE" there, so the
194+
// guard is skipped for EE. This keeps the check entirely in CE with no EE-side override.
195+
if (DeploymentPropertiesCE.EDITION_CE.equals(deploymentProperties.getEdition())) {
196+
// Runs before any migration: hard-stop if this database was previously used by EE.
197+
enterpriseDowngradeGuard.assertNotEnterpriseDatabase(mongoClient, mongoProperties.getMongoClientDatabase());
198+
}
199+
187200
return runnerBuilder.buildInitializingBeanRunner();
188201
}
189202

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package com.appsmith.server.helpers;
2+
3+
import com.appsmith.server.helpers.ce.EnterpriseDowngradeGuardCE;
4+
5+
public interface EnterpriseDowngradeGuard extends EnterpriseDowngradeGuardCE {}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package com.appsmith.server.helpers;
2+
3+
import com.appsmith.server.helpers.ce.EnterpriseDowngradeGuardCEImpl;
4+
import org.springframework.stereotype.Component;
5+
6+
@Component
7+
public class EnterpriseDowngradeGuardImpl extends EnterpriseDowngradeGuardCEImpl implements EnterpriseDowngradeGuard {}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package com.appsmith.server.helpers.ce;
2+
3+
import com.mongodb.reactivestreams.client.MongoClient;
4+
5+
public interface EnterpriseDowngradeGuardCE {
6+
7+
void assertNotEnterpriseDatabase(MongoClient mongoClient, String databaseName);
8+
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
package com.appsmith.server.helpers.ce;
2+
3+
import com.mongodb.client.model.Filters;
4+
import com.mongodb.reactivestreams.client.MongoClient;
5+
import lombok.extern.slf4j.Slf4j;
6+
import org.bson.Document;
7+
import org.bson.conversions.Bson;
8+
import reactor.core.publisher.Mono;
9+
10+
import java.util.regex.Pattern;
11+
12+
@Slf4j
13+
public class EnterpriseDowngradeGuardCEImpl implements EnterpriseDowngradeGuardCE {
14+
15+
// How EE migrations are distinguished from CE migrations in the Mongock audit log:
16+
// - CE migrations live in the "...migrations.db.ce." subpackage
17+
// (e.g. com.appsmith.server.migrations.db.ce.Migration003...).
18+
// - EE migrations live in the PARENT "...migrations.db." package and carry an "EE" token in
19+
// their class name (e.g. com.appsmith.server.migrations.db.Migration003EE01...).
20+
// - The legacy EE changelog class "...migrations.DatabaseChangelogEE" predates the db package.
21+
// So a changeLogClass under "...migrations.db." that is NOT under "...migrations.db.ce." was
22+
// written by EE. Verified against the appsmith-ee repo: 69 EE @ChangeUnit classes, all directly
23+
// in the parent package; CE has zero classes directly in that package (all CE ones are in db.ce).
24+
// (The earlier ".db.ee." prefix was wrong — EE has no such package — so the guard never fired.)
25+
static final String MIGRATIONS_DB_PACKAGE_PREFIX = "com.appsmith.server.migrations.db.";
26+
static final String CE_MIGRATIONS_DB_PACKAGE_PREFIX = "com.appsmith.server.migrations.db.ce.";
27+
static final String LEGACY_EE_CHANGELOG_CLASS = "com.appsmith.server.migrations.DatabaseChangelogEE";
28+
29+
/**
30+
* Thrown to abort CE startup when the connected database was previously used by Appsmith
31+
* Enterprise Edition (EE). Aborting via a thrown {@link RuntimeException} during bean creation
32+
* matches the local convention in {@code MongoConfig} (see {@code checkForbiddenIds}).
33+
*/
34+
public static class EnterpriseDowngradeException extends RuntimeException {
35+
public EnterpriseDowngradeException(String message) {
36+
super(message);
37+
}
38+
}
39+
40+
@Override
41+
public void assertNotEnterpriseDatabase(MongoClient mongoClient, String databaseName) {
42+
// Mongock v5 records every applied (or failed) changeset in this default audit collection
43+
// (the matching lock collection is "mongockLock"). A document whose "changeLogClass" starts
44+
// with the EE migration package proves EE ran against this database.
45+
Document eeChangeLog;
46+
try {
47+
// EE = a migration recorded in the parent "...migrations.db." package but NOT in the CE
48+
// "...migrations.db.ce." subpackage, OR the legacy EE changelog class. Prefixes are
49+
// anchored and escaped via Pattern.quote so only exact package segments match.
50+
// Note: $not(regex) also matches docs where changeLogClass is absent/non-string, but it is
51+
// AND-ed with the positive parent-package regex (which requires a present, matching string),
52+
// so the negation only ever excludes the db.ce subset. Do not split these two clauses.
53+
final Bson filter = Filters.or(
54+
Filters.and(
55+
Filters.regex("changeLogClass", "^" + Pattern.quote(MIGRATIONS_DB_PACKAGE_PREFIX)),
56+
Filters.not(Filters.regex(
57+
"changeLogClass", "^" + Pattern.quote(CE_MIGRATIONS_DB_PACKAGE_PREFIX)))),
58+
Filters.eq("changeLogClass", LEGACY_EE_CHANGELOG_CLASS));
59+
// No "state" filter: a FAILED/partial EE migration still proves EE ran against this DB.
60+
// Blocking is acceptable here: this is one-time startup config code and Mongock's own
61+
// initialization (which runs immediately after) already blocks at this point.
62+
eeChangeLog = Mono.from(mongoClient
63+
.getDatabase(databaseName)
64+
.getCollection("mongockChangeLog")
65+
.find(filter)
66+
.limit(1)
67+
.first())
68+
.block();
69+
} catch (Exception e) {
70+
// Fail OPEN on infrastructure error: the signal is positive, so EE evidence must be
71+
// affirmatively found to justify a hard stop. Treating "couldn't read" as "is EE" would
72+
// brick CE operators with a flaky/locked-down Mongo. Mongock runs immediately after and
73+
// will itself fail loudly if Mongo is genuinely unreachable.
74+
log.warn("Could not run EE->CE downgrade check; proceeding", e);
75+
return;
76+
}
77+
78+
// Detection-abort lives OUTSIDE the catch above so the fail-open handler can never swallow it.
79+
if (eeChangeLog != null) {
80+
final String changeId = stripCrLf(eeChangeLog.getString("changeId"));
81+
final String changeLogClass = stripCrLf(eeChangeLog.getString("changeLogClass"));
82+
83+
// Log the operator-facing banner FIRST so it is the last thing printed before the
84+
// framework's BeanCreationException stack trace. CR/LF-stripped, parameterized values
85+
// avoid log forging.
86+
log.error(
87+
"""
88+
89+
################################################################
90+
APPSMITH STARTUP ABORTED [APPSMITH-EE-DOWNGRADE-BLOCKED]
91+
Enterprise -> Community (EE -> CE) downgrade detected.
92+
This database was previously used by Appsmith Enterprise Edition (EE).
93+
Detected EE migration record in 'mongockChangeLog':
94+
changeId = {}
95+
changeLogClass = {}
96+
Running Community Edition (CE) against an EE database is not supported and can
97+
corrupt your data. Startup has been stopped to protect the database.
98+
Supported paths forward: run an Appsmith EE build against this database, or
99+
restore a CE backup taken before EE was used.
100+
See: https://docs.appsmith.com/
101+
################################################################
102+
""",
103+
changeId,
104+
changeLogClass);
105+
// TODO(docs): replace the placeholder URL above with the runbook URL for the
106+
// APPSMITH-EE-DOWNGRADE-BLOCKED token once the docs team supplies it.
107+
108+
throw new EnterpriseDowngradeException(
109+
"EE -> CE downgrade detected; startup aborted to protect the database.");
110+
}
111+
}
112+
113+
private static String stripCrLf(String value) {
114+
if (value == null) {
115+
return null;
116+
}
117+
return value.replaceAll("[\\r\\n]", "");
118+
}
119+
}
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
package com.appsmith.server.helpers.ce;
2+
3+
import com.mongodb.MongoCommandException;
4+
import com.mongodb.reactivestreams.client.MongoClient;
5+
import com.mongodb.reactivestreams.client.MongoCollection;
6+
import org.bson.Document;
7+
import org.junit.jupiter.api.AfterEach;
8+
import org.junit.jupiter.api.BeforeEach;
9+
import org.junit.jupiter.api.Test;
10+
import org.springframework.beans.factory.annotation.Autowired;
11+
import org.springframework.boot.autoconfigure.mongo.MongoProperties;
12+
import org.springframework.boot.test.context.SpringBootTest;
13+
import org.springframework.test.context.ActiveProfiles;
14+
import reactor.core.publisher.Flux;
15+
import reactor.core.publisher.Mono;
16+
17+
import java.util.List;
18+
19+
import static org.assertj.core.api.Assertions.assertThatCode;
20+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
21+
22+
/**
23+
* Tests for {@link EnterpriseDowngradeGuardCEImpl} (the CE implementation of the EE -> CE downgrade
24+
* guard). Each case seeds the real Mongock audit collection ("mongockChangeLog") in the embedded
25+
* flapdoodle Mongo, runs the guard against the same reactive {@link MongoClient} bean that
26+
* {@code MongoConfig} uses, and asserts whether startup would be aborted.
27+
*/
28+
@SpringBootTest
29+
@ActiveProfiles("test")
30+
public class EnterpriseDowngradeGuardCEImplTest {
31+
32+
private static final String MONGOCK_CHANGE_LOG = "mongockChangeLog";
33+
34+
@Autowired
35+
private MongoClient mongoClient;
36+
37+
@Autowired
38+
private MongoProperties mongoProperties;
39+
40+
// Test the CE implementation directly. The bean wired into MongoConfig is the EE-overridable
41+
// EnterpriseDowngradeGuard; this class verifies the CE behaviour explicitly.
42+
private final EnterpriseDowngradeGuardCEImpl guard = new EnterpriseDowngradeGuardCEImpl();
43+
44+
private String databaseName;
45+
46+
private MongoCollection<Document> changeLogCollection() {
47+
return mongoClient.getDatabase(databaseName).getCollection(MONGOCK_CHANGE_LOG);
48+
}
49+
50+
private void clearChangeLog() {
51+
// deleteMany on a missing collection is a no-op; safe regardless of prior state.
52+
Mono.from(changeLogCollection().deleteMany(new Document())).block();
53+
}
54+
55+
private void seed(List<Document> docs) {
56+
Flux.fromIterable(docs)
57+
.flatMap(doc -> Mono.from(changeLogCollection().insertOne(doc)))
58+
.blockLast();
59+
}
60+
61+
private void runGuard() {
62+
guard.assertNotEnterpriseDatabase(mongoClient, databaseName);
63+
}
64+
65+
@BeforeEach
66+
public void setUp() {
67+
// Same database name resolution that MongoConfig uses to drive Mongock.
68+
databaseName = mongoProperties.getMongoClientDatabase();
69+
clearChangeLog();
70+
}
71+
72+
@AfterEach
73+
public void cleanUp() {
74+
// Drop the collection so no seeded audit record leaks into other tests in the context.
75+
Mono.from(changeLogCollection().drop()).block();
76+
}
77+
78+
private Document changeLog(String changeId, String changeLogClass) {
79+
return new Document().append("changeId", changeId).append("changeLogClass", changeLogClass);
80+
}
81+
82+
private Document changeLog(String changeId, String changeLogClass, String state) {
83+
return changeLog(changeId, changeLogClass).append("state", state);
84+
}
85+
86+
// Case 1: A real EE migration record present (EE class in the parent migrations.db package)
87+
// -> guard throws EnterpriseDowngradeException. Class name matches the actual appsmith-ee
88+
// convention "...migrations.db.MigrationNNNEEnn...".
89+
@Test
90+
public void assertNotEnterpriseDatabase_whenEeMigrationRecordPresent_throws() {
91+
seed(List.of(changeLog(
92+
"add-delete-user-policy",
93+
"com.appsmith.server.migrations.db.Migration003EE01AddDeleteUserPolicyToAllUsersAndAddTenantPolicyToDefaultTenant",
94+
"EXECUTED")));
95+
96+
assertThatThrownBy(this::runGuard)
97+
.isInstanceOf(EnterpriseDowngradeGuardCEImpl.EnterpriseDowngradeException.class);
98+
}
99+
100+
// Case 1b: The legacy EE changelog class "...migrations.DatabaseChangelogEE" -> guard throws.
101+
@Test
102+
public void assertNotEnterpriseDatabase_whenLegacyEeChangelogPresent_throws() {
103+
seed(List.of(changeLog("ee-legacy", "com.appsmith.server.migrations.DatabaseChangelogEE", "EXECUTED")));
104+
105+
assertThatThrownBy(this::runGuard)
106+
.isInstanceOf(EnterpriseDowngradeGuardCEImpl.EnterpriseDowngradeException.class);
107+
}
108+
109+
// Case 2: Only CE migration classes (db.ce subpackage + legacy DatabaseChangelog0/1/2)
110+
// -> does NOT throw. These are exactly the classes a CE-only database contains.
111+
@Test
112+
public void assertNotEnterpriseDatabase_whenOnlyCeMigrationRecordsPresent_doesNotThrow() {
113+
seed(List.of(
114+
changeLog(
115+
"ce-migration-003",
116+
"com.appsmith.server.migrations.db.ce.Migration003AddInstanceNameToTenantConfiguration",
117+
"EXECUTED"),
118+
changeLog("legacy-changelog-0", "com.appsmith.server.migrations.DatabaseChangelog0", "EXECUTED"),
119+
changeLog("legacy-changelog-2", "com.appsmith.server.migrations.DatabaseChangelog2", "EXECUTED")));
120+
121+
assertThatCode(this::runGuard).doesNotThrowAnyException();
122+
}
123+
124+
// Case 3: Empty mongockChangeLog collection -> does NOT throw.
125+
@Test
126+
public void assertNotEnterpriseDatabase_whenChangeLogEmpty_doesNotThrow() {
127+
// Ensure the collection exists but is empty. Ignore only the "collection already exists"
128+
// error (NamespaceExists, code 48); any other failure indicates broken setup and must surface.
129+
Mono.from(mongoClient.getDatabase(databaseName).createCollection(MONGOCK_CHANGE_LOG))
130+
.onErrorResume(
131+
e -> e instanceof MongoCommandException mce && mce.getErrorCode() == 48, e -> Mono.empty())
132+
.block();
133+
clearChangeLog();
134+
135+
assertThatCode(this::runGuard).doesNotThrowAnyException();
136+
}
137+
138+
// Case 4: Missing mongockChangeLog collection (dropped) -> does NOT throw.
139+
@Test
140+
public void assertNotEnterpriseDatabase_whenChangeLogCollectionMissing_doesNotThrow() {
141+
Mono.from(changeLogCollection().drop()).block();
142+
143+
assertThatCode(this::runGuard).doesNotThrowAnyException();
144+
}
145+
146+
// Case 5: EE record with state "FAILED" -> still throws (detection is state-agnostic).
147+
@Test
148+
public void assertNotEnterpriseDatabase_whenEeMigrationFailed_stillThrows() {
149+
seed(List.of(changeLog(
150+
"ee-failed", "com.appsmith.server.migrations.db.Migration042EE01AddWorkflowPlugin", "FAILED")));
151+
152+
assertThatThrownBy(this::runGuard)
153+
.isInstanceOf(EnterpriseDowngradeGuardCEImpl.EnterpriseDowngradeException.class);
154+
}
155+
156+
// Case 6: CE-package near misses -> does NOT throw. These must NOT be mistaken for EE:
157+
// - "...db.ce.EeThing" lives under the CE package and contains "Ee" in its name.
158+
// - "...db.ce.Migration075..." is a normal CE migration in the db.ce subpackage.
159+
// Proves detection excludes the db.ce subpackage rather than naively matching "db." or "EE".
160+
@Test
161+
public void assertNotEnterpriseDatabase_whenCePackageNearMisses_doesNotThrow() {
162+
seed(List.of(
163+
changeLog("ce-eething", "com.appsmith.server.migrations.db.ce.EeThing", "EXECUTED"),
164+
changeLog(
165+
"ce-migration-075",
166+
"com.appsmith.server.migrations.db.ce.Migration075SeedSuperUserSetupLock",
167+
"EXECUTED")));
168+
169+
assertThatCode(this::runGuard).doesNotThrowAnyException();
170+
}
171+
172+
// Case 7: A mixed CE + EE database (the real downgrade scenario) -> throws. An EE record alongside
173+
// many CE records must still trip the guard.
174+
@Test
175+
public void assertNotEnterpriseDatabase_whenCeAndEeRecordsMixed_throws() {
176+
seed(List.of(
177+
changeLog(
178+
"ce-migration-003",
179+
"com.appsmith.server.migrations.db.ce.Migration003AddInstanceNameToTenantConfiguration",
180+
"EXECUTED"),
181+
changeLog(
182+
"ee-add-workflow-plugin",
183+
"com.appsmith.server.migrations.db.Migration042EE01AddWorkflowPlugin",
184+
"EXECUTED")));
185+
186+
assertThatThrownBy(this::runGuard)
187+
.isInstanceOf(EnterpriseDowngradeGuardCEImpl.EnterpriseDowngradeException.class);
188+
}
189+
}

0 commit comments

Comments
 (0)