Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 15 additions & 42 deletions infisical_sdk/resources/secrets.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,14 +86,11 @@ def get_secret_by_name(
if project_slug is None and project_id is None:
raise ValueError("project_slug or project_id must be provided")

cache_params = {
"project_id": project_id,
"environment_slug": environment_slug,
"secret_path": secret_path,
"secret_name": secret_name,
}

cache_key = self.cache.compute_cache_key(CACHE_KEY_SINGLE_SECRET, **cache_params)
# Key on every request parameter, like list_secrets: secrets of other
# projects (by slug), other versions or masked values must not be shared.
cache_key = self.cache.compute_cache_key(
CACHE_KEY_SINGLE_SECRET, secret_name=secret_name, **params
)

if self.cache.enabled:
cached_response = self.cache.get(cache_key)
Expand Down Expand Up @@ -154,17 +151,9 @@ def create_secret_by_name(


if self.cache.enabled:
cache_params = {
"project_id": project_id,
"environment_slug": environment_slug,
"secret_path": secret_path,
"secret_name": secret_name,
}

cache_key = self.cache.compute_cache_key(CACHE_KEY_SINGLE_SECRET, **cache_params)
self.cache.set(cache_key, result.data.secret)

# Invalidates all list secret cache
# The secret may be cached under any project reference (id or slug) and
# read options, so drop every cached single secret and list
self.cache.invalidate_operation(CACHE_KEY_SINGLE_SECRET)
self.cache.invalidate_operation(CACHE_KEY_LIST_SECRETS)

return result.data.secret
Expand Down Expand Up @@ -212,18 +201,10 @@ def update_secret_by_name(
)

if self.cache.enabled:
cache_params = {
"project_id": project_id,
"environment_slug": environment_slug,
"secret_path": secret_path,
"secret_name": current_secret_name,
}

cache_key = self.cache.compute_cache_key(CACHE_KEY_SINGLE_SECRET, **cache_params)
self.cache.unset(cache_key)

# Invalidates all list secret cache
self.cache.invalidate_operation(CACHE_KEY_LIST_SECRETS)
# The secret may be cached under any project reference (id or slug) and
# read options, so drop every cached single secret and list
self.cache.invalidate_operation(CACHE_KEY_SINGLE_SECRET)
self.cache.invalidate_operation(CACHE_KEY_LIST_SECRETS)

return result.data.secret

Expand Down Expand Up @@ -253,17 +234,9 @@ def delete_secret_by_name(
)

if self.cache.enabled:
cache_params = {
"project_id": project_id,
"environment_slug": environment_slug,
"secret_path": secret_path,
"secret_name": secret_name,
}

cache_key = self.cache.compute_cache_key(CACHE_KEY_SINGLE_SECRET, **cache_params)
self.cache.unset(cache_key)

# Invalidates all list secret cache
# The secret may be cached under any project reference (id or slug) and
# read options, so drop every cached single secret and list
self.cache.invalidate_operation(CACHE_KEY_SINGLE_SECRET)
self.cache.invalidate_operation(CACHE_KEY_LIST_SECRETS)

return result.data.secret
87 changes: 87 additions & 0 deletions tests/test_secrets_cache.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
from types import SimpleNamespace
from unittest.mock import MagicMock

import pytest

from infisical_sdk.api_types import BaseSecret
from infisical_sdk.resources.secrets import V3RawSecrets
from infisical_sdk.util import SecretsCache


def make_secret(value):
return BaseSecret.from_dict({
"id": "id",
"_id": "id",
"workspace": "project",
"environment": "dev",
"version": 1,
"type": "shared",
"secretKey": "API_KEY",
"secretValue": value,
"secretComment": "",
"createdAt": "2026-01-01T00:00:00Z",
"updatedAt": "2026-01-01T00:00:00Z",
})


@pytest.fixture
def cache():
cache = SecretsCache(ttl_seconds=60)
yield cache
cache.close()


def make_client(cache, values):
requests = MagicMock()
requests.get.side_effect = [
SimpleNamespace(data=SimpleNamespace(secret=make_secret(value))) for value in values
]
return V3RawSecrets(requests, cache), requests


@pytest.mark.parametrize(
("first", "second"),
[
({"project_slug": "project-a"}, {"project_slug": "project-b"}),
({"project_id": "p1", "version": "1"}, {"project_id": "p1"}),
({"project_id": "p1", "view_secret_value": False}, {"project_id": "p1"}),
({"project_id": "p1", "expand_secret_references": False}, {"project_id": "p1"}),
({"project_id": "p1", "include_imports": False}, {"project_id": "p1"}),
],
)
def test_get_secret_by_name_does_not_share_cache_across_requests(cache, first, second):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 CI skips the new tests

The pull-request workflow still has its pytest step commented out, so these new tests never run in CI. Enable that step now that tests exist. Otherwise, later changes can break secret caching while CI stays green.

secrets, requests = make_client(cache, ["first", "second"])
common = {"secret_name": "API_KEY", "environment_slug": "dev", "secret_path": "/"}

assert secrets.get_secret_by_name(**common, **first).secretValue == "first"
assert secrets.get_secret_by_name(**common, **second).secretValue == "second"
assert requests.get.call_count == 2


def test_get_secret_by_name_uses_cache_for_same_request(cache):
secrets, requests = make_client(cache, ["first"])
args = {
"secret_name": "API_KEY",
"environment_slug": "dev",
"secret_path": "/",
"project_slug": "a",
}

assert secrets.get_secret_by_name(**args).secretValue == "first"
assert secrets.get_secret_by_name(**args).secretValue == "first"
assert requests.get.call_count == 1


@pytest.mark.parametrize("mutation", ["update_secret_by_name", "delete_secret_by_name"])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Create clearing has no test

These tests cover only update_secret_by_name and delete_secret_by_name, although create_secret_by_name also changes from filling the cache to clearing it. Add a case that caches one secret, creates another, and checks that rereading the first hits the API. Removing the new create invalidation call would currently leave every test green.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

def test_mutation_by_slug_invalidates_secret_cached_by_id(cache, mutation):
secrets, requests = make_client(cache, ["old", "new"])
requests.patch.return_value = SimpleNamespace(data=SimpleNamespace(secret=make_secret("new")))
requests.delete.return_value = SimpleNamespace(data=SimpleNamespace(secret=make_secret("old")))
location = {"environment_slug": "dev", "secret_path": "/"}

assert secrets.get_secret_by_name("API_KEY", project_id="p1", **location).secretValue == "old"
name_arg = "current_secret_name" if mutation == "update_secret_by_name" else "secret_name"
getattr(secrets, mutation)(**{name_arg: "API_KEY"}, project_slug="project-a", **location)

assert secrets.get_secret_by_name("API_KEY", project_id="p1", **location).secretValue == "new"
assert requests.get.call_count == 2