mirror of
https://github.com/scm-manager/scm-manager.git
synced 2026-01-21 06:52:11 +01:00
merge 2.0.0-m3
This commit is contained in:
3
.dockerignore
Normal file
3
.dockerignore
Normal file
@@ -0,0 +1,3 @@
|
||||
# ignore everything except scm-server.tar.gz
|
||||
**
|
||||
!scm-server/target/*.tar.gz
|
||||
20
Dockerfile
Normal file
20
Dockerfile
Normal file
@@ -0,0 +1,20 @@
|
||||
FROM openjdk:8u171-alpine3.8
|
||||
|
||||
ENV SCM_HOME=/var/lib/scm
|
||||
|
||||
RUN set -x \
|
||||
&& apk add --no-cache mercurial bash \
|
||||
&& addgroup -S -g 1000 scm \
|
||||
&& adduser -S -s /bin/false -G scm -h /opt/scm-server -D -H -u 1000 scm \
|
||||
&& mkdir ${SCM_HOME} \
|
||||
&& chown scm:scm ${SCM_HOME}
|
||||
|
||||
ADD scm-server/target/scm-server-app.tar.gz /opt
|
||||
RUN chown -R scm:scm /opt/scm-server
|
||||
|
||||
WORKDIR /opt/scm-server
|
||||
VOLUME [ "${SCM_HOME}", "/opt/scm-server/var/log" ]
|
||||
EXPOSE 8080
|
||||
USER scm
|
||||
|
||||
ENTRYPOINT [ "/opt/scm-server/bin/scm-server" ]
|
||||
37
Jenkinsfile
vendored
37
Jenkinsfile
vendored
@@ -4,14 +4,15 @@
|
||||
@Library('github.com/cloudogu/ces-build-lib@59d3e94')
|
||||
import com.cloudogu.ces.cesbuildlib.*
|
||||
|
||||
node() { // No specific label
|
||||
node('docker') {
|
||||
|
||||
// Change this as when we go back to default - necessary for proper SonarQube analysis
|
||||
mainBranch = "2.0.0-m3"
|
||||
|
||||
properties([
|
||||
// Keep only the last 10 build to preserve space
|
||||
buildDiscarder(logRotator(numToKeepStr: '10'))
|
||||
buildDiscarder(logRotator(numToKeepStr: '10')),
|
||||
disableConcurrentBuilds()
|
||||
])
|
||||
|
||||
timeout(activity: true, time: 20, unit: 'MINUTES') {
|
||||
@@ -44,6 +45,26 @@ node() { // No specific label
|
||||
currentBuild.result = 'UNSTABLE'
|
||||
}
|
||||
}
|
||||
|
||||
def commitHash = getCommitHash()
|
||||
def dockerImageTag = "2.0.0-dev-${commitHash.substring(0,7)}-${BUILD_NUMBER}"
|
||||
|
||||
if (isMainBranch()) {
|
||||
stage('Docker') {
|
||||
def image = docker.build('cloudogu/scm-manager')
|
||||
docker.withRegistry('', 'hub.docker.com-cesmarvin') {
|
||||
image.push(dockerImageTag)
|
||||
image.push('latest')
|
||||
}
|
||||
}
|
||||
|
||||
stage('Deployment') {
|
||||
build job: 'scm-manager/next-scm.cloudogu.com', propagate: false, wait: false, parameters: [
|
||||
string(name: 'changeset', value: commitHash),
|
||||
string(name: 'imageTag', value: dockerImageTag)
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Archive Unit and integration test results, if any
|
||||
@@ -62,7 +83,7 @@ Maven setupMavenBuild() {
|
||||
// Keep this version number in sync with .mvn/maven-wrapper.properties
|
||||
Maven mvn = new MavenInDocker(this, "3.5.2-jdk-8")
|
||||
|
||||
if (mainBranch.equals(env.BRANCH_NAME)) {
|
||||
if (isMainBranch()) {
|
||||
// Release starts javadoc, which takes very long, so do only for certain branches
|
||||
mvn.additionalArgs += ' -DperformRelease'
|
||||
// JDK8 is more strict, we should fix this before the next release. Right now, this is just not the focus, yet.
|
||||
@@ -89,7 +110,7 @@ void analyzeWith(Maven mvn) {
|
||||
"-Dsonar.pullrequest.bitbucketcloud.repository=scm-manager "
|
||||
} else {
|
||||
mvnArgs += " -Dsonar.branch.name=${env.BRANCH_NAME} "
|
||||
if (!mainBranch.equals(env.BRANCH_NAME)) {
|
||||
if (!isMainBranch()) {
|
||||
// Avoid exception "The main branch must not have a target" on main branch
|
||||
mvnArgs += " -Dsonar.branch.target=${mainBranch} "
|
||||
}
|
||||
@@ -98,6 +119,10 @@ void analyzeWith(Maven mvn) {
|
||||
}
|
||||
}
|
||||
|
||||
boolean isMainBranch() {
|
||||
return mainBranch.equals(env.BRANCH_NAME)
|
||||
}
|
||||
|
||||
boolean waitForQualityGateWebhookToBeCalled() {
|
||||
boolean isQualityGateSucceeded = true
|
||||
timeout(time: 2, unit: 'MINUTES') { // Needed when there is no webhook for example
|
||||
@@ -114,6 +139,10 @@ String getCommitAuthorComplete() {
|
||||
new Sh(this).returnStdOut 'hg log --branch . --limit 1 --template "{author}"'
|
||||
}
|
||||
|
||||
String getCommitHash() {
|
||||
new Sh(this).returnStdOut 'hg log --branch . --limit 1 --template "{node}"'
|
||||
}
|
||||
|
||||
String getCommitAuthorEmail() {
|
||||
def matcher = getCommitAuthorComplete() =~ "<(.*?)>"
|
||||
matcher ? matcher[0][1] : ""
|
||||
|
||||
21
deployments/helm/.helmignore
Normal file
21
deployments/helm/.helmignore
Normal file
@@ -0,0 +1,21 @@
|
||||
# Patterns to ignore when building packages.
|
||||
# This supports shell glob matching, relative path matching, and
|
||||
# negation (prefixed with !). Only one pattern per line.
|
||||
.DS_Store
|
||||
# Common VCS dirs
|
||||
.git/
|
||||
.gitignore
|
||||
.bzr/
|
||||
.bzrignore
|
||||
.hg/
|
||||
.hgignore
|
||||
.svn/
|
||||
# Common backup files
|
||||
*.swp
|
||||
*.bak
|
||||
*.tmp
|
||||
*~
|
||||
# Various IDEs
|
||||
.project
|
||||
.idea/
|
||||
*.tmproj
|
||||
5
deployments/helm/Chart.yaml
Normal file
5
deployments/helm/Chart.yaml
Normal file
@@ -0,0 +1,5 @@
|
||||
apiVersion: v1
|
||||
appVersion: "1.0"
|
||||
description: A Helm chart for SCM-Manager
|
||||
name: scm-manager
|
||||
version: 0.1.0
|
||||
19
deployments/helm/templates/NOTES.txt
Normal file
19
deployments/helm/templates/NOTES.txt
Normal file
@@ -0,0 +1,19 @@
|
||||
1. Get the application URL by running these commands:
|
||||
{{- if .Values.ingress.enabled }}
|
||||
{{- range .Values.ingress.hosts }}
|
||||
http{{ if $.Values.ingress.tls }}s{{ end }}://{{ . }}{{ $.Values.ingress.path }}
|
||||
{{- end }}
|
||||
{{- else if contains "NodePort" .Values.service.type }}
|
||||
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "scm-manager.fullname" . }})
|
||||
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
|
||||
echo http://$NODE_IP:$NODE_PORT
|
||||
{{- else if contains "LoadBalancer" .Values.service.type }}
|
||||
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
|
||||
You can watch the status of by running 'kubectl get svc -w {{ include "scm-manager.fullname" . }}'
|
||||
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "scm-manager.fullname" . }} -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
|
||||
echo http://$SERVICE_IP:{{ .Values.service.port }}
|
||||
{{- else if contains "ClusterIP" .Values.service.type }}
|
||||
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app={{ include "scm-manager.name" . }},release={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
|
||||
echo "Visit http://127.0.0.1:8080 to use your application"
|
||||
kubectl port-forward $POD_NAME 8080:80
|
||||
{{- end }}
|
||||
32
deployments/helm/templates/_helpers.tpl
Normal file
32
deployments/helm/templates/_helpers.tpl
Normal file
@@ -0,0 +1,32 @@
|
||||
{{/* vim: set filetype=mustache: */}}
|
||||
{{/*
|
||||
Expand the name of the chart.
|
||||
*/}}
|
||||
{{- define "scm-manager.name" -}}
|
||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Create a default fully qualified app name.
|
||||
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
|
||||
If release name contains chart name it will be used as a full name.
|
||||
*/}}
|
||||
{{- define "scm-manager.fullname" -}}
|
||||
{{- if .Values.fullnameOverride -}}
|
||||
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
|
||||
{{- else -}}
|
||||
{{- $name := default .Chart.Name .Values.nameOverride -}}
|
||||
{{- if contains $name .Release.Name -}}
|
||||
{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
|
||||
{{- else -}}
|
||||
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Create chart name and version as used by the chart label.
|
||||
*/}}
|
||||
{{- define "scm-manager.chart" -}}
|
||||
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
160
deployments/helm/templates/configmap.yaml
Normal file
160
deployments/helm/templates/configmap.yaml
Normal file
@@ -0,0 +1,160 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: {{ include "scm-manager.fullname" . }}
|
||||
labels:
|
||||
app: {{ include "scm-manager.name" . }}
|
||||
chart: "{{ .Chart.Name }}-{{ .Chart.Version }}"
|
||||
release: "{{ .Release.Name }}"
|
||||
heritage: "{{ .Release.Service }}"
|
||||
data:
|
||||
server-config.xml: |
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_0.dtd">
|
||||
<Configure id="ScmServer" class="org.eclipse.jetty.server.Server">
|
||||
|
||||
<New id="httpConfig" class="org.eclipse.jetty.server.HttpConfiguration">
|
||||
<!-- increase header size for mercurial -->
|
||||
<Set name="requestHeaderSize">16384</Set>
|
||||
<Set name="responseHeaderSize">16384</Set>
|
||||
|
||||
{{- if .Values.ingress.enabled -}}
|
||||
<!--
|
||||
We have to enable ForwardedRequestCustomizer in order to understand X-Forwarded-xxx headers.
|
||||
Without the ForwardedRequestCustomizer, scm will possibly generate wrong links
|
||||
-->
|
||||
<Call name="addCustomizer">
|
||||
<Arg><New class="org.eclipse.jetty.server.ForwardedRequestCustomizer"/></Arg>
|
||||
</Call>
|
||||
{{- end }}
|
||||
</New>
|
||||
|
||||
<!--
|
||||
Connectors
|
||||
-->
|
||||
<Call name="addConnector">
|
||||
<Arg>
|
||||
<New class="org.eclipse.jetty.server.ServerConnector">
|
||||
<Arg name="server">
|
||||
<Ref refid="ScmServer" />
|
||||
</Arg>
|
||||
<Arg name="factories">
|
||||
<Array type="org.eclipse.jetty.server.ConnectionFactory">
|
||||
<Item>
|
||||
<New class="org.eclipse.jetty.server.HttpConnectionFactory">
|
||||
<Arg name="config">
|
||||
<Ref refid="httpConfig" />
|
||||
</Arg>
|
||||
</New>
|
||||
</Item>
|
||||
</Array>
|
||||
</Arg>
|
||||
<Set name="port">
|
||||
<SystemProperty name="jetty.port" default="8080" />
|
||||
</Set>
|
||||
</New>
|
||||
</Arg>
|
||||
</Call>
|
||||
|
||||
<New id="scm-webapp" class="org.eclipse.jetty.webapp.WebAppContext">
|
||||
<Set name="contextPath">/scm</Set>
|
||||
<Set name="war">
|
||||
<SystemProperty name="basedir" default="."/>/var/webapp/scm-webapp.war</Set>
|
||||
<!-- disable directory listings -->
|
||||
<Call name="setInitParameter">
|
||||
<Arg>org.eclipse.jetty.servlet.Default.dirAllowed</Arg>
|
||||
<Arg>false</Arg>
|
||||
</Call>
|
||||
<Set name="tempDirectory">
|
||||
<SystemProperty name="basedir" default="."/>/work/scm
|
||||
</Set>
|
||||
</New>
|
||||
|
||||
<New id="docroot" class="org.eclipse.jetty.webapp.WebAppContext">
|
||||
<Set name="contextPath">/</Set>
|
||||
<Set name="baseResource">
|
||||
<New class="org.eclipse.jetty.util.resource.ResourceCollection">
|
||||
<Arg>
|
||||
<Array type="java.lang.String">
|
||||
<Item>
|
||||
<SystemProperty name="basedir" default="."/>/var/webapp/docroot</Item>
|
||||
</Array>
|
||||
</Arg>
|
||||
</New>
|
||||
</Set>
|
||||
<Set name="tempDirectory">
|
||||
<SystemProperty name="basedir" default="."/>/work/docroot
|
||||
</Set>
|
||||
</New>
|
||||
|
||||
<Set name="handler">
|
||||
<New class="org.eclipse.jetty.server.handler.HandlerCollection">
|
||||
<Set name="handlers">
|
||||
<Array type="org.eclipse.jetty.server.Handler">
|
||||
<Item>
|
||||
<Ref id="scm-webapp" />
|
||||
</Item>
|
||||
<Item>
|
||||
<Ref id="docroot" />
|
||||
</Item>
|
||||
</Array>
|
||||
</Set>
|
||||
</New>
|
||||
</Set>
|
||||
|
||||
</Configure>
|
||||
|
||||
logging.xml: |
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<configuration>
|
||||
|
||||
<--
|
||||
in a container environment we only need stdout
|
||||
-->
|
||||
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||
|
||||
<encoder>
|
||||
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n</pattern>
|
||||
</encoder>
|
||||
|
||||
</appender>
|
||||
|
||||
<logger name="sonia.scm" level="INFO" />
|
||||
|
||||
<!-- suppress massive gzip logging -->
|
||||
<logger name="sonia.scm.filter.GZipFilter" level="WARN" />
|
||||
<logger name="sonia.scm.filter.GZipResponseStream" level="WARN" />
|
||||
|
||||
<logger name="sonia.scm.util.ServiceUtil" level="WARN" />
|
||||
|
||||
<!-- event bus -->
|
||||
<logger name="sonia.scm.event.LegmanScmEventBus" level="INFO" />
|
||||
|
||||
<!-- shiro -->
|
||||
<!--
|
||||
<logger name="org.apache.shiro" level="INFO" />
|
||||
<logger name="org.apache.shiro.authc.pam.ModularRealmAuthenticator" level="DEBUG" />
|
||||
-->
|
||||
|
||||
<!-- svnkit -->
|
||||
<!--
|
||||
<logger name="svnkit" level="WARN" />
|
||||
<logger name="svnkit.network" level="DEBUG" />
|
||||
<logger name="svnkit.fsfs" level="WARN" />
|
||||
-->
|
||||
|
||||
<!-- javahg -->
|
||||
<!--
|
||||
<logger name="com.aragost.javahg" level="DEBUG" />
|
||||
-->
|
||||
|
||||
<!-- ehcache -->
|
||||
<!--
|
||||
<logger name="net.sf.ehcache" level="DEBUG" />
|
||||
-->
|
||||
|
||||
<root level="WARN">
|
||||
<appender-ref ref="STDOUT" />
|
||||
</root>
|
||||
|
||||
</configuration>
|
||||
77
deployments/helm/templates/deployment.yaml
Normal file
77
deployments/helm/templates/deployment.yaml
Normal file
@@ -0,0 +1,77 @@
|
||||
apiVersion: apps/v1beta2
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "scm-manager.fullname" . }}
|
||||
labels:
|
||||
app: {{ include "scm-manager.name" . }}
|
||||
chart: {{ include "scm-manager.chart" . }}
|
||||
release: {{ .Release.Name }}
|
||||
heritage: {{ .Release.Service }}
|
||||
spec:
|
||||
replicas: 1 # could not be scaled
|
||||
strategy:
|
||||
type: Recreate
|
||||
selector:
|
||||
matchLabels:
|
||||
app: {{ include "scm-manager.name" . }}
|
||||
release: {{ .Release.Name }}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: {{ include "scm-manager.name" . }}
|
||||
release: {{ .Release.Name }}
|
||||
spec:
|
||||
initContainers:
|
||||
- name: volume-permissions
|
||||
image: alpine:3.8
|
||||
imagePullPolicy: IfNotPresent
|
||||
command: ['sh', '-c', 'chown 1000:1000 /data']
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /data
|
||||
containers:
|
||||
- name: {{ .Chart.Name }}
|
||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 8080
|
||||
protocol: TCP
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /scm
|
||||
port: http
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /scm
|
||||
port: http
|
||||
resources:
|
||||
{{ toYaml .Values.resources | indent 12 }}
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /var/lib/scm
|
||||
- name: config
|
||||
mountPath: /opt/scm-server/conf
|
||||
volumes:
|
||||
- name: data
|
||||
{{- if .Values.persistence.enabled }}
|
||||
persistentVolumeClaim:
|
||||
claimName: {{ include "scm-manager.fullname" . }}
|
||||
{{- else }}
|
||||
emptyDir: {}
|
||||
{{- end }}
|
||||
- name: config
|
||||
configMap:
|
||||
name: {{ include "scm-manager.fullname" . }}
|
||||
{{- with .Values.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{ toYaml . | indent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.affinity }}
|
||||
affinity:
|
||||
{{ toYaml . | indent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.tolerations }}
|
||||
tolerations:
|
||||
{{ toYaml . | indent 8 }}
|
||||
{{- end }}
|
||||
38
deployments/helm/templates/ingress.yaml
Normal file
38
deployments/helm/templates/ingress.yaml
Normal file
@@ -0,0 +1,38 @@
|
||||
{{- if .Values.ingress.enabled -}}
|
||||
{{- $fullName := include "scm-manager.fullname" . -}}
|
||||
{{- $ingressPath := .Values.ingress.path -}}
|
||||
apiVersion: extensions/v1beta1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: {{ $fullName }}
|
||||
labels:
|
||||
app: {{ include "scm-manager.name" . }}
|
||||
chart: {{ include "scm-manager.chart" . }}
|
||||
release: {{ .Release.Name }}
|
||||
heritage: {{ .Release.Service }}
|
||||
{{- with .Values.ingress.annotations }}
|
||||
annotations:
|
||||
{{ toYaml . | indent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- if .Values.ingress.tls }}
|
||||
tls:
|
||||
{{- range .Values.ingress.tls }}
|
||||
- hosts:
|
||||
{{- range .hosts }}
|
||||
- {{ . | quote }}
|
||||
{{- end }}
|
||||
secretName: {{ .secretName }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
rules:
|
||||
{{- range .Values.ingress.hosts }}
|
||||
- host: {{ . | quote }}
|
||||
http:
|
||||
paths:
|
||||
- path: {{ $ingressPath }}
|
||||
backend:
|
||||
serviceName: {{ $fullName }}
|
||||
servicePort: http
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
24
deployments/helm/templates/pvc.yaml
Normal file
24
deployments/helm/templates/pvc.yaml
Normal file
@@ -0,0 +1,24 @@
|
||||
{{- if .Values.persistence.enabled -}}
|
||||
kind: PersistentVolumeClaim
|
||||
apiVersion: v1
|
||||
metadata:
|
||||
name: {{ include "scm-manager.fullname" . }}
|
||||
labels:
|
||||
app: {{ include "scm-manager.name" . }}
|
||||
chart: "{{ .Chart.Name }}-{{ .Chart.Version }}"
|
||||
release: "{{ .Release.Name }}"
|
||||
heritage: "{{ .Release.Service }}"
|
||||
spec:
|
||||
accessModes:
|
||||
- {{ .Values.persistence.accessMode | quote }}
|
||||
resources:
|
||||
requests:
|
||||
storage: {{ .Values.persistence.size | quote }}
|
||||
{{- if .Values.persistence.storageClass }}
|
||||
{{- if (eq "-" .Values.persistence.storageClass) }}
|
||||
storageClassName: ""
|
||||
{{- else }}
|
||||
storageClassName: "{{ .Values.persistence.storageClass }}"
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end -}}
|
||||
19
deployments/helm/templates/service.yaml
Normal file
19
deployments/helm/templates/service.yaml
Normal file
@@ -0,0 +1,19 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "scm-manager.fullname" . }}
|
||||
labels:
|
||||
app: {{ include "scm-manager.name" . }}
|
||||
chart: {{ include "scm-manager.chart" . }}
|
||||
release: {{ .Release.Name }}
|
||||
heritage: {{ .Release.Service }}
|
||||
spec:
|
||||
type: {{ .Values.service.type }}
|
||||
ports:
|
||||
- port: {{ .Values.service.port }}
|
||||
targetPort: 8080
|
||||
protocol: TCP
|
||||
name: http
|
||||
selector:
|
||||
app: {{ include "scm-manager.name" . }}
|
||||
release: {{ .Release.Name }}
|
||||
65
deployments/helm/values.yaml
Normal file
65
deployments/helm/values.yaml
Normal file
@@ -0,0 +1,65 @@
|
||||
# Default values for scm-manager.
|
||||
# This is a YAML-formatted file.
|
||||
# Declare variables to be passed into your templates.
|
||||
|
||||
# replicaCount: 1
|
||||
|
||||
image:
|
||||
repository: cloudogu/scm-manager
|
||||
# TODO change after release, to something more stable
|
||||
tag: latest
|
||||
pullPolicy: Always
|
||||
|
||||
nameOverride: ""
|
||||
fullnameOverride: ""
|
||||
|
||||
service:
|
||||
type: LoadBalancer
|
||||
port: 80
|
||||
|
||||
ingress:
|
||||
enabled: false
|
||||
annotations: {}
|
||||
# kubernetes.io/ingress.class: nginx
|
||||
# kubernetes.io/tls-acme: "true"
|
||||
path: /
|
||||
hosts:
|
||||
- scm-manager.local
|
||||
tls: []
|
||||
# - secretName: scm-manager-tls
|
||||
# hosts:
|
||||
# - scm-manager.local
|
||||
|
||||
## Enable persistence using Persistent Volume Claims
|
||||
## ref: http://kubernetes.io/docs/user-guide/persistent-volumes/
|
||||
##
|
||||
persistence:
|
||||
enabled: true
|
||||
## ghost data Persistent Volume Storage Class
|
||||
## If defined, storageClassName: <storageClass>
|
||||
## If set to "-", storageClassName: "", which disables dynamic provisioning
|
||||
## If undefined (the default) or set to null, no storageClassName spec is
|
||||
## set, choosing the default provisioner. (gp2 on AWS, standard on
|
||||
## GKE, AWS & OpenStack)
|
||||
##
|
||||
# storageClass: "-"
|
||||
accessMode: ReadWriteOnce
|
||||
size: 12Gi
|
||||
|
||||
resources:
|
||||
# We usually recommend not to specify default resources and to leave this as a conscious
|
||||
# choice for the user. This also increases chances charts run on environments with little
|
||||
# resources, such as Minikube. If you do want to specify resources, uncomment the following
|
||||
# lines, adjust them as necessary, and remove the curly braces after 'resources:'.
|
||||
limits:
|
||||
cpu: 2000m
|
||||
memory: 2048Mi
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 256Mi
|
||||
|
||||
nodeSelector: {}
|
||||
|
||||
tolerations: []
|
||||
|
||||
affinity: {}
|
||||
7
pom.xml
7
pom.xml
@@ -188,15 +188,14 @@
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- TODO replace by proper version from maven central (group: com.github.sdorra) once its there. -->
|
||||
<dependency>
|
||||
<groupId>com.github.sdorra.shiro-static-permissions</groupId>
|
||||
<groupId>com.github.sdorra</groupId>
|
||||
<artifactId>ssp-lib</artifactId>
|
||||
<version>${ssp.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.github.sdorra.shiro-static-permissions</groupId>
|
||||
<groupId>com.github.sdorra</groupId>
|
||||
<artifactId>ssp-processor</artifactId>
|
||||
<version>${ssp.version}</version>
|
||||
<optional>true</optional>
|
||||
@@ -765,7 +764,7 @@
|
||||
<jetty.maven.version>9.2.10.v20150310</jetty.maven.version>
|
||||
|
||||
<!-- security libraries -->
|
||||
<ssp.version>967c8fd521</ssp.version>
|
||||
<ssp.version>1.1.0</ssp.version>
|
||||
<shiro.version>1.4.0</shiro.version>
|
||||
|
||||
<!-- repostitory libraries -->
|
||||
|
||||
@@ -94,6 +94,12 @@
|
||||
<artifactId>javax.ws.rs-api</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.jboss.resteasy</groupId>
|
||||
<artifactId>resteasy-jaxrs</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- json -->
|
||||
|
||||
<dependency>
|
||||
@@ -160,14 +166,13 @@
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- TODO replace by proper version from maven central (group: com.github.sdorra) once its there. -->
|
||||
<dependency>
|
||||
<groupId>com.github.sdorra.shiro-static-permissions</groupId>
|
||||
<groupId>com.github.sdorra</groupId>
|
||||
<artifactId>ssp-lib</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.github.sdorra.shiro-static-permissions</groupId>
|
||||
<groupId>com.github.sdorra</groupId>
|
||||
<artifactId>ssp-processor</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
@@ -7,9 +7,4 @@ public class NotFoundException extends Exception {
|
||||
|
||||
public NotFoundException() {
|
||||
}
|
||||
|
||||
|
||||
public NotFoundException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,11 +4,11 @@ import java.net.URI;
|
||||
|
||||
public interface ScmPathInfo {
|
||||
|
||||
String REST_API_PATH = "/api/rest";
|
||||
String REST_API_PATH = "/api";
|
||||
|
||||
URI getApiRestUri();
|
||||
|
||||
default URI getRootUri() {
|
||||
return getApiRestUri().resolve("../..");
|
||||
return getApiRestUri().resolve("..");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ import com.github.sdorra.ssp.StaticPermissions;
|
||||
@StaticPermissions(
|
||||
value = "configuration",
|
||||
permissions = {"read", "write"},
|
||||
globalPermissions = {}
|
||||
globalPermissions = {"list"}
|
||||
)
|
||||
public interface Configuration extends PermissionObject {
|
||||
}
|
||||
|
||||
@@ -1,125 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2010, Sebastian Sdorra
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
* 3. Neither the name of SCM-Manager; nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from this
|
||||
* software without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
|
||||
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*
|
||||
* http://bitbucket.org/sdorra/scm-manager
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
|
||||
package sonia.scm.filter;
|
||||
|
||||
//~--- non-JDK imports --------------------------------------------------------
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import sonia.scm.Priority;
|
||||
import sonia.scm.util.WebUtil;
|
||||
import sonia.scm.web.filter.HttpFilter;
|
||||
|
||||
//~--- JDK imports ------------------------------------------------------------
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
/**
|
||||
* Filter for gzip encoding.
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
* @since 1.15
|
||||
*/
|
||||
@Priority(Filters.PRIORITY_PRE_BASEURL)
|
||||
@WebElement(value = Filters.PATTERN_RESOURCE_REGEX, regex = true)
|
||||
public class GZipFilter extends HttpFilter
|
||||
{
|
||||
|
||||
/**
|
||||
* the logger for GZipFilter
|
||||
*/
|
||||
private static final Logger logger =
|
||||
LoggerFactory.getLogger(GZipFilter.class);
|
||||
|
||||
//~--- get methods ----------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Return the configuration for the gzip filter.
|
||||
*
|
||||
*
|
||||
* @return gzip filter configuration
|
||||
*/
|
||||
public GZipFilterConfig getConfig()
|
||||
{
|
||||
return config;
|
||||
}
|
||||
|
||||
//~--- methods --------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Encodes the response, if the request has support for gzip encoding.
|
||||
*
|
||||
*
|
||||
* @param request http request
|
||||
* @param response http response
|
||||
* @param chain filter chain
|
||||
*
|
||||
* @throws IOException
|
||||
* @throws ServletException
|
||||
*/
|
||||
@Override
|
||||
protected void doFilter(HttpServletRequest request,
|
||||
HttpServletResponse response, FilterChain chain)
|
||||
throws IOException, ServletException
|
||||
{
|
||||
if (WebUtil.isGzipSupported(request))
|
||||
{
|
||||
if (logger.isTraceEnabled())
|
||||
{
|
||||
logger.trace("compress output with gzip");
|
||||
}
|
||||
|
||||
GZipResponseWrapper wrappedResponse = new GZipResponseWrapper(response,
|
||||
config);
|
||||
|
||||
chain.doFilter(request, wrappedResponse);
|
||||
wrappedResponse.finishResponse();
|
||||
}
|
||||
else
|
||||
{
|
||||
chain.doFilter(request, response);
|
||||
}
|
||||
}
|
||||
|
||||
//~--- fields ---------------------------------------------------------------
|
||||
|
||||
/** gzip filter configuration */
|
||||
private GZipFilterConfig config = new GZipFilterConfig();
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package sonia.scm.filter;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import sonia.scm.util.WebUtil;
|
||||
|
||||
import javax.ws.rs.container.ContainerRequestContext;
|
||||
import javax.ws.rs.container.ContainerResponseContext;
|
||||
import javax.ws.rs.container.ContainerResponseFilter;
|
||||
import javax.ws.rs.ext.Provider;
|
||||
import java.io.IOException;
|
||||
import java.util.zip.GZIPOutputStream;
|
||||
|
||||
@Provider
|
||||
@Slf4j
|
||||
public class GZipResponseFilter implements ContainerResponseFilter {
|
||||
public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) throws IOException {
|
||||
if (WebUtil.isGzipSupported(requestContext::getHeaderString)) {
|
||||
log.trace("compress output with gzip");
|
||||
GZIPOutputStream wrappedResponse = new GZIPOutputStream(responseContext.getEntityStream());
|
||||
responseContext.getHeaders().add("Content-Encoding", "gzip");
|
||||
responseContext.setEntityStream(wrappedResponse);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -60,7 +60,7 @@ import java.util.List;
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
*/
|
||||
@StaticPermissions("group")
|
||||
@StaticPermissions(value = "group", globalPermissions = {"create", "list"})
|
||||
@XmlRootElement(name = "groups")
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class Group extends BasicPropertiesAware
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package sonia.scm.user;
|
||||
|
||||
public class ChangePasswordNotAllowedException extends RuntimeException {
|
||||
|
||||
public static final String WRONG_USER_TYPE = "User of type {0} are not allowed to change password";
|
||||
|
||||
public ChangePasswordNotAllowedException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package sonia.scm.user;
|
||||
|
||||
public class InvalidPasswordException extends RuntimeException {
|
||||
|
||||
public static final String INVALID_MATCHING = "The given Password does not match with the stored one.";
|
||||
|
||||
public InvalidPasswordException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -55,11 +55,10 @@ import java.security.Principal;
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
*/
|
||||
@StaticPermissions("user")
|
||||
@StaticPermissions(value = "user", globalPermissions = {"create", "list"})
|
||||
@XmlRootElement(name = "users")
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class
|
||||
User extends BasicPropertiesAware implements Principal, ModelObject, PermissionObject
|
||||
public class User extends BasicPropertiesAware implements Principal, ModelObject, PermissionObject
|
||||
{
|
||||
|
||||
/** Field description */
|
||||
@@ -274,6 +273,10 @@ User extends BasicPropertiesAware implements Principal, ModelObject, PermissionO
|
||||
//J+
|
||||
}
|
||||
|
||||
public User changePassword(String password){
|
||||
setPassword(password);
|
||||
return this;
|
||||
}
|
||||
//~--- get methods ----------------------------------------------------------
|
||||
|
||||
/**
|
||||
|
||||
@@ -38,6 +38,11 @@ package sonia.scm.user;
|
||||
import sonia.scm.Manager;
|
||||
import sonia.scm.search.Searchable;
|
||||
|
||||
import java.text.MessageFormat;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static sonia.scm.user.ChangePasswordNotAllowedException.WRONG_USER_TYPE;
|
||||
|
||||
/**
|
||||
* The central class for managing {@link User} objects.
|
||||
* This class is a singleton and is available via injection.
|
||||
@@ -68,4 +73,22 @@ public interface UserManager
|
||||
* @since 1.14
|
||||
*/
|
||||
public String getDefaultType();
|
||||
|
||||
|
||||
/**
|
||||
* Only account of the default type "xml" can change their password
|
||||
*/
|
||||
default Consumer<User> getUserTypeChecker() {
|
||||
return user -> {
|
||||
if (!isTypeDefault(user)) {
|
||||
throw new ChangePasswordNotAllowedException(MessageFormat.format(WRONG_USER_TYPE, user.getType()));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
default boolean isTypeDefault(User user) {
|
||||
return getDefaultType().equals(user.getType());
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -49,6 +49,7 @@ import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
import java.util.TimeZone;
|
||||
import java.util.function.Function;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
@@ -266,7 +267,12 @@ public final class WebUtil
|
||||
*/
|
||||
public static boolean isGzipSupported(HttpServletRequest request)
|
||||
{
|
||||
String enc = request.getHeader(HEADER_ACCEPTENCODING);
|
||||
return isGzipSupported(request::getHeader);
|
||||
}
|
||||
|
||||
public static boolean isGzipSupported(Function<String, String> headerResolver)
|
||||
{
|
||||
String enc = headerResolver.apply(HEADER_ACCEPTENCODING);
|
||||
|
||||
return (enc != null) && enc.contains("gzip");
|
||||
}
|
||||
|
||||
40
scm-core/src/main/java/sonia/scm/web/JsonEnricherBase.java
Normal file
40
scm-core/src/main/java/sonia/scm/web/JsonEnricherBase.java
Normal file
@@ -0,0 +1,40 @@
|
||||
package sonia.scm.web;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public abstract class JsonEnricherBase implements JsonEnricher {
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
protected JsonEnricherBase(ObjectMapper objectMapper) {
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
protected boolean resultHasMediaType(String mediaType, JsonEnricherContext context) {
|
||||
return mediaType.equals(context.getResponseMediaType().toString());
|
||||
}
|
||||
|
||||
protected JsonNode value(Object object) {
|
||||
return objectMapper.convertValue(object, JsonNode.class);
|
||||
}
|
||||
|
||||
protected ObjectNode createObject() {
|
||||
return objectMapper.createObjectNode();
|
||||
}
|
||||
|
||||
protected ObjectNode createObject(Map<String, Object> values) {
|
||||
ObjectNode object = createObject();
|
||||
|
||||
values.forEach((key, value) -> object.set(key, value(value)));
|
||||
|
||||
return object;
|
||||
}
|
||||
|
||||
protected void addPropertyNode(JsonNode parent, String newKey, JsonNode child) {
|
||||
((ObjectNode) parent).set(newKey, child);
|
||||
}
|
||||
}
|
||||
@@ -15,13 +15,14 @@ public class VndMediaType {
|
||||
public static final String PLAIN_TEXT_PREFIX = "text/" + SUBTYPE_PREFIX;
|
||||
public static final String PLAIN_TEXT_SUFFIX = "+plain;v=" + VERSION;
|
||||
|
||||
public static final String INDEX = PREFIX + "index" + SUFFIX;
|
||||
public static final String USER = PREFIX + "user" + SUFFIX;
|
||||
public static final String GROUP = PREFIX + "group" + SUFFIX;
|
||||
public static final String REPOSITORY = PREFIX + "repository" + SUFFIX;
|
||||
public static final String PERMISSION = PREFIX + "permission" + SUFFIX;
|
||||
public static final String CHANGESET = PREFIX + "changeset" + SUFFIX;
|
||||
public static final String CHANGESET_COLLECTION = PREFIX + "changesetCollection" + SUFFIX;
|
||||
public static final String MODIFICATIONS = PREFIX + "modifications" + SUFFIX;;
|
||||
public static final String MODIFICATIONS = PREFIX + "modifications" + SUFFIX;
|
||||
public static final String TAG = PREFIX + "tag" + SUFFIX;
|
||||
public static final String TAG_COLLECTION = PREFIX + "tagCollection" + SUFFIX;
|
||||
public static final String BRANCH = PREFIX + "branch" + SUFFIX;
|
||||
@@ -35,6 +36,8 @@ public class VndMediaType {
|
||||
public static final String REPOSITORY_TYPE = PREFIX + "repositoryType" + SUFFIX;
|
||||
public static final String UI_PLUGIN = PREFIX + "uiPlugin" + SUFFIX;
|
||||
public static final String UI_PLUGIN_COLLECTION = PREFIX + "uiPluginCollection" + SUFFIX;
|
||||
@SuppressWarnings("squid:S2068")
|
||||
public static final String PASSWORD_CHANGE = PREFIX + "passwordChange" + SUFFIX;
|
||||
|
||||
public static final String ME = PREFIX + "me" + SUFFIX;
|
||||
public static final String SOURCE = PREFIX + "source" + SUFFIX;
|
||||
|
||||
@@ -114,7 +114,7 @@ public class InitializingHttpScmProtocolWrapperTest {
|
||||
}
|
||||
|
||||
private OngoingStubbing<ScmPathInfo> mockSetPathInfo() {
|
||||
return when(pathInfoStore.get()).thenReturn(() -> URI.create("http://example.com/scm/api/rest/"));
|
||||
return when(pathInfoStore.get()).thenReturn(() -> URI.create("http://example.com/scm/api/"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
package sonia.scm.web;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import org.junit.Test;
|
||||
|
||||
import javax.ws.rs.core.MediaType;
|
||||
|
||||
import static java.util.Collections.singletonMap;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
public class JsonEnricherBaseTest {
|
||||
|
||||
private ObjectMapper objectMapper = new ObjectMapper();
|
||||
private TestJsonEnricher enricher = new TestJsonEnricher(objectMapper);
|
||||
|
||||
@Test
|
||||
public void testResultHasMediaType() {
|
||||
JsonEnricherContext context = new JsonEnricherContext(null, MediaType.APPLICATION_JSON_TYPE, null);
|
||||
|
||||
assertThat(enricher.resultHasMediaType(MediaType.APPLICATION_JSON, context)).isTrue();
|
||||
assertThat(enricher.resultHasMediaType(MediaType.APPLICATION_XML, context)).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAppendLink() {
|
||||
ObjectNode root = objectMapper.createObjectNode();
|
||||
ObjectNode links = objectMapper.createObjectNode();
|
||||
root.set("_links", links);
|
||||
JsonEnricherContext context = new JsonEnricherContext(null, MediaType.APPLICATION_JSON_TYPE, root);
|
||||
enricher.enrich(context);
|
||||
|
||||
assertThat(links.get("awesome").get("href").asText()).isEqualTo("/my/awesome/link");
|
||||
}
|
||||
|
||||
private static class TestJsonEnricher extends JsonEnricherBase {
|
||||
|
||||
public TestJsonEnricher(ObjectMapper objectMapper) {
|
||||
super(objectMapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void enrich(JsonEnricherContext context) {
|
||||
JsonNode gitConfigRefNode = createObject(singletonMap("href", value("/my/awesome/link")));
|
||||
|
||||
addPropertyNode(context.getResponseEntity().get("_links"), "awesome", gitConfigRefNode);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
48
scm-it/src/test/java/sonia/scm/it/IndexITCase.java
Normal file
48
scm-it/src/test/java/sonia/scm/it/IndexITCase.java
Normal file
@@ -0,0 +1,48 @@
|
||||
package sonia.scm.it;
|
||||
|
||||
import io.restassured.RestAssured;
|
||||
import org.apache.http.HttpStatus;
|
||||
import org.junit.Test;
|
||||
import sonia.scm.it.utils.RestUtil;
|
||||
import sonia.scm.web.VndMediaType;
|
||||
|
||||
import static sonia.scm.it.utils.RegExMatcher.matchesPattern;
|
||||
import static sonia.scm.it.utils.RestUtil.given;
|
||||
|
||||
public class IndexITCase {
|
||||
|
||||
@Test
|
||||
public void shouldLinkEverythingForAdmin() {
|
||||
given(VndMediaType.INDEX)
|
||||
|
||||
.when()
|
||||
.get(RestUtil.createResourceUrl(""))
|
||||
|
||||
.then()
|
||||
.statusCode(HttpStatus.SC_OK)
|
||||
.body(
|
||||
"_links.repositories.href", matchesPattern(".+/repositories/"),
|
||||
"_links.users.href", matchesPattern(".+/users/"),
|
||||
"_links.groups.href", matchesPattern(".+/groups/"),
|
||||
"_links.config.href", matchesPattern(".+/config"),
|
||||
"_links.gitConfig.href", matchesPattern(".+/config/git"),
|
||||
"_links.hgConfig.href", matchesPattern(".+/config/hg"),
|
||||
"_links.svnConfig.href", matchesPattern(".+/config/svn")
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldCreateLoginLinksForAnonymousAccess() {
|
||||
RestAssured.given() // do not specify user credentials
|
||||
|
||||
.when()
|
||||
.get(RestUtil.createResourceUrl(""))
|
||||
|
||||
.then()
|
||||
.statusCode(HttpStatus.SC_OK)
|
||||
.body(
|
||||
"_links.login.href", matchesPattern(".+/auth/.+")
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
65
scm-it/src/test/java/sonia/scm/it/MeITCase.java
Normal file
65
scm-it/src/test/java/sonia/scm/it/MeITCase.java
Normal file
@@ -0,0 +1,65 @@
|
||||
package sonia.scm.it;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import sonia.scm.it.utils.ScmRequests;
|
||||
import sonia.scm.it.utils.TestData;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
public class MeITCase {
|
||||
|
||||
@Before
|
||||
public void init() {
|
||||
TestData.cleanup();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void adminShouldChangeOwnPassword() {
|
||||
String newPassword = TestData.USER_SCM_ADMIN + "1";
|
||||
// admin change the own password
|
||||
ScmRequests.start()
|
||||
.given()
|
||||
.url(TestData.getMeUrl())
|
||||
.usernameAndPassword(TestData.USER_SCM_ADMIN, TestData.USER_SCM_ADMIN)
|
||||
.getMeResource()
|
||||
.assertStatusCode(200)
|
||||
.usingMeResponse()
|
||||
.assertAdmin(aBoolean -> assertThat(aBoolean).isEqualTo(Boolean.TRUE))
|
||||
.assertPassword(Assert::assertNull)
|
||||
.assertType(s -> assertThat(s).isEqualTo("xml"))
|
||||
.requestChangePassword(TestData.USER_SCM_ADMIN, newPassword)
|
||||
.assertStatusCode(204);
|
||||
// assert password is changed -> login with the new Password than undo changes
|
||||
ScmRequests.start()
|
||||
.given()
|
||||
.url(TestData.getUserUrl(TestData.USER_SCM_ADMIN))
|
||||
.usernameAndPassword(TestData.USER_SCM_ADMIN, newPassword)
|
||||
.getMeResource()
|
||||
.assertStatusCode(200)
|
||||
.usingMeResponse()
|
||||
.assertAdmin(aBoolean -> assertThat(aBoolean).isEqualTo(Boolean.TRUE))// still admin
|
||||
.requestChangePassword(newPassword, TestData.USER_SCM_ADMIN)
|
||||
.assertStatusCode(204);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldHidePasswordLinkIfUserTypeIsNotXML() {
|
||||
String newUser = "user";
|
||||
String password = "pass";
|
||||
String type = "not XML Type";
|
||||
TestData.createUser(newUser, password, true, type);
|
||||
ScmRequests.start()
|
||||
.given()
|
||||
.url(TestData.getMeUrl())
|
||||
.usernameAndPassword(newUser, password)
|
||||
.getMeResource()
|
||||
.assertStatusCode(200)
|
||||
.usingMeResponse()
|
||||
.assertAdmin(aBoolean -> assertThat(aBoolean).isEqualTo(Boolean.TRUE))
|
||||
.assertPassword(Assert::assertNull)
|
||||
.assertType(s -> assertThat(s).isEqualTo(type))
|
||||
.assertPasswordLinkDoesNotExists();
|
||||
}
|
||||
}
|
||||
@@ -39,6 +39,8 @@ import org.junit.rules.TemporaryFolder;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.Parameterized;
|
||||
import org.junit.runners.Parameterized.Parameters;
|
||||
import sonia.scm.it.utils.RepositoryUtil;
|
||||
import sonia.scm.it.utils.TestData;
|
||||
import sonia.scm.repository.PermissionType;
|
||||
import sonia.scm.repository.client.api.RepositoryClient;
|
||||
import sonia.scm.repository.client.api.RepositoryClientException;
|
||||
@@ -51,11 +53,11 @@ import java.util.Objects;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static sonia.scm.it.RepositoryUtil.addAndCommitRandomFile;
|
||||
import static sonia.scm.it.RestUtil.given;
|
||||
import static sonia.scm.it.ScmTypes.availableScmTypes;
|
||||
import static sonia.scm.it.TestData.USER_SCM_ADMIN;
|
||||
import static sonia.scm.it.TestData.callRepository;
|
||||
import static sonia.scm.it.utils.RepositoryUtil.addAndCommitRandomFile;
|
||||
import static sonia.scm.it.utils.RestUtil.given;
|
||||
import static sonia.scm.it.utils.ScmTypes.availableScmTypes;
|
||||
import static sonia.scm.it.utils.TestData.USER_SCM_ADMIN;
|
||||
import static sonia.scm.it.utils.TestData.callRepository;
|
||||
|
||||
@RunWith(Parameterized.class)
|
||||
public class PermissionsITCase {
|
||||
|
||||
@@ -36,12 +36,15 @@ package sonia.scm.it;
|
||||
import org.apache.http.HttpStatus;
|
||||
import org.assertj.core.api.Assertions;
|
||||
import org.junit.Before;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.Parameterized;
|
||||
import org.junit.runners.Parameterized.Parameters;
|
||||
import sonia.scm.it.utils.RepositoryUtil;
|
||||
import sonia.scm.it.utils.TestData;
|
||||
import sonia.scm.repository.client.api.RepositoryClient;
|
||||
import sonia.scm.web.VndMediaType;
|
||||
|
||||
@@ -53,11 +56,11 @@ import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.hamcrest.Matchers.nullValue;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static sonia.scm.it.RegExMatcher.matchesPattern;
|
||||
import static sonia.scm.it.RestUtil.createResourceUrl;
|
||||
import static sonia.scm.it.RestUtil.given;
|
||||
import static sonia.scm.it.ScmTypes.availableScmTypes;
|
||||
import static sonia.scm.it.TestData.repositoryJson;
|
||||
import static sonia.scm.it.utils.RegExMatcher.matchesPattern;
|
||||
import static sonia.scm.it.utils.RestUtil.createResourceUrl;
|
||||
import static sonia.scm.it.utils.RestUtil.given;
|
||||
import static sonia.scm.it.utils.ScmTypes.availableScmTypes;
|
||||
import static sonia.scm.it.utils.TestData.repositoryJson;
|
||||
|
||||
@RunWith(Parameterized.class)
|
||||
public class RepositoriesITCase {
|
||||
|
||||
@@ -4,7 +4,6 @@ import io.restassured.response.ExtractableResponse;
|
||||
import io.restassured.response.Response;
|
||||
import org.apache.http.HttpStatus;
|
||||
import org.assertj.core.util.Lists;
|
||||
import org.assertj.core.util.Maps;
|
||||
import org.junit.Assume;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
@@ -12,6 +11,9 @@ import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.Parameterized;
|
||||
import sonia.scm.it.utils.RepositoryUtil;
|
||||
import sonia.scm.it.utils.ScmRequests;
|
||||
import sonia.scm.it.utils.TestData;
|
||||
import sonia.scm.repository.Changeset;
|
||||
import sonia.scm.repository.client.api.ClientCommand;
|
||||
import sonia.scm.repository.client.api.RepositoryClient;
|
||||
@@ -29,10 +31,10 @@ import static java.lang.Thread.sleep;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static sonia.scm.it.RestUtil.ADMIN_PASSWORD;
|
||||
import static sonia.scm.it.RestUtil.ADMIN_USERNAME;
|
||||
import static sonia.scm.it.RestUtil.given;
|
||||
import static sonia.scm.it.ScmTypes.availableScmTypes;
|
||||
import static sonia.scm.it.utils.RestUtil.ADMIN_PASSWORD;
|
||||
import static sonia.scm.it.utils.RestUtil.ADMIN_USERNAME;
|
||||
import static sonia.scm.it.utils.RestUtil.given;
|
||||
import static sonia.scm.it.utils.ScmTypes.availableScmTypes;
|
||||
|
||||
@RunWith(Parameterized.class)
|
||||
public class RepositoryAccessITCase {
|
||||
@@ -42,7 +44,7 @@ public class RepositoryAccessITCase {
|
||||
|
||||
private final String repositoryType;
|
||||
private File folder;
|
||||
private RepositoryRequests.AppliedRepositoryGetRequest repositoryGetRequest;
|
||||
private ScmRequests.AppliedRepositoryRequest repositoryGetRequest;
|
||||
|
||||
public RepositoryAccessITCase(String repositoryType) {
|
||||
this.repositoryType = repositoryType;
|
||||
@@ -57,12 +59,17 @@ public class RepositoryAccessITCase {
|
||||
public void init() {
|
||||
TestData.createDefault();
|
||||
folder = tempFolder.getRoot();
|
||||
repositoryGetRequest = RepositoryRequests.start()
|
||||
repositoryGetRequest = ScmRequests.start()
|
||||
.given()
|
||||
.url(TestData.getDefaultRepositoryUrl(repositoryType))
|
||||
.usernameAndPassword(ADMIN_USERNAME, ADMIN_PASSWORD)
|
||||
.get()
|
||||
.getRepositoryResource()
|
||||
.assertStatusCode(HttpStatus.SC_OK);
|
||||
ScmRequests.AppliedMeRequest meGetRequest = ScmRequests.start()
|
||||
.given()
|
||||
.url(TestData.getMeUrl())
|
||||
.usernameAndPassword(ADMIN_USERNAME, ADMIN_PASSWORD)
|
||||
.getMeResource();
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -165,7 +172,7 @@ public class RepositoryAccessITCase {
|
||||
.isNotNull()
|
||||
.contains(String.format("%s/sources/%s", repositoryUrl, changeset.getId()));
|
||||
|
||||
assertThat(response.body().jsonPath().getString("_embedded.tags.find{it.name=='" + tagName + "'}._links.changesets.href"))
|
||||
assertThat(response.body().jsonPath().getString("_embedded.tags.find{it.name=='" + tagName + "'}._links.changeset.href"))
|
||||
.as("assert single tag changesets link")
|
||||
.isNotNull()
|
||||
.contains(String.format("%s/changesets/%s", repositoryUrl, changeset.getId()));
|
||||
|
||||
@@ -1,293 +0,0 @@
|
||||
package sonia.scm.it;
|
||||
|
||||
import io.restassured.RestAssured;
|
||||
import io.restassured.response.Response;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
|
||||
/**
|
||||
* Encapsulate rest requests of a repository in builder pattern
|
||||
* <p>
|
||||
* A Get Request can be applied with the methods request*()
|
||||
* These methods return a AppliedGet*Request object
|
||||
* This object can be used to apply general assertions over the rest Assured response
|
||||
* In the AppliedGet*Request classes there is a using*Response() method
|
||||
* that return the *Response class containing specific operations related to the specific response
|
||||
* the *Response class contains also the request*() method to apply the next GET request from a link in the response.
|
||||
*/
|
||||
public class RepositoryRequests {
|
||||
|
||||
private String url;
|
||||
private String username;
|
||||
private String password;
|
||||
|
||||
static RepositoryRequests start() {
|
||||
return new RepositoryRequests();
|
||||
}
|
||||
|
||||
Given given() {
|
||||
return new Given();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Apply a GET Request to the extracted url from the given link
|
||||
*
|
||||
* @param linkPropertyName the property name of link
|
||||
* @param response the response containing the link
|
||||
* @return the response of the GET request using the given link
|
||||
*/
|
||||
private Response getResponseFromLink(Response response, String linkPropertyName) {
|
||||
return getResponse(response
|
||||
.then()
|
||||
.extract()
|
||||
.path(linkPropertyName));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Apply a GET Request to the given <code>url</code> and return the response.
|
||||
*
|
||||
* @param url the url of the GET request
|
||||
* @return the response of the GET request using the given <code>url</code>
|
||||
*/
|
||||
private Response getResponse(String url) {
|
||||
return RestAssured.given()
|
||||
.auth().preemptive().basic(username, password)
|
||||
.when()
|
||||
.get(url);
|
||||
}
|
||||
|
||||
private void setUrl(String url) {
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
private void setUsername(String username) {
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
private void setPassword(String password) {
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
private String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
private String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
private String getPassword() {
|
||||
return password;
|
||||
}
|
||||
|
||||
class Given {
|
||||
|
||||
GivenUrl url(String url) {
|
||||
setUrl(url);
|
||||
return new GivenUrl();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class GivenWithUrlAndAuth {
|
||||
AppliedRepositoryGetRequest get() {
|
||||
return new AppliedRepositoryGetRequest(
|
||||
getResponse(url)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AppliedGetRequest<SELF extends AppliedGetRequest> {
|
||||
private Response response;
|
||||
|
||||
public AppliedGetRequest(Response response) {
|
||||
this.response = response;
|
||||
}
|
||||
|
||||
/**
|
||||
* apply custom assertions to the actual response
|
||||
*
|
||||
* @param consumer consume the response in order to assert the content. the header, the payload etc..
|
||||
* @return the self object
|
||||
*/
|
||||
SELF assertResponse(Consumer<Response> consumer) {
|
||||
consumer.accept(response);
|
||||
return (SELF) this;
|
||||
}
|
||||
|
||||
/**
|
||||
* special assertion of the status code
|
||||
*
|
||||
* @param expectedStatusCode the expected status code
|
||||
* @return the self object
|
||||
*/
|
||||
SELF assertStatusCode(int expectedStatusCode) {
|
||||
this.response.then().assertThat().statusCode(expectedStatusCode);
|
||||
return (SELF) this;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class AppliedRepositoryGetRequest extends AppliedGetRequest<AppliedRepositoryGetRequest> {
|
||||
|
||||
AppliedRepositoryGetRequest(Response response) {
|
||||
super(response);
|
||||
}
|
||||
|
||||
RepositoryResponse usingRepositoryResponse() {
|
||||
return new RepositoryResponse(super.response);
|
||||
}
|
||||
}
|
||||
|
||||
class RepositoryResponse {
|
||||
|
||||
private Response repositoryResponse;
|
||||
|
||||
public RepositoryResponse(Response repositoryResponse) {
|
||||
this.repositoryResponse = repositoryResponse;
|
||||
}
|
||||
|
||||
AppliedGetSourcesRequest requestSources() {
|
||||
return new AppliedGetSourcesRequest(getResponseFromLink(repositoryResponse, "_links.sources.href"));
|
||||
}
|
||||
|
||||
AppliedGetChangesetsRequest requestChangesets() {
|
||||
return new AppliedGetChangesetsRequest(getResponseFromLink(repositoryResponse, "_links.changesets.href"));
|
||||
}
|
||||
}
|
||||
|
||||
class AppliedGetChangesetsRequest extends AppliedGetRequest<AppliedGetChangesetsRequest> {
|
||||
|
||||
AppliedGetChangesetsRequest(Response response) {
|
||||
super(response);
|
||||
}
|
||||
|
||||
ChangesetsResponse usingChangesetsResponse() {
|
||||
return new ChangesetsResponse(super.response);
|
||||
}
|
||||
}
|
||||
|
||||
class ChangesetsResponse {
|
||||
private Response changesetsResponse;
|
||||
|
||||
public ChangesetsResponse(Response changesetsResponse) {
|
||||
this.changesetsResponse = changesetsResponse;
|
||||
}
|
||||
|
||||
ChangesetsResponse assertChangesets(Consumer<List<Map>> changesetsConsumer) {
|
||||
List<Map> changesets = changesetsResponse.then().extract().path("_embedded.changesets");
|
||||
changesetsConsumer.accept(changesets);
|
||||
return this;
|
||||
}
|
||||
|
||||
AppliedGetDiffRequest requestDiff(String revision) {
|
||||
return new AppliedGetDiffRequest(getResponseFromLink(changesetsResponse, "_embedded.changesets.find{it.id=='" + revision + "'}._links.diff.href"));
|
||||
}
|
||||
|
||||
public AppliedGetModificationsRequest requestModifications(String revision) {
|
||||
return new AppliedGetModificationsRequest(getResponseFromLink(changesetsResponse, "_embedded.changesets.find{it.id=='" + revision + "'}._links.modifications.href"));
|
||||
}
|
||||
}
|
||||
|
||||
class AppliedGetSourcesRequest extends AppliedGetRequest<AppliedGetSourcesRequest> {
|
||||
|
||||
public AppliedGetSourcesRequest(Response sourcesResponse) {
|
||||
super(sourcesResponse);
|
||||
}
|
||||
|
||||
SourcesResponse usingSourcesResponse() {
|
||||
return new SourcesResponse(super.response);
|
||||
}
|
||||
}
|
||||
|
||||
class SourcesResponse {
|
||||
|
||||
private Response sourcesResponse;
|
||||
|
||||
SourcesResponse(Response sourcesResponse) {
|
||||
this.sourcesResponse = sourcesResponse;
|
||||
}
|
||||
|
||||
SourcesResponse assertRevision(Consumer<String> assertRevision) {
|
||||
String revision = sourcesResponse.then().extract().path("revision");
|
||||
assertRevision.accept(revision);
|
||||
return this;
|
||||
}
|
||||
|
||||
SourcesResponse assertFiles(Consumer<List> assertFiles) {
|
||||
List files = sourcesResponse.then().extract().path("files");
|
||||
assertFiles.accept(files);
|
||||
return this;
|
||||
}
|
||||
|
||||
AppliedGetChangesetsRequest requestFileHistory(String fileName) {
|
||||
return new AppliedGetChangesetsRequest(getResponseFromLink(sourcesResponse, "_embedded.files.find{it.name=='" + fileName + "'}._links.history.href"));
|
||||
}
|
||||
|
||||
AppliedGetSourcesRequest requestSelf(String fileName) {
|
||||
return new AppliedGetSourcesRequest(getResponseFromLink(sourcesResponse, "_embedded.files.find{it.name=='" + fileName + "'}._links.self.href"));
|
||||
}
|
||||
}
|
||||
|
||||
class AppliedGetDiffRequest extends AppliedGetRequest<AppliedGetDiffRequest> {
|
||||
|
||||
AppliedGetDiffRequest(Response response) {
|
||||
super(response);
|
||||
}
|
||||
}
|
||||
|
||||
class GivenUrl {
|
||||
|
||||
GivenWithUrlAndAuth usernameAndPassword(String username, String password) {
|
||||
setUsername(username);
|
||||
setPassword(password);
|
||||
return new GivenWithUrlAndAuth();
|
||||
}
|
||||
}
|
||||
|
||||
class AppliedGetModificationsRequest extends AppliedGetRequest<AppliedGetModificationsRequest> {
|
||||
public AppliedGetModificationsRequest(Response response) { super(response); }
|
||||
ModificationsResponse usingModificationsResponse() {
|
||||
return new ModificationsResponse(super.response);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class ModificationsResponse {
|
||||
private Response resource;
|
||||
|
||||
public ModificationsResponse(Response resource) {
|
||||
this.resource = resource;
|
||||
}
|
||||
|
||||
ModificationsResponse assertRevision(Consumer<String> assertRevision) {
|
||||
String revision = resource.then().extract().path("revision");
|
||||
assertRevision.accept(revision);
|
||||
return this;
|
||||
}
|
||||
|
||||
ModificationsResponse assertAdded(Consumer<List<String>> assertAdded) {
|
||||
List<String > added = resource.then().extract().path("added");
|
||||
assertAdded.accept(added);
|
||||
return this;
|
||||
}
|
||||
|
||||
ModificationsResponse assertRemoved(Consumer<List<String>> assertRemoved) {
|
||||
List<String > removed = resource.then().extract().path("removed");
|
||||
assertRemoved.accept(removed);
|
||||
return this;
|
||||
}
|
||||
|
||||
ModificationsResponse assertModified(Consumer<List<String>> assertModified) {
|
||||
List<String > modified = resource.then().extract().path("modified");
|
||||
assertModified.accept(modified);
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
96
scm-it/src/test/java/sonia/scm/it/UserITCase.java
Normal file
96
scm-it/src/test/java/sonia/scm/it/UserITCase.java
Normal file
@@ -0,0 +1,96 @@
|
||||
package sonia.scm.it;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import sonia.scm.it.utils.ScmRequests;
|
||||
import sonia.scm.it.utils.TestData;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
public class UserITCase {
|
||||
|
||||
@Before
|
||||
public void init(){
|
||||
TestData.cleanup();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void adminShouldChangeOwnPassword() {
|
||||
String newUser = "user";
|
||||
String password = "pass";
|
||||
TestData.createUser(newUser, password, true, "xml");
|
||||
String newPassword = "new_password";
|
||||
// admin change the own password
|
||||
ScmRequests.start()
|
||||
.given()
|
||||
.url(TestData.getUserUrl(newUser))
|
||||
.usernameAndPassword(newUser, password)
|
||||
.getUserResource()
|
||||
.assertStatusCode(200)
|
||||
.usingUserResponse()
|
||||
.assertAdmin(aBoolean -> assertThat(aBoolean).isEqualTo(Boolean.TRUE))
|
||||
.assertPassword(Assert::assertNull)
|
||||
.requestChangePassword(newPassword) // the oldPassword is not needed in the user resource
|
||||
.assertStatusCode(204);
|
||||
// assert password is changed -> login with the new Password
|
||||
ScmRequests.start()
|
||||
.given()
|
||||
.url(TestData.getUserUrl(newUser))
|
||||
.usernameAndPassword(newUser, newPassword)
|
||||
.getUserResource()
|
||||
.assertStatusCode(200)
|
||||
.usingUserResponse()
|
||||
.assertAdmin(isAdmin -> assertThat(isAdmin).isEqualTo(Boolean.TRUE))
|
||||
.assertPassword(Assert::assertNull);
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void adminShouldChangePasswordOfOtherUser() {
|
||||
String newUser = "user";
|
||||
String password = "pass";
|
||||
TestData.createUser(newUser, password, true, "xml");
|
||||
String newPassword = "new_password";
|
||||
// admin change the password of the user
|
||||
ScmRequests.start()
|
||||
.given()
|
||||
.url(TestData.getUserUrl(newUser))// the admin get the user object
|
||||
.usernameAndPassword(TestData.USER_SCM_ADMIN, TestData.USER_SCM_ADMIN)
|
||||
.getUserResource()
|
||||
.assertStatusCode(200)
|
||||
.usingUserResponse()
|
||||
.assertAdmin(aBoolean -> assertThat(aBoolean).isEqualTo(Boolean.TRUE)) // the user anonymous is not an admin
|
||||
.assertPassword(Assert::assertNull)
|
||||
.requestChangePassword(newPassword) // the oldPassword is not needed in the user resource
|
||||
.assertStatusCode(204);
|
||||
// assert password is changed
|
||||
ScmRequests.start()
|
||||
.given()
|
||||
.url(TestData.getUserUrl(newUser))
|
||||
.usernameAndPassword(newUser, newPassword)
|
||||
.getUserResource()
|
||||
.assertStatusCode(200);
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void shouldHidePasswordLinkIfUserTypeIsNotXML() {
|
||||
String newUser = "user";
|
||||
String password = "pass";
|
||||
String type = "not XML Type";
|
||||
TestData.createUser(newUser, password, true, type);
|
||||
ScmRequests.start()
|
||||
.given()
|
||||
.url(TestData.getMeUrl())
|
||||
.usernameAndPassword(newUser, password)
|
||||
.getUserResource()
|
||||
.assertStatusCode(200)
|
||||
.usingUserResponse()
|
||||
.assertAdmin(aBoolean -> assertThat(aBoolean).isEqualTo(Boolean.TRUE))
|
||||
.assertPassword(Assert::assertNull)
|
||||
.assertType(s -> assertThat(s).isEqualTo(type))
|
||||
.assertPasswordLinkDoesNotExists();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package sonia.scm.it.utils;
|
||||
|
||||
import javax.json.JsonArrayBuilder;
|
||||
import javax.json.JsonObject;
|
||||
import javax.json.JsonObjectBuilder;
|
||||
import javax.json.JsonValue;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.BigInteger;
|
||||
|
||||
public class NullAwareJsonObjectBuilder implements JsonObjectBuilder {
|
||||
public static JsonObjectBuilder wrap(JsonObjectBuilder builder) {
|
||||
if (builder == null) {
|
||||
throw new IllegalArgumentException("Can't wrap nothing.");
|
||||
}
|
||||
return new NullAwareJsonObjectBuilder(builder);
|
||||
}
|
||||
|
||||
private final JsonObjectBuilder builder;
|
||||
|
||||
private NullAwareJsonObjectBuilder(JsonObjectBuilder builder) {
|
||||
this.builder = builder;
|
||||
}
|
||||
|
||||
public JsonObjectBuilder add(String name, JsonValue value) {
|
||||
return builder.add(name, (value == null) ? JsonValue.NULL : value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonObjectBuilder add(String name, String value) {
|
||||
if (value != null){
|
||||
return builder.add(name, value );
|
||||
}else{
|
||||
return builder.addNull(name);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonObjectBuilder add(String name, BigInteger value) {
|
||||
if (value != null){
|
||||
return builder.add(name, value );
|
||||
}else{
|
||||
return builder.addNull(name);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonObjectBuilder add(String name, BigDecimal value) {
|
||||
if (value != null){
|
||||
return builder.add(name, value );
|
||||
}else{
|
||||
return builder.addNull(name);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonObjectBuilder add(String s, int i) {
|
||||
return builder.add(s, i);
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonObjectBuilder add(String s, long l) {
|
||||
return builder.add(s, l);
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonObjectBuilder add(String s, double v) {
|
||||
return builder.add(s, v);
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonObjectBuilder add(String s, boolean b) {
|
||||
return builder.add(s, b);
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonObjectBuilder addNull(String s) {
|
||||
return builder.addNull(s);
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonObjectBuilder add(String s, JsonObjectBuilder jsonObjectBuilder) {
|
||||
return builder.add(s, jsonObjectBuilder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonObjectBuilder add(String s, JsonArrayBuilder jsonArrayBuilder) {
|
||||
return builder.add(s, jsonArrayBuilder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonObject build() {
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package sonia.scm.it;
|
||||
package sonia.scm.it.utils;
|
||||
|
||||
import org.hamcrest.BaseMatcher;
|
||||
import org.hamcrest.Description;
|
||||
@@ -6,7 +6,7 @@ import org.hamcrest.Matcher;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
class RegExMatcher extends BaseMatcher<String> {
|
||||
public class RegExMatcher extends BaseMatcher<String> {
|
||||
public static Matcher<String> matchesPattern(String pattern) {
|
||||
return new RegExMatcher(pattern);
|
||||
}
|
||||
@@ -24,6 +24,6 @@ class RegExMatcher extends BaseMatcher<String> {
|
||||
|
||||
@Override
|
||||
public boolean matches(Object o) {
|
||||
return Pattern.compile(pattern).matcher(o.toString()).matches();
|
||||
return o != null && Pattern.compile(pattern).matcher(o.toString()).matches();
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package sonia.scm.it;
|
||||
package sonia.scm.it.utils;
|
||||
|
||||
import com.google.common.base.Charsets;
|
||||
import com.google.common.io.Files;
|
||||
@@ -24,11 +24,11 @@ public class RepositoryUtil {
|
||||
|
||||
private static final RepositoryClientFactory REPOSITORY_CLIENT_FACTORY = new RepositoryClientFactory();
|
||||
|
||||
static RepositoryClient createRepositoryClient(String repositoryType, File folder) throws IOException {
|
||||
public static RepositoryClient createRepositoryClient(String repositoryType, File folder) throws IOException {
|
||||
return createRepositoryClient(repositoryType, folder, "scmadmin", "scmadmin");
|
||||
}
|
||||
|
||||
static RepositoryClient createRepositoryClient(String repositoryType, File folder, String username, String password) throws IOException {
|
||||
public static RepositoryClient createRepositoryClient(String repositoryType, File folder, String username, String password) throws IOException {
|
||||
String httpProtocolUrl = TestData.callRepository(username, password, repositoryType, HttpStatus.SC_OK)
|
||||
.extract()
|
||||
.path("_links.protocol.find{it.name=='http'}.href");
|
||||
@@ -36,14 +36,14 @@ public class RepositoryUtil {
|
||||
return REPOSITORY_CLIENT_FACTORY.create(repositoryType, httpProtocolUrl, username, password, folder);
|
||||
}
|
||||
|
||||
static String addAndCommitRandomFile(RepositoryClient client, String username) throws IOException {
|
||||
public static String addAndCommitRandomFile(RepositoryClient client, String username) throws IOException {
|
||||
String uuid = UUID.randomUUID().toString();
|
||||
String name = "file-" + uuid + ".uuid";
|
||||
createAndCommitFile(client, username, name, uuid);
|
||||
return name;
|
||||
}
|
||||
|
||||
static Changeset createAndCommitFile(RepositoryClient repositoryClient, String username, String fileName, String content) throws IOException {
|
||||
public static Changeset createAndCommitFile(RepositoryClient repositoryClient, String username, String fileName, String content) throws IOException {
|
||||
writeAndAddFile(repositoryClient, fileName, content);
|
||||
return commit(repositoryClient, username, "added " + fileName);
|
||||
}
|
||||
@@ -59,7 +59,7 @@ public class RepositoryUtil {
|
||||
* @return the changeset with all modifications
|
||||
* @throws IOException
|
||||
*/
|
||||
static Changeset commitMultipleFileModifications(RepositoryClient repositoryClient, String username, Map<String, String> addedFiles, Map<String, String> modifiedFiles, List<String> removedFiles) throws IOException {
|
||||
public static Changeset commitMultipleFileModifications(RepositoryClient repositoryClient, String username, Map<String, String> addedFiles, Map<String, String> modifiedFiles, List<String> removedFiles) throws IOException {
|
||||
for (String fileName : addedFiles.keySet()) {
|
||||
writeAndAddFile(repositoryClient, fileName, addedFiles.get(fileName));
|
||||
}
|
||||
@@ -80,7 +80,7 @@ public class RepositoryUtil {
|
||||
return file;
|
||||
}
|
||||
|
||||
static Changeset removeAndCommitFile(RepositoryClient repositoryClient, String username, String fileName) throws IOException {
|
||||
public static Changeset removeAndCommitFile(RepositoryClient repositoryClient, String username, String fileName) throws IOException {
|
||||
deleteFileAndApplyRemoveCommand(repositoryClient, fileName);
|
||||
return commit(repositoryClient, username, "removed " + fileName);
|
||||
}
|
||||
@@ -115,7 +115,7 @@ public class RepositoryUtil {
|
||||
return changeset;
|
||||
}
|
||||
|
||||
static Tag addTag(RepositoryClient repositoryClient, String revision, String tagName) throws IOException {
|
||||
public static Tag addTag(RepositoryClient repositoryClient, String revision, String tagName) throws IOException {
|
||||
if (repositoryClient.isCommandSupported(ClientCommand.TAG)) {
|
||||
Tag tag = repositoryClient.getTagCommand().setRevision(revision).tag(tagName, TestData.USER_SCM_ADMIN);
|
||||
if (repositoryClient.isCommandSupported(ClientCommand.PUSH)) {
|
||||
@@ -1,4 +1,4 @@
|
||||
package sonia.scm.it;
|
||||
package sonia.scm.it.utils;
|
||||
|
||||
import io.restassured.RestAssured;
|
||||
import io.restassured.specification.RequestSpecification;
|
||||
@@ -10,7 +10,7 @@ import static java.net.URI.create;
|
||||
public class RestUtil {
|
||||
|
||||
public static final URI BASE_URL = create("http://localhost:8081/scm/");
|
||||
public static final URI REST_BASE_URL = BASE_URL.resolve("api/rest/v2/");
|
||||
public static final URI REST_BASE_URL = BASE_URL.resolve("api/v2/");
|
||||
|
||||
public static URI createResourceUrl(String path) {
|
||||
return REST_BASE_URL.resolve(path);
|
||||
465
scm-it/src/test/java/sonia/scm/it/utils/ScmRequests.java
Normal file
465
scm-it/src/test/java/sonia/scm/it/utils/ScmRequests.java
Normal file
@@ -0,0 +1,465 @@
|
||||
package sonia.scm.it.utils;
|
||||
|
||||
import io.restassured.RestAssured;
|
||||
import io.restassured.response.Response;
|
||||
import sonia.scm.web.VndMediaType;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static sonia.scm.it.utils.TestData.createPasswordChangeJson;
|
||||
|
||||
|
||||
/**
|
||||
* Encapsulate rest requests of a repository in builder pattern
|
||||
* <p>
|
||||
* A Get Request can be applied with the methods request*()
|
||||
* These methods return a AppliedGet*Request object
|
||||
* This object can be used to apply general assertions over the rest Assured response
|
||||
* In the AppliedGet*Request classes there is a using*Response() method
|
||||
* that return the *Response class containing specific operations related to the specific response
|
||||
* the *Response class contains also the request*() method to apply the next GET request from a link in the response.
|
||||
*/
|
||||
public class ScmRequests {
|
||||
|
||||
private String url;
|
||||
private String username;
|
||||
private String password;
|
||||
|
||||
public static ScmRequests start() {
|
||||
return new ScmRequests();
|
||||
}
|
||||
|
||||
public Given given() {
|
||||
return new Given();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Apply a GET Request to the extracted url from the given link
|
||||
*
|
||||
* @param linkPropertyName the property name of link
|
||||
* @param response the response containing the link
|
||||
* @return the response of the GET request using the given link
|
||||
*/
|
||||
private Response applyGETRequestFromLink(Response response, String linkPropertyName) {
|
||||
return applyGETRequest(response
|
||||
.then()
|
||||
.extract()
|
||||
.path(linkPropertyName));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Apply a GET Request to the given <code>url</code> and return the response.
|
||||
*
|
||||
* @param url the url of the GET request
|
||||
* @return the response of the GET request using the given <code>url</code>
|
||||
*/
|
||||
private Response applyGETRequest(String url) {
|
||||
return RestAssured.given()
|
||||
.auth().preemptive().basic(username, password)
|
||||
.when()
|
||||
.get(url);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Apply a PUT Request to the extracted url from the given link
|
||||
*
|
||||
* @param response the response containing the link
|
||||
* @param linkPropertyName the property name of link
|
||||
* @param body
|
||||
* @return the response of the PUT request using the given link
|
||||
*/
|
||||
private Response applyPUTRequestFromLink(Response response, String linkPropertyName, String content, String body) {
|
||||
return applyPUTRequest(response
|
||||
.then()
|
||||
.extract()
|
||||
.path(linkPropertyName), content, body);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Apply a PUT Request to the given <code>url</code> and return the response.
|
||||
*
|
||||
* @param url the url of the PUT request
|
||||
* @param mediaType
|
||||
* @param body
|
||||
* @return the response of the PUT request using the given <code>url</code>
|
||||
*/
|
||||
private Response applyPUTRequest(String url, String mediaType, String body) {
|
||||
return RestAssured.given()
|
||||
.auth().preemptive().basic(username, password)
|
||||
.when()
|
||||
.contentType(mediaType)
|
||||
.accept(mediaType)
|
||||
.body(body)
|
||||
.put(url);
|
||||
}
|
||||
|
||||
|
||||
private void setUrl(String url) {
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
private void setUsername(String username) {
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
private void setPassword(String password) {
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
private String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
private String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
private String getPassword() {
|
||||
return password;
|
||||
}
|
||||
|
||||
public class Given {
|
||||
|
||||
public GivenUrl url(String url) {
|
||||
setUrl(url);
|
||||
return new GivenUrl();
|
||||
}
|
||||
|
||||
public GivenUrl url(URI url) {
|
||||
setUrl(url.toString());
|
||||
return new GivenUrl();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public class GivenWithUrlAndAuth {
|
||||
public AppliedMeRequest getMeResource() {
|
||||
return new AppliedMeRequest(applyGETRequest(url));
|
||||
}
|
||||
|
||||
public AppliedUserRequest getUserResource() {
|
||||
return new AppliedUserRequest(applyGETRequest(url));
|
||||
}
|
||||
|
||||
public AppliedRepositoryRequest getRepositoryResource() {
|
||||
return new AppliedRepositoryRequest(
|
||||
applyGETRequest(url)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public class AppliedRequest<SELF extends AppliedRequest> {
|
||||
private Response response;
|
||||
|
||||
public AppliedRequest(Response response) {
|
||||
this.response = response;
|
||||
}
|
||||
|
||||
/**
|
||||
* apply custom assertions to the actual response
|
||||
*
|
||||
* @param consumer consume the response in order to assert the content. the header, the payload etc..
|
||||
* @return the self object
|
||||
*/
|
||||
public SELF assertResponse(Consumer<Response> consumer) {
|
||||
consumer.accept(response);
|
||||
return (SELF) this;
|
||||
}
|
||||
|
||||
/**
|
||||
* special assertion of the status code
|
||||
*
|
||||
* @param expectedStatusCode the expected status code
|
||||
* @return the self object
|
||||
*/
|
||||
public SELF assertStatusCode(int expectedStatusCode) {
|
||||
this.response.then().assertThat().statusCode(expectedStatusCode);
|
||||
return (SELF) this;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public class AppliedRepositoryRequest extends AppliedRequest<AppliedRepositoryRequest> {
|
||||
|
||||
public AppliedRepositoryRequest(Response response) {
|
||||
super(response);
|
||||
}
|
||||
|
||||
public RepositoryResponse usingRepositoryResponse() {
|
||||
return new RepositoryResponse(super.response);
|
||||
}
|
||||
}
|
||||
|
||||
public class RepositoryResponse {
|
||||
|
||||
private Response repositoryResponse;
|
||||
|
||||
public RepositoryResponse(Response repositoryResponse) {
|
||||
this.repositoryResponse = repositoryResponse;
|
||||
}
|
||||
|
||||
public AppliedSourcesRequest requestSources() {
|
||||
return new AppliedSourcesRequest(applyGETRequestFromLink(repositoryResponse, "_links.sources.href"));
|
||||
}
|
||||
|
||||
public AppliedChangesetsRequest requestChangesets() {
|
||||
return new AppliedChangesetsRequest(applyGETRequestFromLink(repositoryResponse, "_links.changesets.href"));
|
||||
}
|
||||
}
|
||||
|
||||
public class AppliedChangesetsRequest extends AppliedRequest<AppliedChangesetsRequest> {
|
||||
|
||||
public AppliedChangesetsRequest(Response response) {
|
||||
super(response);
|
||||
}
|
||||
|
||||
public ChangesetsResponse usingChangesetsResponse() {
|
||||
return new ChangesetsResponse(super.response);
|
||||
}
|
||||
}
|
||||
|
||||
public class ChangesetsResponse {
|
||||
private Response changesetsResponse;
|
||||
|
||||
public ChangesetsResponse(Response changesetsResponse) {
|
||||
this.changesetsResponse = changesetsResponse;
|
||||
}
|
||||
|
||||
public ChangesetsResponse assertChangesets(Consumer<List<Map>> changesetsConsumer) {
|
||||
List<Map> changesets = changesetsResponse.then().extract().path("_embedded.changesets");
|
||||
changesetsConsumer.accept(changesets);
|
||||
return this;
|
||||
}
|
||||
|
||||
public AppliedDiffRequest requestDiff(String revision) {
|
||||
return new AppliedDiffRequest(applyGETRequestFromLink(changesetsResponse, "_embedded.changesets.find{it.id=='" + revision + "'}._links.diff.href"));
|
||||
}
|
||||
|
||||
public AppliedModificationsRequest requestModifications(String revision) {
|
||||
return new AppliedModificationsRequest(applyGETRequestFromLink(changesetsResponse, "_embedded.changesets.find{it.id=='" + revision + "'}._links.modifications.href"));
|
||||
}
|
||||
}
|
||||
|
||||
public class AppliedSourcesRequest extends AppliedRequest<AppliedSourcesRequest> {
|
||||
|
||||
public AppliedSourcesRequest(Response sourcesResponse) {
|
||||
super(sourcesResponse);
|
||||
}
|
||||
|
||||
public SourcesResponse usingSourcesResponse() {
|
||||
return new SourcesResponse(super.response);
|
||||
}
|
||||
}
|
||||
|
||||
public class SourcesResponse {
|
||||
|
||||
private Response sourcesResponse;
|
||||
|
||||
public SourcesResponse(Response sourcesResponse) {
|
||||
this.sourcesResponse = sourcesResponse;
|
||||
}
|
||||
|
||||
public SourcesResponse assertRevision(Consumer<String> assertRevision) {
|
||||
String revision = sourcesResponse.then().extract().path("revision");
|
||||
assertRevision.accept(revision);
|
||||
return this;
|
||||
}
|
||||
|
||||
public SourcesResponse assertFiles(Consumer<List> assertFiles) {
|
||||
List files = sourcesResponse.then().extract().path("files");
|
||||
assertFiles.accept(files);
|
||||
return this;
|
||||
}
|
||||
|
||||
public AppliedChangesetsRequest requestFileHistory(String fileName) {
|
||||
return new AppliedChangesetsRequest(applyGETRequestFromLink(sourcesResponse, "_embedded.files.find{it.name=='" + fileName + "'}._links.history.href"));
|
||||
}
|
||||
|
||||
public AppliedSourcesRequest requestSelf(String fileName) {
|
||||
return new AppliedSourcesRequest(applyGETRequestFromLink(sourcesResponse, "_embedded.files.find{it.name=='" + fileName + "'}._links.self.href"));
|
||||
}
|
||||
}
|
||||
|
||||
public class AppliedDiffRequest extends AppliedRequest<AppliedDiffRequest> {
|
||||
|
||||
public AppliedDiffRequest(Response response) {
|
||||
super(response);
|
||||
}
|
||||
}
|
||||
|
||||
public class GivenUrl {
|
||||
|
||||
public GivenWithUrlAndAuth usernameAndPassword(String username, String password) {
|
||||
setUsername(username);
|
||||
setPassword(password);
|
||||
return new GivenWithUrlAndAuth();
|
||||
}
|
||||
}
|
||||
|
||||
public class AppliedModificationsRequest extends AppliedRequest<AppliedModificationsRequest> {
|
||||
public AppliedModificationsRequest(Response response) {
|
||||
super(response);
|
||||
}
|
||||
|
||||
public ModificationsResponse usingModificationsResponse() {
|
||||
return new ModificationsResponse(super.response);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public class ModificationsResponse {
|
||||
private Response resource;
|
||||
|
||||
public ModificationsResponse(Response resource) {
|
||||
this.resource = resource;
|
||||
}
|
||||
|
||||
public ModificationsResponse assertRevision(Consumer<String> assertRevision) {
|
||||
String revision = resource.then().extract().path("revision");
|
||||
assertRevision.accept(revision);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ModificationsResponse assertAdded(Consumer<List<String>> assertAdded) {
|
||||
List<String> added = resource.then().extract().path("added");
|
||||
assertAdded.accept(added);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ModificationsResponse assertRemoved(Consumer<List<String>> assertRemoved) {
|
||||
List<String> removed = resource.then().extract().path("removed");
|
||||
assertRemoved.accept(removed);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ModificationsResponse assertModified(Consumer<List<String>> assertModified) {
|
||||
List<String> modified = resource.then().extract().path("modified");
|
||||
assertModified.accept(modified);
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public class AppliedMeRequest extends AppliedRequest<AppliedMeRequest> {
|
||||
|
||||
public AppliedMeRequest(Response response) {
|
||||
super(response);
|
||||
}
|
||||
|
||||
public MeResponse usingMeResponse() {
|
||||
return new MeResponse(super.response);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public class MeResponse extends UserResponse<MeResponse> {
|
||||
|
||||
|
||||
public MeResponse(Response response) {
|
||||
super(response);
|
||||
}
|
||||
|
||||
public AppliedChangePasswordRequest requestChangePassword(String oldPassword, String newPassword) {
|
||||
return new AppliedChangePasswordRequest(applyPUTRequestFromLink(super.response, "_links.password.href", VndMediaType.PASSWORD_CHANGE, createPasswordChangeJson(oldPassword, newPassword)));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
public class UserResponse<SELF extends UserResponse> extends ModelResponse<SELF> {
|
||||
|
||||
public static final String LINKS_PASSWORD_HREF = "_links.password.href";
|
||||
|
||||
public UserResponse(Response response) {
|
||||
super(response);
|
||||
}
|
||||
|
||||
public SELF assertPassword(Consumer<String> assertPassword) {
|
||||
return super.assertSingleProperty(assertPassword, "password");
|
||||
}
|
||||
|
||||
public SELF assertType(Consumer<String> assertType) {
|
||||
return assertSingleProperty(assertType, "type");
|
||||
}
|
||||
|
||||
public SELF assertAdmin(Consumer<Boolean> assertAdmin) {
|
||||
return assertSingleProperty(assertAdmin, "admin");
|
||||
}
|
||||
|
||||
public SELF assertPasswordLinkDoesNotExists() {
|
||||
return assertPropertyPathDoesNotExists(LINKS_PASSWORD_HREF);
|
||||
}
|
||||
|
||||
public SELF assertPasswordLinkExists() {
|
||||
return assertPropertyPathExists(LINKS_PASSWORD_HREF);
|
||||
}
|
||||
|
||||
public AppliedChangePasswordRequest requestChangePassword(String newPassword) {
|
||||
return new AppliedChangePasswordRequest(applyPUTRequestFromLink(super.response, LINKS_PASSWORD_HREF, VndMediaType.PASSWORD_CHANGE, createPasswordChangeJson(null, newPassword)));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* encapsulate standard assertions over model properties
|
||||
*/
|
||||
public class ModelResponse<SELF extends ModelResponse> {
|
||||
|
||||
protected Response response;
|
||||
|
||||
public ModelResponse(Response response) {
|
||||
this.response = response;
|
||||
}
|
||||
|
||||
public <T> SELF assertSingleProperty(Consumer<T> assertSingleProperty, String propertyJsonPath) {
|
||||
T propertyValue = response.then().extract().path(propertyJsonPath);
|
||||
assertSingleProperty.accept(propertyValue);
|
||||
return (SELF) this;
|
||||
}
|
||||
|
||||
public SELF assertPropertyPathExists(String propertyJsonPath) {
|
||||
response.then().assertThat().body("any { it.containsKey('" + propertyJsonPath + "')}", is(true));
|
||||
return (SELF) this;
|
||||
}
|
||||
|
||||
public SELF assertPropertyPathDoesNotExists(String propertyJsonPath) {
|
||||
response.then().assertThat().body("this.any { it.containsKey('" + propertyJsonPath + "')}", is(false));
|
||||
return (SELF) this;
|
||||
}
|
||||
|
||||
public SELF assertArrayProperty(Consumer<List> assertProperties, String propertyJsonPath) {
|
||||
List properties = response.then().extract().path(propertyJsonPath);
|
||||
assertProperties.accept(properties);
|
||||
return (SELF) this;
|
||||
}
|
||||
}
|
||||
|
||||
public class AppliedChangePasswordRequest extends AppliedRequest<AppliedChangePasswordRequest> {
|
||||
|
||||
public AppliedChangePasswordRequest(Response response) {
|
||||
super(response);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public class AppliedUserRequest extends AppliedRequest<AppliedUserRequest> {
|
||||
|
||||
public AppliedUserRequest(Response response) {
|
||||
super(response);
|
||||
}
|
||||
|
||||
public UserResponse usingUserResponse() {
|
||||
return new UserResponse(super.response);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
package sonia.scm.it;
|
||||
package sonia.scm.it.utils;
|
||||
|
||||
import sonia.scm.util.IOUtil;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
|
||||
class ScmTypes {
|
||||
static Collection<String> availableScmTypes() {
|
||||
public class ScmTypes {
|
||||
public static Collection<String> availableScmTypes() {
|
||||
Collection<String> params = new ArrayList<>();
|
||||
|
||||
params.add("git");
|
||||
@@ -1,4 +1,4 @@
|
||||
package sonia.scm.it;
|
||||
package sonia.scm.it.utils;
|
||||
|
||||
import io.restassured.response.ValidatableResponse;
|
||||
import org.apache.http.HttpStatus;
|
||||
@@ -8,14 +8,16 @@ import sonia.scm.repository.PermissionType;
|
||||
import sonia.scm.web.VndMediaType;
|
||||
|
||||
import javax.json.Json;
|
||||
import javax.json.JsonObjectBuilder;
|
||||
import java.net.URI;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static java.util.Arrays.asList;
|
||||
import static sonia.scm.it.RestUtil.createResourceUrl;
|
||||
import static sonia.scm.it.RestUtil.given;
|
||||
import static sonia.scm.it.ScmTypes.availableScmTypes;
|
||||
import static sonia.scm.it.utils.RestUtil.createResourceUrl;
|
||||
import static sonia.scm.it.utils.RestUtil.given;
|
||||
import static sonia.scm.it.utils.ScmTypes.availableScmTypes;
|
||||
|
||||
public class TestData {
|
||||
|
||||
@@ -26,6 +28,7 @@ public class TestData {
|
||||
private static final List<String> PROTECTED_USERS = asList(USER_SCM_ADMIN, USER_ANONYMOUS);
|
||||
|
||||
private static Map<String, String> DEFAULT_REPOSITORIES = new HashMap<>();
|
||||
public static final JsonObjectBuilder JSON_BUILDER = NullAwareJsonObjectBuilder.wrap(Json.createObjectBuilder());
|
||||
|
||||
public static void createDefault() {
|
||||
cleanup();
|
||||
@@ -44,27 +47,31 @@ public class TestData {
|
||||
}
|
||||
|
||||
public static void createUser(String username, String password) {
|
||||
createUser(username, password, false, "xml");
|
||||
}
|
||||
|
||||
public static void createUser(String username, String password, boolean isAdmin, String type) {
|
||||
LOG.info("create user with username: {}", username);
|
||||
String admin = isAdmin ? "true" : "false";
|
||||
given(VndMediaType.USER)
|
||||
.when()
|
||||
.content(" {\n" +
|
||||
" \"active\": true,\n" +
|
||||
" \"admin\": false,\n" +
|
||||
" \"creationDate\": \"2018-08-21T12:26:46.084Z\",\n" +
|
||||
" \"displayName\": \"" + username + "\",\n" +
|
||||
" \"mail\": \"user1@scm-manager.org\",\n" +
|
||||
" \"name\": \"" + username + "\",\n" +
|
||||
" \"password\": \"" + password + "\",\n" +
|
||||
" \"type\": \"xml\"\n" +
|
||||
" \n" +
|
||||
" }")
|
||||
.post(createResourceUrl("users"))
|
||||
.content(new StringBuilder()
|
||||
.append(" {\n")
|
||||
.append(" \"active\": true,\n")
|
||||
.append(" \"admin\": ").append(admin).append(",\n")
|
||||
.append(" \"creationDate\": \"2018-08-21T12:26:46.084Z\",\n")
|
||||
.append(" \"displayName\": \"").append(username).append("\",\n")
|
||||
.append(" \"mail\": \"user1@scm-manager.org\",\n")
|
||||
.append(" \"name\": \"").append(username).append("\",\n")
|
||||
.append(" \"password\": \"").append(password).append("\",\n")
|
||||
.append(" \"type\": \"").append(type).append("\"\n")
|
||||
.append(" }").toString())
|
||||
.post(getUsersUrl())
|
||||
.then()
|
||||
.statusCode(HttpStatus.SC_CREATED)
|
||||
;
|
||||
}
|
||||
|
||||
|
||||
public static void createUserPermission(String name, PermissionType permissionType, String repositoryType) {
|
||||
String defaultPermissionUrl = TestData.getDefaultPermissionUrl(USER_SCM_ADMIN, USER_SCM_ADMIN, repositoryType);
|
||||
LOG.info("create permission with name {} and type: {} using the endpoint: {}", name, permissionType, defaultPermissionUrl);
|
||||
@@ -183,7 +190,7 @@ public class TestData {
|
||||
}
|
||||
|
||||
public static String repositoryJson(String repositoryType) {
|
||||
return Json.createObjectBuilder()
|
||||
return JSON_BUILDER
|
||||
.add("contact", "zaphod.beeblebrox@hitchhiker.com")
|
||||
.add("description", "Heart of Gold")
|
||||
.add("name", "HeartOfGold-" + repositoryType)
|
||||
@@ -192,6 +199,29 @@ public class TestData {
|
||||
.build().toString();
|
||||
}
|
||||
|
||||
public static URI getMeUrl() {
|
||||
return RestUtil.createResourceUrl("me/");
|
||||
|
||||
}
|
||||
|
||||
public static URI getUsersUrl() {
|
||||
return RestUtil.createResourceUrl("users/");
|
||||
|
||||
}
|
||||
|
||||
public static URI getUserUrl(String username) {
|
||||
return getUsersUrl().resolve(username);
|
||||
|
||||
}
|
||||
|
||||
|
||||
public static String createPasswordChangeJson(String oldPassword, String newPassword) {
|
||||
return JSON_BUILDER
|
||||
.add("oldPassword", oldPassword)
|
||||
.add("newPassword", newPassword)
|
||||
.build().toString();
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
cleanup();
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import sonia.scm.config.ConfigurationPermissions;
|
||||
import sonia.scm.plugin.Extension;
|
||||
import sonia.scm.web.JsonEnricherBase;
|
||||
import sonia.scm.web.JsonEnricherContext;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Provider;
|
||||
|
||||
import static java.util.Collections.singletonMap;
|
||||
import static sonia.scm.web.VndMediaType.INDEX;
|
||||
|
||||
@Extension
|
||||
public class GitConfigInIndexResource extends JsonEnricherBase {
|
||||
|
||||
private final Provider<ScmPathInfoStore> scmPathInfoStore;
|
||||
|
||||
@Inject
|
||||
public GitConfigInIndexResource(Provider<ScmPathInfoStore> scmPathInfoStore, ObjectMapper objectMapper) {
|
||||
super(objectMapper);
|
||||
this.scmPathInfoStore = scmPathInfoStore;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void enrich(JsonEnricherContext context) {
|
||||
if (resultHasMediaType(INDEX, context) && ConfigurationPermissions.list().isPermitted()) {
|
||||
String gitConfigUrl = new LinkBuilder(scmPathInfoStore.get().get(), GitConfigResource.class)
|
||||
.method("get")
|
||||
.parameters()
|
||||
.href();
|
||||
|
||||
JsonNode gitConfigRefNode = createObject(singletonMap("href", value(gitConfigUrl)));
|
||||
|
||||
addPropertyNode(context.getResponseEntity().get("_links"), "gitConfig", gitConfigRefNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import { repositories } from "@scm-manager/ui-components";
|
||||
import type { Repository } from "@scm-manager/ui-types";
|
||||
|
||||
type Props = {
|
||||
@@ -10,14 +11,16 @@ class ProtocolInformation extends React.Component<Props> {
|
||||
|
||||
render() {
|
||||
const { repository } = this.props;
|
||||
if (!repository._links.httpProtocol) {
|
||||
const href = repositories.getProtocolLinkByType(repository, "http");
|
||||
if (!href) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h4>Clone the repository</h4>
|
||||
<pre>
|
||||
<code>git clone {repository._links.httpProtocol.href}</code>
|
||||
<code>git clone {href}</code>
|
||||
</pre>
|
||||
<h4>Create a new repository</h4>
|
||||
<pre>
|
||||
@@ -30,7 +33,7 @@ class ProtocolInformation extends React.Component<Props> {
|
||||
<br />
|
||||
git commit -m "added readme"
|
||||
<br />
|
||||
git remote add origin {repository._links.httpProtocol.href}
|
||||
git remote add origin {href}
|
||||
<br />
|
||||
git push -u origin master
|
||||
<br />
|
||||
@@ -39,7 +42,7 @@ class ProtocolInformation extends React.Component<Props> {
|
||||
<h4>Push an existing repository</h4>
|
||||
<pre>
|
||||
<code>
|
||||
git remote add origin {repository._links.httpProtocol.href}
|
||||
git remote add origin {href}
|
||||
<br />
|
||||
git push -u origin master
|
||||
<br />
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import com.github.sdorra.shiro.ShiroRule;
|
||||
import com.github.sdorra.shiro.SubjectAware;
|
||||
import com.google.inject.util.Providers;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import sonia.scm.web.JsonEnricherContext;
|
||||
import sonia.scm.web.VndMediaType;
|
||||
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import java.net.URI;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
|
||||
@SubjectAware(configuration = "classpath:sonia/scm/configuration/shiro.ini")
|
||||
public class GitConfigInIndexResourceTest {
|
||||
|
||||
@Rule
|
||||
public final ShiroRule shiroRule = new ShiroRule();
|
||||
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
private final ObjectNode root = objectMapper.createObjectNode();
|
||||
private final GitConfigInIndexResource gitConfigInIndexResource;
|
||||
|
||||
public GitConfigInIndexResourceTest() {
|
||||
root.put("_links", objectMapper.createObjectNode());
|
||||
ScmPathInfoStore pathInfoStore = new ScmPathInfoStore();
|
||||
pathInfoStore.set(() -> URI.create("/"));
|
||||
gitConfigInIndexResource = new GitConfigInIndexResource(Providers.of(pathInfoStore), objectMapper);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SubjectAware(username = "admin", password = "secret")
|
||||
public void admin() {
|
||||
JsonEnricherContext context = new JsonEnricherContext(URI.create("/index"), MediaType.valueOf(VndMediaType.INDEX), root);
|
||||
|
||||
gitConfigInIndexResource.enrich(context);
|
||||
|
||||
assertEquals("/v2/config/git", root.get("_links").get("gitConfig").get("href").asText());
|
||||
}
|
||||
|
||||
@Test
|
||||
@SubjectAware(username = "readOnly", password = "secret")
|
||||
public void user() {
|
||||
JsonEnricherContext context = new JsonEnricherContext(URI.create("/index"), MediaType.valueOf(VndMediaType.INDEX), root);
|
||||
|
||||
gitConfigInIndexResource.enrich(context);
|
||||
|
||||
assertFalse(root.get("_links").iterator().hasNext());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void anonymous() {
|
||||
JsonEnricherContext context = new JsonEnricherContext(URI.create("/index"), MediaType.valueOf(VndMediaType.INDEX), root);
|
||||
|
||||
gitConfigInIndexResource.enrich(context);
|
||||
|
||||
assertFalse(root.get("_links").iterator().hasNext());
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,10 @@
|
||||
readOnly = secret, reader
|
||||
writeOnly = secret, writer
|
||||
readWrite = secret, readerWriter
|
||||
admin = secret, admin
|
||||
|
||||
[roles]
|
||||
reader = configuration:read:git
|
||||
writer = configuration:write:git
|
||||
readerWriter = configuration:*:git
|
||||
admin = *
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import sonia.scm.config.ConfigurationPermissions;
|
||||
import sonia.scm.plugin.Extension;
|
||||
import sonia.scm.web.JsonEnricherBase;
|
||||
import sonia.scm.web.JsonEnricherContext;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Provider;
|
||||
|
||||
import static java.util.Collections.singletonMap;
|
||||
import static sonia.scm.web.VndMediaType.INDEX;
|
||||
|
||||
@Extension
|
||||
public class HgConfigInIndexResource extends JsonEnricherBase {
|
||||
|
||||
private final Provider<ScmPathInfoStore> scmPathInfoStore;
|
||||
|
||||
@Inject
|
||||
public HgConfigInIndexResource(Provider<ScmPathInfoStore> scmPathInfoStore, ObjectMapper objectMapper) {
|
||||
super(objectMapper);
|
||||
this.scmPathInfoStore = scmPathInfoStore;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void enrich(JsonEnricherContext context) {
|
||||
if (resultHasMediaType(INDEX, context) && ConfigurationPermissions.list().isPermitted()) {
|
||||
String hgConfigUrl = new LinkBuilder(scmPathInfoStore.get().get(), HgConfigResource.class)
|
||||
.method("get")
|
||||
.parameters()
|
||||
.href();
|
||||
|
||||
JsonNode hgConfigRefNode = createObject(singletonMap("href", value(hgConfigUrl)));
|
||||
|
||||
addPropertyNode(context.getResponseEntity().get("_links"), "hgConfig", hgConfigRefNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import { repositories } from "@scm-manager/ui-components";
|
||||
import type { Repository } from "@scm-manager/ui-types";
|
||||
|
||||
type Props = {
|
||||
@@ -10,14 +11,15 @@ class ProtocolInformation extends React.Component<Props> {
|
||||
|
||||
render() {
|
||||
const { repository } = this.props;
|
||||
if (!repository._links.httpProtocol) {
|
||||
const href = repositories.getProtocolLinkByType(repository, "http");
|
||||
if (!href) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<h4>Clone the repository</h4>
|
||||
<pre>
|
||||
<code>hg clone {repository._links.httpProtocol.href}</code>
|
||||
<code>hg clone {href}</code>
|
||||
</pre>
|
||||
<h4>Create a new repository</h4>
|
||||
<pre>
|
||||
@@ -26,7 +28,7 @@ class ProtocolInformation extends React.Component<Props> {
|
||||
<br />
|
||||
echo "[paths]" > .hg/hgrc
|
||||
<br />
|
||||
echo "default = {repository._links.httpProtocol.href}" > .hg/hgrc
|
||||
echo "default = {href}" > .hg/hgrc
|
||||
<br />
|
||||
echo "# {repository.name}" > README.md
|
||||
<br />
|
||||
@@ -44,7 +46,7 @@ class ProtocolInformation extends React.Component<Props> {
|
||||
<code>
|
||||
# add the repository url as default to your .hg/hgrc e.g:
|
||||
<br />
|
||||
default = {repository._links.httpProtocol.href}
|
||||
default = {href}
|
||||
<br />
|
||||
# push to remote repository
|
||||
<br />
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import com.github.sdorra.shiro.ShiroRule;
|
||||
import com.github.sdorra.shiro.SubjectAware;
|
||||
import com.google.inject.util.Providers;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import sonia.scm.web.JsonEnricherContext;
|
||||
import sonia.scm.web.VndMediaType;
|
||||
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import java.net.URI;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
|
||||
@SubjectAware(configuration = "classpath:sonia/scm/configuration/shiro.ini")
|
||||
public class HgConfigInIndexResourceTest {
|
||||
|
||||
@Rule
|
||||
public final ShiroRule shiroRule = new ShiroRule();
|
||||
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
private final ObjectNode root = objectMapper.createObjectNode();
|
||||
private final HgConfigInIndexResource hgConfigInIndexResource;
|
||||
|
||||
public HgConfigInIndexResourceTest() {
|
||||
root.put("_links", objectMapper.createObjectNode());
|
||||
ScmPathInfoStore pathInfoStore = new ScmPathInfoStore();
|
||||
pathInfoStore.set(() -> URI.create("/"));
|
||||
hgConfigInIndexResource = new HgConfigInIndexResource(Providers.of(pathInfoStore), objectMapper);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SubjectAware(username = "admin", password = "secret")
|
||||
public void admin() {
|
||||
JsonEnricherContext context = new JsonEnricherContext(URI.create("/index"), MediaType.valueOf(VndMediaType.INDEX), root);
|
||||
|
||||
hgConfigInIndexResource.enrich(context);
|
||||
|
||||
assertEquals("/v2/config/hg", root.get("_links").get("hgConfig").get("href").asText());
|
||||
}
|
||||
|
||||
@Test
|
||||
@SubjectAware(username = "readOnly", password = "secret")
|
||||
public void user() {
|
||||
JsonEnricherContext context = new JsonEnricherContext(URI.create("/index"), MediaType.valueOf(VndMediaType.INDEX), root);
|
||||
|
||||
hgConfigInIndexResource.enrich(context);
|
||||
|
||||
assertFalse(root.get("_links").iterator().hasNext());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void anonymous() {
|
||||
JsonEnricherContext context = new JsonEnricherContext(URI.create("/index"), MediaType.valueOf(VndMediaType.INDEX), root);
|
||||
|
||||
hgConfigInIndexResource.enrich(context);
|
||||
|
||||
assertFalse(root.get("_links").iterator().hasNext());
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,10 @@
|
||||
readOnly = secret, reader
|
||||
writeOnly = secret, writer
|
||||
readWrite = secret, readerWriter
|
||||
admin = secret, admin
|
||||
|
||||
[roles]
|
||||
reader = configuration:read:hg
|
||||
writer = configuration:write:hg
|
||||
readerWriter = configuration:*:hg
|
||||
admin = *
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import sonia.scm.config.ConfigurationPermissions;
|
||||
import sonia.scm.plugin.Extension;
|
||||
import sonia.scm.web.JsonEnricherBase;
|
||||
import sonia.scm.web.JsonEnricherContext;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Provider;
|
||||
|
||||
import static java.util.Collections.singletonMap;
|
||||
import static sonia.scm.web.VndMediaType.INDEX;
|
||||
|
||||
@Extension
|
||||
public class SvnConfigInIndexResource extends JsonEnricherBase {
|
||||
|
||||
private final Provider<ScmPathInfoStore> scmPathInfoStore;
|
||||
|
||||
@Inject
|
||||
public SvnConfigInIndexResource(Provider<ScmPathInfoStore> scmPathInfoStore, ObjectMapper objectMapper) {
|
||||
super(objectMapper);
|
||||
this.scmPathInfoStore = scmPathInfoStore;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void enrich(JsonEnricherContext context) {
|
||||
if (resultHasMediaType(INDEX, context) && ConfigurationPermissions.list().isPermitted()) {
|
||||
String svnConfigUrl = new LinkBuilder(scmPathInfoStore.get().get(), SvnConfigResource.class)
|
||||
.method("get")
|
||||
.parameters()
|
||||
.href();
|
||||
|
||||
JsonNode svnConfigRefNode = createObject(singletonMap("href", value(svnConfigUrl)));
|
||||
|
||||
addPropertyNode(context.getResponseEntity().get("_links"), "svnConfig", svnConfigRefNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,122 +32,45 @@
|
||||
|
||||
package sonia.scm.web;
|
||||
|
||||
//~--- non-JDK imports --------------------------------------------------------
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.filter.GZipFilter;
|
||||
import sonia.scm.filter.GZipFilterConfig;
|
||||
import sonia.scm.filter.GZipResponseWrapper;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.SvnRepositoryHandler;
|
||||
import sonia.scm.repository.spi.ScmProviderHttpServlet;
|
||||
import sonia.scm.util.WebUtil;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.FilterConfig;
|
||||
import javax.servlet.ServletConfig;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
|
||||
//~--- JDK imports ------------------------------------------------------------
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
*/
|
||||
public class SvnGZipFilter extends GZipFilter implements ScmProviderHttpServlet
|
||||
{
|
||||
class SvnGZipFilter implements ScmProviderHttpServlet {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(SvnGZipFilter.class);
|
||||
|
||||
private final SvnRepositoryHandler handler;
|
||||
private final ScmProviderHttpServlet delegate;
|
||||
|
||||
//~--- constructors ---------------------------------------------------------
|
||||
private GZipFilterConfig config = new GZipFilterConfig();
|
||||
|
||||
/**
|
||||
* Constructs ...
|
||||
*
|
||||
*
|
||||
* @param handler
|
||||
*/
|
||||
public SvnGZipFilter(SvnRepositoryHandler handler, ScmProviderHttpServlet delegate)
|
||||
{
|
||||
SvnGZipFilter(SvnRepositoryHandler handler, ScmProviderHttpServlet delegate) {
|
||||
this.handler = handler;
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
//~--- methods --------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @param filterConfig
|
||||
*
|
||||
* @throws ServletException
|
||||
*/
|
||||
@Override
|
||||
public void init(FilterConfig filterConfig) throws ServletException
|
||||
{
|
||||
super.init(filterConfig);
|
||||
getConfig().setBufferResponse(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @param request
|
||||
* @param response
|
||||
* @param chain
|
||||
*
|
||||
* @throws IOException
|
||||
* @throws ServletException
|
||||
*/
|
||||
@Override
|
||||
protected void doFilter(HttpServletRequest request,
|
||||
HttpServletResponse response, FilterChain chain)
|
||||
throws IOException, ServletException
|
||||
{
|
||||
if (handler.getConfig().isEnabledGZip())
|
||||
{
|
||||
if (logger.isTraceEnabled())
|
||||
{
|
||||
logger.trace("encode svn request with gzip");
|
||||
}
|
||||
|
||||
super.doFilter(request, response, chain);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (logger.isTraceEnabled())
|
||||
{
|
||||
logger.trace("skip gzip encoding");
|
||||
}
|
||||
|
||||
chain.doFilter(request, response);
|
||||
}
|
||||
config.setBufferResponse(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void service(HttpServletRequest request, HttpServletResponse response, Repository repository) throws ServletException, IOException {
|
||||
if (handler.getConfig().isEnabledGZip())
|
||||
{
|
||||
if (logger.isTraceEnabled())
|
||||
{
|
||||
logger.trace("encode svn request with gzip");
|
||||
}
|
||||
|
||||
super.doFilter(request, response, (servletRequest, servletResponse) -> delegate.service((HttpServletRequest) servletRequest, (HttpServletResponse) servletResponse, repository));
|
||||
}
|
||||
else
|
||||
{
|
||||
if (logger.isTraceEnabled())
|
||||
{
|
||||
logger.trace("skip gzip encoding");
|
||||
}
|
||||
|
||||
if (handler.getConfig().isEnabledGZip() && WebUtil.isGzipSupported(request)) {
|
||||
logger.trace("compress svn response with gzip");
|
||||
GZipResponseWrapper wrappedResponse = new GZipResponseWrapper(response, config);
|
||||
delegate.service(request, wrappedResponse, repository);
|
||||
wrappedResponse.finishResponse();
|
||||
} else {
|
||||
logger.trace("skip gzip encoding");
|
||||
delegate.service(request, response, repository);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import { repositories } from "@scm-manager/ui-components";
|
||||
import type { Repository } from "@scm-manager/ui-types";
|
||||
|
||||
type Props = {
|
||||
@@ -10,14 +11,15 @@ class ProtocolInformation extends React.Component<Props> {
|
||||
|
||||
render() {
|
||||
const { repository } = this.props;
|
||||
if (!repository._links.httpProtocol) {
|
||||
const href = repositories.getProtocolLinkByType(repository, "http");
|
||||
if (!href) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<h4>Checkout the repository</h4>
|
||||
<pre>
|
||||
<code>svn checkout {repository._links.httpProtocol.href}</code>
|
||||
<code>svn checkout {href}</code>
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import com.github.sdorra.shiro.ShiroRule;
|
||||
import com.github.sdorra.shiro.SubjectAware;
|
||||
import com.google.inject.util.Providers;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import sonia.scm.web.JsonEnricherContext;
|
||||
import sonia.scm.web.VndMediaType;
|
||||
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import java.net.URI;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
|
||||
@SubjectAware(configuration = "classpath:sonia/scm/configuration/shiro.ini")
|
||||
public class SvnConfigInIndexResourceTest {
|
||||
|
||||
@Rule
|
||||
public final ShiroRule shiroRule = new ShiroRule();
|
||||
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
private final ObjectNode root = objectMapper.createObjectNode();
|
||||
private final SvnConfigInIndexResource svnConfigInIndexResource;
|
||||
|
||||
public SvnConfigInIndexResourceTest() {
|
||||
root.put("_links", objectMapper.createObjectNode());
|
||||
ScmPathInfoStore pathInfoStore = new ScmPathInfoStore();
|
||||
pathInfoStore.set(() -> URI.create("/"));
|
||||
svnConfigInIndexResource = new SvnConfigInIndexResource(Providers.of(pathInfoStore), objectMapper);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SubjectAware(username = "admin", password = "secret")
|
||||
public void admin() {
|
||||
JsonEnricherContext context = new JsonEnricherContext(URI.create("/index"), MediaType.valueOf(VndMediaType.INDEX), root);
|
||||
|
||||
svnConfigInIndexResource.enrich(context);
|
||||
|
||||
assertEquals("/v2/config/svn", root.get("_links").get("svnConfig").get("href").asText());
|
||||
}
|
||||
|
||||
@Test
|
||||
@SubjectAware(username = "readOnly", password = "secret")
|
||||
public void user() {
|
||||
JsonEnricherContext context = new JsonEnricherContext(URI.create("/index"), MediaType.valueOf(VndMediaType.INDEX), root);
|
||||
|
||||
svnConfigInIndexResource.enrich(context);
|
||||
|
||||
assertFalse(root.get("_links").iterator().hasNext());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void anonymous() {
|
||||
JsonEnricherContext context = new JsonEnricherContext(URI.create("/index"), MediaType.valueOf(VndMediaType.INDEX), root);
|
||||
|
||||
svnConfigInIndexResource.enrich(context);
|
||||
|
||||
assertFalse(root.get("_links").iterator().hasNext());
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,10 @@
|
||||
readOnly = secret, reader
|
||||
writeOnly = secret, writer
|
||||
readWrite = secret, readerWriter
|
||||
admin = secret, admin
|
||||
|
||||
[roles]
|
||||
reader = configuration:read:svn
|
||||
writer = configuration:write:svn
|
||||
readerWriter = configuration:*:svn
|
||||
admin = *
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"bootstrap": "lerna bootstrap",
|
||||
"link": "lerna exec -- yarn link"
|
||||
"link": "lerna exec -- yarn link",
|
||||
"unlink": "lerna exec --no-bail -- yarn unlink"
|
||||
},
|
||||
"devDependencies": {
|
||||
"lerna": "^3.2.1"
|
||||
|
||||
39
scm-ui-components/packages/ui-components/src/Help.js
Normal file
39
scm-ui-components/packages/ui-components/src/Help.js
Normal file
@@ -0,0 +1,39 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import injectSheet from "react-jss";
|
||||
import classNames from "classnames";
|
||||
|
||||
const styles = {
|
||||
img: {
|
||||
display: "block"
|
||||
},
|
||||
q: {
|
||||
float: "left",
|
||||
paddingLeft: "3px",
|
||||
float: "right"
|
||||
}
|
||||
};
|
||||
|
||||
type Props = {
|
||||
message: string,
|
||||
classes: any
|
||||
};
|
||||
|
||||
class Help extends React.Component<Props> {
|
||||
render() {
|
||||
const { message, classes } = this.props;
|
||||
const multiline = message.length > 60 ? "is-tooltip-multiline" : "";
|
||||
return (
|
||||
<div
|
||||
className={classNames("tooltip is-tooltip-right", multiline, classes.q)}
|
||||
data-tooltip={message}
|
||||
>
|
||||
<i
|
||||
className={classNames("fa fa-question has-text-info", classes.img)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default injectSheet(styles)(Help);
|
||||
@@ -0,0 +1,46 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import { Help } from "./index";
|
||||
|
||||
type Props = {
|
||||
label: string,
|
||||
helpText?: string
|
||||
};
|
||||
|
||||
class LabelWithHelpIcon extends React.Component<Props> {
|
||||
renderLabel = () => {
|
||||
const label = this.props.label;
|
||||
if (label) {
|
||||
return <label className="label">{label}</label>;
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
renderHelp = () => {
|
||||
const helpText = this.props.helpText;
|
||||
if (helpText) {
|
||||
return (
|
||||
<div className="control columns is-vcentered">
|
||||
<Help message={helpText} />
|
||||
</div>
|
||||
);
|
||||
} else return null;
|
||||
};
|
||||
|
||||
renderLabelWithHelpIcon = () => {
|
||||
if (this.props.label) {
|
||||
return (
|
||||
<div className="field is-grouped">
|
||||
<div className="control">{this.renderLabel()}</div>
|
||||
{this.renderHelp()}
|
||||
</div>
|
||||
);
|
||||
} else return null;
|
||||
};
|
||||
|
||||
render() {
|
||||
return this.renderLabelWithHelpIcon();
|
||||
}
|
||||
}
|
||||
|
||||
export default LabelWithHelpIcon;
|
||||
@@ -32,7 +32,7 @@ export function createUrl(url: string) {
|
||||
if (url.indexOf("/") !== 0) {
|
||||
urlWithStartingSlash = "/" + urlWithStartingSlash;
|
||||
}
|
||||
return `${contextPath}/api/rest/v2${urlWithStartingSlash}`;
|
||||
return `${contextPath}/api/v2${urlWithStartingSlash}`;
|
||||
}
|
||||
|
||||
class ApiClient {
|
||||
|
||||
@@ -9,7 +9,7 @@ describe("create url", () => {
|
||||
});
|
||||
|
||||
it("should add prefix for api", () => {
|
||||
expect(createUrl("/users")).toBe("/api/rest/v2/users");
|
||||
expect(createUrl("users")).toBe("/api/rest/v2/users");
|
||||
expect(createUrl("/users")).toBe("/api/v2/users");
|
||||
expect(createUrl("users")).toBe("/api/v2/users");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,7 +9,8 @@ type Props = {
|
||||
disabled: boolean,
|
||||
buttonLabel: string,
|
||||
fieldLabel: string,
|
||||
errorMessage: string
|
||||
errorMessage: string,
|
||||
helpText?: string
|
||||
};
|
||||
|
||||
type State = {
|
||||
@@ -25,7 +26,13 @@ class AddEntryToTableField extends React.Component<Props, State> {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { disabled, buttonLabel, fieldLabel, errorMessage } = this.props;
|
||||
const {
|
||||
disabled,
|
||||
buttonLabel,
|
||||
fieldLabel,
|
||||
errorMessage,
|
||||
helpText
|
||||
} = this.props;
|
||||
return (
|
||||
<div className="field">
|
||||
<InputField
|
||||
@@ -36,6 +43,7 @@ class AddEntryToTableField extends React.Component<Props, State> {
|
||||
value={this.state.entryToAdd}
|
||||
onReturnPressed={this.appendEntry}
|
||||
disabled={disabled}
|
||||
helpText={helpText}
|
||||
/>
|
||||
<AddButton
|
||||
label={buttonLabel}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import { Help } from "../index";
|
||||
|
||||
type Props = {
|
||||
label?: string,
|
||||
checked: boolean,
|
||||
onChange?: boolean => void,
|
||||
disabled?: boolean
|
||||
disabled?: boolean,
|
||||
helpText?: string
|
||||
};
|
||||
class Checkbox extends React.Component<Props> {
|
||||
onCheckboxChange = (event: SyntheticInputEvent<HTMLInputElement>) => {
|
||||
@@ -14,9 +16,20 @@ class Checkbox extends React.Component<Props> {
|
||||
}
|
||||
};
|
||||
|
||||
renderHelp = () => {
|
||||
const helpText = this.props.helpText;
|
||||
if (helpText) {
|
||||
return (
|
||||
<div className="control columns is-vcentered">
|
||||
<Help message={helpText} />
|
||||
</div>
|
||||
);
|
||||
} else return null;
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="field">
|
||||
<div className="field is-grouped">
|
||||
<div className="control">
|
||||
<label className="checkbox" disabled={this.props.disabled}>
|
||||
<input
|
||||
@@ -28,6 +41,7 @@ class Checkbox extends React.Component<Props> {
|
||||
{this.props.label}
|
||||
</label>
|
||||
</div>
|
||||
{this.renderHelp()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import classNames from "classnames";
|
||||
import { LabelWithHelpIcon } from "../index";
|
||||
|
||||
type Props = {
|
||||
label?: string,
|
||||
@@ -12,7 +13,8 @@ type Props = {
|
||||
onReturnPressed?: () => void,
|
||||
validationError: boolean,
|
||||
errorMessage: string,
|
||||
disabled?: boolean
|
||||
disabled?: boolean,
|
||||
helpText?: string
|
||||
};
|
||||
|
||||
class InputField extends React.Component<Props> {
|
||||
@@ -33,15 +35,6 @@ class InputField extends React.Component<Props> {
|
||||
this.props.onChange(event.target.value);
|
||||
};
|
||||
|
||||
renderLabel = () => {
|
||||
const label = this.props.label;
|
||||
if (label) {
|
||||
return <label className="label">{label}</label>;
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
|
||||
handleKeyPress = (event: SyntheticKeyboardEvent<HTMLInputElement>) => {
|
||||
const onReturnPressed = this.props.onReturnPressed;
|
||||
if (!onReturnPressed) {
|
||||
@@ -60,7 +53,9 @@ class InputField extends React.Component<Props> {
|
||||
value,
|
||||
validationError,
|
||||
errorMessage,
|
||||
disabled
|
||||
disabled,
|
||||
label,
|
||||
helpText
|
||||
} = this.props;
|
||||
const errorView = validationError ? "is-danger" : "";
|
||||
const helper = validationError ? (
|
||||
@@ -70,7 +65,7 @@ class InputField extends React.Component<Props> {
|
||||
);
|
||||
return (
|
||||
<div className="field">
|
||||
{this.renderLabel()}
|
||||
<LabelWithHelpIcon label={label} helpText={helpText} />
|
||||
<div className="control">
|
||||
<input
|
||||
ref={input => {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import classNames from "classnames";
|
||||
import { LabelWithHelpIcon } from "../index";
|
||||
|
||||
export type SelectItem = {
|
||||
value: string,
|
||||
@@ -10,7 +12,9 @@ type Props = {
|
||||
label?: string,
|
||||
options: SelectItem[],
|
||||
value?: SelectItem,
|
||||
onChange: string => void
|
||||
onChange: string => void,
|
||||
loading?: boolean,
|
||||
helpText?: string
|
||||
};
|
||||
|
||||
class Select extends React.Component<Props> {
|
||||
@@ -28,21 +32,18 @@ class Select extends React.Component<Props> {
|
||||
this.props.onChange(event.target.value);
|
||||
};
|
||||
|
||||
renderLabel = () => {
|
||||
const label = this.props.label;
|
||||
if (label) {
|
||||
return <label className="label">{label}</label>;
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
render() {
|
||||
const { options, value } = this.props;
|
||||
const { options, value, label, helpText, loading } = this.props;
|
||||
const loadingClass = loading ? "is-loading" : "";
|
||||
|
||||
|
||||
return (
|
||||
<div className="field">
|
||||
{this.renderLabel()}
|
||||
<div className="control select">
|
||||
<LabelWithHelpIcon label={label} helpText={helpText} />
|
||||
<div className={classNames(
|
||||
"control select",
|
||||
loadingClass
|
||||
)}>
|
||||
<select
|
||||
ref={input => {
|
||||
this.field = input;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import { LabelWithHelpIcon } from "../index";
|
||||
|
||||
export type SelectItem = {
|
||||
value: string,
|
||||
@@ -10,7 +11,8 @@ type Props = {
|
||||
label?: string,
|
||||
placeholder?: SelectItem[],
|
||||
value?: string,
|
||||
onChange: string => void
|
||||
onChange: string => void,
|
||||
helpText?: string
|
||||
};
|
||||
|
||||
class Textarea extends React.Component<Props> {
|
||||
@@ -20,20 +22,12 @@ class Textarea extends React.Component<Props> {
|
||||
this.props.onChange(event.target.value);
|
||||
};
|
||||
|
||||
renderLabel = () => {
|
||||
const label = this.props.label;
|
||||
if (label) {
|
||||
return <label className="label">{label}</label>;
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
render() {
|
||||
const { placeholder, value } = this.props;
|
||||
const { placeholder, value, label, helpText } = this.props;
|
||||
|
||||
return (
|
||||
<div className="field">
|
||||
{this.renderLabel()}
|
||||
<LabelWithHelpIcon label={label} helpText={helpText} />
|
||||
<div className="control">
|
||||
<textarea
|
||||
className="textarea"
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
|
||||
import * as validation from "./validation.js";
|
||||
import * as urls from "./urls";
|
||||
import * as repositories from "./repositories.js";
|
||||
|
||||
export { validation, urls };
|
||||
export { validation, urls, repositories };
|
||||
|
||||
export { default as DateFromNow } from "./DateFromNow.js";
|
||||
export { default as ErrorNotification } from "./ErrorNotification.js";
|
||||
@@ -15,6 +16,8 @@ export { default as MailLink } from "./MailLink.js";
|
||||
export { default as Notification } from "./Notification.js";
|
||||
export { default as Paginator } from "./Paginator.js";
|
||||
export { default as ProtectedRoute } from "./ProtectedRoute.js";
|
||||
export { default as Help } from "./Help.js";
|
||||
export { default as LabelWithHelpIcon } from "./LabelWithHelpIcon.js";
|
||||
|
||||
export { apiClient, NOT_FOUND_ERROR, UNAUTHORIZED_ERROR } from "./apiclient.js";
|
||||
|
||||
|
||||
19
scm-ui-components/packages/ui-components/src/repositories.js
Normal file
19
scm-ui-components/packages/ui-components/src/repositories.js
Normal file
@@ -0,0 +1,19 @@
|
||||
// @flow
|
||||
import type { Repository } from "@scm-manager/ui-types";
|
||||
|
||||
// util methods for repositories
|
||||
|
||||
export function getProtocolLinkByType(repository: Repository, type: string) {
|
||||
let protocols = repository._links.protocol;
|
||||
if (protocols) {
|
||||
if (!Array.isArray(protocols)) {
|
||||
protocols = [protocols];
|
||||
}
|
||||
for (let proto of protocols) {
|
||||
if (proto.name === type) {
|
||||
return proto.href;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
// @flow
|
||||
|
||||
import type { Repository } from "@scm-manager/ui-types";
|
||||
import { getProtocolLinkByType, getTypePredicate } from "./repositories";
|
||||
|
||||
describe("getProtocolLinkByType tests", () => {
|
||||
|
||||
it("should return the http protocol link", () => {
|
||||
|
||||
const repository: Repository = {
|
||||
namespace: "scm",
|
||||
name: "core",
|
||||
type: "git",
|
||||
_links: {
|
||||
protocol: [{
|
||||
name: "http",
|
||||
href: "http://scm.scm-manager.org/repo/scm/core"
|
||||
}]
|
||||
}
|
||||
};
|
||||
|
||||
const link = getProtocolLinkByType(repository, "http");
|
||||
expect(link).toBe("http://scm.scm-manager.org/repo/scm/core");
|
||||
});
|
||||
|
||||
it("should return the http protocol link from multiple protocols", () => {
|
||||
|
||||
const repository: Repository = {
|
||||
namespace: "scm",
|
||||
name: "core",
|
||||
type: "git",
|
||||
_links: {
|
||||
protocol: [{
|
||||
name: "http",
|
||||
href: "http://scm.scm-manager.org/repo/scm/core"
|
||||
},{
|
||||
name: "ssh",
|
||||
href: "git@scm.scm-manager.org:scm/core"
|
||||
}]
|
||||
}
|
||||
};
|
||||
|
||||
const link = getProtocolLinkByType(repository, "http");
|
||||
expect(link).toBe("http://scm.scm-manager.org/repo/scm/core");
|
||||
});
|
||||
|
||||
it("should return the http protocol, even if the protocol is a single link", () => {
|
||||
|
||||
const repository: Repository = {
|
||||
namespace: "scm",
|
||||
name: "core",
|
||||
type: "git",
|
||||
_links: {
|
||||
protocol: {
|
||||
name: "http",
|
||||
href: "http://scm.scm-manager.org/repo/scm/core"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const link = getProtocolLinkByType(repository, "http");
|
||||
expect(link).toBe("http://scm.scm-manager.org/repo/scm/core");
|
||||
});
|
||||
|
||||
it("should return null, if such a protocol does not exists", () => {
|
||||
|
||||
const repository: Repository = {
|
||||
namespace: "scm",
|
||||
name: "core",
|
||||
type: "git",
|
||||
_links: {
|
||||
protocol: [{
|
||||
name: "http",
|
||||
href: "http://scm.scm-manager.org/repo/scm/core"
|
||||
},{
|
||||
name: "ssh",
|
||||
href: "git@scm.scm-manager.org:scm/core"
|
||||
}]
|
||||
}
|
||||
};
|
||||
|
||||
const link = getProtocolLinkByType(repository, "awesome");
|
||||
expect(link).toBeNull();
|
||||
});
|
||||
|
||||
it("should return null, if no protocols are available", () => {
|
||||
|
||||
const repository: Repository = {
|
||||
namespace: "scm",
|
||||
name: "core",
|
||||
type: "git",
|
||||
_links: {}
|
||||
};
|
||||
|
||||
const link = getProtocolLinkByType(repository, "http");
|
||||
expect(link).toBeNull();
|
||||
});
|
||||
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
// @flow
|
||||
const nameRegex = /^([A-z0-9.\-_@]|[^ ]([A-z0-9.\-_@ ]*[A-z0-9.\-_@]|[^\s])?)$/;
|
||||
const nameRegex = /^[A-Za-z0-9\.\-_][A-Za-z0-9\.\-_@]*$/;
|
||||
|
||||
export const isNameValid = (name: string) => {
|
||||
return nameRegex.test(name);
|
||||
|
||||
@@ -5,6 +5,7 @@ describe("test name validation", () => {
|
||||
it("should return false", () => {
|
||||
// invalid names taken from ValidationUtilTest.java
|
||||
const invalidNames = [
|
||||
"@test",
|
||||
" test 123",
|
||||
" test 123 ",
|
||||
"test 123 ",
|
||||
@@ -35,10 +36,9 @@ describe("test name validation", () => {
|
||||
"Test123-git",
|
||||
"Test_user-123.git",
|
||||
"test@scm-manager.de",
|
||||
"test 123",
|
||||
"test123",
|
||||
"tt",
|
||||
"t",
|
||||
|
||||
"valid_name",
|
||||
"another1",
|
||||
"stillValid",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,17 @@
|
||||
//@flow
|
||||
import type { Links } from "./hal";
|
||||
|
||||
export type Permission = {
|
||||
name: string,
|
||||
type: string,
|
||||
groupPermission: boolean,
|
||||
_links?: Links
|
||||
};
|
||||
|
||||
export type PermissionEntry = {
|
||||
name: string,
|
||||
type: string,
|
||||
groupPermission: boolean
|
||||
}
|
||||
|
||||
export type PermissionCollection = Permission[];
|
||||
@@ -1,9 +1,10 @@
|
||||
// @flow
|
||||
export type Link = {
|
||||
href: string
|
||||
href: string,
|
||||
name?: string
|
||||
};
|
||||
|
||||
export type Links = { [string]: Link };
|
||||
export type Links = { [string]: Link | Link[] };
|
||||
|
||||
export type Collection = {
|
||||
_embedded: Object,
|
||||
|
||||
@@ -14,3 +14,5 @@ export type { Changeset } from "./Changesets";
|
||||
export type { Tag } from "./Tags"
|
||||
|
||||
export type { Config } from "./Config";
|
||||
|
||||
export type { Permission, PermissionEntry, PermissionCollection } from "./RepositoryPermissions";
|
||||
|
||||
@@ -48,6 +48,16 @@
|
||||
<script>bootstrap</script>
|
||||
</configuration>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>unlink</id>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>run</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<script>unlink</script>
|
||||
</configuration>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>link</id>
|
||||
<phase>package</phase>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -8,6 +8,7 @@
|
||||
"@fortawesome/fontawesome-free": "^5.3.1",
|
||||
"@scm-manager/ui-extensions": "^0.0.7",
|
||||
"bulma": "^0.7.1",
|
||||
"bulma-tooltip": "^2.0.2",
|
||||
"classnames": "^2.2.5",
|
||||
"font-awesome": "^4.7.0",
|
||||
"history": "^4.7.2",
|
||||
@@ -15,6 +16,7 @@
|
||||
"i18next-browser-languagedetector": "^2.2.2",
|
||||
"i18next-fetch-backend": "^0.1.0",
|
||||
"moment": "^2.22.2",
|
||||
"node-sass": "^4.9.3",
|
||||
"react": "^16.4.2",
|
||||
"react-dom": "^16.4.2",
|
||||
"react-i18next": "^7.9.0",
|
||||
|
||||
@@ -64,5 +64,29 @@
|
||||
"login-attempt-limit-timeout-invalid": "This is not a number",
|
||||
"login-attempt-limit-invalid": "This is not a number",
|
||||
"plugin-url-invalid": "This is not a valid url"
|
||||
},
|
||||
"help": {
|
||||
"realmDescriptionHelpText": "Enter authentication realm description",
|
||||
"dateFormatHelpText": "Moments date format. Please have a look at the momentjs documentation.",
|
||||
"pluginRepositoryHelpText": "The url of the plugin repository. Explanation of the placeholders: version = SCM-Manager Version; os = Operation System; arch = Architecture",
|
||||
"enableForwardingHelpText": "Enbale mod_proxy port forwarding.",
|
||||
"enableRepositoryArchiveHelpText": "Enable repository archives. A complete page reload is required after a change of this value.",
|
||||
"disableGroupingGridHelpText": "Disable repository Groups. A complete page reload is required after a change of this value.",
|
||||
"allowAnonymousAccessHelpText": "Anonymous users have read access on public repositories.",
|
||||
"skipFailedAuthenticatorsHelpText": "Do not stop the authentication chain, if an authenticator finds the user but fails to authenticate the user.",
|
||||
"adminGroupsHelpText": "Names of groups with admin permissions.",
|
||||
"adminUsersHelpText": "Names of users with admin permissions.",
|
||||
"forceBaseUrlHelpText": "Redirects to the base url if the request comes from a other url",
|
||||
"baseUrlHelpText": "The url of the application (with context path), i.e. http://localhost:8080/scm",
|
||||
"loginAttemptLimitHelpText": "Maximum allowed login attempts. Use -1 to disable the login attempt limit.",
|
||||
"loginAttemptLimitTimeoutHelpText": "Timeout in seconds for users which are temporary disabled, because of too many failed login attempts.",
|
||||
"enableProxyHelpText": "Enable Proxy",
|
||||
"proxyPortHelpText": "The proxy port",
|
||||
"proxyPasswordHelpText": "The password for the proxy server authentication.",
|
||||
"proxyServerHelpText": "The proxy server",
|
||||
"proxyUserHelpText": "The username for the proxy server authentication.",
|
||||
"proxyExcludesHelpText": "Glob patterns for hostnames which should be excluded from proxy settings.",
|
||||
"enableXsrfProtectionHelpText": "Enable Xsrf Cookie Protection. Note: This feature is still experimental.",
|
||||
"defaultNameSpaceStrategyHelpText": "The default namespace strategy"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,12 @@
|
||||
"group-form": {
|
||||
"submit": "Submit",
|
||||
"name-error": "Group name is invalid",
|
||||
"description-error": "Description is invalid"
|
||||
"description-error": "Description is invalid",
|
||||
"help": {
|
||||
"nameHelpText": "Unique name of the group",
|
||||
"descriptionHelpText": "A short description of the group",
|
||||
"memberHelpText": "Usernames of the group members"
|
||||
}
|
||||
},
|
||||
"delete-group-button": {
|
||||
"label": "Delete",
|
||||
|
||||
@@ -23,7 +23,8 @@
|
||||
"back-label": "Back",
|
||||
"navigation-label": "Navigation",
|
||||
"information": "Information",
|
||||
"history": "History"
|
||||
"history": "History",
|
||||
"permissions": "Permissions"
|
||||
},
|
||||
"create": {
|
||||
"title": "Create Repository",
|
||||
@@ -43,5 +44,36 @@
|
||||
"submit": "Yes",
|
||||
"cancel": "No"
|
||||
}
|
||||
},
|
||||
"permission": {
|
||||
"error-title": "Error",
|
||||
"error-subtitle": "Unknown permissions error",
|
||||
"name": "User or Group",
|
||||
"type": "Type",
|
||||
"group-permission": "Group Permission",
|
||||
"edit-permission": {
|
||||
"delete-button": "Delete",
|
||||
"save-button": "Save Changes"
|
||||
},
|
||||
"delete-permission-button": {
|
||||
"label": "Delete",
|
||||
"confirm-alert": {
|
||||
"title": "Delete permission",
|
||||
"message": "Do you really want to delete the permission?",
|
||||
"submit": "Yes",
|
||||
"cancel": "No"
|
||||
}
|
||||
},
|
||||
"add-permission": {
|
||||
"add-permission-heading": "Add new Permission",
|
||||
"submit-button": "Submit",
|
||||
"name-input-invalid": "Permission is not allowed to be empty! If it is not empty, your input name is invalid or it already exists!"
|
||||
}
|
||||
},
|
||||
"help": {
|
||||
"nameHelpText": "The name of the repository. This name will be part of the repository url.",
|
||||
"typeHelpText": "The type of the repository (e.g. Mercurial, Git or Subversion).",
|
||||
"contactHelpText": "Email address of the person who is responsible for this repository.",
|
||||
"descriptionHelpText": "A short description of the repository."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,5 +51,14 @@
|
||||
"password-invalid": "Password has to be between 6 and 32 characters",
|
||||
"passwordValidation-invalid": "Passwords have to be the same",
|
||||
"validatePassword": "Please validate password here"
|
||||
},
|
||||
"help": {
|
||||
"usernameHelpText": "Unique name of the user.",
|
||||
"displayNameHelpText": "Display name of the user.",
|
||||
"mailHelpText": "Email address of the user.",
|
||||
"passwordHelpText": "Plain text password of the user.",
|
||||
"passwordConfirmHelpText": "Repeat the password for validation.",
|
||||
"adminHelpText": "An administrator is able to create, modify and delete repositories, groups and users.",
|
||||
"activeHelpText": "Activate or deactive the user."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,12 +23,14 @@ class BaseUrlSettings extends React.Component<Props> {
|
||||
label={t("base-url-settings.force-base-url")}
|
||||
onChange={this.handleForceBaseUrlChange}
|
||||
disabled={!hasUpdatePermission}
|
||||
helpText={t("help.forceBaseUrlHelpText")}
|
||||
/>
|
||||
<InputField
|
||||
label={t("base-url-settings.base-url")}
|
||||
onChange={this.handleBaseUrlChange}
|
||||
value={baseUrl}
|
||||
disabled={!hasUpdatePermission}
|
||||
helpText={t("help.baseUrlHelpText")}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -41,54 +41,63 @@ class GeneralSettings extends React.Component<Props> {
|
||||
onChange={this.handleRealmDescriptionChange}
|
||||
value={realmDescription}
|
||||
disabled={!hasUpdatePermission}
|
||||
helpText={t("help.realmDescriptionHelpText")}
|
||||
/>
|
||||
<InputField
|
||||
label={t("general-settings.date-format")}
|
||||
onChange={this.handleDateFormatChange}
|
||||
value={dateFormat}
|
||||
disabled={!hasUpdatePermission}
|
||||
helpText={t("help.dateFormatHelpText")}
|
||||
/>
|
||||
<InputField
|
||||
label={t("general-settings.plugin-url")}
|
||||
onChange={this.handlePluginUrlChange}
|
||||
value={pluginUrl}
|
||||
disabled={!hasUpdatePermission}
|
||||
helpText={t("help.pluginRepositoryHelpText")}
|
||||
/>
|
||||
<InputField
|
||||
label={t("general-settings.default-namespace-strategy")}
|
||||
onChange={this.handleDefaultNamespaceStrategyChange}
|
||||
value={defaultNamespaceStrategy}
|
||||
disabled={!hasUpdatePermission}
|
||||
helpText={t("help.defaultNameSpaceStrategyHelpText")}
|
||||
/>
|
||||
<Checkbox
|
||||
checked={enabledXsrfProtection}
|
||||
label={t("general-settings.enabled-xsrf-protection")}
|
||||
onChange={this.handleEnabledXsrfProtectionChange}
|
||||
disabled={!hasUpdatePermission}
|
||||
helpText={t("help.enableXsrfProtectionHelpText")}
|
||||
/>
|
||||
<Checkbox
|
||||
checked={enableRepositoryArchive}
|
||||
label={t("general-settings.enable-repository-archive")}
|
||||
onChange={this.handleEnableRepositoryArchiveChange}
|
||||
disabled={!hasUpdatePermission}
|
||||
helpText={t("help.enableRepositoryArchiveHelpText")}
|
||||
/>
|
||||
<Checkbox
|
||||
checked={disableGroupingGrid}
|
||||
label={t("general-settings.disable-grouping-grid")}
|
||||
onChange={this.handleDisableGroupingGridChange}
|
||||
disabled={!hasUpdatePermission}
|
||||
helpText={t("help.disableGroupingGridHelpText")}
|
||||
/>
|
||||
<Checkbox
|
||||
checked={anonymousAccessEnabled}
|
||||
label={t("general-settings.anonymous-access-enabled")}
|
||||
onChange={this.handleAnonymousAccessEnabledChange}
|
||||
disabled={!hasUpdatePermission}
|
||||
helpText={t("help.allowAnonymousAccessHelpText")}
|
||||
/>
|
||||
<Checkbox
|
||||
checked={skipFailedAuthenticators}
|
||||
label={t("general-settings.skip-failed-authenticators")}
|
||||
onChange={this.handleSkipFailedAuthenticatorsChange}
|
||||
disabled={!hasUpdatePermission}
|
||||
helpText={t("help.skipFailedAuthenticatorsHelpText")}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -47,6 +47,7 @@ class LoginAttempt extends React.Component<Props, State> {
|
||||
disabled={!hasUpdatePermission}
|
||||
validationError={this.state.loginAttemptLimitError}
|
||||
errorMessage={t("validation.login-attempt-limit-invalid")}
|
||||
helpText={t("help.loginAttemptLimitHelpText")}
|
||||
/>
|
||||
<InputField
|
||||
label={t("login-attempt.login-attempt-limit-timeout")}
|
||||
@@ -55,6 +56,7 @@ class LoginAttempt extends React.Component<Props, State> {
|
||||
disabled={!hasUpdatePermission}
|
||||
validationError={this.state.loginAttemptLimitTimeoutError}
|
||||
errorMessage={t("validation.login-attempt-limit-timeout-invalid")}
|
||||
helpText={t("help.loginAttemptLimitTimeoutHelpText")}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -42,6 +42,7 @@ class ProxySettings extends React.Component<Props> {
|
||||
label={t("proxy-settings.enable-proxy")}
|
||||
onChange={this.handleEnableProxyChange}
|
||||
disabled={!hasUpdatePermission}
|
||||
helpText={t("help.enableProxyHelpText")}
|
||||
/>
|
||||
<InputField
|
||||
label={t("proxy-settings.proxy-password")}
|
||||
@@ -49,24 +50,28 @@ class ProxySettings extends React.Component<Props> {
|
||||
value={proxyPassword}
|
||||
type="password"
|
||||
disabled={!enableProxy || !hasUpdatePermission}
|
||||
helpText={t("help.proxyPasswordHelpText")}
|
||||
/>
|
||||
<InputField
|
||||
label={t("proxy-settings.proxy-port")}
|
||||
value={proxyPort}
|
||||
onChange={this.handleProxyPortChange}
|
||||
disabled={!enableProxy || !hasUpdatePermission}
|
||||
helpText={t("help.proxyPortHelpText")}
|
||||
/>
|
||||
<InputField
|
||||
label={t("proxy-settings.proxy-server")}
|
||||
value={proxyServer}
|
||||
onChange={this.handleProxyServerChange}
|
||||
disabled={!enableProxy || !hasUpdatePermission}
|
||||
helpText={t("help.proxyServerHelpText")}
|
||||
/>
|
||||
<InputField
|
||||
label={t("proxy-settings.proxy-user")}
|
||||
value={proxyUser}
|
||||
onChange={this.handleProxyUserChange}
|
||||
disabled={!enableProxy || !hasUpdatePermission}
|
||||
helpText={t("help.proxyUserHelpText")}
|
||||
/>
|
||||
<ProxyExcludesTable
|
||||
proxyExcludes={proxyExcludes}
|
||||
|
||||
@@ -24,6 +24,7 @@ class AdminGroupTable extends React.Component<Props, State> {
|
||||
removeLabel={t("admin-settings.remove-group-button")}
|
||||
onRemove={this.removeEntry}
|
||||
disabled={disabled}
|
||||
helpText={t("help.adminGroupsHelpText")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ class AdminUserTable extends React.Component<Props> {
|
||||
removeLabel={t("admin-settings.remove-user-button")}
|
||||
onRemove={this.removeEntry}
|
||||
disabled={disabled}
|
||||
helpText={t("help.adminUsersHelpText")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import { RemoveEntryOfTableButton } from "@scm-manager/ui-components";
|
||||
import { RemoveEntryOfTableButton, LabelWithHelpIcon } from "@scm-manager/ui-components";
|
||||
|
||||
type Props = {
|
||||
items: string[],
|
||||
label: string,
|
||||
removeLabel: string,
|
||||
onRemove: (string[], string) => void,
|
||||
disabled: boolean
|
||||
disabled: boolean,
|
||||
helpText: string
|
||||
};
|
||||
|
||||
class ArrayConfigTable extends React.Component<Props> {
|
||||
render() {
|
||||
const { label, disabled, removeLabel, items } = this.props;
|
||||
const { label, disabled, removeLabel, items, helpText } = this.props;
|
||||
return (
|
||||
<div>
|
||||
<label className="label">{label}</label>
|
||||
<LabelWithHelpIcon label={label} helpText={helpText}/>
|
||||
<table className="table is-hoverable is-fullwidth">
|
||||
<tbody>
|
||||
{items.map(item => {
|
||||
|
||||
@@ -22,6 +22,7 @@ class ProxyExcludesTable extends React.Component<Props, State> {
|
||||
removeLabel={t("proxy-settings.remove-proxy-exclude-button")}
|
||||
onRemove={this.removeEntry}
|
||||
disabled={disabled}
|
||||
helpText={t("help.proxyExcludesHelpText")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ import reducer, {
|
||||
getConfigUpdatePermission
|
||||
} from "./config";
|
||||
|
||||
const CONFIG_URL = "/api/rest/v2/config";
|
||||
const CONFIG_URL = "/api/v2/config";
|
||||
|
||||
const error = new Error("You have an error!");
|
||||
|
||||
@@ -51,8 +51,8 @@ const config = {
|
||||
enabledXsrfProtection: true,
|
||||
defaultNamespaceStrategy: "sonia.scm.repository.DefaultNamespaceStrategy",
|
||||
_links: {
|
||||
self: { href: "http://localhost:8081/api/rest/v2/config" },
|
||||
update: { href: "http://localhost:8081/api/rest/v2/config" }
|
||||
self: { href: "http://localhost:8081/api/v2/config" },
|
||||
update: { href: "http://localhost:8081/api/v2/config" }
|
||||
}
|
||||
};
|
||||
|
||||
@@ -80,8 +80,8 @@ const configWithNullValues = {
|
||||
enabledXsrfProtection: true,
|
||||
defaultNamespaceStrategy: "sonia.scm.repository.DefaultNamespaceStrategy",
|
||||
_links: {
|
||||
self: { href: "http://localhost:8081/api/rest/v2/config" },
|
||||
update: { href: "http://localhost:8081/api/rest/v2/config" }
|
||||
self: { href: "http://localhost:8081/api/v2/config" },
|
||||
update: { href: "http://localhost:8081/api/v2/config" }
|
||||
}
|
||||
};
|
||||
|
||||
@@ -135,7 +135,7 @@ describe("config fetch()", () => {
|
||||
});
|
||||
|
||||
it("should successfully modify config", () => {
|
||||
fetchMock.putOnce("http://localhost:8081/api/rest/v2/config", {
|
||||
fetchMock.putOnce("http://localhost:8081/api/v2/config", {
|
||||
status: 204
|
||||
});
|
||||
|
||||
@@ -150,7 +150,7 @@ describe("config fetch()", () => {
|
||||
});
|
||||
|
||||
it("should call the callback after modifying config", () => {
|
||||
fetchMock.putOnce("http://localhost:8081/api/rest/v2/config", {
|
||||
fetchMock.putOnce("http://localhost:8081/api/v2/config", {
|
||||
status: 204
|
||||
});
|
||||
|
||||
@@ -169,7 +169,7 @@ describe("config fetch()", () => {
|
||||
});
|
||||
|
||||
it("should fail modifying config on HTTP 500", () => {
|
||||
fetchMock.putOnce("http://localhost:8081/api/rest/v2/config", {
|
||||
fetchMock.putOnce("http://localhost:8081/api/v2/config", {
|
||||
status: 500
|
||||
});
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import groups from "./groups/modules/groups";
|
||||
import auth from "./modules/auth";
|
||||
import pending from "./modules/pending";
|
||||
import failure from "./modules/failure";
|
||||
import permissions from "./repos/permissions/modules/permissions";
|
||||
import config from "./config/modules/config";
|
||||
|
||||
import type { BrowserHistory } from "history/createBrowserHistory";
|
||||
@@ -30,6 +31,7 @@ function createReduxStore(history: BrowserHistory) {
|
||||
repositoryTypes,
|
||||
changesets,
|
||||
branches,
|
||||
permissions,
|
||||
groups,
|
||||
auth,
|
||||
config
|
||||
|
||||
@@ -80,6 +80,7 @@ class GroupForm extends React.Component<Props, State> {
|
||||
onChange={this.handleGroupNameChange}
|
||||
value={group.name}
|
||||
validationError={this.state.nameValidationError}
|
||||
helpText={t("group-form.help.nameHelpText")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -93,6 +94,7 @@ class GroupForm extends React.Component<Props, State> {
|
||||
onChange={this.handleDescriptionChange}
|
||||
value={group.description}
|
||||
validationError={false}
|
||||
helpText={t("group-form.help.descriptionHelpText")}
|
||||
/>
|
||||
<MemberNameTable
|
||||
members={this.state.group.members}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import { translate } from "react-i18next";
|
||||
import { RemoveEntryOfTableButton } from "@scm-manager/ui-components";
|
||||
import {
|
||||
RemoveEntryOfTableButton,
|
||||
LabelWithHelpIcon
|
||||
} from "@scm-manager/ui-components";
|
||||
|
||||
type Props = {
|
||||
members: string[],
|
||||
@@ -16,7 +19,10 @@ class MemberNameTable extends React.Component<Props, State> {
|
||||
const { t } = this.props;
|
||||
return (
|
||||
<div>
|
||||
<label className="label">{t("group.members")}</label>
|
||||
<LabelWithHelpIcon
|
||||
label={t("group.members")}
|
||||
helpText={t("group-form.help.memberHelpText")}
|
||||
/>
|
||||
<table className="table is-hoverable is-fullwidth">
|
||||
<tbody>
|
||||
{this.props.members.map(member => {
|
||||
|
||||
@@ -44,7 +44,7 @@ import reducer, {
|
||||
MODIFY_GROUP_SUCCESS,
|
||||
MODIFY_GROUP_FAILURE
|
||||
} from "./groups";
|
||||
const GROUPS_URL = "/api/rest/v2/groups";
|
||||
const GROUPS_URL = "/api/v2/groups";
|
||||
|
||||
const error = new Error("You have an error!");
|
||||
|
||||
@@ -57,13 +57,13 @@ const humanGroup = {
|
||||
members: ["userZaphod"],
|
||||
_links: {
|
||||
self: {
|
||||
href: "http://localhost:8081/api/rest/v2/groups/humanGroup"
|
||||
href: "http://localhost:8081/api/v2/groups/humanGroup"
|
||||
},
|
||||
delete: {
|
||||
href: "http://localhost:8081/api/rest/v2/groups/humanGroup"
|
||||
href: "http://localhost:8081/api/v2/groups/humanGroup"
|
||||
},
|
||||
update: {
|
||||
href:"http://localhost:8081/api/rest/v2/groups/humanGroup"
|
||||
href:"http://localhost:8081/api/v2/groups/humanGroup"
|
||||
}
|
||||
},
|
||||
_embedded: {
|
||||
@@ -72,7 +72,7 @@ const humanGroup = {
|
||||
name: "userZaphod",
|
||||
_links: {
|
||||
self: {
|
||||
href: "http://localhost:8081/api/rest/v2/users/userZaphod"
|
||||
href: "http://localhost:8081/api/v2/users/userZaphod"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -89,13 +89,13 @@ const emptyGroup = {
|
||||
members: [],
|
||||
_links: {
|
||||
self: {
|
||||
href: "http://localhost:8081/api/rest/v2/groups/emptyGroup"
|
||||
href: "http://localhost:8081/api/v2/groups/emptyGroup"
|
||||
},
|
||||
delete: {
|
||||
href: "http://localhost:8081/api/rest/v2/groups/emptyGroup"
|
||||
href: "http://localhost:8081/api/v2/groups/emptyGroup"
|
||||
},
|
||||
update: {
|
||||
href:"http://localhost:8081/api/rest/v2/groups/emptyGroup"
|
||||
href:"http://localhost:8081/api/v2/groups/emptyGroup"
|
||||
}
|
||||
},
|
||||
_embedded: {
|
||||
@@ -108,16 +108,16 @@ const responseBody = {
|
||||
pageTotal: 1,
|
||||
_links: {
|
||||
self: {
|
||||
href: "http://localhost:3000/api/rest/v2/groups/?page=0&pageSize=10"
|
||||
href: "http://localhost:3000/api/v2/groups/?page=0&pageSize=10"
|
||||
},
|
||||
first: {
|
||||
href: "http://localhost:3000/api/rest/v2/groups/?page=0&pageSize=10"
|
||||
href: "http://localhost:3000/api/v2/groups/?page=0&pageSize=10"
|
||||
},
|
||||
last: {
|
||||
href: "http://localhost:3000/api/rest/v2/groups/?page=0&pageSize=10"
|
||||
href: "http://localhost:3000/api/v2/groups/?page=0&pageSize=10"
|
||||
},
|
||||
create: {
|
||||
href: "http://localhost:3000/api/rest/v2/groups/"
|
||||
href: "http://localhost:3000/api/v2/groups/"
|
||||
}
|
||||
},
|
||||
_embedded: {
|
||||
@@ -244,7 +244,7 @@ describe("groups fetch()", () => {
|
||||
});
|
||||
|
||||
it("should successfully modify group", () => {
|
||||
fetchMock.putOnce("http://localhost:8081/api/rest/v2/groups/humanGroup", {
|
||||
fetchMock.putOnce("http://localhost:8081/api/v2/groups/humanGroup", {
|
||||
status: 204
|
||||
});
|
||||
|
||||
@@ -259,7 +259,7 @@ describe("groups fetch()", () => {
|
||||
});
|
||||
|
||||
it("should call the callback after modifying group", () => {
|
||||
fetchMock.putOnce("http://localhost:8081/api/rest/v2/groups/humanGroup", {
|
||||
fetchMock.putOnce("http://localhost:8081/api/v2/groups/humanGroup", {
|
||||
status: 204
|
||||
});
|
||||
|
||||
@@ -278,7 +278,7 @@ describe("groups fetch()", () => {
|
||||
});
|
||||
|
||||
it("should fail modifying group on HTTP 500", () => {
|
||||
fetchMock.putOnce("http://localhost:8081/api/rest/v2/groups/humanGroup", {
|
||||
fetchMock.putOnce("http://localhost:8081/api/v2/groups/humanGroup", {
|
||||
status: 500
|
||||
});
|
||||
|
||||
@@ -293,7 +293,7 @@ describe("groups fetch()", () => {
|
||||
});
|
||||
|
||||
it("should delete successfully group humanGroup", () => {
|
||||
fetchMock.deleteOnce("http://localhost:8081/api/rest/v2/groups/humanGroup", {
|
||||
fetchMock.deleteOnce("http://localhost:8081/api/v2/groups/humanGroup", {
|
||||
status: 204
|
||||
});
|
||||
|
||||
@@ -308,7 +308,7 @@ describe("groups fetch()", () => {
|
||||
});
|
||||
|
||||
it("should call the callback, after successful delete", () => {
|
||||
fetchMock.deleteOnce("http://localhost:8081/api/rest/v2/groups/humanGroup", {
|
||||
fetchMock.deleteOnce("http://localhost:8081/api/v2/groups/humanGroup", {
|
||||
status: 204
|
||||
});
|
||||
|
||||
@@ -324,7 +324,7 @@ describe("groups fetch()", () => {
|
||||
});
|
||||
|
||||
it("should fail to delete group humanGroup", () => {
|
||||
fetchMock.deleteOnce("http://localhost:8081/api/rest/v2/groups/humanGroup", {
|
||||
fetchMock.deleteOnce("http://localhost:8081/api/v2/groups/humanGroup", {
|
||||
status: 500
|
||||
});
|
||||
|
||||
|
||||
@@ -78,7 +78,7 @@ describe("auth actions", () => {
|
||||
});
|
||||
|
||||
it("should dispatch login success and dispatch fetch me", () => {
|
||||
fetchMock.postOnce("/api/rest/v2/auth/access_token", {
|
||||
fetchMock.postOnce("/api/v2/auth/access_token", {
|
||||
body: {
|
||||
cookie: true,
|
||||
grant_type: "password",
|
||||
@@ -88,7 +88,7 @@ describe("auth actions", () => {
|
||||
headers: { "content-type": "application/json" }
|
||||
});
|
||||
|
||||
fetchMock.getOnce("/api/rest/v2/me", {
|
||||
fetchMock.getOnce("/api/v2/me", {
|
||||
body: me,
|
||||
headers: { "content-type": "application/json" }
|
||||
});
|
||||
@@ -106,7 +106,7 @@ describe("auth actions", () => {
|
||||
});
|
||||
|
||||
it("should dispatch login failure", () => {
|
||||
fetchMock.postOnce("/api/rest/v2/auth/access_token", {
|
||||
fetchMock.postOnce("/api/v2/auth/access_token", {
|
||||
status: 400
|
||||
});
|
||||
|
||||
@@ -120,7 +120,7 @@ describe("auth actions", () => {
|
||||
});
|
||||
|
||||
it("should dispatch fetch me success", () => {
|
||||
fetchMock.getOnce("/api/rest/v2/me", {
|
||||
fetchMock.getOnce("/api/v2/me", {
|
||||
body: me,
|
||||
headers: { "content-type": "application/json" }
|
||||
});
|
||||
@@ -141,7 +141,7 @@ describe("auth actions", () => {
|
||||
});
|
||||
|
||||
it("should dispatch fetch me failure", () => {
|
||||
fetchMock.getOnce("/api/rest/v2/me", {
|
||||
fetchMock.getOnce("/api/v2/me", {
|
||||
status: 500
|
||||
});
|
||||
|
||||
@@ -155,7 +155,7 @@ describe("auth actions", () => {
|
||||
});
|
||||
|
||||
it("should dispatch fetch me unauthorized", () => {
|
||||
fetchMock.getOnce("/api/rest/v2/me", {
|
||||
fetchMock.getOnce("/api/v2/me", {
|
||||
status: 401
|
||||
});
|
||||
|
||||
@@ -173,11 +173,11 @@ describe("auth actions", () => {
|
||||
});
|
||||
|
||||
it("should dispatch logout success", () => {
|
||||
fetchMock.deleteOnce("/api/rest/v2/auth/access_token", {
|
||||
fetchMock.deleteOnce("/api/v2/auth/access_token", {
|
||||
status: 204
|
||||
});
|
||||
|
||||
fetchMock.getOnce("/api/rest/v2/me", {
|
||||
fetchMock.getOnce("/api/v2/me", {
|
||||
status: 401
|
||||
});
|
||||
|
||||
@@ -194,7 +194,7 @@ describe("auth actions", () => {
|
||||
});
|
||||
|
||||
it("should dispatch logout failure", () => {
|
||||
fetchMock.deleteOnce("/api/rest/v2/auth/access_token", {
|
||||
fetchMock.deleteOnce("/api/v2/auth/access_token", {
|
||||
status: 500
|
||||
});
|
||||
|
||||
|
||||
@@ -13,6 +13,20 @@ function extractIdentifierFromFailure(action: Action) {
|
||||
return identifier;
|
||||
}
|
||||
|
||||
function removeAllEntriesOfIdentifierFromState(
|
||||
state: Object,
|
||||
payload: any,
|
||||
identifier: string
|
||||
) {
|
||||
const newState = {};
|
||||
for (let failureType in state) {
|
||||
if (failureType !== identifier && !failureType.startsWith(identifier)) {
|
||||
newState[failureType] = state[failureType];
|
||||
}
|
||||
}
|
||||
return newState;
|
||||
}
|
||||
|
||||
function removeFromState(state: Object, identifier: string) {
|
||||
const newState = {};
|
||||
for (let failureType in state) {
|
||||
@@ -47,7 +61,9 @@ export default function reducer(
|
||||
if (action.itemId) {
|
||||
identifier += "/" + action.itemId;
|
||||
}
|
||||
return removeFromState(state, identifier);
|
||||
if (action.payload)
|
||||
return removeAllEntriesOfIdentifierFromState(state, action.payload, identifier);
|
||||
else return removeFromState(state, identifier);
|
||||
}
|
||||
}
|
||||
return state;
|
||||
|
||||
@@ -19,6 +19,20 @@ function removeFromState(state: Object, identifier: string) {
|
||||
return newState;
|
||||
}
|
||||
|
||||
function removeAllEntriesOfIdentifierFromState(
|
||||
state: Object,
|
||||
payload: any,
|
||||
identifier: string
|
||||
) {
|
||||
const newState = {};
|
||||
for (let childType in state) {
|
||||
if (childType !== identifier && !childType.startsWith(identifier)) {
|
||||
newState[childType] = state[childType];
|
||||
}
|
||||
}
|
||||
return newState;
|
||||
}
|
||||
|
||||
function extractIdentifierFromPending(action: Action) {
|
||||
const type = action.type;
|
||||
let identifier = type.substring(0, type.length - PENDING_SUFFIX.length);
|
||||
@@ -48,7 +62,10 @@ export default function reducer(
|
||||
if (action.itemId) {
|
||||
identifier += "/" + action.itemId;
|
||||
}
|
||||
return removeFromState(state, identifier);
|
||||
if (action.payload)
|
||||
return removeAllEntriesOfIdentifierFromState(state, action.payload, identifier);
|
||||
else
|
||||
return removeFromState(state, identifier);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
28
scm-ui/src/repos/components/PermissionsNavLink.js
Normal file
28
scm-ui/src/repos/components/PermissionsNavLink.js
Normal file
@@ -0,0 +1,28 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import { NavLink } from "@scm-manager/ui-components";
|
||||
import { translate } from "react-i18next";
|
||||
import type { Repository } from "@scm-manager/ui-types";
|
||||
|
||||
type Props = {
|
||||
permissionUrl: string,
|
||||
t: string => string,
|
||||
repository: Repository
|
||||
};
|
||||
|
||||
class PermissionsNavLink extends React.Component<Props> {
|
||||
hasPermissionsLink = () => {
|
||||
return this.props.repository._links.permissions;
|
||||
};
|
||||
render() {
|
||||
if (!this.hasPermissionsLink()) {
|
||||
return null;
|
||||
}
|
||||
const { permissionUrl, t } = this.props;
|
||||
return (
|
||||
<NavLink to={permissionUrl} label={t("repository-root.permissions")} />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate("repos")(PermissionsNavLink);
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user