161 161 | "documentation": "Initial fetch returns 6-hour credentials, selecting the 60-minute advisory window.",
|
162 162 | "expected": { "result": "newCredentials", "sourceContacted": true, "rateLimited": false, "advisoryWindowSeconds": 3600 }
|
163 163 | },
|
164 164 | {
|
165 165 | "type": "advanceTime",
|
166 166 | "seconds": 18060
|
167 167 | },
|
168 168 | {
|
169 169 | "type": "getCredentials",
|
170 170 | "response": "freshCredentials",
|
171 171 | "lifetimeSeconds": 600,
|
172 172 | "documentation": "59 minutes remain until expiration, inside the 60-minute advisory window, so the SDK refreshes. The new 10-minute credentials select the 5-minute advisory window.",
|
173 173 | "expected": { "result": "newCredentials", "sourceContacted": true, "rateLimited": false, "advisoryWindowSeconds": 300 }
|
174 174 | }
|
175 175 | ]
|
176 176 | },
|
177 177 | {
|
178 178 | "documentation": "A customer-configured advisory window overrides the table. Credentials with a 6-hour lifetime would map to 60 minutes, but the configured 30-minute window is used instead.",
|
179 179 | "given": { "cachedCredentials": "none", "configuredAdvisoryWindowSeconds": 1800 },
|
180 180 | "steps": [
|
181 181 | {
|
182 182 | "type": "getCredentials",
|
183 183 | "response": "freshCredentials",
|
184 184 | "lifetimeSeconds": 21600,
|
185 185 | "expected": { "result": "newCredentials", "sourceContacted": true, "rateLimited": false, "advisoryWindowSeconds": 1800 }
|
186 186 | }
|
187 187 | ]
|
188 188 | },
|
189 189 |
|
190 190 | {
|
191 - | "documentation": "Advisory window, non-recoverable failure: the SDK raises immediately. Because no refresh backoff is applied, the next call contacts the source again rather than being rate limited.",
|
191 + | "documentation": "Advisory window, non-recoverable failure: the SDK raises immediately. No refresh backoff is applied, but the error is cached for up to 5 seconds, so a recovering call succeeds once that cache expires.",
|
192 192 | "given": { "cachedCredentials": "advisory" },
|
193 193 | "steps": [
|
194 194 | {
|
195 195 | "type": "getCredentials",
|
196 196 | "response": "nonRecoverableError",
|
197 197 | "documentation": "Non-recoverable failure: the SDK raises and does not apply the refresh backoff.",
|
198 198 | "expected": { "result": "nonRecoverableError", "sourceContacted": true, "rateLimited": false }
|
199 199 | },
|
200 + | {
|
201 + | "type": "advanceTime",
|
202 + | "seconds": 6
|
203 + | },
|
200 204 | {
|
201 205 | "type": "getCredentials",
|
202 206 | "response": "freshCredentials",
|
203 - | "documentation": "No refresh backoff was applied, so this call contacts the source again and succeeds.",
|
207 + | "documentation": "The non-recoverable error cache (max 5 seconds) has expired, so this call contacts the source again and succeeds.",
|
204 208 | "expected": { "result": "newCredentials", "sourceContacted": true, "rateLimited": false }
|
205 209 | }
|
206 210 | ]
|
207 211 | },
|
208 212 | {
|
209 - | "documentation": "Mandatory window, non-recoverable failure: the SDK raises immediately. Because no refresh backoff is applied, the next call contacts the source again rather than being rate limited.",
|
213 + | "documentation": "Mandatory window, non-recoverable failure: the SDK raises immediately. No refresh backoff is applied, but the error is cached for up to 5 seconds, so a recovering call succeeds once that cache expires.",
|
210 214 | "given": { "cachedCredentials": "mandatory" },
|
211 215 | "steps": [
|
212 216 | {
|
213 217 | "type": "getCredentials",
|
214 218 | "response": "nonRecoverableError",
|
215 219 | "documentation": "Non-recoverable failure: the SDK raises and does not apply the refresh backoff.",
|
216 220 | "expected": { "result": "nonRecoverableError", "sourceContacted": true, "rateLimited": false }
|
217 221 | },
|
222 + | {
|
223 + | "type": "advanceTime",
|
224 + | "seconds": 6
|
225 + | },
|
218 226 | {
|
219 227 | "type": "getCredentials",
|
220 228 | "response": "freshCredentials",
|
221 - | "documentation": "No refresh backoff was applied, so this call contacts the source again and succeeds.",
|
229 + | "documentation": "The non-recoverable error cache (max 5 seconds) has expired, so this call contacts the source again and succeeds.",
|
222 230 | "expected": { "result": "newCredentials", "sourceContacted": true, "rateLimited": false }
|
223 231 | }
|
224 232 | ]
|
225 233 | },
|
234 + | {
|
235 + | "documentation": "Non-recoverable error, then an immediate retry with no clock advance: the error is still cached, so the SDK re-raises it without contacting the source. This protects the credential source from an application that swallows the error and retries in a loop.",
|
236 + | "given": { "cachedCredentials": "advisory" },
|
237 + | "steps": [
|
238 + | {
|
239 + | "type": "getCredentials",
|
240 + | "response": "nonRecoverableError",
|
241 + | "documentation": "Non-recoverable failure: the SDK raises and caches the error for up to 5 seconds.",
|
242 + | "expected": { "result": "nonRecoverableError", "sourceContacted": true, "rateLimited": false }
|
243 + | },
|
244 + | {
|
245 + | "type": "getCredentials",
|
246 + | "documentation": "Immediate retry with no clock advance. The cached error is still active, so the SDK re-raises it without contacting the source.",
|
247 + | "expected": { "result": "nonRecoverableError", "sourceContacted": false, "rateLimited": false }
|
248 + | }
|
249 + | ]
|
250 + | },
|
226 251 |
|
227 252 | {
|
228 253 | "documentation": "Invalidate with an access key ID matching the cached credentials routes the next getCredentials through the mandatory refresh path, and the refresh succeeds.",
|
229 254 | "given": { "cachedCredentials": "valid", "accessKeyId": "AKID-1" },
|
230 255 | "steps": [
|
231 256 | { "type": "invalidate", "rejectedAccessKeyId": "AKID-1" },
|
232 257 | {
|
233 258 | "type": "getCredentials",
|
234 259 | "response": "freshCredentials",
|
235 260 | "expected": { "result": "newCredentials", "sourceContacted": true, "rateLimited": false }
|
317 342 | },
|
318 343 | {
|
319 344 | "type": "getCredentials",
|
320 345 | "response": "freshCredentials",
|
321 346 | "documentation": "725s elapsed total and the refresh backoff has elapsed, so the SDK contacts the credential source and the refresh succeeds.",
|
322 347 | "expected": { "result": "newCredentials", "sourceContacted": true, "rateLimited": false }
|
323 348 | }
|
324 349 | ]
|
325 350 | },
|
326 351 | {
|
327 - | "documentation": "No cached credentials and the initial fetch fails with a non-recoverable error: the SDK raises the error directly rather than a generic NoCredentialsError. Because no refresh backoff is applied, the next call contacts the source again.",
|
352 + | "documentation": "No cached credentials and the initial fetch fails with a non-recoverable error: the SDK raises the error directly rather than a generic NoCredentialsError. No refresh backoff is applied, but the error is cached for up to 5 seconds, so a recovering call succeeds once that cache expires.",
|
328 353 | "given": { "cachedCredentials": "none" },
|
329 354 | "steps": [
|
330 355 | {
|
331 356 | "type": "getCredentials",
|
332 357 | "response": "nonRecoverableError",
|
333 358 | "documentation": "Non-recoverable failure: the SDK raises and does not apply the refresh backoff.",
|
334 359 | "expected": { "result": "nonRecoverableError", "sourceContacted": true, "rateLimited": false }
|
335 360 | },
|
361 + | {
|
362 + | "type": "advanceTime",
|
363 + | "seconds": 6
|
364 + | },
|
336 365 | {
|
337 366 | "type": "getCredentials",
|
338 367 | "response": "freshCredentials",
|
339 - | "documentation": "No refresh backoff was applied, so this call contacts the source again and succeeds.",
|
368 + | "documentation": "The non-recoverable error cache (max 5 seconds) has expired, so this call contacts the source again and succeeds.",
|
340 369 | "expected": { "result": "newCredentials", "sourceContacted": true, "rateLimited": false }
|
341 370 | }
|
342 371 | ]
|
343 372 | }
|
344 373 | ]
|