diff --git a/package.json b/package.json index 41d85b7..48cddbc 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "test:deploy": "mocha 'tests/unit/commands/deploy.test.mjs'", "test:file": "mocha 'tests/unit/commands/file.test.mjs'", "test:page": "mocha 'tests/unit/commands/page.test.mjs'", + "test:redirect": "mocha 'tests/unit/commands/redirect.test.mjs'", "lint:cli": "eslint --config .eslintrc.js cli.js", "lint:src": "eslint --config .eslintrc.js src", "lint:tests": "eslint --config tests/.eslintrc.js tests", diff --git a/src/commands/redirect.js b/src/commands/redirect.js index 2828d7c..c373278 100644 --- a/src/commands/redirect.js +++ b/src/commands/redirect.js @@ -76,15 +76,21 @@ const command = { throw new Error('Operation cancelled'); } - if (!await config.fromArgs(args)) { + const context = { + config: this.config || config, + client: this.client || (() => client(config)) + }; + + if (!await context.config.fromArgs(args)) { process.exit(1); } - const quant = client(config); + const quant = context.client(context.config); + const status = args.status || 302; try { - await quant.redirect(args.from, args.to, null, args.status); - return `Created redirect from ${args.from} to ${args.to} (${args.status})`; + await quant.redirect(args.from, args.to, null, status); + return `Created redirect from ${args.from} to ${args.to} (${status})`; } catch (err) { if (isMD5Match(err)) { return `Skipped redirect from ${args.from} to ${args.to} (already exists)`; diff --git a/tests/mocks/quant-client.mjs b/tests/mocks/quant-client.mjs index f9a3906..5065e26 100644 --- a/tests/mocks/quant-client.mjs +++ b/tests/mocks/quant-client.mjs @@ -116,6 +116,18 @@ export default function (_config) { headers: { 'Quant-Url': url } }); return { success: true }; + }, + + redirect: async function(from, to, _headers = null, status = 302) { + history.post.push({ + url: '/redirect', + headers: { + 'Quant-From-Url': from, + 'Quant-To-Url': to, + 'Quant-Status': status + } + }); + return { success: true, uuid: 'mock-uuid-123' }; } }; diff --git a/tests/unit/commands/redirect.test.mjs b/tests/unit/commands/redirect.test.mjs new file mode 100644 index 0000000..fd58f2e --- /dev/null +++ b/tests/unit/commands/redirect.test.mjs @@ -0,0 +1,185 @@ +import { expect } from 'chai'; +import sinon from 'sinon'; +import _fs from 'fs'; +import _path from 'path'; +import mockClient from '../../mocks/quant-client.mjs'; + +const redirect = (await import('../../../src/commands/redirect.js')).default; +const config = (await import('../../../src/config.js')).default; + +describe('Redirect Command', () => { + let mockConfig; + let mockClientInstance; + + beforeEach(() => { + // Reset config state + config.set({}); + + // Create mock config + mockConfig = { + set: sinon.stub(), + get: (key) => ({ + endpoint: 'mock://api.quantcdn.io/v1', + clientid: 'test-client', + project: 'test-project', + token: 'test-token' + })[key], + fromArgs: sinon.stub().resolves(true), + save: sinon.stub() + }; + + // Create mock client instance + mockClientInstance = mockClient(mockConfig); + + // Stub console methods + sinon.stub(console, 'log'); + sinon.stub(console, 'error'); + }); + + afterEach(() => { + sinon.restore(); + }); + + describe('handler', () => { + it('should create a redirect', async () => { + const context = { + config: mockConfig, + client: () => mockClientInstance + }; + + const args = { + from: '/old-path', + to: '/new-path', + status: 301, + clientid: 'test-client', + project: 'test-project', + token: 'test-token' + }; + + const result = await redirect.handler.call(context, args); + expect(result).to.equal('Created redirect from /old-path to /new-path (301)'); + expect(mockClientInstance._history.post.length).to.equal(1); + }); + + it('should handle MD5 match errors', async () => { + const errorClientInstance = mockClient(mockConfig); + errorClientInstance.redirect = async () => { + throw { + response: { + data: { + errorMsg: 'Published version already has md5: abc123' + } + } + }; + }; + + const context = { + config: mockConfig, + client: () => errorClientInstance + }; + + const args = { + from: '/old-path', + to: '/new-path', + status: 301, + clientid: 'test-client', + project: 'test-project', + token: 'test-token' + }; + + const result = await redirect.handler.call(context, args); + expect(result).to.equal('Skipped redirect from /old-path to /new-path (already exists)'); + }); + + it('should handle missing args', async () => { + const context = { + config: mockConfig, + client: () => mockClientInstance, + promptArgs: async () => null + }; + + try { + await redirect.handler.call(context, null); + expect.fail('Should have thrown error'); + } catch (err) { + expect(err.message).to.equal('Operation cancelled'); + } + }); + + it('should handle config fromArgs failure', async () => { + const exit = process.exit; + process.exit = (_code) => { + process.exit = exit; + throw new Error('Process exited with code 1'); + }; + + const context = { + config: { + ...mockConfig, + fromArgs: async () => false + } + }; + + const args = { + from: '/old-path', + to: '/new-path', + status: 301 + }; + + try { + await redirect.handler.call(context, args); + expect.fail('Process exited with code 1'); + } catch (err) { + expect(err.message).to.equal('Process exited with code 1'); + } finally { + process.exit = exit; + } + }); + + it('should handle general errors', async () => { + const errorClientInstance = mockClient(mockConfig); + errorClientInstance.redirect = async () => { + throw new Error('Failed to create redirect'); + }; + + const context = { + config: mockConfig, + client: () => errorClientInstance + }; + + const args = { + from: '/old-path', + to: '/new-path', + status: 301, + clientid: 'test-client', + project: 'test-project', + token: 'test-token' + }; + + try { + await redirect.handler.call(context, args); + expect.fail('Should have thrown error'); + } catch (err) { + expect(err.message).to.include('Failed to create redirect'); + } + }); + + it('should use default status code if not provided', async () => { + const context = { + config: mockConfig, + client: () => mockClientInstance + }; + + const args = { + from: '/old-path', + to: '/new-path' + }; + + const result = await redirect.handler.call(context, args); + expect(result).to.equal('Created redirect from /old-path to /new-path (302)'); + expect(mockClientInstance._history.post.length).to.equal(1); + const [call] = mockClientInstance._history.post; + expect(call.headers['Quant-Status']).to.equal(302); + }); + }); +}); \ No newline at end of file