Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-05-22 15:08:09 +00:00
parent 808c799a67
commit d6e421b21e
29 changed files with 1610 additions and 1028 deletions

View File

@ -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'

View File

@ -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)

View File

@ -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);
};

View File

@ -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?

View File

@ -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

View File

@ -0,0 +1,5 @@
---
title: Removed UltraAuth integration for OmniAuth
merge_request: 29330
author: Kartikey Tanna
type: removed

View File

@ -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

View File

@ -13883,6 +13883,7 @@ COPY "schema_migrations" (version) FROM STDIN;
20200506125731
20200506154421
20200507221434
20200508021128
20200508050301
20200508091106
20200511080113

View File

@ -32,4 +32,3 @@ providers:
- [Shibboleth](../../integration/shibboleth.md)
- [Smartcard](smartcard.md) **(PREMIUM ONLY)**
- [Twitter](../../integration/twitter.md)
- [UltraAuth](../../integration/ultra_auth.md)

View File

@ -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.

View File

@ -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.

View File

@ -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

View File

@ -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)

View File

@ -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.

View File

@ -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`. |

View File

@ -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)

View File

@ -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)

View File

@ -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",

View File

@ -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

View File

@ -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,
);
});
});
});

View File

@ -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' }],
},
],
},
],
};

View File

@ -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);
});
});
});

View File

@ -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,
);
});
});
});

View File

@ -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

View File

@ -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

View File

@ -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"