Expiring a temporary AWS session by advancing simulated time with Yulin
Simulated AWS services in Yulin take their timestamps from a clock which belongs to the simulation rather than to the process running it. That clock can be moved, so you can simulate and test behaviour which only becomes interesting once time has passed.
Here’s a simulated Bucket with an Object in it, and a Role which is allowed to read that Object. They run in a simulation which starts at a fixed instant:
import { CreateRoleCommand, PutRolePolicyCommand } from "@aws-sdk/client-iam";
import {
CreateBucketCommand,
GetObjectCommand,
PutObjectCommand,
} from "@aws-sdk/client-s3";
import { AssumeRoleCommand } from "@aws-sdk/client-sts";
import { SimAws, SimFixedClock } from "@kensio/yulin";
const simAws = new SimAws({
clock: new SimFixedClock(new Date("2026-07-27T09:00:00.000Z")),
});
const simIam = simAws.iam();
const simS3 = simAws.region("eu-west-2").s3();
await simS3.createBucket(new CreateBucketCommand({ Bucket: "foo-bucket" }));
await simS3.putObject(
new PutObjectCommand({
Bucket: "foo-bucket",
Key: "foobar.txt",
Body: "foobar",
}),
);
await simIam.createRole(
new CreateRoleCommand({
RoleName: "FooReaderRole",
AssumeRolePolicyDocument: JSON.stringify({
Version: "2012-10-17",
Statement: {
Effect: "Allow",
Principal: { AWS: `arn:aws:iam::${simAws.defaultAccountId}:root` },
Action: "sts:AssumeRole",
},
}),
}),
);
await simIam.putRolePolicy(
new PutRolePolicyCommand({
RoleName: "FooReaderRole",
PolicyName: "ReadFoobar",
PolicyDocument: JSON.stringify({
Version: "2012-10-17",
Statement: {
Effect: "Allow",
Action: "s3:GetObject",
Resource: "arn:aws:s3:::foo-bucket/*",
},
}),
}),
);
SimFixedClock reports one instant and doesn’t move on its own, so everything created here is
timestamped 09:00 however long the test actually takes to run. A simulation with no clock passed to
it runs in step with the real system clock instead.
If we assume that Role through simulated STS, it issues temporary credentials. The credential expiry is calculated with simulated time rather than real time:
const assumeRoleOutput = await simAws.sts().assumeRole(
new AssumeRoleCommand({
RoleArn: `arn:aws:iam::${simAws.defaultAccountId}:role/FooReaderRole`,
RoleSessionName: "reporting-session",
DurationSeconds: 900,
}),
);
const issued = assumeRoleOutput.Credentials!;
const credentials = {
accessKeyId: issued.AccessKeyId!,
secretAccessKey: issued.SecretAccessKey!,
sessionToken: issued.SessionToken!,
};
console.log(issued.Expiration); // 2026-07-27T09:15:00.000Z
Simulated service commands take an optional caller saying who is making the request. Credentials are one of the options it accepts, so those session credentials can be used to authorise a simulated S3 call. Reading the Object as that session works while the session is current:
await simS3.getObject(
new GetObjectCommand({ Bucket: "foo-bucket", Key: "foobar.txt" }),
{
caller: { kind: "credentials", credentials },
},
);
simAws.clock() lets us control the simulation’s clock, with freeze(), resume(), setTo()
and advanceBy(). Advancing by twenty minutes puts the simulation five minutes past the point where
that session expired. The same read then fails:
await simAws.clock().advanceBy({ minutes: 20 });
console.log(simAws.now()); // 2026-07-27T09:20:00.000Z
await simS3.getObject(
new GetObjectCommand({ Bucket: "foo-bucket", Key: "foobar.txt" }),
{
caller: { kind: "credentials", credentials },
},
);
SimIamInvalidCredentials: Sim IAM could not authenticate access key
ASIARZ9627FERG6GLY4H: the session expired at 2026-07-27T09:15:00.000Z
Credentials are authenticated before any policy evaluation, so an expired session fails there rather
than as an access denied decision. advanceBy() takes any combination of days, hours,
minutes, seconds and milliseconds, which add together, and it only runs forwards. setTo() is
the way to move a clock back.
Moving time also freezes it, so a test which has asked for a specific instant gets to assert on that
instant rather than on that instant plus however long the assertion took. resume() goes back to
running, carrying on from where the clock stopped.
Because the clock belongs to the SimAws instance, moving it doesn’t disturb anything else. Two
simulations in the same test file keep their own time:
const one = new SimAws({
clock: new SimFixedClock(new Date("2026-07-27T09:00:00.000Z")),
});
const two = new SimAws({
clock: new SimFixedClock(new Date("2026-07-27T09:00:00.000Z")),
});
await one.clock().advanceBy({ days: 1 });
console.log(one.now()); // 2026-07-28T09:00:00.000Z
console.log(two.now()); // 2026-07-27T09:00:00.000Z
That’s the main difference with fake timer libraries, which replace the clock for the whole process and have to be installed and torn down around each test.