@datacules/agent-identity-store-azure 0.10.0 → 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/azure.test.ts DELETED
@@ -1,247 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest';
2
-
3
- // Mock Azure SDK modules — constructors return objects with vi.fn() methods.
4
- // vi.mock() calls are hoisted before imports by Vitest.
5
- vi.mock('@azure/identity', () => ({
6
- DefaultAzureCredential: vi.fn(() => ({})),
7
- }));
8
-
9
- vi.mock('@azure/keyvault-secrets', () => ({
10
- SecretClient: vi.fn(() => ({
11
- getSecret: vi.fn(),
12
- listPropertiesOfSecrets: vi.fn(),
13
- })),
14
- }));
15
-
16
- vi.mock('@azure/data-tables', () => ({
17
- TableClient: vi.fn(() => ({
18
- getEntity: vi.fn(),
19
- upsertEntity: vi.fn(),
20
- deleteEntity: vi.fn(),
21
- })),
22
- odata: vi.fn((s: string) => s),
23
- }));
24
-
25
- import { AzureKeyVaultCredentialStore } from './index.js';
26
- import type { Credential } from '@datacules/agent-identity';
27
-
28
- const makeCred = (overrides: Partial<Credential> = {}): Credential => ({
29
- id: 'cred-openai',
30
- kind: 'fixed',
31
- name: 'OpenAI Key',
32
- scope: 'global',
33
- status: 'active',
34
- provider: 'openai',
35
- ref: 'openai-prod-slot',
36
- ...overrides,
37
- });
38
-
39
- // Minimal secret response shape returned by SecretClient.getSecret()
40
- const makeSecretResponse = (cred: Credential) => ({
41
- value: JSON.stringify(cred),
42
- properties: { contentType: cred.status },
43
- });
44
-
45
- describe('AzureKeyVaultCredentialStore', () => {
46
- let store: AzureKeyVaultCredentialStore;
47
- let secretsMock: {
48
- getSecret: ReturnType<typeof vi.fn>;
49
- listPropertiesOfSecrets: ReturnType<typeof vi.fn>;
50
- };
51
- let tableMock: {
52
- getEntity: ReturnType<typeof vi.fn>;
53
- upsertEntity: ReturnType<typeof vi.fn>;
54
- deleteEntity: ReturnType<typeof vi.fn>;
55
- };
56
-
57
- beforeEach(() => {
58
- vi.clearAllMocks();
59
- store = new AzureKeyVaultCredentialStore({
60
- keyVaultUrl: 'https://test.vault.azure.net',
61
- tablesEndpoint: 'https://test.table.core.windows.net',
62
- });
63
- // Access mock clients injected by the mocked SDK constructors
64
- secretsMock = (store as any).secrets as typeof secretsMock;
65
- tableMock = (store as any).table as typeof tableMock;
66
- });
67
-
68
- // ─── constructor ────────────────────────────────────────────────────────────
69
-
70
- describe('constructor', () => {
71
- it('throws when keyVaultUrl is missing from both options and environment', () => {
72
- delete process.env['AZURE_KEYVAULT_URL'];
73
- expect(
74
- () =>
75
- new AzureKeyVaultCredentialStore({
76
- tablesEndpoint: 'https://test.table.core.windows.net',
77
- })
78
- ).toThrow('keyVaultUrl is required');
79
- });
80
-
81
- it('throws when tablesEndpoint is missing from both options and environment', () => {
82
- delete process.env['AZURE_TABLES_ENDPOINT'];
83
- expect(
84
- () =>
85
- new AzureKeyVaultCredentialStore({
86
- keyVaultUrl: 'https://test.vault.azure.net',
87
- })
88
- ).toThrow('tablesEndpoint is required');
89
- });
90
- });
91
-
92
- // ─── findByRef() ────────────────────────────────────────────────────────────
93
-
94
- describe('findByRef()', () => {
95
- it('returns active credential when Key Vault returns a secret with contentType=active', async () => {
96
- const cred = makeCred();
97
- secretsMock.getSecret.mockResolvedValue(makeSecretResponse(cred));
98
- expect(await store.findByRef('openai-prod-slot')).toEqual(cred);
99
- });
100
-
101
- it('returns null when contentType is not active (does not parse the value)', async () => {
102
- const cred = makeCred({ status: 'pending' });
103
- secretsMock.getSecret.mockResolvedValue({
104
- value: JSON.stringify(cred),
105
- properties: { contentType: 'pending' },
106
- });
107
- expect(await store.findByRef('openai-prod-slot')).toBeNull();
108
- });
109
-
110
- it('returns null when secret value is undefined', async () => {
111
- secretsMock.getSecret.mockResolvedValue({
112
- value: undefined,
113
- properties: { contentType: 'active' },
114
- });
115
- expect(await store.findByRef('openai-prod-slot')).toBeNull();
116
- });
117
-
118
- it('returns null without throwing when getSecret throws', async () => {
119
- secretsMock.getSecret.mockRejectedValue(new Error('SecretNotFound'));
120
- expect(await store.findByRef('missing-ref')).toBeNull();
121
- });
122
- });
123
-
124
- // ─── listActive() ───────────────────────────────────────────────────────────
125
-
126
- describe('listActive()', () => {
127
- it('returns active credentials iterated from listPropertiesOfSecrets and fetched via getSecret', async () => {
128
- const cred = makeCred();
129
- secretsMock.listPropertiesOfSecrets.mockReturnValue(
130
- (async function* () {
131
- yield { contentType: 'active', enabled: true, name: 'openai-prod-slot' };
132
- })()
133
- );
134
- secretsMock.getSecret.mockResolvedValue(makeSecretResponse(cred));
135
- const results = await store.listActive();
136
- expect(results).toHaveLength(1);
137
- expect(results[0]).toEqual(cred);
138
- });
139
-
140
- it('skips secrets with contentType !== active without calling getSecret', async () => {
141
- secretsMock.listPropertiesOfSecrets.mockReturnValue(
142
- (async function* () {
143
- yield { contentType: 'pending', enabled: true, name: 'pending-slot' };
144
- })()
145
- );
146
- expect(await store.listActive()).toHaveLength(0);
147
- expect(secretsMock.getSecret).not.toHaveBeenCalled();
148
- });
149
-
150
- it('skips secrets with enabled=false', async () => {
151
- secretsMock.listPropertiesOfSecrets.mockReturnValue(
152
- (async function* () {
153
- yield { contentType: 'active', enabled: false, name: 'disabled-slot' };
154
- })()
155
- );
156
- expect(await store.listActive()).toHaveLength(0);
157
- });
158
-
159
- it('returns empty array when listPropertiesOfSecrets throws (Key Vault unreachable)', async () => {
160
- secretsMock.listPropertiesOfSecrets.mockReturnValue(
161
- // eslint-disable-next-line require-yield
162
- (async function* () {
163
- throw new Error('KeyVaultUnavailable');
164
- })()
165
- );
166
- expect(await store.listActive()).toEqual([]);
167
- });
168
- });
169
-
170
- // ─── listByKind() ───────────────────────────────────────────────────────────
171
-
172
- describe('listByKind()', () => {
173
- it('returns only credentials matching the requested kind', async () => {
174
- const fixed = makeCred({ kind: 'fixed', id: 'cred-fixed', ref: 'fixed-slot' });
175
- const delegated = makeCred({ kind: 'user-delegated', id: 'cred-user', ref: 'user-slot' });
176
- secretsMock.listPropertiesOfSecrets.mockReturnValue(
177
- (async function* () {
178
- yield { contentType: 'active', enabled: true, name: 'fixed-slot' };
179
- yield { contentType: 'active', enabled: true, name: 'user-slot' };
180
- })()
181
- );
182
- secretsMock.getSecret
183
- .mockResolvedValueOnce(makeSecretResponse(fixed))
184
- .mockResolvedValueOnce(makeSecretResponse(delegated));
185
- const result = await store.listByKind('fixed');
186
- expect(result).toHaveLength(1);
187
- expect(result[0].kind).toBe('fixed');
188
- });
189
- });
190
-
191
- // ─── reserve() ──────────────────────────────────────────────────────────────
192
-
193
- describe('reserve()', () => {
194
- it('returns true when no existing lock is found (getEntity throws) and upsert succeeds', async () => {
195
- tableMock.getEntity.mockRejectedValue(new Error('EntityNotFound'));
196
- tableMock.upsertEntity.mockResolvedValue({});
197
- expect(await store.reserve('cred-ref', 'mig-1', 300)).toBe(true);
198
- });
199
-
200
- it('returns false when a different migration holds an unexpired lock', async () => {
201
- const futureExpiry = Math.floor(Date.now() / 1000) + 600;
202
- tableMock.getEntity.mockResolvedValue({
203
- migrationId: 'other-migration',
204
- expiresAt: futureExpiry,
205
- });
206
- expect(await store.reserve('cred-ref', 'mig-2', 300)).toBe(false);
207
- });
208
-
209
- it('returns true when the same migration re-acquires its own lock', async () => {
210
- const futureExpiry = Math.floor(Date.now() / 1000) + 600;
211
- tableMock.getEntity.mockResolvedValue({
212
- migrationId: 'mig-1',
213
- expiresAt: futureExpiry,
214
- });
215
- tableMock.upsertEntity.mockResolvedValue({});
216
- expect(await store.reserve('cred-ref', 'mig-1', 300)).toBe(true);
217
- });
218
- });
219
-
220
- // ─── release() ──────────────────────────────────────────────────────────────
221
-
222
- describe('release()', () => {
223
- it('deletes the entity when migrationId matches the stored lock owner', async () => {
224
- tableMock.getEntity.mockResolvedValue({
225
- migrationId: 'mig-1',
226
- expiresAt: Math.floor(Date.now() / 1000) + 600,
227
- });
228
- tableMock.deleteEntity.mockResolvedValue({});
229
- await store.release('cred-ref', 'mig-1');
230
- expect(tableMock.deleteEntity).toHaveBeenCalledWith('lock', 'cred-ref');
231
- });
232
-
233
- it('does not call deleteEntity when migrationId does not match the stored lock', async () => {
234
- tableMock.getEntity.mockResolvedValue({
235
- migrationId: 'other-mig',
236
- expiresAt: Math.floor(Date.now() / 1000) + 600,
237
- });
238
- await store.release('cred-ref', 'mig-1');
239
- expect(tableMock.deleteEntity).not.toHaveBeenCalled();
240
- });
241
-
242
- it('resolves without throwing when getEntity throws (lock already released)', async () => {
243
- tableMock.getEntity.mockRejectedValue(new Error('EntityNotFound'));
244
- await expect(store.release('cred-ref', 'mig-1')).resolves.toBeUndefined();
245
- });
246
- });
247
- });