diff --git a/td.server/src/controllers/threatmodelcontroller.js b/td.server/src/controllers/threatmodelcontroller.js index 6b22e2fd..2936d561 100644 --- a/td.server/src/controllers/threatmodelcontroller.js +++ b/td.server/src/controllers/threatmodelcontroller.js @@ -54,7 +54,11 @@ const branches = (req, res) => responseWrapper.sendResponseAsync(async () => { const headers = branchesResp[1]; const pageLinks = branchesResp[2]; - const branchNames = branches.map((x) => x.name); + const branchNames = branches.map((x) => ({ + name: x.name, + // Protected branches are not so easy to determine from the API on Bitbucket + protected: x.protected||false + })); const pagination = getPagination(headers, pageLinks, repoInfo.page); diff --git a/td.vue/src/components/AddBranchDialog.vue b/td.vue/src/components/AddBranchDialog.vue index 5dfb657e..d8ed8b3b 100644 --- a/td.vue/src/components/AddBranchDialog.vue +++ b/td.vue/src/components/AddBranchDialog.vue @@ -8,7 +8,7 @@ :title="modalTitle" visible centered - @hide="closeAddBranchDialog" + @hide="closeDialog" hide-footer >
@@ -34,7 +34,7 @@ - @@ -51,7 +51,7 @@ > {{ $t('branch.add') }} - {{ $t('branch.cancel') }} + {{ $t('branch.cancel') }} @@ -61,7 +61,15 @@ import branchActions from '@/store/actions/branch.js'; export default { name: 'AddBranchModal', props: { - branches: Array, + branches: { + type: Array, + validator: (value) => { + return value.every((branch) => { + return typeof branch === 'string' || (branch.value && typeof branch.value === 'string'); + }); + }, + required: true + } }, data() { return { @@ -73,25 +81,30 @@ export default { wait: false }; }, + computed: { + branchNames() { + return this.branches.map(branch => branch.value || branch); + } + }, mounted() { - this.refBranch = this.branches.slice(-1)[0]; + this.refBranch = this.branchNames.slice(-1)[0]; }, watch: { branches: function (newBranches) { if (newBranches.length > 0) { - this.refBranch = newBranches[0]; + this.refBranch = this.branchNames.slice(-1)[0]; } } }, methods: { - closeAddBranchDialog() { - this.$emit('close-add-branch-dialog'); + closeDialog() { + this.$emit('close-dialog'); }, validate() { if (this.newBranchName === '') { this.branchNameError = this.$t('branch.nameRequired'); this.isError = false; - } else if (this.branches.includes(this.newBranchName)) { + } else if (this.branchNames.includes(this.newBranchName)) { this.branchNameError = this.$t('branch.nameExists'); this.isError = false; } else { @@ -107,17 +120,18 @@ export default { return; } this.$store.dispatch(branchActions.create, {branchName: this.newBranchName, refBranch: this.refBranch}); + // sometimes the branch is not immediately available, so we wait for it (only for 30 seconds) for (let i = 0; i < 30; i++) { await this.$store.dispatch(branchActions.fetch, 1); - if (this.branches.includes(this.newBranchName)) { + if (this.branchNames.includes(this.newBranchName)) { break; } await new Promise(resolve => setTimeout(resolve, 1000)); } this.wait = false; - this.closeAddBranchDialog(); + this.closeDialog(); } } }; diff --git a/td.vue/src/components/SelectionPage.vue b/td.vue/src/components/SelectionPage.vue index de14ff39..edfbdcd0 100644 --- a/td.vue/src/components/SelectionPage.vue +++ b/td.vue/src/components/SelectionPage.vue @@ -49,7 +49,16 @@ :key="idx" href="javascript:void(0)" @click="onItemClick(item)"> - {{ isGoogleProvider ? item.name : item }} + {{ item }} + + {{ item.value }} + + @@ -59,7 +68,7 @@ @@ -91,7 +100,15 @@ export default { default: '' }, items: { - required: true + required: true, + type: Array, + validator: (value) => { + return value.every((item) => { + return typeof item === 'string' || (item.value && typeof item.value === 'string') + && (!item.icon || typeof item.icon === 'string') + && (!item.iconTooltip || (typeof item.iconTooltip === 'string' && item.icon)); + }); + } }, page: { required: false, @@ -123,7 +140,8 @@ export default { onEmptyStateClick: { required: false, type: Function, - default: () => {} + default: () => { + } }, showBackItem: { required: false, @@ -133,7 +151,8 @@ export default { onBackClick: { required: false, type: Function, - default: () => {} + default: () => { + } }, isGoogleProvider: { required: false, @@ -143,11 +162,14 @@ export default { }, computed: { displayedItems: function () { - if (!this.filter) { return this.items; } + if (!this.filter) { + return this.items; + } if (this.$props.isGoogleProvider) { return this.items.filter(x => x.name.toLowerCase().includes(this.filter.toLowerCase())); } else { - return this.items.filter(x => x.toLowerCase().includes(this.filter.toLowerCase())); + console.log(this.items); + return this.items.filter(x => (x.value || x).toLowerCase().includes(this.filter.toLowerCase())); } } } diff --git a/td.vue/src/i18n/ar.js b/td.vue/src/i18n/ar.js index 4fee1787..419ffd64 100644 --- a/td.vue/src/i18n/ar.js +++ b/td.vue/src/i18n/ar.js @@ -85,6 +85,7 @@ const ara = { from: 'من القائمة أدناه أو', or: 'أو', chooseRepo: 'اختيار مستودع آخر', + protectedBranch: 'فرع محمي', nameRequired: 'اسم الفرع مطلوب', nameExists: 'اسم الفرع موجود بالفعل', refBranch: 'الفرع المرجعي', diff --git a/td.vue/src/i18n/de.js b/td.vue/src/i18n/de.js index cd258b69..13215c50 100644 --- a/td.vue/src/i18n/de.js +++ b/td.vue/src/i18n/de.js @@ -86,6 +86,7 @@ const deu = { chooseRepo: 'ein anderes Repository auswählen', or: 'oder', addNew: 'füge einen neuen Branch hinzu', + protectedBranch: 'Geschützter Branch', nameRequired: 'Branch Name ist erforderlich', nameExists: 'Branch Name existiert bereits', refBranch: 'Referenz Branch', diff --git a/td.vue/src/i18n/el.js b/td.vue/src/i18n/el.js index b44edbd5..043867d7 100644 --- a/td.vue/src/i18n/el.js +++ b/td.vue/src/i18n/el.js @@ -86,6 +86,7 @@ const ell = { chooseRepo: 'επιλέξτε ένα άλλο αποθετήριο', or: 'ή', addNew: 'να προσθέσετε έναν νέο κλάδο', + protectedBranch: 'Προστατευμένος κλάδος', nameRequired: 'Το όνομα του κλάδου είναι υποχρεωτικό', nameExists: 'Το όνομα του κλάδου υπάρχει ήδη σε αυτό το αποθετήριο', refBranch: 'Κλάδος αναφοράς (Ref Branch)', diff --git a/td.vue/src/i18n/en.js b/td.vue/src/i18n/en.js index 6ed9b703..b5ba0c6b 100644 --- a/td.vue/src/i18n/en.js +++ b/td.vue/src/i18n/en.js @@ -86,6 +86,7 @@ const eng = { chooseRepo: 'choose another repo', or: 'or', addNew: 'add a new branch', + protectedBranch: 'Protected branch', refBranch: 'Reference branch', nameRequired: 'Branch name is required', nameExists: 'Branch name already exists', diff --git a/td.vue/src/i18n/es.js b/td.vue/src/i18n/es.js index ef5071b8..00a5c886 100644 --- a/td.vue/src/i18n/es.js +++ b/td.vue/src/i18n/es.js @@ -85,6 +85,7 @@ const spa = { from: 'de la lista a continuación o', chooseRepo: 'elija otro repositorio', addNew: 'o añadir una nueva rama', + protectedBranch: 'Rama protegida', nameRequired: 'El nombre de la rama es obligatorio', nameExists: 'El nombre de la rama ya existe', refBranch: 'Rama de referencia', diff --git a/td.vue/src/i18n/fi.js b/td.vue/src/i18n/fi.js index 0cd2585d..dc20fe9a 100644 --- a/td.vue/src/i18n/fi.js +++ b/td.vue/src/i18n/fi.js @@ -86,6 +86,7 @@ const fin = { chooseRepo: 'valitse toinen arkisto', or: 'or', addNew: 'lisätä uusi haara', + protectedBranch: 'Suojattu haara', nameRequired: 'Haaran nimi vaaditaan', nameExists: 'Haara on jo olemassa', refBranch: 'Viitehaara', diff --git a/td.vue/src/i18n/fr.js b/td.vue/src/i18n/fr.js index ec9659c7..806fa3da 100644 --- a/td.vue/src/i18n/fr.js +++ b/td.vue/src/i18n/fr.js @@ -86,6 +86,7 @@ const fra = { chooseRepo: 'choisir un autre projet', or: 'ou', addNew: 'ajouter une nouvelle branche', + protectedBranch: 'Branche protégée', nameRequired: 'Le nom de la branche est requis', nameExists: 'Le nom de la branche existe déjà', refBranch: 'branche de référence', diff --git a/td.vue/src/i18n/hi.js b/td.vue/src/i18n/hi.js index bbf7dd51..4e7ac463 100644 --- a/td.vue/src/i18n/hi.js +++ b/td.vue/src/i18n/hi.js @@ -86,6 +86,7 @@ const hin = { chooseRepo: 'एक और रेपो चुनें', or: 'या', addNew: 'नई शाखा जोड़ें', + protectedBranch: 'संरक्षित शाखा', nameRequired: 'एक नाम आवश्यक है', nameExists: 'एक शाखा इस नाम से पहले ही मौजूद है', refBranch: 'आधार शाखा', diff --git a/td.vue/src/i18n/id.js b/td.vue/src/i18n/id.js index b54d35b5..798a4cbd 100644 --- a/td.vue/src/i18n/id.js +++ b/td.vue/src/i18n/id.js @@ -86,6 +86,7 @@ const id = { chooseRepo: 'pilih repo lain', or: 'atau', addNew: 'tambahkan cabang baru', + protectedBranch: 'Cabang dilindungi', nameRequired: 'Nama cabang diperlukan', nameExists: 'Nama cabang sudah ada', refBranch: 'Cabang Referensi', diff --git a/td.vue/src/i18n/ja.js b/td.vue/src/i18n/ja.js index 1356e369..7cf0e5b4 100644 --- a/td.vue/src/i18n/ja.js +++ b/td.vue/src/i18n/ja.js @@ -82,6 +82,7 @@ const jpn = { chooseRepo: 'リポジトリの切り替え', or: 'または', addNew: '新しいブランチを追加します。', + protectedBranch: '保護されたブランチ', nameRequired: 'ブランチ名が必要です。', nameExists: 'ブランチ名が既に存在します。', refBranch: 'リファレンスブランチ', diff --git a/td.vue/src/i18n/ms.js b/td.vue/src/i18n/ms.js index d102d9fd..3787eda5 100644 --- a/td.vue/src/i18n/ms.js +++ b/td.vue/src/i18n/ms.js @@ -86,6 +86,7 @@ const ms = { chooseRepo: 'pilih repo lain', or: 'atau', addNew: 'tambah cawangan baru', + protectedBranch: 'Cawangan Dilindungi', nameRequired: 'Nama cawangan diperlukan', nameExists: 'Nama cawangan sudah wujud', refBranch: 'Cawangan Rujukan', diff --git a/td.vue/src/i18n/pt.js b/td.vue/src/i18n/pt.js index 57280567..87602304 100644 --- a/td.vue/src/i18n/pt.js +++ b/td.vue/src/i18n/pt.js @@ -86,6 +86,7 @@ const por = { chooseRepo: 'escolher outro repositório', or: 'ou', addNew: 'adicionar um novo branch', + protectedBranch: 'Branch protegida', nameRequired: 'Nome da branch é obrigatório', nameExists: 'Nome da branch já existe', refBranch: 'Branch de referência', diff --git a/td.vue/src/i18n/ru.js b/td.vue/src/i18n/ru.js index 20d60450..aac49444 100644 --- a/td.vue/src/i18n/ru.js +++ b/td.vue/src/i18n/ru.js @@ -86,6 +86,7 @@ const rus = { chooseRepo: 'choose another repo', or: 'or', addNew: 'add a new branch', + protectedBranch: 'Protected branch', nameRequired: 'Branch name is required', nameExists: 'Branch name already exists', refBranch: 'Reference branch', diff --git a/td.vue/src/i18n/uk.js b/td.vue/src/i18n/uk.js index acd281df..267bd8b1 100644 --- a/td.vue/src/i18n/uk.js +++ b/td.vue/src/i18n/uk.js @@ -86,6 +86,7 @@ const ukr = { chooseRepo: 'choose another repo', or: 'or', addNew: 'add a new branch', + protectedBranch: 'Protected branch', nameRequired: 'Branch name is required', nameExists: 'Branch name already exists', refBranch: 'Reference branch', diff --git a/td.vue/src/i18n/zh.js b/td.vue/src/i18n/zh.js index 0ab82109..52dc456e 100644 --- a/td.vue/src/i18n/zh.js +++ b/td.vue/src/i18n/zh.js @@ -86,6 +86,7 @@ const zho = { chooseRepo: '选择另一个源', or: '或者', addNew: '添加新分支', + protectedBranch: '受保护的分支', nameRequired: '分支名称是必需的', nameExists: '分支名称已存在', refBranch: '参考分支', diff --git a/td.vue/src/plugins/fontawesome-vue.js b/td.vue/src/plugins/fontawesome-vue.js index 8d95eab6..9c954904 100644 --- a/td.vue/src/plugins/fontawesome-vue.js +++ b/td.vue/src/plugins/fontawesome-vue.js @@ -31,10 +31,10 @@ import { faPrint, faProjectDiagram, faDiagramProject, + faLock } from '@fortawesome/free-solid-svg-icons'; import {faBitbucket, faGithub, faGitlab, faVuejs, faGoogle, faGoogleDrive} from '@fortawesome/free-brands-svg-icons'; - // Add icons to the library for use library.add( faSignOutAlt, @@ -69,7 +69,8 @@ library.add( faProjectDiagram, faDiagramProject, faGoogle, - faGoogleDrive + faGoogleDrive, + faLock ); Vue.component('font-awesome-icon', FontAwesomeIcon); diff --git a/td.vue/src/views/git/BranchAccess.vue b/td.vue/src/views/git/BranchAccess.vue index 05447f9d..986278c4 100644 --- a/td.vue/src/views/git/BranchAccess.vue +++ b/td.vue/src/views/git/BranchAccess.vue @@ -25,7 +25,7 @@ + @close-dialog="toggleNewBranchDialog()"/> @@ -51,7 +51,16 @@ export default { }; }, computed: mapState({ - branches: (state) => state.branch.all, + branches: (state) => state.branch.all.map((branch) => { + if(branch['protected']){ + return { + value: branch.name, + icon: 'lock', + iconTooltip: 'branch.protectedBranch', + }; + } + return branch.name; + }), provider: (state) => state.provider.selected, providerType: (state) => getProviderType(state.provider.selected), providerUri: (state) => state.provider.providerUri, diff --git a/td.vue/tests/unit/components/addBranchDialog.spec.js b/td.vue/tests/unit/components/addBranchDialog.spec.js index 8f6b4a04..8c96c5be 100644 --- a/td.vue/tests/unit/components/addBranchDialog.spec.js +++ b/td.vue/tests/unit/components/addBranchDialog.spec.js @@ -12,10 +12,10 @@ describe('components/AddBranchDialog.vue', () => { describe('with data', () => { const branches = ['main', 'develop', 'feature']; - let closeAddBranchDialog; + let closeDialog; beforeEach(() => { - closeAddBranchDialog = jest.fn(); + closeDialog = jest.fn(); wrapper = shallowMount(AddBranchDialog, { localVue, propsData: { @@ -25,7 +25,7 @@ describe('components/AddBranchDialog.vue', () => { $t: key => key } }); - wrapper.vm.closeAddBranchDialog = closeAddBranchDialog; + wrapper.vm.closeDialog = closeDialog; }); it('displays the modal', () => { @@ -48,14 +48,14 @@ describe('components/AddBranchDialog.vue', () => { expect(wrapper.findAllComponents(BButton).at(1).text()).toBe('branch.cancel'); }); - it('calls closeAddBranchDialog on cancel button click', async () => { + it('calls closeDialog on cancel button click', async () => { await wrapper.findAllComponents(BButton).at(1).trigger('click'); - expect(closeAddBranchDialog).toHaveBeenCalled(); + expect(closeDialog).toHaveBeenCalled(); }); }); describe('validation', () => { - const branches = ['main', 'develop', 'feature']; + const branches = ['develop', 'feature', 'main']; beforeEach(() => { wrapper = shallowMount(AddBranchDialog, { @@ -89,13 +89,14 @@ describe('components/AddBranchDialog.vue', () => { }); describe('addBranch', () => { - let closeAddBranchDialog, dispatch; + let closeDialog, dispatch; beforeEach(() => { const branches = ['main', 'develop', 'feature']; - closeAddBranchDialog = jest.fn(); + closeDialog = jest.fn(); dispatch = jest.fn((branchActions, {branchName}) => { - branches.push(branchName); + if(branchActions === 'BRANCH_CREATE') + branches.push(branchName); }); wrapper = shallowMount(AddBranchDialog, { localVue, @@ -107,21 +108,26 @@ describe('components/AddBranchDialog.vue', () => { $store: { dispatch } + }, + data() { + return { + newBranchName: 'new-branch', + refBranch: 'main' + }; } }); - wrapper.vm.closeAddBranchDialog = closeAddBranchDialog; + wrapper.vm.closeDialog = closeDialog; }); it('dispatches the create action with correct payload', async () => { - await wrapper.setData({ newBranchName: 'new-branch', refBranch: 'main' }); await wrapper.vm.addBranch(); + expect(wrapper.vm.branches).toContain('new-branch'); }); it('closes the dialog after adding the branch', async () => { - const newBranchName = 'new-branch'; - await wrapper.setData({ newBranchName, refBranch: 'main'}); + await wrapper.setData({ newBranchName: 'new-branch', refBranch: 'main'}); await wrapper.vm.addBranch(); - expect(closeAddBranchDialog).toHaveBeenCalled(); + expect(closeDialog).toHaveBeenCalled(); }); }); }); diff --git a/td.vue/tests/unit/components/selectionPage.spec.js b/td.vue/tests/unit/components/selectionPage.spec.js index f59a6e01..1723312b 100644 --- a/td.vue/tests/unit/components/selectionPage.spec.js +++ b/td.vue/tests/unit/components/selectionPage.spec.js @@ -12,7 +12,11 @@ describe('components/SelectionPage.vue', () => { }); describe('with data', () => { - const items = [ 'one', 'two', 'three', 'four' ]; + const items = [ 'one', 'two', 'three', 'four', ({ + value: 'five', + icon: 'lock', + iconTooltip: 'foobar', + }) ]; let onItemClick; beforeEach(() => { @@ -41,7 +45,7 @@ describe('components/SelectionPage.vue', () => { it('displays the items', () => { expect(wrapper.findAllComponents(BListGroupItem).at(1).text()).toEqual(items[1]); }); - + it('filters items based on the search bar', async () => { await wrapper.setData({ filter: 'FOUR' }); expect(wrapper.findComponent(BListGroupItem).text()).toEqual('four'); @@ -51,6 +55,12 @@ describe('components/SelectionPage.vue', () => { await wrapper.findComponent(BListGroupItem).trigger('click'); expect(onItemClick).toHaveBeenCalledTimes(1); }); + + it('display the icon only on the last item', () => { + expect(wrapper.findAllComponents(BListGroupItem).at(3).find('b-icon-stub').exists()).toBeFalsy(); + expect(wrapper.findAllComponents(BListGroupItem).at(3).find('span').text()).toEqual(items[3]); + expect(wrapper.findAllComponents(BListGroupItem).at(4).html()).toMatch(``); + }); }); describe('empty state with action', () => { @@ -73,7 +83,7 @@ describe('components/SelectionPage.vue', () => { } }); }); - + it('shows the empty state', () => { expect(wrapper.findComponent(BListGroupItem).text()).toEqual(emptyStateText); }); diff --git a/td.vue/tests/unit/views/branchAccess.spec.js b/td.vue/tests/unit/views/branchAccess.spec.js index 00e4af6e..d6f7869a 100644 --- a/td.vue/tests/unit/views/branchAccess.spec.js +++ b/td.vue/tests/unit/views/branchAccess.spec.js @@ -43,7 +43,7 @@ describe('views/BranchAccess.vue', () => { }, branch: { selected: 'someBranch', - all: ['b1', 'b2', 'b3'], + all: [{ name: 'b1', protected: true }, { name: 'b2', protected: false }, { name: 'b3', protected: false }], page: 1, pageNext: true, pagePrev: false @@ -53,7 +53,7 @@ describe('views/BranchAccess.vue', () => { } }, actions: { - [BRANCH_FETCH]: () => { }, + [BRANCH_FETCH]: () => Promise.resolve(getMockStore().state.branch.all), [BRANCH_SELECTED]: () => { }, [PROVIDER_SELECTED]: () => { }, [REPOSITORY_CLEAR]: () => { }, @@ -90,6 +90,14 @@ describe('views/BranchAccess.vue', () => { } }); expect(mockStore.dispatch).toHaveBeenCalledWith(BRANCH_FETCH, 1); + expect(wrapper.vm.branches).toEqual([ + { + value: 'b1', + icon: 'lock', + iconTooltip: 'branch.protectedBranch' + }, + 'b2','b3' + ]); }); });