Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
fee19ef336
commit
abd24a801e
30 changed files with 506 additions and 301 deletions
|
@ -1 +1 @@
|
|||
9e5735cc1b202ce5e5657ad83eeeb7b037141e09
|
||||
4e18794f846ad0d27bea3443caa2b51cd9afd722
|
||||
|
|
|
@ -26,26 +26,6 @@ export default class SourceEditor {
|
|||
registerLanguages(...languages);
|
||||
}
|
||||
|
||||
static pushToImportsArray(arr, toImport) {
|
||||
arr.push(import(toImport));
|
||||
}
|
||||
|
||||
static loadExtensions(extensions) {
|
||||
if (!extensions) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
const promises = [];
|
||||
const extensionsArray = typeof extensions === 'string' ? extensions.split(',') : extensions;
|
||||
|
||||
extensionsArray.forEach((ext) => {
|
||||
const prefix = ext.includes('/') ? '' : 'editor/';
|
||||
const trimmedExt = ext.replace(/^\//, '').trim();
|
||||
SourceEditor.pushToImportsArray(promises, `~/${prefix}${trimmedExt}`);
|
||||
});
|
||||
|
||||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
static mixIntoInstance(source, inst) {
|
||||
if (!inst) {
|
||||
return;
|
||||
|
@ -71,23 +51,6 @@ export default class SourceEditor {
|
|||
});
|
||||
}
|
||||
|
||||
static manageDefaultExtensions(instance, el, extensions) {
|
||||
SourceEditor.loadExtensions(extensions, instance)
|
||||
.then((modules) => {
|
||||
if (modules) {
|
||||
modules.forEach((module) => {
|
||||
instance.use(module.default);
|
||||
});
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
el.dispatchEvent(new Event(EDITOR_READY_EVENT));
|
||||
})
|
||||
.catch((e) => {
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
|
||||
static createEditorModel({
|
||||
blobPath,
|
||||
blobContent,
|
||||
|
@ -187,7 +150,6 @@ export default class SourceEditor {
|
|||
blobContent = '',
|
||||
blobOriginalContent = '',
|
||||
blobGlobalId = uuids()[0],
|
||||
extensions = [],
|
||||
isDiff = false,
|
||||
...instanceOptions
|
||||
} = {}) {
|
||||
|
@ -218,9 +180,8 @@ export default class SourceEditor {
|
|||
SourceEditor.instanceDisposeModels(this, instance, model);
|
||||
});
|
||||
|
||||
SourceEditor.manageDefaultExtensions(instance, el, extensions);
|
||||
|
||||
this.instances.push(instance);
|
||||
el.dispatchEvent(new CustomEvent(EDITOR_READY_EVENT, { instance }));
|
||||
return instance;
|
||||
}
|
||||
|
||||
|
@ -234,11 +195,4 @@ export default class SourceEditor {
|
|||
dispose() {
|
||||
this.instances.forEach((instance) => instance.dispose());
|
||||
}
|
||||
|
||||
use(exts) {
|
||||
this.instances.forEach((inst) => {
|
||||
inst.use(exts);
|
||||
});
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -73,9 +73,7 @@ export default class EditorInstance {
|
|||
if (methodExtension) {
|
||||
const extension = extensionsStore.get(methodExtension);
|
||||
|
||||
return (...args) => {
|
||||
return extension.api[prop].call(seInstance, ...args, receiver);
|
||||
};
|
||||
return (...args) => extension.api[prop].call(seInstance, receiver, ...args);
|
||||
}
|
||||
return Reflect.get(seInstance[prop] ? seInstance : target, prop, receiver);
|
||||
},
|
||||
|
|
|
@ -32,8 +32,8 @@ class ApplicationExperiment < Gitlab::Experiment # rubocop:disable Gitlab/Namesp
|
|||
subject = value[:namespace] || value[:group] || value[:project] || value[:user] || value[:actor]
|
||||
return unless ExperimentSubject.valid_subject?(subject)
|
||||
|
||||
variant = :experimental if @variant_name != :control
|
||||
Experiment.add_subject(name, variant: variant || :control, subject: subject)
|
||||
variant_name = :experimental if variant&.name != 'control'
|
||||
Experiment.add_subject(name, variant: variant_name || :control, subject: subject)
|
||||
end
|
||||
|
||||
def record!
|
||||
|
|
|
@ -16,6 +16,8 @@ module Clusters
|
|||
has_many :project_authorizations, class_name: 'Clusters::Agents::ProjectAuthorization'
|
||||
has_many :authorized_projects, class_name: '::Project', through: :project_authorizations, source: :project
|
||||
|
||||
has_many :activity_events, -> { in_timeline_order }, class_name: 'Clusters::Agents::ActivityEvent', inverse_of: :agent
|
||||
|
||||
scope :ordered_by_name, -> { order(:name) }
|
||||
scope :with_name, -> (name) { where(name: name) }
|
||||
|
||||
|
|
34
app/models/clusters/agents/activity_event.rb
Normal file
34
app/models/clusters/agents/activity_event.rb
Normal file
|
@ -0,0 +1,34 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Clusters
|
||||
module Agents
|
||||
class ActivityEvent < ApplicationRecord
|
||||
include NullifyIfBlank
|
||||
|
||||
self.table_name = 'agent_activity_events'
|
||||
|
||||
belongs_to :agent, class_name: 'Clusters::Agent', optional: false
|
||||
belongs_to :user
|
||||
belongs_to :agent_token, class_name: 'Clusters::AgentToken'
|
||||
|
||||
scope :in_timeline_order, -> { order(recorded_at: :desc, id: :desc) }
|
||||
|
||||
validates :recorded_at, :kind, :level, presence: true
|
||||
|
||||
nullify_if_blank :detail
|
||||
|
||||
enum kind: {
|
||||
token_created: 0
|
||||
}, _prefix: true
|
||||
|
||||
enum level: {
|
||||
debug: 0,
|
||||
info: 1,
|
||||
warn: 2,
|
||||
error: 3,
|
||||
fatal: 4,
|
||||
unknown: 5
|
||||
}, _prefix: true
|
||||
end
|
||||
end
|
||||
end
|
|
@ -11,6 +11,8 @@ module Clusters
|
|||
token = ::Clusters::AgentToken.new(filtered_params.merge(created_by_user: current_user))
|
||||
|
||||
if token.save
|
||||
log_activity_event!(token)
|
||||
|
||||
ServiceResponse.success(payload: { secret: token.token, token: token })
|
||||
else
|
||||
ServiceResponse.error(message: token.errors.full_messages)
|
||||
|
@ -26,6 +28,16 @@ module Clusters
|
|||
def filtered_params
|
||||
params.slice(*ALLOWED_PARAMS)
|
||||
end
|
||||
|
||||
def log_activity_event!(token)
|
||||
token.agent.activity_events.create!(
|
||||
kind: :token_created,
|
||||
level: :info,
|
||||
recorded_at: token.created_at,
|
||||
user: current_user,
|
||||
agent_token: token
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2,8 +2,7 @@
|
|||
- @hide_breadcrumbs = true
|
||||
- @no_container = true
|
||||
- page_title user_display_name(@user)
|
||||
- page_description @user.bio
|
||||
- header_title @user.name, user_path(@user)
|
||||
- page_description @user.bio unless @user.blocked? || !@user.confirmed?
|
||||
- page_itemtype 'http://schema.org/Person'
|
||||
- link_classes = "flex-grow-1 mx-1 "
|
||||
|
||||
|
|
|
@ -4,5 +4,5 @@ introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/61152
|
|||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/330104
|
||||
milestone: '13.12'
|
||||
type: development
|
||||
group: group::pipeline execution
|
||||
group: group::fulfillment
|
||||
default_enabled: false
|
||||
|
|
|
@ -4,5 +4,5 @@ introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/61152
|
|||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/330105
|
||||
milestone: '13.12'
|
||||
type: development
|
||||
group: group::pipeline execution
|
||||
group: group::fulfillment
|
||||
default_enabled: false
|
||||
|
|
22
db/migrate/20211110014701_create_agent_activity_events.rb
Normal file
22
db/migrate/20211110014701_create_agent_activity_events.rb
Normal file
|
@ -0,0 +1,22 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class CreateAgentActivityEvents < Gitlab::Database::Migration[1.0]
|
||||
def change
|
||||
create_table :agent_activity_events do |t|
|
||||
t.bigint :agent_id, null: false
|
||||
t.bigint :user_id, index: { where: 'user_id IS NOT NULL' }
|
||||
t.bigint :project_id, index: { where: 'project_id IS NOT NULL' }
|
||||
t.bigint :merge_request_id, index: { where: 'merge_request_id IS NOT NULL' }
|
||||
t.bigint :agent_token_id, index: { where: 'agent_token_id IS NOT NULL' }
|
||||
|
||||
t.datetime_with_timezone :recorded_at, null: false
|
||||
t.integer :kind, limit: 2, null: false
|
||||
t.integer :level, limit: 2, null: false
|
||||
|
||||
t.binary :sha
|
||||
t.text :detail, limit: 255
|
||||
|
||||
t.index [:agent_id, :recorded_at, :id]
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,35 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddAgentActivityEventsForeignKeys < Gitlab::Database::Migration[1.0]
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
add_concurrent_foreign_key :agent_activity_events, :cluster_agents, column: :agent_id, on_delete: :cascade
|
||||
add_concurrent_foreign_key :agent_activity_events, :users, column: :user_id, on_delete: :nullify
|
||||
add_concurrent_foreign_key :agent_activity_events, :projects, column: :project_id, on_delete: :nullify
|
||||
add_concurrent_foreign_key :agent_activity_events, :merge_requests, column: :merge_request_id, on_delete: :nullify
|
||||
add_concurrent_foreign_key :agent_activity_events, :cluster_agent_tokens, column: :agent_token_id, on_delete: :nullify
|
||||
end
|
||||
|
||||
def down
|
||||
with_lock_retries do
|
||||
remove_foreign_key_if_exists :agent_activity_events, column: :agent_id
|
||||
end
|
||||
|
||||
with_lock_retries do
|
||||
remove_foreign_key_if_exists :agent_activity_events, column: :user_id
|
||||
end
|
||||
|
||||
with_lock_retries do
|
||||
remove_foreign_key_if_exists :agent_activity_events, column: :project_id
|
||||
end
|
||||
|
||||
with_lock_retries do
|
||||
remove_foreign_key_if_exists :agent_activity_events, column: :merge_request_id
|
||||
end
|
||||
|
||||
with_lock_retries do
|
||||
remove_foreign_key_if_exists :agent_activity_events, column: :agent_token_id
|
||||
end
|
||||
end
|
||||
end
|
1
db/schema_migrations/20211110014701
Normal file
1
db/schema_migrations/20211110014701
Normal file
|
@ -0,0 +1 @@
|
|||
1c5f65a25c9cf81a50bd9ffa2e74e2621cff04e58a2f90b19c66741ebb459d3e
|
1
db/schema_migrations/20211110015252
Normal file
1
db/schema_migrations/20211110015252
Normal file
|
@ -0,0 +1 @@
|
|||
4038c269ce9c47ca9327fb1b81bb588e9065f0821f291d17c7965d7f8fe1f275
|
|
@ -9721,6 +9721,30 @@ CREATE SEQUENCE abuse_reports_id_seq
|
|||
|
||||
ALTER SEQUENCE abuse_reports_id_seq OWNED BY abuse_reports.id;
|
||||
|
||||
CREATE TABLE agent_activity_events (
|
||||
id bigint NOT NULL,
|
||||
agent_id bigint NOT NULL,
|
||||
user_id bigint,
|
||||
project_id bigint,
|
||||
merge_request_id bigint,
|
||||
agent_token_id bigint,
|
||||
recorded_at timestamp with time zone NOT NULL,
|
||||
kind smallint NOT NULL,
|
||||
level smallint NOT NULL,
|
||||
sha bytea,
|
||||
detail text,
|
||||
CONSTRAINT check_068205e735 CHECK ((char_length(detail) <= 255))
|
||||
);
|
||||
|
||||
CREATE SEQUENCE agent_activity_events_id_seq
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1;
|
||||
|
||||
ALTER SEQUENCE agent_activity_events_id_seq OWNED BY agent_activity_events.id;
|
||||
|
||||
CREATE TABLE agent_group_authorizations (
|
||||
id bigint NOT NULL,
|
||||
group_id bigint NOT NULL,
|
||||
|
@ -21064,6 +21088,8 @@ ALTER SEQUENCE zoom_meetings_id_seq OWNED BY zoom_meetings.id;
|
|||
|
||||
ALTER TABLE ONLY abuse_reports ALTER COLUMN id SET DEFAULT nextval('abuse_reports_id_seq'::regclass);
|
||||
|
||||
ALTER TABLE ONLY agent_activity_events ALTER COLUMN id SET DEFAULT nextval('agent_activity_events_id_seq'::regclass);
|
||||
|
||||
ALTER TABLE ONLY agent_group_authorizations ALTER COLUMN id SET DEFAULT nextval('agent_group_authorizations_id_seq'::regclass);
|
||||
|
||||
ALTER TABLE ONLY agent_project_authorizations ALTER COLUMN id SET DEFAULT nextval('agent_project_authorizations_id_seq'::regclass);
|
||||
|
@ -22412,6 +22438,9 @@ ALTER TABLE ONLY gitlab_partitions_static.product_analytics_events_experimental_
|
|||
ALTER TABLE ONLY abuse_reports
|
||||
ADD CONSTRAINT abuse_reports_pkey PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE ONLY agent_activity_events
|
||||
ADD CONSTRAINT agent_activity_events_pkey PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE ONLY agent_group_authorizations
|
||||
ADD CONSTRAINT agent_group_authorizations_pkey PRIMARY KEY (id);
|
||||
|
||||
|
@ -24907,6 +24936,16 @@ CREATE UNIQUE INDEX idx_vulnerability_issue_links_on_vulnerability_id_and_link_t
|
|||
|
||||
CREATE INDEX index_abuse_reports_on_user_id ON abuse_reports USING btree (user_id);
|
||||
|
||||
CREATE INDEX index_agent_activity_events_on_agent_id_and_recorded_at_and_id ON agent_activity_events USING btree (agent_id, recorded_at, id);
|
||||
|
||||
CREATE INDEX index_agent_activity_events_on_agent_token_id ON agent_activity_events USING btree (agent_token_id) WHERE (agent_token_id IS NOT NULL);
|
||||
|
||||
CREATE INDEX index_agent_activity_events_on_merge_request_id ON agent_activity_events USING btree (merge_request_id) WHERE (merge_request_id IS NOT NULL);
|
||||
|
||||
CREATE INDEX index_agent_activity_events_on_project_id ON agent_activity_events USING btree (project_id) WHERE (project_id IS NOT NULL);
|
||||
|
||||
CREATE INDEX index_agent_activity_events_on_user_id ON agent_activity_events USING btree (user_id) WHERE (user_id IS NOT NULL);
|
||||
|
||||
CREATE UNIQUE INDEX index_agent_group_authorizations_on_agent_id_and_group_id ON agent_group_authorizations USING btree (agent_id, group_id);
|
||||
|
||||
CREATE INDEX index_agent_group_authorizations_on_group_id ON agent_group_authorizations USING btree (group_id);
|
||||
|
@ -28803,6 +28842,9 @@ ALTER TABLE ONLY import_failures
|
|||
ALTER TABLE ONLY project_ci_cd_settings
|
||||
ADD CONSTRAINT fk_24c15d2f2e FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY agent_activity_events
|
||||
ADD CONSTRAINT fk_256c631779 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE SET NULL;
|
||||
|
||||
ALTER TABLE ONLY epics
|
||||
ADD CONSTRAINT fk_25b99c1be3 FOREIGN KEY (parent_id) REFERENCES epics(id) ON DELETE CASCADE;
|
||||
|
||||
|
@ -28875,6 +28917,9 @@ ALTER TABLE ONLY bulk_import_exports
|
|||
ALTER TABLE ONLY ci_builds
|
||||
ADD CONSTRAINT fk_3a9eaa254d FOREIGN KEY (stage_id) REFERENCES ci_stages(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY agent_activity_events
|
||||
ADD CONSTRAINT fk_3af186389b FOREIGN KEY (merge_request_id) REFERENCES merge_requests(id) ON DELETE SET NULL;
|
||||
|
||||
ALTER TABLE ONLY issues
|
||||
ADD CONSTRAINT fk_3b8c72ea56 FOREIGN KEY (sprint_id) REFERENCES sprints(id) ON DELETE SET NULL;
|
||||
|
||||
|
@ -29319,6 +29364,12 @@ ALTER TABLE ONLY geo_event_log
|
|||
ALTER TABLE ONLY issues
|
||||
ADD CONSTRAINT fk_c63cbf6c25 FOREIGN KEY (closed_by_id) REFERENCES users(id) ON DELETE SET NULL;
|
||||
|
||||
ALTER TABLE ONLY agent_activity_events
|
||||
ADD CONSTRAINT fk_c815368376 FOREIGN KEY (agent_id) REFERENCES cluster_agents(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY agent_activity_events
|
||||
ADD CONSTRAINT fk_c8b006d40f FOREIGN KEY (agent_token_id) REFERENCES cluster_agent_tokens(id) ON DELETE SET NULL;
|
||||
|
||||
ALTER TABLE ONLY issue_links
|
||||
ADD CONSTRAINT fk_c900194ff2 FOREIGN KEY (source_id) REFERENCES issues(id) ON DELETE CASCADE;
|
||||
|
||||
|
@ -29373,6 +29424,9 @@ ALTER TABLE ONLY geo_event_log
|
|||
ALTER TABLE ONLY lists
|
||||
ADD CONSTRAINT fk_d6cf4279f7 FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY agent_activity_events
|
||||
ADD CONSTRAINT fk_d6f785c9fc FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL;
|
||||
|
||||
ALTER TABLE ONLY metrics_users_starred_dashboards
|
||||
ADD CONSTRAINT fk_d76a2b9a8c FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
|
||||
|
||||
|
|
|
@ -65,7 +65,8 @@ Example response:
|
|||
"web_url": "http://192.168.1.8:3000/root",
|
||||
"expires_at": "2012-10-22T14:13:35Z",
|
||||
"access_level": 30,
|
||||
"group_saml_identity": null
|
||||
"group_saml_identity": null,
|
||||
"membership_state": "active"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
|
@ -81,7 +82,8 @@ Example response:
|
|||
"extern_uid":"ABC-1234567890",
|
||||
"provider": "group_saml",
|
||||
"saml_provider_id": 10
|
||||
}
|
||||
},
|
||||
"membership_state": "active"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
@ -126,7 +128,8 @@ Example response:
|
|||
"web_url": "http://192.168.1.8:3000/root",
|
||||
"expires_at": "2012-10-22T14:13:35Z",
|
||||
"access_level": 30,
|
||||
"group_saml_identity": null
|
||||
"group_saml_identity": null,
|
||||
"membership_state": "active"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
|
@ -142,7 +145,8 @@ Example response:
|
|||
"extern_uid":"ABC-1234567890",
|
||||
"provider": "group_saml",
|
||||
"saml_provider_id": 10
|
||||
}
|
||||
},
|
||||
"membership_state": "active"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
|
@ -153,7 +157,8 @@ Example response:
|
|||
"web_url": "http://192.168.1.8:3000/root",
|
||||
"expires_at": "2012-11-22T14:13:35Z",
|
||||
"access_level": 30,
|
||||
"group_saml_identity": null
|
||||
"group_saml_identity": null,
|
||||
"membership_state": "active"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
@ -191,7 +196,8 @@ Example response:
|
|||
"email": "john@example.com",
|
||||
"created_at": "2012-10-22T14:13:35Z",
|
||||
"expires_at": null,
|
||||
"group_saml_identity": null
|
||||
"group_saml_identity": null,
|
||||
"membership_state": "active"
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -229,7 +235,8 @@ Example response:
|
|||
"access_level": 30,
|
||||
"email": "john@example.com",
|
||||
"expires_at": null,
|
||||
"group_saml_identity": null
|
||||
"group_saml_identity": null,
|
||||
"membership_state": "active"
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
@ -30,9 +30,6 @@ The squashed commit in this example is followed by a merge commit, because the m
|
|||
|
||||
The squashed commit's default commit message is taken from the merge request title.
|
||||
|
||||
NOTE:
|
||||
This only takes effect if there are at least 2 commits. As there is nothing to squash, the commit message does not change if there is only 1 commit.
|
||||
|
||||
It can be customized before merging a merge request.
|
||||
|
||||
![A squash commit message editor](img/squash_mr_message.png)
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
abuse_reports: :gitlab_main
|
||||
agent_activity_events: :gitlab_main
|
||||
agent_group_authorizations: :gitlab_main
|
||||
agent_project_authorizations: :gitlab_main
|
||||
alert_management_alert_assignees: :gitlab_main
|
||||
|
|
|
@ -38540,9 +38540,15 @@ msgstr ""
|
|||
msgid "VulnerabilityManagement|Something went wrong, could not update vulnerability state."
|
||||
msgstr ""
|
||||
|
||||
msgid "VulnerabilityManagement|Summary, detailed description, steps to reproduce, etc."
|
||||
msgstr ""
|
||||
|
||||
msgid "VulnerabilityManagement|Verified as fixed or mitigated"
|
||||
msgstr ""
|
||||
|
||||
msgid "VulnerabilityManagement|Vulnerability name or type. Ex: Cross-site scripting"
|
||||
msgstr ""
|
||||
|
||||
msgid "VulnerabilityManagement|Will not fix or a false-positive"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe ApplicationExperiment, :experiment do
|
||||
subject { described_class.new('namespaced/stub', **context) }
|
||||
subject(:application_experiment) { described_class.new('namespaced/stub', **context) }
|
||||
|
||||
let(:context) { {} }
|
||||
let(:feature_definition) { { name: 'namespaced_stub', type: 'experiment', default_enabled: false } }
|
||||
|
@ -15,7 +15,7 @@ RSpec.describe ApplicationExperiment, :experiment do
|
|||
end
|
||||
|
||||
before do
|
||||
allow(subject).to receive(:enabled?).and_return(true)
|
||||
allow(application_experiment).to receive(:enabled?).and_return(true)
|
||||
end
|
||||
|
||||
it "doesn't raise an exception without a defined control" do
|
||||
|
@ -26,7 +26,7 @@ RSpec.describe ApplicationExperiment, :experiment do
|
|||
|
||||
describe "#enabled?" do
|
||||
before do
|
||||
allow(subject).to receive(:enabled?).and_call_original
|
||||
allow(application_experiment).to receive(:enabled?).and_call_original
|
||||
|
||||
allow(Feature::Definition).to receive(:get).and_return('_instance_')
|
||||
allow(Gitlab).to receive(:dev_env_or_com?).and_return(true)
|
||||
|
@ -34,25 +34,25 @@ RSpec.describe ApplicationExperiment, :experiment do
|
|||
end
|
||||
|
||||
it "is enabled when all criteria are met" do
|
||||
expect(subject).to be_enabled
|
||||
expect(application_experiment).to be_enabled
|
||||
end
|
||||
|
||||
it "isn't enabled if the feature definition doesn't exist" do
|
||||
expect(Feature::Definition).to receive(:get).with('namespaced_stub').and_return(nil)
|
||||
|
||||
expect(subject).not_to be_enabled
|
||||
expect(application_experiment).not_to be_enabled
|
||||
end
|
||||
|
||||
it "isn't enabled if we're not in dev or dotcom environments" do
|
||||
expect(Gitlab).to receive(:dev_env_or_com?).and_return(false)
|
||||
|
||||
expect(subject).not_to be_enabled
|
||||
expect(application_experiment).not_to be_enabled
|
||||
end
|
||||
|
||||
it "isn't enabled if the feature flag state is :off" do
|
||||
expect(Feature).to receive(:get).with('namespaced_stub').and_return(double(state: :off))
|
||||
|
||||
expect(subject).not_to be_enabled
|
||||
expect(application_experiment).not_to be_enabled
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -60,11 +60,11 @@ RSpec.describe ApplicationExperiment, :experiment do
|
|||
let(:should_track) { true }
|
||||
|
||||
before do
|
||||
allow(subject).to receive(:should_track?).and_return(should_track)
|
||||
allow(application_experiment).to receive(:should_track?).and_return(should_track)
|
||||
end
|
||||
|
||||
it "tracks the assignment", :snowplow do
|
||||
subject.publish
|
||||
application_experiment.publish
|
||||
|
||||
expect_snowplow_event(
|
||||
category: 'namespaced/stub',
|
||||
|
@ -74,24 +74,24 @@ RSpec.describe ApplicationExperiment, :experiment do
|
|||
end
|
||||
|
||||
it "publishes to the client" do
|
||||
expect(subject).to receive(:publish_to_client)
|
||||
expect(application_experiment).to receive(:publish_to_client)
|
||||
|
||||
subject.publish
|
||||
application_experiment.publish
|
||||
end
|
||||
|
||||
it "publishes to the database if we've opted for that" do
|
||||
subject.record!
|
||||
application_experiment.record!
|
||||
|
||||
expect(subject).to receive(:publish_to_database)
|
||||
expect(application_experiment).to receive(:publish_to_database)
|
||||
|
||||
subject.publish
|
||||
application_experiment.publish
|
||||
end
|
||||
|
||||
context 'when we should not track' do
|
||||
let(:should_track) { false }
|
||||
|
||||
it 'does not track an event to Snowplow', :snowplow do
|
||||
subject.publish
|
||||
application_experiment.publish
|
||||
|
||||
expect_no_snowplow_event
|
||||
end
|
||||
|
@ -102,13 +102,13 @@ RSpec.describe ApplicationExperiment, :experiment do
|
|||
signature = { key: '86208ac54ca798e11f127e8b23ec396a', variant: 'control' }
|
||||
expect(Gon).to receive(:push).with({ experiment: { 'namespaced/stub' => hash_including(signature) } }, true)
|
||||
|
||||
subject.publish_to_client
|
||||
application_experiment.publish_to_client
|
||||
end
|
||||
|
||||
it "handles when Gon raises exceptions (like when it can't be pushed into)" do
|
||||
expect(Gon).to receive(:push).and_raise(NoMethodError)
|
||||
|
||||
expect { subject.publish_to_client }.not_to raise_error
|
||||
expect { application_experiment.publish_to_client }.not_to raise_error
|
||||
end
|
||||
|
||||
context 'when we should not track' do
|
||||
|
@ -117,7 +117,7 @@ RSpec.describe ApplicationExperiment, :experiment do
|
|||
it 'returns early' do
|
||||
expect(Gon).not_to receive(:push)
|
||||
|
||||
subject.publish_to_client
|
||||
application_experiment.publish_to_client
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -125,13 +125,15 @@ RSpec.describe ApplicationExperiment, :experiment do
|
|||
describe '#publish_to_database' do
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
let(:publish_to_database) { application_experiment.publish_to_database }
|
||||
|
||||
shared_examples 'does not record to the database' do
|
||||
it 'does not create an experiment record' do
|
||||
expect { subject.publish_to_database }.not_to change(Experiment, :count)
|
||||
expect { publish_to_database }.not_to change(Experiment, :count)
|
||||
end
|
||||
|
||||
it 'does not create an experiment subject record' do
|
||||
expect { subject.publish_to_database }.not_to change(ExperimentSubject, :count)
|
||||
expect { publish_to_database }.not_to change(ExperimentSubject, :count)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -139,16 +141,16 @@ RSpec.describe ApplicationExperiment, :experiment do
|
|||
let(:context) { { context_key => context_value } }
|
||||
|
||||
where(:context_key, :context_value, :object_type) do
|
||||
:namespace | build(:namespace) | :namespace
|
||||
:group | build(:namespace) | :namespace
|
||||
:project | build(:project) | :project
|
||||
:user | build(:user) | :user
|
||||
:actor | build(:user) | :user
|
||||
:namespace | build(:namespace, id: non_existing_record_id) | :namespace
|
||||
:group | build(:namespace, id: non_existing_record_id) | :namespace
|
||||
:project | build(:project, id: non_existing_record_id) | :project
|
||||
:user | build(:user, id: non_existing_record_id) | :user
|
||||
:actor | build(:user, id: non_existing_record_id) | :user
|
||||
end
|
||||
|
||||
with_them do
|
||||
it 'creates an experiment and experiment subject record' do
|
||||
expect { subject.publish_to_database }.to change(Experiment, :count).by(1)
|
||||
expect { publish_to_database }.to change(Experiment, :count).by(1)
|
||||
|
||||
expect(Experiment.last.name).to eq('namespaced/stub')
|
||||
expect(ExperimentSubject.last.send(object_type)).to eq(context[context_key])
|
||||
|
@ -156,6 +158,16 @@ RSpec.describe ApplicationExperiment, :experiment do
|
|||
end
|
||||
end
|
||||
|
||||
context "when experiment hasn't ran" do
|
||||
let(:context) { { user: create(:user) } }
|
||||
|
||||
it 'sets a variant on the experiment subject' do
|
||||
publish_to_database
|
||||
|
||||
expect(ExperimentSubject.last.variant).to eq('control')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there is not a usable subject' do
|
||||
let(:context) { { context_key => context_value } }
|
||||
|
||||
|
@ -183,15 +195,15 @@ RSpec.describe ApplicationExperiment, :experiment do
|
|||
end
|
||||
|
||||
it "doesn't track if we shouldn't track" do
|
||||
allow(subject).to receive(:should_track?).and_return(false)
|
||||
allow(application_experiment).to receive(:should_track?).and_return(false)
|
||||
|
||||
subject.track(:action)
|
||||
application_experiment.track(:action)
|
||||
|
||||
expect_no_snowplow_event
|
||||
end
|
||||
|
||||
it "tracks the event with the expected arguments and merged contexts" do
|
||||
subject.track(:action, property: '_property_', context: [fake_context])
|
||||
application_experiment.track(:action, property: '_property_', context: [fake_context])
|
||||
|
||||
expect_snowplow_event(
|
||||
category: 'namespaced/stub',
|
||||
|
@ -233,7 +245,7 @@ RSpec.describe ApplicationExperiment, :experiment do
|
|||
|
||||
describe "#key_for" do
|
||||
it "generates MD5 hashes" do
|
||||
expect(subject.key_for(foo: :bar)).to eq('6f9ac12afdb9b58c2f19a136d09f9153')
|
||||
expect(application_experiment.key_for(foo: :bar)).to eq('6f9ac12afdb9b58c2f19a136d09f9153')
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -256,26 +268,26 @@ RSpec.describe ApplicationExperiment, :experiment do
|
|||
with_them do
|
||||
it "returns the url or nil if invalid" do
|
||||
allow(Gitlab).to receive(:dev_env_or_com?).and_return(true)
|
||||
expect(subject.process_redirect_url(url)).to eq(processed_url)
|
||||
expect(application_experiment.process_redirect_url(url)).to eq(processed_url)
|
||||
end
|
||||
|
||||
it "considers all urls invalid when not on dev or com" do
|
||||
allow(Gitlab).to receive(:dev_env_or_com?).and_return(false)
|
||||
expect(subject.process_redirect_url(url)).to be_nil
|
||||
expect(application_experiment.process_redirect_url(url)).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
it "generates the correct urls based on where the engine was mounted" do
|
||||
url = Rails.application.routes.url_helpers.experiment_redirect_url(subject, url: 'https://docs.gitlab.com')
|
||||
expect(url).to include("/-/experiment/namespaced%2Fstub:#{subject.context.key}?https://docs.gitlab.com")
|
||||
url = Rails.application.routes.url_helpers.experiment_redirect_url(application_experiment, url: 'https://docs.gitlab.com')
|
||||
expect(url).to include("/-/experiment/namespaced%2Fstub:#{application_experiment.context.key}?https://docs.gitlab.com")
|
||||
end
|
||||
end
|
||||
|
||||
context "when resolving variants" do
|
||||
it "uses the default value as specified in the yaml" do
|
||||
expect(Feature).to receive(:enabled?).with('namespaced_stub', subject, type: :experiment, default_enabled: :yaml)
|
||||
expect(Feature).to receive(:enabled?).with('namespaced_stub', application_experiment, type: :experiment, default_enabled: :yaml)
|
||||
|
||||
expect(subject.variant.name).to eq('control')
|
||||
expect(application_experiment.variant.name).to eq('control')
|
||||
end
|
||||
|
||||
context "when rolled out to 100%" do
|
||||
|
@ -284,10 +296,10 @@ RSpec.describe ApplicationExperiment, :experiment do
|
|||
end
|
||||
|
||||
it "returns the first variant name" do
|
||||
subject.try(:variant1) {}
|
||||
subject.try(:variant2) {}
|
||||
application_experiment.try(:variant1) {}
|
||||
application_experiment.try(:variant2) {}
|
||||
|
||||
expect(subject.variant.name).to eq('variant1')
|
||||
expect(application_experiment.variant.name).to eq('variant1')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -298,18 +310,18 @@ RSpec.describe ApplicationExperiment, :experiment do
|
|||
before do
|
||||
allow(Gitlab::Experiment::Configuration).to receive(:cache).and_call_original
|
||||
|
||||
cache.clear(key: subject.name)
|
||||
cache.clear(key: application_experiment.name)
|
||||
|
||||
subject.use { } # setup the control
|
||||
subject.try { } # setup the candidate
|
||||
application_experiment.use { } # setup the control
|
||||
application_experiment.try { } # setup the candidate
|
||||
end
|
||||
|
||||
it "caches the variant determined by the variant resolver" do
|
||||
expect(subject.variant.name).to eq('candidate') # we should be in the experiment
|
||||
expect(application_experiment.variant.name).to eq('candidate') # we should be in the experiment
|
||||
|
||||
subject.run
|
||||
application_experiment.run
|
||||
|
||||
expect(subject.cache.read).to eq('candidate')
|
||||
expect(application_experiment.cache.read).to eq('candidate')
|
||||
end
|
||||
|
||||
it "doesn't cache a variant if we don't explicitly provide one" do
|
||||
|
@ -320,11 +332,11 @@ RSpec.describe ApplicationExperiment, :experiment do
|
|||
# the control.
|
||||
stub_feature_flags(namespaced_stub: false) # simulate being not rolled out
|
||||
|
||||
expect(subject.variant.name).to eq('control') # if we ask, it should be control
|
||||
expect(application_experiment.variant.name).to eq('control') # if we ask, it should be control
|
||||
|
||||
subject.run
|
||||
application_experiment.run
|
||||
|
||||
expect(subject.cache.read).to be_nil
|
||||
expect(application_experiment.cache.read).to be_nil
|
||||
end
|
||||
|
||||
it "caches a control variant if we assign it specifically" do
|
||||
|
@ -332,27 +344,27 @@ RSpec.describe ApplicationExperiment, :experiment do
|
|||
# that this context will always get the control variant unless we delete
|
||||
# the field from the cache (or clear the entire experiment cache) -- or
|
||||
# write code that would specify a different variant.
|
||||
subject.run(:control)
|
||||
application_experiment.run(:control)
|
||||
|
||||
expect(subject.cache.read).to eq('control')
|
||||
expect(application_experiment.cache.read).to eq('control')
|
||||
end
|
||||
|
||||
context "arbitrary attributes" do
|
||||
before do
|
||||
subject.cache.store.clear(key: subject.name + '_attrs')
|
||||
application_experiment.cache.store.clear(key: application_experiment.name + '_attrs')
|
||||
end
|
||||
|
||||
it "sets and gets attributes about an experiment" do
|
||||
subject.cache.attr_set(:foo, :bar)
|
||||
application_experiment.cache.attr_set(:foo, :bar)
|
||||
|
||||
expect(subject.cache.attr_get(:foo)).to eq('bar')
|
||||
expect(application_experiment.cache.attr_get(:foo)).to eq('bar')
|
||||
end
|
||||
|
||||
it "increments a value for an experiment" do
|
||||
expect(subject.cache.attr_get(:foo)).to be_nil
|
||||
expect(application_experiment.cache.attr_get(:foo)).to be_nil
|
||||
|
||||
expect(subject.cache.attr_inc(:foo)).to eq(1)
|
||||
expect(subject.cache.attr_inc(:foo)).to eq(2)
|
||||
expect(application_experiment.cache.attr_inc(:foo)).to eq(1)
|
||||
expect(application_experiment.cache.attr_inc(:foo)).to eq(2)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
13
spec/factories/clusters/agents/activity_events.rb
Normal file
13
spec/factories/clusters/agents/activity_events.rb
Normal file
|
@ -0,0 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
FactoryBot.define do
|
||||
factory :agent_activity_event, class: 'Clusters::Agents::ActivityEvent' do
|
||||
association :agent, factory: :cluster_agent
|
||||
association :agent_token, factory: :cluster_agent_token
|
||||
user
|
||||
|
||||
kind { :token_created }
|
||||
level { :info }
|
||||
recorded_at { Time.current }
|
||||
end
|
||||
end
|
|
@ -207,34 +207,31 @@ RSpec.describe 'User page' do
|
|||
state: :blocked,
|
||||
organization: 'GitLab - work info test',
|
||||
job_title: 'Frontend Engineer',
|
||||
pronunciation: 'pruh-nuhn-see-ay-shn'
|
||||
pronunciation: 'pruh-nuhn-see-ay-shn',
|
||||
bio: 'My personal bio'
|
||||
)
|
||||
end
|
||||
|
||||
let_it_be(:status) { create(:user_status, user: user, message: "Working hard!") }
|
||||
|
||||
it 'shows no tab' do
|
||||
subject
|
||||
before do
|
||||
visit_profile
|
||||
end
|
||||
|
||||
it 'shows no tab' do
|
||||
expect(page).to have_css("div.profile-header")
|
||||
expect(page).not_to have_css("ul.nav-links")
|
||||
end
|
||||
|
||||
it 'shows blocked message' do
|
||||
subject
|
||||
|
||||
expect(page).to have_content("This user is blocked")
|
||||
end
|
||||
|
||||
it 'shows user name as blocked' do
|
||||
subject
|
||||
|
||||
expect(page).to have_css(".cover-title", text: 'Blocked user')
|
||||
end
|
||||
|
||||
it 'shows no additional fields' do
|
||||
subject
|
||||
|
||||
expect(page).not_to have_css(".profile-user-bio")
|
||||
expect(page).not_to have_content('GitLab - work info test')
|
||||
expect(page).not_to have_content('Frontend Engineer')
|
||||
|
@ -243,10 +240,10 @@ RSpec.describe 'User page' do
|
|||
end
|
||||
|
||||
it 'shows username' do
|
||||
subject
|
||||
|
||||
expect(page).to have_content("@#{user.username}")
|
||||
end
|
||||
|
||||
it_behaves_like 'default brand title page meta description'
|
||||
end
|
||||
|
||||
context 'with unconfirmed user' do
|
||||
|
@ -256,7 +253,8 @@ RSpec.describe 'User page' do
|
|||
:unconfirmed,
|
||||
organization: 'GitLab - work info test',
|
||||
job_title: 'Frontend Engineer',
|
||||
pronunciation: 'pruh-nuhn-see-ay-shn'
|
||||
pronunciation: 'pruh-nuhn-see-ay-shn',
|
||||
bio: 'My personal bio'
|
||||
)
|
||||
end
|
||||
|
||||
|
@ -287,6 +285,8 @@ RSpec.describe 'User page' do
|
|||
it 'shows private profile message' do
|
||||
expect(page).to have_content("This user has a private profile")
|
||||
end
|
||||
|
||||
it_behaves_like 'default brand title page meta description'
|
||||
end
|
||||
|
||||
context 'when visited by an authenticated user' do
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
export class MyClassExtension {
|
||||
export class SEClassExtension {
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
provides() {
|
||||
return {
|
||||
|
@ -8,7 +8,7 @@ export class MyClassExtension {
|
|||
}
|
||||
}
|
||||
|
||||
export function MyFnExtension() {
|
||||
export function SEFnExtension() {
|
||||
return {
|
||||
fnExtMethod: () => 'fn own method',
|
||||
provides: () => {
|
||||
|
@ -19,7 +19,7 @@ export function MyFnExtension() {
|
|||
};
|
||||
}
|
||||
|
||||
export const MyConstExt = () => {
|
||||
export const SEConstExt = () => {
|
||||
return {
|
||||
provides: () => {
|
||||
return {
|
||||
|
@ -29,6 +29,33 @@ export const MyConstExt = () => {
|
|||
};
|
||||
};
|
||||
|
||||
export function SEWithSetupExt() {
|
||||
return {
|
||||
onSetup: (setupOptions = {}, instance) => {
|
||||
if (setupOptions && !Array.isArray(setupOptions)) {
|
||||
Object.entries(setupOptions).forEach(([key, value]) => {
|
||||
Object.assign(instance, {
|
||||
[key]: value,
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
provides: () => {
|
||||
return {
|
||||
returnInstanceAndProps: (instance, stringProp, objProp = {}) => {
|
||||
return [stringProp, objProp, instance];
|
||||
},
|
||||
returnInstance: (instance) => {
|
||||
return instance;
|
||||
},
|
||||
giveMeContext: () => {
|
||||
return this;
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const conflictingExtensions = {
|
||||
WithInstanceExt: () => {
|
||||
return {
|
||||
|
|
|
@ -22,15 +22,15 @@ describe('Editor Extension', () => {
|
|||
|
||||
it.each`
|
||||
definition | setupOptions | expectedName
|
||||
${helpers.MyClassExtension} | ${undefined} | ${'MyClassExtension'}
|
||||
${helpers.MyClassExtension} | ${{}} | ${'MyClassExtension'}
|
||||
${helpers.MyClassExtension} | ${dummyObj} | ${'MyClassExtension'}
|
||||
${helpers.MyFnExtension} | ${undefined} | ${'MyFnExtension'}
|
||||
${helpers.MyFnExtension} | ${{}} | ${'MyFnExtension'}
|
||||
${helpers.MyFnExtension} | ${dummyObj} | ${'MyFnExtension'}
|
||||
${helpers.MyConstExt} | ${undefined} | ${'MyConstExt'}
|
||||
${helpers.MyConstExt} | ${{}} | ${'MyConstExt'}
|
||||
${helpers.MyConstExt} | ${dummyObj} | ${'MyConstExt'}
|
||||
${helpers.SEClassExtension} | ${undefined} | ${'SEClassExtension'}
|
||||
${helpers.SEClassExtension} | ${{}} | ${'SEClassExtension'}
|
||||
${helpers.SEClassExtension} | ${dummyObj} | ${'SEClassExtension'}
|
||||
${helpers.SEFnExtension} | ${undefined} | ${'SEFnExtension'}
|
||||
${helpers.SEFnExtension} | ${{}} | ${'SEFnExtension'}
|
||||
${helpers.SEFnExtension} | ${dummyObj} | ${'SEFnExtension'}
|
||||
${helpers.SEConstExt} | ${undefined} | ${'SEConstExt'}
|
||||
${helpers.SEConstExt} | ${{}} | ${'SEConstExt'}
|
||||
${helpers.SEConstExt} | ${dummyObj} | ${'SEConstExt'}
|
||||
`(
|
||||
'correctly creates extension for definition = $definition and setupOptions = $setupOptions',
|
||||
({ definition, setupOptions, expectedName }) => {
|
||||
|
@ -51,9 +51,9 @@ describe('Editor Extension', () => {
|
|||
describe('api', () => {
|
||||
it.each`
|
||||
definition | expectedKeys
|
||||
${helpers.MyClassExtension} | ${['shared', 'classExtMethod']}
|
||||
${helpers.MyFnExtension} | ${['fnExtMethod']}
|
||||
${helpers.MyConstExt} | ${['constExtMethod']}
|
||||
${helpers.SEClassExtension} | ${['shared', 'classExtMethod']}
|
||||
${helpers.SEFnExtension} | ${['fnExtMethod']}
|
||||
${helpers.SEConstExt} | ${['constExtMethod']}
|
||||
`('correctly returns API for $definition', ({ definition, expectedKeys }) => {
|
||||
const extension = new EditorExtension({ definition });
|
||||
const expectedApi = Object.fromEntries(
|
||||
|
|
|
@ -6,23 +6,29 @@ import {
|
|||
EDITOR_EXTENSION_NOT_REGISTERED_ERROR,
|
||||
EDITOR_EXTENSION_NOT_SPECIFIED_FOR_UNUSE_ERROR,
|
||||
} from '~/editor/constants';
|
||||
import Instance from '~/editor/source_editor_instance';
|
||||
import SourceEditorInstance from '~/editor/source_editor_instance';
|
||||
import { sprintf } from '~/locale';
|
||||
import { MyClassExtension, conflictingExtensions, MyFnExtension, MyConstExt } from './helpers';
|
||||
import {
|
||||
SEClassExtension,
|
||||
conflictingExtensions,
|
||||
SEFnExtension,
|
||||
SEConstExt,
|
||||
SEWithSetupExt,
|
||||
} from './helpers';
|
||||
|
||||
describe('Source Editor Instance', () => {
|
||||
let seInstance;
|
||||
|
||||
const defSetupOptions = { foo: 'bar' };
|
||||
const fullExtensionsArray = [
|
||||
{ definition: MyClassExtension },
|
||||
{ definition: MyFnExtension },
|
||||
{ definition: MyConstExt },
|
||||
{ definition: SEClassExtension },
|
||||
{ definition: SEFnExtension },
|
||||
{ definition: SEConstExt },
|
||||
];
|
||||
const fullExtensionsArrayWithOptions = [
|
||||
{ definition: MyClassExtension, setupOptions: defSetupOptions },
|
||||
{ definition: MyFnExtension, setupOptions: defSetupOptions },
|
||||
{ definition: MyConstExt, setupOptions: defSetupOptions },
|
||||
{ definition: SEClassExtension, setupOptions: defSetupOptions },
|
||||
{ definition: SEFnExtension, setupOptions: defSetupOptions },
|
||||
{ definition: SEConstExt, setupOptions: defSetupOptions },
|
||||
];
|
||||
|
||||
const fooFn = jest.fn();
|
||||
|
@ -40,26 +46,26 @@ describe('Source Editor Instance', () => {
|
|||
});
|
||||
|
||||
it('sets up the registry for the methods coming from extensions', () => {
|
||||
seInstance = new Instance();
|
||||
seInstance = new SourceEditorInstance();
|
||||
expect(seInstance.methods).toBeDefined();
|
||||
|
||||
seInstance.use({ definition: MyClassExtension });
|
||||
seInstance.use({ definition: SEClassExtension });
|
||||
expect(seInstance.methods).toEqual({
|
||||
shared: 'MyClassExtension',
|
||||
classExtMethod: 'MyClassExtension',
|
||||
shared: 'SEClassExtension',
|
||||
classExtMethod: 'SEClassExtension',
|
||||
});
|
||||
|
||||
seInstance.use({ definition: MyFnExtension });
|
||||
seInstance.use({ definition: SEFnExtension });
|
||||
expect(seInstance.methods).toEqual({
|
||||
shared: 'MyClassExtension',
|
||||
classExtMethod: 'MyClassExtension',
|
||||
fnExtMethod: 'MyFnExtension',
|
||||
shared: 'SEClassExtension',
|
||||
classExtMethod: 'SEClassExtension',
|
||||
fnExtMethod: 'SEFnExtension',
|
||||
});
|
||||
});
|
||||
|
||||
describe('proxy', () => {
|
||||
it('returns prop from an extension if extension provides it', () => {
|
||||
seInstance = new Instance();
|
||||
seInstance = new SourceEditorInstance();
|
||||
seInstance.use({ definition: DummyExt });
|
||||
|
||||
expect(fooFn).not.toHaveBeenCalled();
|
||||
|
@ -67,8 +73,58 @@ describe('Source Editor Instance', () => {
|
|||
expect(fooFn).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it.each`
|
||||
stringPropToPass | objPropToPass | setupOptions
|
||||
${undefined} | ${undefined} | ${undefined}
|
||||
${'prop'} | ${undefined} | ${undefined}
|
||||
${'prop'} | ${[]} | ${undefined}
|
||||
${'prop'} | ${{}} | ${undefined}
|
||||
${'prop'} | ${{ alpha: 'beta' }} | ${undefined}
|
||||
${'prop'} | ${{ alpha: 'beta' }} | ${defSetupOptions}
|
||||
${'prop'} | ${undefined} | ${defSetupOptions}
|
||||
${undefined} | ${undefined} | ${defSetupOptions}
|
||||
${''} | ${{}} | ${defSetupOptions}
|
||||
`(
|
||||
'correctly passes arguments ("$stringPropToPass", "$objPropToPass") and instance (with "$setupOptions" setupOptions) to extension methods',
|
||||
({ stringPropToPass, objPropToPass, setupOptions }) => {
|
||||
seInstance = new SourceEditorInstance();
|
||||
seInstance.use({ definition: SEWithSetupExt, setupOptions });
|
||||
|
||||
const [stringProp, objProp, instance] = seInstance.returnInstanceAndProps(
|
||||
stringPropToPass,
|
||||
objPropToPass,
|
||||
);
|
||||
const expectedObjProps = objPropToPass || {};
|
||||
|
||||
expect(instance).toBe(seInstance);
|
||||
expect(stringProp).toBe(stringPropToPass);
|
||||
expect(objProp).toEqual(expectedObjProps);
|
||||
if (setupOptions) {
|
||||
Object.keys(setupOptions).forEach((key) => {
|
||||
expect(instance[key]).toBe(setupOptions[key]);
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it('correctly passes instance to the methods even if no additional props have been passed', () => {
|
||||
seInstance = new SourceEditorInstance();
|
||||
seInstance.use({ definition: SEWithSetupExt });
|
||||
|
||||
const instance = seInstance.returnInstance();
|
||||
|
||||
expect(instance).toBe(seInstance);
|
||||
});
|
||||
|
||||
it("correctly sets the context of the 'this' keyword for the extension's methods", () => {
|
||||
seInstance = new SourceEditorInstance();
|
||||
seInstance.use({ definition: SEWithSetupExt });
|
||||
|
||||
expect(seInstance.giveMeContext().constructor).toEqual(SEWithSetupExt);
|
||||
});
|
||||
|
||||
it('returns props from SE instance itself if no extension provides the prop', () => {
|
||||
seInstance = new Instance({
|
||||
seInstance = new SourceEditorInstance({
|
||||
use: fooFn,
|
||||
});
|
||||
jest.spyOn(seInstance, 'use').mockImplementation(() => {});
|
||||
|
@ -80,7 +136,7 @@ describe('Source Editor Instance', () => {
|
|||
});
|
||||
|
||||
it('returns props from Monaco instance when the prop does not exist on the SE instance', () => {
|
||||
seInstance = new Instance({
|
||||
seInstance = new SourceEditorInstance({
|
||||
fooFn,
|
||||
});
|
||||
|
||||
|
@ -92,13 +148,13 @@ describe('Source Editor Instance', () => {
|
|||
|
||||
describe('public API', () => {
|
||||
it.each(['use', 'unuse'], 'provides "%s" as public method by default', (method) => {
|
||||
seInstance = new Instance();
|
||||
seInstance = new SourceEditorInstance();
|
||||
expect(seInstance[method]).toBeDefined();
|
||||
});
|
||||
|
||||
describe('use', () => {
|
||||
it('extends the SE instance with methods provided by an extension', () => {
|
||||
seInstance = new Instance();
|
||||
seInstance = new SourceEditorInstance();
|
||||
seInstance.use({ definition: DummyExt });
|
||||
|
||||
expect(fooFn).not.toHaveBeenCalled();
|
||||
|
@ -108,15 +164,15 @@ describe('Source Editor Instance', () => {
|
|||
|
||||
it.each`
|
||||
extensions | expectedProps
|
||||
${{ definition: MyClassExtension }} | ${['shared', 'classExtMethod']}
|
||||
${{ definition: MyFnExtension }} | ${['fnExtMethod']}
|
||||
${{ definition: MyConstExt }} | ${['constExtMethod']}
|
||||
${{ definition: SEClassExtension }} | ${['shared', 'classExtMethod']}
|
||||
${{ definition: SEFnExtension }} | ${['fnExtMethod']}
|
||||
${{ definition: SEConstExt }} | ${['constExtMethod']}
|
||||
${fullExtensionsArray} | ${['shared', 'classExtMethod', 'fnExtMethod', 'constExtMethod']}
|
||||
${fullExtensionsArrayWithOptions} | ${['shared', 'classExtMethod', 'fnExtMethod', 'constExtMethod']}
|
||||
`(
|
||||
'Should register $expectedProps when extension is "$extensions"',
|
||||
({ extensions, expectedProps }) => {
|
||||
seInstance = new Instance();
|
||||
seInstance = new SourceEditorInstance();
|
||||
expect(seInstance.extensionsAPI).toHaveLength(0);
|
||||
|
||||
seInstance.use(extensions);
|
||||
|
@ -127,15 +183,15 @@ describe('Source Editor Instance', () => {
|
|||
|
||||
it.each`
|
||||
definition | preInstalledExtDefinition | expectedErrorProp
|
||||
${conflictingExtensions.WithInstanceExt} | ${MyClassExtension} | ${'use'}
|
||||
${conflictingExtensions.WithInstanceExt} | ${SEClassExtension} | ${'use'}
|
||||
${conflictingExtensions.WithInstanceExt} | ${null} | ${'use'}
|
||||
${conflictingExtensions.WithAnotherExt} | ${null} | ${undefined}
|
||||
${conflictingExtensions.WithAnotherExt} | ${MyClassExtension} | ${'shared'}
|
||||
${MyClassExtension} | ${conflictingExtensions.WithAnotherExt} | ${'shared'}
|
||||
${conflictingExtensions.WithAnotherExt} | ${SEClassExtension} | ${'shared'}
|
||||
${SEClassExtension} | ${conflictingExtensions.WithAnotherExt} | ${'shared'}
|
||||
`(
|
||||
'logs the naming conflict error when registering $definition',
|
||||
({ definition, preInstalledExtDefinition, expectedErrorProp }) => {
|
||||
seInstance = new Instance();
|
||||
seInstance = new SourceEditorInstance();
|
||||
jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
|
||||
if (preInstalledExtDefinition) {
|
||||
|
@ -175,7 +231,7 @@ describe('Source Editor Instance', () => {
|
|||
`(
|
||||
'Should throw $thrownError when extension is "$extensions"',
|
||||
({ extensions, thrownError }) => {
|
||||
seInstance = new Instance();
|
||||
seInstance = new SourceEditorInstance();
|
||||
const useExtension = () => {
|
||||
seInstance.use(extensions);
|
||||
};
|
||||
|
@ -188,24 +244,24 @@ describe('Source Editor Instance', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
extensionStore = new Map();
|
||||
seInstance = new Instance({}, extensionStore);
|
||||
seInstance = new SourceEditorInstance({}, extensionStore);
|
||||
});
|
||||
|
||||
it('stores _instances_ of the used extensions in a global registry', () => {
|
||||
const extension = seInstance.use({ definition: MyClassExtension });
|
||||
const extension = seInstance.use({ definition: SEClassExtension });
|
||||
|
||||
expect(extensionStore.size).toBe(1);
|
||||
expect(extensionStore.entries().next().value).toEqual(['MyClassExtension', extension]);
|
||||
expect(extensionStore.entries().next().value).toEqual(['SEClassExtension', extension]);
|
||||
});
|
||||
|
||||
it('does not duplicate entries in the registry', () => {
|
||||
jest.spyOn(extensionStore, 'set');
|
||||
|
||||
const extension1 = seInstance.use({ definition: MyClassExtension });
|
||||
seInstance.use({ definition: MyClassExtension });
|
||||
const extension1 = seInstance.use({ definition: SEClassExtension });
|
||||
seInstance.use({ definition: SEClassExtension });
|
||||
|
||||
expect(extensionStore.set).toHaveBeenCalledTimes(1);
|
||||
expect(extensionStore.set).toHaveBeenCalledWith('MyClassExtension', extension1);
|
||||
expect(extensionStore.set).toHaveBeenCalledWith('SEClassExtension', extension1);
|
||||
});
|
||||
|
||||
it.each`
|
||||
|
@ -222,20 +278,20 @@ describe('Source Editor Instance', () => {
|
|||
jest.spyOn(extensionStore, 'set');
|
||||
|
||||
const extension1 = seInstance.use({
|
||||
definition: MyClassExtension,
|
||||
definition: SEClassExtension,
|
||||
setupOptions: currentSetupOptions,
|
||||
});
|
||||
const extension2 = seInstance.use({
|
||||
definition: MyClassExtension,
|
||||
definition: SEClassExtension,
|
||||
setupOptions: newSetupOptions,
|
||||
});
|
||||
|
||||
expect(extensionStore.size).toBe(1);
|
||||
expect(extensionStore.set).toHaveBeenCalledTimes(expectedCallTimes);
|
||||
if (expectedCallTimes > 1) {
|
||||
expect(extensionStore.set).toHaveBeenCalledWith('MyClassExtension', extension2);
|
||||
expect(extensionStore.set).toHaveBeenCalledWith('SEClassExtension', extension2);
|
||||
} else {
|
||||
expect(extensionStore.set).toHaveBeenCalledWith('MyClassExtension', extension1);
|
||||
expect(extensionStore.set).toHaveBeenCalledWith('SEClassExtension', extension1);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
@ -252,7 +308,7 @@ describe('Source Editor Instance', () => {
|
|||
`(
|
||||
`Should throw "${EDITOR_EXTENSION_NOT_SPECIFIED_FOR_UNUSE_ERROR}" when extension is "$unuseExtension"`,
|
||||
({ unuseExtension, thrownError }) => {
|
||||
seInstance = new Instance();
|
||||
seInstance = new SourceEditorInstance();
|
||||
const unuse = () => {
|
||||
seInstance.unuse(unuseExtension);
|
||||
};
|
||||
|
@ -262,16 +318,16 @@ describe('Source Editor Instance', () => {
|
|||
|
||||
it.each`
|
||||
initExtensions | unuseExtensionIndex | remainingAPI
|
||||
${{ definition: MyClassExtension }} | ${0} | ${[]}
|
||||
${{ definition: MyFnExtension }} | ${0} | ${[]}
|
||||
${{ definition: MyConstExt }} | ${0} | ${[]}
|
||||
${{ definition: SEClassExtension }} | ${0} | ${[]}
|
||||
${{ definition: SEFnExtension }} | ${0} | ${[]}
|
||||
${{ definition: SEConstExt }} | ${0} | ${[]}
|
||||
${fullExtensionsArray} | ${0} | ${['fnExtMethod', 'constExtMethod']}
|
||||
${fullExtensionsArray} | ${1} | ${['shared', 'classExtMethod', 'constExtMethod']}
|
||||
${fullExtensionsArray} | ${2} | ${['shared', 'classExtMethod', 'fnExtMethod']}
|
||||
`(
|
||||
'un-registers properties introduced by single extension $unuseExtension',
|
||||
({ initExtensions, unuseExtensionIndex, remainingAPI }) => {
|
||||
seInstance = new Instance();
|
||||
seInstance = new SourceEditorInstance();
|
||||
const extensions = seInstance.use(initExtensions);
|
||||
|
||||
if (Array.isArray(initExtensions)) {
|
||||
|
@ -291,7 +347,7 @@ describe('Source Editor Instance', () => {
|
|||
`(
|
||||
'un-registers properties introduced by multiple extensions $unuseExtension',
|
||||
({ unuseExtensionIndex, remainingAPI }) => {
|
||||
seInstance = new Instance();
|
||||
seInstance = new SourceEditorInstance();
|
||||
const extensions = seInstance.use(fullExtensionsArray);
|
||||
const extensionsToUnuse = extensions.filter((ext, index) =>
|
||||
unuseExtensionIndex.includes(index),
|
||||
|
@ -304,11 +360,11 @@ describe('Source Editor Instance', () => {
|
|||
|
||||
it('it does not remove entry from the global registry to keep for potential future re-use', () => {
|
||||
const extensionStore = new Map();
|
||||
seInstance = new Instance({}, extensionStore);
|
||||
seInstance = new SourceEditorInstance({}, extensionStore);
|
||||
const extensions = seInstance.use(fullExtensionsArray);
|
||||
const verifyExpectations = () => {
|
||||
const entries = extensionStore.entries();
|
||||
const mockExtensions = ['MyClassExtension', 'MyFnExtension', 'MyConstExt'];
|
||||
const mockExtensions = ['SEClassExtension', 'SEFnExtension', 'SEConstExt'];
|
||||
expect(extensionStore.size).toBe(mockExtensions.length);
|
||||
mockExtensions.forEach((ext, index) => {
|
||||
expect(entries.next().value).toEqual([ext, extensions[index]]);
|
||||
|
@ -326,7 +382,7 @@ describe('Source Editor Instance', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
instanceModel = monacoEditor.createModel('');
|
||||
seInstance = new Instance({
|
||||
seInstance = new SourceEditorInstance({
|
||||
getModel: () => instanceModel,
|
||||
});
|
||||
});
|
||||
|
@ -363,7 +419,7 @@ describe('Source Editor Instance', () => {
|
|||
};
|
||||
|
||||
it('passes correct arguments to callback fns when using an extension', () => {
|
||||
seInstance = new Instance();
|
||||
seInstance = new SourceEditorInstance();
|
||||
seInstance.use({
|
||||
definition: MyFullExtWithCallbacks,
|
||||
setupOptions: defSetupOptions,
|
||||
|
@ -373,7 +429,7 @@ describe('Source Editor Instance', () => {
|
|||
});
|
||||
|
||||
it('passes correct arguments to callback fns when un-using an extension', () => {
|
||||
seInstance = new Instance();
|
||||
seInstance = new SourceEditorInstance();
|
||||
const extension = seInstance.use({
|
||||
definition: MyFullExtWithCallbacks,
|
||||
setupOptions: defSetupOptions,
|
||||
|
|
|
@ -57,7 +57,7 @@ describe('Markdown Extension for Source Editor', () => {
|
|||
blobPath: markdownPath,
|
||||
blobContent: text,
|
||||
});
|
||||
editor.use(new EditorMarkdownExtension({ instance, previewMarkdownPath }));
|
||||
instance.use(new EditorMarkdownExtension({ instance, previewMarkdownPath }));
|
||||
panelSpy = jest.spyOn(EditorMarkdownExtension, 'togglePreviewPanel');
|
||||
});
|
||||
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
/* eslint-disable max-classes-per-file */
|
||||
import { editor as monacoEditor, languages as monacoLanguages } from 'monaco-editor';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import {
|
||||
SOURCE_EDITOR_INSTANCE_ERROR_NO_EL,
|
||||
URI_PREFIX,
|
||||
|
@ -531,105 +530,19 @@ describe('Base editor', () => {
|
|||
instance.use(FunctionExt);
|
||||
expect(instance.inst()).toEqual(editor.instances[0]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('extensions as an instance parameter', () => {
|
||||
let editorExtensionSpy;
|
||||
const instanceConstructor = (extensions = []) => {
|
||||
return editor.createInstance({
|
||||
el: editorEl,
|
||||
blobPath,
|
||||
blobContent,
|
||||
extensions,
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
editorExtensionSpy = jest
|
||||
.spyOn(SourceEditor, 'pushToImportsArray')
|
||||
.mockImplementation((arr) => {
|
||||
arr.push(
|
||||
Promise.resolve({
|
||||
default: {},
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it.each([undefined, [], [''], ''])(
|
||||
'does not fail and makes no fetch if extensions is %s',
|
||||
() => {
|
||||
instance = instanceConstructor(null);
|
||||
expect(editorExtensionSpy).not.toHaveBeenCalled();
|
||||
},
|
||||
);
|
||||
|
||||
it.each`
|
||||
type | value | callsCount
|
||||
${'simple string'} | ${'foo'} | ${1}
|
||||
${'combined string'} | ${'foo, bar'} | ${2}
|
||||
${'array of strings'} | ${['foo', 'bar']} | ${2}
|
||||
`('accepts $type as an extension parameter', ({ value, callsCount }) => {
|
||||
instance = instanceConstructor(value);
|
||||
expect(editorExtensionSpy).toHaveBeenCalled();
|
||||
expect(editorExtensionSpy.mock.calls).toHaveLength(callsCount);
|
||||
});
|
||||
|
||||
it.each`
|
||||
desc | path | expectation
|
||||
${'~/editor'} | ${'foo'} | ${'~/editor/foo'}
|
||||
${'~/CUSTOM_PATH with leading slash'} | ${'/my_custom_path/bar'} | ${'~/my_custom_path/bar'}
|
||||
${'~/CUSTOM_PATH without leading slash'} | ${'my_custom_path/delta'} | ${'~/my_custom_path/delta'}
|
||||
`('fetches extensions from $desc path', ({ path, expectation }) => {
|
||||
instance = instanceConstructor(path);
|
||||
expect(editorExtensionSpy).toHaveBeenCalledWith(expect.any(Array), expectation);
|
||||
});
|
||||
|
||||
it('emits EDITOR_READY_EVENT event after all extensions were applied', async () => {
|
||||
const calls = [];
|
||||
const eventSpy = jest.fn().mockImplementation(() => {
|
||||
calls.push('event');
|
||||
});
|
||||
const useSpy = jest.fn().mockImplementation(() => {
|
||||
calls.push('use');
|
||||
});
|
||||
jest.spyOn(SourceEditor, 'convertMonacoToELInstance').mockImplementation((inst) => {
|
||||
const decoratedInstance = inst;
|
||||
decoratedInstance.use = useSpy;
|
||||
return decoratedInstance;
|
||||
it('emits the EDITOR_READY_EVENT event after setting up the instance', () => {
|
||||
jest.spyOn(monacoEditor, 'create').mockImplementation(() => {
|
||||
return {
|
||||
setModel: jest.fn(),
|
||||
onDidDispose: jest.fn(),
|
||||
};
|
||||
});
|
||||
const eventSpy = jest.fn();
|
||||
editorEl.addEventListener(EDITOR_READY_EVENT, eventSpy);
|
||||
instance = instanceConstructor('foo, bar');
|
||||
await waitForPromises();
|
||||
expect(useSpy.mock.calls).toHaveLength(2);
|
||||
expect(calls).toEqual(['use', 'use', 'event']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('multiple instances', () => {
|
||||
let inst1;
|
||||
let inst2;
|
||||
let editorEl1;
|
||||
let editorEl2;
|
||||
|
||||
beforeEach(() => {
|
||||
setFixtures('<div id="editor1"></div><div id="editor2"></div>');
|
||||
editorEl1 = document.getElementById('editor1');
|
||||
editorEl2 = document.getElementById('editor2');
|
||||
inst1 = editor.createInstance({ el: editorEl1, blobPath: `foo-${blobPath}` });
|
||||
inst2 = editor.createInstance({ el: editorEl2, blobPath: `bar-${blobPath}` });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
editor.dispose();
|
||||
editorEl1.remove();
|
||||
editorEl2.remove();
|
||||
});
|
||||
|
||||
it('extends all instances if no specific instance is passed', () => {
|
||||
editor.use(AlphaExt);
|
||||
expect(inst1.alpha()).toEqual(alphaRes);
|
||||
expect(inst2.alpha()).toEqual(alphaRes);
|
||||
expect(eventSpy).not.toHaveBeenCalled();
|
||||
instance = editor.createInstance({ el: editorEl });
|
||||
expect(eventSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
32
spec/models/clusters/agents/activity_event_spec.rb
Normal file
32
spec/models/clusters/agents/activity_event_spec.rb
Normal file
|
@ -0,0 +1,32 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Clusters::Agents::ActivityEvent do
|
||||
it { is_expected.to belong_to(:agent).class_name('Clusters::Agent').required }
|
||||
it { is_expected.to belong_to(:user).optional }
|
||||
it { is_expected.to belong_to(:agent_token).class_name('Clusters::AgentToken').optional }
|
||||
|
||||
it { is_expected.to validate_presence_of(:kind) }
|
||||
it { is_expected.to validate_presence_of(:level) }
|
||||
it { is_expected.to validate_presence_of(:recorded_at) }
|
||||
it { is_expected.to nullify_if_blank(:detail) }
|
||||
|
||||
describe 'scopes' do
|
||||
let_it_be(:agent) { create(:cluster_agent) }
|
||||
|
||||
describe '.in_timeline_order' do
|
||||
let(:recorded_at) { 1.hour.ago }
|
||||
|
||||
let!(:event1) { create(:agent_activity_event, agent: agent, recorded_at: recorded_at) }
|
||||
let!(:event2) { create(:agent_activity_event, agent: agent, recorded_at: Time.current) }
|
||||
let!(:event3) { create(:agent_activity_event, agent: agent, recorded_at: recorded_at) }
|
||||
|
||||
subject { described_class.in_timeline_order }
|
||||
|
||||
it 'sorts by recorded_at: :desc, id: :desc' do
|
||||
is_expected.to eq([event2, event3, event1])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -47,6 +47,21 @@ RSpec.describe Clusters::AgentTokens::CreateService do
|
|||
expect(token.name).to eq(params[:name])
|
||||
end
|
||||
|
||||
it 'creates an activity event' do
|
||||
expect { subject }.to change { ::Clusters::Agents::ActivityEvent.count }.by(1)
|
||||
|
||||
token = subject.payload[:token].reload
|
||||
event = cluster_agent.activity_events.last
|
||||
|
||||
expect(event).to have_attributes(
|
||||
kind: 'token_created',
|
||||
level: 'info',
|
||||
recorded_at: token.created_at,
|
||||
user: token.created_by_user,
|
||||
agent_token: token
|
||||
)
|
||||
end
|
||||
|
||||
context 'when params are invalid' do
|
||||
let(:params) { { agent_id: 'bad_id' } }
|
||||
|
||||
|
@ -54,6 +69,10 @@ RSpec.describe Clusters::AgentTokens::CreateService do
|
|||
expect { subject }.not_to change(::Clusters::AgentToken, :count)
|
||||
end
|
||||
|
||||
it 'does not create an activity event' do
|
||||
expect { subject }.not_to change { ::Clusters::Agents::ActivityEvent.count }
|
||||
end
|
||||
|
||||
it 'returns validation errors', :aggregate_failures do
|
||||
expect(subject.status).to eq(:error)
|
||||
expect(subject.message).to eq(["Agent must exist", "Name can't be blank"])
|
||||
|
|
|
@ -7,3 +7,13 @@ RSpec.shared_examples 'page meta description' do |expected_description|
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.shared_examples 'default brand title page meta description' do
|
||||
include AppearancesHelper
|
||||
|
||||
it 'renders the page with description, og:description, and twitter:description meta tags with the default brand title', :aggregate_failures do
|
||||
%w(name='description' property='og:description' property='twitter:description').each do |selector|
|
||||
expect(page).to have_selector("meta[#{selector}][content='#{default_brand_title}']", visible: false)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in a new issue