/** * Lambda Layers compatibility tests. * * Verifies that the real @aws-sdk/client-lambda can round-trip through * Floci's layer endpoints without wire-format issues. */ import { describe, it, expect, beforeAll, afterAll } from 'vitest'; import { LambdaClient, PublishLayerVersionCommand, GetLayerVersionCommand, ListLayerVersionsCommand, ListLayersCommand, DeleteLayerVersionCommand, CreateFunctionCommand, GetFunctionConfigurationCommand, UpdateFunctionConfigurationCommand, DeleteFunctionCommand, } from '@aws-sdk/client-lambda'; import { makeClient, uniqueName, ACCOUNT, buildMinimalZip } from './setup'; describe('Lambda Layers', () => { let lambda: LambdaClient; let layerName: string; let layerArn: string; let layerVersionArn1: string; let layerVersionArn2: string; beforeAll(() => { lambda = makeClient(LambdaClient); layerName = `compat-layer-${uniqueName()}`; }); afterAll(async () => { // Clean up any remaining layer versions try { await lambda.send(new DeleteLayerVersionCommand({ LayerName: layerName, VersionNumber: 1 })); } catch { /* ignore */ } try { await lambda.send(new DeleteLayerVersionCommand({ LayerName: layerName, VersionNumber: 2 })); } catch { /* ignore */ } }); it('should publish layer version 1', async () => { const layerContent = Buffer.from('module.exports.util = () => "v1";'); const zipBuffer = buildMinimalZip('nodejs/node_modules/shared/index.js', layerContent); const response = await lambda.send( new PublishLayerVersionCommand({ LayerName: layerName, Content: { ZipFile: zipBuffer }, CompatibleRuntimes: ['nodejs20.x', 'nodejs22.x'], CompatibleArchitectures: ['x86_64'], Description: 'Shared utilities v1', LicenseInfo: 'MIT', }) ); expect(response.Version).toBe(1); expect(response.LayerArn).toContain(`:layer:${layerName}`); expect(response.LayerVersionArn).toContain(`:layer:${layerName}:1`); expect(response.Description).toBe('Shared utilities v1'); expect(response.LicenseInfo).toBe('MIT'); expect(response.CompatibleRuntimes).toContain('nodejs20.x'); expect(response.CompatibleRuntimes).toContain('nodejs22.x'); expect(response.CompatibleArchitectures).toContain('x86_64'); expect(response.Content?.CodeSize).toBeGreaterThan(0); expect(response.Content?.CodeSha256).toBeTruthy(); expect(response.CreatedDate).toBeTruthy(); layerArn = response.LayerArn!; layerVersionArn1 = response.LayerVersionArn!; }); it('should publish layer version 2', async () => { const layerContent = Buffer.from('module.exports.util = () => "v2";'); const zipBuffer = buildMinimalZip('nodejs/node_modules/shared/index.js', layerContent); const response = await lambda.send( new PublishLayerVersionCommand({ LayerName: layerName, Content: { ZipFile: zipBuffer }, CompatibleRuntimes: ['nodejs22.x'], Description: 'Shared utilities v2', }) ); expect(response.Version).toBe(2); expect(response.LayerVersionArn).toContain(`:layer:${layerName}:2`); expect(response.Description).toBe('Shared utilities v2'); layerVersionArn2 = response.LayerVersionArn!; }); it('should get layer version 1', async () => { const response = await lambda.send( new GetLayerVersionCommand({ LayerName: layerName, VersionNumber: 1, }) ); expect(response.Version).toBe(1); expect(response.LayerArn).toBe(layerArn); expect(response.LayerVersionArn).toBe(layerVersionArn1); expect(response.Description).toBe('Shared utilities v1'); expect(response.Content?.CodeSha256).toBeTruthy(); expect(response.Content?.CodeSize).toBeGreaterThan(0); }); it('should fail to get non-existent layer version', async () => { await expect( lambda.send(new GetLayerVersionCommand({ LayerName: layerName, VersionNumber: 99 })) ).rejects.toThrow(); }); it('should list layer versions', async () => { const response = await lambda.send( new ListLayerVersionsCommand({ LayerName: layerName }) ); expect(response.LayerVersions).toHaveLength(2); expect(response.LayerVersions![0].Version).toBe(1); expect(response.LayerVersions![1].Version).toBe(2); }); it('should list layers and include our layer', async () => { const response = await lambda.send(new ListLayersCommand({})); const ourLayer = response.Layers?.find((l) => l.LayerName === layerName); expect(ourLayer).toBeDefined(); expect(ourLayer!.LayerArn).toBe(layerArn); expect(ourLayer!.LatestMatchingVersion?.Version).toBe(2); }); it('should delete layer version 1', async () => { // DeleteLayerVersion returns nothing on success (204) await lambda.send( new DeleteLayerVersionCommand({ LayerName: layerName, VersionNumber: 1 }) ); // Verify it's gone await expect( lambda.send(new GetLayerVersionCommand({ LayerName: layerName, VersionNumber: 1 })) ).rejects.toThrow(); }); it('should list layer versions after deletion', async () => { const response = await lambda.send( new ListLayerVersionsCommand({ LayerName: layerName }) ); expect(response.LayerVersions).toHaveLength(1); expect(response.LayerVersions![0].Version).toBe(2); }); it('should handle delete of non-existent version gracefully', async () => { // AWS returns 204 even for non-existent versions await lambda.send( new DeleteLayerVersionCommand({ LayerName: layerName, VersionNumber: 99 }) ); }); }); describe('Lambda Layer Attachment', () => { let lambda: LambdaClient; let layerName: string; let layerVersionArn: string; let fnName: string; beforeAll(async () => { lambda = makeClient(LambdaClient); layerName = `attach-layer-${uniqueName()}`; fnName = `layer-fn-${uniqueName()}`; // Publish a layer to attach const layerContent = Buffer.from('module.exports.dep = () => "shared";'); const zipBuffer = buildMinimalZip('nodejs/node_modules/dep/index.js', layerContent); const layerResp = await lambda.send( new PublishLayerVersionCommand({ LayerName: layerName, Content: { ZipFile: zipBuffer }, CompatibleRuntimes: ['nodejs20.x'], }) ); layerVersionArn = layerResp.LayerVersionArn!; }); afterAll(async () => { try { await lambda.send(new DeleteFunctionCommand({ FunctionName: fnName })); } catch { /* ignore */ } try { await lambda.send(new DeleteLayerVersionCommand({ LayerName: layerName, VersionNumber: 1 })); } catch { /* ignore */ } }); it('should create function with layer attached', async () => { const handlerCode = "exports.handler = async () => ({ statusCode: 200 });"; const zipBuffer = buildMinimalZip('index.js', Buffer.from(handlerCode)); 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: zipBuffer }, Layers: [layerVersionArn], }) ); expect(response.Layers).toHaveLength(1); expect(response.Layers![0].Arn).toBe(layerVersionArn); }); it('should show layers in function configuration', async () => { const response = await lambda.send( new GetFunctionConfigurationCommand({ FunctionName: fnName }) ); expect(response.Layers).toHaveLength(1); expect(response.Layers![0].Arn).toBe(layerVersionArn); }); it('should update function to remove layers', async () => { const response = await lambda.send( new UpdateFunctionConfigurationCommand({ FunctionName: fnName, Layers: [], }) ); // After removing layers, the field should be empty or absent expect(response.Layers ?? []).toHaveLength(0); }); it('should update function to add layers back', async () => { const response = await lambda.send( new UpdateFunctionConfigurationCommand({ FunctionName: fnName, Layers: [layerVersionArn], }) ); expect(response.Layers).toHaveLength(1); expect(response.Layers![0].Arn).toBe(layerVersionArn); }); }); describe('Lambda Layers listing parameters', () => { let lambda: LambdaClient; let layerName: string; const zip = (marker: string) => buildMinimalZip('nodejs/node_modules/shared/index.js', Buffer.from(marker)); beforeAll(async () => { lambda = makeClient(LambdaClient); layerName = `compat-layer-params-${uniqueName()}`; // v1 nodejs20.x/x86_64, v2 python3.13/arm64, v3 nodejs20.x/arm64: every filter // combination below then selects a different subset of the same layer. await lambda.send( new PublishLayerVersionCommand({ LayerName: layerName, Content: { ZipFile: zip('v1') }, CompatibleRuntimes: ['nodejs20.x'], CompatibleArchitectures: ['x86_64'], }) ); await lambda.send( new PublishLayerVersionCommand({ LayerName: layerName, Content: { ZipFile: zip('v2') }, CompatibleRuntimes: ['python3.13'], CompatibleArchitectures: ['arm64'], }) ); await lambda.send( new PublishLayerVersionCommand({ LayerName: layerName, Content: { ZipFile: zip('v3') }, CompatibleRuntimes: ['nodejs20.x'], CompatibleArchitectures: ['arm64'], }) ); }); afterAll(async () => { for (const VersionNumber of [1, 2, 3]) { try { await lambda.send(new DeleteLayerVersionCommand({ LayerName: layerName, VersionNumber })); } catch { /* ignore */ } } }); it('filters listLayerVersions by CompatibleRuntime', async () => { const response = await lambda.send( new ListLayerVersionsCommand({ LayerName: layerName, CompatibleRuntime: 'python3.13' }) ); expect(response.LayerVersions?.map((v) => v.Version)).toEqual([2]); }); it('filters listLayerVersions by CompatibleArchitecture', async () => { const response = await lambda.send( new ListLayerVersionsCommand({ LayerName: layerName, CompatibleArchitecture: 'arm64' }) ); expect(response.LayerVersions?.map((v) => v.Version).sort()).toEqual([2, 3]); }); it('applies both filters together', async () => { const response = await lambda.send( new ListLayerVersionsCommand({ LayerName: layerName, CompatibleRuntime: 'nodejs20.x', CompatibleArchitecture: 'arm64', }) ); expect(response.LayerVersions?.map((v) => v.Version)).toEqual([3]); }); it('pages listLayerVersions with MaxItems and resumes from NextMarker', async () => { const seen: number[] = []; let marker: string | undefined; let pages = 0; do { const page = await lambda.send( new ListLayerVersionsCommand({ LayerName: layerName, MaxItems: 1, Marker: marker }) ); expect(page.LayerVersions).toHaveLength(1); seen.push(page.LayerVersions![0].Version!); marker = page.NextMarker; pages += 1; } while (marker && pages < 5); // The marker walks every version exactly once and clears on the last page. expect(marker).toBeFalsy(); expect(seen.sort()).toEqual([1, 2, 3]); }); it('reports the newest matching version as LatestMatchingVersion, not the newest overall', async () => { const python = await lambda.send( new ListLayersCommand({ CompatibleRuntime: 'python3.13' }) ); const node = await lambda.send(new ListLayersCommand({ CompatibleRuntime: 'nodejs20.x' })); expect( python.Layers?.find((l) => l.LayerName === layerName)?.LatestMatchingVersion?.Version ).toBe(2); expect( node.Layers?.find((l) => l.LayerName === layerName)?.LatestMatchingVersion?.Version ).toBe(3); }); it('drops a layer with no matching version out of listLayers', async () => { const response = await lambda.send(new ListLayersCommand({ CompatibleRuntime: 'java21' })); expect(response.Layers?.some((l) => l.LayerName === layerName)).toBe(false); }); it('rejects an unknown CompatibleRuntime with ValidationException', async () => { await expect( lambda.send(new ListLayersCommand({ CompatibleRuntime: 'bogus9.x' })) ).rejects.toMatchObject({ name: 'ValidationException' }); }); });