Commit 7dfae341 authored by Neil McCorrison's avatar Neil McCorrison Committed by Vitaly Slobodin

Update secure License end to end tests

Given that the buttons for accepting or denying
licenses are being removed from the modals from
Merge Requests and Pipelines, this commit
updates the tests such that Licence Compliance
dashboard is used, but the licenses are updated
within Merge Requests and Pipelines.
parent 0e8c176d
......@@ -37,10 +37,7 @@ compliance report will be shown properly.
![License Compliance Widget](img/license_compliance_v13_0.png)
If you are a project or group Maintainer, you can click on a license to be given
the choice to allow it or deny it.
![License approval decision](img/license_compliance_decision_v13_0.png)
You can click on a license to see more information.
When GitLab detects a **Denied** license, you can view it in the [license list](#license-list).
......
<script>
import { mapActions } from 'vuex';
import { GlLink } from '@gitlab/ui';
import { LICENSE_MANAGEMENT } from 'ee/vue_shared/license_compliance/store/constants';
import LicensePackages from './license_packages.vue';
export default {
name: 'LicenseIssueBody',
components: { LicensePackages },
components: { LicensePackages, GlLink },
props: {
issue: {
type: Object,
......@@ -19,15 +20,7 @@ export default {
<template>
<div class="report-block-info license-item">
<button
class="btn-blank btn-link gl-mr-2"
type="button"
data-toggle="modal"
data-target="#modal-set-license-approval"
@click="setLicenseInModal(issue)"
>
{{ issue.name }}
</button>
<gl-link :href="issue.url" target="_blank">{{ issue.name }}</gl-link>
<license-packages :packages="issue.packages" class="text-secondary" />
</div>
</template>
<script>
import { mapActions, mapState } from 'vuex';
import { GlLink } from '@gitlab/ui';
import { LICENSE_MANAGEMENT } from 'ee/vue_shared/license_compliance/store/constants';
import { s__ } from '~/locale';
import DeprecatedModal2 from '~/vue_shared/components/deprecated_modal_2.vue';
import LicensePackages from './license_packages.vue';
import { LICENSE_APPROVAL_STATUS } from '../constants';
export default {
name: 'LicenseSetApprovalStatusModal',
components: { GlLink, LicensePackages, GlModal: DeprecatedModal2 },
computed: {
...mapState(LICENSE_MANAGEMENT, ['currentLicenseInModal', 'canManageLicenses']),
headerTitleText() {
if (!this.canManageLicenses) {
return s__('LicenseCompliance|License details');
}
return s__('LicenseCompliance|License review');
},
canApprove() {
return (
this.canManageLicenses &&
this.currentLicenseInModal &&
this.currentLicenseInModal.approvalStatus !== LICENSE_APPROVAL_STATUS.ALLOWED
);
},
canBlacklist() {
return (
this.canManageLicenses &&
this.currentLicenseInModal &&
this.currentLicenseInModal.approvalStatus !== LICENSE_APPROVAL_STATUS.DENIED
);
},
},
methods: {
...mapActions(LICENSE_MANAGEMENT, ['resetLicenseInModal', 'allowLicense', 'denyLicense']),
},
};
</script>
<template>
<gl-modal
id="modal-set-license-approval"
:header-title-text="headerTitleText"
modal-size="lg"
data-qa-selector="license_management_modal"
@cancel="resetLicenseInModal"
>
<slot v-if="currentLicenseInModal">
<div class="row gl-mt-3 gl-mb-3 js-license-name">
<label class="col-sm-3 text-right font-weight-bold">
{{ s__('LicenseCompliance|License') }}:
</label>
<div class="col-sm-9 text-secondary">{{ currentLicenseInModal.name }}</div>
</div>
<div v-if="currentLicenseInModal.url" class="row gl-mt-3 gl-mb-3 js-license-url">
<label class="col-sm-3 text-right font-weight-bold">
{{ s__('LicenseCompliance|URL') }}:
</label>
<div class="col-sm-9 text-secondary">
<gl-link :href="currentLicenseInModal.url" target="_blank" rel="nofollow">{{
currentLicenseInModal.url
}}</gl-link>
</div>
</div>
<div class="row gl-mt-3 gl-mb-3 js-license-packages">
<label class="col-sm-3 text-right font-weight-bold">
{{ s__('LicenseCompliance|Packages') }}:
</label>
<license-packages
:packages="currentLicenseInModal.packages"
class="col-sm-9 text-secondary"
/>
</div>
</slot>
<template slot="footer">
<button
type="button"
class="btn js-modal-cancel-action"
data-dismiss="modal"
@click="resetLicenseInModal"
>
{{ s__('Modal|Cancel') }}
</button>
<button
v-if="canBlacklist"
class="btn btn-remove btn-inverted js-modal-secondary-action"
data-dismiss="modal"
data-qa-selector="deny_license_button"
@click="denyLicense(currentLicenseInModal)"
>
{{ s__('LicenseCompliance|Deny') }}
</button>
<button
v-if="canApprove"
type="button"
class="btn btn-success js-modal-primary-action"
data-dismiss="modal"
data-qa-selector="approve_license_button"
@click="allowLicense(currentLicenseInModal)"
>
{{ s__('LicenseCompliance|Allow') }}
</button>
</template>
</gl-modal>
</template>
......@@ -2,7 +2,6 @@
import { mapState, mapGetters, mapActions } from 'vuex';
import { GlLink, GlIcon } from '@gitlab/ui';
import reportsMixin from 'ee/vue_shared/security_reports/mixins/reports_mixin';
import SetLicenseApprovalModal from 'ee/vue_shared/license_compliance/components/set_approval_status_modal.vue';
import { componentNames } from 'ee/reports/components/issue_body';
import { LICENSE_MANAGEMENT } from 'ee/vue_shared/license_compliance/store/constants';
import ReportItem from '~/reports/components/report_item.vue';
......@@ -20,7 +19,6 @@ export default {
GlLink,
ReportItem,
ReportSection,
SetLicenseApprovalModal,
SmartVirtualList,
GlIcon,
},
......@@ -121,7 +119,6 @@ export default {
</script>
<template>
<div>
<set-license-approval-modal />
<report-section
:status="licenseReportStatus"
:loading-text="licenseSummaryText"
......@@ -185,6 +182,7 @@ export default {
:class="{ 'gl-mr-3': fullReportPath }"
:href="licenseManagementSettingsPath"
class="btn btn-default btn-sm js-manage-licenses"
data-qa-selector="manage_licenses_button"
>
{{ s__('ciReport|Manage licenses') }}
</a>
......
---
title: Removes ability to change license status through MR and Pipeline pages
merge_request: 43470
author:
type: changed
......@@ -51,6 +51,7 @@ exports[`License Report MR Widget report section should render correctly 1`] = `
>
<a
class="btn btn-default btn-sm js-manage-licenses gl-mr-3"
data-qa-selector="manage_licenses_button"
href="http://test.host/lm_settings"
>
......
......@@ -21,25 +21,13 @@ describe('LicenseIssueBody', () => {
vm.$destroy();
});
describe('interaction', () => {
it('clicking the button triggers openModal with the current license', () => {
const linkEl = vm.$el.querySelector('.license-item > .btn-link');
expect(store.state.licenseManagement.currentLicenseInModal).toBe(null);
linkEl.click();
expect(store.state.licenseManagement.currentLicenseInModal).toBe(issue);
});
});
describe('template', () => {
it('renders component container element with class `license-item`', () => {
expect(vm.$el.classList.contains('license-item')).toBe(true);
});
it('renders button to open modal', () => {
const linkEl = vm.$el.querySelector('.license-item > .btn-link');
it('renders link to view license', () => {
const linkEl = vm.$el.querySelector('.license-item > a');
expect(linkEl).not.toBeNull();
expect(linkEl.innerText.trim()).toBe(issue.name);
......
import Vue from 'vue';
import Vuex from 'vuex';
import SetApprovalModal from 'ee/vue_shared/license_compliance/components/set_approval_status_modal.vue';
import { LICENSE_APPROVAL_STATUS } from 'ee/vue_shared/license_compliance/constants';
import { trimText } from 'helpers/text_helper';
import { mountComponentWithStore } from 'helpers/vue_mount_component_helper';
import { licenseReport } from '../mock_data';
Vue.use(Vuex);
describe('SetApprovalModal', () => {
const Component = Vue.extend(SetApprovalModal);
let vm;
let store;
let actions;
beforeEach(() => {
actions = {
resetLicenseInModal: jest.fn(),
allowLicense: jest.fn(),
denyLicense: jest.fn(),
};
store = new Vuex.Store({
modules: {
licenseManagement: {
namespaced: true,
state: {
currentLicenseInModal: licenseReport[0],
canManageLicenses: true,
},
actions,
},
},
});
vm = mountComponentWithStore(Component, { store });
});
afterEach(() => {
vm.$destroy();
});
describe('for approved license', () => {
beforeEach(done => {
store.replaceState({
licenseManagement: {
currentLicenseInModal: {
...licenseReport[0],
approvalStatus: LICENSE_APPROVAL_STATUS.ALLOWED,
},
canManageLicenses: true,
},
});
Vue.nextTick(done);
});
describe('computed', () => {
it('headerTitleText returns `License review', () => {
expect(vm.headerTitleText).toBe('License review');
});
it('canApprove is false', () => {
expect(vm.canApprove).toBe(false);
});
it('canBlacklist is true', () => {
expect(vm.canBlacklist).toBe(true);
});
});
describe('template correctly', () => {
it('renders modal title', () => {
const headerEl = vm.$el.querySelector('.modal-title');
expect(headerEl).not.toBeNull();
expect(headerEl.innerText.trim()).toBe('License review');
});
it('renders no Allow button in modal footer', () => {
const footerButton = vm.$el.querySelector('.js-modal-primary-action');
expect(footerButton).toBeNull();
});
it('renders Deny button in modal footer', () => {
const footerButton = vm.$el.querySelector('.js-modal-secondary-action');
expect(footerButton).not.toBeNull();
expect(footerButton.innerText.trim()).toBe('Deny');
});
});
});
describe('for unapproved license', () => {
beforeEach(done => {
store.replaceState({
licenseManagement: {
currentLicenseInModal: {
...licenseReport[0],
approvalStatus: undefined,
},
canManageLicenses: true,
},
});
Vue.nextTick(done);
});
describe('computed', () => {
it('headerTitleText returns `License review`', () => {
expect(vm.headerTitleText).toBe('License review');
});
it('canApprove is true', () => {
expect(vm.canApprove).toBe(true);
});
it('canBlacklist is true', () => {
expect(vm.canBlacklist).toBe(true);
});
});
describe('template', () => {
it('renders modal title', () => {
const headerEl = vm.$el.querySelector('.modal-title');
expect(headerEl).not.toBeNull();
expect(headerEl.innerText.trim()).toBe('License review');
});
it('renders Allow button in modal footer', () => {
const footerButton = vm.$el.querySelector('.js-modal-primary-action');
expect(footerButton).not.toBeNull();
expect(footerButton.innerText.trim()).toBe('Allow');
});
it('renders Deny button in modal footer', () => {
const footerButton = vm.$el.querySelector('.js-modal-secondary-action');
expect(footerButton).not.toBeNull();
expect(footerButton.innerText.trim()).toBe('Deny');
});
});
});
describe('for blacklisted license', () => {
beforeEach(done => {
store.replaceState({
licenseManagement: {
currentLicenseInModal: {
...licenseReport[0],
approvalStatus: LICENSE_APPROVAL_STATUS.DENIED,
},
canManageLicenses: true,
},
});
Vue.nextTick(done);
});
describe('computed', () => {
it('headerTitleText returns `License review`', () => {
expect(vm.headerTitleText).toBe('License review');
});
it('canApprove is true', () => {
expect(vm.canApprove).toBe(true);
});
it('canBlacklist is false', () => {
expect(vm.canBlacklist).toBe(false);
});
});
describe('template', () => {
it('renders modal title', () => {
const headerEl = vm.$el.querySelector('.modal-title');
expect(headerEl).not.toBeNull();
expect(headerEl.innerText.trim()).toBe('License review');
});
it('renders Allow button in modal footer', () => {
const footerButton = vm.$el.querySelector('.js-modal-primary-action');
expect(footerButton).not.toBeNull();
expect(footerButton.innerText.trim()).toBe('Allow');
});
it('renders no Deny button in modal footer', () => {
const footerButton = vm.$el.querySelector('.js-modal-secondary-action');
expect(footerButton).toBeNull();
});
});
});
describe('for user without the rights to manage licenses', () => {
beforeEach(done => {
store.replaceState({
licenseManagement: {
currentLicenseInModal: {
...licenseReport[0],
approvalStatus: undefined,
},
canManageLicenses: false,
},
});
Vue.nextTick(done);
});
describe('computed', () => {
it('headerTitleText returns `License details`', () => {
expect(vm.headerTitleText).toBe('License details');
});
it('canApprove is false', () => {
expect(vm.canApprove).toBe(false);
});
it('canBlacklist is false', () => {
expect(vm.canBlacklist).toBe(false);
});
});
describe('template', () => {
it('renders modal title', () => {
const headerEl = vm.$el.querySelector('.modal-title');
expect(headerEl).not.toBeNull();
expect(headerEl.innerText.trim()).toBe('License details');
});
it('renders no Approve button in modal footer', () => {
const footerButton = vm.$el.querySelector('.js-modal-primary-action');
expect(footerButton).toBeNull();
});
it('renders no Blacklist button in modal footer', () => {
const footerButton = vm.$el.querySelector('.js-modal-secondary-action');
expect(footerButton).toBeNull();
});
});
});
describe('Modal Body', () => {
it('renders the license name', () => {
const licenseName = vm.$el.querySelector('.js-license-name');
expect(licenseName).not.toBeNull();
expect(trimText(licenseName.innerText)).toBe(`License: ${licenseReport[0].name}`);
});
it('renders the license url with link', () => {
const licenseName = vm.$el.querySelector('.js-license-url');
expect(licenseName).not.toBeNull();
expect(trimText(licenseName.innerText)).toBe(`URL: ${licenseReport[0].url}`);
const licenseLink = licenseName.querySelector('a');
expect(licenseLink.getAttribute('href')).toBe(licenseReport[0].url);
expect(trimText(licenseLink.innerText)).toBe(licenseReport[0].url);
});
it('renders the license url', () => {
const licenseName = vm.$el.querySelector('.js-license-packages');
expect(licenseName).not.toBeNull();
expect(trimText(licenseName.innerText)).toBe('Packages: Used by pg, puma, foo, and 2 more');
});
});
describe('interaction', () => {
describe('triggering resetLicenseInModal on canceling', () => {
it('by clicking the cancel button', () => {
const linkEl = vm.$el.querySelector('.js-modal-cancel-action');
linkEl.click();
expect(actions.resetLicenseInModal).toHaveBeenCalled();
});
it('triggering resetLicenseInModal by clicking the X button', () => {
const linkEl = vm.$el.querySelector('.js-modal-close-action');
linkEl.click();
expect(actions.resetLicenseInModal).toHaveBeenCalled();
});
});
describe('triggering allowLicense on approving', () => {
it('by clicking the confirmation button', () => {
const linkEl = vm.$el.querySelector('.js-modal-primary-action');
linkEl.click();
expect(actions.allowLicense).toHaveBeenCalledWith(
expect.any(Object),
store.state.licenseManagement.currentLicenseInModal,
);
});
});
describe('triggering denyLicense on blacklisting', () => {
it('by clicking the confirmation button', () => {
const linkEl = vm.$el.querySelector('.js-modal-secondary-action');
linkEl.click();
expect(actions.denyLicense).toHaveBeenCalledWith(
expect.any(Object),
store.state.licenseManagement.currentLicenseInModal,
);
});
});
});
it('does not render a XSS link', done => {
// eslint-disable-next-line no-script-url
const badURL = 'javascript:alert("")';
store.replaceState({
licenseManagement: {
currentLicenseInModal: {
...licenseReport[0],
url: badURL,
approvalStatus: LICENSE_APPROVAL_STATUS.ALLOWED,
},
},
});
Vue.nextTick()
.then(() => {
const licenseName = vm.$el.querySelector('.js-license-url');
expect(licenseName).not.toBeNull();
expect(trimText(licenseName.innerText)).toBe(`URL: ${badURL}`);
expect(licenseName.querySelector('a').getAttribute('href')).toBe('about:blank');
expect(licenseName.querySelector('a').innerText).toBe(badURL);
})
.then(done)
.catch(done.fail);
});
});
......@@ -333,12 +333,6 @@ describe('License Report MR Widget', () => {
});
});
it('should render set approval modal', () => {
mountComponent();
expect(wrapper.find('#modal-set-license-approval')).not.toBeNull();
});
it('should init store after mount', () => {
const actions = {
setAPISettings: jest.fn(),
......
......@@ -15946,9 +15946,6 @@ msgstr ""
msgid "LicenseCompliance|Learn more about %{linkStart}License Approvals%{linkEnd}"
msgstr ""
msgid "LicenseCompliance|License"
msgstr ""
msgid "LicenseCompliance|License Approvals"
msgstr ""
......@@ -15988,18 +15985,9 @@ msgstr ""
msgid "LicenseCompliance|License Compliance detected no new licenses"
msgstr ""
msgid "LicenseCompliance|License details"
msgstr ""
msgid "LicenseCompliance|License name"
msgstr ""
msgid "LicenseCompliance|License review"
msgstr ""
msgid "LicenseCompliance|Packages"
msgstr ""
msgid "LicenseCompliance|Remove license"
msgstr ""
......@@ -16015,9 +16003,6 @@ msgstr ""
msgid "LicenseCompliance|This license already exists in this project."
msgstr ""
msgid "LicenseCompliance|URL"
msgstr ""
msgid "LicenseCompliance|You are about to remove the license, %{name}, from this project."
msgstr ""
......
......@@ -9,23 +9,25 @@
{
"id": "MIT",
"name": "MIT License",
"url": "https://opensource.org/licenses/MIT"
"url": "http://opensource.org/licenses/mit-license"
}
],
"dependencies": [
{
"name": "actioncable",
"version": "6.0.3.3",
"name": "test_dependency",
"version": "0.1.0",
"package_manager": "bundler",
"path": "Gemfile.lock",
"licenses": ["MIT"]
"licenses": ["Apache-2.0"]
},
{
"name": "test_package",
"version": "0.1.0",
"name": "actioncable",
"version": "1.2",
"url": "http://rubyonrails.org",
"package_manager": "bundler",
"path": "Gemfile.lock",
"licenses": ["Apache-2.0"]
"description": "WebSocket framework for Rails.",
"path": ".",
"licenses": ["MIT"]
}
]
}
......@@ -19,14 +19,9 @@ module QA
element :icon_status, ':data-qa-selector="`status_${status}_icon`" ' # rubocop:disable QA/ElementWithPattern
end
view 'ee/app/assets/javascripts/vue_shared/license_compliance/components/set_approval_status_modal.vue' do
element :license_management_modal
element :approve_license_button
element :deny_license_button
end
view 'ee/app/assets/javascripts/vue_shared/license_compliance/mr_widget_license_report.vue' do
element :license_report_widget
element :manage_licenses_button
end
end
end
......@@ -50,20 +45,14 @@ module QA
wait_for_animated_element(:license_management_modal)
end
def approve_license(name)
wait_until(reload: true) do
click_license(name)
has_element?(:approve_license_button, wait: 1)
def click_manage_licenses_button
previous_page = page.current_url
within_element(:license_report_widget) do
click_element :manage_licenses_button
end
click_element(:approve_license_button)
end
def deny_license(name)
wait_until(reload: true) do
click_license(name)
has_element?(:deny_license_button, wait: 1)
wait_until(max_duration: 15, reload: false) do
page.current_url != previous_page
end
click_element(:deny_license_button)
end
end
end
......
......@@ -114,16 +114,6 @@ module QA
end
end
def approve_license_with_mr(name)
expand_license_report unless license_report_expanded?
approve_license(name)
end
def deny_license_with_mr(name)
expand_license_report unless license_report_expanded?
deny_license(name)
end
def expand_vulnerability_report
within_element :vulnerability_report_grouped do
click_element :expand_report_button unless has_content? 'Collapse'
......
......@@ -28,7 +28,7 @@ module QA
def approve_license(license)
click_element :license_add_button
expand_select_list
search_and_select license
search_and_select_exact license
click_element :approved_license_radio
click_element :add_license_submit_button
......@@ -36,6 +36,7 @@ module QA
end
def has_approved_license?(name)
has_element?(:admin_license_compliance_row, text: name)
within_element(:admin_license_compliance_row, text: name) do
has_element?(:status_success_icon)
end
......@@ -44,7 +45,7 @@ module QA
def deny_license(license)
click_element :license_add_button
expand_select_list
search_and_select license
search_and_select_exact license
click_element :blacklisted_license_radio
click_element :add_license_submit_button
......@@ -52,6 +53,7 @@ module QA
end
def has_denied_license?(name)
has_element?(:admin_license_compliance_row, text: name)
within_element(:admin_license_compliance_row, text: name) do
has_element?(:status_failed_icon)
end
......
......@@ -38,6 +38,16 @@ module QA
select_item(item_text)
end
def search_and_select_exact(item_text)
QA::Runtime::Logger.info "Searching and selecting: #{item_text}"
search_item(item_text)
raise QA::Page::Base::ElementNotFound, %Q(Couldn't find option named "#{item_text}") unless has_item?(item_text)
find('.select2-result-label', text: item_text, exact_text: true).click
end
def expand_select_list
find('span.select2-arrow').click
end
......
......@@ -26,6 +26,10 @@ module QA
element :child_pipeline
end
view 'app/assets/javascripts/reports/components/report_section.vue' do
element :expand_report_button
end
view 'app/assets/javascripts/vue_shared/components/ci_icon.vue' do
element :status_icon, 'ci-status-icon-${status}' # rubocop:disable QA/ElementWithPattern
end
......@@ -78,6 +82,12 @@ module QA
end
end
def expand_license_report
within_element(:license_report_widget) do
click_element(:expand_report_button)
end
end
def click_on_first_job
first('.js-pipeline-graph-job-link', wait: QA::Support::Repeater::DEFAULT_MAX_WAIT_TIME).click
end
......
......@@ -71,7 +71,6 @@ module QA
end
describe 'License Compliance pipeline reports' do
let(:number_of_licenses_in_fixture) { 2 }
let(:executor) {"qa-runner-#{Time.now.to_i}"}
after do
......@@ -101,29 +100,29 @@ module QA
.new(__dir__)
.join('../../../../../ee/fixtures/secure_premade_reports')
project_push.commit_message = 'Create Secure compatible application to serve premade reports'
end.project.visit!
end
@project.visit!
Page::Project::Menu.perform(&:click_ci_cd_pipelines)
Page::Project::Pipeline::Index.perform(&:wait_for_latest_pipeline_success)
Page::Project::Menu.perform(&:click_on_license_compliance)
end
it 'can approve and deny licenses in the pipeline', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/965' do
Flow::Pipeline.visit_latest_pipeline
EE::Page::Project::Secure::LicenseCompliance.perform do |license_compliance|
license_compliance.open_tab
license_compliance.approve_license approved_license_name
license_compliance.deny_license denied_license_name
end
@project.visit!
Page::Project::Menu.perform(&:click_ci_cd_pipelines)
Page::Project::Pipeline::Index.perform(&:click_on_latest_pipeline)
Page::Project::Pipeline::Show.perform do |pipeline|
pipeline.click_on_licenses
expect(pipeline).to have_license_count_of number_of_licenses_in_fixture
pipeline.approve_license(approved_license_name)
pipeline.deny_license(denied_license_name)
end
Page::Project::Menu.perform(&:click_on_license_compliance)
EE::Page::Project::Secure::LicenseCompliance.perform do |license_compliance|
license_compliance.open_tab
expect(license_compliance).to have_approved_license approved_license_name
expect(license_compliance).to have_denied_license denied_license_name
expect(pipeline).to have_approved_license approved_license_name
expect(pipeline).to have_denied_license denied_license_name
end
end
end
......
......@@ -103,11 +103,21 @@ module QA
@merge_request.visit!
Page::MergeRequest::Show.perform do |show|
show.approve_license_with_mr(approved_license_name)
show.deny_license_with_mr(denied_license_name)
show.wait_for_license_compliance_report
show.click_manage_licenses_button
end
EE::Page::Project::Secure::LicenseCompliance.perform do |license_compliance|
license_compliance.open_tab
license_compliance.approve_license approved_license_name
license_compliance.deny_license denied_license_name
end
@merge_request.visit!
Page::MergeRequest::Show.perform do |show|
show.wait_for_license_compliance_report
show.expand_license_report
expect(show).to have_approved_license approved_license_name
expect(show).to have_denied_license denied_license_name
end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment