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
31 changes: 30 additions & 1 deletion backend/tests/apps/api/internal/mutations/api_key_test.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from datetime import timedelta
from datetime import datetime, timedelta
from unittest.mock import MagicMock, patch
from uuid import uuid4

Expand Down Expand Up @@ -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."""
Expand Down
36 changes: 33 additions & 3 deletions frontend/__tests__/unit/pages/ApiKeysPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Comment thread
Shashwat-Darshan marked this conversation as resolved.
return endOfDayLocal.toISOString()
}

beforeEach(() => setupMocks())
afterEach(() => jest.clearAllMocks())

Expand Down Expand Up @@ -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)

Comment thread
Shashwat-Darshan marked this conversation as resolved.
await waitFor(() => {
expect(mockCreateMutation).toHaveBeenCalledWith({
Expand All @@ -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(<ApiKeysPage />)
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,
},
})
})
Expand Down Expand Up @@ -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,
Expand Down
22 changes: 21 additions & 1 deletion frontend/src/app/settings/api-keys/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = () => (
<div className="rounded-md bg-red-50 p-4 text-red-700 dark:bg-red-900/20 dark:text-red-400">
Expand Down Expand Up @@ -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 })
}
Expand Down