diff --git a/backend/tests/apps/api/internal/mutations/api_key_test.py b/backend/tests/apps/api/internal/mutations/api_key_test.py index 0d28b8e3d8..2e4715dcc1 100644 --- a/backend/tests/apps/api/internal/mutations/api_key_test.py +++ b/backend/tests/apps/api/internal/mutations/api_key_test.py @@ -1,4 +1,4 @@ -from datetime import timedelta +from datetime import datetime, timedelta from unittest.mock import MagicMock, patch from uuid import uuid4 @@ -56,6 +56,35 @@ def test_create_api_key_success(self, mock_api_key_create, api_key_mutations): assert result.api_key == mock_instance assert result.raw_key == raw_key + @patch("apps.api.internal.mutations.api_key.ApiKey.create") + @patch("apps.api.internal.mutations.api_key.timezone.now") + def test_create_api_key_end_of_day_is_valid( + self, mock_now, mock_api_key_create, api_key_mutations + ): + """Ensure an end-of-day expiry on the current date is accepted.""" + info = mock_info() + user = info.context.request.user + fixed_now = datetime(2026, 1, 1, 12, 0, tzinfo=timezone.utc) + + mock_now.return_value = fixed_now + + expires_at = datetime(2026, 1, 1, 23, 59, 59, 999000, tzinfo=timezone.utc) + + mock_instance = MagicMock(spec=ApiKey) + mock_api_key_create.return_value = (mock_instance, "raw_key") + + result = api_key_mutations.create_api_key(info, name="Valid Name", expires_at=expires_at) + + mock_api_key_create.assert_called_once_with( + user=user, name="Valid Name", expires_at=expires_at + ) + + assert isinstance(result, CreateApiKeyResult) + assert result.ok + assert result.code == "SUCCESS" + assert result.api_key == mock_instance + assert result.raw_key == "raw_key" + @patch("apps.api.internal.mutations.api_key.ApiKey.create", return_value=None) def test_create_api_key_limit_reached(self, mock_api_key_create, api_key_mutations): """Test creating an API key when the user has reached their active key limit.""" diff --git a/frontend/__tests__/unit/pages/ApiKeysPage.test.tsx b/frontend/__tests__/unit/pages/ApiKeysPage.test.tsx index 49be7ba55d..d487bdd035 100644 --- a/frontend/__tests__/unit/pages/ApiKeysPage.test.tsx +++ b/frontend/__tests__/unit/pages/ApiKeysPage.test.tsx @@ -109,6 +109,16 @@ describe('ApiKeysPage Component', () => { } } + const toLocalEndOfDayIso = (date: string) => { + const [yearStr, monthStr, dayStr] = date.split('-') + const year = Number(yearStr) + const month = Number(monthStr) + const day = Number(dayStr) + + const endOfDayLocal = new Date(year, month - 1, day, 23, 59, 59, 999) + return endOfDayLocal.toISOString() + } + beforeEach(() => setupMocks()) afterEach(() => jest.clearAllMocks()) @@ -154,7 +164,7 @@ describe('ApiKeysPage Component', () => { fireEvent.click(screen.getByRole('button', { name: /create api key/i })) const expectedDate = format(addDays(new Date(), 30), 'yyyy-MM-dd') - const expectedIso = new Date(`${expectedDate}T00:00:00.000Z`).toISOString() + const expectedIso = toLocalEndOfDayIso(expectedDate) await waitFor(() => { expect(mockCreateMutation).toHaveBeenCalledWith({ @@ -176,7 +186,27 @@ describe('ApiKeysPage Component', () => { expect(mockCreateMutation).toHaveBeenCalledWith({ variables: { name: 'Custom Expiry Key', - expiresAt: new Date('2025-12-31T00:00:00.000Z').toISOString(), + expiresAt: toLocalEndOfDayIso('2025-12-31'), + }, + }) + }) + }) + + test('creates API key for today using end-of-day UTC', async () => { + render() + await openCreateModal() + + const today = format(new Date(), 'yyyy-MM-dd') + fillKeyForm('Today Key', today) + fireEvent.click(screen.getByRole('button', { name: /create api key/i })) + + const expectedIso = toLocalEndOfDayIso(today) + + await waitFor(() => { + expect(mockCreateMutation).toHaveBeenCalledWith({ + variables: { + name: 'Today Key', + expiresAt: expectedIso, }, }) }) @@ -415,7 +445,7 @@ describe('ApiKeysPage Component', () => { name: 'third key', isRevoked: false, createdAt: '2025-07-10T08:17:45.406011+00:00', - expiresAt: '2025-12-31T00:00:00+00:00', + expiresAt: '2025-12-31T23:59:59.999Z', }, ], activeApiKeyCount: 3, diff --git a/frontend/src/app/settings/api-keys/page.tsx b/frontend/src/app/settings/api-keys/page.tsx index 94006ba6d8..1d7c59d6e7 100644 --- a/frontend/src/app/settings/api-keys/page.tsx +++ b/frontend/src/app/settings/api-keys/page.tsx @@ -20,6 +20,26 @@ import { ApiKeysSkeleton } from 'components/skeletons/ApiKeySkeleton' const MAX_ACTIVE_KEYS = 3 +// Use local end-of-day so selecting "today" remains valid after UTC serialization. +const toEndOfDayUtcIso = (date: string): string => { + const [yearStr, monthStr, dayStr] = date.split('-') + const year = Number(yearStr) + const month = Number(monthStr) + const day = Number(dayStr) + + if (!Number.isFinite(year) || !Number.isFinite(month) || !Number.isFinite(day)) { + const fallbackDate = new Date(date) + if (Number.isNaN(fallbackDate.getTime())) { + return new Date().toISOString() + } + fallbackDate.setHours(23, 59, 59, 999) + return fallbackDate.toISOString() + } + + const endOfDayLocal = new Date(year, month - 1, day, 23, 59, 59, 999) + return endOfDayLocal.toISOString() +} + // Content state components const ErrorState = () => (
@@ -184,7 +204,7 @@ export default function Page() { } const variables: { name: string; expiresAt: string } = { name: newKeyName.trim(), - expiresAt: new Date(newKeyExpiry).toISOString(), + expiresAt: toEndOfDayUtcIso(newKeyExpiry), } createApiKey({ variables }) }