mirror of https://github.com/portainer/portainer
352 lines
9.1 KiB
TypeScript
352 lines
9.1 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
|
|
import { validateYAML, hasYamlChanged } from './stackYamlValidation';
|
|
|
|
describe('validateYAML', () => {
|
|
describe('YAML syntax validation', () => {
|
|
it('should return empty string for valid YAML', () => {
|
|
const yaml = `
|
|
version: '3'
|
|
services:
|
|
web:
|
|
image: nginx
|
|
container_name: my-nginx
|
|
`;
|
|
|
|
const result = validateYAML(yaml);
|
|
|
|
expect(result).toBe('');
|
|
});
|
|
|
|
it('should return error message for invalid YAML syntax', () => {
|
|
const yaml = `
|
|
version: '3'
|
|
services:
|
|
web:
|
|
image: nginx
|
|
container_name: [invalid
|
|
`;
|
|
|
|
const result = validateYAML(yaml);
|
|
|
|
expect(result).toContain('There is an error in the yaml syntax:');
|
|
});
|
|
|
|
it('should handle empty YAML string', () => {
|
|
const result = validateYAML('');
|
|
|
|
expect(result).toBe('');
|
|
});
|
|
|
|
it('should handle YAML with comments', () => {
|
|
const yaml = `
|
|
# This is a comment
|
|
version: '3'
|
|
services:
|
|
web:
|
|
image: nginx # inline comment
|
|
`;
|
|
|
|
const result = validateYAML(yaml);
|
|
|
|
expect(result).toBe('');
|
|
});
|
|
});
|
|
|
|
describe('container name conflict detection', () => {
|
|
it('should detect single container name conflict', () => {
|
|
const yaml = `
|
|
version: '3'
|
|
services:
|
|
web:
|
|
image: nginx
|
|
container_name: existing-container
|
|
`;
|
|
const containerNames = ['existing-container', 'other-container'];
|
|
const originalContainerNames: string[] = [];
|
|
|
|
const result = validateYAML(yaml, containerNames, originalContainerNames);
|
|
|
|
expect(result).toBe(
|
|
'This container name is already used by another container running in this environment: existing-container.'
|
|
);
|
|
});
|
|
|
|
it('should detect multiple container name conflicts', () => {
|
|
const yaml = `
|
|
version: '3'
|
|
services:
|
|
web:
|
|
image: nginx
|
|
container_name: existing-1
|
|
db:
|
|
image: postgres
|
|
container_name: existing-2
|
|
`;
|
|
const containerNames = ['existing-1', 'existing-2', 'other-container'];
|
|
const originalContainerNames: string[] = [];
|
|
|
|
const result = validateYAML(yaml, containerNames, originalContainerNames);
|
|
|
|
expect(result).toBe(
|
|
'These container names are already used by another container running in this environment: existing-1, existing-2.'
|
|
);
|
|
});
|
|
|
|
it('should ignore conflicts with original container names', () => {
|
|
const yaml = `
|
|
version: '3'
|
|
services:
|
|
web:
|
|
image: nginx
|
|
container_name: my-container
|
|
`;
|
|
const containerNames = ['my-container', 'other-container'];
|
|
const originalContainerNames = ['my-container'];
|
|
|
|
const result = validateYAML(yaml, containerNames, originalContainerNames);
|
|
|
|
expect(result).toBe('');
|
|
});
|
|
|
|
it('should detect conflict when editing container names in existing stack', () => {
|
|
const yaml = `
|
|
version: '3'
|
|
services:
|
|
web:
|
|
image: nginx
|
|
container_name: conflicting-name
|
|
`;
|
|
const containerNames = [
|
|
'original-name',
|
|
'conflicting-name',
|
|
'other-container',
|
|
];
|
|
const originalContainerNames = ['original-name'];
|
|
|
|
const result = validateYAML(yaml, containerNames, originalContainerNames);
|
|
|
|
expect(result).toBe(
|
|
'This container name is already used by another container running in this environment: conflicting-name.'
|
|
);
|
|
});
|
|
|
|
it('should allow renaming container in same stack', () => {
|
|
const yaml = `
|
|
version: '3'
|
|
services:
|
|
web:
|
|
image: nginx
|
|
container_name: new-name
|
|
`;
|
|
const containerNames = ['old-name', 'other-container'];
|
|
const originalContainerNames = ['old-name'];
|
|
|
|
const result = validateYAML(yaml, containerNames, originalContainerNames);
|
|
|
|
expect(result).toBe('');
|
|
});
|
|
|
|
it('should handle empty containerNames array', () => {
|
|
const yaml = `
|
|
version: '3'
|
|
services:
|
|
web:
|
|
image: nginx
|
|
container_name: my-container
|
|
`;
|
|
|
|
const result = validateYAML(yaml, [], []);
|
|
|
|
expect(result).toBe('');
|
|
});
|
|
|
|
it('should handle YAML without container_name properties', () => {
|
|
const yaml = `
|
|
version: '3'
|
|
services:
|
|
web:
|
|
image: nginx
|
|
db:
|
|
image: postgres
|
|
`;
|
|
const containerNames = ['existing-container'];
|
|
|
|
const result = validateYAML(yaml, containerNames);
|
|
|
|
expect(result).toBe('');
|
|
});
|
|
|
|
it('should detect partial conflicts in multi-service stack', () => {
|
|
const yaml = `
|
|
version: '3'
|
|
services:
|
|
web:
|
|
image: nginx
|
|
container_name: unique-name
|
|
db:
|
|
image: postgres
|
|
container_name: conflicting-name
|
|
cache:
|
|
image: redis
|
|
container_name: another-unique
|
|
`;
|
|
const containerNames = ['conflicting-name', 'other-container'];
|
|
const originalContainerNames: string[] = [];
|
|
|
|
const result = validateYAML(yaml, containerNames, originalContainerNames);
|
|
|
|
expect(result).toBe(
|
|
'This container name is already used by another container running in this environment: conflicting-name.'
|
|
);
|
|
});
|
|
|
|
it('should use default empty arrays when containerNames not provided', () => {
|
|
const yaml = `
|
|
version: '3'
|
|
services:
|
|
web:
|
|
image: nginx
|
|
container_name: my-container
|
|
`;
|
|
|
|
const result = validateYAML(yaml);
|
|
|
|
expect(result).toBe('');
|
|
});
|
|
});
|
|
|
|
describe('edge cases', () => {
|
|
it('should handle YAML with duplicate container names in same stack', () => {
|
|
const yaml = `
|
|
version: '3'
|
|
services:
|
|
web:
|
|
image: nginx
|
|
container_name: duplicate-name
|
|
api:
|
|
image: node
|
|
container_name: duplicate-name
|
|
`;
|
|
const containerNames = ['duplicate-name'];
|
|
const originalContainerNames: string[] = [];
|
|
|
|
const result = validateYAML(yaml, containerNames, originalContainerNames);
|
|
|
|
expect(result).toBe(
|
|
'This container name is already used by another container running in this environment: duplicate-name.'
|
|
);
|
|
});
|
|
|
|
it('should handle nested container_name properties', () => {
|
|
const yaml = `
|
|
version: '3'
|
|
services:
|
|
web:
|
|
image: nginx
|
|
container_name: main-container
|
|
deploy:
|
|
metadata:
|
|
container_name: nested-container
|
|
`;
|
|
const containerNames = ['nested-container'];
|
|
const originalContainerNames: string[] = [];
|
|
|
|
const result = validateYAML(yaml, containerNames, originalContainerNames);
|
|
|
|
expect(result).toBe(
|
|
'This container name is already used by another container running in this environment: nested-container.'
|
|
);
|
|
});
|
|
|
|
it('should return validation error before checking conflicts for invalid YAML', () => {
|
|
const yaml = 'invalid: [yaml: syntax';
|
|
const containerNames = ['existing-container'];
|
|
|
|
const result = validateYAML(yaml, containerNames);
|
|
|
|
expect(result).toContain('There is an error in the yaml syntax:');
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('hasYamlChanged', () => {
|
|
it('should return false for identical strings', () => {
|
|
const original = 'version: "3"\nservices:\n web:\n image: nginx';
|
|
const updated = 'version: "3"\nservices:\n web:\n image: nginx';
|
|
|
|
const result = hasYamlChanged(original, updated);
|
|
|
|
expect(result).toBe(false);
|
|
});
|
|
|
|
it('should return true for different content', () => {
|
|
const original = 'version: "3"\nservices:\n web:\n image: nginx';
|
|
const updated = 'version: "3"\nservices:\n web:\n image: apache';
|
|
|
|
const result = hasYamlChanged(original, updated);
|
|
|
|
expect(result).toBe(true);
|
|
});
|
|
|
|
it('should ignore line ending differences (LF vs CRLF)', () => {
|
|
const original = 'version: "3"\nservices:\n web:\n image: nginx';
|
|
const updated = 'version: "3"\r\nservices:\r\n web:\r\n image: nginx';
|
|
|
|
const result = hasYamlChanged(original, updated);
|
|
|
|
expect(result).toBe(false);
|
|
});
|
|
|
|
it('should ignore mixed line endings', () => {
|
|
const original = 'version: "3"\nservices:\r\n web:\n image: nginx';
|
|
const updated = 'version: "3"\r\nservices:\n web:\r\n image: nginx';
|
|
|
|
const result = hasYamlChanged(original, updated);
|
|
|
|
expect(result).toBe(false);
|
|
});
|
|
|
|
it('should detect changes when whitespace differs significantly', () => {
|
|
const original = 'version: "3"\nservices:\n web:\n image: nginx';
|
|
const updated = 'version: "3"\nservices:\n web:\n image: nginx'; // Extra space
|
|
|
|
const result = hasYamlChanged(original, updated);
|
|
|
|
expect(result).toBe(true);
|
|
});
|
|
|
|
it('should return false for empty strings', () => {
|
|
const result = hasYamlChanged('', '');
|
|
|
|
expect(result).toBe(false);
|
|
});
|
|
|
|
it('should return true when one string is empty', () => {
|
|
const original = 'version: "3"';
|
|
const updated = '';
|
|
|
|
const result = hasYamlChanged(original, updated);
|
|
|
|
expect(result).toBe(true);
|
|
});
|
|
|
|
it('should ignore old Mac line endings (CR)', () => {
|
|
const original = 'version: "3"\nservices:\n web:\n image: nginx';
|
|
const updated = 'version: "3"\rservices:\r web:\r image: nginx';
|
|
|
|
const result = hasYamlChanged(original, updated);
|
|
|
|
expect(result).toBe(false);
|
|
});
|
|
|
|
it('should handle strings with only line ending differences', () => {
|
|
const original = '\n\n\n';
|
|
const updated = '\r\n\r\n\r\n';
|
|
|
|
const result = hasYamlChanged(original, updated);
|
|
|
|
expect(result).toBe(false);
|
|
});
|
|
});
|