This is an automated email from the ASF dual-hosted git repository.

roryqi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gravitino.git


The following commit(s) were added to refs/heads/main by this push:
     new 1773ede613 [#12218] feat(secret): Add GravitinoSecretProvider SPI and 
in-memory provider (#12215)
1773ede613 is described below

commit 1773ede613d0115e955274737f1678749da0b05f
Author: MaSai <[email protected]>
AuthorDate: Mon Aug 3 19:52:56 2026 +0800

    [#12218] feat(secret): Add GravitinoSecretProvider SPI and in-memory 
provider (#12215)
    
    ### What changes were proposed in this pull request?
    
    Add the secret-provider SPI (`GravitinoSecretProvider`) with:
    - `initialize` / `type` / `writeSecret` / `readSecret` / `deleteSecret`
    / `close`
    - a process-local `InMemorySecretsProvider` for development and unit
    tests
    - minimal supporting types used by the SPI (`SecretWriteContext`,
    `SecretUrn`,
      `SecretConstants`)
    
    This PR intentionally does **not** wire create/alter/drop flows, REST
    APIs,
    provider registry configuration, or external secret references.
    
    ### Why are the changes needed?
    
    To land the OSS foundation for entity connection-secret backends. Later
    PRs can
    add provider registry, create-time write-through, resolve/omit-on-read,
    and REST.
    
    Fix: #12218
    
    ### Does this PR introduce _any_ user-facing change?
    
    - No user-facing REST/config change in this PR.
    - Adds internal SPI types under `org.apache.gravitino.secret`.
    
    ### How was this patch tested?
    
    - `./gradlew :core:test --tests
    'org.apache.gravitino.secret.TestInMemorySecretsProvider' --tests
    'org.apache.gravitino.secret.TestSecretUrn' -PskipITs`
    
    ---------
    
    Co-authored-by: Cursor <[email protected]>
---
 .../apache/gravitino/secret/SecretConstants.java   |  41 +++++
 .../org/apache/gravitino/secret/SecretUrn.java     | 174 +++++++++++++++++++++
 .../org/apache/gravitino/secret/TestSecretUrn.java |  77 +++++++++
 .../apache/gravitino/secret/SecretProvider.java    |  84 ++++++++++
 .../secret/memory/InMemorySecretsProvider.java     |  98 ++++++++++++
 .../secret/TestInMemorySecretsProvider.java        |  91 +++++++++++
 6 files changed, 565 insertions(+)

diff --git a/api/src/main/java/org/apache/gravitino/secret/SecretConstants.java 
b/api/src/main/java/org/apache/gravitino/secret/SecretConstants.java
new file mode 100644
index 0000000000..00a252c876
--- /dev/null
+++ b/api/src/main/java/org/apache/gravitino/secret/SecretConstants.java
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.gravitino.secret;
+
+import org.apache.gravitino.annotation.DeveloperApi;
+
+/** Constants for Gravitino entity secret management. */
+@DeveloperApi
+public final class SecretConstants {
+
+  /** Prefix for Gravitino secret URNs. */
+  public static final String URN_PREFIX = "urn:gravitino-secret:";
+
+  /** Write attribute: entity type ({@code catalog}, {@code schema}, or {@code 
fileset}). */
+  public static final String ATTR_ENTITY_TYPE = "entityType";
+
+  /** Write attribute: stable numeric entity id. */
+  public static final String ATTR_ENTITY_ID = "entityId";
+
+  /** Write attribute: entity property key that holds the secret. */
+  public static final String ATTR_PROPERTY_KEY = "propertyKey";
+
+  private SecretConstants() {}
+}
diff --git a/api/src/main/java/org/apache/gravitino/secret/SecretUrn.java 
b/api/src/main/java/org/apache/gravitino/secret/SecretUrn.java
new file mode 100644
index 0000000000..d9bf7dc6c1
--- /dev/null
+++ b/api/src/main/java/org/apache/gravitino/secret/SecretUrn.java
@@ -0,0 +1,174 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.gravitino.secret;
+
+import static org.apache.gravitino.secret.SecretConstants.ATTR_ENTITY_ID;
+import static org.apache.gravitino.secret.SecretConstants.ATTR_ENTITY_TYPE;
+import static org.apache.gravitino.secret.SecretConstants.ATTR_PROPERTY_KEY;
+import static org.apache.gravitino.secret.SecretConstants.URN_PREFIX;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.regex.Pattern;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.gravitino.annotation.DeveloperApi;
+
+/**
+ * Value object for a Gravitino secret URN of the form {@code
+ * urn:gravitino-secret:<providerName>:<identifier-segments...>}.
+ */
+@DeveloperApi
+public final class SecretUrn {
+
+  // Allow '.' so dotted property keys (e.g. authentication.password) can 
appear in URNs.
+  private static final Pattern SEGMENT_PATTERN = 
Pattern.compile("[a-zA-Z0-9._-]+");
+
+  private final String providerName;
+  private final List<String> identifierSegments;
+
+  private SecretUrn(String providerName, List<String> identifierSegments) {
+    this.providerName = providerName;
+    this.identifierSegments = identifierSegments;
+  }
+
+  /**
+   * Returns the provider name from the URN.
+   *
+   * @return the provider name
+   */
+  public String providerName() {
+    return providerName;
+  }
+
+  /**
+   * Returns the type-specific identifier split into colon-separated segments.
+   *
+   * @return the identifier segments
+   */
+  public List<String> identifierSegments() {
+    return identifierSegments;
+  }
+
+  /**
+   * Builds a write-through secret URN for an entity property secret.
+   *
+   * <p>Required attributes: {@link SecretConstants#ATTR_ENTITY_TYPE}, {@link
+   * SecretConstants#ATTR_ENTITY_ID}, and {@link 
SecretConstants#ATTR_PROPERTY_KEY}.
+   *
+   * @param providerName the configured provider name
+   * @param attributes write-through attributes
+   * @return the write-through secret URN
+   */
+  public static SecretUrn buildWriteThrough(String providerName, Map<String, 
String> attributes) {
+    if (attributes == null) {
+      throw new IllegalArgumentException("attributes must not be null");
+    }
+    String entityType = requiredAttribute(attributes, ATTR_ENTITY_TYPE);
+    String entityId = requiredAttribute(attributes, ATTR_ENTITY_ID);
+    String propertyKey = requiredAttribute(attributes, ATTR_PROPERTY_KEY);
+    try {
+      Long.parseLong(entityId);
+    } catch (NumberFormatException e) {
+      throw new IllegalArgumentException(
+          "attributes." + ATTR_ENTITY_ID + " must be a numeric entity id: " + 
entityId, e);
+    }
+    validateSegment(providerName);
+    validateSegment(entityType);
+    validateSegment(entityId);
+    validateSegment(propertyKey);
+    return new SecretUrn(
+        providerName,
+        Collections.unmodifiableList(Arrays.asList(entityType, entityId, 
propertyKey)));
+  }
+
+  /**
+   * Parses a Gravitino secret URN string.
+   *
+   * @param urn the secret URN string
+   * @return the parsed secret URN
+   */
+  public static SecretUrn parse(String urn) {
+    if (!StringUtils.startsWith(urn, URN_PREFIX)) {
+      throw new IllegalArgumentException("Invalid Gravitino secret URN: " + 
urn);
+    }
+
+    String remainder = urn.substring(URN_PREFIX.length());
+    if (StringUtils.isEmpty(remainder)) {
+      throw new IllegalArgumentException("Invalid Gravitino secret URN: " + 
urn);
+    }
+
+    String[] segments = remainder.split(":", -1);
+    if (segments.length < 2) {
+      throw new IllegalArgumentException("Invalid Gravitino secret URN: " + 
urn);
+    }
+
+    for (String segment : segments) {
+      validateSegment(segment);
+    }
+
+    String[] identifierParts = Arrays.copyOfRange(segments, 1, 
segments.length);
+    return new SecretUrn(segments[0], 
Collections.unmodifiableList(Arrays.asList(identifierParts)));
+  }
+
+  /**
+   * Validates that a URN segment contains only allowed characters.
+   *
+   * @param segment the segment to validate
+   */
+  public static void validateSegment(String segment) {
+    if (StringUtils.isEmpty(segment) || 
!SEGMENT_PATTERN.matcher(segment).matches()) {
+      throw new IllegalArgumentException("Invalid URN segment: " + segment);
+    }
+  }
+
+  private static String requiredAttribute(Map<String, String> attributes, 
String key) {
+    String value = attributes.get(key);
+    if (StringUtils.isBlank(value)) {
+      throw new IllegalArgumentException("attributes." + key + " must not be 
blank");
+    }
+    return value;
+  }
+
+  @Override
+  public String toString() {
+    return URN_PREFIX + providerName + ":" + String.join(":", 
identifierSegments);
+  }
+
+  @Override
+  public boolean equals(Object other) {
+    if (this == other) {
+      return true;
+    }
+    if (!(other instanceof SecretUrn)) {
+      return false;
+    }
+    SecretUrn that = (SecretUrn) other;
+    return Objects.equals(providerName, that.providerName)
+        && Objects.equals(identifierSegments, that.identifierSegments);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(providerName, identifierSegments);
+  }
+}
diff --git a/api/src/test/java/org/apache/gravitino/secret/TestSecretUrn.java 
b/api/src/test/java/org/apache/gravitino/secret/TestSecretUrn.java
new file mode 100644
index 0000000000..be0c42f3e6
--- /dev/null
+++ b/api/src/test/java/org/apache/gravitino/secret/TestSecretUrn.java
@@ -0,0 +1,77 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.gravitino.secret;
+
+import static org.apache.gravitino.secret.SecretConstants.ATTR_ENTITY_ID;
+import static org.apache.gravitino.secret.SecretConstants.ATTR_ENTITY_TYPE;
+import static org.apache.gravitino.secret.SecretConstants.ATTR_PROPERTY_KEY;
+
+import java.util.List;
+import java.util.Map;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class TestSecretUrn {
+
+  private static Map<String, String> attributes(String entityType, String 
entityId, String key) {
+    return Map.of(
+        ATTR_ENTITY_TYPE, entityType,
+        ATTR_ENTITY_ID, entityId,
+        ATTR_PROPERTY_KEY, key);
+  }
+
+  @Test
+  public void testBuildAndParseWriteThroughUrn() {
+    SecretUrn urn =
+        SecretUrn.buildWriteThrough("memory", attributes("catalog", "42", 
"jdbc-password"));
+    
Assertions.assertEquals("urn:gravitino-secret:memory:catalog:42:jdbc-password", 
urn.toString());
+    Assertions.assertEquals("memory", urn.providerName());
+    Assertions.assertEquals(List.of("catalog", "42", "jdbc-password"), 
urn.identifierSegments());
+
+    SecretUrn parsed = SecretUrn.parse(urn.toString());
+    Assertions.assertEquals(urn, parsed);
+  }
+
+  @Test
+  public void testDottedPropertyKeyInUrn() {
+    SecretUrn urn =
+        SecretUrn.buildWriteThrough("local", attributes("catalog", "1", 
"authentication.password"));
+    Assertions.assertEquals(
+        "urn:gravitino-secret:local:catalog:1:authentication.password", 
urn.toString());
+    Assertions.assertEquals("local", urn.providerName());
+    Assertions.assertEquals(
+        List.of("catalog", "1", "authentication.password"), 
urn.identifierSegments());
+  }
+
+  @Test
+  public void testInvalidUrn() {
+    Assertions.assertThrows(IllegalArgumentException.class, () -> 
SecretUrn.parse("not-a-urn"));
+    Assertions.assertThrows(
+        IllegalArgumentException.class,
+        () -> SecretUrn.buildWriteThrough("memory", attributes("catalog", "1", 
"bad key")));
+    Assertions.assertThrows(
+        IllegalArgumentException.class,
+        () -> SecretUrn.buildWriteThrough("memory", attributes("catalog", 
"abc", "password")));
+    Assertions.assertThrows(
+        IllegalArgumentException.class, () -> 
SecretUrn.buildWriteThrough("memory", null));
+    Assertions.assertThrows(
+        IllegalArgumentException.class, () -> 
SecretUrn.validateSegment("bad/key"));
+  }
+}
diff --git 
a/common/src/main/java/org/apache/gravitino/secret/SecretProvider.java 
b/common/src/main/java/org/apache/gravitino/secret/SecretProvider.java
new file mode 100644
index 0000000000..2db4d8421f
--- /dev/null
+++ b/common/src/main/java/org/apache/gravitino/secret/SecretProvider.java
@@ -0,0 +1,84 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.gravitino.secret;
+
+import java.util.Map;
+import org.apache.gravitino.annotation.DeveloperApi;
+
+/** Service provider interface for secret backends. */
+@DeveloperApi
+public interface SecretProvider {
+
+  /**
+   * Initializes this provider after construction.
+   *
+   * <p>Implementations must override this method and explicitly decide 
whether configuration is
+   * required. An empty body is acceptable for providers that need no setup.
+   *
+   * @param name the configured provider instance name
+   * @param config provider-specific configuration (without the {@code 
gravitino.secret.provider.
+   *     <name>.} prefix)
+   */
+  void initialize(String name, Map<String, String> config);
+
+  /**
+   * Returns the provider type identifier.
+   *
+   * @return the provider type
+   */
+  String type();
+
+  /**
+   * Writes a plaintext secret and returns its URN.
+   *
+   * <p>Provider-specific write metadata is supplied as {@code attributes}. 
Required keys depend on
+   * the provider implementation; for example the in-memory write-through 
provider expects {@link
+   * SecretConstants#ATTR_ENTITY_TYPE}, {@link 
SecretConstants#ATTR_ENTITY_ID}, and {@link
+   * SecretConstants#ATTR_PROPERTY_KEY}.
+   *
+   * @param plaintext the secret plaintext
+   * @param attributes provider-specific write attributes
+   * @return the secret URN
+   */
+  SecretUrn writeSecret(String plaintext, Map<String, String> attributes);
+
+  /**
+   * Reads a secret by URN.
+   *
+   * @param urn the secret URN
+   * @return the secret plaintext
+   */
+  String readSecret(SecretUrn urn);
+
+  /**
+   * Deletes a secret by URN.
+   *
+   * @param urn the secret URN
+   */
+  void deleteSecret(SecretUrn urn);
+
+  /**
+   * Releases resources owned by this provider.
+   *
+   * <p>Implementations must override this method and explicitly release any 
held resources. An
+   * empty body is acceptable when there is nothing to clean up.
+   */
+  void close();
+}
diff --git 
a/common/src/main/java/org/apache/gravitino/secret/memory/InMemorySecretsProvider.java
 
b/common/src/main/java/org/apache/gravitino/secret/memory/InMemorySecretsProvider.java
new file mode 100644
index 0000000000..3df5166980
--- /dev/null
+++ 
b/common/src/main/java/org/apache/gravitino/secret/memory/InMemorySecretsProvider.java
@@ -0,0 +1,98 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.gravitino.secret.memory;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.gravitino.secret.SecretProvider;
+import org.apache.gravitino.secret.SecretUrn;
+
+/**
+ * In-memory secret provider for development and unit tests only.
+ *
+ * <p>Secrets are stored in process memory only and are lost on restart. 
Values are Base64-encoded
+ * for opaque storage, which is <strong>not</strong> encryption. {@link 
#close()} clears map
+ * references but does not securely zero heap contents ({@link String} is not 
wipeable); that is
+ * acceptable only because this backend is test-oriented. Production providers 
should store wipeable
+ * {@code byte[]}/{@code char[]} and explicitly zero them on delete/close. Do 
not use this provider
+ * in production.
+ */
+public class InMemorySecretsProvider implements SecretProvider {
+
+  private final ConcurrentHashMap<String, String> secrets = new 
ConcurrentHashMap<>();
+  private String providerName;
+
+  @Override
+  public void initialize(String name, Map<String, String> config) {
+    if (StringUtils.isBlank(name)) {
+      throw new IllegalArgumentException("provider name must not be blank");
+    }
+    this.providerName = name;
+  }
+
+  @Override
+  public String type() {
+    return "memory";
+  }
+
+  @Override
+  public SecretUrn writeSecret(String plaintext, Map<String, String> 
attributes) {
+    if (plaintext == null) {
+      throw new IllegalArgumentException("plaintext must not be null");
+    }
+    if (providerName == null) {
+      throw new IllegalStateException("InMemorySecretsProvider is not 
initialized");
+    }
+
+    SecretUrn urn = SecretUrn.buildWriteThrough(providerName, attributes);
+    secrets.put(
+        urn.toString(),
+        
Base64.getEncoder().encodeToString(plaintext.getBytes(StandardCharsets.UTF_8)));
+    return urn;
+  }
+
+  @Override
+  public String readSecret(SecretUrn urn) {
+    if (urn == null) {
+      throw new IllegalArgumentException("urn must not be null");
+    }
+    String encoded = secrets.get(urn.toString());
+    if (encoded == null) {
+      throw new IllegalArgumentException("Secret not found for URN: " + urn);
+    }
+    return new String(Base64.getDecoder().decode(encoded), 
StandardCharsets.UTF_8);
+  }
+
+  @Override
+  public void deleteSecret(SecretUrn urn) {
+    if (urn == null) {
+      throw new IllegalArgumentException("urn must not be null");
+    }
+    secrets.remove(urn.toString());
+  }
+
+  @Override
+  public void close() {
+    secrets.clear();
+  }
+}
diff --git 
a/common/src/test/java/org/apache/gravitino/secret/TestInMemorySecretsProvider.java
 
b/common/src/test/java/org/apache/gravitino/secret/TestInMemorySecretsProvider.java
new file mode 100644
index 0000000000..33d9072db4
--- /dev/null
+++ 
b/common/src/test/java/org/apache/gravitino/secret/TestInMemorySecretsProvider.java
@@ -0,0 +1,91 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.gravitino.secret;
+
+import static org.apache.gravitino.secret.SecretConstants.ATTR_ENTITY_ID;
+import static org.apache.gravitino.secret.SecretConstants.ATTR_ENTITY_TYPE;
+import static org.apache.gravitino.secret.SecretConstants.ATTR_PROPERTY_KEY;
+
+import java.util.Map;
+import org.apache.gravitino.secret.memory.InMemorySecretsProvider;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class TestInMemorySecretsProvider {
+
+  private static Map<String, String> writeAttributes() {
+    return Map.of(
+        ATTR_ENTITY_TYPE, "catalog",
+        ATTR_ENTITY_ID, "10",
+        ATTR_PROPERTY_KEY, "password");
+  }
+
+  @Test
+  public void testWriteReadDelete() {
+    InMemorySecretsProvider provider = new InMemorySecretsProvider();
+    Assertions.assertEquals("memory", provider.type());
+
+    provider.initialize("memory", Map.of());
+    SecretUrn urn = provider.writeSecret("s3cr3t", writeAttributes());
+    Assertions.assertEquals("urn:gravitino-secret:memory:catalog:10:password", 
urn.toString());
+    Assertions.assertEquals("s3cr3t", provider.readSecret(urn));
+
+    provider.deleteSecret(urn);
+    Assertions.assertThrows(IllegalArgumentException.class, () -> 
provider.readSecret(urn));
+    provider.close();
+  }
+
+  @Test
+  public void testWriteSecretRejectsNullArguments() {
+    InMemorySecretsProvider provider = new InMemorySecretsProvider();
+    provider.initialize("memory", Map.of());
+    Assertions.assertThrows(
+        IllegalArgumentException.class, () -> provider.writeSecret(null, 
writeAttributes()));
+    Assertions.assertThrows(
+        IllegalArgumentException.class, () -> provider.writeSecret("s3cr3t", 
null));
+  }
+
+  @Test
+  public void testWriteSecretRejectsMissingAttributes() {
+    InMemorySecretsProvider provider = new InMemorySecretsProvider();
+    provider.initialize("memory", Map.of());
+    Assertions.assertThrows(
+        IllegalArgumentException.class, () -> provider.writeSecret("s3cr3t", 
Map.of()));
+  }
+
+  @Test
+  public void testReadDeleteRejectNullUrn() {
+    InMemorySecretsProvider provider = new InMemorySecretsProvider();
+    provider.initialize("memory", Map.of());
+    Assertions.assertThrows(IllegalArgumentException.class, () -> 
provider.readSecret(null));
+    Assertions.assertThrows(IllegalArgumentException.class, () -> 
provider.deleteSecret(null));
+  }
+
+  @Test
+  public void testCloseClearsStoredSecrets() {
+    InMemorySecretsProvider provider = new InMemorySecretsProvider();
+    provider.initialize("memory", Map.of());
+    SecretUrn urn = provider.writeSecret("s3cr3t", writeAttributes());
+    Assertions.assertEquals("s3cr3t", provider.readSecret(urn));
+
+    provider.close();
+    Assertions.assertThrows(IllegalArgumentException.class, () -> 
provider.readSecret(urn));
+  }
+}

Reply via email to