Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-05-11 12:10:20 +00:00
parent 7fa274753d
commit c33a9adb70
30 changed files with 347 additions and 48 deletions

View File

@ -1 +1 @@
7ad4fcea44bc4c1a8682d77ad6b46aeacb8c6e1d
4f0cd9404f31511f5051e49b363adc06aa3ec365

View File

@ -18,7 +18,7 @@ gem 'default_value_for', '~> 3.4.0'
gem 'pg', '~> 1.1'
gem 'rugged', '~> 1.1'
gem 'grape-path-helpers', '~> 1.6.1'
gem 'grape-path-helpers', '~> 1.6.3'
gem 'faraday', '~> 1.0'
gem 'marginalia', '~> 1.10.0'

View File

@ -541,7 +541,7 @@ GEM
grape-entity (0.7.1)
activesupport (>= 4.0)
multi_json (>= 1.3.2)
grape-path-helpers (1.6.1)
grape-path-helpers (1.6.3)
activesupport
grape (~> 1.3)
rake (> 12)
@ -1471,7 +1471,7 @@ DEPENDENCIES
gpgme (~> 2.0.19)
grape (~> 1.5.2)
grape-entity (~> 0.7.1)
grape-path-helpers (~> 1.6.1)
grape-path-helpers (~> 1.6.3)
grape_logging (~> 1.7)
graphiql-rails (~> 1.4.10)
graphlient (~> 0.4.0)

View File

