Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ dist
node_modules
.vscode-test/
*.vsix
.dependencygraph
.dependencygraph
local/
15 changes: 5 additions & 10 deletions src/generator/module/ModuleLicenseGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,16 @@ export default class ModuleLicenseGenerator extends TemplateGenerator<
| TemplatePath.LicenseApache20
| TemplatePath.LicenseOslv3
> {
public constructor(protected data: ModuleWizardData | ModuleWizardComposerData) {
const params = {
public constructor(data: ModuleWizardData | ModuleWizardComposerData) {
super('LICENSE.txt', TemplatePath.LicenseMit, {
...data,
year: new Date().getFullYear(),
};

super('LICENSE.txt', TemplatePath.LicenseMit, params);
});
}

public async generate(workspaceUri: Uri): Promise<GeneratedFile> {
const moduleDirectory = Magento.getModuleDirectory(
this.data.vendor,
this.data.module,
workspaceUri
);
const data = this.data as ModuleWizardData | ModuleWizardComposerData;
const moduleDirectory = Magento.getModuleDirectory(data.vendor, data.module, workspaceUri);

return super.generate(moduleDirectory);
}
Expand Down
83 changes: 83 additions & 0 deletions src/test/cache/DocumentCache.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import * as assert from 'assert';
import { describe, it } from 'mocha';
import { TextDocument } from 'vscode';
import DocumentCache from 'cache/DocumentCache';

function fakeDocument(fsPath: string, version: number): TextDocument {
return { uri: { fsPath }, version } as unknown as TextDocument;
}

describe('DocumentCache Tests', () => {
it('should return a cached value when the document version matches', () => {
const doc = fakeDocument('/ws/a.xml', 1);
DocumentCache.set(doc, 'ast', { nodes: 3 });

assert.deepStrictEqual(DocumentCache.get(doc, 'ast'), { nodes: 3 });
assert.strictEqual(DocumentCache.has(doc, 'ast'), true);
});

it('should miss when the document version has advanced', () => {
const v1 = fakeDocument('/ws/b.xml', 1);
DocumentCache.set(v1, 'ast', { nodes: 3 });

const v2 = fakeDocument('/ws/b.xml', 2);
assert.strictEqual(DocumentCache.get(v2, 'ast'), undefined);
assert.strictEqual(DocumentCache.has(v2, 'ast'), false);
});

it('should return undefined for an unknown key', () => {
const doc = fakeDocument('/ws/c.xml', 1);
assert.strictEqual(DocumentCache.get(doc, 'missing'), undefined);
assert.strictEqual(DocumentCache.has(doc, 'missing'), false);
});

it('should keep entries for different paths isolated', () => {
const a = fakeDocument('/ws/d.xml', 1);
const b = fakeDocument('/ws/e.xml', 1);
DocumentCache.set(a, 'ast', 'value-a');
DocumentCache.set(b, 'ast', 'value-b');

assert.strictEqual(DocumentCache.get(a, 'ast'), 'value-a');
assert.strictEqual(DocumentCache.get(b, 'ast'), 'value-b');
});

it('should keep entries for different keys on the same document isolated', () => {
const doc = fakeDocument('/ws/f.xml', 1);
DocumentCache.set(doc, 'ast', 'ast-value');
DocumentCache.set(doc, 'symbols', 'symbols-value');

assert.strictEqual(DocumentCache.get(doc, 'ast'), 'ast-value');
assert.strictEqual(DocumentCache.get(doc, 'symbols'), 'symbols-value');
});

it('should delete a single entry', () => {
const doc = fakeDocument('/ws/g.xml', 1);
DocumentCache.set(doc, 'ast', 'value');
DocumentCache.delete(doc, 'ast');

assert.strictEqual(DocumentCache.get(doc, 'ast'), undefined);
});

it('should clear every key for a document by path prefix', () => {
const doc = fakeDocument('/ws/h.xml', 1);
DocumentCache.set(doc, 'ast', 'a');
DocumentCache.set(doc, 'symbols', 'b');

DocumentCache.clear(doc);

assert.strictEqual(DocumentCache.get(doc, 'ast'), undefined);
assert.strictEqual(DocumentCache.get(doc, 'symbols'), undefined);
});

it('should not clear a path that merely shares a prefix string', () => {
const target = fakeDocument('/ws/i.xml', 1);
const sibling = fakeDocument('/ws/i.xml.bak', 1);
DocumentCache.set(target, 'ast', 'target');
DocumentCache.set(sibling, 'ast', 'sibling');

DocumentCache.clear(target);

assert.strictEqual(DocumentCache.get(target, 'ast'), undefined);
assert.strictEqual(DocumentCache.get(sibling, 'ast'), 'sibling');
});
});
57 changes: 57 additions & 0 deletions src/test/common/MagentoCli.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import * as assert from 'assert';
import { describe, it } from 'mocha';
import MagentoCli from 'common/MagentoCli';

// `quote` is a private static helper; it is the shell-injection defence for every
// command MagentoCli runs, so it is tested directly via a cast.
const quote = (value: string): string =>
(MagentoCli as unknown as { quote(value: string): string }).quote(value);

describe('MagentoCli.quote Tests', () => {
it('should wrap a plain token in single quotes', () => {
assert.strictEqual(quote('setup:upgrade'), `'setup:upgrade'`);
});

it('should quote an empty string as an empty quoted token', () => {
assert.strictEqual(quote(''), `''`);
});

it('should keep spaces safe inside the quotes', () => {
assert.strictEqual(quote('a b c'), `'a b c'`);
});

it('should escape a single quote using the POSIX close-escape-reopen sequence', () => {
assert.strictEqual(quote(`it's`), `'it'\\''s'`);
});

it('should escape multiple consecutive single quotes', () => {
assert.strictEqual(quote(`'''`), `''\\'''\\'''\\'''`);
});

it('should not expand command substitution, backticks or variables', () => {
assert.strictEqual(quote('$(whoami)'), `'$(whoami)'`);
assert.strictEqual(quote('`id`'), "'`id`'");
assert.strictEqual(quote('$HOME'), `'$HOME'`);
});

it('should keep command separators inert', () => {
assert.strictEqual(quote('; rm -rf /'), `'; rm -rf /'`);
assert.strictEqual(quote('foo && bar'), `'foo && bar'`);
assert.strictEqual(quote('a | b'), `'a | b'`);
});

it('should neutralise an injection payload that breaks out with a quote', () => {
const payload = `'; rm -rf /; echo '`;
const quoted = quote(payload);

// The result must be a single shell token: it starts and ends with a quote,
// and every embedded quote is the escaped `'\''` sequence so nothing breaks out.
assert.ok(quoted.startsWith(`'`));
assert.ok(quoted.endsWith(`'`));
assert.strictEqual(quoted.replace(/'\\''/g, ''), `'; rm -rf /; echo '`);
});

it('should preserve newlines inside the quoted token', () => {
assert.strictEqual(quote('line1\nline2'), `'line1\nline2'`);
});
});
112 changes: 112 additions & 0 deletions src/test/common/PhpNamespace.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import * as assert from 'assert';
import { describe, it } from 'mocha';
import PhpNamespace from 'common/PhpNamespace';

describe('PhpNamespace Tests', () => {
describe('fromString', () => {
it('should split a namespace into parts', () => {
assert.deepStrictEqual(PhpNamespace.fromString('Vendor\\Module\\Model').getParts(), [
'Vendor',
'Module',
'Model',
]);
});

it('should trim a leading separator', () => {
assert.deepStrictEqual(PhpNamespace.fromString('\\Vendor\\Module').getParts(), [
'Vendor',
'Module',
]);
});

it('should drop empty parts from doubled separators', () => {
assert.deepStrictEqual(PhpNamespace.fromString('Vendor\\\\Module').getParts(), [
'Vendor',
'Module',
]);
});

it('should round-trip through toString', () => {
assert.strictEqual(
PhpNamespace.fromString('Vendor\\Module\\Model').toString(),
'Vendor\\Module\\Model'
);
});
});

describe('head / tail / pop', () => {
it('should expose the head and tail parts', () => {
const ns = PhpNamespace.fromParts(['Vendor', 'Module', 'Model']);
assert.strictEqual(ns.getHead(), 'Vendor');
assert.strictEqual(ns.getTail(), 'Model');
});

it('should pop the last part', () => {
const ns = PhpNamespace.fromParts(['Vendor', 'Module', 'Model']);
assert.strictEqual(ns.pop(), 'Model');
assert.deepStrictEqual(ns.getParts(), ['Vendor', 'Module']);
});
});

describe('append', () => {
it('should append string parts', () => {
assert.strictEqual(
PhpNamespace.fromParts(['Vendor', 'Module']).append('Model', 'Product').toString(),
'Vendor\\Module\\Model\\Product'
);
});

it('should append another namespace by flattening its parts', () => {
const base = PhpNamespace.fromParts(['Vendor', 'Module']);
const tail = PhpNamespace.fromParts(['Model', 'Product']);
assert.strictEqual(base.append(tail).toString(), 'Vendor\\Module\\Model\\Product');
});

it('should not mutate the original namespace', () => {
const base = PhpNamespace.fromParts(['Vendor', 'Module']);
base.append('Model');
assert.deepStrictEqual(base.getParts(), ['Vendor', 'Module']);
});
});

describe('prepend', () => {
it('should prepend a string part', () => {
assert.strictEqual(
PhpNamespace.fromParts(['Module', 'Model']).prepend('Vendor').toString(),
'Vendor\\Module\\Model'
);
});

it('should prepend another namespace', () => {
const ns = PhpNamespace.fromParts(['Model']);
const prefix = PhpNamespace.fromParts(['Vendor', 'Module']);
assert.strictEqual(ns.prepend(prefix).toString(), 'Vendor\\Module\\Model');
});
});

describe('isSubNamespaceOf', () => {
it('should return true for a deeper namespace', () => {
const child = PhpNamespace.fromString('Vendor\\Module\\Model\\Product');
const parent = PhpNamespace.fromString('Vendor\\Module');
assert.strictEqual(child.isSubNamespaceOf(parent), true);
});

it('should return true for an equal namespace', () => {
const a = PhpNamespace.fromString('Vendor\\Module');
const b = PhpNamespace.fromString('Vendor\\Module');
assert.strictEqual(a.isSubNamespaceOf(b), true);
});

it('should return false for a shallower namespace', () => {
const parent = PhpNamespace.fromString('Vendor\\Module');
const child = PhpNamespace.fromString('Vendor\\Module\\Model');
assert.strictEqual(parent.isSubNamespaceOf(child), false);
});

it('should return false for a sibling namespace', () => {
const a = PhpNamespace.fromString('Vendor\\Other\\Model');
const b = PhpNamespace.fromString('Vendor\\Module');
assert.strictEqual(a.isSubNamespaceOf(b), false);
});
});
});
57 changes: 57 additions & 0 deletions src/test/common/Validation.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import * as assert from 'assert';
import { describe, it } from 'mocha';
import Validation from 'common/Validation';

describe('Validation Tests', () => {
describe('isValidModuleName', () => {
['Vendor_Module', 'Foo_Bar', 'Magento_Catalog', 'A1_B2'].forEach(name => {
it(`should accept "${name}"`, () => {
assert.strictEqual(Validation.isValidModuleName(name).isValid, true);
});
});

[
'vendor_module',
'Vendor',
'Vendor_',
'_Module',
'Vendor__Module',
'Vendor_module',
'',
].forEach(name => {
it(`should reject "${name}"`, () => {
const result = Validation.isValidModuleName(name);
assert.strictEqual(result.isValid, false);
assert.ok(result.errors && result.errors.length > 0);
});
});
});

describe('isValidClassName', () => {
['ClassName', '_Class', 'classNAME', 'Class123'].forEach(name => {
it(`should accept "${name}"`, () => {
assert.strictEqual(Validation.isValidClassName(name).isValid, true);
});
});

['123Class', 'class-name', 'class name', ''].forEach(name => {
it(`should reject "${name}"`, () => {
assert.strictEqual(Validation.isValidClassName(name).isValid, false);
});
});
});

describe('isSnakeCase', () => {
['snake_case', 'one', 'a_b_c', 'a_1_b', 'value123'].forEach(name => {
it(`should accept "${name}"`, () => {
assert.strictEqual(Validation.isSnakeCase(name).isValid, true);
});
});

['camelCase', 'PascalCase', 'snake-case', 'UPPER', 'has space', ''].forEach(name => {
it(`should reject "${name}"`, () => {
assert.strictEqual(Validation.isSnakeCase(name).isValid, false);
});
});
});
});
Loading
Loading