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);
|
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) {
|
static mixIntoInstance(source, inst) {
|
||||||
if (!inst) {
|
if (!inst) {
|
||||||
return;
|
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({
|
static createEditorModel({
|
||||||
blobPath,
|
blobPath,
|
||||||
blobContent,
|
blobContent,
|
||||||
|
@ -187,7 +150,6 @@ export default class SourceEditor {
|
||||||
blobContent = '',
|
blobContent = '',
|
||||||
blobOriginalContent = '',
|
blobOriginalContent = '',
|
||||||
blobGlobalId = uuids()[0],
|
blobGlobalId = uuids()[0],
|
||||||
extensions = [],
|
|
||||||
isDiff = false,
|
isDiff = false,
|
||||||
...instanceOptions
|
...instanceOptions
|
||||||
} = {}) {
|
} = {}) {
|
||||||
|
@ -218,9 +180,8 @@ export default class SourceEditor {
|
||||||
SourceEditor.instanceDisposeModels(this, instance, model);
|
SourceEditor.instanceDisposeModels(this, instance, model);
|
||||||
});
|
});
|
||||||
|
|
||||||
SourceEditor.manageDefaultExtensions(instance, el, extensions);
|
|
||||||
|
|
||||||
this.instances.push(instance);
|
this.instances.push(instance);
|
||||||
|
el.dispatchEvent(new CustomEvent(EDITOR_READY_EVENT, { instance }));
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -234,11 +195,4 @@ export default class SourceEditor {
|
||||||
dispose() {
|
dispose() {
|
||||||
this.instances.forEach((instance) => instance.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) {
|
if (methodExtension) {
|
||||||
const extension = extensionsStore.get(methodExtension);
|
const extension = extensionsStore.get(methodExtension);
|
||||||
|
|
||||||
return (...args) => {
|
return (...args) => extension.api[prop].call(seInstance, receiver, ...args);
|
||||||
return extension.api[prop].call(seInstance, ...args, receiver);
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
return Reflect.get(seInstance[prop] ? seInstance : target, prop, receiver);
|
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]
|
subject = value[:namespace] || value[:group] || value[:project] || value[:user] || value[:actor]
|
||||||
return unless ExperimentSubject.valid_subject?(subject)
|
return unless ExperimentSubject.valid_subject?(subject)
|
||||||
|
|
||||||
variant = :experimental if @variant_name != :control
|
variant_name = :experimental if variant&.name != 'control'
|
||||||
Experiment.add_subject(name, variant: variant || :control, subject: subject)
|
Experiment.add_subject(name, variant: variant_name || :control, subject: subject)
|
||||||
end
|
end
|
||||||
|
|
||||||
def record!
|
def record!
|
||||||
|
|
|
@ -16,6 +16,8 @@ module Clusters
|
||||||
has_many :project_authorizations, class_name: 'Clusters::Agents::ProjectAuthorization'
|
has_many :project_authorizations, class_name: 'Clusters::Agents::ProjectAuthorization'
|
||||||
has_many :authorized_projects, class_name: '::Project', through: :project_authorizations, source: :project
|
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 :ordered_by_name, -> { order(:name) }
|
||||||
scope :with_name, -> (name) { where(name: 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))
|
token = ::Clusters::AgentToken.new(filtered_params.merge(created_by_user: current_user))
|
||||||
|
|
||||||
if token.save
|
if token.save
|
||||||
|
log_activity_event!(token)
|
||||||
|
|
||||||
ServiceResponse.success(payload: { secret: token.token, token: token })
|
ServiceResponse.success(payload: { secret: token.token, token: token })
|
||||||
else
|
else
|
||||||
ServiceResponse.error(message: token.errors.full_messages)
|
ServiceResponse.error(message: token.errors.full_messages)
|
||||||
|
@ -26,6 +28,16 @@ module Clusters
|
||||||
def filtered_params
|
def filtered_params
|
||||||
params.slice(*ALLOWED_PARAMS)
|
params.slice(*ALLOWED_PARAMS)
|
||||||
end
|
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
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,8 +2,7 @@
|
||||||
- @hide_breadcrumbs = true
|
- @hide_breadcrumbs = true
|
||||||
- @no_container = true
|
- @no_container = true
|
||||||
- page_title user_display_name(@user)
|
- page_title user_display_name(@user)
|
||||||
- page_description @user.bio
|
- page_description @user.bio unless @user.blocked? || !@user.confirmed?
|
||||||
- header_title @user.name, user_path(@user)
|
|
||||||
- page_itemtype 'http://schema.org/Person'
|
- page_itemtype 'http://schema.org/Person'
|
||||||
- link_classes = "flex-grow-1 mx-1 "
|
- link_classes = "flex-grow-1 mx-1 "
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
---
|
---
|
||||||
name: ci_require_credit_card_on_free_plan
|
name: ci_require_credit_card_on_free_plan
|
||||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/61152
|
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/61152
|
||||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/330104
|
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/330104
|
||||||
milestone: '13.12'
|
milestone: '13.12'
|
||||||
type: development
|
type: development
|
||||||
group: group::pipeline execution
|
group: group::fulfillment
|
||||||
default_enabled: false
|
default_enabled: false
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
---
|
---
|
||||||
name: ci_require_credit_card_on_trial_plan
|
name: ci_require_credit_card_on_trial_plan
|
||||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/61152
|
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/61152
|
||||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/330105
|
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/330105
|
||||||
milestone: '13.12'
|
milestone: '13.12'
|
||||||
type: development
|
type: development
|
||||||
group: group::pipeline execution
|
group: group::fulfillment
|
||||||
default_enabled: false
|
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;
|
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 (
|
CREATE TABLE agent_group_authorizations (
|
||||||
id bigint NOT NULL,
|
id bigint NOT NULL,
|
||||||
group_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 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_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);
|
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
|
ALTER TABLE ONLY abuse_reports
|
||||||
ADD CONSTRAINT abuse_reports_pkey PRIMARY KEY (id);
|
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
|
ALTER TABLE ONLY agent_group_authorizations
|
||||||
ADD CONSTRAINT agent_group_authorizations_pkey PRIMARY KEY (id);
|
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_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 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);
|
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
|
ALTER TABLE ONLY project_ci_cd_settings
|
||||||
ADD CONSTRAINT fk_24c15d2f2e FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
|
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
|
ALTER TABLE ONLY epics
|
||||||
ADD CONSTRAINT fk_25b99c1be3 FOREIGN KEY (parent_id) REFERENCES epics(id) ON DELETE CASCADE;
|
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
|
ALTER TABLE ONLY ci_builds
|
||||||
ADD CONSTRAINT fk_3a9eaa254d FOREIGN KEY (stage_id) REFERENCES ci_stages(id) ON DELETE CASCADE;
|
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
|
ALTER TABLE ONLY issues
|
||||||
ADD CONSTRAINT fk_3b8c72ea56 FOREIGN KEY (sprint_id) REFERENCES sprints(id) ON DELETE SET NULL;
|
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
|
ALTER TABLE ONLY issues
|
||||||
ADD CONSTRAINT fk_c63cbf6c25 FOREIGN KEY (closed_by_id) REFERENCES users(id) ON DELETE SET NULL;
|
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
|
ALTER TABLE ONLY issue_links
|
||||||
ADD CONSTRAINT fk_c900194ff2 FOREIGN KEY (source_id) REFERENCES issues(id) ON DELETE CASCADE;
|
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
|
ALTER TABLE ONLY lists
|
||||||
ADD CONSTRAINT fk_d6cf4279f7 FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
|
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
|
ALTER TABLE ONLY metrics_users_starred_dashboards
|
||||||
ADD CONSTRAINT fk_d76a2b9a8c FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
|
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",
|
"web_url": "http://192.168.1.8:3000/root",
|
||||||
"expires_at": "2012-10-22T14:13:35Z",
|
"expires_at": "2012-10-22T14:13:35Z",
|
||||||
"access_level": 30,
|
"access_level": 30,
|
||||||
"group_saml_identity": null
|
"group_saml_identity": null,
|
||||||
|
"membership_state": "active"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 2,
|
"id": 2,
|
||||||
|
@ -81,7 +82,8 @@ Example response:
|
||||||
"extern_uid":"ABC-1234567890",
|
"extern_uid":"ABC-1234567890",
|
||||||
"provider": "group_saml",
|
"provider": "group_saml",
|
||||||
"saml_provider_id": 10
|
"saml_provider_id": 10
|
||||||
}
|
},
|
||||||
|
"membership_state": "active"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
@ -126,7 +128,8 @@ Example response:
|
||||||
"web_url": "http://192.168.1.8:3000/root",
|
"web_url": "http://192.168.1.8:3000/root",
|
||||||
"expires_at": "2012-10-22T14:13:35Z",
|
"expires_at": "2012-10-22T14:13:35Z",
|
||||||
"access_level": 30,
|
"access_level": 30,
|
||||||
"group_saml_identity": null
|
"group_saml_identity": null,
|
||||||
|
"membership_state": "active"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 2,
|
"id": 2,
|
||||||
|
@ -142,7 +145,8 @@ Example response:
|
||||||
"extern_uid":"ABC-1234567890",
|
"extern_uid":"ABC-1234567890",
|
||||||
"provider": "group_saml",
|
"provider": "group_saml",
|
||||||
"saml_provider_id": 10
|
"saml_provider_id": 10
|
||||||
}
|
},
|
||||||
|
"membership_state": "active"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 3,
|
"id": 3,
|
||||||
|
@ -153,7 +157,8 @@ Example response:
|
||||||
"web_url": "http://192.168.1.8:3000/root",
|
"web_url": "http://192.168.1.8:3000/root",
|
||||||
"expires_at": "2012-11-22T14:13:35Z",
|
"expires_at": "2012-11-22T14:13:35Z",
|
||||||
"access_level": 30,
|
"access_level": 30,
|
||||||
"group_saml_identity": null
|
"group_saml_identity": null,
|
||||||
|
"membership_state": "active"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
@ -191,7 +196,8 @@ Example response:
|
||||||
"email": "john@example.com",
|
"email": "john@example.com",
|
||||||
"created_at": "2012-10-22T14:13:35Z",
|
"created_at": "2012-10-22T14:13:35Z",
|
||||||
"expires_at": null,
|
"expires_at": null,
|
||||||
"group_saml_identity": null
|
"group_saml_identity": null,
|
||||||
|
"membership_state": "active"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -229,7 +235,8 @@ Example response:
|
||||||
"access_level": 30,
|
"access_level": 30,
|
||||||
"email": "john@example.com",
|
"email": "john@example.com",
|
||||||
"expires_at": null,
|
"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.
|
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.
|
It can be customized before merging a merge request.
|
||||||
|
|
||||||
![A squash commit message editor](img/squash_mr_message.png)
|
![A squash commit message editor](img/squash_mr_message.png)
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
abuse_reports: :gitlab_main
|
abuse_reports: :gitlab_main
|
||||||
|
agent_activity_events: :gitlab_main
|
||||||
agent_group_authorizations: :gitlab_main
|
agent_group_authorizations: :gitlab_main
|
||||||
agent_project_authorizations: :gitlab_main
|
agent_project_authorizations: :gitlab_main
|
||||||
alert_management_alert_assignees: :gitlab_main
|
alert_management_alert_assignees: :gitlab_main
|
||||||
|
|
|
@ -38540,9 +38540,15 @@ msgstr ""
|
||||||
msgid "VulnerabilityManagement|Something went wrong, could not update vulnerability state."
|
msgid "VulnerabilityManagement|Something went wrong, could not update vulnerability state."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "VulnerabilityManagement|Summary, detailed description, steps to reproduce, etc."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "VulnerabilityManagement|Verified as fixed or mitigated"
|
msgid "VulnerabilityManagement|Verified as fixed or mitigated"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "VulnerabilityManagement|Vulnerability name or type. Ex: Cross-site scripting"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "VulnerabilityManagement|Will not fix or a false-positive"
|
msgid "VulnerabilityManagement|Will not fix or a false-positive"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
require 'spec_helper'
|
require 'spec_helper'
|
||||||
|
|
||||||
RSpec.describe ApplicationExperiment, :experiment do
|
RSpec.describe ApplicationExperiment, :experiment do
|
||||||
subject { described_class.new('namespaced/stub', **context) }
|
subject(:application_experiment) { described_class.new('namespaced/stub', **context) }
|
||||||
|
|
||||||
let(:context) { {} }
|
let(:context) { {} }
|
||||||
let(:feature_definition) { { name: 'namespaced_stub', type: 'experiment', default_enabled: false } }
|
let(:feature_definition) { { name: 'namespaced_stub', type: 'experiment', default_enabled: false } }
|
||||||
|
@ -15,7 +15,7 @@ RSpec.describe ApplicationExperiment, :experiment do
|
||||||
end
|
end
|
||||||
|
|
||||||
before do
|
before do
|
||||||
allow(subject).to receive(:enabled?).and_return(true)
|
allow(application_experiment).to receive(:enabled?).and_return(true)
|
||||||
end
|
end
|
||||||
|
|
||||||
it "doesn't raise an exception without a defined control" do
|
it "doesn't raise an exception without a defined control" do
|
||||||
|
@ -26,7 +26,7 @@ RSpec.describe ApplicationExperiment, :experiment do
|
||||||
|
|
||||||
describe "#enabled?" do
|
describe "#enabled?" do
|
||||||
before 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(Feature::Definition).to receive(:get).and_return('_instance_')
|
||||||
allow(Gitlab).to receive(:dev_env_or_com?).and_return(true)
|
allow(Gitlab).to receive(:dev_env_or_com?).and_return(true)
|
||||||
|
@ -34,25 +34,25 @@ RSpec.describe ApplicationExperiment, :experiment do
|
||||||
end
|
end
|
||||||
|
|
||||||
it "is enabled when all criteria are met" do
|
it "is enabled when all criteria are met" do
|
||||||
expect(subject).to be_enabled
|
expect(application_experiment).to be_enabled
|
||||||
end
|
end
|
||||||
|
|
||||||
it "isn't enabled if the feature definition doesn't exist" do
|
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(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
|
end
|
||||||
|
|
||||||
it "isn't enabled if we're not in dev or dotcom environments" do
|
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(Gitlab).to receive(:dev_env_or_com?).and_return(false)
|
||||||
|
|
||||||
expect(subject).not_to be_enabled
|
expect(application_experiment).not_to be_enabled
|
||||||
end
|
end
|
||||||
|
|
||||||
it "isn't enabled if the feature flag state is :off" do
|
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(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
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -60,11 +60,11 @@ RSpec.describe ApplicationExperiment, :experiment do
|
||||||
let(:should_track) { true }
|
let(:should_track) { true }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
allow(subject).to receive(:should_track?).and_return(should_track)
|
allow(application_experiment).to receive(:should_track?).and_return(should_track)
|
||||||
end
|
end
|
||||||
|
|
||||||
it "tracks the assignment", :snowplow do
|
it "tracks the assignment", :snowplow do
|
||||||
subject.publish
|
application_experiment.publish
|
||||||
|
|
||||||
expect_snowplow_event(
|
expect_snowplow_event(
|
||||||
category: 'namespaced/stub',
|
category: 'namespaced/stub',
|
||||||
|
@ -74,24 +74,24 @@ RSpec.describe ApplicationExperiment, :experiment do
|
||||||
end
|
end
|
||||||
|
|
||||||
it "publishes to the client" do
|
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
|
end
|
||||||
|
|
||||||
it "publishes to the database if we've opted for that" do
|
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
|
end
|
||||||
|
|
||||||
context 'when we should not track' do
|
context 'when we should not track' do
|
||||||
let(:should_track) { false }
|
let(:should_track) { false }
|
||||||
|
|
||||||
it 'does not track an event to Snowplow', :snowplow do
|
it 'does not track an event to Snowplow', :snowplow do
|
||||||
subject.publish
|
application_experiment.publish
|
||||||
|
|
||||||
expect_no_snowplow_event
|
expect_no_snowplow_event
|
||||||
end
|
end
|
||||||
|
@ -102,13 +102,13 @@ RSpec.describe ApplicationExperiment, :experiment do
|
||||||
signature = { key: '86208ac54ca798e11f127e8b23ec396a', variant: 'control' }
|
signature = { key: '86208ac54ca798e11f127e8b23ec396a', variant: 'control' }
|
||||||
expect(Gon).to receive(:push).with({ experiment: { 'namespaced/stub' => hash_including(signature) } }, true)
|
expect(Gon).to receive(:push).with({ experiment: { 'namespaced/stub' => hash_including(signature) } }, true)
|
||||||
|
|
||||||
subject.publish_to_client
|
application_experiment.publish_to_client
|
||||||
end
|
end
|
||||||
|
|
||||||
it "handles when Gon raises exceptions (like when it can't be pushed into)" do
|
it "handles when Gon raises exceptions (like when it can't be pushed into)" do
|
||||||
expect(Gon).to receive(:push).and_raise(NoMethodError)
|
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
|
end
|
||||||
|
|
||||||
context 'when we should not track' do
|
context 'when we should not track' do
|
||||||
|
@ -117,7 +117,7 @@ RSpec.describe ApplicationExperiment, :experiment do
|
||||||
it 'returns early' do
|
it 'returns early' do
|
||||||
expect(Gon).not_to receive(:push)
|
expect(Gon).not_to receive(:push)
|
||||||
|
|
||||||
subject.publish_to_client
|
application_experiment.publish_to_client
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -125,13 +125,15 @@ RSpec.describe ApplicationExperiment, :experiment do
|
||||||
describe '#publish_to_database' do
|
describe '#publish_to_database' do
|
||||||
using RSpec::Parameterized::TableSyntax
|
using RSpec::Parameterized::TableSyntax
|
||||||
|
|
||||||
|
let(:publish_to_database) { application_experiment.publish_to_database }
|
||||||
|
|
||||||
shared_examples 'does not record to the database' do
|
shared_examples 'does not record to the database' do
|
||||||
it 'does not create an experiment record' 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
|
end
|
||||||
|
|
||||||
it 'does not create an experiment subject record' do
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -139,16 +141,16 @@ RSpec.describe ApplicationExperiment, :experiment do
|
||||||
let(:context) { { context_key => context_value } }
|
let(:context) { { context_key => context_value } }
|
||||||
|
|
||||||
where(:context_key, :context_value, :object_type) do
|
where(:context_key, :context_value, :object_type) do
|
||||||
:namespace | build(:namespace) | :namespace
|
:namespace | build(:namespace, id: non_existing_record_id) | :namespace
|
||||||
:group | build(:namespace) | :namespace
|
:group | build(:namespace, id: non_existing_record_id) | :namespace
|
||||||
:project | build(:project) | :project
|
:project | build(:project, id: non_existing_record_id) | :project
|
||||||
:user | build(:user) | :user
|
:user | build(:user, id: non_existing_record_id) | :user
|
||||||
:actor | build(:user) | :user
|
:actor | build(:user, id: non_existing_record_id) | :user
|
||||||
end
|
end
|
||||||
|
|
||||||
with_them do
|
with_them do
|
||||||
it 'creates an experiment and experiment subject record' 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(Experiment.last.name).to eq('namespaced/stub')
|
||||||
expect(ExperimentSubject.last.send(object_type)).to eq(context[context_key])
|
expect(ExperimentSubject.last.send(object_type)).to eq(context[context_key])
|
||||||
|
@ -156,6 +158,16 @@ RSpec.describe ApplicationExperiment, :experiment do
|
||||||
end
|
end
|
||||||
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
|
context 'when there is not a usable subject' do
|
||||||
let(:context) { { context_key => context_value } }
|
let(:context) { { context_key => context_value } }
|
||||||
|
|
||||||
|
@ -183,15 +195,15 @@ RSpec.describe ApplicationExperiment, :experiment do
|
||||||
end
|
end
|
||||||
|
|
||||||
it "doesn't track if we shouldn't track" do
|
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
|
expect_no_snowplow_event
|
||||||
end
|
end
|
||||||
|
|
||||||
it "tracks the event with the expected arguments and merged contexts" do
|
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(
|
expect_snowplow_event(
|
||||||
category: 'namespaced/stub',
|
category: 'namespaced/stub',
|
||||||
|
@ -233,7 +245,7 @@ RSpec.describe ApplicationExperiment, :experiment do
|
||||||
|
|
||||||
describe "#key_for" do
|
describe "#key_for" do
|
||||||
it "generates MD5 hashes" 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
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -256,26 +268,26 @@ RSpec.describe ApplicationExperiment, :experiment do
|
||||||
with_them do
|
with_them do
|
||||||
it "returns the url or nil if invalid" do
|
it "returns the url or nil if invalid" do
|
||||||
allow(Gitlab).to receive(:dev_env_or_com?).and_return(true)
|
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
|
end
|
||||||
|
|
||||||
it "considers all urls invalid when not on dev or com" do
|
it "considers all urls invalid when not on dev or com" do
|
||||||
allow(Gitlab).to receive(:dev_env_or_com?).and_return(false)
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
it "generates the correct urls based on where the engine was mounted" do
|
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')
|
url = Rails.application.routes.url_helpers.experiment_redirect_url(application_experiment, url: 'https://docs.gitlab.com')
|
||||||
expect(url).to include("/-/experiment/namespaced%2Fstub:#{subject.context.key}?https://docs.gitlab.com")
|
expect(url).to include("/-/experiment/namespaced%2Fstub:#{application_experiment.context.key}?https://docs.gitlab.com")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context "when resolving variants" do
|
context "when resolving variants" do
|
||||||
it "uses the default value as specified in the yaml" 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
|
end
|
||||||
|
|
||||||
context "when rolled out to 100%" do
|
context "when rolled out to 100%" do
|
||||||
|
@ -284,10 +296,10 @@ RSpec.describe ApplicationExperiment, :experiment do
|
||||||
end
|
end
|
||||||
|
|
||||||
it "returns the first variant name" do
|
it "returns the first variant name" do
|
||||||
subject.try(:variant1) {}
|
application_experiment.try(:variant1) {}
|
||||||
subject.try(:variant2) {}
|
application_experiment.try(:variant2) {}
|
||||||
|
|
||||||
expect(subject.variant.name).to eq('variant1')
|
expect(application_experiment.variant.name).to eq('variant1')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -298,18 +310,18 @@ RSpec.describe ApplicationExperiment, :experiment do
|
||||||
before do
|
before do
|
||||||
allow(Gitlab::Experiment::Configuration).to receive(:cache).and_call_original
|
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
|
application_experiment.use { } # setup the control
|
||||||
subject.try { } # setup the candidate
|
application_experiment.try { } # setup the candidate
|
||||||
end
|
end
|
||||||
|
|
||||||
it "caches the variant determined by the variant resolver" do
|
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
|
end
|
||||||
|
|
||||||
it "doesn't cache a variant if we don't explicitly provide one" do
|
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.
|
# the control.
|
||||||
stub_feature_flags(namespaced_stub: false) # simulate being not rolled out
|
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
|
end
|
||||||
|
|
||||||
it "caches a control variant if we assign it specifically" do
|
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
|
# that this context will always get the control variant unless we delete
|
||||||
# the field from the cache (or clear the entire experiment cache) -- or
|
# the field from the cache (or clear the entire experiment cache) -- or
|
||||||
# write code that would specify a different variant.
|
# 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
|
end
|
||||||
|
|
||||||
context "arbitrary attributes" do
|
context "arbitrary attributes" do
|
||||||
before do
|
before do
|
||||||
subject.cache.store.clear(key: subject.name + '_attrs')
|
application_experiment.cache.store.clear(key: application_experiment.name + '_attrs')
|
||||||
end
|
end
|
||||||
|
|
||||||
it "sets and gets attributes about an experiment" do
|
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
|
end
|
||||||
|
|
||||||
it "increments a value for an experiment" do
|
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(application_experiment.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(2)
|
||||||
end
|
end
|
||||||
end
|
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,
|
state: :blocked,
|
||||||
organization: 'GitLab - work info test',
|
organization: 'GitLab - work info test',
|
||||||
job_title: 'Frontend Engineer',
|
job_title: 'Frontend Engineer',
|
||||||
pronunciation: 'pruh-nuhn-see-ay-shn'
|
pronunciation: 'pruh-nuhn-see-ay-shn',
|
||||||
|
bio: 'My personal bio'
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
let_it_be(:status) { create(:user_status, user: user, message: "Working hard!") }
|
let_it_be(:status) { create(:user_status, user: user, message: "Working hard!") }
|
||||||
|
|
||||||
it 'shows no tab' do
|
before do
|
||||||
subject
|
visit_profile
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'shows no tab' do
|
||||||
expect(page).to have_css("div.profile-header")
|
expect(page).to have_css("div.profile-header")
|
||||||
expect(page).not_to have_css("ul.nav-links")
|
expect(page).not_to have_css("ul.nav-links")
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'shows blocked message' do
|
it 'shows blocked message' do
|
||||||
subject
|
|
||||||
|
|
||||||
expect(page).to have_content("This user is blocked")
|
expect(page).to have_content("This user is blocked")
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'shows user name as blocked' do
|
it 'shows user name as blocked' do
|
||||||
subject
|
|
||||||
|
|
||||||
expect(page).to have_css(".cover-title", text: 'Blocked user')
|
expect(page).to have_css(".cover-title", text: 'Blocked user')
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'shows no additional fields' do
|
it 'shows no additional fields' do
|
||||||
subject
|
|
||||||
|
|
||||||
expect(page).not_to have_css(".profile-user-bio")
|
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('GitLab - work info test')
|
||||||
expect(page).not_to have_content('Frontend Engineer')
|
expect(page).not_to have_content('Frontend Engineer')
|
||||||
|
@ -243,10 +240,10 @@ RSpec.describe 'User page' do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'shows username' do
|
it 'shows username' do
|
||||||
subject
|
|
||||||
|
|
||||||
expect(page).to have_content("@#{user.username}")
|
expect(page).to have_content("@#{user.username}")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'default brand title page meta description'
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'with unconfirmed user' do
|
context 'with unconfirmed user' do
|
||||||
|
@ -256,7 +253,8 @@ RSpec.describe 'User page' do
|
||||||
:unconfirmed,
|
:unconfirmed,
|
||||||
organization: 'GitLab - work info test',
|
organization: 'GitLab - work info test',
|
||||||
job_title: 'Frontend Engineer',
|
job_title: 'Frontend Engineer',
|
||||||
pronunciation: 'pruh-nuhn-see-ay-shn'
|
pronunciation: 'pruh-nuhn-see-ay-shn',
|
||||||
|
bio: 'My personal bio'
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -287,6 +285,8 @@ RSpec.describe 'User page' do
|
||||||
it 'shows private profile message' do
|
it 'shows private profile message' do
|
||||||
expect(page).to have_content("This user has a private profile")
|
expect(page).to have_content("This user has a private profile")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'default brand title page meta description'
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when visited by an authenticated user' do
|
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
|
// eslint-disable-next-line class-methods-use-this
|
||||||
provides() {
|
provides() {
|
||||||
return {
|
return {
|
||||||
|
@ -8,7 +8,7 @@ export class MyClassExtension {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MyFnExtension() {
|
export function SEFnExtension() {
|
||||||
return {
|
return {
|
||||||
fnExtMethod: () => 'fn own method',
|
fnExtMethod: () => 'fn own method',
|
||||||
provides: () => {
|
provides: () => {
|
||||||
|
@ -19,7 +19,7 @@ export function MyFnExtension() {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MyConstExt = () => {
|
export const SEConstExt = () => {
|
||||||
return {
|
return {
|
||||||
provides: () => {
|
provides: () => {
|
||||||
return {
|
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 = {
|
export const conflictingExtensions = {
|
||||||
WithInstanceExt: () => {
|
WithInstanceExt: () => {
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -22,15 +22,15 @@ describe('Editor Extension', () => {
|
||||||
|
|
||||||
it.each`
|
it.each`
|
||||||
definition | setupOptions | expectedName
|
definition | setupOptions | expectedName
|
||||||
${helpers.MyClassExtension} | ${undefined} | ${'MyClassExtension'}
|
${helpers.SEClassExtension} | ${undefined} | ${'SEClassExtension'}
|
||||||
${helpers.MyClassExtension} | ${{}} | ${'MyClassExtension'}
|
${helpers.SEClassExtension} | ${{}} | ${'SEClassExtension'}
|
||||||
${helpers.MyClassExtension} | ${dummyObj} | ${'MyClassExtension'}
|
${helpers.SEClassExtension} | ${dummyObj} | ${'SEClassExtension'}
|
||||||
${helpers.MyFnExtension} | ${undefined} | ${'MyFnExtension'}
|
${helpers.SEFnExtension} | ${undefined} | ${'SEFnExtension'}
|
||||||
${helpers.MyFnExtension} | ${{}} | ${'MyFnExtension'}
|
${helpers.SEFnExtension} | ${{}} | ${'SEFnExtension'}
|
||||||
${helpers.MyFnExtension} | ${dummyObj} | ${'MyFnExtension'}
|
${helpers.SEFnExtension} | ${dummyObj} | ${'SEFnExtension'}
|
||||||
${helpers.MyConstExt} | ${undefined} | ${'MyConstExt'}
|
${helpers.SEConstExt} | ${undefined} | ${'SEConstExt'}
|
||||||
${helpers.MyConstExt} | ${{}} | ${'MyConstExt'}
|
${helpers.SEConstExt} | ${{}} | ${'SEConstExt'}
|
||||||
${helpers.MyConstExt} | ${dummyObj} | ${'MyConstExt'}
|
${helpers.SEConstExt} | ${dummyObj} | ${'SEConstExt'}
|
||||||
`(
|
`(
|
||||||
'correctly creates extension for definition = $definition and setupOptions = $setupOptions',
|
'correctly creates extension for definition = $definition and setupOptions = $setupOptions',
|
||||||
({ definition, setupOptions, expectedName }) => {
|
({ definition, setupOptions, expectedName }) => {
|
||||||
|
@ -51,9 +51,9 @@ describe('Editor Extension', () => {
|
||||||
describe('api', () => {
|
describe('api', () => {
|
||||||
it.each`
|
it.each`
|
||||||
definition | expectedKeys
|
definition | expectedKeys
|
||||||
${helpers.MyClassExtension} | ${['shared', 'classExtMethod']}
|
${helpers.SEClassExtension} | ${['shared', 'classExtMethod']}
|
||||||
${helpers.MyFnExtension} | ${['fnExtMethod']}
|
${helpers.SEFnExtension} | ${['fnExtMethod']}
|
||||||
${helpers.MyConstExt} | ${['constExtMethod']}
|
${helpers.SEConstExt} | ${['constExtMethod']}
|
||||||
`('correctly returns API for $definition', ({ definition, expectedKeys }) => {
|
`('correctly returns API for $definition', ({ definition, expectedKeys }) => {
|
||||||
const extension = new EditorExtension({ definition });
|
const extension = new EditorExtension({ definition });
|
||||||
const expectedApi = Object.fromEntries(
|
const expectedApi = Object.fromEntries(
|
||||||
|
|
|
@ -6,23 +6,29 @@ import {
|
||||||
EDITOR_EXTENSION_NOT_REGISTERED_ERROR,
|
EDITOR_EXTENSION_NOT_REGISTERED_ERROR,
|
||||||
EDITOR_EXTENSION_NOT_SPECIFIED_FOR_UNUSE_ERROR,
|
EDITOR_EXTENSION_NOT_SPECIFIED_FOR_UNUSE_ERROR,
|
||||||
} from '~/editor/constants';
|
} from '~/editor/constants';
|
||||||
import Instance from '~/editor/source_editor_instance';
|
import SourceEditorInstance from '~/editor/source_editor_instance';
|
||||||
import { sprintf } from '~/locale';
|
import { sprintf } from '~/locale';
|
||||||
import { MyClassExtension, conflictingExtensions, MyFnExtension, MyConstExt } from './helpers';
|
import {
|
||||||
|
SEClassExtension,
|
||||||
|
conflictingExtensions,
|
||||||
|
SEFnExtension,
|
||||||
|
SEConstExt,
|
||||||
|
SEWithSetupExt,
|
||||||
|
} from './helpers';
|
||||||
|
|
||||||
describe('Source Editor Instance', () => {
|
describe('Source Editor Instance', () => {
|
||||||
let seInstance;
|
let seInstance;
|
||||||
|
|
||||||
const defSetupOptions = { foo: 'bar' };
|
const defSetupOptions = { foo: 'bar' };
|
||||||
const fullExtensionsArray = [
|
const fullExtensionsArray = [
|
||||||
{ definition: MyClassExtension },
|
{ definition: SEClassExtension },
|
||||||
{ definition: MyFnExtension },
|
{ definition: SEFnExtension },
|
||||||
{ definition: MyConstExt },
|
{ definition: SEConstExt },
|
||||||
];
|
];
|
||||||
const fullExtensionsArrayWithOptions = [
|
const fullExtensionsArrayWithOptions = [
|
||||||
{ definition: MyClassExtension, setupOptions: defSetupOptions },
|
{ definition: SEClassExtension, setupOptions: defSetupOptions },
|
||||||
{ definition: MyFnExtension, setupOptions: defSetupOptions },
|
{ definition: SEFnExtension, setupOptions: defSetupOptions },
|
||||||
{ definition: MyConstExt, setupOptions: defSetupOptions },
|
{ definition: SEConstExt, setupOptions: defSetupOptions },
|
||||||
];
|
];
|
||||||
|
|
||||||
const fooFn = jest.fn();
|
const fooFn = jest.fn();
|
||||||
|
@ -40,26 +46,26 @@ describe('Source Editor Instance', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('sets up the registry for the methods coming from extensions', () => {
|
it('sets up the registry for the methods coming from extensions', () => {
|
||||||
seInstance = new Instance();
|
seInstance = new SourceEditorInstance();
|
||||||
expect(seInstance.methods).toBeDefined();
|
expect(seInstance.methods).toBeDefined();
|
||||||
|
|
||||||
seInstance.use({ definition: MyClassExtension });
|
seInstance.use({ definition: SEClassExtension });
|
||||||
expect(seInstance.methods).toEqual({
|
expect(seInstance.methods).toEqual({
|
||||||
shared: 'MyClassExtension',
|
shared: 'SEClassExtension',
|
||||||
classExtMethod: 'MyClassExtension',
|
classExtMethod: 'SEClassExtension',
|
||||||
});
|
});
|
||||||
|
|
||||||
seInstance.use({ definition: MyFnExtension });
|
seInstance.use({ definition: SEFnExtension });
|
||||||
expect(seInstance.methods).toEqual({
|
expect(seInstance.methods).toEqual({
|
||||||
shared: 'MyClassExtension',
|
shared: 'SEClassExtension',
|
||||||
classExtMethod: 'MyClassExtension',
|
classExtMethod: 'SEClassExtension',
|
||||||
fnExtMethod: 'MyFnExtension',
|
fnExtMethod: 'SEFnExtension',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('proxy', () => {
|
describe('proxy', () => {
|
||||||
it('returns prop from an extension if extension provides it', () => {
|
it('returns prop from an extension if extension provides it', () => {
|
||||||
seInstance = new Instance();
|
seInstance = new SourceEditorInstance();
|
||||||
seInstance.use({ definition: DummyExt });
|
seInstance.use({ definition: DummyExt });
|
||||||
|
|
||||||
expect(fooFn).not.toHaveBeenCalled();
|
expect(fooFn).not.toHaveBeenCalled();
|
||||||
|
@ -67,8 +73,58 @@ describe('Source Editor Instance', () => {
|
||||||
expect(fooFn).toHaveBeenCalled();
|
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', () => {
|
it('returns props from SE instance itself if no extension provides the prop', () => {
|
||||||
seInstance = new Instance({
|
seInstance = new SourceEditorInstance({
|
||||||
use: fooFn,
|
use: fooFn,
|
||||||
});
|
});
|
||||||
jest.spyOn(seInstance, 'use').mockImplementation(() => {});
|
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', () => {
|
it('returns props from Monaco instance when the prop does not exist on the SE instance', () => {
|
||||||
seInstance = new Instance({
|
seInstance = new SourceEditorInstance({
|
||||||
fooFn,
|
fooFn,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -92,13 +148,13 @@ describe('Source Editor Instance', () => {
|
||||||
|
|
||||||
describe('public API', () => {
|
describe('public API', () => {
|
||||||
it.each(['use', 'unuse'], 'provides "%s" as public method by default', (method) => {
|
it.each(['use', 'unuse'], 'provides "%s" as public method by default', (method) => {
|
||||||
seInstance = new Instance();
|
seInstance = new SourceEditorInstance();
|
||||||
expect(seInstance[method]).toBeDefined();
|
expect(seInstance[method]).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('use', () => {
|
describe('use', () => {
|
||||||
it('extends the SE instance with methods provided by an extension', () => {
|
it('extends the SE instance with methods provided by an extension', () => {
|
||||||
seInstance = new Instance();
|
seInstance = new SourceEditorInstance();
|
||||||
seInstance.use({ definition: DummyExt });
|
seInstance.use({ definition: DummyExt });
|
||||||
|
|
||||||
expect(fooFn).not.toHaveBeenCalled();
|
expect(fooFn).not.toHaveBeenCalled();
|
||||||
|
@ -108,15 +164,15 @@ describe('Source Editor Instance', () => {
|
||||||
|
|
||||||
it.each`
|
it.each`
|
||||||
extensions | expectedProps
|
extensions | expectedProps
|
||||||
${{ definition: MyClassExtension }} | ${['shared', 'classExtMethod']}
|
${{ definition: SEClassExtension }} | ${['shared', 'classExtMethod']}
|
||||||
${{ definition: MyFnExtension }} | ${['fnExtMethod']}
|
${{ definition: SEFnExtension }} | ${['fnExtMethod']}
|
||||||
${{ definition: MyConstExt }} | ${['constExtMethod']}
|
${{ definition: SEConstExt }} | ${['constExtMethod']}
|
||||||
${fullExtensionsArray} | ${['shared', 'classExtMethod', 'fnExtMethod', 'constExtMethod']}
|
${fullExtensionsArray} | ${['shared', 'classExtMethod', 'fnExtMethod', 'constExtMethod']}
|
||||||
${fullExtensionsArrayWithOptions} | ${['shared', 'classExtMethod', 'fnExtMethod', 'constExtMethod']}
|
${fullExtensionsArrayWithOptions} | ${['shared', 'classExtMethod', 'fnExtMethod', 'constExtMethod']}
|
||||||
`(
|
`(
|
||||||
'Should register $expectedProps when extension is "$extensions"',
|
'Should register $expectedProps when extension is "$extensions"',
|
||||||
({ extensions, expectedProps }) => {
|
({ extensions, expectedProps }) => {
|
||||||
seInstance = new Instance();
|
seInstance = new SourceEditorInstance();
|
||||||
expect(seInstance.extensionsAPI).toHaveLength(0);
|
expect(seInstance.extensionsAPI).toHaveLength(0);
|
||||||
|
|
||||||
seInstance.use(extensions);
|
seInstance.use(extensions);
|
||||||
|
@ -127,15 +183,15 @@ describe('Source Editor Instance', () => {
|
||||||
|
|
||||||
it.each`
|
it.each`
|
||||||
definition | preInstalledExtDefinition | expectedErrorProp
|
definition | preInstalledExtDefinition | expectedErrorProp
|
||||||
${conflictingExtensions.WithInstanceExt} | ${MyClassExtension} | ${'use'}
|
${conflictingExtensions.WithInstanceExt} | ${SEClassExtension} | ${'use'}
|
||||||
${conflictingExtensions.WithInstanceExt} | ${null} | ${'use'}
|
${conflictingExtensions.WithInstanceExt} | ${null} | ${'use'}
|
||||||
${conflictingExtensions.WithAnotherExt} | ${null} | ${undefined}
|
${conflictingExtensions.WithAnotherExt} | ${null} | ${undefined}
|
||||||
${conflictingExtensions.WithAnotherExt} | ${MyClassExtension} | ${'shared'}
|
${conflictingExtensions.WithAnotherExt} | ${SEClassExtension} | ${'shared'}
|
||||||
${MyClassExtension} | ${conflictingExtensions.WithAnotherExt} | ${'shared'}
|
${SEClassExtension} | ${conflictingExtensions.WithAnotherExt} | ${'shared'}
|
||||||
`(
|
`(
|
||||||
'logs the naming conflict error when registering $definition',
|
'logs the naming conflict error when registering $definition',
|
||||||
({ definition, preInstalledExtDefinition, expectedErrorProp }) => {
|
({ definition, preInstalledExtDefinition, expectedErrorProp }) => {
|
||||||
seInstance = new Instance();
|
seInstance = new SourceEditorInstance();
|
||||||
jest.spyOn(console, 'error').mockImplementation(() => {});
|
jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||||
|
|
||||||
if (preInstalledExtDefinition) {
|
if (preInstalledExtDefinition) {
|
||||||
|
@ -175,7 +231,7 @@ describe('Source Editor Instance', () => {
|
||||||
`(
|
`(
|
||||||
'Should throw $thrownError when extension is "$extensions"',
|
'Should throw $thrownError when extension is "$extensions"',
|
||||||
({ extensions, thrownError }) => {
|
({ extensions, thrownError }) => {
|
||||||
seInstance = new Instance();
|
seInstance = new SourceEditorInstance();
|
||||||
const useExtension = () => {
|
const useExtension = () => {
|
||||||
seInstance.use(extensions);
|
seInstance.use(extensions);
|
||||||
};
|
};
|
||||||
|
@ -188,24 +244,24 @@ describe('Source Editor Instance', () => {
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
extensionStore = new Map();
|
extensionStore = new Map();
|
||||||
seInstance = new Instance({}, extensionStore);
|
seInstance = new SourceEditorInstance({}, extensionStore);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('stores _instances_ of the used extensions in a global registry', () => {
|
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.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', () => {
|
it('does not duplicate entries in the registry', () => {
|
||||||
jest.spyOn(extensionStore, 'set');
|
jest.spyOn(extensionStore, 'set');
|
||||||
|
|
||||||
const extension1 = seInstance.use({ definition: MyClassExtension });
|
const extension1 = seInstance.use({ definition: SEClassExtension });
|
||||||
seInstance.use({ definition: MyClassExtension });
|
seInstance.use({ definition: SEClassExtension });
|
||||||
|
|
||||||
expect(extensionStore.set).toHaveBeenCalledTimes(1);
|
expect(extensionStore.set).toHaveBeenCalledTimes(1);
|
||||||
expect(extensionStore.set).toHaveBeenCalledWith('MyClassExtension', extension1);
|
expect(extensionStore.set).toHaveBeenCalledWith('SEClassExtension', extension1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each`
|
it.each`
|
||||||
|
@ -222,20 +278,20 @@ describe('Source Editor Instance', () => {
|
||||||
jest.spyOn(extensionStore, 'set');
|
jest.spyOn(extensionStore, 'set');
|
||||||
|
|
||||||
const extension1 = seInstance.use({
|
const extension1 = seInstance.use({
|
||||||
definition: MyClassExtension,
|
definition: SEClassExtension,
|
||||||
setupOptions: currentSetupOptions,
|
setupOptions: currentSetupOptions,
|
||||||
});
|
});
|
||||||
const extension2 = seInstance.use({
|
const extension2 = seInstance.use({
|
||||||
definition: MyClassExtension,
|
definition: SEClassExtension,
|
||||||
setupOptions: newSetupOptions,
|
setupOptions: newSetupOptions,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(extensionStore.size).toBe(1);
|
expect(extensionStore.size).toBe(1);
|
||||||
expect(extensionStore.set).toHaveBeenCalledTimes(expectedCallTimes);
|
expect(extensionStore.set).toHaveBeenCalledTimes(expectedCallTimes);
|
||||||
if (expectedCallTimes > 1) {
|
if (expectedCallTimes > 1) {
|
||||||
expect(extensionStore.set).toHaveBeenCalledWith('MyClassExtension', extension2);
|
expect(extensionStore.set).toHaveBeenCalledWith('SEClassExtension', extension2);
|
||||||
} else {
|
} 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"`,
|
`Should throw "${EDITOR_EXTENSION_NOT_SPECIFIED_FOR_UNUSE_ERROR}" when extension is "$unuseExtension"`,
|
||||||
({ unuseExtension, thrownError }) => {
|
({ unuseExtension, thrownError }) => {
|
||||||
seInstance = new Instance();
|
seInstance = new SourceEditorInstance();
|
||||||
const unuse = () => {
|
const unuse = () => {
|
||||||
seInstance.unuse(unuseExtension);
|
seInstance.unuse(unuseExtension);
|
||||||
};
|
};
|
||||||
|
@ -262,16 +318,16 @@ describe('Source Editor Instance', () => {
|
||||||
|
|
||||||
it.each`
|
it.each`
|
||||||
initExtensions | unuseExtensionIndex | remainingAPI
|
initExtensions | unuseExtensionIndex | remainingAPI
|
||||||
${{ definition: MyClassExtension }} | ${0} | ${[]}
|
${{ definition: SEClassExtension }} | ${0} | ${[]}
|
||||||
${{ definition: MyFnExtension }} | ${0} | ${[]}
|
${{ definition: SEFnExtension }} | ${0} | ${[]}
|
||||||
${{ definition: MyConstExt }} | ${0} | ${[]}
|
${{ definition: SEConstExt }} | ${0} | ${[]}
|
||||||
${fullExtensionsArray} | ${0} | ${['fnExtMethod', 'constExtMethod']}
|
${fullExtensionsArray} | ${0} | ${['fnExtMethod', 'constExtMethod']}
|
||||||
${fullExtensionsArray} | ${1} | ${['shared', 'classExtMethod', 'constExtMethod']}
|
${fullExtensionsArray} | ${1} | ${['shared', 'classExtMethod', 'constExtMethod']}
|
||||||
${fullExtensionsArray} | ${2} | ${['shared', 'classExtMethod', 'fnExtMethod']}
|
${fullExtensionsArray} | ${2} | ${['shared', 'classExtMethod', 'fnExtMethod']}
|
||||||
`(
|
`(
|
||||||
'un-registers properties introduced by single extension $unuseExtension',
|
'un-registers properties introduced by single extension $unuseExtension',
|
||||||
({ initExtensions, unuseExtensionIndex, remainingAPI }) => {
|
({ initExtensions, unuseExtensionIndex, remainingAPI }) => {
|
||||||
seInstance = new Instance();
|
seInstance = new SourceEditorInstance();
|
||||||
const extensions = seInstance.use(initExtensions);
|
const extensions = seInstance.use(initExtensions);
|
||||||
|
|
||||||
if (Array.isArray(initExtensions)) {
|
if (Array.isArray(initExtensions)) {
|
||||||
|
@ -291,7 +347,7 @@ describe('Source Editor Instance', () => {
|
||||||
`(
|
`(
|
||||||
'un-registers properties introduced by multiple extensions $unuseExtension',
|
'un-registers properties introduced by multiple extensions $unuseExtension',
|
||||||
({ unuseExtensionIndex, remainingAPI }) => {
|
({ unuseExtensionIndex, remainingAPI }) => {
|
||||||
seInstance = new Instance();
|
seInstance = new SourceEditorInstance();
|
||||||
const extensions = seInstance.use(fullExtensionsArray);
|
const extensions = seInstance.use(fullExtensionsArray);
|
||||||
const extensionsToUnuse = extensions.filter((ext, index) =>
|
const extensionsToUnuse = extensions.filter((ext, index) =>
|
||||||
unuseExtensionIndex.includes(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', () => {
|
it('it does not remove entry from the global registry to keep for potential future re-use', () => {
|
||||||
const extensionStore = new Map();
|
const extensionStore = new Map();
|
||||||
seInstance = new Instance({}, extensionStore);
|
seInstance = new SourceEditorInstance({}, extensionStore);
|
||||||
const extensions = seInstance.use(fullExtensionsArray);
|
const extensions = seInstance.use(fullExtensionsArray);
|
||||||
const verifyExpectations = () => {
|
const verifyExpectations = () => {
|
||||||
const entries = extensionStore.entries();
|
const entries = extensionStore.entries();
|
||||||
const mockExtensions = ['MyClassExtension', 'MyFnExtension', 'MyConstExt'];
|
const mockExtensions = ['SEClassExtension', 'SEFnExtension', 'SEConstExt'];
|
||||||
expect(extensionStore.size).toBe(mockExtensions.length);
|
expect(extensionStore.size).toBe(mockExtensions.length);
|
||||||
mockExtensions.forEach((ext, index) => {
|
mockExtensions.forEach((ext, index) => {
|
||||||
expect(entries.next().value).toEqual([ext, extensions[index]]);
|
expect(entries.next().value).toEqual([ext, extensions[index]]);
|
||||||
|
@ -326,7 +382,7 @@ describe('Source Editor Instance', () => {
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
instanceModel = monacoEditor.createModel('');
|
instanceModel = monacoEditor.createModel('');
|
||||||
seInstance = new Instance({
|
seInstance = new SourceEditorInstance({
|
||||||
getModel: () => instanceModel,
|
getModel: () => instanceModel,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -363,7 +419,7 @@ describe('Source Editor Instance', () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
it('passes correct arguments to callback fns when using an extension', () => {
|
it('passes correct arguments to callback fns when using an extension', () => {
|
||||||
seInstance = new Instance();
|
seInstance = new SourceEditorInstance();
|
||||||
seInstance.use({
|
seInstance.use({
|
||||||
definition: MyFullExtWithCallbacks,
|
definition: MyFullExtWithCallbacks,
|
||||||
setupOptions: defSetupOptions,
|
setupOptions: defSetupOptions,
|
||||||
|
@ -373,7 +429,7 @@ describe('Source Editor Instance', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('passes correct arguments to callback fns when un-using an extension', () => {
|
it('passes correct arguments to callback fns when un-using an extension', () => {
|
||||||
seInstance = new Instance();
|
seInstance = new SourceEditorInstance();
|
||||||
const extension = seInstance.use({
|
const extension = seInstance.use({
|
||||||
definition: MyFullExtWithCallbacks,
|
definition: MyFullExtWithCallbacks,
|
||||||
setupOptions: defSetupOptions,
|
setupOptions: defSetupOptions,
|
||||||
|
|
|
@ -57,7 +57,7 @@ describe('Markdown Extension for Source Editor', () => {
|
||||||
blobPath: markdownPath,
|
blobPath: markdownPath,
|
||||||
blobContent: text,
|
blobContent: text,
|
||||||
});
|
});
|
||||||
editor.use(new EditorMarkdownExtension({ instance, previewMarkdownPath }));
|
instance.use(new EditorMarkdownExtension({ instance, previewMarkdownPath }));
|
||||||
panelSpy = jest.spyOn(EditorMarkdownExtension, 'togglePreviewPanel');
|
panelSpy = jest.spyOn(EditorMarkdownExtension, 'togglePreviewPanel');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
/* eslint-disable max-classes-per-file */
|
/* eslint-disable max-classes-per-file */
|
||||||
import { editor as monacoEditor, languages as monacoLanguages } from 'monaco-editor';
|
import { editor as monacoEditor, languages as monacoLanguages } from 'monaco-editor';
|
||||||
import waitForPromises from 'helpers/wait_for_promises';
|
|
||||||
import {
|
import {
|
||||||
SOURCE_EDITOR_INSTANCE_ERROR_NO_EL,
|
SOURCE_EDITOR_INSTANCE_ERROR_NO_EL,
|
||||||
URI_PREFIX,
|
URI_PREFIX,
|
||||||
|
@ -531,105 +530,19 @@ describe('Base editor', () => {
|
||||||
instance.use(FunctionExt);
|
instance.use(FunctionExt);
|
||||||
expect(instance.inst()).toEqual(editor.instances[0]);
|
expect(instance.inst()).toEqual(editor.instances[0]);
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
describe('extensions as an instance parameter', () => {
|
it('emits the EDITOR_READY_EVENT event after setting up the instance', () => {
|
||||||
let editorExtensionSpy;
|
jest.spyOn(monacoEditor, 'create').mockImplementation(() => {
|
||||||
const instanceConstructor = (extensions = []) => {
|
return {
|
||||||
return editor.createInstance({
|
setModel: jest.fn(),
|
||||||
el: editorEl,
|
onDidDispose: jest.fn(),
|
||||||
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;
|
|
||||||
});
|
});
|
||||||
|
const eventSpy = jest.fn();
|
||||||
editorEl.addEventListener(EDITOR_READY_EVENT, eventSpy);
|
editorEl.addEventListener(EDITOR_READY_EVENT, eventSpy);
|
||||||
instance = instanceConstructor('foo, bar');
|
expect(eventSpy).not.toHaveBeenCalled();
|
||||||
await waitForPromises();
|
instance = editor.createInstance({ el: editorEl });
|
||||||
expect(useSpy.mock.calls).toHaveLength(2);
|
expect(eventSpy).toHaveBeenCalled();
|
||||||
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);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
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])
|
expect(token.name).to eq(params[:name])
|
||||||
end
|
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
|
context 'when params are invalid' do
|
||||||
let(:params) { { agent_id: 'bad_id' } }
|
let(:params) { { agent_id: 'bad_id' } }
|
||||||
|
|
||||||
|
@ -54,6 +69,10 @@ RSpec.describe Clusters::AgentTokens::CreateService do
|
||||||
expect { subject }.not_to change(::Clusters::AgentToken, :count)
|
expect { subject }.not_to change(::Clusters::AgentToken, :count)
|
||||||
end
|
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
|
it 'returns validation errors', :aggregate_failures do
|
||||||
expect(subject.status).to eq(:error)
|
expect(subject.status).to eq(:error)
|
||||||
expect(subject.message).to eq(["Agent must exist", "Name can't be blank"])
|
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
|
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