From 93911e129cfd65bab0a3a99b0e7ee5adb24584af Mon Sep 17 00:00:00 2001 From: Darin Kuntze Date: Mon, 2 Feb 2026 11:54:46 -0800 Subject: [PATCH 1/6] support for continuation headers --- docs/openapi/headers.yaml | 5 +++++ docs/openapi/list-api.yaml | 3 ++- docs/openapi/parameters.yaml | 7 +++++++ docs/openapi/responses.yaml | 3 +++ src/storage/object/list.js | 10 ++++++++-- src/utils/daCtx.js | 7 ++++++- src/utils/daResp.js | 4 ++++ test/index.test.js | 25 +++++++++++++++++++++++++ test/storage/object/list.test.js | 24 ++++++++++++++++++++++++ test/utils/daCtx.test.js | 24 ++++++++++++++++++++++++ 10 files changed, 108 insertions(+), 4 deletions(-) diff --git a/docs/openapi/headers.yaml b/docs/openapi/headers.yaml index 06a7565d..04061d86 100644 --- a/docs/openapi/headers.yaml +++ b/docs/openapi/headers.yaml @@ -8,3 +8,8 @@ location: description: The redirect location schema: type: string + +xDaContinuationToken: + description: Continuation token for fetching the next page of list results. + schema: + type: string diff --git a/docs/openapi/list-api.yaml b/docs/openapi/list-api.yaml index 2d682ee6..c486f368 100644 --- a/docs/openapi/list-api.yaml +++ b/docs/openapi/list-api.yaml @@ -9,10 +9,11 @@ list: - $ref: "./parameters.yaml#/orgParam" - $ref: "./parameters.yaml#/repoParam" - $ref: "./parameters.yaml#/pathParam" + - $ref: "./parameters.yaml#/continuationTokenParam" responses: '200': $ref: "./responses.yaml#/list/200" '400': $ref: "./responses.yaml#/400" '404': - $ref: "./responses.yaml#/404" \ No newline at end of file + $ref: "./responses.yaml#/404" diff --git a/docs/openapi/parameters.yaml b/docs/openapi/parameters.yaml index 059a6303..129e9ab0 100644 --- a/docs/openapi/parameters.yaml +++ b/docs/openapi/parameters.yaml @@ -26,6 +26,13 @@ pathParam: required: true schema: type: string +continuationTokenParam: + name: continuation-token + in: query + description: Continuation token for paginated list results. + required: false + schema: + type: string guidParam: name: guid in: path diff --git a/docs/openapi/responses.yaml b/docs/openapi/responses.yaml index c7611e92..be820643 100644 --- a/docs/openapi/responses.yaml +++ b/docs/openapi/responses.yaml @@ -35,6 +35,9 @@ source: list: '200': description: The list of sources + headers: + X-da-continuation-token: + $ref: "./headers.yaml#/xDaContinuationToken" content: application/json: schema: diff --git a/src/storage/object/list.js b/src/storage/object/list.js index a00ababf..b1e4ac8f 100644 --- a/src/storage/object/list.js +++ b/src/storage/object/list.js @@ -18,7 +18,7 @@ import getS3Config from '../utils/config.js'; import formatList from '../utils/list.js'; function buildInput({ - bucket, org, key, maxKeys, + bucket, org, key, maxKeys, continuationToken, }) { const input = { Bucket: bucket, @@ -26,6 +26,7 @@ function buildInput({ Delimiter: '/', }; if (maxKeys) input.MaxKeys = maxKeys; + if (continuationToken) input.ContinuationToken = continuationToken; return input; } @@ -33,7 +34,11 @@ export default async function listObjects(env, daCtx, maxKeys) { const config = getS3Config(env); const client = new S3Client(config); - const input = buildInput({ ...daCtx, maxKeys }); + const input = buildInput({ + ...daCtx, + maxKeys, + continuationToken: daCtx.continuationToken, + }); const command = new ListObjectsV2Command(input); try { const resp = await client.send(command); @@ -43,6 +48,7 @@ export default async function listObjects(env, daCtx, maxKeys) { body: JSON.stringify(body), status: resp.$metadata.httpStatusCode, contentType: resp.ContentType, + continuationToken: resp.NextContinuationToken, }; } catch (e) { return { body: '', status: 404 }; diff --git a/src/utils/daCtx.js b/src/utils/daCtx.js index 2964db7c..fdb0da07 100644 --- a/src/utils/daCtx.js +++ b/src/utils/daCtx.js @@ -18,7 +18,8 @@ import { getAclCtx, getUsers } from './auth.js'; * @returns {DaCtx} The Dark Alley Context. */ export default async function getDaCtx(req, env) { - let { pathname } = new URL(req.url); + const url = new URL(req.url); + let { pathname } = url; // Remove proxied api route if (pathname.startsWith('/api')) pathname = pathname.replace('/api', ''); @@ -38,6 +39,9 @@ export default async function getDaCtx(req, env) { // Extract conditional headers const ifMatch = req.headers?.get('if-match') || null; const ifNoneMatch = req.headers?.get('if-none-match') || null; + const continuationToken = url.searchParams.get('continuation-token') + || url.searchParams.get('continuationToken') + || null; // Set base details const daCtx = { @@ -53,6 +57,7 @@ export default async function getDaCtx(req, env) { ifMatch, ifNoneMatch, }, + continuationToken, }; // Sanitize the remaining path parts diff --git a/src/utils/daResp.js b/src/utils/daResp.js index 633972f4..c8d5b364 100644 --- a/src/utils/daResp.js +++ b/src/utils/daResp.js @@ -27,6 +27,7 @@ export default function daResp({ contentLength, metadata, etag, + continuationToken, }, ctx = null) { const headers = new Headers(); headers.append('Access-Control-Allow-Origin', '*'); @@ -48,6 +49,9 @@ export default function daResp({ if (etag) { headers.append('ETag', etag); } + if (continuationToken) { + headers.append('X-da-continuation-token', continuationToken); + } if (ctx?.aclCtx && status < 500) { headers.append('X-da-actions', `/${ctx.key}=${[...ctx.aclCtx.actionSet]}`); diff --git a/test/index.test.js b/test/index.test.js index 55580639..652f5099 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -90,6 +90,31 @@ describe('fetch', () => { const resp = await hnd.fetch({ method: 'GET', url: 'http://www.example.com/source/org/repo/file.html' }, {}); assert.strictEqual(resp.status, 500); }); + + it('should expose continuation token header for list responses', async () => { + const hnd = await esmock('../src/index.js', { + '../src/utils/daCtx.js': { + default: async () => ({ + authorized: true, + users: [{ email: 'test@example.com' }], + path: '/list/org/repo/path', + key: 'repo/path', + }), + }, + '../src/handlers/get.js': { + default: async () => ({ + status: 200, + body: '[]', + contentType: 'application/json', + continuationToken: 'next-token', + }), + }, + }); + + const resp = await hnd.fetch({ method: 'GET', url: 'http://www.example.com/list/org/repo/path' }, {}); + assert.strictEqual(resp.status, 200); + assert.strictEqual(resp.headers.get('X-da-continuation-token'), 'next-token'); + }); }); describe('invalid routes', () => { diff --git a/test/storage/object/list.test.js b/test/storage/object/list.test.js index 04cf3cae..8668282b 100644 --- a/test/storage/object/list.test.js +++ b/test/storage/object/list.test.js @@ -56,4 +56,28 @@ describe('List Objects', () => { const data = JSON.parse(resp.body); assert.strictEqual(data.length, 2, 'Should only return 2 items'); }); + + it('passes continuation token and returns next token', async () => { + s3Mock.on(ListObjectsV2Command, { + Bucket: 'rt-bkt', + Prefix: 'acme/wknd/', + Delimiter: '/', + ContinuationToken: 'prev-token', + }).resolves({ + $metadata: { httpStatusCode: 200 }, + Contents: [Contents[0]], + NextContinuationToken: 'next-token', + }); + + const daCtx = { + bucket: 'rt-bkt', + org: 'acme', + key: 'wknd', + continuationToken: 'prev-token', + }; + const resp = await listObjects({}, daCtx); + const data = JSON.parse(resp.body); + assert.strictEqual(data.length, 1, 'Should only return 1 item'); + assert.strictEqual(resp.continuationToken, 'next-token'); + }); }); diff --git a/test/utils/daCtx.test.js b/test/utils/daCtx.test.js index 4ecdd049..2a44ef1a 100644 --- a/test/utils/daCtx.test.js +++ b/test/utils/daCtx.test.js @@ -217,4 +217,28 @@ describe('DA context', () => { assert.strictEqual(daCtx.conditionalHeaders.ifNoneMatch, null); }); }); + + describe('Continuation token', async () => { + it('should extract continuation-token query param', async () => { + const req = { + url: 'http://localhost:8787/list/org/site/path?continuation-token=token123', + headers: { + get: () => null, + }, + }; + const daCtx = await getDaCtx(req, env); + assert.strictEqual(daCtx.continuationToken, 'token123'); + }); + + it('should default continuation token to null', async () => { + const req = { + url: 'http://localhost:8787/list/org/site/path', + headers: { + get: () => null, + }, + }; + const daCtx = await getDaCtx(req, env); + assert.strictEqual(daCtx.continuationToken, null); + }); + }); }); From 7d2183f5236ec98f890df619697a5d1d1c4a8380 Mon Sep 17 00:00:00 2001 From: Darin Kuntze Date: Mon, 2 Feb 2026 13:18:33 -0800 Subject: [PATCH 2/6] make compatible with browser --- src/utils/daResp.js | 2 +- test/utils/daResp.test.js | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/utils/daResp.js b/src/utils/daResp.js index c8d5b364..53ee6607 100644 --- a/src/utils/daResp.js +++ b/src/utils/daResp.js @@ -33,7 +33,7 @@ export default function daResp({ headers.append('Access-Control-Allow-Origin', '*'); headers.append('Access-Control-Allow-Methods', 'HEAD, GET, PUT, POST, DELETE'); headers.append('Access-Control-Allow-Headers', '*'); - headers.append('Access-Control-Expose-Headers', 'X-da-actions, X-da-child-actions, X-da-acltrace, X-da-id, ETag'); + headers.append('Access-Control-Expose-Headers', 'X-da-actions, X-da-child-actions, X-da-acltrace, X-da-id, X-da-continuation-token, ETag'); headers.append('Content-Type', contentType); if (contentLength) { headers.append('Content-Length', contentLength); diff --git a/test/utils/daResp.test.js b/test/utils/daResp.test.js index 78781197..34deede2 100644 --- a/test/utils/daResp.test.js +++ b/test/utils/daResp.test.js @@ -28,7 +28,7 @@ describe('DA Resp', () => { assert.strictEqual('*', resp.headers.get('Access-Control-Allow-Origin')); assert.strictEqual('HEAD, GET, PUT, POST, DELETE', resp.headers.get('Access-Control-Allow-Methods')); assert.strictEqual('*', resp.headers.get('Access-Control-Allow-Headers')); - assert.strictEqual('X-da-actions, X-da-child-actions, X-da-acltrace, X-da-id, ETag', resp.headers.get('Access-Control-Expose-Headers')); + assert.strictEqual('X-da-actions, X-da-child-actions, X-da-acltrace, X-da-id, X-da-continuation-token, ETag', resp.headers.get('Access-Control-Expose-Headers')); assert.strictEqual('text/plain', resp.headers.get('Content-Type')); assert.strictEqual('777', resp.headers.get('Content-Length')); assert.strictEqual('/foo/bar.html=read,write', resp.headers.get('X-da-actions')); @@ -49,7 +49,7 @@ describe('DA Resp', () => { assert.strictEqual('*', resp.headers.get('Access-Control-Allow-Origin')); assert.strictEqual('HEAD, GET, PUT, POST, DELETE', resp.headers.get('Access-Control-Allow-Methods')); assert.strictEqual('*', resp.headers.get('Access-Control-Allow-Headers')); - assert.strictEqual('X-da-actions, X-da-child-actions, X-da-acltrace, X-da-id, ETag', resp.headers.get('Access-Control-Expose-Headers')); + assert.strictEqual('X-da-actions, X-da-child-actions, X-da-acltrace, X-da-id, X-da-continuation-token, ETag', resp.headers.get('Access-Control-Expose-Headers')); assert.strictEqual('application/json', resp.headers.get('Content-Type')); assert(!resp.headers.get('Content-Length')); assert.strictEqual('/foo/blah.html=read', resp.headers.get('X-da-actions')); @@ -76,7 +76,7 @@ describe('DA Resp', () => { const ctx = { key: 'foo/bar.html', aclCtx }; const resp = daResp({ status: 200, body: 'foobar' }, ctx); assert.strictEqual(200, resp.status); - assert.strictEqual('X-da-actions, X-da-child-actions, X-da-acltrace, X-da-id, ETag', resp.headers.get('Access-Control-Expose-Headers')); + assert.strictEqual('X-da-actions, X-da-child-actions, X-da-acltrace, X-da-id, X-da-continuation-token, ETag', resp.headers.get('Access-Control-Expose-Headers')); assert.strictEqual('/haha/hoho/**=read,write', resp.headers.get('X-da-child-actions')); }); }); From 92ae943f5375970c15dc4fea48d7e35e6d7c21b7 Mon Sep 17 00:00:00 2001 From: Darin Kuntze Date: Mon, 2 Feb 2026 14:21:33 -0800 Subject: [PATCH 3/6] clean up header name --- docs/openapi/headers.yaml | 2 +- docs/openapi/responses.yaml | 4 ++-- src/utils/daResp.js | 4 ++-- test/index.test.js | 2 +- test/utils/daResp.test.js | 6 +++--- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/openapi/headers.yaml b/docs/openapi/headers.yaml index 04061d86..e41c1ac7 100644 --- a/docs/openapi/headers.yaml +++ b/docs/openapi/headers.yaml @@ -9,7 +9,7 @@ location: schema: type: string -xDaContinuationToken: +daContinuationToken: description: Continuation token for fetching the next page of list results. schema: type: string diff --git a/docs/openapi/responses.yaml b/docs/openapi/responses.yaml index be820643..3811751e 100644 --- a/docs/openapi/responses.yaml +++ b/docs/openapi/responses.yaml @@ -36,8 +36,8 @@ list: '200': description: The list of sources headers: - X-da-continuation-token: - $ref: "./headers.yaml#/xDaContinuationToken" + da-continuation-token: + $ref: "./headers.yaml#/daContinuationToken" content: application/json: schema: diff --git a/src/utils/daResp.js b/src/utils/daResp.js index 53ee6607..fde39645 100644 --- a/src/utils/daResp.js +++ b/src/utils/daResp.js @@ -33,7 +33,7 @@ export default function daResp({ headers.append('Access-Control-Allow-Origin', '*'); headers.append('Access-Control-Allow-Methods', 'HEAD, GET, PUT, POST, DELETE'); headers.append('Access-Control-Allow-Headers', '*'); - headers.append('Access-Control-Expose-Headers', 'X-da-actions, X-da-child-actions, X-da-acltrace, X-da-id, X-da-continuation-token, ETag'); + headers.append('Access-Control-Expose-Headers', 'X-da-actions, X-da-child-actions, X-da-acltrace, X-da-id, da-continuation-token, ETag'); headers.append('Content-Type', contentType); if (contentLength) { headers.append('Content-Length', contentLength); @@ -50,7 +50,7 @@ export default function daResp({ headers.append('ETag', etag); } if (continuationToken) { - headers.append('X-da-continuation-token', continuationToken); + headers.append('da-continuation-token', continuationToken); } if (ctx?.aclCtx && status < 500) { diff --git a/test/index.test.js b/test/index.test.js index 652f5099..529add43 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -113,7 +113,7 @@ describe('fetch', () => { const resp = await hnd.fetch({ method: 'GET', url: 'http://www.example.com/list/org/repo/path' }, {}); assert.strictEqual(resp.status, 200); - assert.strictEqual(resp.headers.get('X-da-continuation-token'), 'next-token'); + assert.strictEqual(resp.headers.get('da-continuation-token'), 'next-token'); }); }); diff --git a/test/utils/daResp.test.js b/test/utils/daResp.test.js index 34deede2..25134b84 100644 --- a/test/utils/daResp.test.js +++ b/test/utils/daResp.test.js @@ -28,7 +28,7 @@ describe('DA Resp', () => { assert.strictEqual('*', resp.headers.get('Access-Control-Allow-Origin')); assert.strictEqual('HEAD, GET, PUT, POST, DELETE', resp.headers.get('Access-Control-Allow-Methods')); assert.strictEqual('*', resp.headers.get('Access-Control-Allow-Headers')); - assert.strictEqual('X-da-actions, X-da-child-actions, X-da-acltrace, X-da-id, X-da-continuation-token, ETag', resp.headers.get('Access-Control-Expose-Headers')); + assert.strictEqual('X-da-actions, X-da-child-actions, X-da-acltrace, X-da-id, da-continuation-token, ETag', resp.headers.get('Access-Control-Expose-Headers')); assert.strictEqual('text/plain', resp.headers.get('Content-Type')); assert.strictEqual('777', resp.headers.get('Content-Length')); assert.strictEqual('/foo/bar.html=read,write', resp.headers.get('X-da-actions')); @@ -49,7 +49,7 @@ describe('DA Resp', () => { assert.strictEqual('*', resp.headers.get('Access-Control-Allow-Origin')); assert.strictEqual('HEAD, GET, PUT, POST, DELETE', resp.headers.get('Access-Control-Allow-Methods')); assert.strictEqual('*', resp.headers.get('Access-Control-Allow-Headers')); - assert.strictEqual('X-da-actions, X-da-child-actions, X-da-acltrace, X-da-id, X-da-continuation-token, ETag', resp.headers.get('Access-Control-Expose-Headers')); + assert.strictEqual('X-da-actions, X-da-child-actions, X-da-acltrace, X-da-id, da-continuation-token, ETag', resp.headers.get('Access-Control-Expose-Headers')); assert.strictEqual('application/json', resp.headers.get('Content-Type')); assert(!resp.headers.get('Content-Length')); assert.strictEqual('/foo/blah.html=read', resp.headers.get('X-da-actions')); @@ -76,7 +76,7 @@ describe('DA Resp', () => { const ctx = { key: 'foo/bar.html', aclCtx }; const resp = daResp({ status: 200, body: 'foobar' }, ctx); assert.strictEqual(200, resp.status); - assert.strictEqual('X-da-actions, X-da-child-actions, X-da-acltrace, X-da-id, X-da-continuation-token, ETag', resp.headers.get('Access-Control-Expose-Headers')); + assert.strictEqual('X-da-actions, X-da-child-actions, X-da-acltrace, X-da-id, da-continuation-token, ETag', resp.headers.get('Access-Control-Expose-Headers')); assert.strictEqual('/haha/hoho/**=read,write', resp.headers.get('X-da-child-actions')); }); }); From 80144c283f4dc7491f28c267b5b829a196fd1439 Mon Sep 17 00:00:00 2001 From: Darin Kuntze Date: Mon, 9 Feb 2026 13:05:30 -0600 Subject: [PATCH 4/6] Use header-only continuation token in daCtx --- src/storage/object/list.js | 1 - src/utils/daCtx.js | 5 +++-- test/utils/daCtx.test.js | 32 ++++++++++++++++++++++++++++++-- 3 files changed, 33 insertions(+), 5 deletions(-) diff --git a/src/storage/object/list.js b/src/storage/object/list.js index b1e4ac8f..142d0154 100644 --- a/src/storage/object/list.js +++ b/src/storage/object/list.js @@ -37,7 +37,6 @@ export default async function listObjects(env, daCtx, maxKeys) { const input = buildInput({ ...daCtx, maxKeys, - continuationToken: daCtx.continuationToken, }); const command = new ListObjectsV2Command(input); try { diff --git a/src/utils/daCtx.js b/src/utils/daCtx.js index fdb0da07..8e9736e2 100644 --- a/src/utils/daCtx.js +++ b/src/utils/daCtx.js @@ -39,8 +39,9 @@ export default async function getDaCtx(req, env) { // Extract conditional headers const ifMatch = req.headers?.get('if-match') || null; const ifNoneMatch = req.headers?.get('if-none-match') || null; - const continuationToken = url.searchParams.get('continuation-token') - || url.searchParams.get('continuationToken') + const continuationToken = req.headers?.get('da-continuation-token') + || req.headers?.get('continuation-token') + || req.headers?.get('continuationToken') || null; // Set base details diff --git a/test/utils/daCtx.test.js b/test/utils/daCtx.test.js index 2a44ef1a..a29f600a 100644 --- a/test/utils/daCtx.test.js +++ b/test/utils/daCtx.test.js @@ -219,7 +219,35 @@ describe('DA context', () => { }); describe('Continuation token', async () => { - it('should extract continuation-token query param', async () => { + it('should extract da-continuation-token header', async () => { + const req = { + url: 'http://localhost:8787/list/org/site/path', + headers: { + get: (name) => { + if (name === 'da-continuation-token') return 'header-token'; + return null; + }, + }, + }; + const daCtx = await getDaCtx(req, env); + assert.strictEqual(daCtx.continuationToken, 'header-token'); + }); + + it('should ignore continuation-token query param when header is present', async () => { + const req = { + url: 'http://localhost:8787/list/org/site/path?continuation-token=query-token', + headers: { + get: (name) => { + if (name === 'da-continuation-token') return 'header-token'; + return null; + }, + }, + }; + const daCtx = await getDaCtx(req, env); + assert.strictEqual(daCtx.continuationToken, 'header-token'); + }); + + it('should ignore continuation-token query param', async () => { const req = { url: 'http://localhost:8787/list/org/site/path?continuation-token=token123', headers: { @@ -227,7 +255,7 @@ describe('DA context', () => { }, }; const daCtx = await getDaCtx(req, env); - assert.strictEqual(daCtx.continuationToken, 'token123'); + assert.strictEqual(daCtx.continuationToken, null); }); it('should default continuation token to null', async () => { From ca89de3406db63f362d05beaf22972da9085d283 Mon Sep 17 00:00:00 2001 From: Darin Kuntze Date: Mon, 9 Feb 2026 13:29:46 -0600 Subject: [PATCH 5/6] Use da-continuation-token header only --- docs/openapi/parameters.yaml | 6 +++--- src/utils/daCtx.js | 5 +---- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/docs/openapi/parameters.yaml b/docs/openapi/parameters.yaml index 129e9ab0..de0001f6 100644 --- a/docs/openapi/parameters.yaml +++ b/docs/openapi/parameters.yaml @@ -27,9 +27,9 @@ pathParam: schema: type: string continuationTokenParam: - name: continuation-token - in: query - description: Continuation token for paginated list results. + name: da-continuation-token + in: header + description: Continuation token from the previous list response header. required: false schema: type: string diff --git a/src/utils/daCtx.js b/src/utils/daCtx.js index 8e9736e2..2475dce2 100644 --- a/src/utils/daCtx.js +++ b/src/utils/daCtx.js @@ -39,10 +39,7 @@ export default async function getDaCtx(req, env) { // Extract conditional headers const ifMatch = req.headers?.get('if-match') || null; const ifNoneMatch = req.headers?.get('if-none-match') || null; - const continuationToken = req.headers?.get('da-continuation-token') - || req.headers?.get('continuation-token') - || req.headers?.get('continuationToken') - || null; + const continuationToken = req.headers?.get('da-continuation-token') || null; // Set base details const daCtx = { From dbfe7a9818e1d81ef1bab854df31e1131349ab71 Mon Sep 17 00:00:00 2001 From: Darin Kuntze Date: Mon, 9 Feb 2026 14:58:28 -0600 Subject: [PATCH 6/6] add robust pagination contract --- src/storage/object/list.js | 7 ++++- test/storage/object/list.test.js | 47 ++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/src/storage/object/list.js b/src/storage/object/list.js index 142d0154..72860f26 100644 --- a/src/storage/object/list.js +++ b/src/storage/object/list.js @@ -43,11 +43,16 @@ export default async function listObjects(env, daCtx, maxKeys) { const resp = await client.send(command); // console.log(resp); const body = formatList(resp); + const nextContinuationToken = resp.IsTruncated + && resp.NextContinuationToken + && resp.NextContinuationToken !== daCtx.continuationToken + ? resp.NextContinuationToken + : undefined; return { body: JSON.stringify(body), status: resp.$metadata.httpStatusCode, contentType: resp.ContentType, - continuationToken: resp.NextContinuationToken, + continuationToken: nextContinuationToken, }; } catch (e) { return { body: '', status: 404 }; diff --git a/test/storage/object/list.test.js b/test/storage/object/list.test.js index 8668282b..993f2a88 100644 --- a/test/storage/object/list.test.js +++ b/test/storage/object/list.test.js @@ -66,6 +66,7 @@ describe('List Objects', () => { }).resolves({ $metadata: { httpStatusCode: 200 }, Contents: [Contents[0]], + IsTruncated: true, NextContinuationToken: 'next-token', }); @@ -80,4 +81,50 @@ describe('List Objects', () => { assert.strictEqual(data.length, 1, 'Should only return 1 item'); assert.strictEqual(resp.continuationToken, 'next-token'); }); + + it('does not return continuation token on terminal page', async () => { + s3Mock.on(ListObjectsV2Command, { + Bucket: 'rt-bkt', + Prefix: 'acme/wknd/', + Delimiter: '/', + ContinuationToken: 'prev-token', + }).resolves({ + $metadata: { httpStatusCode: 200 }, + Contents: [Contents[1]], + IsTruncated: false, + NextContinuationToken: 'should-not-be-returned', + }); + + const daCtx = { + bucket: 'rt-bkt', + org: 'acme', + key: 'wknd', + continuationToken: 'prev-token', + }; + const resp = await listObjects({}, daCtx); + assert.strictEqual(resp.continuationToken, undefined); + }); + + it('does not return same continuation token again', async () => { + s3Mock.on(ListObjectsV2Command, { + Bucket: 'rt-bkt', + Prefix: 'acme/wknd/', + Delimiter: '/', + ContinuationToken: 'prev-token', + }).resolves({ + $metadata: { httpStatusCode: 200 }, + Contents: [Contents[2]], + IsTruncated: true, + NextContinuationToken: 'prev-token', + }); + + const daCtx = { + bucket: 'rt-bkt', + org: 'acme', + key: 'wknd', + continuationToken: 'prev-token', + }; + const resp = await listObjects({}, daCtx); + assert.strictEqual(resp.continuationToken, undefined); + }); });