From bf5d81cbb5d3956bb7b0205002ef5b1efbc1f9a1 Mon Sep 17 00:00:00 2001 From: lhazlewood <121180+lhazlewood@users.noreply.github.com> Date: Fri, 15 Sep 2023 09:42:44 -0700 Subject: [PATCH] Ensured various builder collection argument methods were semantically the same (append operations, not one append, one replace) (#830) --- README.md | 2 +- .../java/io/jsonwebtoken/ClaimsMutator.java | 20 +++++---- .../io/jsonwebtoken/JwtParserBuilder.java | 4 +- .../jsonwebtoken/ProtectedHeaderMutator.java | 26 ++++++++--- .../io/jsonwebtoken/lang/Collections.java | 13 +++++- .../io/jsonwebtoken/security/JwkBuilder.java | 12 +++--- .../jsonwebtoken/lang/CollectionsTest.groovy | 23 +++++++++- .../impl/DefaultJweHeaderMutator.java | 28 +++++++++++- .../impl/DelegatingClaimsMutator.java | 28 ++++++++---- .../impl/security/AbstractJwkBuilder.java | 12 ++---- .../impl/DefaultJwtBuilderTest.groovy | 42 ++++++++++++++++-- .../impl/DefaultJwtHeaderBuilderTest.groovy | 41 ++++++++++++++---- .../impl/security/JwksTest.groovy | 43 ++++++++++++++++++- 13 files changed, 235 insertions(+), 59 deletions(-) diff --git a/README.md b/README.md index 90f99afe..274a7d70 100644 --- a/README.md +++ b/README.md @@ -157,7 +157,7 @@ enforcement. * Convenient and readable [fluent](http://en.wikipedia.org/wiki/Fluent_interface) interfaces, great for IDE auto-completion to write code quickly * Fully RFC specification compliant on all implemented functionality, tested against RFC-specified test vectors - * Stable implementation with over 1,300+ tests and enforced 100% test code coverage. Every single method, statement + * Stable implementation with over 1,400+ tests and enforced 100% test code coverage. Every single method, statement and conditional branch variant in the entire codebase is tested and required to pass on every build. * Creating, parsing and verifying digitally signed compact JWTs (aka JWSs) with all standard JWS algorithms: diff --git a/api/src/main/java/io/jsonwebtoken/ClaimsMutator.java b/api/src/main/java/io/jsonwebtoken/ClaimsMutator.java index 1941348b..b73f4f0d 100644 --- a/api/src/main/java/io/jsonwebtoken/ClaimsMutator.java +++ b/api/src/main/java/io/jsonwebtoken/ClaimsMutator.java @@ -97,7 +97,7 @@ public interface ClaimsMutator> { * data type for recipients. * * @param aud the value to use as the {@code aud} Claim single-String value (and not an array of Strings), or - * {@code null} to remove the property from the JSON map. + * {@code null}, empty or whitespace to remove the property from the JSON map. * @return the instance for method chaining * @since JJWT_RELEASE_VERSION * @deprecated This is technically not deprecated because the JWT RFC mandates support for single string values, @@ -108,22 +108,26 @@ public interface ClaimsMutator> { T audienceSingle(String aud); /** - * Adds the specified {@code aud} value to the {@link #audience(Collection) audience} Claim set (JSON Array). This - * method may be called multiple times. + * Adds (appends) the specified {@code aud} value to the {@link #audience(Collection) audience} Claim set + * (JSON Array) unless it is {@code null}, empty, whitespace-only or already exists in the set. + * + *

This method may be called multiple times.

* * @param aud a JWT {@code aud} value to add to the {@link #audience(Collection) audience} Claim set. * @return the {@code Claims} instance for method chaining. * @throws IllegalArgumentException if the {@code aud} argument is null or empty. * @since JJWT_RELEASE_VERSION */ - T audience(String aud) throws IllegalArgumentException; + T audience(String aud); /** - * Sets the JWT aud (audience) - * Claim set, replacing any previous value(s). + * Adds (appends) the specified values to the JWT + * aud (audience) Claim + * set, quietly ignoring any null, empty, whitespace-only, or existing value already in the set. * - * @param aud the values to set as the {@code aud} Claim set (JSON Array), or {@code null}/empty to remove the - * {@code aud} claim from the JSON map entirely. + *

This method may be called multiple times.

+ * + * @param aud the values to add to the {@code aud} Claim set (JSON Array) * @return the instance for method chaining * @since JJWT_RELEASE_VERSION */ diff --git a/api/src/main/java/io/jsonwebtoken/JwtParserBuilder.java b/api/src/main/java/io/jsonwebtoken/JwtParserBuilder.java index f9c1c027..aec98d15 100644 --- a/api/src/main/java/io/jsonwebtoken/JwtParserBuilder.java +++ b/api/src/main/java/io/jsonwebtoken/JwtParserBuilder.java @@ -102,8 +102,8 @@ public interface JwtParserBuilder extends Builder { * *

Extension Behavior

* - *

The {@code crit} set only identifies header parameter names that are used in extensions supported by the - * application. Application developers, not JJWT, MUST perform the associated extension behavior + *

The {@code crit} set argument only identifies header parameter names that are used in extensions supported + * by the application. Application developers, not JJWT, MUST perform the associated extension behavior * using the parsed JWT.

* * @param crit the header parameter names used in JWT extensions supported by the application. diff --git a/api/src/main/java/io/jsonwebtoken/ProtectedHeaderMutator.java b/api/src/main/java/io/jsonwebtoken/ProtectedHeaderMutator.java index 5c329a34..4fc20eae 100644 --- a/api/src/main/java/io/jsonwebtoken/ProtectedHeaderMutator.java +++ b/api/src/main/java/io/jsonwebtoken/ProtectedHeaderMutator.java @@ -19,7 +19,7 @@ import io.jsonwebtoken.security.PublicJwk; import io.jsonwebtoken.security.X509Mutator; import java.net.URI; -import java.util.Set; +import java.util.Collection; /** * Mutation (modifications) to a {@link ProtectedHeader Header} instance. @@ -30,17 +30,29 @@ import java.util.Set; public interface ProtectedHeaderMutator> extends HeaderMutator, X509Mutator { /** - * Sets the header parameter names that use extensions to the JWT or JWA specification that MUST - * be understood and supported by the JWT recipient. A {@code null} value will remove the - * property from the JSON map. + * Adds the name of a header parameter used by a JWT or JWA specification extension that MUST be understood + * and supported by the JWT recipient. A {@code null}, empty, whitespace-only or already existing value is ignored. * - * @param crit the header parameter names that use extensions to the JWT or JWA specification that MUST - * be understood and supported by the JWT recipient. + * @param crit the name of a header parameter used by a JWT or JWA specification extension that MUST be + * understood and supported by the JWT recipient. * @return the header for method chaining. * @see JWS crit (Critical) Header Parameter * @see JWS crit (Critical) Header Parameter */ - T critical(Set crit); + T critical(String crit); + + /** + * Adds names of header parameters used by JWT or JWA specification extensions that MUST be + * understood and supported by the JWT recipient. {@code null}, empty, whitespace-only or already existing + * values are ignored. + * + * @param crit names of header parameters used by JWT or JWA specification extensions that MUST be + * understood and supported by the JWT recipient + * @return the header for method chaining. + * @see JWS crit (Critical) Header Parameter + * @see JWS crit (Critical) Header Parameter + */ + T critical(Collection crit); /** * Sets the {@code jwk} (JSON Web Key) associated with the JWT. When set for a {@link JwsHeader}, the diff --git a/api/src/main/java/io/jsonwebtoken/lang/Collections.java b/api/src/main/java/io/jsonwebtoken/lang/Collections.java index 85a1ba0a..4126c4e5 100644 --- a/api/src/main/java/io/jsonwebtoken/lang/Collections.java +++ b/api/src/main/java/io/jsonwebtoken/lang/Collections.java @@ -180,7 +180,6 @@ public final class Collections { /** * Returns a non-null set, either {@code s} if it is not null, or {@link #emptySet()} otherwise. - * Returns the specified set if not null or {@link #emptySet()} otherwise. * * @param s the set to check for null * @param type of elements in the set @@ -191,6 +190,18 @@ public final class Collections { return s == null ? Collections.emptySet() : s; } + /** + * Returns a non-null collection, either {@code c} if it is not null, or {@link #emptyList()} otherwise. + * + * @param c the collection to check for null + * @param type of elements in the collection + * @return a non-null collection, either {@code c} if it is not null, or {@link #emptyList()} otherwise. + * @since JJWT_RELEASE_VERSION + */ + public static Collection nullSafe(Collection c) { + return c == null ? Collections.emptyList() : c; + } + /** * Return true if the supplied Collection is null * or empty. Otherwise, return false. diff --git a/api/src/main/java/io/jsonwebtoken/security/JwkBuilder.java b/api/src/main/java/io/jsonwebtoken/security/JwkBuilder.java index 547e32bb..b55761bd 100644 --- a/api/src/main/java/io/jsonwebtoken/security/JwkBuilder.java +++ b/api/src/main/java/io/jsonwebtoken/security/JwkBuilder.java @@ -138,14 +138,14 @@ public interface JwkBuilder, T extends JwkBuilde * * @param operation the value to add to the JWK {@code key_ops} value set * @return the builder for method chaining. - * @throws IllegalArgumentException if {@code op} is {@code null} or if the operation is not permitted - * by the operations {@link #operationPolicy(KeyOperationPolicy) policy}. + * @throws IllegalArgumentException if the {@code op} is not permitted by the operations + * {@link #operationPolicy(KeyOperationPolicy) policy}. * @see Jwks.OP */ T operation(KeyOperation operation) throws IllegalArgumentException; /** - * Sets the JWK {@code key_ops} + * Adds {@code ops} to the JWK {@code key_ops} * (Key Operations) Parameter values. * *

The {@code key_ops} (key operations) parameter identifies the operation(s) for which the key is @@ -176,10 +176,10 @@ public interface JwkBuilder, T extends JwkBuilde *

For best interoperability with other applications however, it is recommended to use only the {@link Jwks.OP} * constants.

* - * @param ops the JWK {@code key_ops} value set, or {@code null} if not present. + * @param ops the {@code KeyOperation} values to add to the JWK {@code key_ops} value set * @return the builder for method chaining. - * @throws IllegalArgumentException {@code ops} is {@code null} or empty, or if any of the operations are not - * permitted by the operations {@link #operationPolicy(KeyOperationPolicy) policy}. + * @throws IllegalArgumentException if any of the operations are not permitted by the operations + * {@link #operationPolicy(KeyOperationPolicy) policy}. * @see Jwks.OP */ T operations(Collection ops) throws IllegalArgumentException; diff --git a/api/src/test/groovy/io/jsonwebtoken/lang/CollectionsTest.groovy b/api/src/test/groovy/io/jsonwebtoken/lang/CollectionsTest.groovy index 718ab454..a36a133b 100644 --- a/api/src/test/groovy/io/jsonwebtoken/lang/CollectionsTest.groovy +++ b/api/src/test/groovy/io/jsonwebtoken/lang/CollectionsTest.groovy @@ -52,7 +52,7 @@ class CollectionsTest { @Test void testNullSafeSetWithNullArgument() { - def set = Collections.nullSafe(null) + def set = Collections.nullSafe((Set)null) assertNotNull set assertTrue set.isEmpty() } @@ -70,4 +70,25 @@ class CollectionsTest { def b = Collections.nullSafe(a) assertSame a, b } + + @Test + void testNullSafeCollectionWithNullArgument() { + Collection c = Collections.nullSafe((Collection)null) + assertNotNull c + assertTrue c.isEmpty() + } + + @Test + void testNullSafeCollectionWithEmptyArgument() { + Collection a = new LinkedHashSet() as Collection + def b = Collections.nullSafe(a) + assertSame a, b + } + + @Test + void testNullSafeCollectionWithNonEmptyArgument() { + Collection a = ["hello"] as Collection + def b = Collections.nullSafe(a) + assertSame a, b + } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweHeaderMutator.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweHeaderMutator.java index f2bdc33f..bbbd4a75 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweHeaderMutator.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweHeaderMutator.java @@ -19,11 +19,14 @@ import io.jsonwebtoken.JweHeaderMutator; import io.jsonwebtoken.impl.lang.DelegatingMapMutator; import io.jsonwebtoken.impl.lang.Parameter; import io.jsonwebtoken.impl.security.X509BuilderSupport; +import io.jsonwebtoken.lang.Collections; import io.jsonwebtoken.lang.Strings; import io.jsonwebtoken.security.PublicJwk; import java.net.URI; import java.security.cert.X509Certificate; +import java.util.Collection; +import java.util.LinkedHashSet; import java.util.List; import java.util.Set; @@ -102,8 +105,29 @@ public class DefaultJweHeaderMutator> // ============================================================= @Override - public T critical(Set crit) { - return put(DefaultProtectedHeader.CRIT, crit); + public T critical(String crit) { + crit = Strings.clean(crit); + if (Strings.hasText(crit)) { + critical(Collections.setOf(crit)); + } + return self(); + } + + @Override + public T critical(Collection crit) { + if (!Collections.isEmpty(crit)) { + Set existing = Collections.nullSafe(this.DELEGATE.get(DefaultProtectedHeader.CRIT)); + Set set = new LinkedHashSet<>(existing.size() + crit.size()); + set.addAll(existing); + for (String s : crit) { + s = Strings.clean(s); + if (s != null) { + set.add(s); + } + } + put(DefaultProtectedHeader.CRIT, set); + } + return self(); } @Override diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DelegatingClaimsMutator.java b/impl/src/main/java/io/jsonwebtoken/impl/DelegatingClaimsMutator.java index 677b8dd2..ff8da9a0 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DelegatingClaimsMutator.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DelegatingClaimsMutator.java @@ -19,7 +19,6 @@ import io.jsonwebtoken.ClaimsMutator; import io.jsonwebtoken.impl.lang.DelegatingMapMutator; import io.jsonwebtoken.impl.lang.Parameter; import io.jsonwebtoken.impl.lang.Parameters; -import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.lang.Collections; import io.jsonwebtoken.lang.MapMutator; import io.jsonwebtoken.lang.Strings; @@ -88,8 +87,7 @@ public class DelegatingClaimsMutator & C put(DefaultClaims.AUDIENCE, Collections.setOf(existing)); // replace as Set } } - Set aud = get(DefaultClaims.AUDIENCE); - return aud != null ? aud : Collections.emptySet(); + return get(DefaultClaims.AUDIENCE); } @Override @@ -108,16 +106,28 @@ public class DelegatingClaimsMutator & C @Override public T audience(String aud) { - aud = Assert.hasText(Strings.clean(aud), "Audience string value cannot be null or empty."); - Set set = new LinkedHashSet<>(getAudience()); - set.add(aud); - return audience(set); + aud = Strings.clean(aud); + if (Strings.hasText(aud)) { + audience(java.util.Collections.singleton(aud)); + } + return self(); } @Override public T audience(Collection aud) { - getAudience(); //coerce to Set if necessary - return put(DefaultClaims.AUDIENCE, Collections.asSet(aud)); + if (!Collections.isEmpty(aud)) { + Set existing = Collections.nullSafe(getAudience()); + Set set = new LinkedHashSet<>(existing.size() + aud.size()); + set.addAll(existing); + for (String s : aud) { + s = Strings.clean(s); + if (s != null) { + set.add(s); + } + } + put(DefaultClaims.AUDIENCE, set); + } + return self(); } @Override diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractJwkBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractJwkBuilder.java index b9801502..51cd62ae 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractJwkBuilder.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractJwkBuilder.java @@ -109,18 +109,14 @@ abstract class AbstractJwkBuilder, T extends Jwk @Override public T operation(KeyOperation operation) throws IllegalArgumentException { - Assert.notNull(operation, "KeyOperation cannot be null."); - return operations(Collections.setOf(operation)); + return operation != null ? operations(Collections.setOf(operation)) : self(); } @Override public T operations(Collection ops) { - Assert.notEmpty(ops, "KeyOperations collection argument cannot be null or empty."); - Set set = new LinkedHashSet<>(ops); // new ones override existing ones - Set existing = this.DELEGATE.getOperations(); - if (!Collections.isEmpty(existing)) { - set.addAll(existing); - } + Set set = new LinkedHashSet<>(Collections.nullSafe(ops)); // new ones override existing ones + Set existing = Collections.nullSafe(this.DELEGATE.getOperations()); + set.addAll(existing); this.opsPolicy.validate(set); this.DELEGATE.setOperations(set); return self(); diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtBuilderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtBuilderTest.groovy index 40a1ad50..a768610a 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtBuilderTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtBuilderTest.groovy @@ -647,6 +647,18 @@ class DefaultJwtBuilderTest { assertEquals aud, Jwts.parser().enableUnsecured().build().parseClaimsJwt(jwt).payload.getAudience().iterator().next() } + @Test + void testAudienceNullString() { + def jwt = Jwts.builder().subject('me').audience(null).compact() + assertNull Jwts.parser().enableUnsecured().build().parseClaimsJwt(jwt).payload.getAudience() + } + + @Test + void testAudienceEmptyString() { + def jwt = Jwts.builder().subject('me').audience(' ').compact() + assertNull Jwts.parser().enableUnsecured().build().parseClaimsJwt(jwt).payload.getAudience() + } + @Test void testAudienceMultipleTimes() { def one = 'one' @@ -657,6 +669,28 @@ class DefaultJwtBuilderTest { assertTrue aud.contains(two) } + @Test + void testAudienceNullCollection() { + Collection c = null + def jwt = Jwts.builder().subject('me').audience(c).compact() + assertNull Jwts.parser().enableUnsecured().build().parseClaimsJwt(jwt).payload.getAudience() + } + + @Test + void testAudienceEmptyCollection() { + Collection c = new ArrayList() + def jwt = Jwts.builder().subject('me').audience(c).compact() + assertNull Jwts.parser().enableUnsecured().build().parseClaimsJwt(jwt).payload.getAudience() + } + + @Test + void testAudienceCollectionWithNullElement() { + Collection c = new ArrayList() + c.add(null) + def jwt = Jwts.builder().subject('me').audience(c).compact() + assertNull Jwts.parser().enableUnsecured().build().parseClaimsJwt(jwt).payload.getAudience() + } + /** * Asserts that if someone calls builder.audienceSingle and then audience(String), that the audience value * will automatically be coerced from a String to a Set and contain both elements. @@ -693,17 +727,17 @@ class DefaultJwtBuilderTest { /** * Asserts that if someone calls builder.audienceSingle and then audience(Collection), the builder coerces the - * aud to a Set and only the elements in the Collection will be applied since audience(Collection) is a - * full-replacement operation. + * aud to a Set and all elements will be applied since audience(Collection). */ @Test void testAudienceSingleThenAudienceCollection() { def single = 'one' def collection = ['two', 'three'] as Set + def expected = ['one', 'two', 'three'] as Set def jwt = Jwts.builder().audienceSingle(single).audience(collection).compact() def aud = Jwts.parser().enableUnsecured().build().parseClaimsJwt(jwt).payload.getAudience() - assertEquals collection.size(), aud.size() - assertTrue aud.containsAll(collection) + assertEquals expected.size(), aud.size() + assertTrue aud.contains(single) && aud.containsAll(collection) } /** diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtHeaderBuilderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtHeaderBuilderTest.groovy index d01e5ba2..2abc0000 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtHeaderBuilderTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtHeaderBuilderTest.groovy @@ -496,21 +496,46 @@ class DefaultJwtHeaderBuilderTest { } @Test - void testCritNullRemovesValues() { - def crit = ['test'] as Set - def header = jws().add('test', 'foo').critical(crit).build() as ProtectedHeader - assertEquals crit, header.getCritical() - header = builder.critical(null).build() - assertFalse header.containsKey('crit') // removed + void testCritSingle() { + def crit = 'test' + def header = jws().add(crit, 'foo').critical(crit).build() as ProtectedHeader + def expected = [crit] as Set + assertEquals expected, header.getCritical() } @Test - void testCritEmptyRemovesValues() { + void testCritSingleNullIgnored() { + def crit = 'test' + def expected = [crit] as Set + def header = jws().add(crit, 'foo').critical(crit).build() as ProtectedHeader + assertEquals expected, header.getCritical() + header = builder.critical((String) null).build() as ProtectedHeader // ignored + assertEquals expected, header.getCritical() // nothing changed + } + + @Test + void testCritNullCollectionIgnored() { + def crit = ['test'] as Set + def header = jws().add('test', 'foo').critical(crit).build() as ProtectedHeader + assertEquals crit, header.getCritical() + header = builder.critical((Collection) null).build() as ProtectedHeader + assertEquals crit, header.getCritical() // nothing changed + } + + @Test + void testCritCollectionWithNullElement() { + def crit = [null] as Set + def header = jws().add('test', 'foo').critical(crit).build() as ProtectedHeader + assertNull header.getCritical() + } + + @Test + void testCritEmptyIgnored() { def crit = ['test'] as Set def header = jws().add('test', 'foo').critical(crit).build() as ProtectedHeader assertEquals crit, header.getCritical() header = builder.critical([] as Set).build() - assertFalse header.containsKey('crit') // removed + assertEquals crit, header.getCritical() // ignored } /** diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwksTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwksTest.groovy index 780c9ab4..744ce4d2 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwksTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwksTest.groovy @@ -142,11 +142,50 @@ class JwksTest { testProperty('id', 'kid', srandom()) } + @Test + void testSingleOperation() { + def op = Jwks.OP.ENCRYPT + def expected = [op] as Set + def canonical = [op.getId()] as Set + def jwk = Jwks.builder().key(TestKeys.A128GCM).operation(op).build() + assertEquals expected, jwk.getOperations() + assertEquals canonical, jwk.get(AbstractJwk.KEY_OPS.id) + } + + @Test + void testSingleOperationNull() { + def jwk = Jwks.builder().key(TestKeys.A128GCM).operation(null).build() //ignored null + assertNull jwk.getOperations() //nothing added + assertFalse jwk.containsKey(AbstractJwk.KEY_OPS.id) + } + + @Test + void testSingleOperationAppends() { + def expected = [Jwks.OP.ENCRYPT, Jwks.OP.DECRYPT] as Set + def jwk = Jwks.builder().key(TestKeys.A128GCM) + .operation(Jwks.OP.ENCRYPT).operation(Jwks.OP.DECRYPT) + .build() + assertEquals expected, jwk.getOperations() + } + @Test void testOperations() { def val = [Jwks.OP.SIGN, Jwks.OP.VERIFY] as Set - def canonical = Collections.setOf('sign', 'verify') - testProperty('operations', 'key_ops', val, canonical) + def jwk = Jwks.builder().key(TestKeys.A128GCM).operations(val).build() + assertEquals val, jwk.getOperations() + } + + @Test + void testOperationsNull() { + def jwk = Jwks.builder().key(TestKeys.A128GCM).operations(null).build() + assertNull jwk.getOperations() + assertFalse jwk.containsKey(AbstractJwk.KEY_OPS.id) + } + + @Test + void testOperationsEmpty() { + def jwk = Jwks.builder().key(TestKeys.A128GCM).operations(Collections.emptyList()).build() + assertNull jwk.getOperations() } @Test