diff --git a/.env.example b/.env.example index 20c939f..693f03c 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 eb66b5e..394ec88 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 941a05e..d3303c5 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/package.json b/package.json index 4eb8b5b..f600dd5 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 7a78b4b..12a7106 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 5683975..c0f7bbb 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 82a653c..3a23055 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,13 @@ 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(); + 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; @@ -155,6 +165,14 @@ async function useSandboxTypes () { if (isOwner) { isOwner.valueOf(); } + if (defaultTemplate) { + defaultTemplate.templateID.length; + } + if (defaultTemplateBuildStatus) { + defaultTemplateBuildStatus.length; + } + defaultTemplateIsTyped.valueOf(); + defaultTemplateBuildStatusIsTyped.valueOf(); } void useSandboxTypes;