@ -1,7 +1,10 @@
<script>
import { GlTable } from '@gitlab/ui';
import { __ } from '~/locale';
import { GlSkeletonLoader, GlTable } from '@gitlab/ui';
import createFlash from '~/flash';
import { convertNodeIdsFromGraphQLIds } from '~/graphql_shared/utils';
import { s__, __ } from '~/locale';
import UserDate from '~/vue_shared/components/user_date.vue';
import getUsersGroupCountsQuery from '../graphql/queries/get_users_group_counts.query.graphql';
import UserActions from './user_actions.vue';
import UserAvatar from './user_avatar.vue';
@ -11,6 +14,7 @@ const thWidthClass = (width) => `gl-w-${width}p ${DEFAULT_TH_CLASSES}`;
export default {
components: {
GlSkeletonLoader,
GlTable,
UserAvatar,
UserActions,
@ -26,6 +30,45 @@ export default {
required: true,
},
},
data() {
return {
groupCounts: [],
};
},
apollo: {
groupCounts: {
query: getUsersGroupCountsQuery,
variables() {
return {
usernames: this.users.map((user) => user.username),
};
},
update(data) {
const nodes = data?.users?.nodes || [];
const parsedIds = convertNodeIdsFromGraphQLIds(nodes);
return parsedIds.reduce((acc, { id, groupCount }) => {
acc[id] = groupCount || 0;
return acc;
}, {});
},
error(error) {
createFlash({
message: this.$options.i18n.groupCountFetchError,
captureError: true,
error,
});
},
skip() {
return !this.users.length;
},
},
},
i18n: {
groupCountFetchError: s__(
'AdminUsers|Could not load user group counts. Please refresh the page to try again.',
),
},
fields: [
{
key: 'name',
@ -37,6 +80,11 @@ export default {
label: __('Projects'),
thClass: thWidthClass(10),
},
{
key: 'groupCount',
label: __('Groups'),
thClass: thWidthClass(10),
},
{
key: 'createdAt',
label: __('Created on'),
@ -50,7 +98,7 @@ export default {
{
key: 'settings',
label: '',
thClass: thWidthClass(20),
thClass: thWidthClass(10),
},
],
};
@ -77,6 +125,13 @@ export default {
<user-date :date="lastActivityOn" show-never />
</template>
<template #cell(groupCount)="{ item: { id } }">
<div :data-testid="`user-group-count-${id}`">
<gl-skeleton-loader v-if="$apollo.loading" :width="40" :lines="1" />
<span v-else>{{ groupCounts[id] }}</span>
</div>
</template>
<template #cell(projectsCount)="{ item: { id, projectsCount } }">
<div :data-testid="`user-project-count-${id}`">{{ projectsCount }}</div>
</template>

View File

@ -0,0 +1,8 @@
query getUsersGroupCounts($usernames: [String!]) {
users(usernames: $usernames) {
nodes {
id
groupCount
}
}
}

View File

@ -1,7 +1,15 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import AdminUsersApp from './components/app.vue';
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient({}, { assumeImmutableResults: true }),
});
export const initAdminUsersApp = (el = document.querySelector('#js-admin-users-app')) => {
if (!el) {
return false;
@ -11,6 +19,7 @@ export const initAdminUsersApp = (el = document.querySelector('#js-admin-users-a
return new Vue({
el,
apolloProvider,
render: (createElement) =>
createElement(AdminUsersApp, {
props: {

View File

@ -64,8 +64,7 @@ module Types
description: 'Group memberships of the user.'
field :group_count,
resolver: Resolvers::Users::GroupCountResolver,
description: 'Group count for the user.',
feature_flag: :user_group_counts
description: 'Group count for the user.'
field :status,
type: Types::UserStatusType,
null: true,

View File

@ -5,6 +5,10 @@ module WebpackHelper
javascript_include_tag(*webpack_entrypoint_paths(bundle))
end
def webpack_preload_asset_tag(asset, options = {})
preload_link_tag(Gitlab::Webpack::Manifest.asset_paths(asset).first, options)
end
def webpack_controller_bundle_tags
chunks = []

View File

@ -32,6 +32,8 @@
- if page_canonical_link
%link{ rel: 'canonical', href: page_canonical_link }
= webpack_preload_asset_tag("monaco")
= favicon_link_tag favicon, id: 'favicon', data: { original_href: favicon }, type: 'image/png'
= render 'layouts/startup_css'

View File

@ -0,0 +1,5 @@
---
title: Change conan token expiration from 1 hour to 24 hours
merge_request: 60763
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Show total group counts in admin users table
merge_request: 60998
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Enable Content-Security-Policy header by default
merge_request: 56923
author:
type: other

View File

@ -0,0 +1,5 @@
---
title: Update grape-path-helpers to v1.6.3
merge_request: 61196
author:
type: performance

View File

@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/329664
milestone: '13.12'
type: development
group: group::gitaly
default_enabled: true
default_enabled: false

View File

@ -1,8 +0,0 @@
---
name: user_group_counts
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/44069/
rollout_issue_url:
milestone: '13.6'
type: development
group: group::compliance
default_enabled: false

View File

@ -6,7 +6,7 @@ product_stage: manage
product_group: group::optimize
product_category:
value_type: number
status: data_available
status: removed
time_frame: 28d
data_source: redis_hll
distribution:

View File

@ -6,8 +6,9 @@ product_stage: manage
product_group: group::optimize
product_category:
value_type: number
status: data_available
time_frame: all
status: removed
time_frame: 7d
data_source: redis_hll
data_source:
distribution:
- ce

View File

@ -9844,7 +9844,7 @@ A user assigned to a merge request.
| <a id="mergerequestassigneebot"></a>`bot` | [`Boolean!`](#boolean) | Indicates if the user is a bot. |
| <a id="mergerequestassigneecallouts"></a>`callouts` | [`UserCalloutConnection`](#usercalloutconnection) | User callouts that belong to the user. (see [Connections](#connections)) |
| <a id="mergerequestassigneeemail"></a>`email` **{warning-solid}** | [`String`](#string) | **Deprecated** in 13.7. This was renamed. Use: [`User.publicEmail`](#userpublicemail). |
| <a id="mergerequestassigneegroupcount"></a>`groupCount` | [`Int`](#int) | Group count for the user. Available only when feature flag `user_group_counts` is enabled. |
| <a id="mergerequestassigneegroupcount"></a>`groupCount` | [`Int`](#int) | Group count for the user. |
| <a id="mergerequestassigneegroupmemberships"></a>`groupMemberships` | [`GroupMemberConnection`](#groupmemberconnection) | Group memberships of the user. (see [Connections](#connections)) |
| <a id="mergerequestassigneeid"></a>`id` | [`ID!`](#id) | ID of the user. |
| <a id="mergerequestassigneelocation"></a>`location` | [`String`](#string) | The location of the user. |
@ -10050,7 +10050,7 @@ A user assigned to a merge request as a reviewer.
| <a id="mergerequestreviewerbot"></a>`bot` | [`Boolean!`](#boolean) | Indicates if the user is a bot. |
| <a id="mergerequestreviewercallouts"></a>`callouts` | [`UserCalloutConnection`](#usercalloutconnection) | User callouts that belong to the user. (see [Connections](#connections)) |
| <a id="mergerequestrevieweremail"></a>`email` **{warning-solid}** | [`String`](#string) | **Deprecated** in 13.7. This was renamed. Use: [`User.publicEmail`](#userpublicemail). |
| <a id="mergerequestreviewergroupcount"></a>`groupCount` | [`Int`](#int) | Group count for the user. Available only when feature flag `user_group_counts` is enabled. |
| <a id="mergerequestreviewergroupcount"></a>`groupCount` | [`Int`](#int) | Group count for the user. |
| <a id="mergerequestreviewergroupmemberships"></a>`groupMemberships` | [`GroupMemberConnection`](#groupmemberconnection) | Group memberships of the user. (see [Connections](#connections)) |
| <a id="mergerequestreviewerid"></a>`id` | [`ID!`](#id) | ID of the user. |
| <a id="mergerequestreviewerlocation"></a>`location` | [`String`](#string) | The location of the user. |
@ -12679,7 +12679,7 @@ Core represention of a GitLab user.
| <a id="usercorebot"></a>`bot` | [`Boolean!`](#boolean) | Indicates if the user is a bot. |
| <a id="usercorecallouts"></a>`callouts` | [`UserCalloutConnection`](#usercalloutconnection) | User callouts that belong to the user. (see [Connections](#connections)) |
| <a id="usercoreemail"></a>`email` **{warning-solid}** | [`String`](#string) | **Deprecated** in 13.7. This was renamed. Use: [`User.publicEmail`](#userpublicemail). |
| <a id="usercoregroupcount"></a>`groupCount` | [`Int`](#int) | Group count for the user. Available only when feature flag `user_group_counts` is enabled. |
| <a id="usercoregroupcount"></a>`groupCount` | [`Int`](#int) | Group count for the user. |
| <a id="usercoregroupmemberships"></a>`groupMemberships` | [`GroupMemberConnection`](#groupmemberconnection) | Group memberships of the user. (see [Connections](#connections)) |
| <a id="usercoreid"></a>`id` | [`ID!`](#id) | ID of the user. |
| <a id="usercorelocation"></a>`location` | [`String`](#string) | The location of the user. |
@ -15346,7 +15346,7 @@ Implementations:
| <a id="userbot"></a>`bot` | [`Boolean!`](#boolean) | Indicates if the user is a bot. |
| <a id="usercallouts"></a>`callouts` | [`UserCalloutConnection`](#usercalloutconnection) | User callouts that belong to the user. (see [Connections](#connections)) |
| <a id="useremail"></a>`email` **{warning-solid}** | [`String`](#string) | **Deprecated** in 13.7. This was renamed. Use: [`User.publicEmail`](#userpublicemail). |
| <a id="usergroupcount"></a>`groupCount` | [`Int`](#int) | Group count for the user. Available only when feature flag `user_group_counts` is enabled. |
| <a id="usergroupcount"></a>`groupCount` | [`Int`](#int) | Group count for the user. |
| <a id="usergroupmemberships"></a>`groupMemberships` | [`GroupMemberConnection`](#groupmemberconnection) | Group memberships of the user. (see [Connections](#connections)) |
| <a id="userid"></a>`id` | [`ID!`](#id) | ID of the user. |
| <a id="userlocation"></a>`location` | [`String`](#string) | The location of the user. |

View File

@ -114,7 +114,7 @@ Unique visitors to /groups/:group/-/analytics/merge_request_analytics
Group: `group::optimize`
Status: `data_available`
Status: `removed`
Tiers: `free`
@ -7410,7 +7410,7 @@ Missing description
Group: `group::optimize`
Status: `data_available`
Status: `removed`
Tiers: `free`
@ -7422,7 +7422,7 @@ Missing description
Group: `group::optimize`
Status: `data_available`
Status: `removed`
Tiers:

View File

@ -8,6 +8,7 @@
module Gitlab
class ConanToken
HMAC_KEY = 'gitlab-conan-packages'
CONAN_TOKEN_EXPIRE_TIME = 1.day.freeze
attr_reader :access_token_id, :user_id
@ -57,7 +58,7 @@ module Gitlab
JSONWebToken::HMACToken.new(self.class.secret).tap do |token|
token['access_token'] = access_token_id
token['user_id'] = user_id
token.expire_time = token.issued_at + 1.hour
token.expire_time = token.issued_at + CONAN_TOKEN_EXPIRE_TIME
end
end
end

View File

@ -8,11 +8,33 @@ module Gitlab
media_src object_src report_uri script_src style_src worker_src).freeze
def self.default_settings_hash
{
'enabled' => false,
settings_hash = {
'enabled' => true,
'report_only' => false,
'directives' => DIRECTIVES.each_with_object({}) { |directive, hash| hash[directive] = nil }
'directives' => {
'default_src' => "'self'",
'base_uri' => "'self'",
'child_src' => "'none'",
'connect_src' => "'self'",
'font_src' => "'self'",
'form_action' => "'self' https: http:",
'frame_ancestors' => "'self'",
'frame_src' => "'self' https://www.recaptcha.net/ https://content.googleapis.com https://content-compute.googleapis.com https://content-cloudbilling.googleapis.com https://content-cloudresourcemanager.googleapis.com",
'img_src' => "'self' data: blob: http: https:",
'manifest_src' => "'self'",
'media_src' => "'self'",
'script_src' => "'strict-dynamic' 'self' 'unsafe-inline' 'unsafe-eval' https://www.recaptcha.net https://apis.google.com",
'style_src' => "'self' 'unsafe-inline'",
'worker_src' => "'self'",
'object_src' => "'none'",
'report_uri' => nil
}
}
allow_webpack_dev_server(settings_hash) if Rails.env.development?
allow_cdn(settings_hash) if ENV['GITLAB_CDN_HOST'].present?
settings_hash
end
def initialize(csp_directives)
@ -38,6 +60,26 @@ module Gitlab
arguments.strip.split(' ').map(&:strip)
end
def self.allow_webpack_dev_server(settings_hash)
secure = Settings.webpack.dev_server['https']
host_and_port = "#{Settings.webpack.dev_server['host']}:#{Settings.webpack.dev_server['port']}"
http_url = "#{secure ? 'https' : 'http'}://#{host_and_port}"
ws_url = "#{secure ? 'wss' : 'ws'}://#{host_and_port}"
append_to_directive(settings_hash, 'connect_src', "#{http_url} #{ws_url}")
end
def self.allow_cdn(settings_hash)
cdn_host = ENV['GITLAB_CDN_HOST']
append_to_directive(settings_hash, 'script_src', cdn_host)
append_to_directive(settings_hash, 'style_src', cdn_host)
end
def self.append_to_directive(settings_hash, directive, text)
settings_hash['directives'][directive] = "#{settings_hash['directives'][directive]} #{text}".strip
end
end
end
end

View File

@ -2528,6 +2528,9 @@ msgstr ""
msgid "AdminUsers|Cohorts"
msgstr ""
msgid "AdminUsers|Could not load user group counts. Please refresh the page to try again."
msgstr ""
msgid "AdminUsers|Deactivate"
msgstr ""

View File

@ -547,6 +547,32 @@ RSpec.describe 'Admin::Users' do
end
end
# TODO: Move to main GET /admin/users block once feature flag is removed. Issue: https://gitlab.com/gitlab-org/gitlab/-/issues/290737
context 'with vue_admin_users feature flag enabled', :js do
before do
stub_feature_flags(vue_admin_users: true)
end
describe 'GET /admin/users' do
context 'user group count', :js do
before do
group = create(:group)
group.add_developer(current_user)
project = create(:project, group: create(:group))
project.add_reporter(current_user)
end
it 'displays count of the users authorized groups' do
visit admin_users_path
wait_for_requests
expect(page.find("[data-testid='user-group-count-#{current_user.id}']").text).to eq("2")
end
end
end
end
def click_user_dropdown_toggle(user_id)
page.within("[data-testid='user-actions-#{user_id}']") do
find("[data-testid='dropdown-toggle']").click

View File

@ -0,0 +1,26 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'New project from template', :js do
let(:user) { create(:user) }
before do
sign_in(user)
visit new_project_path
end
context 'create from template' do
before do
page.find('a[href="#create_from_template"]').click
wait_for_requests
end
it 'shows template tabs' do
page.within('#create-from-template-pane') do
expect(page).to have_link('Built-in', href: '#built-in')
end
end
end
end

View File

@ -1,16 +1,36 @@
import { GlTable } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { GlTable, GlSkeletonLoader } from '@gitlab/ui';
import { createLocalVue } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import AdminUserActions from '~/admin/users/components/user_actions.vue';
import AdminUserAvatar from '~/admin/users/components/user_avatar.vue';
import AdminUsersTable from '~/admin/users/components/users_table.vue';
import getUsersGroupCountsQuery from '~/admin/users/graphql/queries/get_users_group_counts.query.graphql';
import createFlash from '~/flash';
import AdminUserDate from '~/vue_shared/components/user_date.vue';
import { users, paths } from '../mock_data';
import { users, paths, createGroupCountResponse } from '../mock_data';
jest.mock('~/flash');
const localVue = createLocalVue();
localVue.use(VueApollo);
describe('AdminUsersTable component', () => {
let wrapper;
const user = users[0];
const createFetchGroupCount = (data) =>
jest.fn().mockResolvedValue(createGroupCountResponse(data));
const fetchGroupCountsLoading = jest.fn().mockResolvedValue(new Promise(() => {}));
const fetchGroupCountsError = jest.fn().mockRejectedValue(new Error('Network error'));
const fetchGroupCountsResponse = createFetchGroupCount([{ id: user.id, groupCount: 5 }]);
const findUserGroupCount = (id) => wrapper.findByTestId(`user-group-count-${id}`);
const findUserGroupCountLoader = (id) => findUserGroupCount(id).find(GlSkeletonLoader);
const getCellByLabel = (trIdx, label) => {
return wrapper
.find(GlTable)
@ -20,8 +40,16 @@ describe('AdminUsersTable component', () => {
.find(`[data-label="${label}"][role="cell"]`);
};
const initComponent = (props = {}) => {
wrapper = mount(AdminUsersTable, {
function createMockApolloProvider(resolverMock) {
const requestHandlers = [[getUsersGroupCountsQuery, resolverMock]];
return createMockApollo(requestHandlers);
}
const initComponent = (props = {}, resolverMock = fetchGroupCountsResponse) => {
wrapper = mountExtended(AdminUsersTable, {
localVue,
apolloProvider: createMockApolloProvider(resolverMock),
propsData: {
users,
paths,
@ -36,8 +64,6 @@ describe('AdminUsersTable component', () => {
});
describe('when there are users', () => {
const user = users[0];
beforeEach(() => {
initComponent();
});
@ -69,4 +95,51 @@ describe('AdminUsersTable component', () => {
expect(wrapper.text()).toContain('No users found');
});
});
describe('group counts', () => {
describe('when fetching the data', () => {
beforeEach(() => {
initComponent({}, fetchGroupCountsLoading);
});
it('renders a loader for each user', () => {
expect(findUserGroupCountLoader(user.id).exists()).toBe(true);
});
});
describe('when the data has been fetched', () => {
beforeEach(() => {
initComponent();
});
it("renders the user's group count", () => {
expect(findUserGroupCount(user.id).text()).toBe('5');
});
describe("and a user's group count is null", () => {
beforeEach(() => {
initComponent({}, createFetchGroupCount([{ id: user.id, groupCount: null }]));
});
it("renders the user's group count as 0", () => {
expect(findUserGroupCount(user.id).text()).toBe('0');
});
});
});
describe('when there is an error while fetching the data', () => {
beforeEach(() => {
initComponent({}, fetchGroupCountsError);
});
it('creates a flash message and captures the error', () => {
expect(createFlash).toHaveBeenCalledTimes(1);
expect(createFlash).toHaveBeenCalledWith({
message: 'Could not load user group counts. Please refresh the page to try again.',
captureError: true,
error: expect.any(Error),
});
});
});
});
});

View File

@ -10,7 +10,7 @@ export const users = [
'https://secure.gravatar.com/avatar/054f062d8b1a42b123f17e13a173cda8?s=80\\u0026d=identicon',
badges: [
{ text: 'Admin', variant: 'success' },
{ text: "It's you!", variant: null },
{ text: "It's you!", variant: 'muted' },
],
projectsCount: 0,
actions: [],
@ -31,3 +31,16 @@ export const paths = {
deleteWithContributions: '/admin/users/id',
adminUser: '/admin/users/id',
};
export const createGroupCountResponse = (groupCounts) => ({
data: {
users: {
nodes: groupCounts.map(({ id, groupCount }) => ({
id: `gid://gitlab/User/${id}`,
groupCount,
__typename: 'UserCore',
})),
__typename: 'UserCoreConnection',
},
},
});

View File

@ -20,7 +20,7 @@ RSpec.describe Gitlab::ConanToken do
JSONWebToken::HMACToken.new(jwt_secret).tap do |jwt|
jwt['access_token'] = access_token_id
jwt['user_id'] = user_id || user_id
jwt.expire_time = expire_time || jwt.issued_at + 1.hour
jwt.expire_time = expire_time || jwt.issued_at + ::Gitlab::ConanToken::CONAN_TOKEN_EXPIRE_TIME
end
end
@ -75,7 +75,7 @@ RSpec.describe Gitlab::ConanToken do
it 'returns nil for expired JWT' do
jwt = build_jwt(access_token_id: 123,
user_id: 456,
expire_time: Time.zone.now - 2.hours)
expire_time: Time.zone.now - (::Gitlab::ConanToken::CONAN_TOKEN_EXPIRE_TIME + 1.hour))
expect(described_class.decode(jwt.encoded)).to be_nil
end

View File

@ -20,15 +20,34 @@ RSpec.describe Gitlab::ContentSecurityPolicy::ConfigLoader do
end
describe '.default_settings_hash' do
it 'returns empty defaults' do
it 'returns defaults for all keys' do
settings = described_class.default_settings_hash
expect(settings['enabled']).to be_falsey
expect(settings['enabled']).to be_truthy
expect(settings['report_only']).to be_falsey
described_class::DIRECTIVES.each do |directive|
expect(settings['directives'].has_key?(directive)).to be_truthy
expect(settings['directives'][directive]).to be_nil
directives = settings['directives']
directive_names = (described_class::DIRECTIVES - ['report_uri'])
directive_names.each do |directive|
expect(directives.has_key?(directive)).to be_truthy
expect(directives[directive]).to be_truthy
end
expect(directives.has_key?('report_uri')).to be_truthy
expect(directives['report_uri']).to be_nil
end
context 'when GITLAB_CDN_HOST is set' do
before do
stub_env('GITLAB_CDN_HOST', 'https://example.com')
end
it 'adds GITLAB_CDN_HOST to CSP' do
settings = described_class.default_settings_hash
directives = settings['directives']
expect(directives['script_src']).to eq("'strict-dynamic' 'self' 'unsafe-inline' 'unsafe-eval' https://www.recaptcha.net https://apis.google.com https://example.com")
expect(directives['style_src']).to eq("'self' 'unsafe-inline' https://example.com")
end
end
end

View File

@ -106,7 +106,7 @@ RSpec.shared_examples 'conan authenticate endpoint' do
expect(payload['user_id']).to eq(personal_access_token.user_id)
duration = payload['exp'] - payload['iat']
expect(duration).to eq(1.hour)
expect(duration).to eq(::Gitlab::ConanToken::CONAN_TOKEN_EXPIRE_TIME)
end
end
end

View File

@ -62,6 +62,12 @@ RSpec.describe 'layouts/_head' do
expect(rendered).to match('<link rel="stylesheet" media="print" href="/stylesheets/highlight/themes/solarised-light.css" />')
end
it 'preloads Monaco' do
render
expect(rendered).to match('<link rel="preload" href="/assets/webpack/monaco.chunk.js" as="script" type="text/javascript">')
end
context 'when an asset_host is set and snowplow url is set' do
let(:asset_host) { 'http://test.host' }
let(:snowplow_collector_hostname) { 'www.snow.plow' }