Commit 8633c996 authored by Miguel Rincon's avatar Miguel Rincon Committed by Kerri Miller

Migrate triggers settings table to Vue

So we can add more functionality to the triggers table we
would like to move the table from HAML to Vue.

As a temporary measure, this change loads the data from a JSON
string that gets added to the HTML of the page, instead of loading
the data via AJAX.
parent 8bf5e565
<script>
import { GlTable, GlButton, GlBadge, GlTooltipDirective } from '@gitlab/ui';
import { s__ } from '~/locale';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
export default {
components: {
GlTable,
GlButton,
GlBadge,
ClipboardButton,
TooltipOnTruncate,
UserAvatarLink,
TimeAgoTooltip,
},
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
triggers: {
type: Array,
required: false,
default: () => [],
},
},
fields: [
{
key: 'token',
label: s__('Pipelines|Token'),
},
{
key: 'description',
label: s__('Pipelines|Description'),
},
{
key: 'owner',
label: s__('Pipelines|Owner'),
},
{
key: 'lastUsed',
label: s__('Pipelines|Last Used'),
},
{
key: 'actions',
label: '',
tdClass: 'gl-text-right gl-white-space-nowrap',
},
],
};
</script>
<template>
<div>
<gl-table
v-if="triggers.length"
:fields="$options.fields"
:items="triggers"
class="triggers-list"
responsive
>
<template #cell(token)="{item}">
{{ item.token }}
<clipboard-button
v-if="item.hasTokenExposed"
:text="item.token"
data-testid="clipboard-btn"
data-qa-selector="clipboard_button"
:title="s__('Pipelines|Copy trigger token')"
css-class="gl-border-none gl-py-0 gl-px-2"
/>
<div class="label-container">
<gl-badge v-if="!item.canAccessProject" variant="danger">
<span
v-gl-tooltip.viewport
boundary="viewport"
:title="s__('Pipelines|Trigger user has insufficient permissions to project')"
>{{ s__('Pipelines|invalid') }}</span
>
</gl-badge>
</div>
</template>
<template #cell(description)="{item}">
<tooltip-on-truncate
:title="item.description"
truncate-target="child"
placement="top"
class="trigger-description gl-display-flex"
>
<div class="gl-flex-fill-1 gl-text-truncate">{{ item.description }}</div>
</tooltip-on-truncate>
</template>
<template #cell(owner)="{item}">
<span class="trigger-owner sr-only">{{ item.owner.name }}</span>
<user-avatar-link
v-if="item.owner"
:link-href="item.owner.path"
:img-src="item.owner.avatarUrl"
:tooltip-text="item.owner.name"
:img-alt="item.owner.name"
/>
</template>
<template #cell(lastUsed)="{item}">
<time-ago-tooltip v-if="item.lastUsed" :time="item.lastUsed" />
<span v-else>{{ __('Never') }}</span>
</template>
<template #cell(actions)="{item}">
<gl-button
:title="s__('Pipelines|Edit')"
icon="pencil"
data-testid="edit-btn"
:href="item.editProjectTriggerPath"
/>
<gl-button
:title="s__('Pipelines|Revoke')"
icon="remove"
variant="warning"
:data-confirm="
s__(
'Pipelines|By revoking a trigger you will break any processes making use of it. Are you sure?',
)
"
data-method="delete"
rel="nofollow"
class="gl-ml-3"
data-testid="trigger_revoke_button"
data-qa-selector="trigger_revoke_button"
:href="item.projectTriggerPath"
/>
</template>
</gl-table>
<div
v-else
data-testid="no_triggers_content"
data-qa-selector="no_triggers_content"
class="settings-message gl-text-center gl-mb-3"
>
{{ s__('Pipelines|No triggers have been created yet. Add one using the form above.') }}
</div>
</div>
</template>
import Vue from 'vue';
import TriggersList from './components/triggers_list.vue';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
const parseJsonArray = triggers => {
try {
return convertObjectPropsToCamelCase(JSON.parse(triggers), { deep: true });
} catch {
return [];
}
};
export default (containerId = 'js-ci-pipeline-triggers-list') => {
const containerEl = document.getElementById(containerId);
// Note: Remove this check when FF `ci_pipeline_triggers_settings_vue_ui` is removed.
if (!containerEl) {
return null;
}
const triggers = parseJsonArray(containerEl.dataset.triggers);
return new Vue({
el: containerEl,
components: {
TriggersList,
},
render(h) {
return h(TriggersList, {
props: {
triggers,
},
});
},
});
};
...@@ -4,6 +4,7 @@ import AjaxVariableList from '~/ci_variable_list/ajax_variable_list'; ...@@ -4,6 +4,7 @@ import AjaxVariableList from '~/ci_variable_list/ajax_variable_list';
import registrySettingsApp from '~/registry/settings/registry_settings_bundle'; import registrySettingsApp from '~/registry/settings/registry_settings_bundle';
import initVariableList from '~/ci_variable_list'; import initVariableList from '~/ci_variable_list';
import initDeployFreeze from '~/deploy_freeze'; import initDeployFreeze from '~/deploy_freeze';
import initSettingsPipelinesTriggers from '~/ci_settings_pipeline_triggers';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
// Initialize expandable settings panels // Initialize expandable settings panels
...@@ -42,4 +43,6 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -42,4 +43,6 @@ document.addEventListener('DOMContentLoaded', () => {
registrySettingsApp(); registrySettingsApp();
initDeployFreeze(); initDeployFreeze();
initSettingsPipelinesTriggers();
}); });
...@@ -5,6 +5,10 @@ ...@@ -5,6 +5,10 @@
} }
} }
.trigger-description {
max-width: 100px;
}
.trigger-actions { .trigger-actions {
white-space: nowrap; white-space: nowrap;
......
...@@ -12,6 +12,11 @@ module Projects ...@@ -12,6 +12,11 @@ module Projects
end end
def show def show
if Feature.enabled?(:ci_pipeline_triggers_settings_vue_ui, @project)
@triggers_json = ::Ci::TriggerSerializer.new.represent(
@project.triggers, current_user: current_user, project: @project
).to_json
end
end end
def update def update
...@@ -116,6 +121,7 @@ module Projects ...@@ -116,6 +121,7 @@ module Projects
def define_triggers_variables def define_triggers_variables
@triggers = @project.triggers @triggers = @project.triggers
.present(current_user: current_user) .present(current_user: current_user)
@trigger = ::Ci::Trigger.new @trigger = ::Ci::Trigger.new
.present(current_user: current_user) .present(current_user: current_user)
end end
......
# frozen_string_literal: true
module Ci
class TriggerEntity < Grape::Entity
include Gitlab::Routing
include Gitlab::Allowable
expose :description
expose :owner, using: UserEntity
expose :last_used
expose :token do |trigger|
can_admin_trigger?(trigger) ? trigger.token : trigger.short_token
end
expose :has_token_exposed do |trigger|
can_admin_trigger?(trigger)
end
expose :can_access_project do |trigger|
trigger.can_access_project?
end
expose :project_trigger_path, if: -> (trigger) { can_manage_trigger?(trigger) } do |trigger|
project_trigger_path(options[:project], trigger)
end
expose :edit_project_trigger_path, if: -> (trigger) { can_admin_trigger?(trigger) } do |trigger|
edit_project_trigger_path(options[:project], trigger)
end
private
def can_manage_trigger?(trigger)
can?(options[:current_user], :manage_trigger, trigger)
end
def can_admin_trigger?(trigger)
can?(options[:current_user], :admin_trigger, trigger)
end
end
end
# frozen_string_literal: true
module Ci
class TriggerSerializer < BaseSerializer
entity ::Ci::TriggerEntity
end
end
...@@ -6,23 +6,26 @@ ...@@ -6,23 +6,26 @@
.card-body .card-body
= render "projects/triggers/form", btn_text: "Add trigger" = render "projects/triggers/form", btn_text: "Add trigger"
%hr %hr
- if @triggers.any? - if Feature.enabled?(:ci_pipeline_triggers_settings_vue_ui, @project)
.table-responsive.triggers-list #js-ci-pipeline-triggers-list.triggers-list{ data: { triggers: @triggers_json } }
%table.table
%thead
%th
%strong Token
%th
%strong Description
%th
%strong Owner
%th
%strong Last used
%th
= render partial: 'projects/triggers/trigger', collection: @triggers, as: :trigger
- else - else
%p.settings-message.text-center.gl-mb-3 - if @triggers.any?
No triggers have been created yet. Add one using the form above. .table-responsive.triggers-list
%table.table
%thead
%th
%strong Token
%th
%strong Description
%th
%strong Owner
%th
%strong Last used
%th
= render partial: 'projects/triggers/trigger', collection: @triggers, as: :trigger
- else
%p.settings-message.text-center.gl-mb-3{ data: { testid: 'no_triggers_content' } }
No triggers have been created yet. Add one using the form above.
.card-footer .card-footer
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
%td %td
- if trigger.has_token_exposed? - if trigger.has_token_exposed?
%span= trigger.token %span= trigger.token
= clipboard_button(text: trigger.token, title: _("Copy trigger token")) = clipboard_button(text: trigger.token, title: _("Copy trigger token"), testid: 'clipboard-btn')
- else - else
%span= trigger.short_token %span= trigger.short_token
...@@ -33,5 +33,5 @@ ...@@ -33,5 +33,5 @@
= link_to edit_project_trigger_path(@project, trigger), method: :get, title: "Edit", class: "btn btn-default btn-sm" do = link_to edit_project_trigger_path(@project, trigger), method: :get, title: "Edit", class: "btn btn-default btn-sm" do
= sprite_icon('pencil') = sprite_icon('pencil')
- if can?(current_user, :manage_trigger, trigger) - if can?(current_user, :manage_trigger, trigger)
= link_to project_trigger_path(@project, trigger), data: { confirm: revoke_trigger_confirmation }, method: :delete, title: "Revoke", class: "btn btn-default btn-warning btn-sm btn-trigger-revoke" do = link_to project_trigger_path(@project, trigger), data: { confirm: revoke_trigger_confirmation, testid: 'trigger_revoke_button' }, method: :delete, title: "Revoke", class: "btn btn-default btn-warning btn-sm btn-trigger-revoke" do
= sprite_icon('remove') = sprite_icon('remove')
---
name: ci_pipeline_triggers_settings_vue_ui
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/41864
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/247486
group: group::continuous integration
type: development
default_enabled: false
...@@ -18627,6 +18627,9 @@ msgstr "" ...@@ -18627,6 +18627,9 @@ msgstr ""
msgid "Pipelines|Build with confidence" msgid "Pipelines|Build with confidence"
msgstr "" msgstr ""
msgid "Pipelines|By revoking a trigger you will break any processes making use of it. Are you sure?"
msgstr ""
msgid "Pipelines|CI Lint" msgid "Pipelines|CI Lint"
msgstr "" msgstr ""
...@@ -18639,6 +18642,15 @@ msgstr "" ...@@ -18639,6 +18642,15 @@ msgstr ""
msgid "Pipelines|Continuous Integration can help catch bugs by running your tests automatically, while Continuous Deployment can help you deliver code to your product environment." msgid "Pipelines|Continuous Integration can help catch bugs by running your tests automatically, while Continuous Deployment can help you deliver code to your product environment."
msgstr "" msgstr ""
msgid "Pipelines|Copy trigger token"
msgstr ""
msgid "Pipelines|Description"
msgstr ""
msgid "Pipelines|Edit"
msgstr ""
msgid "Pipelines|Get started with Pipelines" msgid "Pipelines|Get started with Pipelines"
msgstr "" msgstr ""
...@@ -18654,15 +18666,27 @@ msgstr "" ...@@ -18654,15 +18666,27 @@ msgstr ""
msgid "Pipelines|It is recommended the code is reviewed thoroughly before running this pipeline with the parent project's CI resource." msgid "Pipelines|It is recommended the code is reviewed thoroughly before running this pipeline with the parent project's CI resource."
msgstr "" msgstr ""
msgid "Pipelines|Last Used"
msgstr ""
msgid "Pipelines|Loading Pipelines" msgid "Pipelines|Loading Pipelines"
msgstr "" msgstr ""
msgid "Pipelines|More Information" msgid "Pipelines|More Information"
msgstr "" msgstr ""
msgid "Pipelines|No triggers have been created yet. Add one using the form above."
msgstr ""
msgid "Pipelines|Owner"
msgstr ""
msgid "Pipelines|Project cache successfully reset." msgid "Pipelines|Project cache successfully reset."
msgstr "" msgstr ""
msgid "Pipelines|Revoke"
msgstr ""
msgid "Pipelines|Run Pipeline" msgid "Pipelines|Run Pipeline"
msgstr "" msgstr ""
...@@ -18687,6 +18711,15 @@ msgstr "" ...@@ -18687,6 +18711,15 @@ msgstr ""
msgid "Pipelines|This project is not currently set up to run pipelines." msgid "Pipelines|This project is not currently set up to run pipelines."
msgstr "" msgstr ""
msgid "Pipelines|Token"
msgstr ""
msgid "Pipelines|Trigger user has insufficient permissions to project"
msgstr ""
msgid "Pipelines|invalid"
msgstr ""
msgid "Pipelines|parent" msgid "Pipelines|parent"
msgstr "" msgstr ""
......
...@@ -19,114 +19,132 @@ RSpec.describe 'Triggers', :js do ...@@ -19,114 +19,132 @@ RSpec.describe 'Triggers', :js do
visit project_settings_ci_cd_path(@project) visit project_settings_ci_cd_path(@project)
end end
describe 'create trigger workflow' do shared_examples 'triggers page' do
it 'prevents adding new trigger with no description' do describe 'create trigger workflow' do
fill_in 'trigger_description', with: '' it 'prevents adding new trigger with no description' do
click_button 'Add trigger' fill_in 'trigger_description', with: ''
click_button 'Add trigger'
# See if input has error due to empty value
expect(page.find('form.gl-show-field-errors .gl-field-error')).to be_visible # See if input has error due to empty value
end expect(page.find('form.gl-show-field-errors .gl-field-error')).to be_visible
end
it 'adds new trigger with description' do it 'adds new trigger with description' do
fill_in 'trigger_description', with: 'trigger desc' fill_in 'trigger_description', with: 'trigger desc'
click_button 'Add trigger' click_button 'Add trigger'
# See if "trigger creation successful" message displayed and description and owner are correct aggregate_failures 'display creation notice and trigger is created' do
expect(page.find('.flash-notice')).to have_content 'Trigger was created successfully.' expect(page.find('.flash-notice')).to have_content 'Trigger was created successfully.'
expect(page.find('.triggers-list')).to have_content 'trigger desc' expect(page.find('.triggers-list')).to have_content 'trigger desc'
expect(page.find('.triggers-list .trigger-owner')).to have_content user.name expect(page.find('.triggers-list .trigger-owner')).to have_content user.name
end
end
end end
end
describe 'edit trigger workflow' do
let(:new_trigger_title) { 'new trigger' }
it 'click on edit trigger opens edit trigger page' do describe 'edit trigger workflow' do
create(:ci_trigger, owner: user, project: @project, description: trigger_title) let(:new_trigger_title) { 'new trigger' }
visit project_settings_ci_cd_path(@project)
# See if edit page has correct descrption it 'click on edit trigger opens edit trigger page' do
find('a[title="Edit"]').send_keys(:return) create(:ci_trigger, owner: user, project: @project, description: trigger_title)
expect(page.find('#trigger_description').value).to have_content 'trigger desc' visit project_settings_ci_cd_path(@project)
end
it 'edit trigger and save' do # See if edit page has correct descrption
create(:ci_trigger, owner: user, project: @project, description: trigger_title) find('a[title="Edit"]').send_keys(:return)
visit project_settings_ci_cd_path(@project) expect(page.find('#trigger_description').value).to have_content 'trigger desc'
end
# See if edit page opens, then fill in new description and save it 'edit trigger and save' do
find('a[title="Edit"]').send_keys(:return) create(:ci_trigger, owner: user, project: @project, description: trigger_title)
fill_in 'trigger_description', with: new_trigger_title visit project_settings_ci_cd_path(@project)
click_button 'Save trigger'
# See if "trigger updated successfully" message displayed and description and owner are correct # See if edit page opens, then fill in new description and save
expect(page.find('.flash-notice')).to have_content 'Trigger was successfully updated.' find('a[title="Edit"]').send_keys(:return)
expect(page.find('.triggers-list')).to have_content new_trigger_title fill_in 'trigger_description', with: new_trigger_title
expect(page.find('.triggers-list .trigger-owner')).to have_content user.name click_button 'Save trigger'
end
end
describe 'trigger "Revoke" workflow' do aggregate_failures 'display update notice and trigger is updated' do
before do expect(page.find('.flash-notice')).to have_content 'Trigger was successfully updated.'
create(:ci_trigger, owner: user2, project: @project, description: trigger_title) expect(page.find('.triggers-list')).to have_content new_trigger_title
visit project_settings_ci_cd_path(@project) expect(page.find('.triggers-list .trigger-owner')).to have_content user.name
end
end
end end
it 'button "Revoke" has correct alert' do describe 'trigger "Revoke" workflow' do
expected_alert = 'By revoking a trigger you will break any processes making use of it. Are you sure?' before do
expect(page.find('a.btn-trigger-revoke')['data-confirm']).to eq expected_alert create(:ci_trigger, owner: user2, project: @project, description: trigger_title)
end visit project_settings_ci_cd_path(@project)
end
it 'revoke trigger' do it 'button "Revoke" has correct alert' do
# See if "Revoke" on trigger works post trigger creation expected_alert = 'By revoking a trigger you will break any processes making use of it. Are you sure?'
page.accept_confirm do expect(page.find('[data-testid="trigger_revoke_button"]')['data-confirm']).to eq expected_alert
find('a.btn-trigger-revoke').send_keys(:return)
end end
expect(page.find('.flash-notice')).to have_content 'Trigger removed' it 'revoke trigger' do
expect(page).to have_selector('p.settings-message.text-center.gl-mb-3') # See if "Revoke" on trigger works post trigger creation
end page.accept_confirm do
end find('[data-testid="trigger_revoke_button"]').send_keys(:return)
end
describe 'show triggers workflow' do aggregate_failures 'trigger is removed' do
it 'contains trigger description placeholder' do expect(page.find('.flash-notice')).to have_content 'Trigger removed'
expect(page.find('#trigger_description')['placeholder']).to eq 'Trigger description' expect(page).to have_css('[data-testid="no_triggers_content"]')
end
end
end end
it 'show "invalid" badge for trigger with owner having insufficient permissions' do describe 'show triggers workflow' do
create(:ci_trigger, owner: guest_user, project: @project, description: trigger_title) it 'contains trigger description placeholder' do
visit project_settings_ci_cd_path(@project) expect(page.find('#trigger_description')['placeholder']).to eq 'Trigger description'
end
expect(page.find('.triggers-list')).to have_content 'invalid' it 'show "invalid" badge for trigger with owner having insufficient permissions' do
expect(page.find('.triggers-list')).not_to have_selector('a[title="Edit"]') create(:ci_trigger, owner: guest_user, project: @project, description: trigger_title)
end visit project_settings_ci_cd_path(@project)
aggregate_failures 'has invalid badge and no edit link' do
expect(page.find('.triggers-list')).to have_content 'invalid'
expect(page.find('.triggers-list')).not_to have_selector('a[title="Edit"]')
end
end
it 'do not show "Edit" or full token for not owned trigger' do it 'do not show "Edit" or full token for not owned trigger' do
# Create trigger with user different from current_user # Create trigger with user different from current_user
create(:ci_trigger, owner: user2, project: @project, description: trigger_title) create(:ci_trigger, owner: user2, project: @project, description: trigger_title)
visit project_settings_ci_cd_path(@project) visit project_settings_ci_cd_path(@project)
aggregate_failures 'shows truncated token, no clipboard button and no edit link' do
expect(page.find('.triggers-list')).to have_content(@project.triggers.first.token[0..3])
expect(page.find('.triggers-list')).not_to have_selector('[data-testid="clipboard-btn"]')
expect(page.find('.triggers-list .trigger-owner')).not_to have_content user.name
expect(page.find('.triggers-list')).not_to have_selector('a[title="Edit"]')
end
end
# See if trigger not owned by current_user shows only first few token chars and doesn't have copy-to-clipboard button it 'show "Edit" and full token for owned trigger' do
expect(page.find('.triggers-list')).to have_content(@project.triggers.first.token[0..3]) create(:ci_trigger, owner: user, project: @project, description: trigger_title)
expect(page.find('.triggers-list')).not_to have_selector('button.btn-clipboard') visit project_settings_ci_cd_path(@project)
# See if trigger owner name doesn't match with current_user and trigger is non-editable aggregate_failures 'shows full token, clipboard button and edit link' do
expect(page.find('.triggers-list .trigger-owner')).not_to have_content user.name expect(page.find('.triggers-list')).to have_content @project.triggers.first.token
expect(page.find('.triggers-list')).not_to have_selector('a[title="Edit"]') expect(page.find('.triggers-list')).to have_selector('[data-testid="clipboard-btn"]')
expect(page.find('.triggers-list .trigger-owner')).to have_content user.name
expect(page.find('.triggers-list')).to have_selector('a[title="Edit"]')
end
end
end end
end
it 'show "Edit" and full token for owned trigger' do context 'when ci_pipeline_triggers_settings_vue_ui is enabled' do
create(:ci_trigger, owner: user, project: @project, description: trigger_title) it_behaves_like 'triggers page'
visit project_settings_ci_cd_path(@project) end
# See if trigger shows full token and has copy-to-clipboard button
expect(page.find('.triggers-list')).to have_content @project.triggers.first.token
expect(page.find('.triggers-list')).to have_selector('button.btn-clipboard')
# See if trigger owner name matches with current_user and is editable context 'when ci_pipeline_triggers_settings_vue_ui is disabled' do
expect(page.find('.triggers-list .trigger-owner')).to have_content user.name before do
expect(page.find('.triggers-list')).to have_selector('a[title="Edit"]') stub_feature_flags(ci_pipeline_triggers_settings_vue_ui: false)
end end
it_behaves_like 'triggers page'
end end
end end
{
"type": "object",
"required": [
"description",
"owner",
"last_used",
"has_token_exposed",
"token",
"can_access_project"
],
"properties": {
"description": {
"type": ["string", "null"]
},
"owner": {
"type": "object",
"$ref": "user.json"
},
"last_used": {
"type": ["datetime", "null"]
},
"token": {
"type": "string"
},
"has_token_exposed": {
"type": "boolean"
},
"can_access_project": {
"type": "boolean"
},
"edit_project_trigger_path": {
"type": "string"
},
"project_trigger_path": {
"type": "string"
}
},
"additionalProperties": false
}
import { mount } from '@vue/test-utils';
import { GlTable, GlBadge } from '@gitlab/ui';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import TriggersList from '~/ci_settings_pipeline_triggers/components/triggers_list.vue';
import { triggers } from '../mock_data';
describe('TriggersList', () => {
let wrapper;
const createComponent = (props = {}) => {
wrapper = mount(TriggersList, {
propsData: { triggers, ...props },
});
};
const findTable = () => wrapper.find(GlTable);
const findHeaderAt = i => wrapper.findAll('thead th').at(i);
const findRows = () => wrapper.findAll('tbody tr');
const findRowAt = i => findRows().at(i);
const findCell = (i, col) =>
findRowAt(i)
.findAll('td')
.at(col);
const findClipboardBtn = i => findCell(i, 0).find(ClipboardButton);
const findInvalidBadge = i => findCell(i, 0).find(GlBadge);
const findEditBtn = i => findRowAt(i).find('[data-testid="edit-btn"]');
const findRevokeBtn = i => findRowAt(i).find('[data-testid="trigger_revoke_button"]');
beforeEach(() => {
createComponent();
return wrapper.vm.$nextTick();
});
it('displays a table with expected headers', () => {
const headers = ['Token', 'Description', 'Owner', 'Last Used', ''];
headers.forEach((header, i) => {
expect(findHeaderAt(i).text()).toBe(header);
});
});
it('displays a table with rows', () => {
expect(findRows()).toHaveLength(triggers.length);
const [trigger] = triggers;
expect(findCell(0, 0).text()).toBe(trigger.token);
expect(findCell(0, 1).text()).toBe(trigger.description);
expect(findCell(0, 2).text()).toContain(trigger.owner.name);
});
it('displays a "copy to cliboard" button for exposed tokens', () => {
expect(findClipboardBtn(0).exists()).toBe(true);
expect(findClipboardBtn(0).props('text')).toBe(triggers[0].token);
expect(findClipboardBtn(1).exists()).toBe(false);
});
it('displays an "invalid" label for tokens without access', () => {
expect(findInvalidBadge(0).exists()).toBe(false);
expect(findInvalidBadge(1).exists()).toBe(true);
});
it('displays a time ago label when last used', () => {
expect(findCell(0, 3).text()).toBe('Never');
expect(
findCell(1, 3)
.find(TimeAgoTooltip)
.props('time'),
).toBe(triggers[1].lastUsed);
});
it('displays actions in a rows', () => {
const [data] = triggers;
expect(findEditBtn(0).attributes('href')).toBe(data.editProjectTriggerPath);
expect(findRevokeBtn(0).attributes('href')).toBe(data.projectTriggerPath);
expect(findRevokeBtn(0).attributes('data-method')).toBe('delete');
expect(findRevokeBtn(0).attributes('data-confirm')).toBeTruthy();
});
describe('when there are no triggers set', () => {
beforeEach(() => {
createComponent({ triggers: [] });
});
it('does not display a table', () => {
expect(findTable().exists()).toBe(false);
});
it('displays a message', () => {
expect(wrapper.text()).toBe(
'No triggers have been created yet. Add one using the form above.',
);
});
});
});
export const triggers = [
{
hasTokenExposed: true,
token: '0000',
description: 'My trigger',
owner: {
name: 'My User',
username: 'user1',
path: '/user1',
},
lastUsed: null,
canAccessProject: true,
editProjectTriggerPath: '/triggers/1/edit',
projectTriggerPath: '/trigger/1',
},
{
hasTokenExposed: false,
token: '1111',
description: "Anothe user's trigger",
owner: {
name: 'Someone else',
username: 'user2',
path: '/user2',
},
lastUsed: '2020-09-10T08:26:47.410Z',
canAccessProject: false,
editProjectTriggerPath: '/triggers/1/edit',
projectTriggerPath: '/trigger/1',
},
];
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Ci::TriggerEntity do
let(:project) { create(:project) }
let(:trigger) { create(:ci_trigger, project: project, token: '237f3604900a4cd71ed06ef13e57b96d') }
let(:user) { create(:user) }
let(:entity) { described_class.new(trigger, current_user: user, project: project) }
describe '#as_json' do
let(:as_json) { entity.as_json }
let(:project_trigger_path) { "/#{project.full_path}/-/triggers/#{trigger.id}" }
it 'contains required fields' do
expect(as_json).to include(
:description, :owner, :last_used, :token, :has_token_exposed, :can_access_project
)
end
it 'contains user fields' do
expect(as_json[:owner].to_json).to match_schema('entities/user')
end
context 'when current user can manage triggers' do
before do
project.add_maintainer(user)
end
it 'returns short_token as token' do
expect(as_json[:token]).to eq(trigger.short_token)
end
it 'contains project_trigger_path' do
expect(as_json[:project_trigger_path]).to eq(project_trigger_path)
end
it 'does not contain edit_project_trigger_path' do
expect(as_json).not_to include(:edit_project_trigger_path)
end
it 'returns has_token_exposed' do
expect(as_json[:has_token_exposed]).to eq(false)
end
end
context 'when current user is the owner of the trigger' do
before do
project.add_maintainer(user)
trigger.update!(owner: user)
end
it 'returns token as token' do
expect(as_json[:token]).to eq(trigger.token)
end
it 'contains project_trigger_path' do
expect(as_json[:project_trigger_path]).to eq(project_trigger_path)
end
it 'contains edit_project_trigger_path' do
expect(as_json[:edit_project_trigger_path]).to eq("#{project_trigger_path}/edit")
end
it 'returns has_token_exposed' do
expect(as_json[:has_token_exposed]).to eq(true)
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Ci::TriggerSerializer do
describe '#represent' do
let(:represent) { described_class.new.represent(trigger) }
let(:trigger) { build(:ci_trigger) }
it 'matches schema' do
expect(represent.to_json).to match_schema('entities/trigger')
end
end
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