From 07126e5b3b4827536d6d2dd8a0df327651f19eb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20Wlaz=C5=82y?= Date: Tue, 30 Jun 2026 08:14:21 +0000 Subject: [PATCH] feat(codereadiness): add hook for version-based flag validation Introduce the codereadiness hook to control feature flag evaluation by comparing the application's current version with a required minimum version specified in the flag's metadata. If the comparator returns false the hook returns an error to trigger fallback to the default flag value. --- .release-please-manifest.json | 1 + hooks/codereadiness/README.md | 87 +++++++ hooks/codereadiness/pom.xml | 31 +++ .../codereadiness/CodeReadinessHook.java | 95 ++++++++ .../hooks/codereadiness/SemVerComparator.java | 29 +++ .../codereadiness/VersionComparator.java | 21 ++ .../codereadiness/CodeReadinessHookTest.java | 228 ++++++++++++++++++ .../codereadiness/SemVerComparatorTest.java | 61 +++++ pom.xml | 1 + release-please-config.json | 11 + 10 files changed, 565 insertions(+) create mode 100644 hooks/codereadiness/README.md create mode 100644 hooks/codereadiness/pom.xml create mode 100644 hooks/codereadiness/src/main/java/dev/openfeature/contrib/hooks/codereadiness/CodeReadinessHook.java create mode 100644 hooks/codereadiness/src/main/java/dev/openfeature/contrib/hooks/codereadiness/SemVerComparator.java create mode 100644 hooks/codereadiness/src/main/java/dev/openfeature/contrib/hooks/codereadiness/VersionComparator.java create mode 100644 hooks/codereadiness/src/test/java/dev/openfeature/contrib/hooks/codereadiness/CodeReadinessHookTest.java create mode 100644 hooks/codereadiness/src/test/java/dev/openfeature/contrib/hooks/codereadiness/SemVerComparatorTest.java diff --git a/.release-please-manifest.json b/.release-please-manifest.json index aeaaaad2d..68e55afa1 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,5 +1,6 @@ { "hooks/open-telemetry": "3.3.1", + "hooks/codereadiness": "0.1.0", "providers/flagd": "0.14.0", "providers/go-feature-flag": "1.1.2", "providers/flagsmith": "0.0.13", diff --git a/hooks/codereadiness/README.md b/hooks/codereadiness/README.md new file mode 100644 index 000000000..aa869247e --- /dev/null +++ b/hooks/codereadiness/README.md @@ -0,0 +1,87 @@ +# Code Readiness Hook + +The `codereadiness` hook allows controlling feature flag evaluation based on the version of the application code. +It does this by comparing the current application version with a required minimum version specified in the flag's metadata. +If the comparison fails (i.e., the application version is lower than the required version), the hook returns an error, causing the flag evaluation to resolve to its configured default value. + +## Installation + +```xml + + dev.openfeature.contrib.hooks + code-readiness-hook + 0.1.0 + +``` + +## Setup + +First, import the OpenFeature SDK and the code readiness hook: + +```java +import dev.openfeature.sdk.OpenFeatureAPI; +import dev.openfeature.contrib.hooks.codereadiness.CodeReadinessHook; +``` + +Then, configure the hook with the current version of the application code and register it: + +```java +// currentVersion is the current version of the code, which can be retrieved +// from environment variables, build properties, or configuration files. +String currentVersion = "1.0.0"; + +CodeReadinessHook codeReadinessHook = CodeReadinessHook.builder(currentVersion).build(); + +// Register the hook globally at the OpenFeature API level +OpenFeatureAPI.getInstance().addHooks(codeReadinessHook); +``` + +## How It Works + +1. The hook runs during the **After** phase of flag evaluation. +2. It extracts the metadata associated with the evaluated flag. +3. It looks for a specific metadata key (by default, `minCodeVersion`). +4. If found, it compares the current application version against the required minimum version using the configured comparator (by default, a semver comparison). +5. If the current version is **lower** than the required version, it returns an error. This triggers the OpenFeature SDK's fallback mechanism, returning the flag's **default value** to the caller. + +## Options + +The behavior of the hook can be customized by passing options to the builder: + +### Strict Validation + +By default, the hook will **not** fail if the `minCodeVersion` metadata or the current application version is missing. To enforce version validation and return an error when these versions are missing, use `strictValidation(true)`. + +```java +CodeReadinessHook codeReadinessHook = CodeReadinessHook.builder("1.0.0") + .strictValidation(true) + .build(); +``` + +### Custom Metadata Key + +To configure the hook to look for a key other than the default `"minCodeVersion"` in the flag's metadata, use `metadataMinVerKey()`. + +```java +CodeReadinessHook codeReadinessHook = CodeReadinessHook.builder("1.0.0") + .metadataMinVerKey("customMetadataKey") + .build(); +``` + +### Custom Comparator + +By default, the hook performs a standard semver comparison. If the application uses a different versioning scheme (such as date-based versioning, revision numbers, or custom build numbers), a custom comparison interface implementation can be provided using `comparator()`. + +```java +import dev.openfeature.contrib.hooks.codereadiness.VersionComparator; + +VersionComparator customComparator = (current, required) -> { + // Custom comparison logic: return true if current is ready/sufficient, + // or false if current is lower than required. + return current.compareTo(required) >= 0; +}; + +CodeReadinessHook codeReadinessHook = CodeReadinessHook.builder("2026.06.30") + .comparator(customComparator) + .build(); +``` \ No newline at end of file diff --git a/hooks/codereadiness/pom.xml b/hooks/codereadiness/pom.xml new file mode 100644 index 000000000..40d5cfd9b --- /dev/null +++ b/hooks/codereadiness/pom.xml @@ -0,0 +1,31 @@ + + + 4.0.0 + + dev.openfeature.contrib + parent + [1.0,2.0) + ../../pom.xml + + dev.openfeature.contrib.hooks + code-readiness-hook + 0.1.0 + + code-readiness-hook + Code Readiness Hook + https://openfeature.dev + + + + ${groupId}.codereadiness + + + + + org.semver4j + semver4j + 5.8.0 + + + \ No newline at end of file diff --git a/hooks/codereadiness/src/main/java/dev/openfeature/contrib/hooks/codereadiness/CodeReadinessHook.java b/hooks/codereadiness/src/main/java/dev/openfeature/contrib/hooks/codereadiness/CodeReadinessHook.java new file mode 100644 index 000000000..e352a920b --- /dev/null +++ b/hooks/codereadiness/src/main/java/dev/openfeature/contrib/hooks/codereadiness/CodeReadinessHook.java @@ -0,0 +1,95 @@ +package dev.openfeature.contrib.hooks.codereadiness; + +import dev.openfeature.sdk.FlagEvaluationDetails; +import dev.openfeature.sdk.Hook; +import dev.openfeature.sdk.HookContext; +import dev.openfeature.sdk.ImmutableMetadata; +import dev.openfeature.sdk.exceptions.GeneralError; + +import java.util.Map; +import java.util.Objects; + +import lombok.Builder; +import lombok.extern.slf4j.Slf4j; + +@Builder(builderMethodName = "", builderClassName = "Builder") +@Slf4j +public class CodeReadinessHook implements Hook { + + private static final String DEFAULT_MIN_CODE_VERSION_KEY = "minCodeVersion"; + private static final boolean DEFAULT_STRICT_VALIDATION = false; + private static final VersionComparator DEFAULT_VERSION_COMPARATOR = new SemVerComparator(); + + private final String currentVersion; + + @Builder.Default + private final boolean strictValidation = DEFAULT_STRICT_VALIDATION; + + @Builder.Default + private final String metadataMinVerKey = DEFAULT_MIN_CODE_VERSION_KEY; + + @Builder.Default + private final VersionComparator comparator = DEFAULT_VERSION_COMPARATOR; + + CodeReadinessHook(String currentVersion, boolean strictValidation, String metadataMinVerKey, VersionComparator comparator) { + this.currentVersion = Objects.requireNonNull(currentVersion, "codereadiness: currentVersion cannot be null"); + this.strictValidation = strictValidation; + this.metadataMinVerKey = Objects.requireNonNull(metadataMinVerKey, "codereadiness: metadataMinVerKey cannot be null"); + this.comparator = Objects.requireNonNull(comparator, "codereadiness: comparator cannot be null"); + } + + public static Builder builder(String currentVersion) { + Objects.requireNonNull(currentVersion, "codereadiness: currentVersion cannot be null"); + return new Builder().currentVersion(currentVersion); + } + + @Override + public void after(HookContext ctx, FlagEvaluationDetails details, Map hints) { + ImmutableMetadata metadata = details != null ? details.getFlagMetadata() : null; + if (metadata == null || metadata.isEmpty()) { + if (strictValidation) { + throw new GeneralError(String.format("flag metadata is null for flag \"%s\"", ctx.getFlagKey())); + } + log.debug("flag metadata is null for flag \"{}\", skipping validation", ctx.getFlagKey()); + return; + } + Object minVerObj = metadata.asUnmodifiableMap().get(metadataMinVerKey); + if (minVerObj == null) { + if (strictValidation) { + throw new GeneralError(String.format("key \"%s\" missing in flag's \"%s\" metadata", metadataMinVerKey, ctx.getFlagKey())); + } + log.debug("key \"{}\" missing in flag's \"{}\" metadata, skipping validation", metadataMinVerKey, ctx.getFlagKey()); + return; + } + if (!(minVerObj instanceof String)) { + if (strictValidation) { + throw new GeneralError(String.format("metadata \"%s\" is not a string for flag \"%s\"", metadataMinVerKey, ctx.getFlagKey())); + } + log.debug("metadata \"{}\" is not a string for flag \"{}\", skipping validation", metadataMinVerKey, ctx.getFlagKey()); + return; + } + String minCodeVersion = (String) minVerObj; + if (minCodeVersion.isEmpty()) { + if (strictValidation) { + throw new GeneralError(String.format("metadata \"%s\" is empty for flag \"%s\"", metadataMinVerKey, ctx.getFlagKey())); + } + log.debug("metadata \"{}\" is empty for flag \"{}\", skipping validation", metadataMinVerKey, ctx.getFlagKey()); + return; + } + boolean isCodeReady; + try { + isCodeReady = comparator.compare(currentVersion, minCodeVersion); + } catch (Exception err) { + throw new GeneralError( + String.format( + "current version: \"%s\" required minimum version: \"%s\" check failed: %s", + currentVersion, minCodeVersion, err.getMessage()), + err); + } + if (!isCodeReady) { + throw new GeneralError(String.format("current version: \"%s\" required minimum version: \"%s\" check failed", currentVersion, minCodeVersion)); + } + } + + +} \ No newline at end of file diff --git a/hooks/codereadiness/src/main/java/dev/openfeature/contrib/hooks/codereadiness/SemVerComparator.java b/hooks/codereadiness/src/main/java/dev/openfeature/contrib/hooks/codereadiness/SemVerComparator.java new file mode 100644 index 000000000..3e7a267b2 --- /dev/null +++ b/hooks/codereadiness/src/main/java/dev/openfeature/contrib/hooks/codereadiness/SemVerComparator.java @@ -0,0 +1,29 @@ +package dev.openfeature.contrib.hooks.codereadiness; + +import dev.openfeature.sdk.FlagEvaluationDetails; +import dev.openfeature.sdk.Hook; +import dev.openfeature.sdk.HookContext; +import dev.openfeature.sdk.ImmutableMetadata; +import dev.openfeature.sdk.exceptions.GeneralError; +import java.util.Map; +import org.semver4j.Semver; + +public class SemVerComparator implements VersionComparator { + @Override + public boolean compare(String currentVersion, String minCodeVersion) throws Exception { + String currentFormatted = currentVersion != null && currentVersion.startsWith("v") ? currentVersion : "v" + currentVersion; + String minCodeVersionFormatted = minCodeVersion != null && minCodeVersion.startsWith("v") ? minCodeVersion : "v" + minCodeVersion; + + Semver currentSemver = Semver.parse(currentFormatted); + if (currentSemver == null) { + throw new IllegalArgumentException(String.format("invalid current semver: \"%s\"", currentVersion)); + } + + Semver minCodeVersionSemver = Semver.parse(minCodeVersionFormatted); + if (minCodeVersionSemver == null) { + throw new IllegalArgumentException(String.format("invalid min code version semver: \"%s\"", minCodeVersion)); + } + + return currentSemver.isGreaterThan(minCodeVersionSemver) || currentSemver.isEqualTo(minCodeVersionSemver); + } +} diff --git a/hooks/codereadiness/src/main/java/dev/openfeature/contrib/hooks/codereadiness/VersionComparator.java b/hooks/codereadiness/src/main/java/dev/openfeature/contrib/hooks/codereadiness/VersionComparator.java new file mode 100644 index 000000000..cbefbb9f4 --- /dev/null +++ b/hooks/codereadiness/src/main/java/dev/openfeature/contrib/hooks/codereadiness/VersionComparator.java @@ -0,0 +1,21 @@ +package dev.openfeature.contrib.hooks.codereadiness; + +/** + * Defines the contract for comparing code version strings (current and minimum required + * version) according to specified rules. Used by {@link CodeReadinessHook}. + * + *

The {@link CodeReadinessHook} uses {@link SemVerComparator} by default for standard Semantic + * Versioning, but developers may implement this interface to support custom or non-standard + * versioning schemes. + */ +public interface VersionComparator { + /** + * Compare current version with required version. + * + * @param currentVersion of the application + * @param minCodeVersion required minimum version + * @return true if currentVersion is greater than or equal to minCodeVersion + * @throws Exception if there is a parsing error + */ + boolean compare(String currentVersion, String minCodeVersion) throws Exception; +} \ No newline at end of file diff --git a/hooks/codereadiness/src/test/java/dev/openfeature/contrib/hooks/codereadiness/CodeReadinessHookTest.java b/hooks/codereadiness/src/test/java/dev/openfeature/contrib/hooks/codereadiness/CodeReadinessHookTest.java new file mode 100644 index 000000000..94ecdf19f --- /dev/null +++ b/hooks/codereadiness/src/test/java/dev/openfeature/contrib/hooks/codereadiness/CodeReadinessHookTest.java @@ -0,0 +1,228 @@ +package dev.openfeature.contrib.hooks.codereadiness; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import dev.openfeature.sdk.FlagEvaluationDetails; +import dev.openfeature.sdk.HookContext; +import dev.openfeature.sdk.ImmutableMetadata; +import dev.openfeature.sdk.exceptions.GeneralError; +import java.util.Collections; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class CodeReadinessHookTest { + + private final HookContext hookContext = mock(HookContext.class); + + @BeforeEach + void setUp() { + when(hookContext.getFlagKey()).thenReturn("testFlag"); + } + + @Test + @DisplayName("Should pass when current version is equal or greater than required metadata version") + void testValidVersionPasses() { + CodeReadinessHook hook = CodeReadinessHook.builder("1.5.0").build(); + FlagEvaluationDetails details = createDetailsWithMetadata("minCodeVersion", "1.2.0"); + + assertThatCode(() -> hook.after(hookContext, details, Collections.emptyMap())) + .doesNotThrowAnyException(); + } + + @Test + @DisplayName("Should throw GeneralError when current version is less than required metadata version") + void testInvalidVersionThrowsGeneralError() { + CodeReadinessHook hook = CodeReadinessHook.builder("1.0.0").build(); + FlagEvaluationDetails details = createDetailsWithMetadata("minCodeVersion", "1.2.0"); + + assertThatThrownBy(() -> hook.after(hookContext, details, Collections.emptyMap())) + .isInstanceOf(GeneralError.class) + .hasMessage("current version: \"1.0.0\" required minimum version: \"1.2.0\" check failed"); + } + + @Test + @DisplayName("Should ignore missing metadata when strictValidation is false") + void testMissingMetadataIgnoredWhenValidationNotRequired() { + CodeReadinessHook hook = CodeReadinessHook.builder("1.0.0") + .strictValidation(false) + .build(); + + assertThatCode(() -> hook.after(hookContext, null, Collections.emptyMap())) + .doesNotThrowAnyException(); + + FlagEvaluationDetails emptyMetadataDetails = FlagEvaluationDetails.builder() + .flagMetadata(ImmutableMetadata.builder().build()) + .build(); + assertThatCode(() -> hook.after(hookContext, emptyMetadataDetails, Collections.emptyMap())) + .doesNotThrowAnyException(); + } + + @Test + @DisplayName("Should throw GeneralError when metadata is missing and strictValidation is true") + void testMissingMetadataThrowsWhenStrictValidation() { + CodeReadinessHook hook = CodeReadinessHook.builder("1.0.0") + .strictValidation(true) + .build(); + + assertThatThrownBy(() -> hook.after(hookContext, null, Collections.emptyMap())) + .isInstanceOf(GeneralError.class) + .hasMessage("flag metadata is null for flag \"testFlag\""); + + FlagEvaluationDetails emptyMetadataDetails = FlagEvaluationDetails.builder() + .flagMetadata(ImmutableMetadata.builder().build()) + .build(); + assertThatThrownBy(() -> hook.after(hookContext, emptyMetadataDetails, Collections.emptyMap())) + .isInstanceOf(GeneralError.class) + .hasMessage("flag metadata is null for flag \"testFlag\""); + } + + @Test + @DisplayName("Should ignore missing minCodeVersion key when strictValidation is false") + void testMissingKeyIgnoredWhenStrictValidationNotRequired() { + CodeReadinessHook hook = CodeReadinessHook.builder("1.0.0") + .strictValidation(false) + .build(); + FlagEvaluationDetails details = createDetailsWithMetadata("otherKey", "1.0.0"); + + assertThatCode(() -> hook.after(hookContext, details, Collections.emptyMap())) + .doesNotThrowAnyException(); + } + + @Test + @DisplayName("Should throw GeneralError when minCodeVersion key is missing and strictValidation is true") + void testMissingKeyThrowsWhenStrictValidation() { + CodeReadinessHook hook = CodeReadinessHook.builder("1.0.0") + .strictValidation(true) + .build(); + FlagEvaluationDetails details = createDetailsWithMetadata("otherKey", "1.0.0"); + + assertThatThrownBy(() -> hook.after(hookContext, details, Collections.emptyMap())) + .isInstanceOf(GeneralError.class) + .hasMessage("key \"minCodeVersion\" missing in flag's \"testFlag\" metadata"); + } + + @Test + @DisplayName("Should use custom metadataMinVerKey when specified") + void testCustomMetadataMinVerKey() { + CodeReadinessHook hook = CodeReadinessHook.builder("2.0.0") + .metadataMinVerKey("customMinVersion") + .strictValidation(true) + .build(); + FlagEvaluationDetails details = createDetailsWithMetadata("customMinVersion", "1.5.0"); + + assertThatCode(() -> hook.after(hookContext, details, Collections.emptyMap())) + .doesNotThrowAnyException(); + } + + @Test + @DisplayName("Should throw GeneralError when metadata value is not a string and strictValidation is true") + void testNonStringMetadataValueThrows() { + CodeReadinessHook hook = CodeReadinessHook.builder("1.0.0").strictValidation(true).build(); + FlagEvaluationDetails details = createDetailsWithMetadata("minCodeVersion", true); + + assertThatThrownBy(() -> hook.after(hookContext, details, Collections.emptyMap())) + .isInstanceOf(GeneralError.class) + .hasMessage("metadata \"minCodeVersion\" is not a string for flag \"testFlag\""); + } + + @Test + @DisplayName("Should ignore non string metadata value when strictValidation is false") + void testNonStringMetadataValueIgnores() { + CodeReadinessHook hook = CodeReadinessHook.builder("1.0.0").strictValidation(false).build(); + FlagEvaluationDetails details = createDetailsWithMetadata("minCodeVersion", true); + + assertThatCode(() -> hook.after(hookContext, details, Collections.emptyMap())) + .doesNotThrowAnyException(); + } + + @Test + @DisplayName("Should ignore empty minCodeVersion value when strictValidation is false") + void testEmptyVersionStringIgnoredWhenStrictValidationNotRequired() { + CodeReadinessHook hook = CodeReadinessHook.builder("1.0.0") + .strictValidation(false) + .build(); + FlagEvaluationDetails details = createDetailsWithMetadata("minCodeVersion", ""); + + assertThatCode(() -> hook.after(hookContext, details, Collections.emptyMap())) + .doesNotThrowAnyException(); + } + + @Test + @DisplayName("Should throw GeneralError when minCodeVersion string is empty and strictValidation is true") + void testEmptyVersionStringThrowsWhenStrictValidation() { + CodeReadinessHook hook = CodeReadinessHook.builder("1.0.0") + .strictValidation(true) + .build(); + FlagEvaluationDetails details = createDetailsWithMetadata("minCodeVersion", ""); + + assertThatThrownBy(() -> hook.after(hookContext, details, Collections.emptyMap())) + .isInstanceOf(GeneralError.class) + .hasMessage("metadata \"minCodeVersion\" is empty for flag \"testFlag\""); + } + + @Test + @DisplayName("Should throw NullPointerException when building hook with null arguments") + void testNullArgumentsThrowNpeAtBuildTime() { + assertThatThrownBy(() -> CodeReadinessHook.builder(null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("codereadiness: currentVersion cannot be null"); + + assertThatThrownBy(() -> CodeReadinessHook.builder("1.0.0").comparator(null).build()) + .isInstanceOf(NullPointerException.class) + .hasMessage("codereadiness: comparator cannot be null"); + + assertThatThrownBy(() -> CodeReadinessHook.builder("1.0.0").metadataMinVerKey(null).build()) + .isInstanceOf(NullPointerException.class) + .hasMessage("codereadiness: metadataMinVerKey cannot be null"); + } + + @Test + @DisplayName("Should use custom comparator when configured") + void testCustomComparator() throws Exception { + VersionComparator mockComparator = mock(VersionComparator.class); + when(mockComparator.compare(anyString(), anyString())).thenReturn(false); + + CodeReadinessHook hook = CodeReadinessHook.builder("10.0.0") + .comparator(mockComparator) + .build(); + FlagEvaluationDetails details = createDetailsWithMetadata("minCodeVersion", "1.0.0"); + + assertThatThrownBy(() -> hook.after(hookContext, details, Collections.emptyMap())) + .isInstanceOf(GeneralError.class) + .hasMessage("current version: \"10.0.0\" required minimum version: \"1.0.0\" check failed"); + } + + @Test + @DisplayName("Should wrap exception thrown by comparator into GeneralError") + void testComparatorExceptionWrappedInGeneralError() throws Exception { + VersionComparator mockComparator = mock(VersionComparator.class); + when(mockComparator.compare(anyString(), anyString())).thenThrow(new RuntimeException("comparator error")); + + CodeReadinessHook hook = CodeReadinessHook.builder("1.0.0") + .comparator(mockComparator) + .build(); + FlagEvaluationDetails details = createDetailsWithMetadata("minCodeVersion", "1.0.0"); + + assertThatThrownBy(() -> hook.after(hookContext, details, Collections.emptyMap())) + .isInstanceOf(GeneralError.class) + .hasMessage("current version: \"1.0.0\" required minimum version: \"1.0.0\" check failed: comparator error") + .hasCauseInstanceOf(RuntimeException.class); + } + + private FlagEvaluationDetails createDetailsWithMetadata(String key, Object value) { + ImmutableMetadata.ImmutableMetadataBuilder builder = ImmutableMetadata.builder(); + if (value instanceof Boolean) { + builder.addBoolean(key, (Boolean) value); + } else { + builder.addString(key, value.toString()); + } + return FlagEvaluationDetails.builder() + .flagMetadata(builder.build()) + .build(); + } +} diff --git a/hooks/codereadiness/src/test/java/dev/openfeature/contrib/hooks/codereadiness/SemVerComparatorTest.java b/hooks/codereadiness/src/test/java/dev/openfeature/contrib/hooks/codereadiness/SemVerComparatorTest.java new file mode 100644 index 000000000..44869d8ef --- /dev/null +++ b/hooks/codereadiness/src/test/java/dev/openfeature/contrib/hooks/codereadiness/SemVerComparatorTest.java @@ -0,0 +1,61 @@ +package dev.openfeature.contrib.hooks.codereadiness; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +class SemVerComparatorTest { + + private final SemVerComparator comparator = new SemVerComparator(); + + @ParameterizedTest + @CsvSource({ + "1.2.0, 1.1.0, true", + "1.1.0, 1.1.0, true", + "v1.2.0, 1.1.0, true", + "1.2.0, v1.1.0, true", + "v1.2.0, v1.1.0, true", + "2.0.0, 1.9.9, true", + "1.0.0, 1.1.0, false", + "v1.0.0, v1.1.0, false" + }) + @DisplayName("Should validate versions correctly according to SemVer rules") + void testVersionComparison(String currentVersion, String minCodeVersion, boolean expectedResult) throws Exception { + boolean result = comparator.compare(currentVersion, minCodeVersion); + assertThat(result).isEqualTo(expectedResult); + } + + @Test + @DisplayName("Should throw IllegalArgumentException when currentVersion is invalid semver") + void testInvalidCurrentVersion() { + assertThatThrownBy(() -> comparator.compare("invalid-version", "1.0.0")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("invalid current semver"); + } + + @Test + @DisplayName("Should throw IllegalArgumentException when minCodeVersion is invalid semver") + void testInvalidMinCodeVersion() { + assertThatThrownBy(() -> comparator.compare("1.0.0", "invalid-version")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("invalid min code version semver"); + } + + @Test + @DisplayName("Should throw IllegalArgumentException when currentVersion is null") + void testNullCurrentVersion() { + assertThatThrownBy(() -> comparator.compare(null, "1.0.0")) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + @DisplayName("Should throw IllegalArgumentException when minCodeVersion is null") + void testNullMinCodeVersion() { + assertThatThrownBy(() -> comparator.compare("1.0.0", null)) + .isInstanceOf(IllegalArgumentException.class); + } +} diff --git a/pom.xml b/pom.xml index 1667517cd..29963bb06 100644 --- a/pom.xml +++ b/pom.xml @@ -32,6 +32,7 @@ tools/flagd-api tools/flagd-core hooks/open-telemetry + hooks/codereadiness tools/junit-openfeature providers/flagd providers/flagsmith diff --git a/release-please-config.json b/release-please-config.json index 7cb16289d..d3281c2d5 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -161,6 +161,17 @@ "README.md" ] }, + "hooks/codereadiness": { + "package-name": "dev.openfeature.contrib.hooks.codereadiness", + "release-type": "simple", + "bump-minor-pre-major": true, + "bump-patch-for-minor-pre-major": true, + "versioning": "default", + "extra-files": [ + "pom.xml", + "README.md" + ] + }, "tools/junit-openfeature": { "package-name": "dev.openfeature.contrib.tools.junitopenfeature", "release-type": "simple",