313 lines
10 KiB
TypeScript
313 lines
10 KiB
TypeScript
/**
|
|
* Lambda integration tests.
|
|
*/
|
|
|
|
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
import {
|
|
LambdaClient,
|
|
CreateFunctionCommand,
|
|
GetFunctionCommand,
|
|
GetFunctionConfigurationCommand,
|
|
UpdateFunctionConfigurationCommand,
|
|
ListFunctionsCommand,
|
|
DeleteFunctionCommand,
|
|
CreateAliasCommand,
|
|
GetAliasCommand,
|
|
ListAliasesCommand,
|
|
UpdateAliasCommand,
|
|
DeleteAliasCommand,
|
|
PublishVersionCommand,
|
|
ListVersionsByFunctionCommand,
|
|
UpdateFunctionCodeCommand,
|
|
} from '@aws-sdk/client-lambda';
|
|
import { makeClient, uniqueName, ACCOUNT, buildMinimalZip } from './setup';
|
|
|
|
describe('Lambda', () => {
|
|
let lambda: LambdaClient;
|
|
let fnName: string;
|
|
|
|
beforeAll(() => {
|
|
lambda = makeClient(LambdaClient);
|
|
fnName = `test-fn-${uniqueName()}`;
|
|
});
|
|
|
|
afterAll(async () => {
|
|
try {
|
|
await lambda.send(new DeleteAliasCommand({ FunctionName: fnName, Name: 'live' }));
|
|
} catch {
|
|
// ignore
|
|
}
|
|
try {
|
|
await lambda.send(new DeleteFunctionCommand({ FunctionName: fnName }));
|
|
} catch {
|
|
// ignore
|
|
}
|
|
});
|
|
|
|
it('should create function', async () => {
|
|
const handlerCode = "exports.handler = async (event) => ({ statusCode: 200, body: 'ok' });";
|
|
const zipBuffer = buildMinimalZip('index.js', Buffer.from(handlerCode));
|
|
|
|
await lambda.send(
|
|
new CreateFunctionCommand({
|
|
FunctionName: fnName,
|
|
Runtime: 'nodejs18.x',
|
|
Role: `arn:aws:iam::${ACCOUNT}:role/lambda-role`,
|
|
Handler: 'index.handler',
|
|
Code: { ZipFile: zipBuffer },
|
|
})
|
|
);
|
|
});
|
|
|
|
it('should get function', async () => {
|
|
const response = await lambda.send(new GetFunctionCommand({ FunctionName: fnName }));
|
|
expect(response.Configuration?.FunctionName).toBe(fnName);
|
|
});
|
|
|
|
it('should list functions', async () => {
|
|
const response = await lambda.send(new ListFunctionsCommand({}));
|
|
expect(response.Functions?.some((f) => f.FunctionName === fnName)).toBe(true);
|
|
});
|
|
|
|
it('should publish version', async () => {
|
|
const response = await lambda.send(
|
|
new PublishVersionCommand({ FunctionName: fnName, Description: 'v1' })
|
|
);
|
|
expect(response.Version).toBeTruthy();
|
|
});
|
|
|
|
it('should create alias', async () => {
|
|
const response = await lambda.send(
|
|
new CreateAliasCommand({
|
|
FunctionName: fnName,
|
|
Name: 'live',
|
|
FunctionVersion: '$LATEST',
|
|
Description: 'live alias',
|
|
})
|
|
);
|
|
expect(response.AliasArn).toBeTruthy();
|
|
});
|
|
|
|
it('should get alias', async () => {
|
|
const response = await lambda.send(
|
|
new GetAliasCommand({ FunctionName: fnName, Name: 'live' })
|
|
);
|
|
expect(response.Name).toBe('live');
|
|
});
|
|
|
|
it('should list aliases', async () => {
|
|
const response = await lambda.send(new ListAliasesCommand({ FunctionName: fnName }));
|
|
expect(response.Aliases?.some((a) => a.Name === 'live')).toBe(true);
|
|
});
|
|
|
|
it('should update alias', async () => {
|
|
const response = await lambda.send(
|
|
new UpdateAliasCommand({
|
|
FunctionName: fnName,
|
|
Name: 'live',
|
|
Description: 'updated description',
|
|
})
|
|
);
|
|
expect(response.Description).toBe('updated description');
|
|
});
|
|
|
|
it('should delete alias', async () => {
|
|
await lambda.send(new DeleteAliasCommand({ FunctionName: fnName, Name: 'live' }));
|
|
});
|
|
|
|
it('should fail to get deleted alias', async () => {
|
|
await expect(
|
|
lambda.send(new GetAliasCommand({ FunctionName: fnName, Name: 'live' }))
|
|
).rejects.toThrow();
|
|
});
|
|
|
|
it('should delete function', async () => {
|
|
await lambda.send(new DeleteFunctionCommand({ FunctionName: fnName }));
|
|
fnName = '';
|
|
});
|
|
|
|
it('should fail to get deleted function', async () => {
|
|
await expect(
|
|
lambda.send(new GetFunctionCommand({ FunctionName: `test-fn-${uniqueName()}` }))
|
|
).rejects.toThrow();
|
|
});
|
|
});
|
|
|
|
describe('Lambda ImageConfig.WorkingDirectory', () => {
|
|
let lambda: LambdaClient;
|
|
const IMAGE_URI = '000000000000.dkr.ecr.us-east-1.amazonaws.com/fake-repo:latest';
|
|
const ROLE = 'arn:aws:iam::000000000000:role/lambda-role';
|
|
|
|
beforeAll(() => {
|
|
lambda = makeClient(LambdaClient);
|
|
});
|
|
|
|
it('round-trips WorkingDirectory through create and get', async () => {
|
|
const fnName = `test-imgwd-${uniqueName()}`;
|
|
try {
|
|
const createResp = await lambda.send(new CreateFunctionCommand({
|
|
FunctionName: fnName,
|
|
PackageType: 'Image',
|
|
Role: ROLE,
|
|
Code: { ImageUri: IMAGE_URI },
|
|
ImageConfig: { WorkingDirectory: '/app' },
|
|
}));
|
|
|
|
expect(createResp.ImageConfigResponse?.ImageConfig?.WorkingDirectory).toBe('/app');
|
|
|
|
const getResp = await lambda.send(new GetFunctionConfigurationCommand({
|
|
FunctionName: fnName,
|
|
}));
|
|
|
|
expect(getResp.ImageConfigResponse?.ImageConfig?.WorkingDirectory).toBe('/app');
|
|
} finally {
|
|
await lambda.send(new DeleteFunctionCommand({ FunctionName: fnName })).catch(() => {});
|
|
}
|
|
});
|
|
|
|
it('updates WorkingDirectory via updateFunctionConfiguration', async () => {
|
|
const fnName = `test-imgwd-upd-${uniqueName()}`;
|
|
try {
|
|
await lambda.send(new CreateFunctionCommand({
|
|
FunctionName: fnName,
|
|
PackageType: 'Image',
|
|
Role: ROLE,
|
|
Code: { ImageUri: IMAGE_URI },
|
|
ImageConfig: { WorkingDirectory: '/initial' },
|
|
}));
|
|
|
|
const updateResp = await lambda.send(new UpdateFunctionConfigurationCommand({
|
|
FunctionName: fnName,
|
|
ImageConfig: { WorkingDirectory: '/updated' },
|
|
}));
|
|
|
|
expect(updateResp.ImageConfigResponse?.ImageConfig?.WorkingDirectory).toBe('/updated');
|
|
} finally {
|
|
await lambda.send(new DeleteFunctionCommand({ FunctionName: fnName })).catch(() => {});
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('Lambda Publish flag', () => {
|
|
let lambda: LambdaClient;
|
|
let fnName: string;
|
|
|
|
beforeAll(() => {
|
|
lambda = makeClient(LambdaClient);
|
|
fnName = `test-publish-${uniqueName()}`;
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await lambda.send(new DeleteFunctionCommand({ FunctionName: fnName })).catch(() => {});
|
|
});
|
|
|
|
it('should publish version 1 on create and keep the ARN unqualified', async () => {
|
|
const response = await lambda.send(new CreateFunctionCommand({
|
|
FunctionName: fnName,
|
|
Runtime: 'nodejs20.x',
|
|
Role: `arn:aws:iam::${ACCOUNT}:role/lambda-role`,
|
|
Handler: 'index.handler',
|
|
Code: { ZipFile: buildMinimalZip('index.js', Buffer.from('exports.handler = async () => 1;')) },
|
|
Publish: true,
|
|
}));
|
|
|
|
expect(response.Version).toBe('1');
|
|
expect(response.FunctionArn).toMatch(new RegExp(`:function:${fnName}$`));
|
|
|
|
const listed = await lambda.send(new ListVersionsByFunctionCommand({ FunctionName: fnName }));
|
|
expect((listed.Versions ?? []).map((v) => v.Version).sort()).toEqual(['$LATEST', '1']);
|
|
});
|
|
|
|
it('should publish the next version on code update and return a qualified ARN', async () => {
|
|
const response = await lambda.send(new UpdateFunctionCodeCommand({
|
|
FunctionName: fnName,
|
|
ZipFile: buildMinimalZip('index.js', Buffer.from('exports.handler = async () => 2;')),
|
|
Publish: true,
|
|
}));
|
|
|
|
expect(response.Version).toBe('2');
|
|
expect(response.FunctionArn).toMatch(new RegExp(`:function:${fnName}:2$`));
|
|
});
|
|
|
|
it('should create no version when Publish is not set', async () => {
|
|
const response = await lambda.send(new UpdateFunctionCodeCommand({
|
|
FunctionName: fnName,
|
|
ZipFile: buildMinimalZip('index.js', Buffer.from('exports.handler = async () => 3;')),
|
|
}));
|
|
|
|
expect(response.Version).toBe('$LATEST');
|
|
|
|
const listed = await lambda.send(new ListVersionsByFunctionCommand({ FunctionName: fnName }));
|
|
expect((listed.Versions ?? []).map((v) => v.Version).sort()).toEqual(['$LATEST', '1', '2']);
|
|
});
|
|
});
|
|
|
|
describe('Lambda version deletion', () => {
|
|
let lambda: LambdaClient;
|
|
let fnName: string;
|
|
|
|
const versions = async () => {
|
|
const r = await lambda.send(new ListVersionsByFunctionCommand({ FunctionName: fnName }));
|
|
return (r.Versions ?? []).map((v) => v.Version).sort();
|
|
};
|
|
|
|
beforeAll(async () => {
|
|
lambda = makeClient(LambdaClient);
|
|
fnName = `test-delver-${uniqueName()}`;
|
|
|
|
await lambda.send(new CreateFunctionCommand({
|
|
FunctionName: fnName,
|
|
Runtime: 'nodejs20.x',
|
|
Role: `arn:aws:iam::${ACCOUNT}:role/lambda-role`,
|
|
Handler: 'index.handler',
|
|
Code: { ZipFile: buildMinimalZip('index.js', Buffer.from('exports.handler = async () => 1;')) },
|
|
}));
|
|
await lambda.send(new PublishVersionCommand({ FunctionName: fnName }));
|
|
await lambda.send(new UpdateFunctionCodeCommand({
|
|
FunctionName: fnName,
|
|
ZipFile: buildMinimalZip('index.js', Buffer.from('exports.handler = async () => 2;')),
|
|
}));
|
|
await lambda.send(new PublishVersionCommand({ FunctionName: fnName }));
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await lambda.send(new DeleteFunctionCommand({ FunctionName: fnName })).catch(() => {});
|
|
});
|
|
|
|
it('should delete only the qualified version, leaving the function intact', async () => {
|
|
expect(await versions()).toEqual(['$LATEST', '1', '2']);
|
|
|
|
await lambda.send(new DeleteFunctionCommand({ FunctionName: fnName, Qualifier: '2' }));
|
|
|
|
// The function survives — before this was fixed, the whole function was deleted.
|
|
const fn = await lambda.send(new GetFunctionCommand({ FunctionName: fnName }));
|
|
expect(fn.Configuration?.FunctionName).toBe(fnName);
|
|
expect(await versions()).toEqual(['$LATEST', '1']);
|
|
});
|
|
|
|
it('should refuse to delete $LATEST by qualifier', async () => {
|
|
await expect(
|
|
lambda.send(new DeleteFunctionCommand({ FunctionName: fnName, Qualifier: '$LATEST' }))
|
|
).rejects.toThrow(/cannot be deleted without deleting the function/);
|
|
expect(await versions()).toEqual(['$LATEST', '1']);
|
|
});
|
|
|
|
it('should refuse to delete a version an alias references', async () => {
|
|
await lambda.send(new CreateAliasCommand({
|
|
FunctionName: fnName,
|
|
Name: 'pinned',
|
|
FunctionVersion: '1',
|
|
}));
|
|
|
|
await expect(
|
|
lambda.send(new DeleteFunctionCommand({ FunctionName: fnName, Qualifier: '1' }))
|
|
).rejects.toThrow(/aliases reference it/);
|
|
expect(await versions()).toEqual(['$LATEST', '1']);
|
|
});
|
|
|
|
it('should treat a version that does not exist as a no-op', async () => {
|
|
await lambda.send(new DeleteFunctionCommand({ FunctionName: fnName, Qualifier: '99' }));
|
|
expect(await versions()).toEqual(['$LATEST', '1']);
|
|
});
|
|
});
|