diff --git a/common/src/main/java/com/skyflow/logs/WarningLogs.java b/common/src/main/java/com/skyflow/logs/WarningLogs.java index 1eb1bcd8..74a8b8ad 100644 --- a/common/src/main/java/com/skyflow/logs/WarningLogs.java +++ b/common/src/main/java/com/skyflow/logs/WarningLogs.java @@ -8,7 +8,8 @@ public enum WarningLogs { EMPTY_DELETE_TOKENS_RESPONSE("DeleteTokens response did not include any token results."), INCOMPLETE_DELETE_TOKENS_RESPONSE("DeleteTokens response did not account for all requested tokens."), EMPTY_TOKENIZE_RESPONSE("Tokenize response did not include any record results."), - INCOMPLETE_TOKENIZE_RESPONSE("Tokenize response did not account for all requested records.") + INCOMPLETE_TOKENIZE_RESPONSE("Tokenize response did not account for all requested records."), + BETA_BUILD_WARNING("This is a beta/pre-release build of the Skyflow SDK (v%s1). Beta builds are intended for acceptance testing only - you appear to be connecting to a Production vault. Contact your Skyflow representative before using this build in Production.") ; private final String log; diff --git a/common/src/main/java/com/skyflow/utils/BaseUtils.java b/common/src/main/java/com/skyflow/utils/BaseUtils.java index e536c111..5fef9027 100644 --- a/common/src/main/java/com/skyflow/utils/BaseUtils.java +++ b/common/src/main/java/com/skyflow/utils/BaseUtils.java @@ -10,6 +10,7 @@ import java.security.spec.PKCS8EncodedKeySpec; import java.util.Base64; import java.util.Map; +import java.util.regex.Pattern; import com.google.gson.JsonObject; import com.skyflow.config.BaseCredentials; @@ -110,6 +111,14 @@ public static String parameterizedString(String base, String... args) { return base; } + // Beta/dev builds are published as ..-beta. or + // -dev. (see scripts/bump_version.sh); a plain public release has no suffix. + private static final Pattern GA_VERSION_PATTERN = Pattern.compile("^\\d+\\.\\d+\\.\\d+$"); + + public static boolean isNonGaVersion(String version) { + return version == null || !GA_VERSION_PATTERN.matcher(version).matches(); + } + protected static JsonObject getCommonMetrics() { JsonObject details = new JsonObject(); String deviceModel; diff --git a/common/src/test/java/com/skyflow/utils/BaseUtilsTests.java b/common/src/test/java/com/skyflow/utils/BaseUtilsTests.java index bc1bd216..769e665e 100644 --- a/common/src/test/java/com/skyflow/utils/BaseUtilsTests.java +++ b/common/src/test/java/com/skyflow/utils/BaseUtilsTests.java @@ -6,6 +6,7 @@ import com.skyflow.errors.ErrorCode; import com.skyflow.errors.ErrorMessage; import com.skyflow.errors.SkyflowException; +import com.skyflow.logs.WarningLogs; import org.junit.Assert; import org.junit.BeforeClass; import org.junit.Test; @@ -241,4 +242,57 @@ public void testGetCommonMetrics() { Assert.assertNotNull(metrics.get(BaseConstants.SDK_METRIC_RUNTIME_DETAILS)); Assert.assertNotNull(metrics.get(BaseConstants.SDK_METRIC_CLIENT_OS_DETAILS)); } + + // ── isNonGaVersion (SK-2963: beta-build-in-prod warning) ──────────────────── + + @Test + public void testIsNonGaVersion_plainSemverIsGa() { + Assert.assertFalse(BaseUtils.isNonGaVersion("1.0.0")); + Assert.assertFalse(BaseUtils.isNonGaVersion("2.11.3")); + } + + @Test + public void testIsNonGaVersion_betaSuffixIsNonGa() { + Assert.assertTrue(BaseUtils.isNonGaVersion("2.1.0-beta.1")); + } + + @Test + public void testIsNonGaVersion_devSuffixIsNonGa() { + Assert.assertTrue(BaseUtils.isNonGaVersion("2.1.0-dev.18f8f1ba")); + } + + @Test + public void testIsNonGaVersion_combinedBetaDevSuffixIsNonGa() { + // Real convention seen in the wild, e.g. flowvault samples pom.xml: + // 3.0.0-beta.13-dev.18f8f1ba + Assert.assertTrue(BaseUtils.isNonGaVersion("3.0.0-beta.13-dev.18f8f1ba")); + } + + @Test + public void testIsNonGaVersion_nullIsTreatedAsNonGa() { + Assert.assertTrue(BaseUtils.isNonGaVersion(null)); + } + + @Test + public void testIsNonGaVersion_nonSemverStringIsNonGa() { + Assert.assertTrue(BaseUtils.isNonGaVersion("v2")); + Assert.assertTrue(BaseUtils.isNonGaVersion("")); + } + + @Test + public void testIsNonGaVersion_zeroMajorPlainSemverIsTreatedAsGa() { + // Known cross-SDK design limitation, not unique to Java: a pre-1.0 version with no + // explicit pre-release suffix (e.g. "0.9.0") fully matches major.minor.patch and is + // therefore classified as GA here. Documenting the current behavior so a future change + // is a deliberate decision (flagged to the SK-2963 design owner) rather than a silent + // regression either way. + Assert.assertFalse(BaseUtils.isNonGaVersion("0.9.0")); + } + + @Test + public void testBetaBuildWarningMessage_interpolatesVersionCorrectly() { + String message = BaseUtils.parameterizedString(WarningLogs.BETA_BUILD_WARNING.getLog(), "1.2.3-beta.1"); + Assert.assertTrue(message.contains("v1.2.3-beta.1")); + Assert.assertFalse("Placeholder must be fully substituted", message.contains("%s1")); + } } diff --git a/flowvault/src/main/java/com/skyflow/Skyflow.java b/flowvault/src/main/java/com/skyflow/Skyflow.java index 5c2e682c..2f8604ca 100644 --- a/flowvault/src/main/java/com/skyflow/Skyflow.java +++ b/flowvault/src/main/java/com/skyflow/Skyflow.java @@ -2,11 +2,13 @@ import com.skyflow.config.Credentials; import com.skyflow.config.VaultConfig; +import com.skyflow.enums.Env; import com.skyflow.enums.LogLevel; import com.skyflow.errors.ErrorMessage; import com.skyflow.errors.SkyflowException; import com.skyflow.logs.ErrorLogs; import com.skyflow.logs.InfoLogs; +import com.skyflow.logs.WarningLogs; import com.skyflow.utils.Constants; import com.skyflow.utils.SdkVersion; import com.skyflow.utils.Utils; @@ -14,6 +16,7 @@ import com.skyflow.utils.validations.Validations; import com.skyflow.vault.controller.VaultController; +import java.util.Collection; import java.util.LinkedHashMap; public final class Skyflow extends BaseSkyflow { @@ -298,8 +301,28 @@ private void propagateHttpConfig() { } public Skyflow build() { + if (shouldWarnBetaBuildInProd(Constants.SDK_VERSION, this.vaultConfigMap.values())) { + LogUtil.printWarningLog(Utils.parameterizedString( + WarningLogs.BETA_BUILD_WARNING.getLog(), Constants.SDK_VERSION)); + } return new Skyflow(this); } + + // Package-private so it's directly unit-testable without needing a non-GA + // Constants.SDK_VERSION on the test classpath: build() is otherwise only + // exercisable end-to-end against whatever GA version this checkout ships. + static boolean shouldWarnBetaBuildInProd(String sdkVersion, Collection vaultConfigs) { + return Utils.isNonGaVersion(sdkVersion) && anyVaultIsProd(vaultConfigs); + } + + static boolean anyVaultIsProd(Collection vaultConfigs) { + for (VaultConfig vaultConfig : vaultConfigs) { + if (vaultConfig.getEnv() == Env.PROD) { + return true; + } + } + return false; + } } } \ No newline at end of file diff --git a/flowvault/src/test/java/com/skyflow/SkyflowTests.java b/flowvault/src/test/java/com/skyflow/SkyflowTests.java index 7986e68a..6f04fea9 100644 --- a/flowvault/src/test/java/com/skyflow/SkyflowTests.java +++ b/flowvault/src/test/java/com/skyflow/SkyflowTests.java @@ -5,22 +5,57 @@ import com.skyflow.enums.Env; import com.skyflow.enums.LogLevel; import com.skyflow.errors.SkyflowException; +import com.skyflow.utils.logger.LogUtil; import com.skyflow.vault.controller.VaultController; import org.junit.Assert; import org.junit.Test; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.LogRecord; +import java.util.logging.Logger; + public class SkyflowTests { private static final String EXCEPTION_NOT_THROWN = "Should have thrown an exception"; private static final String INVALID_EXCEPTION_THROWN = "Should not have thrown any exception"; private static VaultConfig buildConfig(String vaultId, String clusterId) { + return buildConfig(vaultId, clusterId, Env.DEV); + } + + private static VaultConfig buildConfig(String vaultId, String clusterId, Env env) { VaultConfig config = new VaultConfig(); config.setVaultId(vaultId); config.setClusterId(clusterId); - config.setEnv(Env.DEV); + config.setEnv(env); return config; } + // setupLogger calls LogManager.reset(), clearing all handlers, so the capturing + // handler must be attached after setupLogger runs (see LogUtilLevelTests). + private static class CapturingHandler extends Handler { + final List records = new ArrayList<>(); + + @Override + public void publish(LogRecord record) { + records.add(record); + } + + @Override public void flush() {} + @Override public void close() {} + } + + private static CapturingHandler attachCapture(LogLevel logLevel) { + LogUtil.setupLogger(logLevel); + CapturingHandler handler = new CapturingHandler(); + handler.setLevel(Level.ALL); + Logger.getLogger(LogUtil.class.getName()).addHandler(handler); + return handler; + } + // ── addVaultConfig ──────────────────────────────────────────────────────── @Test @@ -516,4 +551,73 @@ public void testGetVaultConfig_returnsNullForUnknownVaultId() throws SkyflowExce Skyflow client = Skyflow.builder().addVaultConfig(buildConfig("vault1", "cluster1")).build(); Assert.assertNull(client.getVaultConfig("vault-unknown")); } + + // ── SK-2963: beta-build-in-prod warning ────────────────────────────────── + + @Test + public void testAnyVaultIsProd_emptyCollectionIsFalse() { + Assert.assertFalse(Skyflow.SkyflowClientBuilder.anyVaultIsProd(new ArrayList<>())); + } + + @Test + public void testAnyVaultIsProd_noVaultIsProd() { + List configs = Arrays.asList( + buildConfig("vault1", "cluster1", Env.DEV), + buildConfig("vault2", "cluster2", Env.SANDBOX), + buildConfig("vault3", "cluster3", Env.STAGE)); + Assert.assertFalse(Skyflow.SkyflowClientBuilder.anyVaultIsProd(configs)); + } + + @Test + public void testAnyVaultIsProd_oneOfManyIsProd() { + List configs = Arrays.asList( + buildConfig("vault1", "cluster1", Env.DEV), + buildConfig("vault2", "cluster2", Env.PROD)); + Assert.assertTrue(Skyflow.SkyflowClientBuilder.anyVaultIsProd(configs)); + } + + @Test + public void testAnyVaultIsProd_allProd() { + List configs = Arrays.asList( + buildConfig("vault1", "cluster1", Env.PROD), + buildConfig("vault2", "cluster2", Env.PROD)); + Assert.assertTrue(Skyflow.SkyflowClientBuilder.anyVaultIsProd(configs)); + } + + // shouldWarnBetaBuildInProd is the exact predicate build() gates on. Constants.SDK_VERSION + // is a clean GA version in this checkout, so build() itself can never be driven down the + // "fires" branch end-to-end here; testing this predicate directly with an injected version + // string is what actually covers the && combination (e.g. would catch it being swapped for ||). + @Test + public void testShouldWarnBetaBuildInProd_betaVersionAndProdVault_isTrue() { + List configs = Arrays.asList(buildConfig("vault1", "cluster1", Env.PROD)); + Assert.assertTrue(Skyflow.SkyflowClientBuilder.shouldWarnBetaBuildInProd("1.0.0-beta.1", configs)); + } + + @Test + public void testShouldWarnBetaBuildInProd_gaVersionAndProdVault_isFalse() { + List configs = Arrays.asList(buildConfig("vault1", "cluster1", Env.PROD)); + Assert.assertFalse(Skyflow.SkyflowClientBuilder.shouldWarnBetaBuildInProd("1.0.0", configs)); + } + + @Test + public void testShouldWarnBetaBuildInProd_betaVersionAndNoProdVault_isFalse() { + List configs = Arrays.asList(buildConfig("vault1", "cluster1", Env.DEV)); + Assert.assertFalse(Skyflow.SkyflowClientBuilder.shouldWarnBetaBuildInProd("1.0.0-beta.1", configs)); + } + + // build() itself is wired against Constants.SDK_VERSION, which is a clean GA + // version in this checkout, so this only exercises the "stays silent" path + // end-to-end. isNonGaVersion's own beta/dev detection is covered directly in + // common's BaseUtilsTests; anyVaultIsProd's PROD-detection is covered above. + @Test + public void testBuild_currentGaVersionNeverWarnsEvenAgainstProdVault() throws SkyflowException { + CapturingHandler handler = attachCapture(LogLevel.WARN); + + Skyflow.builder().addVaultConfig(buildConfig("vault1", "cluster1", Env.PROD)).build(); + + boolean betaWarningLogged = handler.records.stream() + .anyMatch(r -> r.getLevel().equals(Level.WARNING) && r.getMessage().contains("beta/pre-release build")); + Assert.assertFalse("A GA build must never emit the beta-build warning", betaWarningLogged); + } } diff --git a/skyvault/src/main/java/com/skyflow/Skyflow.java b/skyvault/src/main/java/com/skyflow/Skyflow.java index 940f5008..30bb60a4 100644 --- a/skyvault/src/main/java/com/skyflow/Skyflow.java +++ b/skyvault/src/main/java/com/skyflow/Skyflow.java @@ -3,12 +3,15 @@ import com.skyflow.config.ConnectionConfig; import com.skyflow.config.Credentials; import com.skyflow.config.VaultConfig; +import com.skyflow.enums.Env; import com.skyflow.enums.LogLevel; import com.skyflow.errors.ErrorCode; import com.skyflow.errors.ErrorMessage; import com.skyflow.errors.SkyflowException; import com.skyflow.logs.ErrorLogs; import com.skyflow.logs.InfoLogs; +import com.skyflow.logs.WarningLogs; +import com.skyflow.utils.Constants; import com.skyflow.utils.Utils; import com.skyflow.utils.logger.LogUtil; import com.skyflow.utils.validations.Validations; @@ -16,6 +19,7 @@ import com.skyflow.vault.controller.DetectController; import com.skyflow.vault.controller.VaultController; +import java.util.Collection; import java.util.LinkedHashMap; public final class Skyflow extends BaseSkyflow { @@ -249,9 +253,29 @@ public SkyflowClientBuilder setLogLevel(LogLevel logLevel) { } public Skyflow build() { + if (shouldWarnBetaBuildInProd(Constants.SDK_VERSION, this.vaultConfigMap.values())) { + LogUtil.printWarningLog(Utils.parameterizedString( + WarningLogs.BETA_BUILD_WARNING.getLog(), Constants.SDK_VERSION)); + } return new Skyflow(this); } + // Package-private so it's directly unit-testable without needing a non-GA + // Constants.SDK_VERSION on the test classpath: build() is otherwise only + // exercisable end-to-end against whatever GA version this checkout ships. + static boolean shouldWarnBetaBuildInProd(String sdkVersion, Collection vaultConfigs) { + return Utils.isNonGaVersion(sdkVersion) && anyVaultIsProd(vaultConfigs); + } + + static boolean anyVaultIsProd(Collection vaultConfigs) { + for (VaultConfig vaultConfig : vaultConfigs) { + if (vaultConfig.getEnv() == Env.PROD) { + return true; + } + } + return false; + } + private ConnectionConfig findAndUpdateConnectionConfig(ConnectionConfig connectionConfig) { ConnectionConfig previousConfig = this.connectionConfigMap.get(connectionConfig.getConnectionId()); String connectionURL = connectionConfig.getConnectionUrl() != null ? connectionConfig.getConnectionUrl() : previousConfig.getConnectionUrl(); diff --git a/skyvault/src/test/java/com/skyflow/SkyflowTests.java b/skyvault/src/test/java/com/skyflow/SkyflowTests.java index 07386a5b..3f810260 100644 --- a/skyvault/src/test/java/com/skyflow/SkyflowTests.java +++ b/skyvault/src/test/java/com/skyflow/SkyflowTests.java @@ -16,6 +16,7 @@ import org.junit.Test; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.logging.Handler; import java.util.logging.Level; @@ -535,4 +536,82 @@ public void testFindAndUpdateConnectionConfig_withNullConnectionUrl_fallsBackToP Assert.fail("Reflection failed: " + e.getMessage()); } } + + // ── SK-2963: beta-build-in-prod warning ────────────────────────────────── + + private static VaultConfig betaTestVaultConfig(String vaultId, String clusterId, Env env) { + VaultConfig config = new VaultConfig(); + config.setVaultId(vaultId); + config.setClusterId(clusterId); + config.setEnv(env); + return config; + } + + @Test + public void testAnyVaultIsProd_emptyCollectionIsFalse() { + Assert.assertFalse(Skyflow.SkyflowClientBuilder.anyVaultIsProd(new ArrayList<>())); + } + + @Test + public void testAnyVaultIsProd_noVaultIsProd() { + List configs = Arrays.asList( + betaTestVaultConfig("vault1", "cluster1", Env.DEV), + betaTestVaultConfig("vault2", "cluster2", Env.SANDBOX), + betaTestVaultConfig("vault3", "cluster3", Env.STAGE)); + Assert.assertFalse(Skyflow.SkyflowClientBuilder.anyVaultIsProd(configs)); + } + + @Test + public void testAnyVaultIsProd_oneOfManyIsProd() { + List configs = Arrays.asList( + betaTestVaultConfig("vault1", "cluster1", Env.DEV), + betaTestVaultConfig("vault2", "cluster2", Env.PROD)); + Assert.assertTrue(Skyflow.SkyflowClientBuilder.anyVaultIsProd(configs)); + } + + @Test + public void testAnyVaultIsProd_allProd() { + List configs = Arrays.asList( + betaTestVaultConfig("vault1", "cluster1", Env.PROD), + betaTestVaultConfig("vault2", "cluster2", Env.PROD)); + Assert.assertTrue(Skyflow.SkyflowClientBuilder.anyVaultIsProd(configs)); + } + + // shouldWarnBetaBuildInProd is the exact predicate build() gates on. Constants.SDK_VERSION + // is a clean GA version in this checkout, so build() itself can never be driven down the + // "fires" branch end-to-end here; testing this predicate directly with an injected version + // string is what actually covers the && combination (e.g. would catch it being swapped for ||). + @Test + public void testShouldWarnBetaBuildInProd_betaVersionAndProdVault_isTrue() { + List configs = Arrays.asList(betaTestVaultConfig("vault1", "cluster1", Env.PROD)); + Assert.assertTrue(Skyflow.SkyflowClientBuilder.shouldWarnBetaBuildInProd("1.0.0-beta.1", configs)); + } + + @Test + public void testShouldWarnBetaBuildInProd_gaVersionAndProdVault_isFalse() { + List configs = Arrays.asList(betaTestVaultConfig("vault1", "cluster1", Env.PROD)); + Assert.assertFalse(Skyflow.SkyflowClientBuilder.shouldWarnBetaBuildInProd("1.0.0", configs)); + } + + @Test + public void testShouldWarnBetaBuildInProd_betaVersionAndNoProdVault_isFalse() { + List configs = Arrays.asList(betaTestVaultConfig("vault1", "cluster1", Env.DEV)); + Assert.assertFalse(Skyflow.SkyflowClientBuilder.shouldWarnBetaBuildInProd("1.0.0-beta.1", configs)); + } + + // build() itself is wired against Constants.SDK_VERSION, which is a clean GA + // version in this checkout, so this only exercises the "stays silent" path + // end-to-end. isNonGaVersion's own beta/dev detection is covered directly in + // common's BaseUtilsTests; anyVaultIsProd's PROD-detection is covered above. + @Test + public void testBuild_currentGaVersionNeverWarnsEvenAgainstProdVault() throws SkyflowException { + LogUtil.setupLogger(LogLevel.WARN); + CapturingHandler handler = attachCapture(); + + Skyflow.builder().addVaultConfig(betaTestVaultConfig("vault1", "cluster1", Env.PROD)).build(); + + boolean betaWarningLogged = handler.records.stream() + .anyMatch(r -> r.getLevel().equals(Level.WARNING) && r.getMessage().contains("beta/pre-release build")); + Assert.assertFalse("A GA build must never emit the beta-build warning", betaWarningLogged); + } }