From 19f3bd89d167122e99b0f05985fc47db02e8c34d Mon Sep 17 00:00:00 2001 From: Miclle Zheng Date: Tue, 28 Jul 2026 17:11:46 +0800 Subject: [PATCH 1/2] feat(sandbox): type and verify default templates Expose the existing default-template listing as TemplateInfo[] and add focused, live, and example coverage with safe read-only defaults. --- .env.example | 9 ++++- examples/sandbox_templates.js | 16 +++++++- index.d.ts | 2 +- package.json | 2 + test/sandbox_integration.test.js | 17 +++++++++ test/sandbox_template.test.js | 63 ++++++++++++++++++++++++++++++++ test/sandbox_types.ts | 10 +++++ 7 files changed, 116 insertions(+), 3 deletions(-) diff --git a/.env.example b/.env.example index 20c939f5..693f03c8 100644 --- a/.env.example +++ b/.env.example @@ -1,7 +1,11 @@ # Copy this file to .env in the project root before running sandbox examples # or sandbox integration tests. +# +# Read-only default-template validation: +# npm run test:sandbox:integration:default-templates +# node examples/sandbox_templates.js -# Sandbox API key auth. +# Required for sandbox examples and integration tests. QINIU_SANDBOX_API_KEY= # Optional custom endpoint. @@ -10,6 +14,9 @@ QINIU_SANDBOX_ENDPOINT= # Optional template alias or ID. Defaults to base. QINIU_SANDBOX_TEMPLATE=base +# Opt in to create and delete a template in examples/sandbox_templates.js. +QINIU_SANDBOX_TEMPLATE_MUTATION_EXAMPLE=false + # Required only for injection-rule and Kodo resource examples. QINIU_SANDBOX_ACCESS_KEY= QINIU_SANDBOX_SECRET_KEY= diff --git a/examples/sandbox_templates.js b/examples/sandbox_templates.js index eb66b5ea..394ec88a 100644 --- a/examples/sandbox_templates.js +++ b/examples/sandbox_templates.js @@ -10,12 +10,19 @@ runExample(() => { const client = sandboxClient({ accessToken: env('QINIU_SANDBOX_ACCESS_TOKEN') }); + const allowMutation = env('QINIU_SANDBOX_TEMPLATE_MUTATION_EXAMPLE') === 'true'; const templateName = `nodejs-sdk-example-${Date.now()}`; let templateID; let buildID; return client.listDefaultTemplates().then(defaultTemplates => { - console.log('Default templates:', Array.isArray(defaultTemplates) ? defaultTemplates.length : defaultTemplates); + console.log('Default templates:', Array.isArray(defaultTemplates) + ? defaultTemplates.map(template => ({ + templateID: template.templateID, + names: template.names, + buildStatus: template.buildStatus + })) + : defaultTemplates); return client.listTemplates({ limit: 10 }); }).then(templates => { console.log('Templates:', Array.isArray(templates) ? templates.map(item => item.templateID || item.template_id || item.name) : templates); @@ -34,6 +41,10 @@ runExample(() => { }); }); }).then(() => { + if (!allowMutation) { + console.log('Skipping template mutation; set QINIU_SANDBOX_TEMPLATE_MUTATION_EXAMPLE=true to enable it.'); + return null; + } const advanced = qiniu.sandbox.Template() .fromDockerfile('FROM node:22\nWORKDIR /app\nENV NODE_ENV=production\nRUN npm ci') .copyItems([{ src: 'package.json', dest: '/app/' }]) @@ -55,6 +66,9 @@ runExample(() => { tags: ['nodejs-sdk-example'] }); }).then(result => { + if (!result) { + return null; + } templateID = result.templateID || result.template_id || result.id; buildID = result.buildID || result.build_id; console.log('Template created:', templateID, 'build:', buildID); diff --git a/index.d.ts b/index.d.ts index 941a05e1..8ee8c63a 100644 --- a/index.d.ts +++ b/index.d.ts @@ -491,7 +491,7 @@ export declare namespace sandbox { createTemplateV3(options?: any): Promise; createTemplateV2(options?: any): Promise; getTemplateFiles(templateID: string, hash: string): Promise; - listDefaultTemplates(): Promise; + listDefaultTemplates(): Promise; listTemplates(options?: any): Promise; getTemplate(templateID: string, options?: any): Promise; deleteTemplate(templateID: string): Promise; diff --git a/package.json b/package.json index 4eb8b5bf..f600dd5c 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,8 @@ "check-type": "tsc --noEmit", "test": "NODE_ENV=test mocha -t 300000 --retries 3", "test:sandbox": "NODE_ENV=test mocha -t 300000 --retries 3 test/sandbox_client.test.js test/sandbox_commands.test.js test/sandbox_facade.test.js test/sandbox_filesystem.test.js test/sandbox_git.test.js test/sandbox_pty.test.js test/sandbox_template.test.js", + "test:sandbox:default-templates": "NODE_ENV=test mocha -t 300000 test/sandbox_template.test.js --grep 'default templates'", + "test:sandbox:integration:default-templates": "QINIU_SANDBOX_REQUIRE_API_KEY=true NODE_ENV=test mocha -t 300000 test/sandbox_integration.test.js --grep 'lists default templates'", "cover": "nyc npm run test", "report": "nyc report --reporter=html", "lint": "eslint ." diff --git a/test/sandbox_integration.test.js b/test/sandbox_integration.test.js index 7a78b4b8..12a71067 100644 --- a/test/sandbox_integration.test.js +++ b/test/sandbox_integration.test.js @@ -64,6 +64,9 @@ function integrationConfig () { } const config = integrationConfig(); +if (process.env.QINIU_SANDBOX_REQUIRE_API_KEY === 'true' && !config.apiKey) { + throw new Error('QINIU_SANDBOX_API_KEY is required for this integration test'); +} const describeIntegration = config.apiKey ? describe : describe.skip; function authedGitUrl (repoUrl, username, password) { @@ -434,6 +437,20 @@ describeIntegration('sandbox integration', function () { return null; }); }); + + it('lists default templates', function () { + const client = sandboxClient(); + + return client.listDefaultTemplates().then(templates => { + Array.isArray(templates).should.eql(true); + templates.length.should.be.above(0); + templates.forEach(template => { + should(template.templateID).be.String(); + Array.isArray(template.names).should.eql(true); + }); + integrationLog('default template integration completed', templates.length); + }); + }); }); if (!config.apiKey) { diff --git a/test/sandbox_template.test.js b/test/sandbox_template.test.js index 56839754..c0f7bbb3 100644 --- a/test/sandbox_template.test.js +++ b/test/sandbox_template.test.js @@ -7,6 +7,69 @@ const { } = require('./sandbox_helpers'); describe('test sandbox template module', function () { + it('lists default templates with their public fields', function () { + return startServer((req, res) => { + res.setHeader('Content-Type', 'application/json'); + res.statusCode = 200; + res.end(JSON.stringify([{ + templateID: 'base', + public: true, + aliases: ['base'], + names: ['qiniu/base'], + buildStatus: 'ready' + }])); + }).then(fixture => { + const client = new qiniu.sandbox.SandboxClient({ + apiKey: 'sandbox-key', + endpoint: fixture.endpoint + }); + + return client.listDefaultTemplates().then(templates => { + templates.should.eql([{ + templateID: 'base', + public: true, + aliases: ['base'], + names: ['qiniu/base'], + buildStatus: 'ready' + }]); + fixture.requests.length.should.eql(1); + fixture.requests[0].method.should.eql('GET'); + fixture.requests[0].url.should.eql('/default-templates'); + fixture.requests[0].headers['x-api-key'].should.eql('sandbox-key'); + }).then(() => closeServer(fixture.server), err => { + return closeServer(fixture.server).then(() => { + throw err; + }); + }); + }); + }); + + it('surfaces errors from the default templates API', function () { + return startServer((req, res) => { + res.setHeader('Content-Type', 'application/json'); + res.statusCode = 401; + res.end(JSON.stringify({ message: 'unauthorized' })); + }).then(fixture => { + const client = new qiniu.sandbox.SandboxClient({ + apiKey: 'invalid-key', + endpoint: fixture.endpoint + }); + + return client.listDefaultTemplates().then(() => { + throw new Error('expected listDefaultTemplates to fail'); + }, err => { + err.name.should.eql('SandboxError'); + err.message.should.eql('Sandbox API request failed with status 401: unauthorized'); + err.response.statusCode.should.eql(401); + err.data.should.eql({ message: 'unauthorized' }); + }).then(() => closeServer(fixture.server), err => { + return closeServer(fixture.server).then(() => { + throw err; + }); + }); + }); + }); + it('maps template, build, tag, and access-token APIs', function () { return startServer((req, res) => { res.setHeader('Content-Type', 'application/json'); diff --git a/test/sandbox_types.ts b/test/sandbox_types.ts index 82a653c5..2d101efd 100644 --- a/test/sandbox_types.ts +++ b/test/sandbox_types.ts @@ -1,5 +1,8 @@ import * as qiniu from '../index'; +type IsAny = 0 extends (1 & T) ? true : false; +type AssertFalse = T; + async function useSandboxTypes () { const client = new qiniu.sandbox.SandboxClient({ endpoint: 'https://sandbox.example.com', @@ -142,6 +145,9 @@ async function useSandboxTypes () { const templateInfo: qiniu.sandbox.TemplateWithBuilds = await client.getTemplate('typed-template'); const templateNames: string[] = templateInfo.names; const isOwner: boolean = templateInfo.isOwner; + const defaultTemplates = await client.listDefaultTemplates(); + const defaultTemplatesAreTyped: AssertFalse> = false; + const defaultTemplate: qiniu.sandbox.TemplateInfo | undefined = defaultTemplates[0]; qiniu.Sandbox.list({ client, query: { template: templateNames } }); qiniu.CommandExitError.name; bytes.length; @@ -155,6 +161,10 @@ async function useSandboxTypes () { if (isOwner) { isOwner.valueOf(); } + if (defaultTemplate) { + defaultTemplate.templateID.length; + } + defaultTemplatesAreTyped.valueOf(); } void useSandboxTypes; From abf9f7385ea3e0bf232833d09641cdbb63d1940c Mon Sep 17 00:00:00 2001 From: Miclle Zheng Date: Tue, 28 Jul 2026 17:25:21 +0800 Subject: [PATCH 2/2] fix(sandbox): type default template status Add a dedicated default-template response type and assert both element and build-status types to prevent any regressions. --- index.d.ts | 8 +++++++- test/sandbox_types.ts | 14 +++++++++++--- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/index.d.ts b/index.d.ts index 8ee8c63a..d3303c5f 100644 --- a/index.d.ts +++ b/index.d.ts @@ -143,6 +143,12 @@ export declare namespace sandbox { [key: string]: any; } + type TemplateBuildStatus = 'building' | 'waiting' | 'ready' | 'error' | 'uploaded'; + + interface DefaultTemplateInfo extends TemplateInfo { + buildStatus: TemplateBuildStatus; + } + interface TemplateWithBuilds extends TemplateInfo { isOwner: boolean; builds: any[]; @@ -491,7 +497,7 @@ export declare namespace sandbox { createTemplateV3(options?: any): Promise; createTemplateV2(options?: any): Promise; getTemplateFiles(templateID: string, hash: string): Promise; - listDefaultTemplates(): Promise; + listDefaultTemplates(): Promise; listTemplates(options?: any): Promise; getTemplate(templateID: string, options?: any): Promise; deleteTemplate(templateID: string): Promise; diff --git a/test/sandbox_types.ts b/test/sandbox_types.ts index 2d101efd..3a230556 100644 --- a/test/sandbox_types.ts +++ b/test/sandbox_types.ts @@ -146,8 +146,12 @@ async function useSandboxTypes () { const templateNames: string[] = templateInfo.names; const isOwner: boolean = templateInfo.isOwner; const defaultTemplates = await client.listDefaultTemplates(); - const defaultTemplatesAreTyped: AssertFalse> = false; - const defaultTemplate: qiniu.sandbox.TemplateInfo | undefined = defaultTemplates[0]; + type DefaultTemplate = typeof defaultTemplates[number]; + const defaultTemplateIsTyped: AssertFalse> = false; + const defaultTemplateBuildStatusIsTyped: AssertFalse> = false; + const defaultTemplate: qiniu.sandbox.DefaultTemplateInfo | undefined = defaultTemplates[0]; + const defaultTemplateBuildStatus: qiniu.sandbox.TemplateBuildStatus | undefined = + defaultTemplate && defaultTemplate.buildStatus; qiniu.Sandbox.list({ client, query: { template: templateNames } }); qiniu.CommandExitError.name; bytes.length; @@ -164,7 +168,11 @@ async function useSandboxTypes () { if (defaultTemplate) { defaultTemplate.templateID.length; } - defaultTemplatesAreTyped.valueOf(); + if (defaultTemplateBuildStatus) { + defaultTemplateBuildStatus.length; + } + defaultTemplateIsTyped.valueOf(); + defaultTemplateBuildStatusIsTyped.valueOf(); } void useSandboxTypes;