diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-12-16 03:10:37 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-12-16 03:10:37 +0300 |
commit | 84025108bca604c1428d2cf6a6b69616ee90956c (patch) | |
tree | 7c0fd8ec032c12bee563daeee6c721e2d7f7d44f /spec | |
parent | 73778b9c53d13a2e06a693c30073366deedead8f (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
33 files changed, 2005 insertions, 43 deletions
diff --git a/spec/fixtures/scripts/internal_events/events/ee_event_without_identifiers.yml b/spec/fixtures/scripts/internal_events/events/ee_event_without_identifiers.yml new file mode 100644 index 00000000000..07f606fbe33 --- /dev/null +++ b/spec/fixtures/scripts/internal_events/events/ee_event_without_identifiers.yml @@ -0,0 +1,14 @@ +--- +description: Internal Event CLI is opened +category: InternalEventTracking +action: internal_events_cli_opened +product_section: analytics +product_stage: monitor +product_group: analytics_instrumentation +milestone: '16.6' +introduced_by_url: TODO +distributions: +- ee +tiers: +- premium +- ultimate diff --git a/spec/fixtures/scripts/internal_events/events/event_with_identifiers.yml b/spec/fixtures/scripts/internal_events/events/event_with_identifiers.yml new file mode 100644 index 00000000000..5050953920d --- /dev/null +++ b/spec/fixtures/scripts/internal_events/events/event_with_identifiers.yml @@ -0,0 +1,20 @@ +--- +description: Engineer uses Internal Event CLI to define a new event +category: InternalEventTracking +action: internal_events_cli_used +identifiers: +- project +- namespace +- user +product_section: analytics +product_stage: monitor +product_group: analytics_instrumentation +milestone: '16.6' +introduced_by_url: TODO +distributions: +- ce +- ee +tiers: +- free +- premium +- ultimate diff --git a/spec/fixtures/scripts/internal_events/events/keyboard_smashed_event.yml b/spec/fixtures/scripts/internal_events/events/keyboard_smashed_event.yml new file mode 100644 index 00000000000..c0ccbc03af7 --- /dev/null +++ b/spec/fixtures/scripts/internal_events/events/keyboard_smashed_event.yml @@ -0,0 +1,20 @@ +--- +description: random event string +category: InternalEventTracking +action: random_name +identifiers: +- project +- namespace +- user +product_section: core_platform +product_stage: manage +product_group: import_and_integrate +milestone: '16.6' +introduced_by_url: TODO +distributions: +- ce +- ee +tiers: +- free +- premium +- ultimate diff --git a/spec/fixtures/scripts/internal_events/events/secondary_event_with_identifiers.yml b/spec/fixtures/scripts/internal_events/events/secondary_event_with_identifiers.yml new file mode 100644 index 00000000000..4e2e77e0c5c --- /dev/null +++ b/spec/fixtures/scripts/internal_events/events/secondary_event_with_identifiers.yml @@ -0,0 +1,20 @@ +--- +description: Engineer closes Internal Event CLI +category: InternalEventTracking +action: internal_events_cli_closed +identifiers: +- project +- namespace +- user +product_section: analytics +product_stage: monitor +product_group: analytics_instrumentation +milestone: '16.6' +introduced_by_url: TODO +distributions: +- ce +- ee +tiers: +- free +- premium +- ultimate diff --git a/spec/fixtures/scripts/internal_events/metrics/ee_total_28d_single_event.yml b/spec/fixtures/scripts/internal_events/metrics/ee_total_28d_single_event.yml new file mode 100644 index 00000000000..ba56d782871 --- /dev/null +++ b/spec/fixtures/scripts/internal_events/metrics/ee_total_28d_single_event.yml @@ -0,0 +1,25 @@ +--- +key_path: counts.count_total_internal_events_cli_used_monthly +description: Monthly count of when an event was defined using the CLI +product_section: analytics +product_stage: monitor +product_group: analytics_instrumentation +performance_indicator_type: [] +value_type: number +status: active +milestone: '16.6' +introduced_by_url: TODO +time_frame: 28d +data_source: internal_events +data_category: optional +instrumentation_class: TotalCountMetric +distribution: +- ee +tier: +- premium +- ultimate +options: + events: + - internal_events_cli_used +events: +- name: internal_events_cli_used diff --git a/spec/fixtures/scripts/internal_events/metrics/ee_total_7d_single_event.yml b/spec/fixtures/scripts/internal_events/metrics/ee_total_7d_single_event.yml new file mode 100644 index 00000000000..e6bdcb9d2ae --- /dev/null +++ b/spec/fixtures/scripts/internal_events/metrics/ee_total_7d_single_event.yml @@ -0,0 +1,25 @@ +--- +key_path: counts.count_total_internal_events_cli_used_weekly +description: Weekly count of when an event was defined using the CLI +product_section: analytics +product_stage: monitor +product_group: analytics_instrumentation +performance_indicator_type: [] +value_type: number +status: active +milestone: '16.6' +introduced_by_url: TODO +time_frame: 7d +data_source: internal_events +data_category: optional +instrumentation_class: TotalCountMetric +distribution: +- ee +tier: +- premium +- ultimate +options: + events: + - internal_events_cli_used +events: +- name: internal_events_cli_used diff --git a/spec/fixtures/scripts/internal_events/metrics/ee_total_single_event.yml b/spec/fixtures/scripts/internal_events/metrics/ee_total_single_event.yml new file mode 100644 index 00000000000..b1bf89dc095 --- /dev/null +++ b/spec/fixtures/scripts/internal_events/metrics/ee_total_single_event.yml @@ -0,0 +1,25 @@ +--- +key_path: counts.count_total_internal_events_cli_used +description: Total count of when an event was defined using the CLI +product_section: analytics +product_stage: monitor +product_group: analytics_instrumentation +performance_indicator_type: [] +value_type: number +status: active +milestone: '16.6' +introduced_by_url: TODO +time_frame: all +data_source: internal_events +data_category: optional +instrumentation_class: TotalCountMetric +distribution: +- ee +tier: +- premium +- ultimate +options: + events: + - internal_events_cli_used +events: +- name: internal_events_cli_used diff --git a/spec/fixtures/scripts/internal_events/metrics/keyboard_smashed_metric_28d.yml b/spec/fixtures/scripts/internal_events/metrics/keyboard_smashed_metric_28d.yml new file mode 100644 index 00000000000..8476cb8561b --- /dev/null +++ b/spec/fixtures/scripts/internal_events/metrics/keyboard_smashed_metric_28d.yml @@ -0,0 +1,28 @@ +--- +key_path: redis_hll_counters.count_distinct_user_id_from_random_name_monthly +description: Monthly count of unique users random metric string +product_section: core_platform +product_stage: manage +product_group: import_and_integrate +performance_indicator_type: [] +value_type: number +status: active +milestone: '16.6' +introduced_by_url: TODO +time_frame: 28d +data_source: internal_events +data_category: optional +instrumentation_class: RedisHLLMetric +distribution: +- ce +- ee +tier: +- free +- premium +- ultimate +options: + events: + - random_name +events: +- name: random_name + unique: user.id diff --git a/spec/fixtures/scripts/internal_events/metrics/keyboard_smashed_metric_7d.yml b/spec/fixtures/scripts/internal_events/metrics/keyboard_smashed_metric_7d.yml new file mode 100644 index 00000000000..b4cc2fc8b55 --- /dev/null +++ b/spec/fixtures/scripts/internal_events/metrics/keyboard_smashed_metric_7d.yml @@ -0,0 +1,28 @@ +--- +key_path: redis_hll_counters.count_distinct_user_id_from_random_name_weekly +description: Weekly count of unique users random metric string +product_section: core_platform +product_stage: manage +product_group: import_and_integrate +performance_indicator_type: [] +value_type: number +status: active +milestone: '16.6' +introduced_by_url: TODO +time_frame: 7d +data_source: internal_events +data_category: optional +instrumentation_class: RedisHLLMetric +distribution: +- ce +- ee +tier: +- free +- premium +- ultimate +options: + events: + - random_name +events: +- name: random_name + unique: user.id diff --git a/spec/fixtures/scripts/internal_events/metrics/project_id_28d_multiple_events.yml b/spec/fixtures/scripts/internal_events/metrics/project_id_28d_multiple_events.yml new file mode 100644 index 00000000000..754702c8c74 --- /dev/null +++ b/spec/fixtures/scripts/internal_events/metrics/project_id_28d_multiple_events.yml @@ -0,0 +1,31 @@ +--- +key_path: redis_hll_counters.count_distinct_project_id_from_internal_events_cli_closed_and_internal_events_cli_used_monthly +description: Monthly count of unique projects where a defition file was created with the CLI +product_section: analytics +product_stage: monitor +product_group: analytics_instrumentation +performance_indicator_type: [] +value_type: number +status: active +milestone: '16.6' +introduced_by_url: TODO +time_frame: 28d +data_source: internal_events +data_category: optional +instrumentation_class: RedisHLLMetric +distribution: +- ce +- ee +tier: +- free +- premium +- ultimate +options: + events: + - internal_events_cli_closed + - internal_events_cli_used +events: +- name: internal_events_cli_closed + unique: project.id +- name: internal_events_cli_used + unique: project.id diff --git a/spec/fixtures/scripts/internal_events/metrics/project_id_7d_multiple_events.yml b/spec/fixtures/scripts/internal_events/metrics/project_id_7d_multiple_events.yml new file mode 100644 index 00000000000..95f429e9b40 --- /dev/null +++ b/spec/fixtures/scripts/internal_events/metrics/project_id_7d_multiple_events.yml @@ -0,0 +1,31 @@ +--- +key_path: redis_hll_counters.count_distinct_project_id_from_internal_events_cli_closed_and_internal_events_cli_used_weekly +description: Weekly count of unique projects where a defition file was created with the CLI +product_section: analytics +product_stage: monitor +product_group: analytics_instrumentation +performance_indicator_type: [] +value_type: number +status: active +milestone: '16.6' +introduced_by_url: TODO +time_frame: 7d +data_source: internal_events +data_category: optional +instrumentation_class: RedisHLLMetric +distribution: +- ce +- ee +tier: +- free +- premium +- ultimate +options: + events: + - internal_events_cli_closed + - internal_events_cli_used +events: +- name: internal_events_cli_closed + unique: project.id +- name: internal_events_cli_used + unique: project.id diff --git a/spec/fixtures/scripts/internal_events/metrics/total_single_event.yml b/spec/fixtures/scripts/internal_events/metrics/total_single_event.yml new file mode 100644 index 00000000000..5bdb4c45a52 --- /dev/null +++ b/spec/fixtures/scripts/internal_events/metrics/total_single_event.yml @@ -0,0 +1,27 @@ +--- +key_path: counts.count_total_internal_events_cli_used +description: Total count of when an event was defined using the CLI +product_section: analytics +product_stage: monitor +product_group: analytics_instrumentation +performance_indicator_type: [] +value_type: number +status: active +milestone: '16.6' +introduced_by_url: TODO +time_frame: all +data_source: internal_events +data_category: optional +instrumentation_class: TotalCountMetric +distribution: +- ce +- ee +tier: +- free +- premium +- ultimate +options: + events: + - internal_events_cli_used +events: +- name: internal_events_cli_used diff --git a/spec/fixtures/scripts/internal_events/metrics/user_id_28d_single_event.yml b/spec/fixtures/scripts/internal_events/metrics/user_id_28d_single_event.yml new file mode 100644 index 00000000000..b176b23b46a --- /dev/null +++ b/spec/fixtures/scripts/internal_events/metrics/user_id_28d_single_event.yml @@ -0,0 +1,28 @@ +--- +key_path: redis_hll_counters.count_distinct_user_id_from_internal_events_cli_used_monthly +description: Monthly count of unique users who defined an internal event using the CLI +product_section: analytics +product_stage: monitor +product_group: analytics_instrumentation +performance_indicator_type: [] +value_type: number +status: active +milestone: '16.6' +introduced_by_url: TODO +time_frame: 28d +data_source: internal_events +data_category: optional +instrumentation_class: RedisHLLMetric +distribution: +- ce +- ee +tier: +- free +- premium +- ultimate +options: + events: + - internal_events_cli_used +events: +- name: internal_events_cli_used + unique: user.id diff --git a/spec/fixtures/scripts/internal_events/metrics/user_id_7d_single_event.yml b/spec/fixtures/scripts/internal_events/metrics/user_id_7d_single_event.yml new file mode 100644 index 00000000000..8a0fca2cbdc --- /dev/null +++ b/spec/fixtures/scripts/internal_events/metrics/user_id_7d_single_event.yml @@ -0,0 +1,28 @@ +--- +key_path: redis_hll_counters.count_distinct_user_id_from_internal_events_cli_used_weekly +description: Weekly count of unique users who defined an internal event using the CLI +product_section: analytics +product_stage: monitor +product_group: analytics_instrumentation +performance_indicator_type: [] +value_type: number +status: active +milestone: '16.6' +introduced_by_url: TODO +time_frame: 7d +data_source: internal_events +data_category: optional +instrumentation_class: RedisHLLMetric +distribution: +- ce +- ee +tier: +- free +- premium +- ultimate +options: + events: + - internal_events_cli_used +events: +- name: internal_events_cli_used + unique: user.id diff --git a/spec/fixtures/scripts/internal_events/new_events.yml b/spec/fixtures/scripts/internal_events/new_events.yml new file mode 100644 index 00000000000..6f39fc5e93c --- /dev/null +++ b/spec/fixtures/scripts/internal_events/new_events.yml @@ -0,0 +1,183 @@ +- description: Creates a new event and flows directly into metric creation + inputs: + keystrokes: + - "1\n" # Enum-select: New Event -- start tracking when an action or scenario occurs on gitlab instances + - "Engineer uses Internal Event CLI to define a new event\n" # Submit description + - "internal_events_cli_used\n" # Submit action name + - "1\n" # Select: [namespace, project, user] + - "\n" # Skip MR URL + - "instrumentation" # Filters to the analytics instrumentation group + - "\n" # Accept analytics:monitor:analytics_instrumentation + - "1\n" # Select: [free, premium, ultimate] + - "y\n" # Create file + - "1\n" # Select: Create Metric --- define a new metric + - "\e[A" # Arrow up to: Total count of events + - "\n" # Select: Total count of events + - "when an event was defined using the CLI\n" # Input description + - "1\n" # Select: Copy & continue + - "y\n" # Create file + - "2\n" # Exit + outputs: + files: + - path: config/events/internal_events_cli_used.yml + content: spec/fixtures/scripts/internal_events/events/event_with_identifiers.yml + - path: config/metrics/counts_all/count_total_internal_events_cli_used.yml + content: spec/fixtures/scripts/internal_events/metrics/total_single_event.yml + +- description: Requires description & action before continuing + inputs: + keystrokes: + - "1\n" # Enum-select: New Event -- start tracking when an action or scenario occurs on gitlab instances + - "\n" # Attempt to skip writing description --> should get help message + - "Engineer uses Internal Event CLI to define a new event\n" # Submit description + - "\n" # Attempt to skip naming action --> should get help message + - "internal_events_cli_used\n" # Submit action name + - "1\n" # Select [namespace, project, user] + - "\n" # Skip MR URL + - "instrumentation" # Filters to the analytics instrumentation group + - "\n" # Accept analytics:monitor:analytics_instrumentation + - "1\n" # Select [free, premium, ultimate] + - "y\n" # Create file + - "3\n" # Exit + outputs: + files: + - path: config/events/internal_events_cli_used.yml + content: spec/fixtures/scripts/internal_events/events/event_with_identifiers.yml + +- description: Does not allow existing events for action + inputs: + files: + - path: config/events/internal_events_cli_used.yml + content: spec/fixtures/scripts/internal_events/events/event_with_identifiers.yml + keystrokes: + - "1\n" # Enum-select: New Event -- start tracking when an action or scenario occurs on gitlab instances + - "Engineer closes Internal Event CLI\n" # Submit description + - "internal_events_cli_used\n" # Submit already-existing action name + - "internal_events_cli_closed\n" # Submit alterred action name + - "1\n" # Select [namespace, project, user] + - "\n" # Skip MR URL + - "instrumentation" # Filters to the analytics instrumentation group + - "\n" # Accept analytics:monitor:analytics_instrumentation + - "1\n" # Select [free, premium, ultimate] + - "y\n" # Create file + - "3\n" # Exit + outputs: + files: + - path: config/events/internal_events_cli_closed.yml + content: spec/fixtures/scripts/internal_events/events/secondary_event_with_identifiers.yml + +- description: Creates a new event without identifiers + inputs: + keystrokes: + - "1\n" # Enum-select: New Event -- start tracking when an action or scenario occurs on gitlab instances + - "Internal Event CLI is opened\n" # Submit description + - "internal_events_cli_opened\n" # Submit action name + - "6\n" # Select: None + - "\n" # Skip MR URL + - "instrumentation" # Filters to the analytics instrumentation group + - "\n" # Accept analytics:monitor:analytics_instrumentation + - "2\n" # Select [premium, ultimate] + - "y\n" # Create file + - "3\n" # Exit + outputs: + files: + - path: ee/config/events/internal_events_cli_opened.yml + content: spec/fixtures/scripts/internal_events/events/ee_event_without_identifiers.yml + +- description: Smashing the keyboard/return creates an event & metrics with the most common attributes, then shows usage + inputs: + keystrokes: + - "\n" # Enum-select: New Event -- start tracking when an action or scenario occurs on gitlab instances + - "random event string\n" # Submit keyboard-smashing description + - "random_name\n" # Submit keyboard-smashing action name + - "\n" # Select: [namespace, project, user] + - "\n" # Skip MR URL + - "\n" # Select core_platform:manage:import_and_integrate + - "\n" # Select [free, premium, ultimate] + - "\n" # Create file + - "\n" # Select: Create Metric --- define a new metric + - "\n" # Select: Weekly/Monthly count of unique users + - "random metric string\n" # Submit keyboard-smashing description + - "\n" # Accept weekly description for monthly + - "\n" # Select: Copy & continue + - "\n" # Skip URL + - "\n" # Create file + - "\n" # Create file + - "\n" # Select: View Usage -- look at code examples + - "\n" # Select: Ruby/Rails + - "8\n" # Exit + outputs: + files: + - path: config/events/random_name.yml + content: spec/fixtures/scripts/internal_events/events/keyboard_smashed_event.yml + - path: config/metrics/counts_28d/count_distinct_user_id_from_random_name_monthly.yml + content: spec/fixtures/scripts/internal_events/metrics/keyboard_smashed_metric_28d.yml + - path: config/metrics/counts_7d/count_distinct_user_id_from_random_name_weekly.yml + content: spec/fixtures/scripts/internal_events/metrics/keyboard_smashed_metric_7d.yml + +- description: Creates an event after helping the user figure out next steps + inputs: + keystrokes: + - "4\n" # Enum-select: ...am I in the right place? + - "y\n" # Yes --> Are you trying to track customer usage of a GitLab feature? + - "y\n" # Yes --> Can usage for the feature be measured by tracking a specific user action? + - "n\n" # No --> Is the event already tracked? + - "y\n" # Yes --> Ready to start? + - "Internal Event CLI is opened\n" # Submit description + - "internal_events_cli_opened\n" # Submit action name + - "6\n" # Select: None + - "\n" # Skip MR URL + - "instrumentation" # Filters to the analytics instrumentation group + - "\n" # Accept analytics:monitor:analytics_instrumentation + - "2\n" # Select [premium, ultimate] + - "y\n" # Create file + - "3\n" # Exit + outputs: + files: + - path: ee/config/events/internal_events_cli_opened.yml + content: spec/fixtures/scripts/internal_events/events/ee_event_without_identifiers.yml + +- description: Creates a new event and flows directly into usage examples + inputs: + keystrokes: + - "1\n" # Enum-select: New Event -- start tracking when an action or scenario occurs on gitlab instances + - "Engineer uses Internal Event CLI to define a new event\n" # Submit description + - "internal_events_cli_used\n" # Submit action name + - "1\n" # Select: [namespace, project, user] + - "\n" # Skip MR URL + - "instrumentation" # Filters to the analytics instrumentation group + - "\n" # Accept analytics:monitor:analytics_instrumentation + - "1\n" # Select: [free, premium, ultimate] + - "y\n" # Create file + - "2\n" # Select: View Usage + - "8\n" # Exit + outputs: + files: + - path: config/events/internal_events_cli_used.yml + content: spec/fixtures/scripts/internal_events/events/event_with_identifiers.yml + +- description: Skips event creation, then saves event & flows directly into metric creation + inputs: + keystrokes: + - "1\n" # Enum-select: New Event -- start tracking when an action or scenario occurs on gitlab instances + - "Engineer uses Internal Event CLI to define a new event\n" # Submit description + - "internal_events_cli_used\n" # Submit action name + - "1\n" # Select: [namespace, project, user] + - "\n" # Skip MR URL + - "instrumentation" # Filters to the analytics instrumentation group + - "\n" # Accept analytics:monitor:analytics_instrumentation + - "1\n" # Select: [free, premium, ultimate] + - "n\n" # Create file + - "1\n" # Select: Save event & create Metric --- define a new metric + - "\e[A" # Arrow up to: Total count of events + - "\n" # Select: Total count of events + - "when an event was defined using the CLI\n" # Input description + - "1\n" # Select: Copy & continue + - "y\n" # Create file + - "2\n" # Exit + outputs: + files: + - path: config/events/internal_events_cli_used.yml + content: spec/fixtures/scripts/internal_events/events/event_with_identifiers.yml + - path: config/metrics/counts_all/count_total_internal_events_cli_used.yml + content: spec/fixtures/scripts/internal_events/metrics/total_single_event.yml diff --git a/spec/fixtures/scripts/internal_events/new_metrics.yml b/spec/fixtures/scripts/internal_events/new_metrics.yml new file mode 100644 index 00000000000..2a207ee84f4 --- /dev/null +++ b/spec/fixtures/scripts/internal_events/new_metrics.yml @@ -0,0 +1,196 @@ +- description: Create a weekly/monthly metric for a single event + inputs: + files: + - path: config/events/internal_events_cli_used.yml + content: spec/fixtures/scripts/internal_events/events/event_with_identifiers.yml + keystrokes: + - "2\n" # Enum-select: New Metric -- calculate how often one or more existing events occur over time + - "1\n" # Enum-select: Single event -- count occurrences of a specific event or user interaction + - 'internal_events_cli_used' # Filters to this event + - "\n" # Select: config/events/internal_events_cli_used.yml + - "\n" # Select: Weekly count of unique users + - "who defined an internal event using the CLI\n" # Input description + - "\n" # Submit weekly description for monthly + - "1\n" # Enum-select: Copy & continue + - "y\n" # Create file + - "y\n" # Create file + - "2\n" # Exit + outputs: + files: + - path: config/metrics/counts_28d/count_distinct_user_id_from_internal_events_cli_used_monthly.yml + content: spec/fixtures/scripts/internal_events/metrics/user_id_28d_single_event.yml + - path: config/metrics/counts_7d/count_distinct_user_id_from_internal_events_cli_used_weekly.yml + content: spec/fixtures/scripts/internal_events/metrics/user_id_7d_single_event.yml + +- description: Create a weekly/monthly metric for a multiple events, but select only one event + inputs: + files: + - path: config/events/internal_events_cli_used.yml + content: spec/fixtures/scripts/internal_events/events/event_with_identifiers.yml + keystrokes: + - "2\n" # Enum-select: New Metric -- calculate how often one or more existing events occur over time + - "2\n" # Enum-select: Multiple events -- count occurrences of several separate events or interactions + - 'internal_events_cli_used' # Filters to this event + - " " # Multi-select: config/events/internal_events_cli_used.yml + - "\n" # Submit selections + - "\n" # Select: Weekly count of unique projects + - "who defined an internal event using the CLI\n" # Input description + - "\n" # Submit weekly description for monthly + - "1\n" # Enum-select: Copy & continue + - "y\n" # Create file + - "y\n" # Create file + - "2\n" # Exit + outputs: + files: + - path: config/metrics/counts_28d/count_distinct_user_id_from_internal_events_cli_used_monthly.yml + content: spec/fixtures/scripts/internal_events/metrics/user_id_28d_single_event.yml + - path: config/metrics/counts_7d/count_distinct_user_id_from_internal_events_cli_used_weekly.yml + content: spec/fixtures/scripts/internal_events/metrics/user_id_7d_single_event.yml + +- description: Create a weekly/monthly metric for multiple events + inputs: + files: + - path: config/events/internal_events_cli_used.yml + content: spec/fixtures/scripts/internal_events/events/event_with_identifiers.yml + - path: config/events/internal_events_cli_closed.yml + content: spec/fixtures/scripts/internal_events/events/secondary_event_with_identifiers.yml + keystrokes: + - "2\n" # Enum-select: New Metric -- calculate how often one or more existing events occur over time + - "2\n" # Enum-select: Multiple events -- count occurrences of several separate events or interactions + - 'internal_events_cli' # Filters to the relevant events + - ' ' # Multi-select: internal_events_cli_closed + - "\e[B" # Arrow down to: internal_events_cli_used + - ' ' # Multi-select: internal_events_cli_used + - "\n" # Submit selections + - "\e[B" # Arrow down to: Weekly count of unique projects + - "\n" # Select: Weekly count of unique projects + - "where a defition file was created with the CLI\n" # Input description + - "\n" # Submit weekly description for monthly + - "1\n" # Select: Copy & continue + - "y\n" # Create file + - "y\n" # Create file + - "2\n" # Exit + outputs: + files: + - path: config/metrics/counts_28d/count_distinct_project_id_from_internal_events_cli_closed_and_internal_events_cli_used_monthly.yml + content: spec/fixtures/scripts/internal_events/metrics/project_id_28d_multiple_events.yml + - path: config/metrics/counts_7d/count_distinct_project_id_from_internal_events_cli_closed_and_internal_events_cli_used_weekly.yml + content: spec/fixtures/scripts/internal_events/metrics/project_id_7d_multiple_events.yml + +- description: Create an all time total metric for a single event + inputs: + files: + - path: config/events/internal_events_cli_used.yml + content: spec/fixtures/scripts/internal_events/events/event_with_identifiers.yml + keystrokes: + - "2\n" # Enum-select: New Metric -- calculate how often one or more existing events occur over time + - "1\n" # Enum-select: Single event -- count occurrences of a specific event or user interaction + - 'internal_events_cli_used' # Filters to this event + - "\n" # Select: config/events/internal_events_cli_used.yml + - "\e[A" # Arrow up to: Total count of events + - "\n" # Select: Total count of events + - "when an event was defined using the CLI\n" # Input description + - "1\n" # Select: Copy & continue + - "y\n" # Create file + - "2\n" # Exit + outputs: + files: + - path: config/metrics/counts_all/count_total_internal_events_cli_used.yml + content: spec/fixtures/scripts/internal_events/metrics/total_single_event.yml + +- description: Try to create a database metric + inputs: + keystrokes: + - "2\n" # Enum-select: New Metric -- calculate how often one or more existing events occur over time + - "3\n" # Enum-select: Database -- record value of a particular field or count of database rows + +- description: Create an all time total metric for a single event, and confirm each attribute copied from event + inputs: + files: + - path: config/events/internal_events_cli_used.yml + content: spec/fixtures/scripts/internal_events/events/event_with_identifiers.yml + keystrokes: + - "2\n" # Enum-select: New Metric -- calculate how often one or more existing events occur over time + - "1\n" # Enum-select: Single event -- count occurrences of a specific event or user interaction + - 'internal_events_cli_used' # Filters to this event + - "\n" # Select: config/events/internal_events_cli_used.yml + - "\e[A" # Arrow up to: Total count of events + - "\n" # Select: Total count of events + - "when an event was defined using the CLI\n" # Input description + - "2\n" # Enum-select: Modify attributes + - "\n" # Accept group/section/stage from event definition + - "\n" # Accept URL from event definition + - "2\n" # Override tier -> Select: [premium, ultimate] + - "y\n" # Create file + - "2\n" # Exit + outputs: + files: + - path: ee/config/metrics/counts_all/count_total_internal_events_cli_used.yml + content: spec/fixtures/scripts/internal_events/metrics/ee_total_single_event.yml + +- description: Create a metric after helping the user figure out next steps + inputs: + files: + - path: config/events/internal_events_cli_used.yml + content: spec/fixtures/scripts/internal_events/events/event_with_identifiers.yml + keystrokes: + - "4\n" # Enum-select: ...am I in the right place? + - "y\n" # Yes --> Are you trying to track customer usage of a GitLab feature? + - "y\n" # Yes --> Can usage for the feature be measured by tracking a specific user action? + - "y\n" # Yes --> Is the event already tracked? + - "y\n" # Yes --> Ready to start? + - "1\n" # Enum-select: Single event -- count occurrences of a specific event or user interaction + - 'internal_events_cli_used' # Filters to this event + - "\n" # Select: config/events/internal_events_cli_used.yml + - "\e[A" # Arrow up to: Total count of events + - "\n" # Select: Total count of events + - "when an event was defined using the CLI\n" # Input description + - "1\n" # Select: Copy & continue + - "y\n" # Create file + - "2\n" # Exit + outputs: + files: + - path: config/metrics/counts_all/count_total_internal_events_cli_used.yml + content: spec/fixtures/scripts/internal_events/metrics/total_single_event.yml + +- description: User overwrites metric that already exists + inputs: + files: + - path: config/events/internal_events_cli_used.yml + content: spec/fixtures/scripts/internal_events/events/event_with_identifiers.yml + - path: config/metrics/counts_all/count_total_internal_events_cli_used.yml + content: spec/fixtures/scripts/internal_events/metrics/ee_total_7d_single_event.yml # wrong content + keystrokes: + - "2\n" # Enum-select: New Metric -- calculate how often one or more existing events occur over time + - "1\n" # Enum-select: Single event -- count occurrences of a specific event or user interaction + - 'internal_events_cli_used' # Filters to this event + - "\n" # Select: config/events/internal_events_cli_used.yml + - "\e[A" # Arrow up to: Total count of events + - "\n" # Select: Total count of events + - "when an event was defined using the CLI\n" # Input description + - "1\n" # Select: Copy & continue + - "y\n" # Overwrite file + - "2\n" # Exit + outputs: + files: + - path: config/metrics/counts_all/count_total_internal_events_cli_used.yml + content: spec/fixtures/scripts/internal_events/metrics/total_single_event.yml + +- description: User opts not to overwrite metric that already exists + inputs: + files: + - path: config/events/internal_events_cli_used.yml + content: spec/fixtures/scripts/internal_events/events/event_with_identifiers.yml + - path: config/metrics/counts_all/count_total_internal_events_cli_used.yml + content: spec/fixtures/scripts/internal_events/metrics/ee_total_7d_single_event.yml # wrong content + keystrokes: + - "2\n" # Enum-select: New Metric -- calculate how often one or more existing events occur over time + - "1\n" # Enum-select: Single event -- count occurrences of a specific event or user interaction + - 'internal_events_cli_used' # Filters to this event + - "\n" # Select: config/events/internal_events_cli_used.yml + - "\e[A" # Arrow up to: Total count of events + - "\n" # Select: Total count of events + - "when an event was defined using the CLI\n" # Input description + - "1\n" # Select: Copy & continue + - "n\n" # Don't overwrite file + - "2\n" # Exit diff --git a/spec/fixtures/scripts/internal_events/stages.yml b/spec/fixtures/scripts/internal_events/stages.yml new file mode 100644 index 00000000000..d5db9dcbe6d --- /dev/null +++ b/spec/fixtures/scripts/internal_events/stages.yml @@ -0,0 +1,78 @@ +stages: + manage: + display_name: "Manage" + section: core_platform + groups: + import_and_integrate: + name: Import and Integrate + foundations: + name: Foundations + + plan: + display_name: "Plan" + section: dev + groups: + project_management: + name: Project Management + product_planning: + name: Product Planning + knowledge: + name: Knowledge + optimize: + name: Optimize + + create: + display_name: "Create" + section: dev + slack: + channel: s_create + groups: + source_code: + name: Source Code + code_review: + name: Code Review + ide: + name: IDE + editor_extensions: + name: Editor Extensions + code_creation: + name: Code Creation + + verify: + display_name: "Verify" + section: ci + slack: + channel: s_verify + groups: + pipeline_execution: + name: "Pipeline Execution" + pipeline_authoring: + name: "Pipeline Authoring" + runner: + name: "Runner" + runner_saas: + name: "Runner SaaS" + pipeline_security: + name: "Pipeline Security" + + package: + display_name: "Package" + section: ci + slack: + channel: s_package + groups: + package_registry: + name: Package Registry + container_registry: + name: Container Registry + + monitor: + display_name: Monitor + section: analytics + groups: + analytics_instrumentation: + name: Analytics Instrumentation + product_analytics: + name: Product Analytics + observability: + name: "Observability" diff --git a/spec/frontend/admin/abuse_report/components/notes/abuse_report_note_spec.js b/spec/frontend/admin/abuse_report/components/notes/abuse_report_note_spec.js index f0174cd7b4a..bc7aa8ef5de 100644 --- a/spec/frontend/admin/abuse_report/components/notes/abuse_report_note_spec.js +++ b/spec/frontend/admin/abuse_report/components/notes/abuse_report_note_spec.js @@ -93,6 +93,26 @@ describe('Abuse Report Note', () => { }); describe('Editing', () => { + it('should show edit button when resolveNote is true', () => { + createComponent({ + note: { ...mockNote, userPermissions: { resolveNote: true } }, + }); + + expect(findNoteActions().props()).toMatchObject({ + showEditButton: true, + }); + }); + + it('should not show edit button when resolveNote is false', () => { + createComponent({ + note: { ...mockNote, userPermissions: { resolveNote: false } }, + }); + + expect(findNoteActions().props()).toMatchObject({ + showEditButton: false, + }); + }); + it('should not be in edit mode by default', () => { expect(findEditNote().exists()).toBe(false); }); @@ -164,16 +184,14 @@ describe('Abuse Report Note', () => { }); }); - describe('Actions', () => { - it('should show note actions', () => { - expect(findNoteActions().exists()).toBe(true); + describe('Replying', () => { + it('should show reply button', () => { expect(findNoteActions().props()).toMatchObject({ - showReplyButton: mockShowReplyButton, - showEditButton: true, + showReplyButton: true, }); }); - it('should emit `startReplying`', () => { + it('should bubble up `startReplying` event', () => { findNoteActions().vm.$emit('startReplying'); expect(wrapper.emitted('startReplying')).toHaveLength(1); diff --git a/spec/frontend/admin/abuse_report/mock_data.js b/spec/frontend/admin/abuse_report/mock_data.js index a958c4d30b1..9790b44c976 100644 --- a/spec/frontend/admin/abuse_report/mock_data.js +++ b/spec/frontend/admin/abuse_report/mock_data.js @@ -153,7 +153,7 @@ export const mockDiscussionWithNoReplies = [ }, lastEditedBy: null, userPermissions: { - adminNote: true, + resolveNote: true, __typename: 'NotePermissions', }, discussion: { @@ -192,7 +192,7 @@ export const mockDiscussionWithReplies = [ }, lastEditedBy: null, userPermissions: { - adminNote: true, + resolveNote: true, __typename: 'NotePermissions', }, discussion: { @@ -237,7 +237,7 @@ export const mockDiscussionWithReplies = [ }, lastEditedBy: null, userPermissions: { - adminNote: true, + resolveNote: true, __typename: 'NotePermissions', }, discussion: { @@ -282,7 +282,7 @@ export const mockDiscussionWithReplies = [ }, lastEditedBy: null, userPermissions: { - adminNote: true, + resolveNote: true, __typename: 'NotePermissions', }, discussion: { @@ -368,7 +368,7 @@ export const createAbuseReportNoteResponse = { }, lastEditedBy: null, userPermissions: { - adminNote: true, + resolveNote: true, }, discussion: { id: 'gid://gitlab/Discussion/90ca230051611e6e1676c50ba7178e0baeabd98d', @@ -413,7 +413,7 @@ export const editAbuseReportNoteResponse = { }, lastEditedBy: 'root', userPermissions: { - adminNote: true, + resolveNote: true, __typename: 'NotePermissions', }, }, diff --git a/spec/frontend/deploy_keys/graphql/resolvers_spec.js b/spec/frontend/deploy_keys/graphql/resolvers_spec.js new file mode 100644 index 00000000000..458232697cb --- /dev/null +++ b/spec/frontend/deploy_keys/graphql/resolvers_spec.js @@ -0,0 +1,249 @@ +import MockAdapter from 'axios-mock-adapter'; +import { HTTP_STATUS_OK, HTTP_STATUS_NOT_FOUND } from '~/lib/utils/http_status'; +import axios from '~/lib/utils/axios_utils'; +import pageInfoQuery from '~/graphql_shared/client/page_info.query.graphql'; +import currentPageQuery from '~/deploy_keys/graphql/queries/current_page.query.graphql'; +import currentScopeQuery from '~/deploy_keys/graphql/queries/current_scope.query.graphql'; +import confirmRemoveKeyQuery from '~/deploy_keys/graphql/queries/confirm_remove_key.query.graphql'; +import { resolvers } from '~/deploy_keys/graphql/resolvers'; + +const ENDPOINTS = { + enabledKeysEndpoint: '/enabled_keys', + availableProjectKeysEndpoint: '/available_project_keys', + availablePublicKeysEndpoint: '/available_public_keys', +}; + +describe('~/deploy_keys/graphql/resolvers', () => { + let mockResolvers; + let mock; + let client; + + beforeEach(() => { + mockResolvers = resolvers(ENDPOINTS); + mock = new MockAdapter(axios); + client = { + writeQuery: jest.fn(), + readQuery: jest.fn(), + readFragment: jest.fn(), + cache: { evict: jest.fn(), gc: jest.fn() }, + }; + }); + + afterEach(() => { + mock.reset(); + }); + + describe('deployKeys', () => { + const key = { id: 1, title: 'hello', edit_path: '/edit' }; + + it.each(['enabledKeys', 'availableProjectKeys', 'availablePublicKeys'])( + 'should request the endpoint for the %s scope', + async (scope) => { + mock.onGet(ENDPOINTS[`${scope}Endpoint`]).reply(HTTP_STATUS_OK, { keys: [key] }); + + const keys = await mockResolvers.Project.deployKeys(null, { scope, page: 1 }, { client }); + + expect(keys).toEqual([ + { id: 1, title: 'hello', editPath: '/edit', __typename: 'LocalDeployKey' }, + ]); + }, + ); + + it('should default to enabled keys if a bad scope is given', async () => { + const scope = 'bad'; + mock.onGet(ENDPOINTS.enabledKeysEndpoint).reply(HTTP_STATUS_OK, { keys: [key] }); + + const keys = await mockResolvers.Project.deployKeys(null, { scope, page: 1 }, { client }); + + expect(keys).toEqual([ + { id: 1, title: 'hello', editPath: '/edit', __typename: 'LocalDeployKey' }, + ]); + }); + + it('should request the given page', async () => { + const scope = 'enabledKeys'; + const page = 2; + mock + .onGet(ENDPOINTS.enabledKeysEndpoint, { params: { page } }) + .reply(HTTP_STATUS_OK, { keys: [key] }); + + const keys = await mockResolvers.Project.deployKeys(null, { scope, page }, { client }); + + expect(keys).toEqual([ + { id: 1, title: 'hello', editPath: '/edit', __typename: 'LocalDeployKey' }, + ]); + }); + + it('should write pagination info to the cache', async () => { + const scope = 'enabledKeys'; + const page = 1; + + mock.onGet(ENDPOINTS.enabledKeysEndpoint).reply( + HTTP_STATUS_OK, + { keys: [key] }, + { + 'x-next-page': '2', + 'x-page': '1', + 'X-Per-Page': '2', + 'X-Prev-Page': '', + 'X-TOTAL': '37', + 'X-Total-Pages': '5', + }, + ); + + await mockResolvers.Project.deployKeys(null, { scope, page }, { client }); + + expect(client.writeQuery).toHaveBeenCalledWith({ + query: pageInfoQuery, + variables: { input: { scope, page } }, + data: { + pageInfo: { + total: 37, + perPage: 2, + previousPage: NaN, + totalPages: 5, + nextPage: 2, + page: 1, + __typename: 'LocalPageInfo', + }, + }, + }); + }); + + it('should not write page info if the request fails', async () => { + const scope = 'enabledKeys'; + const page = 1; + + mock.onGet(ENDPOINTS.enabledKeysEndpoint).reply(HTTP_STATUS_NOT_FOUND); + + try { + await mockResolvers.Project.deployKeys(null, { scope, page }, { client }); + } catch { + expect(client.writeQuery).not.toHaveBeenCalled(); + } + }); + }); + + describe('currentPage', () => { + it('sets the current page', () => { + const page = 5; + mockResolvers.Mutation.currentPage(null, { page }, { client }); + + expect(client.writeQuery).toHaveBeenCalledWith({ + query: currentPageQuery, + data: { currentPage: page }, + }); + }); + }); + + describe('currentScope', () => { + let scope; + + beforeEach(() => { + scope = 'enabledKeys'; + mockResolvers.Mutation.currentScope(null, { scope }, { client }); + }); + + it('sets the current scope', () => { + expect(client.writeQuery).toHaveBeenCalledWith({ + query: currentScopeQuery, + data: { currentScope: scope }, + }); + }); + + it('resets the page to 1', () => { + expect(client.writeQuery).toHaveBeenCalledWith({ + query: currentPageQuery, + data: { currentPage: 1 }, + }); + }); + }); + + describe('disableKey', () => { + it('disables the key that is pending confirmation', async () => { + const key = { id: 1, title: 'hello', disablePath: '/disable', __typename: 'LocalDeployKey' }; + client.readQuery.mockReturnValue({ deployKeyToRemove: key }); + client.readFragment.mockReturnValue(key); + mock.onPut(key.disablePath).reply(HTTP_STATUS_OK); + await mockResolvers.Mutation.disableKey(null, null, { client }); + + expect(client.readQuery).toHaveBeenCalledWith({ query: confirmRemoveKeyQuery }); + expect(client.readFragment).toHaveBeenCalledWith( + expect.objectContaining({ id: `LocalDeployKey:${key.id}` }), + ); + expect(client.cache.evict).toHaveBeenCalledWith({ fieldName: 'deployKeyToRemove' }); + expect(client.cache.evict).toHaveBeenCalledWith({ id: `LocalDeployKey:${key.id}` }); + expect(client.cache.gc).toHaveBeenCalled(); + }); + + it('does not remove the key from the cache on fail', async () => { + const key = { id: 1, title: 'hello', disablePath: '/disable', __typename: 'LocalDeployKey' }; + client.readQuery.mockReturnValue({ deployKeyToRemove: key }); + client.readFragment.mockReturnValue(key); + mock.onPut(key.disablePath).reply(HTTP_STATUS_NOT_FOUND); + + try { + await mockResolvers.Mutation.disableKey(null, null, { client }); + } catch { + expect(client.readQuery).toHaveBeenCalledWith({ query: confirmRemoveKeyQuery }); + expect(client.readFragment).toHaveBeenCalledWith( + expect.objectContaining({ id: `LocalDeployKey:${key.id}` }), + ); + expect(client.cache.evict).not.toHaveBeenCalled(); + expect(client.cache.gc).not.toHaveBeenCalled(); + } + }); + }); + + describe('enableKey', () => { + it("calls the key's enable path", async () => { + const key = { id: 1, title: 'hello', enablePath: '/enable', __typename: 'LocalDeployKey' }; + client.readQuery.mockReturnValue({ deployKeyToRemove: key }); + client.readFragment.mockReturnValue(key); + mock.onPut(key.enablePath).reply(HTTP_STATUS_OK); + await mockResolvers.Mutation.enableKey(null, key, { client }); + + expect(client.readFragment).toHaveBeenCalledWith( + expect.objectContaining({ id: `LocalDeployKey:${key.id}` }), + ); + expect(client.cache.evict).toHaveBeenCalledWith({ id: `LocalDeployKey:${key.id}` }); + expect(client.cache.gc).toHaveBeenCalled(); + }); + + it('does not remove the key from the cache on failure', async () => { + const key = { id: 1, title: 'hello', enablePath: '/enable', __typename: 'LocalDeployKey' }; + client.readQuery.mockReturnValue({ deployKeyToRemove: key }); + client.readFragment.mockReturnValue(key); + mock.onPut(key.enablePath).reply(HTTP_STATUS_NOT_FOUND); + try { + await mockResolvers.Mutation.enableKey(null, key, { client }); + } catch { + expect(client.readFragment).toHaveBeenCalledWith( + expect.objectContaining({ id: `LocalDeployKey:${key.id}` }), + ); + expect(client.cache.evict).not.toHaveBeenCalled(); + expect(client.cache.gc).not.toHaveBeenCalled(); + } + }); + }); + + describe('confirmDisable', () => { + it('sets the key to disable', () => { + const key = { id: 1, title: 'hello', enablePath: '/enable', __typename: 'LocalDeployKey' }; + mockResolvers.Mutation.confirmDisable(null, key, { client }); + + expect(client.writeQuery).toHaveBeenCalledWith({ + query: confirmRemoveKeyQuery, + data: { deployKeyToRemove: { id: key.id, __type: 'LocalDeployKey' } }, + }); + }); + it('clears the value when null id is passed', () => { + mockResolvers.Mutation.confirmDisable(null, { id: null }, { client }); + + expect(client.writeQuery).toHaveBeenCalledWith({ + query: confirmRemoveKeyQuery, + data: { deployKeyToRemove: null }, + }); + }); + }); +}); diff --git a/spec/frontend/environments/graphql/resolvers/base_spec.js b/spec/frontend/environments/graphql/resolvers/base_spec.js index f78146fe48e..939ccc0780c 100644 --- a/spec/frontend/environments/graphql/resolvers/base_spec.js +++ b/spec/frontend/environments/graphql/resolvers/base_spec.js @@ -9,7 +9,7 @@ import environmentToStopQuery from '~/environments/graphql/queries/environment_t import createMockApollo from 'helpers/mock_apollo_helper'; import pollIntervalQuery from '~/environments/graphql/queries/poll_interval.query.graphql'; import isEnvironmentStoppingQuery from '~/environments/graphql/queries/is_environment_stopping.query.graphql'; -import pageInfoQuery from '~/environments/graphql/queries/page_info.query.graphql'; +import pageInfoQuery from '~/graphql_shared/client/page_info.query.graphql'; import { TEST_HOST } from 'helpers/test_constants'; import { environmentsApp, diff --git a/spec/frontend/security_configuration/components/app_spec.js b/spec/frontend/security_configuration/components/app_spec.js index 364fe733a41..94d888bb067 100644 --- a/spec/frontend/security_configuration/components/app_spec.js +++ b/spec/frontend/security_configuration/components/app_spec.js @@ -5,10 +5,10 @@ import { useLocalStorageSpy } from 'helpers/local_storage_helper'; import { makeMockUserCalloutDismisser } from 'helpers/mock_user_callout_dismisser'; import stubChildren from 'helpers/stub_children'; import { mountExtended } from 'helpers/vue_test_utils_helper'; -import SecurityConfigurationApp, { i18n } from '~/security_configuration/components/app.vue'; +import SecurityConfigurationApp from '~/security_configuration/components/app.vue'; import AutoDevopsAlert from '~/security_configuration/components/auto_dev_ops_alert.vue'; import AutoDevopsEnabledAlert from '~/security_configuration/components/auto_dev_ops_enabled_alert.vue'; -import { AUTO_DEVOPS_ENABLED_ALERT_DISMISSED_STORAGE_KEY } from '~/security_configuration/components/constants'; +import { AUTO_DEVOPS_ENABLED_ALERT_DISMISSED_STORAGE_KEY } from '~/security_configuration/constants'; import FeatureCard from '~/security_configuration/components/feature_card.vue'; import TrainingProviderList from '~/security_configuration/components/training_provider_list.vue'; import { securityFeaturesMock, provideMock } from '../mock_data'; @@ -19,6 +19,8 @@ const { vulnerabilityTrainingDocsPath, projectFullPath } = provideMock; useLocalStorageSpy(); Vue.use(VueApollo); +const { i18n } = SecurityConfigurationApp; + describe('~/security_configuration/components/app', () => { let wrapper; let userCalloutDismissSpy; diff --git a/spec/frontend/security_configuration/components/feature_card_spec.js b/spec/frontend/security_configuration/components/feature_card_spec.js index 983a66a7fd3..9efee2a409a 100644 --- a/spec/frontend/security_configuration/components/feature_card_spec.js +++ b/spec/frontend/security_configuration/components/feature_card_spec.js @@ -1,7 +1,7 @@ import { GlIcon } from '@gitlab/ui'; import { mount } from '@vue/test-utils'; import { extendedWrapper } from 'helpers/vue_test_utils_helper'; -import { securityFeatures } from '~/security_configuration/components/constants'; +import { securityFeatures } from '~/security_configuration/constants'; import FeatureCard from '~/security_configuration/components/feature_card.vue'; import FeatureCardBadge from '~/security_configuration/components/feature_card_badge.vue'; import ManageViaMr from '~/vue_shared/security_configuration/components/manage_via_mr.vue'; diff --git a/spec/frontend/security_configuration/components/training_provider_list_spec.js b/spec/frontend/security_configuration/components/training_provider_list_spec.js index 5b2b3f46df6..ef20d8f56a4 100644 --- a/spec/frontend/security_configuration/components/training_provider_list_spec.js +++ b/spec/frontend/security_configuration/components/training_provider_list_spec.js @@ -19,8 +19,8 @@ import { TRACK_TOGGLE_TRAINING_PROVIDER_LABEL, TRACK_PROVIDER_LEARN_MORE_CLICK_ACTION, TRACK_PROVIDER_LEARN_MORE_CLICK_LABEL, + TEMP_PROVIDER_URLS, } from '~/security_configuration/constants'; -import { TEMP_PROVIDER_URLS } from '~/security_configuration/components/constants'; import TrainingProviderList from '~/security_configuration/components/training_provider_list.vue'; import { updateSecurityTrainingOptimisticResponse } from '~/security_configuration/graphql/cache_utils'; import securityTrainingProvidersQuery from '~/security_configuration/graphql/security_training_providers.query.graphql'; @@ -61,10 +61,9 @@ const TEMP_PROVIDER_LOGOS = { svg: '<svg>Secure Code Warrior</svg>', }, }; -jest.mock('~/security_configuration/components/constants', () => { +jest.mock('~/security_configuration/constants', () => { return { - TEMP_PROVIDER_URLS: jest.requireActual('~/security_configuration/components/constants') - .TEMP_PROVIDER_URLS, + TEMP_PROVIDER_URLS: jest.requireActual('~/security_configuration/constants').TEMP_PROVIDER_URLS, // NOTE: Jest hoists all mocks to the top so we can't use TEMP_PROVIDER_LOGOS // here directly. TEMP_PROVIDER_LOGOS: { diff --git a/spec/frontend/security_configuration/mock_data.js b/spec/frontend/security_configuration/mock_data.js index df10d33e2f0..208256afdbd 100644 --- a/spec/frontend/security_configuration/mock_data.js +++ b/spec/frontend/security_configuration/mock_data.js @@ -4,7 +4,7 @@ import { SAST_DESCRIPTION, SAST_HELP_PATH, SAST_CONFIG_HELP_PATH, -} from '~/security_configuration/components/constants'; +} from '~/security_configuration/constants'; import { REPORT_TYPE_SAST } from '~/vue_shared/security_reports/constants'; export const testProjectPath = 'foo/bar'; diff --git a/spec/frontend/security_configuration/utils_spec.js b/spec/frontend/security_configuration/utils_spec.js index ea04e9e7993..3c6d4baa30f 100644 --- a/spec/frontend/security_configuration/utils_spec.js +++ b/spec/frontend/security_configuration/utils_spec.js @@ -1,5 +1,5 @@ import { augmentFeatures, translateScannerNames } from '~/security_configuration/utils'; -import { SCANNER_NAMES_MAP } from '~/security_configuration/components/constants'; +import { SCANNER_NAMES_MAP } from '~/security_configuration/constants'; describe('augmentFeatures', () => { const mockSecurityFeatures = [ diff --git a/spec/frontend/vue_shared/security_reports/components/manage_via_mr_spec.js b/spec/frontend/vue_shared/security_reports/components/manage_via_mr_spec.js index f3d0d66cdd1..2b36344cfa8 100644 --- a/spec/frontend/vue_shared/security_reports/components/manage_via_mr_spec.js +++ b/spec/frontend/vue_shared/security_reports/components/manage_via_mr_spec.js @@ -2,7 +2,7 @@ import { GlButton } from '@gitlab/ui'; import { mount } from '@vue/test-utils'; import Vue from 'vue'; import VueApollo from 'vue-apollo'; -import { featureToMutationMap } from 'ee_else_ce/security_configuration/components/constants'; +import { featureToMutationMap } from 'ee_else_ce/security_configuration/constants'; import createMockApollo from 'helpers/mock_apollo_helper'; import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import waitForPromises from 'helpers/wait_for_promises'; diff --git a/spec/graphql/types/permission_types/abuse_report_spec.rb b/spec/graphql/types/permission_types/abuse_report_spec.rb deleted file mode 100644 index 399df137a78..00000000000 --- a/spec/graphql/types/permission_types/abuse_report_spec.rb +++ /dev/null @@ -1,15 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Types::PermissionTypes::AbuseReport, feature_category: :insider_threat do - it do - expected_permissions = [ - :read_abuse_report, :create_note - ] - - expected_permissions.each do |permission| - expect(described_class).to have_graphql_field(permission) - end - end -end diff --git a/spec/helpers/groups_helper_spec.rb b/spec/helpers/groups_helper_spec.rb index fe8ed2691fb..8aee337f51c 100644 --- a/spec/helpers/groups_helper_spec.rb +++ b/spec/helpers/groups_helper_spec.rb @@ -656,6 +656,41 @@ RSpec.describe GroupsHelper, feature_category: :groups_and_projects do end end + context 'when a user has different access for different groups in the hierarchy' do + let_it_be(:grand_parent) { create(:group) } + let_it_be(:parent) { create(:group, parent: grand_parent) } + let_it_be(:child) { create(:group, parent: parent) } + let_it_be(:grand_child) { create(:group, parent: child) } + + before_all do + parent.add_developer(user) + child.add_maintainer(user) + grand_child.add_owner(user) + end + + it 'returns the access levels that are peers or lower' do + expect(helper.access_level_roles_user_can_assign(grand_parent)).to be_empty + expect(helper.access_level_roles_user_can_assign(parent)).to eq({ + 'Guest' => ::Gitlab::Access::GUEST, + 'Reporter' => ::Gitlab::Access::REPORTER, + 'Developer' => ::Gitlab::Access::DEVELOPER + }) + expect(helper.access_level_roles_user_can_assign(child)).to eq(::Gitlab::Access.options) + expect(helper.access_level_roles_user_can_assign(grand_child)).to eq(::Gitlab::Access.options_with_owner) + end + end + + context 'when a group is linked to another' do + let_it_be(:other_group) { create(:group) } + let_it_be(:group_link) { create(:group_group_link, shared_group: group, shared_with_group: other_group, group_access: Gitlab::Access::MAINTAINER) } + + before_all do + other_group.add_owner(user) + end + + it { is_expected.to eq(::Gitlab::Access.options) } + end + context 'when user is not provided' do before do allow(helper).to receive(:current_user).and_return(nil) diff --git a/spec/policies/abuse_report_policy_spec.rb b/spec/policies/abuse_report_policy_spec.rb index 01ab29d1cf1..3cc92749ab4 100644 --- a/spec/policies/abuse_report_policy_spec.rb +++ b/spec/policies/abuse_report_policy_spec.rb @@ -12,6 +12,7 @@ RSpec.describe AbuseReportPolicy, feature_category: :insider_threat do it 'cannot read_abuse_report' do expect(policy).to be_disallowed(:read_abuse_report) + expect(policy).to be_disallowed(:read_note) expect(policy).to be_disallowed(:create_note) end end @@ -21,6 +22,7 @@ RSpec.describe AbuseReportPolicy, feature_category: :insider_threat do it 'can read_abuse_report' do expect(policy).to be_allowed(:read_abuse_report) + expect(policy).to be_allowed(:read_note) expect(policy).to be_allowed(:create_note) end end diff --git a/spec/requests/api/graphql/abuse_report_spec.rb b/spec/requests/api/graphql/abuse_report_spec.rb index f74b1fb4061..8ab0e92d838 100644 --- a/spec/requests/api/graphql/abuse_report_spec.rb +++ b/spec/requests/api/graphql/abuse_report_spec.rb @@ -25,11 +25,7 @@ RSpec.describe 'Querying an Abuse Report', feature_category: :insider_threat do it 'returns all fields' do expect(abuse_report_data).to include( - 'id' => global_id, - 'userPermissions' => { - 'readAbuseReport' => true, - 'createNote' => true - } + 'id' => global_id ) end end diff --git a/spec/scripts/internal_events/cli_spec.rb b/spec/scripts/internal_events/cli_spec.rb new file mode 100644 index 00000000000..d84a4498fe8 --- /dev/null +++ b/spec/scripts/internal_events/cli_spec.rb @@ -0,0 +1,866 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' +require 'tty/prompt/test' +require_relative '../../../scripts/internal_events/cli' + +RSpec.describe Cli, feature_category: :service_ping do + let(:prompt) { TTY::Prompt::Test.new } + let(:files_to_cleanup) { [] } + + let(:event1_filepath) { 'config/events/internal_events_cli_used.yml' } + let(:event1_content) { internal_event_fixture('events/event_with_identifiers.yml') } + let(:event2_filepath) { 'ee/config/events/internal_events_cli_opened.yml' } + let(:event2_content) { internal_event_fixture('events/ee_event_without_identifiers.yml') } + let(:event3_filepath) { 'config/events/internal_events_cli_closed.yml' } + let(:event3_content) { internal_event_fixture('events/secondary_event_with_identifiers.yml') } + + before do + stub_milestone('16.6') + collect_file_writes(files_to_cleanup) + stub_product_groups(File.read('spec/fixtures/scripts/internal_events/stages.yml')) + stub_helper(:fetch_window_size, '50') + end + + after do + delete_files(files_to_cleanup) + end + + shared_examples 'creates the right defintion files' do |description, test_case = {}| + # For expected keystroke mapping, see https://github.com/piotrmurach/tty-reader/blob/master/lib/tty/reader/keys.rb + let(:keystrokes) { test_case.dig('inputs', 'keystrokes') || [] } + let(:input_files) { test_case.dig('inputs', 'files') || [] } + let(:output_files) { test_case.dig('outputs', 'files') || [] } + + subject { run_with_verbose_timeout } + + it "in scenario: #{description}" do + delete_old_ouputs # just in case + prep_input_files + queue_cli_inputs(keystrokes) + expect_file_creation + + subject + end + + private + + def delete_old_ouputs + [input_files, output_files].flatten.each do |file_info| + FileUtils.rm_f(Rails.root.join(file_info['path'])) + end + end + + def prep_input_files + input_files.each do |file| + File.write( + Rails.root.join(file['path']), + File.read(Rails.root.join(file['content'])) + ) + end + end + + def expect_file_creation + if output_files.any? + output_files.each do |file| + expect(File).to receive(:write).with(file['path'], File.read(file['content'])) + end + else + expect(File).not_to receive(:write) + end + end + end + + context 'when creating new events' do + YAML.safe_load(File.read('spec/fixtures/scripts/internal_events/new_events.yml')).each do |test_case| + it_behaves_like 'creates the right defintion files', test_case['description'], test_case + end + end + + context 'when creating new metrics' do + YAML.safe_load(File.read('spec/fixtures/scripts/internal_events/new_metrics.yml')).each do |test_case| + it_behaves_like 'creates the right defintion files', test_case['description'], test_case + end + + context 'when creating a metric from multiple events' do + let(:events) do + [{ + action: '00_event1', category: 'InternalEventTracking', + product_section: 'dev', product_stage: 'plan', product_group: 'optimize' + }, { + action: '00_event2', category: 'InternalEventTracking', + product_section: 'dev', product_stage: 'create', product_group: 'ide' + }, { + action: '00_event3', category: 'InternalEventTracking', + product_section: 'dev', product_stage: 'create', product_group: 'source_code' + }] + end + + before do + events.each do |event| + File.write("config/events/#{event[:action]}.yml", event.transform_keys(&:to_s).to_yaml) + end + end + + it 'filters the product group options based on common section' do + # Select 00_event1 & #00_event2 + queue_cli_inputs([ + "2\n", # Enum-select: New Metric -- calculate how often one or more existing events occur over time + "2\n", # Enum-select: Multiple events -- count occurrences of several separate events or interactions + " ", # Multi-select: __event1 + "\e[B", # Arrow down to: __event2 + " ", # Multi-select: __event2 + "\n", # Submit selections + "\n", # Select: Weekly/Monthly count of unique users + "aggregate metric description\n", # Submit description + "\n", # Accept description for weekly + "\n" # Copy & continue + ]) + + run_with_timeout + + # Filter down to "dev" options + expect(plain_last_lines(9)).to eq <<~TEXT.chomp + ‣ dev:plan:project_management + dev:plan:product_planning + dev:plan:knowledge + dev:plan:optimize + dev:create:source_code + dev:create:code_review + dev:create:ide + dev:create:editor_extensions + dev:create:code_creation + TEXT + end + + it 'filters the product group options based on common section & stage' do + # Select 00_event2 & #00_event3 + queue_cli_inputs([ + "2\n", # Enum-select: New Metric -- calculate how often one or more existing events occur over time + "2\n", # Enum-select: Multiple events -- count occurrences of several separate events or interactions + "\e[B", # Arrow down to: __event2 + " ", # Multi-select: __event2 + "\e[B", # Arrow down to: __event3 + " ", # Multi-select: __event3 + "\n", # Submit selections + "\n", # Select: Weekly/Monthly count of unique users + "aggregate metric description\n", # Submit description + "\n", # Accept description for weekly + "\n" # Copy & continue + ]) + + run_with_timeout + + # Filter down to "dev:create" options + expect(plain_last_lines(5)).to eq <<~TEXT.chomp + ‣ dev:create:source_code + dev:create:code_review + dev:create:ide + dev:create:editor_extensions + dev:create:code_creation + TEXT + end + end + + context 'when product group for event no longer exists' do + let(:event) do + { + action: '00_event1', category: 'InternalEventTracking', + product_section: 'other', product_stage: 'other', product_group: 'other' + } + end + + before do + File.write("config/events/#{event[:action]}.yml", event.transform_keys(&:to_s).to_yaml) + end + + it 'prompts user to select another group' do + queue_cli_inputs([ + "2\n", # Enum-select: New Metric -- calculate how often one or more existing events occur over time + "1\n", # Enum-select: Single event -- count occurrences of a specific event or user interaction + "\n", # Select: 00__event1 + "\n", # Select: Weekly/Monthly count of unique users + "aggregate metric description\n", # Submit description + "\n", # Accept description for weekly + "2\n" # Modify attributes + ]) + + run_with_timeout + + # Filter down to "dev" options + expect(plain_last_lines(50)).to include 'Select one: Which group owns the metric?' + end + end + + context 'when creating a metric for an event which has metrics' do + before do + File.write(event1_filepath, File.read(event1_content)) + end + + it 'shows all metrics options' do + select_event_from_list + + expect(plain_last_lines(5)).to eq <<~TEXT.chomp + ‣ Monthly/Weekly count of unique users [who triggered internal_events_cli_used] + Monthly/Weekly count of unique projects [where internal_events_cli_used occurred] + Monthly/Weekly count of unique namespaces [where internal_events_cli_used occurred] + Monthly/Weekly count of [internal_events_cli_used occurrences] + Total count of [internal_events_cli_used occurrences] + TEXT + end + + context 'with an existing weekly metric' do + before do + File.write( + 'ee/config/metrics/counts_7d/count_total_internal_events_cli_used_weekly.yml', + File.read('spec/fixtures/scripts/internal_events/metrics/ee_total_7d_single_event.yml') + ) + end + + it 'partially filters metric options' do + select_event_from_list + + expect(plain_last_lines(6)).to eq <<~TEXT.chomp + ‣ Monthly/Weekly count of unique users [who triggered internal_events_cli_used] + Monthly/Weekly count of unique projects [where internal_events_cli_used occurred] + Monthly/Weekly count of unique namespaces [where internal_events_cli_used occurred] + Monthly count of [internal_events_cli_used occurrences] + ✘ Weekly count of [internal_events_cli_used occurrences] (already defined) + Total count of [internal_events_cli_used occurrences] + TEXT + end + end + + context 'with an existing total metric' do + before do + File.write( + 'ee/config/metrics/counts_all/count_total_internal_events_cli_used.yml', + File.read('spec/fixtures/scripts/internal_events/metrics/ee_total_single_event.yml') + ) + end + + it 'filters whole metric options' do + select_event_from_list + + expect(plain_last_lines(5)).to eq <<~TEXT.chomp + ‣ Monthly/Weekly count of unique users [who triggered internal_events_cli_used] + Monthly/Weekly count of unique projects [where internal_events_cli_used occurred] + Monthly/Weekly count of unique namespaces [where internal_events_cli_used occurred] + Monthly/Weekly count of [internal_events_cli_used occurrences] + ✘ Total count of [internal_events_cli_used occurrences] (already defined) + TEXT + end + end + + private + + def select_event_from_list + queue_cli_inputs([ + "2\n", # Enum-select: New Metric -- calculate how often one or more existing events occur over time + "1\n", # Enum-select: Single event -- count occurrences of a specific event or user interaction + 'internal_events_cli_used', # Filters to this event + "\n" # Select: config/events/internal_events_cli_used.yml + ]) + + run_with_timeout + end + end + + context 'when event excludes identifiers' do + before do + File.write(event2_filepath, File.read(event2_content)) + end + + it 'filters unavailable identifiers' do + queue_cli_inputs([ + "2\n", # Enum-select: New Metric -- calculate how often one or more existing events occur over time + "1\n", # Enum-select: Single event -- count occurrences of a specific event or user interaction + 'internal_events_cli_opened', # Filters to this event + "\n" # Select: config/events/internal_events_cli_opened.yml + ]) + + run_with_timeout + + expect(plain_last_lines(5)).to eq <<~TEXT.chomp + ✘ Monthly/Weekly count of unique users [who triggered internal_events_cli_opened] (user unavailable) + ✘ Monthly/Weekly count of unique projects [where internal_events_cli_opened occurred] (project unavailable) + ✘ Monthly/Weekly count of unique namespaces [where internal_events_cli_opened occurred] (namespace unavailable) + ‣ Monthly/Weekly count of [internal_events_cli_opened occurrences] + Total count of [internal_events_cli_opened occurrences] + TEXT + end + end + + context 'when all metrics already exist' do + let(:event) { { action: '00_event1', category: 'InternalEventTracking' } } + let(:metric) { { options: { 'events' => ['00_event1'] }, events: [{ 'name' => '00_event1' }] } } + + let(:files) do + [ + ['config/events/00_event1.yml', event], + ['config/metrics/counts_all/count_total_00_event1.yml', metric.merge(time_frame: 'all')], + ['config/metrics/counts_7d/count_total_00_event1_weekly.yml', metric.merge(time_frame: '7d')], + ['config/metrics/counts_28d/count_total_00_event1_monthly.yml', metric.merge(time_frame: '28d')] + ] + end + + before do + files.each do |path, content| + File.write(path, content.transform_keys(&:to_s).to_yaml) + end + end + + it 'exits the script and directs user to search for existing metrics' do + queue_cli_inputs([ + "2\n", # Enum-select: New Metric -- calculate how often one or more existing events occur over time + "1\n", # Enum-select: Single event -- count occurrences of a specific event or user interaction + '00_event1', # Filters to this event + "\n" # Select: config/events/00_event1.yml + ]) + + run_with_timeout + + expect(plain_last_lines(15)).to include 'Looks like the potential metrics for this event ' \ + 'either already exist or are unsupported.' + end + end + end + + context 'when showing usage examples' do + let(:expected_example_prompt) do + <<~TEXT.chomp + Select one: Select a use-case to view examples for: (Press ↑/↓ arrow or 1-8 number to move and Enter to select) + ‣ 1. ruby/rails + 2. rspec + 3. javascript (vue) + 4. javascript (plain) + 5. vue template + 6. haml + 7. View examples for a different event + 8. Exit + TEXT + end + + context 'for an event with identifiers' do + let(:expected_rails_example) do + <<~TEXT.chomp + -------------------------------------------------- + # RAILS + + Gitlab::InternalEvents.track_event( + 'internal_events_cli_used', + project: project, + namespace: project.namespace, + user: user + ) + + -------------------------------------------------- + TEXT + end + + let(:expected_rspec_example) do + <<~TEXT.chomp + -------------------------------------------------- + # RSPEC + + it_behaves_like 'internal event tracking' do + let(:event) { 'internal_events_cli_used' } + let(:project) { project } + let(:namespace) { project.namespace } + let(:user) { user } + end + + -------------------------------------------------- + TEXT + end + + before do + File.write(event1_filepath, File.read(event1_content)) + end + + it 'shows backend examples' do + queue_cli_inputs([ + "3\n", # Enum-select: View Usage -- look at code examples for an existing event + 'internal_events_cli_used', # Filters to this event + "\n", # Select: config/events/internal_events_cli_used.yml + "\n", # Select: ruby/rails + "\e[B", # Arrow down to: rspec + "\n", # Select: rspec + "8\n" # Exit + ]) + + run_with_timeout + + output = plain_last_lines(100) + + expect(output).to include expected_example_prompt + expect(output).to include expected_rails_example + expect(output).to include expected_rspec_example + end + end + + context 'for an event without identifiers' do + let(:expected_rails_example) do + <<~TEXT.chomp + -------------------------------------------------- + # RAILS + + Gitlab::InternalEvents.track_event('internal_events_cli_opened') + + -------------------------------------------------- + TEXT + end + + let(:expected_rspec_example) do + <<~TEXT.chomp + -------------------------------------------------- + # RSPEC + + it_behaves_like 'internal event tracking' do + let(:event) { 'internal_events_cli_opened' } + end + + -------------------------------------------------- + TEXT + end + + let(:expected_vue_example) do + <<~TEXT.chomp + -------------------------------------------------- + // VUE + + <script> + import { InternalEvents } from '~/tracking'; + import { GlButton } from '@gitlab/ui'; + + const trackingMixin = InternalEvents.mixin(); + + export default { + mixins: [trackingMixin], + components: { GlButton }, + methods: { + performAction() { + this.trackEvent('internal_events_cli_opened'); + }, + }, + }; + </script> + + <template> + <gl-button @click=performAction>Click Me</gl-button> + </template> + + -------------------------------------------------- + TEXT + end + + let(:expected_js_example) do + <<~TEXT.chomp + -------------------------------------------------- + // FRONTEND -- RAW JAVASCRIPT + + import { InternalEvents } from '~/tracking'; + + export const performAction = () => { + InternalEvents.trackEvent('internal_events_cli_opened'); + + return true; + }; + + -------------------------------------------------- + TEXT + end + + let(:expected_vue_template_example) do + <<~TEXT.chomp + -------------------------------------------------- + // VUE TEMPLATE -- ON-CLICK + + <script> + import { GlButton } from '@gitlab/ui'; + + export default { + components: { GlButton } + }; + </script> + + <template> + <gl-button data-event-tracking="internal_events_cli_opened"> + Click Me + </gl-button> + </template> + + -------------------------------------------------- + // VUE TEMPLATE -- ON-LOAD + + <script> + import { GlButton } from '@gitlab/ui'; + + export default { + components: { GlButton } + }; + </script> + + <template> + <gl-button data-event-tracking-load="internal_events_cli_opened"> + Click Me + </gl-button> + </template> + + -------------------------------------------------- + TEXT + end + + let(:expected_haml_example) do + <<~TEXT.chomp + -------------------------------------------------- + # HAML -- ON-CLICK + + .gl-display-inline-block{ data: { event_tracking: 'internal_events_cli_opened' } } + = _('Important Text') + + -------------------------------------------------- + # HAML -- COMPONENT ON-CLICK + + = render Pajamas::ButtonComponent.new(button_options: { data: { event_tracking: 'internal_events_cli_opened' } }) + + -------------------------------------------------- + # HAML -- COMPONENT ON-LOAD + + = render Pajamas::ButtonComponent.new(button_options: { data: { event_tracking_load: true, event_tracking: 'internal_events_cli_opened' } }) + + -------------------------------------------------- + TEXT + end + + before do + File.write(event2_filepath, File.read(event2_content)) + end + + it 'shows all examples' do + queue_cli_inputs([ + "3\n", # Enum-select: View Usage -- look at code examples for an existing event + 'internal_events_cli_opened', # Filters to this event + "\n", # Select: config/events/internal_events_cli_used.yml + "\n", # Select: ruby/rails + "\e[B", # Arrow down to: rspec + "\n", # Select: rspec + "\e[B", # Arrow down to: js vue + "\n", # Select: js vue + "\e[B", # Arrow down to: js plain + "\n", # Select: js plain + "\e[B", # Arrow down to: vue template + "\n", # Select: vue template + "\e[B", # Arrow down to: haml + "\n", # Select: haml + "8\n" # Exit + ]) + + run_with_timeout + + output = plain_last_lines(1000) + + expect(output).to include expected_example_prompt + expect(output).to include expected_rails_example + expect(output).to include expected_rspec_example + expect(output).to include expected_vue_example + expect(output).to include expected_js_example + expect(output).to include expected_vue_template_example + expect(output).to include expected_haml_example + end + end + + context 'when viewing examples for multiple events' do + let(:expected_event1_example) do + <<~TEXT.chomp + -------------------------------------------------- + # RAILS + + Gitlab::InternalEvents.track_event( + 'internal_events_cli_used', + project: project, + namespace: project.namespace, + user: user + ) + + -------------------------------------------------- + TEXT + end + + let(:expected_event2_example) do + <<~TEXT.chomp + -------------------------------------------------- + # RAILS + + Gitlab::InternalEvents.track_event('internal_events_cli_opened') + + -------------------------------------------------- + TEXT + end + + before do + File.write(event1_filepath, File.read(event1_content)) + File.write(event2_filepath, File.read(event2_content)) + end + + it 'switches between events gracefully' do + queue_cli_inputs([ + "3\n", # Enum-select: View Usage -- look at code examples for an existing event + 'internal_events_cli_used', # Filters to this event + "\n", # Select: config/events/internal_events_cli_used.yml + "\n", # Select: ruby/rails + "7\n", # Select: View examples for a different event + 'internal_events_cli_opened', # Filters to this event + "\n", # Select: config/events/internal_events_cli_opened.yml + "\n", # Select: ruby/rails + "8\n" # Exit + ]) + + run_with_timeout + + output = plain_last_lines(300) + + expect(output).to include expected_example_prompt + expect(output).to include expected_event1_example + expect(output).to include expected_event2_example + end + end + end + + context 'when offline' do + before do + stub_product_groups(nil) + end + + it_behaves_like 'creates the right defintion files', + 'Creates a new event with product stage/section/group input manually' do + let(:keystrokes) do + [ + "1\n", # Enum-select: New Event -- start tracking when an action or scenario occurs on gitlab instances + "Internal Event CLI is opened\n", # Submit description + "internal_events_cli_opened\n", # Submit action name + "6\n", # Select: None + "\n", # Skip MR URL + "analytics\n", # Input section + "monitor\n", # Input stage + "analytics_instrumentation\n", # Input group + "2\n", # Select [premium, ultimate] + "y\n", # Create file + "3\n" # Exit + ] + end + + let(:output_files) { [{ 'path' => event2_filepath, 'content' => event2_content }] } + end + + it_behaves_like 'creates the right defintion files', + 'Creates a new metric with product stage/section/group input manually' do + let(:keystrokes) do + [ + "2\n", # Enum-select: New Metric -- calculate how often one or more existing events occur over time + "2\n", # Enum-select: Multiple events -- count occurrences of several separate events or interactions + 'internal_events_cli', # Filters to the relevant events + ' ', # Multi-select: internal_events_cli_closed + "\e[B", # Arrow down to: internal_events_cli_used + ' ', # Multi-select: internal_events_cli_used + "\n", # Submit selections + "\e[B", # Arrow down to: Weekly count of unique projects + "\n", # Select: Weekly count of unique projects + "where a defition file was created with the CLI\n", # Input description + "\n", # Submit weekly description for monthly + "2\n", # Select: Modify attributes + "\n", # Accept section + "\n", # Accept stage + "\n", # Accept group + "\n", # Skip URL + "1\n", # Select: [free, premium, ultimate] + "y\n", # Create file + "y\n", # Create file + "2\n" # Exit + ] + end + + let(:input_files) do + [ + { 'path' => event1_filepath, 'content' => event1_content }, + { 'path' => event3_filepath, 'content' => event3_content } + ] + end + + let(:output_files) do + # rubocop:disable Layout/LineLength -- Long filepaths read better unbroken + [{ + 'path' => 'config/metrics/counts_28d/count_distinct_project_id_from_internal_events_cli_closed_and_internal_events_cli_used_monthly.yml', + 'content' => 'spec/fixtures/scripts/internal_events/metrics/project_id_28d_multiple_events.yml' + }, { + 'path' => 'config/metrics/counts_7d/count_distinct_project_id_from_internal_events_cli_closed_and_internal_events_cli_used_weekly.yml', + 'content' => 'spec/fixtures/scripts/internal_events/metrics/project_id_7d_multiple_events.yml' + }] + # rubocop:enable Layout/LineLength + end + end + end + + context 'when window size is unavailable' do + before do + # `tput <cmd>` returns empty string on error + stub_helper(:fetch_window_size, '') + stub_helper(:fetch_window_height, '') + end + + it_behaves_like 'creates the right defintion files', + 'Terminal size does not prevent file creation' do + let(:keystrokes) do + [ + "1\n", # Enum-select: New Event -- start tracking when an action or scenario occurs on gitlab instances + "Internal Event CLI is opened\n", # Submit description + "internal_events_cli_opened\n", # Submit action name + "6\n", # Select: None + "\n", # Skip MR URL + "instrumentation\n", # Filter & select group + "2\n", # Select [premium, ultimate] + "y\n", # Create file + "3\n" # Exit + ] + end + + let(:output_files) { [{ 'path' => event2_filepath, 'content' => event2_content }] } + end + end + + context "when user doesn't know what they're trying to do" do + it "handles when user isn't trying to track product usage" do + queue_cli_inputs([ + "4\n", # Enum-select: ...am I in the right place? + "n\n" # No --> Are you trying to track customer usage of a GitLab feature? + ]) + + run_with_timeout + + expect(plain_last_lines(50)).to include("Oh no! This probably isn't the tool you need!") + end + + it "handles when product usage can't be tracked with events" do + queue_cli_inputs([ + "4\n", # Enum-select: ...am I in the right place? + "y\n", # Yes --> Are you trying to track customer usage of a GitLab feature? + "n\n" # No --> Can usage for the feature be measured by tracking a specific user action? + ]) + + run_with_timeout + + expect(plain_last_lines(50)).to include("Oh no! This probably isn't the tool you need!") + end + + it 'handles when user needs to add a new event' do + queue_cli_inputs([ + "4\n", # Enum-select: ...am I in the right place? + "y\n", # Yes --> Are you trying to track customer usage of a GitLab feature? + "y\n", # Yes --> Can usage for the feature be measured by tracking a specific user action? + "n\n", # No --> Is the event already tracked? + "n\n" # No --> Ready to start? + ]) + + run_with_timeout + + expect(plain_last_lines(30)).to include("Okay! The next step is adding a new event! (~5 min)") + end + + it 'handles when user needs to add a new metric' do + queue_cli_inputs([ + "4\n", # Enum-select: ...am I in the right place? + "y\n", # Yes --> Are you trying to track customer usage of a GitLab feature? + "y\n", # Yes --> Can usage for the feature be measured by tracking a specific user action? + "y\n", # Yes --> Is the event already tracked? + "n\n" # No --> Ready to start? + ]) + + run_with_timeout + + expect(plain_last_lines(30)).to include("Amazing! The next step is adding a new metric! (~8 min)") + end + end + + private + + def queue_cli_inputs(keystrokes) + prompt.input << keystrokes.join('') + prompt.input.rewind + end + + def run_with_timeout(duration = 1) + Timeout.timeout(duration) { described_class.new(prompt).run } + rescue Timeout::Error + # Timeout is needed to break out of the CLI, but we may want + # to make assertions afterwards + end + + def run_with_verbose_timeout(duration = 1) + Timeout.timeout(duration) { described_class.new(prompt).run } + rescue Timeout::Error => e + # Re-raise error so CLI output is printed with the error + message = <<~TEXT + Awaiting input too long. Entire CLI output: + + #{ + prompt.output.string.lines + .map { |line| "\e[0;37m#{line}\e[0m" } # wrap in white + .join('') + .gsub("\e[1G", "\e[1G ") # align to error indent + } + + + TEXT + + raise e.class, message, e.backtrace + end + + def plain_last_lines(size) + prompt.output.string + .lines + .last(size) + .join('') + .gsub(/\e[^\sm]{2,4}[mh]/, '') + end + + def collect_file_writes(collector) + allow(File).to receive(:write).and_wrap_original do |original_method, *args, &block| + filepath = args.first + collector << filepath + + dirname = Pathname.new(filepath).dirname + unless dirname.directory? + FileUtils.mkdir_p dirname + collector << dirname.to_s + end + + original_method.call(*args, &block) + end + end + + def stub_milestone(milestone) + stub_const("InternalEventsCli::Helpers::MILESTONE", milestone) + end + + def stub_product_groups(body) + allow(Net::HTTP).to receive(:get) + .with(URI('https://gitlab.com/gitlab-com/www-gitlab-com/-/raw/master/data/stages.yml')) + .and_return(body) + end + + def stub_helper(helper, value) + # rubocop:disable RSpec/AnyInstanceOf -- 'Next' helper not included in fast_spec_helper & next is insufficient + allow_any_instance_of(InternalEventsCli::Helpers).to receive(helper).and_return(value) + # rubocop:enable RSpec/AnyInstanceOf + end + + def delete_files(files) + files.each do |filepath| + FileUtils.rm_f(Rails.root.join(filepath)) + end + end + + def internal_event_fixture(filepath) + Rails.root.join('spec', 'fixtures', 'scripts', 'internal_events', filepath) + end +end diff --git a/spec/serializers/merge_request_poll_widget_entity_spec.rb b/spec/serializers/merge_request_poll_widget_entity_spec.rb index 6b80609c348..171f2324cf1 100644 --- a/spec/serializers/merge_request_poll_widget_entity_spec.rb +++ b/spec/serializers/merge_request_poll_widget_entity_spec.rb @@ -22,6 +22,9 @@ RSpec.describe MergeRequestPollWidgetEntity do .to eq(resource.default_merge_commit_message(include_description: true)) end + it { is_expected.to include(ff_only_enabled: false) } + it { is_expected.to include(ff_merge_possible: false) } + describe 'new_blob_path' do context 'when user can push to project' do it 'returns path' do |