Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
808c799a67
commit
d6e421b21e
1
Gemfile
1
Gemfile
|
@ -44,7 +44,6 @@ gem 'omniauth-twitter', '~> 1.4'
|
|||
gem 'omniauth_crowd', '~> 2.2.0'
|
||||
gem 'omniauth-authentiq', '~> 0.3.3'
|
||||
gem 'omniauth_openid_connect', '~> 0.3.3'
|
||||
gem "omniauth-ultraauth", '~> 0.0.2'
|
||||
gem 'omniauth-salesforce', '~> 1.0.5'
|
||||
gem 'rack-oauth2', '~> 1.9.3'
|
||||
gem 'jwt', '~> 2.1.0'
|
||||
|
|
|
@ -722,8 +722,6 @@ GEM
|
|||
omniauth-twitter (1.4.0)
|
||||
omniauth-oauth (~> 1.1)
|
||||
rack
|
||||
omniauth-ultraauth (0.0.2)
|
||||
omniauth_openid_connect (~> 0.3.0)
|
||||
omniauth_crowd (2.2.3)
|
||||
activesupport
|
||||
nokogiri (>= 1.4.4)
|
||||
|
@ -1317,7 +1315,6 @@ DEPENDENCIES
|
|||
omniauth-saml (~> 1.10)
|
||||
omniauth-shibboleth (~> 1.3.0)
|
||||
omniauth-twitter (~> 1.4)
|
||||
omniauth-ultraauth (~> 0.0.2)
|
||||
omniauth_crowd (~> 2.2.0)
|
||||
omniauth_openid_connect (~> 0.3.3)
|
||||
org-ruby (~> 0.9.12)
|
||||
|
|
|
@ -0,0 +1,187 @@
|
|||
import { sankey, sankeyLeft } from 'd3-sankey';
|
||||
import { uniqWith, isEqual } from 'lodash';
|
||||
|
||||
/*
|
||||
The following functions are the main engine in transforming the data as
|
||||
received from the endpoint into the format the d3 graph expects.
|
||||
|
||||
Input is of the form:
|
||||
[stages]
|
||||
stages: {name, groups}
|
||||
groups: [{ name, size, jobs }]
|
||||
name is a group name; in the case that the group has one job, it is
|
||||
also the job name
|
||||
size is the number of parallel jobs
|
||||
jobs: [{ name, needs}]
|
||||
job name is either the same as the group name or group x/y
|
||||
|
||||
Output is of the form:
|
||||
{ nodes: [node], links: [link] }
|
||||
node: { name, category }, + unused info passed through
|
||||
link: { source, target, value }, with source & target being node names
|
||||
and value being a constant
|
||||
|
||||
We create nodes, create links, and then dedupe the links, so that in the case where
|
||||
job 4 depends on job 1 and job 2, and job 2 depends on job 1, we show only a single link
|
||||
from job 1 to job 2 then another from job 2 to job 4.
|
||||
|
||||
CREATE NODES
|
||||
stage.name -> node.category
|
||||
stage.group.name -> node.name (this is the group name if there are parallel jobs)
|
||||
stage.group.jobs -> node.jobs
|
||||
stage.group.size -> node.size
|
||||
|
||||
CREATE LINKS
|
||||
stages.groups.name -> target
|
||||
stages.groups.needs.each -> source (source is the name of the group, not the parallel job)
|
||||
10 -> value (constant)
|
||||
*/
|
||||
|
||||
export const createNodes = data => {
|
||||
return data.flatMap(({ groups, name }) => {
|
||||
return groups.map(group => {
|
||||
return { ...group, category: name };
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const createNodeDict = nodes => {
|
||||
return nodes.reduce((acc, node) => {
|
||||
const newNode = {
|
||||
...node,
|
||||
needs: node.jobs.map(job => job.needs || []).flat(),
|
||||
};
|
||||
|
||||
if (node.size > 1) {
|
||||
node.jobs.forEach(job => {
|
||||
acc[job.name] = newNode;
|
||||
});
|
||||
}
|
||||
|
||||
acc[node.name] = newNode;
|
||||
return acc;
|
||||
}, {});
|
||||
};
|
||||
|
||||
export const createNodesStructure = data => {
|
||||
const nodes = createNodes(data);
|
||||
const nodeDict = createNodeDict(nodes);
|
||||
|
||||
return { nodes, nodeDict };
|
||||
};
|
||||
|
||||
export const makeLinksFromNodes = (nodes, nodeDict) => {
|
||||
const constantLinkValue = 10; // all links are the same weight
|
||||
return nodes
|
||||
.map(group => {
|
||||
return group.jobs.map(job => {
|
||||
if (!job.needs) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return job.needs.map(needed => {
|
||||
return {
|
||||
source: nodeDict[needed]?.name,
|
||||
target: group.name,
|
||||
value: constantLinkValue,
|
||||
};
|
||||
});
|
||||
});
|
||||
})
|
||||
.flat(2);
|
||||
};
|
||||
|
||||
export const getAllAncestors = (nodes, nodeDict) => {
|
||||
const needs = nodes
|
||||
.map(node => {
|
||||
return nodeDict[node].needs || '';
|
||||
})
|
||||
.flat()
|
||||
.filter(Boolean);
|
||||
|
||||
if (needs.length) {
|
||||
return [...needs, ...getAllAncestors(needs, nodeDict)];
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
export const filterByAncestors = (links, nodeDict) =>
|
||||
links.filter(({ target, source }) => {
|
||||
/*
|
||||
|
||||
for every link, check out it's target
|
||||
for every target, get the target node's needs
|
||||
then drop the current link source from that list
|
||||
|
||||
call a function to get all ancestors, recursively
|
||||
is the current link's source in the list of all parents?
|
||||
then we drop this link
|
||||
|
||||
*/
|
||||
const targetNode = target;
|
||||
const targetNodeNeeds = nodeDict[targetNode].needs;
|
||||
const targetNodeNeedsMinusSource = targetNodeNeeds.filter(need => need !== source);
|
||||
|
||||
const allAncestors = getAllAncestors(targetNodeNeedsMinusSource, nodeDict);
|
||||
return !allAncestors.includes(source);
|
||||
});
|
||||
|
||||
export const parseData = data => {
|
||||
const { nodes, nodeDict } = createNodesStructure(data);
|
||||
const allLinks = makeLinksFromNodes(nodes, nodeDict);
|
||||
const filteredLinks = filterByAncestors(allLinks, nodeDict);
|
||||
const links = uniqWith(filteredLinks, isEqual);
|
||||
|
||||
return { nodes, links };
|
||||
};
|
||||
|
||||
/*
|
||||
createSankey calls the d3 layout to generate the relationships and positioning
|
||||
values for the nodes and links in the graph.
|
||||
*/
|
||||
|
||||
export const createSankey = ({ width, height, nodeWidth, nodePadding, paddingForLabels }) => {
|
||||
const sankeyGenerator = sankey()
|
||||
.nodeId(({ name }) => name)
|
||||
.nodeAlign(sankeyLeft)
|
||||
.nodeWidth(nodeWidth)
|
||||
.nodePadding(nodePadding)
|
||||
.extent([
|
||||
[paddingForLabels, paddingForLabels],
|
||||
[width - paddingForLabels, height - paddingForLabels],
|
||||
]);
|
||||
return ({ nodes, links }) =>
|
||||
sankeyGenerator({
|
||||
nodes: nodes.map(d => ({ ...d })),
|
||||
links: links.map(d => ({ ...d })),
|
||||
});
|
||||
};
|
||||
|
||||
/*
|
||||
The number of nodes in the most populous generation drives the height of the graph.
|
||||
*/
|
||||
|
||||
export const getMaxNodes = nodes => {
|
||||
const counts = nodes.reduce((acc, { layer }) => {
|
||||
if (!acc[layer]) {
|
||||
acc[layer] = 0;
|
||||
}
|
||||
|
||||
acc[layer] += 1;
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
return Math.max(...counts);
|
||||
};
|
||||
|
||||
/*
|
||||
Because we cannot know if a node is part of a relationship until after we
|
||||
generate the links with createSankey, this function is used after the first call
|
||||
to find nodes that have no relations.
|
||||
*/
|
||||
|
||||
export const removeOrphanNodes = sankeyfiedNodes => {
|
||||
return sankeyfiedNodes.filter(node => node.sourceLinks.length || node.targetLinks.length);
|
||||
};
|
|
@ -23,8 +23,7 @@ module EnforcesTwoFactorAuthentication
|
|||
|
||||
def two_factor_authentication_required?
|
||||
Gitlab::CurrentSettings.require_two_factor_authentication? ||
|
||||
current_user.try(:require_two_factor_authentication_from_group?) ||
|
||||
current_user.try(:ultraauth_user?)
|
||||
current_user.try(:require_two_factor_authentication_from_group?)
|
||||
end
|
||||
|
||||
def current_user_requires_two_factor?
|
||||
|
|
|
@ -954,11 +954,11 @@ class User < ApplicationRecord
|
|||
end
|
||||
|
||||
def allow_password_authentication_for_web?
|
||||
Gitlab::CurrentSettings.password_authentication_enabled_for_web? && !ldap_user? && !ultraauth_user?
|
||||
Gitlab::CurrentSettings.password_authentication_enabled_for_web? && !ldap_user?
|
||||
end
|
||||
|
||||
def allow_password_authentication_for_git?
|
||||
Gitlab::CurrentSettings.password_authentication_enabled_for_git? && !ldap_user? && !ultraauth_user?
|
||||
Gitlab::CurrentSettings.password_authentication_enabled_for_git? && !ldap_user?
|
||||
end
|
||||
|
||||
def can_change_username?
|
||||
|
@ -1046,14 +1046,6 @@ class User < ApplicationRecord
|
|||
end
|
||||
end
|
||||
|
||||
def ultraauth_user?
|
||||
if identities.loaded?
|
||||
identities.find { |identity| Gitlab::Auth::OAuth::Provider.ultraauth_provider?(identity.provider) && !identity.extern_uid.nil? }
|
||||
else
|
||||
identities.exists?(["provider = ? AND extern_uid IS NOT NULL", "ultraauth"])
|
||||
end
|
||||
end
|
||||
|
||||
def ldap_identity
|
||||
@ldap_identity ||= identities.find_by(["provider LIKE ?", "ldap%"])
|
||||
end
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Removed UltraAuth integration for OmniAuth
|
||||
merge_request: 29330
|
||||
author: Kartikey Tanna
|
||||
type: removed
|
|
@ -0,0 +1,18 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class RemoveUltraauthProviderFromIdentities < ActiveRecord::Migration[6.0]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
DOWNTIME = false
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
add_concurrent_index :identities, :provider
|
||||
execute "DELETE FROM identities WHERE provider = 'ultraauth'"
|
||||
remove_concurrent_index :identities, :provider
|
||||
end
|
||||
|
||||
def down
|
||||
end
|
||||
end
|
|
@ -13883,6 +13883,7 @@ COPY "schema_migrations" (version) FROM STDIN;
|
|||
20200506125731
|
||||
20200506154421
|
||||
20200507221434
|
||||
20200508021128
|
||||
20200508050301
|
||||
20200508091106
|
||||
20200511080113
|
||||
|
|
|
@ -32,4 +32,3 @@ providers:
|
|||
- [Shibboleth](../../integration/shibboleth.md)
|
||||
- [Smartcard](smartcard.md) **(PREMIUM ONLY)**
|
||||
- [Twitter](../../integration/twitter.md)
|
||||
- [UltraAuth](../../integration/ultra_auth.md)
|
||||
|
|
|
@ -2,7 +2,10 @@
|
|||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/9566) in [GitLab Premium](https://about.gitlab.com/pricing/) 12.5.
|
||||
|
||||
The API for creating, updating, reading and deleting [Feature Flag Specs](../user/project/operations/feature_flags.md#define-environment-specs).
|
||||
CAUTION: **Deprecation**
|
||||
This API is deprecated and [scheduled for removal in GitLab 14.0](https://gitlab.com/gitlab-org/gitlab/-/issues/213369).
|
||||
|
||||
The API for creating, updating, reading and deleting Feature Flag Specs.
|
||||
Automation engineers benefit from this API by being able to modify Feature Flag Specs without accessing user interface.
|
||||
To manage the [Feature Flag](../user/project/operations/feature_flags.md) resources via public API, please refer to the [Feature Flags API](feature_flags.md) document.
|
||||
|
||||
|
|
|
@ -2,6 +2,9 @@
|
|||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/9566) in [GitLab Premium](https://about.gitlab.com/pricing/) 12.5.
|
||||
|
||||
NOTE: **Note**
|
||||
This API is behind a [feature flag](../user/project/operations/feature_flags.md#feature-flag-behavior-change-in-130). If this flag is not enabled in your environment, you can use the [legacy feature flags API](feature_flags_legacy.md).
|
||||
|
||||
API for accessing resources of [GitLab Feature Flags](../user/project/operations/feature_flags.md).
|
||||
|
||||
Users with Developer or higher [permissions](../user/permissions.md) can access Feature Flag API.
|
||||
|
@ -35,187 +38,51 @@ Example response:
|
|||
{
|
||||
"name":"merge_train",
|
||||
"description":"This feature is about merge train",
|
||||
"version": "new_version_flag",
|
||||
"created_at":"2019-11-04T08:13:51.423Z",
|
||||
"updated_at":"2019-11-04T08:13:51.423Z",
|
||||
"scopes":[
|
||||
{
|
||||
"id":82,
|
||||
"active":false,
|
||||
"environment_scope":"*",
|
||||
"strategies":[
|
||||
{
|
||||
"name":"default",
|
||||
"parameters":{
|
||||
|
||||
}
|
||||
}
|
||||
],
|
||||
"created_at":"2019-11-04T08:13:51.425Z",
|
||||
"updated_at":"2019-11-04T08:13:51.425Z"
|
||||
},
|
||||
{
|
||||
"id":83,
|
||||
"active":true,
|
||||
"environment_scope":"review/*",
|
||||
"strategies":[
|
||||
{
|
||||
"name":"default",
|
||||
"parameters":{
|
||||
|
||||
}
|
||||
}
|
||||
],
|
||||
"created_at":"2019-11-04T08:13:51.427Z",
|
||||
"updated_at":"2019-11-04T08:13:51.427Z"
|
||||
},
|
||||
{
|
||||
"id":84,
|
||||
"active":false,
|
||||
"environment_scope":"production",
|
||||
"strategies":[
|
||||
{
|
||||
"name":"default",
|
||||
"parameters":{
|
||||
|
||||
}
|
||||
}
|
||||
],
|
||||
"created_at":"2019-11-04T08:13:51.428Z",
|
||||
"updated_at":"2019-11-04T08:13:51.428Z"
|
||||
}
|
||||
"scopes":[],
|
||||
"strategies": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "userWithId",
|
||||
"parameters": {
|
||||
"userIds": "user1"
|
||||
},
|
||||
"scopes": [
|
||||
{
|
||||
"id": 1,
|
||||
"environment_scope": "production"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name":"new_live_trace",
|
||||
"description":"This is a new live trace feature",
|
||||
"version": "new_version_flag",
|
||||
"created_at":"2019-11-04T08:13:10.507Z",
|
||||
"updated_at":"2019-11-04T08:13:10.507Z",
|
||||
"scopes":[
|
||||
{
|
||||
"id":79,
|
||||
"active":false,
|
||||
"environment_scope":"*",
|
||||
"strategies":[
|
||||
{
|
||||
"name":"default",
|
||||
"parameters":{
|
||||
|
||||
}
|
||||
}
|
||||
],
|
||||
"created_at":"2019-11-04T08:13:10.516Z",
|
||||
"updated_at":"2019-11-04T08:13:10.516Z"
|
||||
},
|
||||
{
|
||||
"id":80,
|
||||
"active":true,
|
||||
"environment_scope":"staging",
|
||||
"strategies":[
|
||||
{
|
||||
"name":"default",
|
||||
"parameters":{
|
||||
|
||||
}
|
||||
}
|
||||
],
|
||||
"created_at":"2019-11-04T08:13:10.525Z",
|
||||
"updated_at":"2019-11-04T08:13:10.525Z"
|
||||
},
|
||||
{
|
||||
"id":81,
|
||||
"active":false,
|
||||
"environment_scope":"production",
|
||||
"strategies":[
|
||||
{
|
||||
"name":"default",
|
||||
"parameters":{
|
||||
|
||||
}
|
||||
}
|
||||
],
|
||||
"created_at":"2019-11-04T08:13:10.527Z",
|
||||
"updated_at":"2019-11-04T08:13:10.527Z"
|
||||
}
|
||||
"scopes":[]
|
||||
"strategies": [
|
||||
{
|
||||
"id": 2,
|
||||
"name": "default",
|
||||
"parameters": {},
|
||||
"scopes": [
|
||||
{
|
||||
"id": 2,
|
||||
"environment_scope": "staging"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## New feature flag
|
||||
|
||||
Creates a new feature flag.
|
||||
|
||||
```plaintext
|
||||
POST /projects/:id/feature_flags
|
||||
```
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
| ------------------- | ---------------- | ---------- | ---------------------------------------------------------------------------------------|
|
||||
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding). |
|
||||
| `name` | string | yes | The name of the feature flag. |
|
||||
| `description` | string | no | The description of the feature flag. |
|
||||
| `scopes` | JSON | no | The [feature flag specs](../user/project/operations/feature_flags.md#define-environment-specs) of the feature flag. |
|
||||
| `scopes:environment_scope` | string | no | The [environment spec](../ci/environments/index.md#scoping-environments-with-specs). |
|
||||
| `scopes:active` | boolean | no | Whether the spec is active. |
|
||||
| `scopes:strategies` | JSON | no | The [strategies](../user/project/operations/feature_flags.md#feature-flag-strategies) of the feature flag spec. |
|
||||
|
||||
```shell
|
||||
curl https://gitlab.example.com/api/v4/projects/1/feature_flags \
|
||||
--header "PRIVATE-TOKEN: <your_access_token>" \
|
||||
--header "Content-type: application/json" \
|
||||
--data @- << EOF
|
||||
{
|
||||
"name": "awesome_feature",
|
||||
"scopes": [{ "environment_scope": "*", "active": false, "strategies": [{ "name": "default", "parameters": {} }] },
|
||||
{ "environment_scope": "production", "active": true, "strategies": [{ "name": "userWithId", "parameters": { "userIds": "1,2,3" } }] }]
|
||||
}
|
||||
EOF
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
||||
```json
|
||||
{
|
||||
"name":"awesome_feature",
|
||||
"description":null,
|
||||
"created_at":"2019-11-04T08:32:27.288Z",
|
||||
"updated_at":"2019-11-04T08:32:27.288Z",
|
||||
"scopes":[
|
||||
{
|
||||
"id":85,
|
||||
"active":false,
|
||||
"environment_scope":"*",
|
||||
"strategies":[
|
||||
{
|
||||
"name":"default",
|
||||
"parameters":{
|
||||
|
||||
}
|
||||
}
|
||||
],
|
||||
"created_at":"2019-11-04T08:32:29.324Z",
|
||||
"updated_at":"2019-11-04T08:32:29.324Z"
|
||||
},
|
||||
{
|
||||
"id":86,
|
||||
"active":true,
|
||||
"environment_scope":"production",
|
||||
"strategies":[
|
||||
{
|
||||
"name":"userWithId",
|
||||
"parameters":{
|
||||
"userIds":"1,2,3"
|
||||
}
|
||||
}
|
||||
],
|
||||
"created_at":"2019-11-04T08:32:29.328Z",
|
||||
"updated_at":"2019-11-04T08:32:29.328Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Single feature flag
|
||||
## Get a single feature flag
|
||||
|
||||
Gets a single feature flag.
|
||||
|
||||
|
@ -226,71 +93,170 @@ GET /projects/:id/feature_flags/:name
|
|||
| Attribute | Type | Required | Description |
|
||||
| ------------------- | ---------------- | ---------- | ---------------------------------------------------------------------------------------|
|
||||
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding). |
|
||||
| `name` | string | yes | The name of the feature flag. |
|
||||
| `name` | string | yes | The name of the feature flag. |
|
||||
|
||||
```shell
|
||||
curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/1/feature_flags/new_live_trace
|
||||
curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/1/feature_flags/awesome_feature
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
||||
```json
|
||||
{
|
||||
"name":"new_live_trace",
|
||||
"description":"This is a new live trace feature",
|
||||
"created_at":"2019-11-04T08:13:10.507Z",
|
||||
"updated_at":"2019-11-04T08:13:10.507Z",
|
||||
"scopes":[
|
||||
{
|
||||
"id":79,
|
||||
"active":false,
|
||||
"environment_scope":"*",
|
||||
"strategies":[
|
||||
{
|
||||
"name":"default",
|
||||
"parameters":{
|
||||
|
||||
}
|
||||
}
|
||||
],
|
||||
"created_at":"2019-11-04T08:13:10.516Z",
|
||||
"updated_at":"2019-11-04T08:13:10.516Z"
|
||||
},
|
||||
{
|
||||
"id":80,
|
||||
"active":true,
|
||||
"environment_scope":"staging",
|
||||
"strategies":[
|
||||
{
|
||||
"name":"default",
|
||||
"parameters":{
|
||||
|
||||
}
|
||||
}
|
||||
],
|
||||
"created_at":"2019-11-04T08:13:10.525Z",
|
||||
"updated_at":"2019-11-04T08:13:10.525Z"
|
||||
},
|
||||
{
|
||||
"id":81,
|
||||
"active":false,
|
||||
"environment_scope":"production",
|
||||
"strategies":[
|
||||
{
|
||||
"name":"default",
|
||||
"parameters":{
|
||||
|
||||
}
|
||||
}
|
||||
],
|
||||
"created_at":"2019-11-04T08:13:10.527Z",
|
||||
"updated_at":"2019-11-04T08:13:10.527Z"
|
||||
}
|
||||
]
|
||||
"name": "awesome_feature",
|
||||
"description": null,
|
||||
"version": "new_version_flag",
|
||||
"created_at": "2020-05-13T19:56:33.119Z",
|
||||
"updated_at": "2020-05-13T19:56:33.119Z",
|
||||
"scopes": [],
|
||||
"strategies": [
|
||||
{
|
||||
"id": 36,
|
||||
"name": "default",
|
||||
"parameters": {},
|
||||
"scopes": [
|
||||
{
|
||||
"id": 37,
|
||||
"environment_scope": "production"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Delete feature flag
|
||||
## Create a feature flag
|
||||
|
||||
Creates a new feature flag.
|
||||
|
||||
```plaintext
|
||||
POST /projects/:id/feature_flags
|
||||
```
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
| ------------------- | ---------------- | ---------- | ---------------------------------------------------------------------------------------|
|
||||
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding). |
|
||||
| `name` | string | yes | The name of the feature flag. |
|
||||
| `version` | string | yes | The version of the feature flag. Must be `new_version_flag`. Omit or set to `legacy_flag` to create a [Legacy Feature Flag](feature_flags_legacy.md). |
|
||||
| `description` | string | no | The description of the feature flag. |
|
||||
| `strategies` | JSON | no | The feature flag [strategies](../user/project/operations/feature_flags.md#feature-flag-strategies). |
|
||||
| `strategies:name` | JSON | no | The strategy name. |
|
||||
| `strategies:parameters` | JSON | no | The strategy parameters. |
|
||||
| `strategies:scopes` | JSON | no | The scopes for the strategy. |
|
||||
| `strategies:scopes:environment_scope` | string | no | The environment spec for the scope. |
|
||||
|
||||
```shell
|
||||
curl https://gitlab.example.com/api/v4/projects/1/feature_flags \
|
||||
--header "PRIVATE-TOKEN: <your_access_token>" \
|
||||
--header "Content-type: application/json" \
|
||||
--data @- << EOF
|
||||
{
|
||||
"name": "awesome_feature",
|
||||
"version": "new_version_flag",
|
||||
"strategies": [{ "name": "default", "parameters": {}, "scopes": [{ "environment_scope": "production" }] }]
|
||||
}
|
||||
EOF
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "awesome_feature",
|
||||
"description": null,
|
||||
"version": "new_version_flag",
|
||||
"created_at": "2020-05-13T19:56:33.119Z",
|
||||
"updated_at": "2020-05-13T19:56:33.119Z",
|
||||
"scopes": [],
|
||||
"strategies": [
|
||||
{
|
||||
"id": 36,
|
||||
"name": "default",
|
||||
"parameters": {},
|
||||
"scopes": [
|
||||
{
|
||||
"id": 37,
|
||||
"environment_scope": "production"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Update a feature flag
|
||||
|
||||
Updates a feature flag.
|
||||
|
||||
```plaintext
|
||||
PUT /projects/:id/feature_flags/:name
|
||||
```
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
| ------------------- | ---------------- | ---------- | ---------------------------------------------------------------------------------------|
|
||||
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding). |
|
||||
| `name` | string | yes | The name of the feature flag. |
|
||||
| `description` | string | no | The description of the feature flag. |
|
||||
| `strategies` | JSON | no | The feature flag [strategies](../user/project/operations/feature_flags.md#feature-flag-strategies). |
|
||||
| `strategies:id` | JSON | no | The feature flag strategy id. |
|
||||
| `strategies:name` | JSON | no | The strategy name. |
|
||||
| `strategies:parameters` | JSON | no | The strategy parameters. |
|
||||
| `strategies:scopes` | JSON | no | The scopes for the strategy. |
|
||||
| `strategies:scopes:id` | JSON | no | The scopes id. |
|
||||
| `strategies:scopes:environment_scope` | string | no | The environment spec for the scope. |
|
||||
|
||||
```shell
|
||||
curl https://gitlab.example.com/api/v4/projects/1/feature_flags/awesome_feature \
|
||||
--header "PRIVATE-TOKEN: <your_access_token>" \
|
||||
--header "Content-type: application/json" \
|
||||
--data @- << EOF
|
||||
{
|
||||
"strategies": [{ "name": "gradualRolloutUserId", "parameters": { "groupId": "default", "percentage": "25" }, "scopes": [{ "environment_scope": "staging" }] }]
|
||||
}
|
||||
EOF
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "awesome_feature",
|
||||
"description": null,
|
||||
"version": "new_version_flag",
|
||||
"created_at": "2020-05-13T20:10:32.891Z",
|
||||
"updated_at": "2020-05-13T20:10:32.891Z",
|
||||
"scopes": [],
|
||||
"strategies": [
|
||||
{
|
||||
"id": 38,
|
||||
"name": "gradualRolloutUserId",
|
||||
"parameters": {
|
||||
"groupId": "default",
|
||||
"percentage": "25"
|
||||
},
|
||||
"scopes": [
|
||||
{
|
||||
"id": 40,
|
||||
"environment_scope": "staging"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 37,
|
||||
"name": "default",
|
||||
"parameters": {},
|
||||
"scopes": [
|
||||
{
|
||||
"id": 39,
|
||||
"environment_scope": "production"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Delete a feature flag
|
||||
|
||||
Deletes a feature flag.
|
||||
|
||||
|
|
|
@ -0,0 +1,311 @@
|
|||
# Legacy Feature Flags API **(PREMIUM)**
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/9566) in [GitLab Premium](https://about.gitlab.com/pricing/) 12.5.
|
||||
|
||||
CAUTION: **Deprecation**
|
||||
This API is deprecated and [scheduled for removal in GitLab 14.0](https://gitlab.com/gitlab-org/gitlab/-/issues/213369). Use [this API](feature_flags.md) instead.
|
||||
|
||||
API for accessing resources of [GitLab Feature Flags](../user/project/operations/feature_flags.md).
|
||||
|
||||
Users with Developer or higher [permissions](../user/permissions.md) can access Feature Flag API.
|
||||
|
||||
## Feature Flags pagination
|
||||
|
||||
By default, `GET` requests return 20 results at a time because the API results
|
||||
are [paginated](README.md#pagination).
|
||||
|
||||
## List feature flags for a project
|
||||
|
||||
Gets all feature flags of the requested project.
|
||||
|
||||
```plaintext
|
||||
GET /projects/:id/feature_flags
|
||||
```
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
| ------------------- | ---------------- | ---------- | --------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding). |
|
||||
| `scope` | string | no | The condition of feature flags, one of: `enabled`, `disabled`. |
|
||||
|
||||
```shell
|
||||
curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/1/feature_flags
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"name":"merge_train",
|
||||
"description":"This feature is about merge train",
|
||||
"created_at":"2019-11-04T08:13:51.423Z",
|
||||
"updated_at":"2019-11-04T08:13:51.423Z",
|
||||
"scopes":[
|
||||
{
|
||||
"id":82,
|
||||
"active":false,
|
||||
"environment_scope":"*",
|
||||
"strategies":[
|
||||
{
|
||||
"name":"default",
|
||||
"parameters":{
|
||||
|
||||
}
|
||||
}
|
||||
],
|
||||
"created_at":"2019-11-04T08:13:51.425Z",
|
||||
"updated_at":"2019-11-04T08:13:51.425Z"
|
||||
},
|
||||
{
|
||||
"id":83,
|
||||
"active":true,
|
||||
"environment_scope":"review/*",
|
||||
"strategies":[
|
||||
{
|
||||
"name":"default",
|
||||
"parameters":{
|
||||
|
||||
}
|
||||
}
|
||||
],
|
||||
"created_at":"2019-11-04T08:13:51.427Z",
|
||||
"updated_at":"2019-11-04T08:13:51.427Z"
|
||||
},
|
||||
{
|
||||
"id":84,
|
||||
"active":false,
|
||||
"environment_scope":"production",
|
||||
"strategies":[
|
||||
{
|
||||
"name":"default",
|
||||
"parameters":{
|
||||
|
||||
}
|
||||
}
|
||||
],
|
||||
"created_at":"2019-11-04T08:13:51.428Z",
|
||||
"updated_at":"2019-11-04T08:13:51.428Z"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name":"new_live_trace",
|
||||
"description":"This is a new live trace feature",
|
||||
"created_at":"2019-11-04T08:13:10.507Z",
|
||||
"updated_at":"2019-11-04T08:13:10.507Z",
|
||||
"scopes":[
|
||||
{
|
||||
"id":79,
|
||||
"active":false,
|
||||
"environment_scope":"*",
|
||||
"strategies":[
|
||||
{
|
||||
"name":"default",
|
||||
"parameters":{
|
||||
|
||||
}
|
||||
}
|
||||
],
|
||||
"created_at":"2019-11-04T08:13:10.516Z",
|
||||
"updated_at":"2019-11-04T08:13:10.516Z"
|
||||
},
|
||||
{
|
||||
"id":80,
|
||||
"active":true,
|
||||
"environment_scope":"staging",
|
||||
"strategies":[
|
||||
{
|
||||
"name":"default",
|
||||
"parameters":{
|
||||
|
||||
}
|
||||
}
|
||||
],
|
||||
"created_at":"2019-11-04T08:13:10.525Z",
|
||||
"updated_at":"2019-11-04T08:13:10.525Z"
|
||||
},
|
||||
{
|
||||
"id":81,
|
||||
"active":false,
|
||||
"environment_scope":"production",
|
||||
"strategies":[
|
||||
{
|
||||
"name":"default",
|
||||
"parameters":{
|
||||
|
||||
}
|
||||
}
|
||||
],
|
||||
"created_at":"2019-11-04T08:13:10.527Z",
|
||||
"updated_at":"2019-11-04T08:13:10.527Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## New feature flag
|
||||
|
||||
Creates a new feature flag.
|
||||
|
||||
```plaintext
|
||||
POST /projects/:id/feature_flags
|
||||
```
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
| ------------------- | ---------------- | ---------- | ---------------------------------------------------------------------------------------|
|
||||
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding). |
|
||||
| `name` | string | yes | The name of the feature flag. |
|
||||
| `description` | string | no | The description of the feature flag. |
|
||||
| `scopes` | JSON | no | The feature flag specs of the feature flag. |
|
||||
| `scopes:environment_scope` | string | no | The environment spec. |
|
||||
| `scopes:active` | boolean | no | Whether the spec is active. |
|
||||
| `scopes:strategies` | JSON | no | The [strategies](../user/project/operations/feature_flags.md#feature-flag-strategies) of the feature flag spec. |
|
||||
|
||||
```shell
|
||||
curl https://gitlab.example.com/api/v4/projects/1/feature_flags \
|
||||
--header "PRIVATE-TOKEN: <your_access_token>" \
|
||||
--header "Content-type: application/json" \
|
||||
--data @- << EOF
|
||||
{
|
||||
"name": "awesome_feature",
|
||||
"scopes": [{ "environment_scope": "*", "active": false, "strategies": [{ "name": "default", "parameters": {} }] },
|
||||
{ "environment_scope": "production", "active": true, "strategies": [{ "name": "userWithId", "parameters": { "userIds": "1,2,3" } }] }]
|
||||
}
|
||||
EOF
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
||||
```json
|
||||
{
|
||||
"name":"awesome_feature",
|
||||
"description":null,
|
||||
"created_at":"2019-11-04T08:32:27.288Z",
|
||||
"updated_at":"2019-11-04T08:32:27.288Z",
|
||||
"scopes":[
|
||||
{
|
||||
"id":85,
|
||||
"active":false,
|
||||
"environment_scope":"*",
|
||||
"strategies":[
|
||||
{
|
||||
"name":"default",
|
||||
"parameters":{
|
||||
|
||||
}
|
||||
}
|
||||
],
|
||||
"created_at":"2019-11-04T08:32:29.324Z",
|
||||
"updated_at":"2019-11-04T08:32:29.324Z"
|
||||
},
|
||||
{
|
||||
"id":86,
|
||||
"active":true,
|
||||
"environment_scope":"production",
|
||||
"strategies":[
|
||||
{
|
||||
"name":"userWithId",
|
||||
"parameters":{
|
||||
"userIds":"1,2,3"
|
||||
}
|
||||
}
|
||||
],
|
||||
"created_at":"2019-11-04T08:32:29.328Z",
|
||||
"updated_at":"2019-11-04T08:32:29.328Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Single feature flag
|
||||
|
||||
Gets a single feature flag.
|
||||
|
||||
```plaintext
|
||||
GET /projects/:id/feature_flags/:name
|
||||
```
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
| ------------------- | ---------------- | ---------- | ---------------------------------------------------------------------------------------|
|
||||
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding). |
|
||||
| `name` | string | yes | The name of the feature flag. |
|
||||
|
||||
```shell
|
||||
curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/1/feature_flags/new_live_trace
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
||||
```json
|
||||
{
|
||||
"name":"new_live_trace",
|
||||
"description":"This is a new live trace feature",
|
||||
"created_at":"2019-11-04T08:13:10.507Z",
|
||||
"updated_at":"2019-11-04T08:13:10.507Z",
|
||||
"scopes":[
|
||||
{
|
||||
"id":79,
|
||||
"active":false,
|
||||
"environment_scope":"*",
|
||||
"strategies":[
|
||||
{
|
||||
"name":"default",
|
||||
"parameters":{
|
||||
|
||||
}
|
||||
}
|
||||
],
|
||||
"created_at":"2019-11-04T08:13:10.516Z",
|
||||
"updated_at":"2019-11-04T08:13:10.516Z"
|
||||
},
|
||||
{
|
||||
"id":80,
|
||||
"active":true,
|
||||
"environment_scope":"staging",
|
||||
"strategies":[
|
||||
{
|
||||
"name":"default",
|
||||
"parameters":{
|
||||
|
||||
}
|
||||
}
|
||||
],
|
||||
"created_at":"2019-11-04T08:13:10.525Z",
|
||||
"updated_at":"2019-11-04T08:13:10.525Z"
|
||||
},
|
||||
{
|
||||
"id":81,
|
||||
"active":false,
|
||||
"environment_scope":"production",
|
||||
"strategies":[
|
||||
{
|
||||
"name":"default",
|
||||
"parameters":{
|
||||
|
||||
}
|
||||
}
|
||||
],
|
||||
"created_at":"2019-11-04T08:13:10.527Z",
|
||||
"updated_at":"2019-11-04T08:13:10.527Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Delete feature flag
|
||||
|
||||
Deletes a feature flag.
|
||||
|
||||
```plaintext
|
||||
DELETE /projects/:id/feature_flags/:name
|
||||
```
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
| ------------------- | ---------------- | ---------- | ---------------------------------------------------------------------------------------|
|
||||
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding). |
|
||||
| `name` | string | yes | The name of the feature flag. |
|
||||
|
||||
```shell
|
||||
curl --header "PRIVATE-TOKEN: <your_access_token>" --request DELETE https://gitlab.example.com/api/v4/projects/1/feature_flags/awesome_feature
|
||||
```
|
Binary file not shown.
Before Width: | Height: | Size: 52 KiB |
Binary file not shown.
Before Width: | Height: | Size: 38 KiB |
Binary file not shown.
Before Width: | Height: | Size: 67 KiB |
|
@ -34,7 +34,6 @@ contains some settings that are common for all providers.
|
|||
- [OAuth2Generic](oauth2_generic.md)
|
||||
- [JWT](../administration/auth/jwt.md)
|
||||
- [OpenID Connect](../administration/auth/oidc.md)
|
||||
- [UltraAuth](ultra_auth.md)
|
||||
- [Salesforce](salesforce.md)
|
||||
- [AWS Cognito](../administration/auth/cognito.md)
|
||||
|
||||
|
|
|
@ -1,87 +0,0 @@
|
|||
# UltraAuth OmniAuth Provider
|
||||
|
||||
You can integrate your GitLab instance with [UltraAuth](https://github.com/ultraauth) to enable users to perform secure biometric authentication to your GitLab instance with your UltraAuth account. Users have to perform the biometric authentication using their mobile device with fingerprint sensor.
|
||||
|
||||
## Create UltraAuth Application
|
||||
|
||||
To enable UltraAuth OmniAuth provider, you must use UltraAuth's credentials for your GitLab instance.
|
||||
To get the credentials (a pair of Client ID and Client Secret), you must register an application on UltraAuth.
|
||||
|
||||
1. Sign in to [UltraAuth](https://app.ultraauth.com).
|
||||
1. Navigate to **Create an App** and click on **Ruby on Rails**.
|
||||
1. Scroll down the page that is displayed to locate the **Client ID** and **Client Secret**.
|
||||
Keep this page open as you continue configuration.
|
||||
|
||||
![UltraAuth Credentials: OPENID_CLIENT_ID and OPENID_CLIENT_SECRET](img/ultra_auth_credentials.png)
|
||||
|
||||
1. Click on "Edit Callback URL" link.
|
||||
|
||||
![Edit UltraAuth Callback URL](img/ultra_auth_edit_callback_url_highlighted.png)
|
||||
|
||||
1. The callback URL will be `http(s)://<your_domain>/users/auth/ultraauth/callback`
|
||||
|
||||
![UltraAuth Callback URL](img/ultra_auth_edit_callback_url.png)
|
||||
|
||||
1. Select **Register application**.
|
||||
1. On your GitLab server, open the configuration file.
|
||||
|
||||
For Omnibus package:
|
||||
|
||||
```shell
|
||||
sudo editor /etc/gitlab/gitlab.rb
|
||||
```
|
||||
|
||||
For installations from source:
|
||||
|
||||
```shell
|
||||
cd /home/git/gitlab
|
||||
sudo -u git -H editor config/gitlab.yml
|
||||
```
|
||||
|
||||
1. See [Initial OmniAuth Configuration](omniauth.md#initial-omniauth-configuration) for initial settings.
|
||||
1. Add the provider configuration:
|
||||
|
||||
For Omnibus package:
|
||||
|
||||
```ruby
|
||||
gitlab_rails['omniauth_providers'] = [
|
||||
{
|
||||
"name" => "ultraauth",
|
||||
"app_id" => "OPENID_CLIENT_ID",
|
||||
"app_secret" => "OPENID_CLIENT_SECRET",
|
||||
"args" => {
|
||||
"client_options" => {
|
||||
"redirect_uri" => "https://example.com/users/auth/ultraauth/callback"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
For installation from source:
|
||||
|
||||
```yaml
|
||||
- { name: 'ultraauth',
|
||||
app_id: 'OPENID_CLIENT_ID',
|
||||
app_secret: 'OPENID_CLIENT_SECRET',
|
||||
args: {
|
||||
client_options: {
|
||||
redirect_uri: 'https://example.com/users/auth/ultraauth/callback'
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
__Replace `https://example.com/users/auth/ultraauth/callback` with your application's Callback URL.__
|
||||
|
||||
1. Change `OPENID_CLIENT_ID` to the Client ID from the UltraAuth application page.
|
||||
1. Change `OPENID_CLIENT_SECRET` to the Client Secret from the UltraAuth application page.
|
||||
1. Save the configuration file.
|
||||
1. [Reconfigure GitLab](../administration/restart_gitlab.md#omnibus-gitlab-reconfigure) or [restart GitLab](../administration/restart_gitlab.md#installations-from-source) for the changes to take effect if you
|
||||
installed GitLab via Omnibus or from source respectively.
|
||||
|
||||
On the sign in page, there should now be an UltraAuth icon below the regular sign in form.
|
||||
Click the icon to begin the authentication process. UltraAuth will ask the user to sign in and authorize the GitLab application.
|
||||
If everything goes well, the user will be returned to GitLab and will be signed in.
|
||||
|
||||
GitLab requires the email address of each new user. Once the user is logged in using UltraAuth, GitLab will redirect the user to the profile page where they will have to provide the email and verify the email. Password authentication will be disabled for UltraAuth users and two-factor authentication (2FA) will be enforced.
|
|
@ -456,7 +456,7 @@ DAST can be [configured](#customizing-the-dast-settings) using environment varia
|
|||
| `DAST_FULL_SCAN_DOMAIN_VALIDATION_REQUIRED` | no | Requires [domain validation](#domain-validation) when running DAST full scans. Boolean. `true`, `True`, or `1` are considered as true value, otherwise false. Defaults to `false`. Not supported for API scans. |
|
||||
| `DAST_AUTO_UPDATE_ADDONS` | no | By default the versions of ZAP add-ons are pinned to those provided with the DAST image. Set to `true` to allow ZAP to download the latest versions. |
|
||||
| `DAST_API_HOST_OVERRIDE` | no | Used to override domains defined in API specification files. |
|
||||
| `DAST_EXCLUDE_RULES` | no | Set to a comma-separated list of Vulnerability Rule IDs to exclude them from scans. Rule IDs are numbers and can be found from the DAST log or on the [ZAP project](https://github.com/zaproxy/zaproxy/blob/master/docs/scanners.md). For example, `HTTP Parameter Override` has a rule ID of `10026`. |
|
||||
| `DAST_EXCLUDE_RULES` | no | Set to a comma-separated list of Vulnerability Rule IDs to exclude them from the scan report. Currently, excluded rules will get executed but the alerts from them will be suppressed. Rule IDs are numbers and can be found from the DAST log or on the [ZAP project](https://github.com/zaproxy/zaproxy/blob/develop/docs/scanners.md). For example, `HTTP Parameter Override` has a rule ID of `10026`. |
|
||||
| `DAST_REQUEST_HEADERS` | no | Set to a comma-separated list of request header names and values. For example, `Cache-control: no-cache,User-Agent: DAST/1.0` |
|
||||
| `DAST_ZAP_USE_AJAX_SPIDER` | no | Use the AJAX spider in addition to the traditional spider, useful for crawling sites that require JavaScript. Boolean. `true`, `True`, or `1` are considered as true value, otherwise false. Defaults to `false`. |
|
||||
|
||||
|
|
|
@ -311,6 +311,6 @@ end
|
|||
You can create, update, read, and delete Feature Flags via API
|
||||
to control them in an automated flow:
|
||||
|
||||
- [Feature Flags API](../../../api/feature_flags.md)
|
||||
- [Legacy Feature Flags API](../../../api/feature_flags_legacy.md)
|
||||
- [Feature Flag Specs API](../../../api/feature_flag_specs.md)
|
||||
- [Feature Flag User Lists API](../../../api/feature_flag_user_lists.md)
|
||||
|
|
|
@ -41,10 +41,6 @@ module Gitlab
|
|||
name.to_s.start_with?('ldap')
|
||||
end
|
||||
|
||||
def self.ultraauth_provider?(name)
|
||||
name.to_s.eql?('ultraauth')
|
||||
end
|
||||
|
||||
def self.sync_profile_from_provider?(provider)
|
||||
return true if ldap_provider?(provider)
|
||||
|
||||
|
|
|
@ -71,6 +71,7 @@
|
|||
"core-js": "^3.6.4",
|
||||
"cropper": "^2.3.0",
|
||||
"css-loader": "^2.1.1",
|
||||
"d3-sankey": "^0.12.3",
|
||||
"d3-scale": "^2.2.2",
|
||||
"d3-selection": "^1.2.0",
|
||||
"dateformat": "^3.0.3",
|
||||
|
|
|
@ -310,13 +310,6 @@ describe ApplicationController do
|
|||
|
||||
expect(subject).to be_truthy
|
||||
end
|
||||
|
||||
it 'returns true if user has signed up using omniauth-ultraauth' do
|
||||
user = create(:omniauth_user, provider: 'ultraauth')
|
||||
allow(controller).to receive(:current_user).and_return(user)
|
||||
|
||||
expect(subject).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
describe '#two_factor_grace_period' do
|
||||
|
|
|
@ -0,0 +1,662 @@
|
|||
import DropDown from '~/droplab/drop_down';
|
||||
import utils from '~/droplab/utils';
|
||||
import { SELECTED_CLASS } from '~/droplab/constants';
|
||||
|
||||
describe('DropLab DropDown', () => {
|
||||
let testContext;
|
||||
|
||||
beforeEach(() => {
|
||||
testContext = {};
|
||||
});
|
||||
|
||||
describe('class constructor', () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(DropDown.prototype, 'getItems').mockImplementation(() => {});
|
||||
jest.spyOn(DropDown.prototype, 'initTemplateString').mockImplementation(() => {});
|
||||
jest.spyOn(DropDown.prototype, 'addEvents').mockImplementation(() => {});
|
||||
|
||||
testContext.list = { innerHTML: 'innerHTML' };
|
||||
testContext.dropdown = new DropDown(testContext.list);
|
||||
});
|
||||
|
||||
it('sets the .hidden property to true', () => {
|
||||
expect(testContext.dropdown.hidden).toBe(true);
|
||||
});
|
||||
|
||||
it('sets the .list property', () => {
|
||||
expect(testContext.dropdown.list).toBe(testContext.list);
|
||||
});
|
||||
|
||||
it('calls .getItems', () => {
|
||||
expect(DropDown.prototype.getItems).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls .initTemplateString', () => {
|
||||
expect(DropDown.prototype.initTemplateString).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls .addEvents', () => {
|
||||
expect(DropDown.prototype.addEvents).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('sets the .initialState property to the .list.innerHTML', () => {
|
||||
expect(testContext.dropdown.initialState).toBe(testContext.list.innerHTML);
|
||||
});
|
||||
|
||||
describe('if the list argument is a string', () => {
|
||||
beforeEach(() => {
|
||||
testContext.element = {};
|
||||
testContext.selector = '.selector';
|
||||
|
||||
jest.spyOn(Document.prototype, 'querySelector').mockReturnValue(testContext.element);
|
||||
|
||||
testContext.dropdown = new DropDown(testContext.selector);
|
||||
});
|
||||
|
||||
it('calls .querySelector with the selector string', () => {
|
||||
expect(Document.prototype.querySelector).toHaveBeenCalledWith(testContext.selector);
|
||||
});
|
||||
|
||||
it('sets the .list property element', () => {
|
||||
expect(testContext.dropdown.list).toBe(testContext.element);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getItems', () => {
|
||||
beforeEach(() => {
|
||||
testContext.list = { querySelectorAll: () => {} };
|
||||
testContext.dropdown = { list: testContext.list };
|
||||
testContext.nodeList = [];
|
||||
|
||||
jest.spyOn(testContext.list, 'querySelectorAll').mockReturnValue(testContext.nodeList);
|
||||
|
||||
testContext.getItems = DropDown.prototype.getItems.call(testContext.dropdown);
|
||||
});
|
||||
|
||||
it('calls .querySelectorAll with a list item query', () => {
|
||||
expect(testContext.list.querySelectorAll).toHaveBeenCalledWith('li');
|
||||
});
|
||||
|
||||
it('sets the .items property to the returned list items', () => {
|
||||
expect(testContext.dropdown.items).toEqual(expect.any(Array));
|
||||
});
|
||||
|
||||
it('returns the .items', () => {
|
||||
expect(testContext.getItems).toEqual(expect.any(Array));
|
||||
});
|
||||
});
|
||||
|
||||
describe('initTemplateString', () => {
|
||||
beforeEach(() => {
|
||||
testContext.items = [{ outerHTML: '<a></a>' }, { outerHTML: '<img>' }];
|
||||
testContext.dropdown = { items: testContext.items };
|
||||
|
||||
DropDown.prototype.initTemplateString.call(testContext.dropdown);
|
||||
});
|
||||
|
||||
it('should set .templateString to the last items .outerHTML', () => {
|
||||
expect(testContext.dropdown.templateString).toBe(testContext.items[1].outerHTML);
|
||||
});
|
||||
|
||||
it('should not set .templateString to a non-last items .outerHTML', () => {
|
||||
expect(testContext.dropdown.templateString).not.toBe(testContext.items[0].outerHTML);
|
||||
});
|
||||
|
||||
describe('if .items is not set', () => {
|
||||
beforeEach(() => {
|
||||
testContext.dropdown = { getItems: () => {} };
|
||||
|
||||
jest.spyOn(testContext.dropdown, 'getItems').mockReturnValue([]);
|
||||
|
||||
DropDown.prototype.initTemplateString.call(testContext.dropdown);
|
||||
});
|
||||
|
||||
it('should call .getItems', () => {
|
||||
expect(testContext.dropdown.getItems).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('if items array is empty', () => {
|
||||
beforeEach(() => {
|
||||
testContext.dropdown = { items: [] };
|
||||
|
||||
DropDown.prototype.initTemplateString.call(testContext.dropdown);
|
||||
});
|
||||
|
||||
it('should set .templateString to an empty string', () => {
|
||||
expect(testContext.dropdown.templateString).toBe('');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('clickEvent', () => {
|
||||
beforeEach(() => {
|
||||
testContext.classList = {
|
||||
contains: jest.fn(),
|
||||
};
|
||||
testContext.list = { dispatchEvent: () => {} };
|
||||
testContext.dropdown = {
|
||||
hideOnClick: true,
|
||||
hide: () => {},
|
||||
list: testContext.list,
|
||||
addSelectedClass: () => {},
|
||||
};
|
||||
testContext.event = {
|
||||
preventDefault: () => {},
|
||||
target: {
|
||||
classList: testContext.classList,
|
||||
closest: () => null,
|
||||
},
|
||||
};
|
||||
|
||||
testContext.dummyListItem = document.createElement('li');
|
||||
jest.spyOn(testContext.event.target, 'closest').mockImplementation(selector => {
|
||||
if (selector === 'li') {
|
||||
return testContext.dummyListItem;
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
jest.spyOn(testContext.dropdown, 'hide').mockImplementation(() => {});
|
||||
jest.spyOn(testContext.dropdown, 'addSelectedClass').mockImplementation(() => {});
|
||||
jest.spyOn(testContext.list, 'dispatchEvent').mockImplementation(() => {});
|
||||
jest.spyOn(testContext.event, 'preventDefault').mockImplementation(() => {});
|
||||
window.CustomEvent = jest.fn();
|
||||
testContext.classList.contains.mockReturnValue(false);
|
||||
});
|
||||
|
||||
describe('normal click event', () => {
|
||||
beforeEach(() => {
|
||||
DropDown.prototype.clickEvent.call(testContext.dropdown, testContext.event);
|
||||
});
|
||||
it('should call event.target.closest', () => {
|
||||
expect(testContext.event.target.closest).toHaveBeenCalledWith('.droplab-item-ignore');
|
||||
expect(testContext.event.target.closest).toHaveBeenCalledWith('li');
|
||||
});
|
||||
|
||||
it('should call addSelectedClass', () => {
|
||||
expect(testContext.dropdown.addSelectedClass).toHaveBeenCalledWith(
|
||||
testContext.dummyListItem,
|
||||
);
|
||||
});
|
||||
|
||||
it('should call .preventDefault', () => {
|
||||
expect(testContext.event.preventDefault).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call .hide', () => {
|
||||
expect(testContext.dropdown.hide).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should construct CustomEvent', () => {
|
||||
expect(window.CustomEvent).toHaveBeenCalledWith('click.dl', expect.any(Object));
|
||||
});
|
||||
|
||||
it('should call .dispatchEvent with the customEvent', () => {
|
||||
expect(testContext.list.dispatchEvent).toHaveBeenCalledWith({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('if the target is a UL element', () => {
|
||||
beforeEach(() => {
|
||||
testContext.event.target = document.createElement('ul');
|
||||
|
||||
jest.spyOn(testContext.event.target, 'closest').mockImplementation(() => {});
|
||||
});
|
||||
|
||||
it('should return immediately', () => {
|
||||
DropDown.prototype.clickEvent.call(testContext.dropdown, testContext.event);
|
||||
|
||||
expect(testContext.event.target.closest).not.toHaveBeenCalled();
|
||||
expect(testContext.dropdown.addSelectedClass).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('if the target has the droplab-item-ignore class', () => {
|
||||
beforeEach(() => {
|
||||
testContext.ignoredButton = document.createElement('button');
|
||||
testContext.ignoredButton.classList.add('droplab-item-ignore');
|
||||
testContext.event.target = testContext.ignoredButton;
|
||||
|
||||
jest.spyOn(testContext.ignoredButton, 'closest');
|
||||
});
|
||||
|
||||
it('does not select element', () => {
|
||||
DropDown.prototype.clickEvent.call(testContext.dropdown, testContext.event);
|
||||
|
||||
expect(testContext.ignoredButton.closest.mock.calls.length).toBe(1);
|
||||
expect(testContext.ignoredButton.closest).toHaveBeenCalledWith('.droplab-item-ignore');
|
||||
expect(testContext.dropdown.addSelectedClass).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('if no selected element exists', () => {
|
||||
beforeEach(() => {
|
||||
testContext.event.preventDefault.mockReset();
|
||||
testContext.dummyListItem = null;
|
||||
});
|
||||
|
||||
it('should return before .preventDefault is called', () => {
|
||||
DropDown.prototype.clickEvent.call(testContext.dropdown, testContext.event);
|
||||
|
||||
expect(testContext.event.preventDefault).not.toHaveBeenCalled();
|
||||
expect(testContext.dropdown.addSelectedClass).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('if hideOnClick is false', () => {
|
||||
beforeEach(() => {
|
||||
testContext.dropdown.hideOnClick = false;
|
||||
testContext.dropdown.hide.mockReset();
|
||||
});
|
||||
|
||||
it('should not call .hide', () => {
|
||||
DropDown.prototype.clickEvent.call(testContext.dropdown, testContext.event);
|
||||
|
||||
expect(testContext.dropdown.hide).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('addSelectedClass', () => {
|
||||
beforeEach(() => {
|
||||
testContext.items = Array(4).forEach((item, i) => {
|
||||
testContext.items[i] = { classList: { add: () => {} } };
|
||||
jest.spyOn(testContext.items[i].classList, 'add').mockImplementation(() => {});
|
||||
});
|
||||
testContext.selected = { classList: { add: () => {} } };
|
||||
testContext.dropdown = { removeSelectedClasses: () => {} };
|
||||
|
||||
jest.spyOn(testContext.dropdown, 'removeSelectedClasses').mockImplementation(() => {});
|
||||
jest.spyOn(testContext.selected.classList, 'add').mockImplementation(() => {});
|
||||
|
||||
DropDown.prototype.addSelectedClass.call(testContext.dropdown, testContext.selected);
|
||||
});
|
||||
|
||||
it('should call .removeSelectedClasses', () => {
|
||||
expect(testContext.dropdown.removeSelectedClasses).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call .classList.add', () => {
|
||||
expect(testContext.selected.classList.add).toHaveBeenCalledWith(SELECTED_CLASS);
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeSelectedClasses', () => {
|
||||
beforeEach(() => {
|
||||
testContext.items = [...Array(4)];
|
||||
testContext.items.forEach((item, i) => {
|
||||
testContext.items[i] = { classList: { add: jest.fn(), remove: jest.fn() } };
|
||||
});
|
||||
testContext.dropdown = { items: testContext.items };
|
||||
|
||||
DropDown.prototype.removeSelectedClasses.call(testContext.dropdown);
|
||||
});
|
||||
|
||||
it('should call .classList.remove for all items', () => {
|
||||
testContext.items.forEach((_, i) => {
|
||||
expect(testContext.items[i].classList.remove).toHaveBeenCalledWith(SELECTED_CLASS);
|
||||
});
|
||||
});
|
||||
|
||||
describe('if .items is not set', () => {
|
||||
beforeEach(() => {
|
||||
testContext.dropdown = { getItems: () => {} };
|
||||
|
||||
jest.spyOn(testContext.dropdown, 'getItems').mockReturnValue([]);
|
||||
|
||||
DropDown.prototype.removeSelectedClasses.call(testContext.dropdown);
|
||||
});
|
||||
|
||||
it('should call .getItems', () => {
|
||||
expect(testContext.dropdown.getItems).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('addEvents', () => {
|
||||
beforeEach(() => {
|
||||
testContext.list = {
|
||||
addEventListener: () => {},
|
||||
querySelectorAll: () => [],
|
||||
};
|
||||
testContext.dropdown = {
|
||||
list: testContext.list,
|
||||
clickEvent: () => {},
|
||||
closeDropdown: () => {},
|
||||
eventWrapper: {},
|
||||
};
|
||||
});
|
||||
|
||||
it('should call .addEventListener', () => {
|
||||
jest.spyOn(testContext.list, 'addEventListener').mockImplementation(() => {});
|
||||
|
||||
DropDown.prototype.addEvents.call(testContext.dropdown);
|
||||
|
||||
expect(testContext.list.addEventListener).toHaveBeenCalledWith('click', expect.any(Function));
|
||||
expect(testContext.list.addEventListener).toHaveBeenCalledWith('keyup', expect.any(Function));
|
||||
});
|
||||
});
|
||||
|
||||
describe('setData', () => {
|
||||
beforeEach(() => {
|
||||
testContext.dropdown = { render: () => {} };
|
||||
testContext.data = ['data'];
|
||||
|
||||
jest.spyOn(testContext.dropdown, 'render').mockImplementation(() => {});
|
||||
|
||||
DropDown.prototype.setData.call(testContext.dropdown, testContext.data);
|
||||
});
|
||||
|
||||
it('should set .data', () => {
|
||||
expect(testContext.dropdown.data).toBe(testContext.data);
|
||||
});
|
||||
|
||||
it('should call .render with the .data', () => {
|
||||
expect(testContext.dropdown.render).toHaveBeenCalledWith(testContext.data);
|
||||
});
|
||||
});
|
||||
|
||||
describe('addData', () => {
|
||||
beforeEach(() => {
|
||||
testContext.dropdown = { render: () => {}, data: ['data1'] };
|
||||
testContext.data = ['data2'];
|
||||
|
||||
jest.spyOn(testContext.dropdown, 'render').mockImplementation(() => {});
|
||||
jest.spyOn(Array.prototype, 'concat');
|
||||
|
||||
DropDown.prototype.addData.call(testContext.dropdown, testContext.data);
|
||||
});
|
||||
|
||||
it('should call .concat with data', () => {
|
||||
expect(Array.prototype.concat).toHaveBeenCalledWith(testContext.data);
|
||||
});
|
||||
|
||||
it('should set .data with concatination', () => {
|
||||
expect(testContext.dropdown.data).toStrictEqual(['data1', 'data2']);
|
||||
});
|
||||
|
||||
it('should call .render with the .data', () => {
|
||||
expect(testContext.dropdown.render).toHaveBeenCalledWith(['data1', 'data2']);
|
||||
});
|
||||
|
||||
describe('if .data is undefined', () => {
|
||||
beforeEach(() => {
|
||||
testContext.dropdown = { render: () => {}, data: undefined };
|
||||
testContext.data = ['data2'];
|
||||
|
||||
jest.spyOn(testContext.dropdown, 'render').mockImplementation(() => {});
|
||||
|
||||
DropDown.prototype.addData.call(testContext.dropdown, testContext.data);
|
||||
});
|
||||
|
||||
it('should set .data with concatination', () => {
|
||||
expect(testContext.dropdown.data).toStrictEqual(['data2']);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('render', () => {
|
||||
beforeEach(() => {
|
||||
testContext.renderableList = {};
|
||||
testContext.list = {
|
||||
querySelector: q => {
|
||||
if (q === '.filter-dropdown-loading') {
|
||||
return false;
|
||||
}
|
||||
return testContext.renderableList;
|
||||
},
|
||||
dispatchEvent: () => {},
|
||||
};
|
||||
testContext.dropdown = { renderChildren: () => {}, list: testContext.list };
|
||||
testContext.data = [0, 1];
|
||||
testContext.customEvent = {};
|
||||
|
||||
jest.spyOn(testContext.dropdown, 'renderChildren').mockImplementation(data => data);
|
||||
jest.spyOn(testContext.list, 'dispatchEvent').mockImplementation(() => {});
|
||||
jest.spyOn(testContext.data, 'map');
|
||||
jest.spyOn(window, 'CustomEvent').mockReturnValue(testContext.customEvent);
|
||||
|
||||
DropDown.prototype.render.call(testContext.dropdown, testContext.data);
|
||||
});
|
||||
|
||||
it('should call .map', () => {
|
||||
expect(testContext.data.map).toHaveBeenCalledWith(expect.any(Function));
|
||||
});
|
||||
|
||||
it('should call .renderChildren for each data item', () => {
|
||||
expect(testContext.dropdown.renderChildren.mock.calls.length).toBe(testContext.data.length);
|
||||
});
|
||||
|
||||
it('sets the renderableList .innerHTML', () => {
|
||||
expect(testContext.renderableList.innerHTML).toBe('01');
|
||||
});
|
||||
|
||||
it('should call render.dl', () => {
|
||||
expect(window.CustomEvent).toHaveBeenCalledWith('render.dl', expect.any(Object));
|
||||
});
|
||||
|
||||
it('should call dispatchEvent with the customEvent', () => {
|
||||
expect(testContext.list.dispatchEvent).toHaveBeenCalledWith(testContext.customEvent);
|
||||
});
|
||||
|
||||
describe('if no data argument is passed', () => {
|
||||
beforeEach(() => {
|
||||
testContext.data.map.mockReset();
|
||||
testContext.dropdown.renderChildren.mockReset();
|
||||
|
||||
DropDown.prototype.render.call(testContext.dropdown, undefined);
|
||||
});
|
||||
|
||||
it('should not call .map', () => {
|
||||
expect(testContext.data.map).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not call .renderChildren', () => {
|
||||
expect(testContext.dropdown.renderChildren).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('if no dynamic list is present', () => {
|
||||
beforeEach(() => {
|
||||
testContext.list = { querySelector: () => {}, dispatchEvent: () => {} };
|
||||
testContext.dropdown = { renderChildren: () => {}, list: testContext.list };
|
||||
testContext.data = [0, 1];
|
||||
|
||||
jest.spyOn(testContext.dropdown, 'renderChildren').mockImplementation(data => data);
|
||||
jest.spyOn(testContext.list, 'querySelector').mockImplementation(() => {});
|
||||
jest.spyOn(testContext.data, 'map');
|
||||
|
||||
DropDown.prototype.render.call(testContext.dropdown, testContext.data);
|
||||
});
|
||||
|
||||
it('sets the .list .innerHTML', () => {
|
||||
expect(testContext.list.innerHTML).toBe('01');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('renderChildren', () => {
|
||||
beforeEach(() => {
|
||||
testContext.templateString = 'templateString';
|
||||
testContext.dropdown = { templateString: testContext.templateString };
|
||||
testContext.data = { droplab_hidden: true };
|
||||
testContext.html = 'html';
|
||||
testContext.template = { firstChild: { outerHTML: 'outerHTML', style: {} } };
|
||||
|
||||
jest.spyOn(utils, 'template').mockReturnValue(testContext.html);
|
||||
jest.spyOn(document, 'createElement').mockReturnValue(testContext.template);
|
||||
jest.spyOn(DropDown, 'setImagesSrc').mockImplementation(() => {});
|
||||
|
||||
testContext.renderChildren = DropDown.prototype.renderChildren.call(
|
||||
testContext.dropdown,
|
||||
testContext.data,
|
||||
);
|
||||
});
|
||||
|
||||
it('should call utils.t with .templateString and data', () => {
|
||||
expect(utils.template).toHaveBeenCalledWith(testContext.templateString, testContext.data);
|
||||
});
|
||||
|
||||
it('should call document.createElement', () => {
|
||||
expect(document.createElement).toHaveBeenCalledWith('div');
|
||||
});
|
||||
|
||||
it('should set the templates .innerHTML to the HTML', () => {
|
||||
expect(testContext.template.innerHTML).toBe(testContext.html);
|
||||
});
|
||||
|
||||
it('should call .setImagesSrc with the template', () => {
|
||||
expect(DropDown.setImagesSrc).toHaveBeenCalledWith(testContext.template);
|
||||
});
|
||||
|
||||
it('should set the template display to none', () => {
|
||||
expect(testContext.template.firstChild.style.display).toBe('none');
|
||||
});
|
||||
|
||||
it('should return the templates .firstChild.outerHTML', () => {
|
||||
expect(testContext.renderChildren).toBe(testContext.template.firstChild.outerHTML);
|
||||
});
|
||||
|
||||
describe('if droplab_hidden is false', () => {
|
||||
beforeEach(() => {
|
||||
testContext.data = { droplab_hidden: false };
|
||||
testContext.renderChildren = DropDown.prototype.renderChildren.call(
|
||||
testContext.dropdown,
|
||||
testContext.data,
|
||||
);
|
||||
});
|
||||
|
||||
it('should set the template display to block', () => {
|
||||
expect(testContext.template.firstChild.style.display).toBe('block');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('setImagesSrc', () => {
|
||||
beforeEach(() => {
|
||||
testContext.template = { querySelectorAll: () => {} };
|
||||
|
||||
jest.spyOn(testContext.template, 'querySelectorAll').mockReturnValue([]);
|
||||
|
||||
DropDown.setImagesSrc(testContext.template);
|
||||
});
|
||||
|
||||
it('should call .querySelectorAll', () => {
|
||||
expect(testContext.template.querySelectorAll).toHaveBeenCalledWith('img[data-src]');
|
||||
});
|
||||
});
|
||||
|
||||
describe('show', () => {
|
||||
beforeEach(() => {
|
||||
testContext.list = { style: {} };
|
||||
testContext.dropdown = { list: testContext.list, hidden: true };
|
||||
|
||||
DropDown.prototype.show.call(testContext.dropdown);
|
||||
});
|
||||
|
||||
it('it should set .list display to block', () => {
|
||||
expect(testContext.list.style.display).toBe('block');
|
||||
});
|
||||
|
||||
it('it should set .hidden to false', () => {
|
||||
expect(testContext.dropdown.hidden).toBe(false);
|
||||
});
|
||||
|
||||
describe('if .hidden is false', () => {
|
||||
beforeEach(() => {
|
||||
testContext.list = { style: {} };
|
||||
testContext.dropdown = { list: testContext.list, hidden: false };
|
||||
|
||||
testContext.show = DropDown.prototype.show.call(testContext.dropdown);
|
||||
});
|
||||
|
||||
it('should return undefined', () => {
|
||||
expect(testContext.show).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should not set .list display to block', () => {
|
||||
expect(testContext.list.style.display).not.toBe('block');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('hide', () => {
|
||||
beforeEach(() => {
|
||||
testContext.list = { style: {} };
|
||||
testContext.dropdown = { list: testContext.list };
|
||||
|
||||
DropDown.prototype.hide.call(testContext.dropdown);
|
||||
});
|
||||
|
||||
it('it should set .list display to none', () => {
|
||||
expect(testContext.list.style.display).toBe('none');
|
||||
});
|
||||
|
||||
it('it should set .hidden to true', () => {
|
||||
expect(testContext.dropdown.hidden).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('toggle', () => {
|
||||
beforeEach(() => {
|
||||
testContext.hidden = true;
|
||||
testContext.dropdown = { hidden: testContext.hidden, show: () => {}, hide: () => {} };
|
||||
|
||||
jest.spyOn(testContext.dropdown, 'show').mockImplementation(() => {});
|
||||
jest.spyOn(testContext.dropdown, 'hide').mockImplementation(() => {});
|
||||
|
||||
DropDown.prototype.toggle.call(testContext.dropdown);
|
||||
});
|
||||
|
||||
it('should call .show', () => {
|
||||
expect(testContext.dropdown.show).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe('if .hidden is false', () => {
|
||||
beforeEach(() => {
|
||||
testContext.hidden = false;
|
||||
testContext.dropdown = { hidden: testContext.hidden, show: () => {}, hide: () => {} };
|
||||
|
||||
jest.spyOn(testContext.dropdown, 'show').mockImplementation(() => {});
|
||||
jest.spyOn(testContext.dropdown, 'hide').mockImplementation(() => {});
|
||||
|
||||
DropDown.prototype.toggle.call(testContext.dropdown);
|
||||
});
|
||||
|
||||
it('should call .hide', () => {
|
||||
expect(testContext.dropdown.hide).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('destroy', () => {
|
||||
beforeEach(() => {
|
||||
testContext.list = { removeEventListener: () => {} };
|
||||
testContext.eventWrapper = { clickEvent: 'clickEvent' };
|
||||
testContext.dropdown = {
|
||||
list: testContext.list,
|
||||
hide: () => {},
|
||||
eventWrapper: testContext.eventWrapper,
|
||||
};
|
||||
|
||||
jest.spyOn(testContext.list, 'removeEventListener').mockImplementation(() => {});
|
||||
jest.spyOn(testContext.dropdown, 'hide').mockImplementation(() => {});
|
||||
|
||||
DropDown.prototype.destroy.call(testContext.dropdown);
|
||||
});
|
||||
|
||||
it('it should call .hide', () => {
|
||||
expect(testContext.dropdown.hide).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('it should call .removeEventListener', () => {
|
||||
expect(testContext.list.removeEventListener).toHaveBeenCalledWith(
|
||||
'click',
|
||||
testContext.eventWrapper.clickEvent,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
It is important that the simple base include parallel jobs
|
||||
as well as non-parallel jobs with spaces in the name to prevent
|
||||
us relying on spaces as an indicator.
|
||||
*/
|
||||
export default {
|
||||
stages: [
|
||||
{
|
||||
name: 'test',
|
||||
groups: [
|
||||
{
|
||||
name: 'jest',
|
||||
size: 2,
|
||||
jobs: [{ name: 'jest 1/2', needs: ['frontend fixtures'] }, { name: 'jest 2/2' }],
|
||||
},
|
||||
{
|
||||
name: 'rspec',
|
||||
size: 1,
|
||||
jobs: [{ name: 'rspec', needs: ['frontend fixtures'] }],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'fixtures',
|
||||
groups: [
|
||||
{
|
||||
name: 'frontend fixtures',
|
||||
size: 1,
|
||||
jobs: [{ name: 'frontend fixtures' }],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'un-needed',
|
||||
groups: [
|
||||
{
|
||||
name: 'un-needed',
|
||||
size: 1,
|
||||
jobs: [{ name: 'un-needed' }],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
|
@ -0,0 +1,171 @@
|
|||
import {
|
||||
createNodesStructure,
|
||||
makeLinksFromNodes,
|
||||
filterByAncestors,
|
||||
parseData,
|
||||
createSankey,
|
||||
removeOrphanNodes,
|
||||
getMaxNodes,
|
||||
} from '~/pipelines/components/dag/utils';
|
||||
|
||||
import mockGraphData from './mock_data';
|
||||
|
||||
describe('DAG visualization parsing utilities', () => {
|
||||
const { nodes, nodeDict } = createNodesStructure(mockGraphData.stages);
|
||||
const unfilteredLinks = makeLinksFromNodes(nodes, nodeDict);
|
||||
const parsed = parseData(mockGraphData.stages);
|
||||
|
||||
const layoutSettings = {
|
||||
width: 200,
|
||||
height: 200,
|
||||
nodeWidth: 10,
|
||||
nodePadding: 20,
|
||||
paddingForLabels: 100,
|
||||
};
|
||||
|
||||
const sankeyLayout = createSankey(layoutSettings)(parsed);
|
||||
|
||||
describe('createNodesStructure', () => {
|
||||
const parallelGroupName = 'jest';
|
||||
const parallelJobName = 'jest 1/2';
|
||||
const singleJobName = 'frontend fixtures';
|
||||
|
||||
const { name, jobs, size } = mockGraphData.stages[0].groups[0];
|
||||
|
||||
it('returns the expected node structure', () => {
|
||||
expect(nodes[0]).toHaveProperty('category', mockGraphData.stages[0].name);
|
||||
expect(nodes[0]).toHaveProperty('name', name);
|
||||
expect(nodes[0]).toHaveProperty('jobs', jobs);
|
||||
expect(nodes[0]).toHaveProperty('size', size);
|
||||
});
|
||||
|
||||
it('adds needs to top level of nodeDict entries', () => {
|
||||
expect(nodeDict[parallelGroupName]).toHaveProperty('needs');
|
||||
expect(nodeDict[parallelJobName]).toHaveProperty('needs');
|
||||
expect(nodeDict[singleJobName]).toHaveProperty('needs');
|
||||
});
|
||||
|
||||
it('makes entries in nodeDict for jobs and parallel jobs', () => {
|
||||
const nodeNames = Object.keys(nodeDict);
|
||||
|
||||
expect(nodeNames.includes(parallelGroupName)).toBe(true);
|
||||
expect(nodeNames.includes(parallelJobName)).toBe(true);
|
||||
expect(nodeNames.includes(singleJobName)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('makeLinksFromNodes', () => {
|
||||
it('returns the expected link structure', () => {
|
||||
expect(unfilteredLinks[0]).toHaveProperty('source', 'frontend fixtures');
|
||||
expect(unfilteredLinks[0]).toHaveProperty('target', 'jest');
|
||||
expect(unfilteredLinks[0]).toHaveProperty('value', 10);
|
||||
});
|
||||
});
|
||||
|
||||
describe('filterByAncestors', () => {
|
||||
const allLinks = [
|
||||
{ source: 'job1', target: 'job4' },
|
||||
{ source: 'job1', target: 'job2' },
|
||||
{ source: 'job2', target: 'job4' },
|
||||
];
|
||||
|
||||
const dedupedLinks = [{ source: 'job1', target: 'job2' }, { source: 'job2', target: 'job4' }];
|
||||
|
||||
const nodeLookup = {
|
||||
job1: {
|
||||
name: 'job1',
|
||||
},
|
||||
job2: {
|
||||
name: 'job2',
|
||||
needs: ['job1'],
|
||||
},
|
||||
job4: {
|
||||
name: 'job4',
|
||||
needs: ['job1', 'job2'],
|
||||
category: 'build',
|
||||
},
|
||||
};
|
||||
|
||||
it('dedupes links', () => {
|
||||
expect(filterByAncestors(allLinks, nodeLookup)).toMatchObject(dedupedLinks);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseData parent function', () => {
|
||||
it('returns an object containing a list of nodes and links', () => {
|
||||
// an array of nodes exist and the values are defined
|
||||
expect(parsed).toHaveProperty('nodes');
|
||||
expect(Array.isArray(parsed.nodes)).toBe(true);
|
||||
expect(parsed.nodes.filter(Boolean)).not.toHaveLength(0);
|
||||
|
||||
// an array of links exist and the values are defined
|
||||
expect(parsed).toHaveProperty('links');
|
||||
expect(Array.isArray(parsed.links)).toBe(true);
|
||||
expect(parsed.links.filter(Boolean)).not.toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createSankey', () => {
|
||||
it('returns a nodes data structure with expected d3-added properties', () => {
|
||||
expect(sankeyLayout.nodes[0]).toHaveProperty('sourceLinks');
|
||||
expect(sankeyLayout.nodes[0]).toHaveProperty('targetLinks');
|
||||
expect(sankeyLayout.nodes[0]).toHaveProperty('depth');
|
||||
expect(sankeyLayout.nodes[0]).toHaveProperty('layer');
|
||||
expect(sankeyLayout.nodes[0]).toHaveProperty('x0');
|
||||
expect(sankeyLayout.nodes[0]).toHaveProperty('x1');
|
||||
expect(sankeyLayout.nodes[0]).toHaveProperty('y0');
|
||||
expect(sankeyLayout.nodes[0]).toHaveProperty('y1');
|
||||
});
|
||||
|
||||
it('returns a links data structure with expected d3-added properties', () => {
|
||||
expect(sankeyLayout.links[0]).toHaveProperty('source');
|
||||
expect(sankeyLayout.links[0]).toHaveProperty('target');
|
||||
expect(sankeyLayout.links[0]).toHaveProperty('width');
|
||||
expect(sankeyLayout.links[0]).toHaveProperty('y0');
|
||||
expect(sankeyLayout.links[0]).toHaveProperty('y1');
|
||||
});
|
||||
|
||||
describe('data structure integrity', () => {
|
||||
const newObject = { name: 'bad-actor' };
|
||||
|
||||
beforeEach(() => {
|
||||
sankeyLayout.nodes.unshift(newObject);
|
||||
});
|
||||
|
||||
it('sankey does not propagate changes back to the original', () => {
|
||||
expect(sankeyLayout.nodes[0]).toBe(newObject);
|
||||
expect(parsed.nodes[0]).not.toBe(newObject);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sankeyLayout.nodes.shift();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeOrphanNodes', () => {
|
||||
it('removes sankey nodes that have no needs and are not needed', () => {
|
||||
const cleanedNodes = removeOrphanNodes(sankeyLayout.nodes);
|
||||
expect(cleanedNodes).toHaveLength(sankeyLayout.nodes.length - 1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getMaxNodes', () => {
|
||||
it('returns the number of nodes in the most populous generation', () => {
|
||||
const layerNodes = [
|
||||
{ layer: 0 },
|
||||
{ layer: 0 },
|
||||
{ layer: 1 },
|
||||
{ layer: 1 },
|
||||
{ layer: 0 },
|
||||
{ layer: 3 },
|
||||
{ layer: 2 },
|
||||
{ layer: 4 },
|
||||
{ layer: 1 },
|
||||
{ layer: 3 },
|
||||
{ layer: 4 },
|
||||
];
|
||||
expect(getMaxNodes(layerNodes)).toBe(3);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,650 +0,0 @@
|
|||
import DropDown from '~/droplab/drop_down';
|
||||
import utils from '~/droplab/utils';
|
||||
import { SELECTED_CLASS } from '~/droplab/constants';
|
||||
|
||||
describe('DropLab DropDown', function() {
|
||||
describe('class constructor', function() {
|
||||
beforeEach(function() {
|
||||
spyOn(DropDown.prototype, 'getItems');
|
||||
spyOn(DropDown.prototype, 'initTemplateString');
|
||||
spyOn(DropDown.prototype, 'addEvents');
|
||||
|
||||
this.list = { innerHTML: 'innerHTML' };
|
||||
this.dropdown = new DropDown(this.list);
|
||||
});
|
||||
|
||||
it('sets the .hidden property to true', function() {
|
||||
expect(this.dropdown.hidden).toBe(true);
|
||||
});
|
||||
|
||||
it('sets the .list property', function() {
|
||||
expect(this.dropdown.list).toBe(this.list);
|
||||
});
|
||||
|
||||
it('calls .getItems', function() {
|
||||
expect(DropDown.prototype.getItems).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls .initTemplateString', function() {
|
||||
expect(DropDown.prototype.initTemplateString).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls .addEvents', function() {
|
||||
expect(DropDown.prototype.addEvents).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('sets the .initialState property to the .list.innerHTML', function() {
|
||||
expect(this.dropdown.initialState).toBe(this.list.innerHTML);
|
||||
});
|
||||
|
||||
describe('if the list argument is a string', function() {
|
||||
beforeEach(function() {
|
||||
this.element = {};
|
||||
this.selector = '.selector';
|
||||
|
||||
spyOn(Document.prototype, 'querySelector').and.returnValue(this.element);
|
||||
|
||||
this.dropdown = new DropDown(this.selector);
|
||||
});
|
||||
|
||||
it('calls .querySelector with the selector string', function() {
|
||||
expect(Document.prototype.querySelector).toHaveBeenCalledWith(this.selector);
|
||||
});
|
||||
|
||||
it('sets the .list property element', function() {
|
||||
expect(this.dropdown.list).toBe(this.element);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getItems', function() {
|
||||
beforeEach(function() {
|
||||
this.list = { querySelectorAll: () => {} };
|
||||
this.dropdown = { list: this.list };
|
||||
this.nodeList = [];
|
||||
|
||||
spyOn(this.list, 'querySelectorAll').and.returnValue(this.nodeList);
|
||||
|
||||
this.getItems = DropDown.prototype.getItems.call(this.dropdown);
|
||||
});
|
||||
|
||||
it('calls .querySelectorAll with a list item query', function() {
|
||||
expect(this.list.querySelectorAll).toHaveBeenCalledWith('li');
|
||||
});
|
||||
|
||||
it('sets the .items property to the returned list items', function() {
|
||||
expect(this.dropdown.items).toEqual(jasmine.any(Array));
|
||||
});
|
||||
|
||||
it('returns the .items', function() {
|
||||
expect(this.getItems).toEqual(jasmine.any(Array));
|
||||
});
|
||||
});
|
||||
|
||||
describe('initTemplateString', function() {
|
||||
beforeEach(function() {
|
||||
this.items = [{ outerHTML: '<a></a>' }, { outerHTML: '<img>' }];
|
||||
this.dropdown = { items: this.items };
|
||||
|
||||
DropDown.prototype.initTemplateString.call(this.dropdown);
|
||||
});
|
||||
|
||||
it('should set .templateString to the last items .outerHTML', function() {
|
||||
expect(this.dropdown.templateString).toBe(this.items[1].outerHTML);
|
||||
});
|
||||
|
||||
it('should not set .templateString to a non-last items .outerHTML', function() {
|
||||
expect(this.dropdown.templateString).not.toBe(this.items[0].outerHTML);
|
||||
});
|
||||
|
||||
describe('if .items is not set', function() {
|
||||
beforeEach(function() {
|
||||
this.dropdown = { getItems: () => {} };
|
||||
|
||||
spyOn(this.dropdown, 'getItems').and.returnValue([]);
|
||||
|
||||
DropDown.prototype.initTemplateString.call(this.dropdown);
|
||||
});
|
||||
|
||||
it('should call .getItems', function() {
|
||||
expect(this.dropdown.getItems).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('if items array is empty', function() {
|
||||
beforeEach(function() {
|
||||
this.dropdown = { items: [] };
|
||||
|
||||
DropDown.prototype.initTemplateString.call(this.dropdown);
|
||||
});
|
||||
|
||||
it('should set .templateString to an empty string', function() {
|
||||
expect(this.dropdown.templateString).toBe('');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('clickEvent', function() {
|
||||
beforeEach(function() {
|
||||
this.classList = jasmine.createSpyObj('classList', ['contains']);
|
||||
this.list = { dispatchEvent: () => {} };
|
||||
this.dropdown = {
|
||||
hideOnClick: true,
|
||||
hide: () => {},
|
||||
list: this.list,
|
||||
addSelectedClass: () => {},
|
||||
};
|
||||
this.event = {
|
||||
preventDefault: () => {},
|
||||
target: {
|
||||
classList: this.classList,
|
||||
closest: () => null,
|
||||
},
|
||||
};
|
||||
this.customEvent = {};
|
||||
this.dummyListItem = document.createElement('li');
|
||||
spyOn(this.event.target, 'closest').and.callFake(selector => {
|
||||
if (selector === 'li') {
|
||||
return this.dummyListItem;
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
spyOn(this.dropdown, 'hide');
|
||||
spyOn(this.dropdown, 'addSelectedClass');
|
||||
spyOn(this.list, 'dispatchEvent');
|
||||
spyOn(this.event, 'preventDefault');
|
||||
spyOn(window, 'CustomEvent').and.returnValue(this.customEvent);
|
||||
this.classList.contains.and.returnValue(false);
|
||||
});
|
||||
|
||||
it('should call event.target.closest', function() {
|
||||
DropDown.prototype.clickEvent.call(this.dropdown, this.event);
|
||||
|
||||
expect(this.event.target.closest).toHaveBeenCalledWith('.droplab-item-ignore');
|
||||
expect(this.event.target.closest).toHaveBeenCalledWith('li');
|
||||
});
|
||||
|
||||
it('should call addSelectedClass', function() {
|
||||
DropDown.prototype.clickEvent.call(this.dropdown, this.event);
|
||||
|
||||
expect(this.dropdown.addSelectedClass).toHaveBeenCalledWith(this.dummyListItem);
|
||||
});
|
||||
|
||||
it('should call .preventDefault', function() {
|
||||
DropDown.prototype.clickEvent.call(this.dropdown, this.event);
|
||||
|
||||
expect(this.event.preventDefault).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call .hide', function() {
|
||||
DropDown.prototype.clickEvent.call(this.dropdown, this.event);
|
||||
|
||||
expect(this.dropdown.hide).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should construct CustomEvent', function() {
|
||||
DropDown.prototype.clickEvent.call(this.dropdown, this.event);
|
||||
|
||||
expect(window.CustomEvent).toHaveBeenCalledWith('click.dl', jasmine.any(Object));
|
||||
});
|
||||
|
||||
it('should call .dispatchEvent with the customEvent', function() {
|
||||
DropDown.prototype.clickEvent.call(this.dropdown, this.event);
|
||||
|
||||
expect(this.list.dispatchEvent).toHaveBeenCalledWith(this.customEvent);
|
||||
});
|
||||
|
||||
describe('if the target is a UL element', function() {
|
||||
beforeEach(function() {
|
||||
this.event.target = document.createElement('ul');
|
||||
|
||||
spyOn(this.event.target, 'closest');
|
||||
});
|
||||
|
||||
it('should return immediately', function() {
|
||||
DropDown.prototype.clickEvent.call(this.dropdown, this.event);
|
||||
|
||||
expect(this.event.target.closest).not.toHaveBeenCalled();
|
||||
expect(this.dropdown.addSelectedClass).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('if the target has the droplab-item-ignore class', function() {
|
||||
beforeEach(function() {
|
||||
this.ignoredButton = document.createElement('button');
|
||||
this.ignoredButton.classList.add('droplab-item-ignore');
|
||||
this.event.target = this.ignoredButton;
|
||||
|
||||
spyOn(this.ignoredButton, 'closest').and.callThrough();
|
||||
});
|
||||
|
||||
it('does not select element', function() {
|
||||
DropDown.prototype.clickEvent.call(this.dropdown, this.event);
|
||||
|
||||
expect(this.ignoredButton.closest.calls.count()).toBe(1);
|
||||
expect(this.ignoredButton.closest).toHaveBeenCalledWith('.droplab-item-ignore');
|
||||
expect(this.dropdown.addSelectedClass).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('if no selected element exists', function() {
|
||||
beforeEach(function() {
|
||||
this.event.preventDefault.calls.reset();
|
||||
this.dummyListItem = null;
|
||||
});
|
||||
|
||||
it('should return before .preventDefault is called', function() {
|
||||
DropDown.prototype.clickEvent.call(this.dropdown, this.event);
|
||||
|
||||
expect(this.event.preventDefault).not.toHaveBeenCalled();
|
||||
expect(this.dropdown.addSelectedClass).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('if hideOnClick is false', () => {
|
||||
beforeEach(function() {
|
||||
this.dropdown.hideOnClick = false;
|
||||
this.dropdown.hide.calls.reset();
|
||||
});
|
||||
|
||||
it('should not call .hide', function() {
|
||||
DropDown.prototype.clickEvent.call(this.dropdown, this.event);
|
||||
|
||||
expect(this.dropdown.hide).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('addSelectedClass', function() {
|
||||
beforeEach(function() {
|
||||
this.items = Array(4).forEach((item, i) => {
|
||||
this.items[i] = { classList: { add: () => {} } };
|
||||
spyOn(this.items[i].classList, 'add');
|
||||
});
|
||||
this.selected = { classList: { add: () => {} } };
|
||||
this.dropdown = { removeSelectedClasses: () => {} };
|
||||
|
||||
spyOn(this.dropdown, 'removeSelectedClasses');
|
||||
spyOn(this.selected.classList, 'add');
|
||||
|
||||
DropDown.prototype.addSelectedClass.call(this.dropdown, this.selected);
|
||||
});
|
||||
|
||||
it('should call .removeSelectedClasses', function() {
|
||||
expect(this.dropdown.removeSelectedClasses).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call .classList.add', function() {
|
||||
expect(this.selected.classList.add).toHaveBeenCalledWith(SELECTED_CLASS);
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeSelectedClasses', function() {
|
||||
beforeEach(function() {
|
||||
this.items = Array(4);
|
||||
this.items.forEach((item, i) => {
|
||||
this.items[i] = { classList: { add: () => {} } };
|
||||
spyOn(this.items[i].classList, 'add');
|
||||
});
|
||||
this.dropdown = { items: this.items };
|
||||
|
||||
DropDown.prototype.removeSelectedClasses.call(this.dropdown);
|
||||
});
|
||||
|
||||
it('should call .classList.remove for all items', function() {
|
||||
this.items.forEach((item, i) => {
|
||||
expect(this.items[i].classList.add).toHaveBeenCalledWith(SELECTED_CLASS);
|
||||
});
|
||||
});
|
||||
|
||||
describe('if .items is not set', function() {
|
||||
beforeEach(function() {
|
||||
this.dropdown = { getItems: () => {} };
|
||||
|
||||
spyOn(this.dropdown, 'getItems').and.returnValue([]);
|
||||
|
||||
DropDown.prototype.removeSelectedClasses.call(this.dropdown);
|
||||
});
|
||||
|
||||
it('should call .getItems', function() {
|
||||
expect(this.dropdown.getItems).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('addEvents', function() {
|
||||
beforeEach(function() {
|
||||
this.list = {
|
||||
addEventListener: () => {},
|
||||
querySelectorAll: () => [],
|
||||
};
|
||||
this.dropdown = {
|
||||
list: this.list,
|
||||
clickEvent: () => {},
|
||||
closeDropdown: () => {},
|
||||
eventWrapper: {},
|
||||
};
|
||||
});
|
||||
|
||||
it('should call .addEventListener', function() {
|
||||
spyOn(this.list, 'addEventListener');
|
||||
|
||||
DropDown.prototype.addEvents.call(this.dropdown);
|
||||
|
||||
expect(this.list.addEventListener).toHaveBeenCalledWith('click', jasmine.any(Function));
|
||||
expect(this.list.addEventListener).toHaveBeenCalledWith('keyup', jasmine.any(Function));
|
||||
});
|
||||
});
|
||||
|
||||
describe('setData', function() {
|
||||
beforeEach(function() {
|
||||
this.dropdown = { render: () => {} };
|
||||
this.data = ['data'];
|
||||
|
||||
spyOn(this.dropdown, 'render');
|
||||
|
||||
DropDown.prototype.setData.call(this.dropdown, this.data);
|
||||
});
|
||||
|
||||
it('should set .data', function() {
|
||||
expect(this.dropdown.data).toBe(this.data);
|
||||
});
|
||||
|
||||
it('should call .render with the .data', function() {
|
||||
expect(this.dropdown.render).toHaveBeenCalledWith(this.data);
|
||||
});
|
||||
});
|
||||
|
||||
describe('addData', function() {
|
||||
beforeEach(function() {
|
||||
this.dropdown = { render: () => {}, data: ['data1'] };
|
||||
this.data = ['data2'];
|
||||
|
||||
spyOn(this.dropdown, 'render');
|
||||
spyOn(Array.prototype, 'concat').and.callThrough();
|
||||
|
||||
DropDown.prototype.addData.call(this.dropdown, this.data);
|
||||
});
|
||||
|
||||
it('should call .concat with data', function() {
|
||||
expect(Array.prototype.concat).toHaveBeenCalledWith(this.data);
|
||||
});
|
||||
|
||||
it('should set .data with concatination', function() {
|
||||
expect(this.dropdown.data).toEqual(['data1', 'data2']);
|
||||
});
|
||||
|
||||
it('should call .render with the .data', function() {
|
||||
expect(this.dropdown.render).toHaveBeenCalledWith(['data1', 'data2']);
|
||||
});
|
||||
|
||||
describe('if .data is undefined', function() {
|
||||
beforeEach(function() {
|
||||
this.dropdown = { render: () => {}, data: undefined };
|
||||
this.data = ['data2'];
|
||||
|
||||
spyOn(this.dropdown, 'render');
|
||||
|
||||
DropDown.prototype.addData.call(this.dropdown, this.data);
|
||||
});
|
||||
|
||||
it('should set .data with concatination', function() {
|
||||
expect(this.dropdown.data).toEqual(['data2']);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('render', function() {
|
||||
beforeEach(function() {
|
||||
this.renderableList = {};
|
||||
this.list = {
|
||||
querySelector: q => {
|
||||
if (q === '.filter-dropdown-loading') {
|
||||
return false;
|
||||
}
|
||||
return this.renderableList;
|
||||
},
|
||||
dispatchEvent: () => {},
|
||||
};
|
||||
this.dropdown = { renderChildren: () => {}, list: this.list };
|
||||
this.data = [0, 1];
|
||||
this.customEvent = {};
|
||||
|
||||
spyOn(this.dropdown, 'renderChildren').and.callFake(data => data);
|
||||
spyOn(this.list, 'dispatchEvent');
|
||||
spyOn(this.data, 'map').and.callThrough();
|
||||
spyOn(window, 'CustomEvent').and.returnValue(this.customEvent);
|
||||
|
||||
DropDown.prototype.render.call(this.dropdown, this.data);
|
||||
});
|
||||
|
||||
it('should call .map', function() {
|
||||
expect(this.data.map).toHaveBeenCalledWith(jasmine.any(Function));
|
||||
});
|
||||
|
||||
it('should call .renderChildren for each data item', function() {
|
||||
expect(this.dropdown.renderChildren.calls.count()).toBe(this.data.length);
|
||||
});
|
||||
|
||||
it('sets the renderableList .innerHTML', function() {
|
||||
expect(this.renderableList.innerHTML).toBe('01');
|
||||
});
|
||||
|
||||
it('should call render.dl', function() {
|
||||
expect(window.CustomEvent).toHaveBeenCalledWith('render.dl', jasmine.any(Object));
|
||||
});
|
||||
|
||||
it('should call dispatchEvent with the customEvent', function() {
|
||||
expect(this.list.dispatchEvent).toHaveBeenCalledWith(this.customEvent);
|
||||
});
|
||||
|
||||
describe('if no data argument is passed', function() {
|
||||
beforeEach(function() {
|
||||
this.data.map.calls.reset();
|
||||
this.dropdown.renderChildren.calls.reset();
|
||||
|
||||
DropDown.prototype.render.call(this.dropdown, undefined);
|
||||
});
|
||||
|
||||
it('should not call .map', function() {
|
||||
expect(this.data.map).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not call .renderChildren', function() {
|
||||
expect(this.dropdown.renderChildren).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('if no dynamic list is present', function() {
|
||||
beforeEach(function() {
|
||||
this.list = { querySelector: () => {}, dispatchEvent: () => {} };
|
||||
this.dropdown = { renderChildren: () => {}, list: this.list };
|
||||
this.data = [0, 1];
|
||||
|
||||
spyOn(this.dropdown, 'renderChildren').and.callFake(data => data);
|
||||
spyOn(this.list, 'querySelector');
|
||||
spyOn(this.data, 'map').and.callThrough();
|
||||
|
||||
DropDown.prototype.render.call(this.dropdown, this.data);
|
||||
});
|
||||
|
||||
it('sets the .list .innerHTML', function() {
|
||||
expect(this.list.innerHTML).toBe('01');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('renderChildren', function() {
|
||||
beforeEach(function() {
|
||||
this.templateString = 'templateString';
|
||||
this.dropdown = { templateString: this.templateString };
|
||||
this.data = { droplab_hidden: true };
|
||||
this.html = 'html';
|
||||
this.template = { firstChild: { outerHTML: 'outerHTML', style: {} } };
|
||||
|
||||
spyOn(utils, 'template').and.returnValue(this.html);
|
||||
spyOn(document, 'createElement').and.returnValue(this.template);
|
||||
spyOn(DropDown, 'setImagesSrc');
|
||||
|
||||
this.renderChildren = DropDown.prototype.renderChildren.call(this.dropdown, this.data);
|
||||
});
|
||||
|
||||
it('should call utils.t with .templateString and data', function() {
|
||||
expect(utils.template).toHaveBeenCalledWith(this.templateString, this.data);
|
||||
});
|
||||
|
||||
it('should call document.createElement', function() {
|
||||
expect(document.createElement).toHaveBeenCalledWith('div');
|
||||
});
|
||||
|
||||
it('should set the templates .innerHTML to the HTML', function() {
|
||||
expect(this.template.innerHTML).toBe(this.html);
|
||||
});
|
||||
|
||||
it('should call .setImagesSrc with the template', function() {
|
||||
expect(DropDown.setImagesSrc).toHaveBeenCalledWith(this.template);
|
||||
});
|
||||
|
||||
it('should set the template display to none', function() {
|
||||
expect(this.template.firstChild.style.display).toBe('none');
|
||||
});
|
||||
|
||||
it('should return the templates .firstChild.outerHTML', function() {
|
||||
expect(this.renderChildren).toBe(this.template.firstChild.outerHTML);
|
||||
});
|
||||
|
||||
describe('if droplab_hidden is false', function() {
|
||||
beforeEach(function() {
|
||||
this.data = { droplab_hidden: false };
|
||||
this.renderChildren = DropDown.prototype.renderChildren.call(this.dropdown, this.data);
|
||||
});
|
||||
|
||||
it('should set the template display to block', function() {
|
||||
expect(this.template.firstChild.style.display).toBe('block');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('setImagesSrc', function() {
|
||||
beforeEach(function() {
|
||||
this.template = { querySelectorAll: () => {} };
|
||||
|
||||
spyOn(this.template, 'querySelectorAll').and.returnValue([]);
|
||||
|
||||
DropDown.setImagesSrc(this.template);
|
||||
});
|
||||
|
||||
it('should call .querySelectorAll', function() {
|
||||
expect(this.template.querySelectorAll).toHaveBeenCalledWith('img[data-src]');
|
||||
});
|
||||
});
|
||||
|
||||
describe('show', function() {
|
||||
beforeEach(function() {
|
||||
this.list = { style: {} };
|
||||
this.dropdown = { list: this.list, hidden: true };
|
||||
|
||||
DropDown.prototype.show.call(this.dropdown);
|
||||
});
|
||||
|
||||
it('it should set .list display to block', function() {
|
||||
expect(this.list.style.display).toBe('block');
|
||||
});
|
||||
|
||||
it('it should set .hidden to false', function() {
|
||||
expect(this.dropdown.hidden).toBe(false);
|
||||
});
|
||||
|
||||
describe('if .hidden is false', function() {
|
||||
beforeEach(function() {
|
||||
this.list = { style: {} };
|
||||
this.dropdown = { list: this.list, hidden: false };
|
||||
|
||||
this.show = DropDown.prototype.show.call(this.dropdown);
|
||||
});
|
||||
|
||||
it('should return undefined', function() {
|
||||
expect(this.show).toEqual(undefined);
|
||||
});
|
||||
|
||||
it('should not set .list display to block', function() {
|
||||
expect(this.list.style.display).not.toEqual('block');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('hide', function() {
|
||||
beforeEach(function() {
|
||||
this.list = { style: {} };
|
||||
this.dropdown = { list: this.list };
|
||||
|
||||
DropDown.prototype.hide.call(this.dropdown);
|
||||
});
|
||||
|
||||
it('it should set .list display to none', function() {
|
||||
expect(this.list.style.display).toBe('none');
|
||||
});
|
||||
|
||||
it('it should set .hidden to true', function() {
|
||||
expect(this.dropdown.hidden).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('toggle', function() {
|
||||
beforeEach(function() {
|
||||
this.hidden = true;
|
||||
this.dropdown = { hidden: this.hidden, show: () => {}, hide: () => {} };
|
||||
|
||||
spyOn(this.dropdown, 'show');
|
||||
spyOn(this.dropdown, 'hide');
|
||||
|
||||
DropDown.prototype.toggle.call(this.dropdown);
|
||||
});
|
||||
|
||||
it('should call .show', function() {
|
||||
expect(this.dropdown.show).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe('if .hidden is false', function() {
|
||||
beforeEach(function() {
|
||||
this.hidden = false;
|
||||
this.dropdown = { hidden: this.hidden, show: () => {}, hide: () => {} };
|
||||
|
||||
spyOn(this.dropdown, 'show');
|
||||
spyOn(this.dropdown, 'hide');
|
||||
|
||||
DropDown.prototype.toggle.call(this.dropdown);
|
||||
});
|
||||
|
||||
it('should call .hide', function() {
|
||||
expect(this.dropdown.hide).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('destroy', function() {
|
||||
beforeEach(function() {
|
||||
this.list = { removeEventListener: () => {} };
|
||||
this.eventWrapper = { clickEvent: 'clickEvent' };
|
||||
this.dropdown = { list: this.list, hide: () => {}, eventWrapper: this.eventWrapper };
|
||||
|
||||
spyOn(this.list, 'removeEventListener');
|
||||
spyOn(this.dropdown, 'hide');
|
||||
|
||||
DropDown.prototype.destroy.call(this.dropdown);
|
||||
});
|
||||
|
||||
it('it should call .hide', function() {
|
||||
expect(this.dropdown.hide).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('it should call .removeEventListener', function() {
|
||||
expect(this.list.removeEventListener).toHaveBeenCalledWith(
|
||||
'click',
|
||||
this.eventWrapper.clickEvent,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -2197,26 +2197,6 @@ describe User do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#ultraauth_user?' do
|
||||
it 'is true if provider is ultraauth' do
|
||||
user = create(:omniauth_user, provider: 'ultraauth')
|
||||
|
||||
expect(user.ultraauth_user?).to be_truthy
|
||||
end
|
||||
|
||||
it 'is false with othe provider' do
|
||||
user = create(:omniauth_user, provider: 'not-ultraauth')
|
||||
|
||||
expect(user.ultraauth_user?).to be_falsey
|
||||
end
|
||||
|
||||
it 'is false if no extern_uid is provided' do
|
||||
user = create(:omniauth_user, extern_uid: nil)
|
||||
|
||||
expect(user.ldap_user?).to be_falsey
|
||||
end
|
||||
end
|
||||
|
||||
describe '#full_website_url' do
|
||||
let(:user) { create(:user) }
|
||||
|
||||
|
@ -3492,12 +3472,6 @@ describe User do
|
|||
|
||||
expect(user.allow_password_authentication_for_web?).to be_falsey
|
||||
end
|
||||
|
||||
it 'returns false for ultraauth user' do
|
||||
user = create(:omniauth_user, provider: 'ultraauth')
|
||||
|
||||
expect(user.allow_password_authentication_for_web?).to be_falsey
|
||||
end
|
||||
end
|
||||
|
||||
describe '#allow_password_authentication_for_git?' do
|
||||
|
@ -3520,12 +3494,6 @@ describe User do
|
|||
|
||||
expect(user.allow_password_authentication_for_git?).to be_falsey
|
||||
end
|
||||
|
||||
it 'returns false for ultraauth user' do
|
||||
user = create(:omniauth_user, provider: 'ultraauth')
|
||||
|
||||
expect(user.allow_password_authentication_for_git?).to be_falsey
|
||||
end
|
||||
end
|
||||
|
||||
describe '#assigned_open_merge_requests_count' do
|
||||
|
|
|
@ -14,7 +14,7 @@ describe UploaderHelper do
|
|||
end
|
||||
|
||||
describe '#extension_match?' do
|
||||
it 'returns false if file does not exists' do
|
||||
it 'returns false if file does not exist' do
|
||||
expect(uploader.file).to be_nil
|
||||
expect(uploader.send(:extension_match?, 'jpg')).to eq false
|
||||
end
|
||||
|
|
18
yarn.lock
18
yarn.lock
|
@ -3355,7 +3355,7 @@ cyclist@~0.2.2:
|
|||
resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-0.2.2.tgz#1b33792e11e914a2fd6d6ed6447464444e5fa640"
|
||||
integrity sha1-GzN5LhHpFKL9bW7WRHRkRE5fpkA=
|
||||
|
||||
d3-array@1, d3-array@^1.1.1, d3-array@^1.2.0:
|
||||
d3-array@1, "d3-array@1 - 2", d3-array@^1.1.1, d3-array@^1.2.0:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-1.2.1.tgz#d1ca33de2f6ac31efadb8e050a021d7e2396d5dc"
|
||||
integrity sha512-CyINJQ0SOUHojDdFDH4JEM0552vCR1utGyLHegJHyYH0JyCpSeTPxi4OBqHMA2jJZq4NH782LtaJWBImqI/HBw==
|
||||
|
@ -3489,6 +3489,14 @@ d3-random@1:
|
|||
resolved "https://registry.yarnpkg.com/d3-random/-/d3-random-1.1.0.tgz#6642e506c6fa3a648595d2b2469788a8d12529d3"
|
||||
integrity sha1-ZkLlBsb6OmSFldKyRpeIqNElKdM=
|
||||
|
||||
d3-sankey@^0.12.3:
|
||||
version "0.12.3"
|
||||
resolved "https://registry.yarnpkg.com/d3-sankey/-/d3-sankey-0.12.3.tgz#b3c268627bd72e5d80336e8de6acbfec9d15d01d"
|
||||
integrity sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==
|
||||
dependencies:
|
||||
d3-array "1 - 2"
|
||||
d3-shape "^1.2.0"
|
||||
|
||||
d3-scale-chromatic@1:
|
||||
version "1.3.3"
|
||||
resolved "https://registry.yarnpkg.com/d3-scale-chromatic/-/d3-scale-chromatic-1.3.3.tgz#dad4366f0edcb288f490128979c3c793583ed3c0"
|
||||
|
@ -3514,10 +3522,10 @@ d3-selection@1, d3-selection@^1.1.0, d3-selection@^1.2.0:
|
|||
resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-1.3.0.tgz#d53772382d3dc4f7507bfb28bcd2d6aed2a0ad6d"
|
||||
integrity sha512-qgpUOg9tl5CirdqESUAu0t9MU/t3O9klYfGfyKsXEmhyxyzLpzpeh08gaxBUTQw1uXIOkr/30Ut2YRjSSxlmHA==
|
||||
|
||||
d3-shape@1:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-1.2.0.tgz#45d01538f064bafd05ea3d6d2cb748fd8c41f777"
|
||||
integrity sha1-RdAVOPBkuv0F6j1tLLdI/YxB93c=
|
||||
d3-shape@1, d3-shape@^1.2.0:
|
||||
version "1.3.7"
|
||||
resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-1.3.7.tgz#df63801be07bc986bc54f63789b4fe502992b5d7"
|
||||
integrity sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==
|
||||
dependencies:
|
||||
d3-path "1"
|
||||
|
||||
|
|
Loading…
Reference in New Issue