merge with 2.0.0-m3

This commit is contained in:
Sebastian Sdorra
2018-10-01 13:19:20 +02:00
70 changed files with 2224 additions and 487 deletions

3
.dockerignore Normal file
View File

@@ -0,0 +1,3 @@
# ignore everything except scm-server.tar.gz
**
!scm-server/target/*.tar.gz

20
Dockerfile Normal file
View 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
View File

@@ -4,14 +4,15 @@
@Library('github.com/cloudogu/ces-build-lib@59d3e94') @Library('github.com/cloudogu/ces-build-lib@59d3e94')
import com.cloudogu.ces.cesbuildlib.* 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 // Change this as when we go back to default - necessary for proper SonarQube analysis
mainBranch = "2.0.0-m3" mainBranch = "2.0.0-m3"
properties([ properties([
// Keep only the last 10 build to preserve space // 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') { timeout(activity: true, time: 20, unit: 'MINUTES') {
@@ -44,6 +45,26 @@ node() { // No specific label
currentBuild.result = 'UNSTABLE' 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 // 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 // Keep this version number in sync with .mvn/maven-wrapper.properties
Maven mvn = new MavenInDocker(this, "3.5.2-jdk-8") 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 // Release starts javadoc, which takes very long, so do only for certain branches
mvn.additionalArgs += ' -DperformRelease' mvn.additionalArgs += ' -DperformRelease'
// JDK8 is more strict, we should fix this before the next release. Right now, this is just not the focus, yet. // 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 " "-Dsonar.pullrequest.bitbucketcloud.repository=scm-manager "
} else { } else {
mvnArgs += " -Dsonar.branch.name=${env.BRANCH_NAME} " 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 // Avoid exception "The main branch must not have a target" on main branch
mvnArgs += " -Dsonar.branch.target=${mainBranch} " mvnArgs += " -Dsonar.branch.target=${mainBranch} "
} }
@@ -98,6 +119,10 @@ void analyzeWith(Maven mvn) {
} }
} }
boolean isMainBranch() {
return mainBranch.equals(env.BRANCH_NAME)
}
boolean waitForQualityGateWebhookToBeCalled() { boolean waitForQualityGateWebhookToBeCalled() {
boolean isQualityGateSucceeded = true boolean isQualityGateSucceeded = true
timeout(time: 2, unit: 'MINUTES') { // Needed when there is no webhook for example 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}"' 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() { String getCommitAuthorEmail() {
def matcher = getCommitAuthorComplete() =~ "<(.*?)>" def matcher = getCommitAuthorComplete() =~ "<(.*?)>"
matcher ? matcher[0][1] : "" matcher ? matcher[0][1] : ""

View 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

View File

@@ -0,0 +1,5 @@
apiVersion: v1
appVersion: "1.0"
description: A Helm chart for SCM-Manager
name: scm-manager
version: 0.1.0

View 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 }}

View 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 -}}

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

View 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 }}

View 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 }}

View 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 -}}

View 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 }}

View 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: {}

View File

@@ -7,9 +7,4 @@ public class NotFoundException extends Exception {
public NotFoundException() { public NotFoundException() {
} }
public NotFoundException(String message) {
super(message);
}
} }

View File

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

View File

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

View File

@@ -274,6 +274,10 @@ User extends BasicPropertiesAware implements Principal, ModelObject, PermissionO
//J+ //J+
} }
public User changePassword(String password){
setPassword(password);
return this;
}
//~--- get methods ---------------------------------------------------------- //~--- get methods ----------------------------------------------------------
/** /**

View File

@@ -38,6 +38,11 @@ package sonia.scm.user;
import sonia.scm.Manager; import sonia.scm.Manager;
import sonia.scm.search.Searchable; 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. * The central class for managing {@link User} objects.
* This class is a singleton and is available via injection. * This class is a singleton and is available via injection.
@@ -68,4 +73,22 @@ public interface UserManager
* @since 1.14 * @since 1.14
*/ */
public String getDefaultType(); 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());
}
} }

View File

@@ -21,7 +21,7 @@ public class VndMediaType {
public static final String PERMISSION = PREFIX + "permission" + SUFFIX; public static final String PERMISSION = PREFIX + "permission" + SUFFIX;
public static final String CHANGESET = PREFIX + "changeset" + SUFFIX; public static final String CHANGESET = PREFIX + "changeset" + SUFFIX;
public static final String CHANGESET_COLLECTION = PREFIX + "changesetCollection" + 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 = PREFIX + "tag" + SUFFIX;
public static final String TAG_COLLECTION = PREFIX + "tagCollection" + SUFFIX; public static final String TAG_COLLECTION = PREFIX + "tagCollection" + SUFFIX;
public static final String BRANCH = PREFIX + "branch" + SUFFIX; public static final String BRANCH = PREFIX + "branch" + SUFFIX;
@@ -35,6 +35,8 @@ public class VndMediaType {
public static final String REPOSITORY_TYPE = PREFIX + "repositoryType" + SUFFIX; public static final String REPOSITORY_TYPE = PREFIX + "repositoryType" + SUFFIX;
public static final String UI_PLUGIN = PREFIX + "uiPlugin" + SUFFIX; public static final String UI_PLUGIN = PREFIX + "uiPlugin" + SUFFIX;
public static final String UI_PLUGIN_COLLECTION = PREFIX + "uiPluginCollection" + 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 ME = PREFIX + "me" + SUFFIX;
public static final String SOURCE = PREFIX + "source" + SUFFIX; public static final String SOURCE = PREFIX + "source" + SUFFIX;

View File

@@ -0,0 +1,79 @@
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();
}
@Test
public void shouldGet403IfUserIsNotAdmin() {
String newUser = "user";
String password = "pass";
String type = "xml";
TestData.createUser(newUser, password, false, type);
ScmRequests.start()
.given()
.url(TestData.getMeUrl())
.usernameAndPassword(newUser, password)
.getMeResource()
.assertStatusCode(403);
}
}

View File

@@ -39,6 +39,8 @@ import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.junit.runners.Parameterized; import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters; 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.PermissionType;
import sonia.scm.repository.client.api.RepositoryClient; import sonia.scm.repository.client.api.RepositoryClient;
import sonia.scm.repository.client.api.RepositoryClientException; 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.assertEquals;
import static org.junit.Assert.assertNull; import static org.junit.Assert.assertNull;
import static sonia.scm.it.RepositoryUtil.addAndCommitRandomFile; import static sonia.scm.it.utils.RepositoryUtil.addAndCommitRandomFile;
import static sonia.scm.it.RestUtil.given; import static sonia.scm.it.utils.RestUtil.given;
import static sonia.scm.it.ScmTypes.availableScmTypes; import static sonia.scm.it.utils.ScmTypes.availableScmTypes;
import static sonia.scm.it.TestData.USER_SCM_ADMIN; import static sonia.scm.it.utils.TestData.USER_SCM_ADMIN;
import static sonia.scm.it.TestData.callRepository; import static sonia.scm.it.utils.TestData.callRepository;
@RunWith(Parameterized.class) @RunWith(Parameterized.class)
public class PermissionsITCase { public class PermissionsITCase {

View File

@@ -42,6 +42,8 @@ import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.junit.runners.Parameterized; import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters; 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.repository.client.api.RepositoryClient;
import sonia.scm.web.VndMediaType; import sonia.scm.web.VndMediaType;
@@ -53,11 +55,11 @@ import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.nullValue; import static org.hamcrest.Matchers.nullValue;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static sonia.scm.it.RegExMatcher.matchesPattern; import static sonia.scm.it.utils.RegExMatcher.matchesPattern;
import static sonia.scm.it.RestUtil.createResourceUrl; import static sonia.scm.it.utils.RestUtil.createResourceUrl;
import static sonia.scm.it.RestUtil.given; import static sonia.scm.it.utils.RestUtil.given;
import static sonia.scm.it.ScmTypes.availableScmTypes; import static sonia.scm.it.utils.ScmTypes.availableScmTypes;
import static sonia.scm.it.TestData.repositoryJson; import static sonia.scm.it.utils.TestData.repositoryJson;
@RunWith(Parameterized.class) @RunWith(Parameterized.class)
public class RepositoriesITCase { public class RepositoriesITCase {

View File

@@ -4,7 +4,6 @@ import io.restassured.response.ExtractableResponse;
import io.restassured.response.Response; import io.restassured.response.Response;
import org.apache.http.HttpStatus; import org.apache.http.HttpStatus;
import org.assertj.core.util.Lists; import org.assertj.core.util.Lists;
import org.assertj.core.util.Maps;
import org.junit.Assume; import org.junit.Assume;
import org.junit.Before; import org.junit.Before;
import org.junit.Rule; import org.junit.Rule;
@@ -12,6 +11,9 @@ import org.junit.Test;
import org.junit.rules.TemporaryFolder; import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.junit.runners.Parameterized; 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.Changeset;
import sonia.scm.repository.client.api.ClientCommand; import sonia.scm.repository.client.api.ClientCommand;
import sonia.scm.repository.client.api.RepositoryClient; 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.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotNull;
import static sonia.scm.it.RestUtil.ADMIN_PASSWORD; import static sonia.scm.it.utils.RestUtil.ADMIN_PASSWORD;
import static sonia.scm.it.RestUtil.ADMIN_USERNAME; import static sonia.scm.it.utils.RestUtil.ADMIN_USERNAME;
import static sonia.scm.it.RestUtil.given; import static sonia.scm.it.utils.RestUtil.given;
import static sonia.scm.it.ScmTypes.availableScmTypes; import static sonia.scm.it.utils.ScmTypes.availableScmTypes;
@RunWith(Parameterized.class) @RunWith(Parameterized.class)
public class RepositoryAccessITCase { public class RepositoryAccessITCase {
@@ -42,7 +44,7 @@ public class RepositoryAccessITCase {
private final String repositoryType; private final String repositoryType;
private File folder; private File folder;
private RepositoryRequests.AppliedRepositoryGetRequest repositoryGetRequest; private ScmRequests.AppliedRepositoryRequest repositoryGetRequest;
public RepositoryAccessITCase(String repositoryType) { public RepositoryAccessITCase(String repositoryType) {
this.repositoryType = repositoryType; this.repositoryType = repositoryType;
@@ -57,12 +59,17 @@ public class RepositoryAccessITCase {
public void init() { public void init() {
TestData.createDefault(); TestData.createDefault();
folder = tempFolder.getRoot(); folder = tempFolder.getRoot();
repositoryGetRequest = RepositoryRequests.start() repositoryGetRequest = ScmRequests.start()
.given() .given()
.url(TestData.getDefaultRepositoryUrl(repositoryType)) .url(TestData.getDefaultRepositoryUrl(repositoryType))
.usernameAndPassword(ADMIN_USERNAME, ADMIN_PASSWORD) .usernameAndPassword(ADMIN_USERNAME, ADMIN_PASSWORD)
.get() .getRepositoryResource()
.assertStatusCode(HttpStatus.SC_OK); .assertStatusCode(HttpStatus.SC_OK);
ScmRequests.AppliedMeRequest meGetRequest = ScmRequests.start()
.given()
.url(TestData.getMeUrl())
.usernameAndPassword(ADMIN_USERNAME, ADMIN_PASSWORD)
.getMeResource();
} }
@Test @Test
@@ -165,7 +172,7 @@ public class RepositoryAccessITCase {
.isNotNull() .isNotNull()
.contains(String.format("%s/sources/%s", repositoryUrl, changeset.getId())); .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") .as("assert single tag changesets link")
.isNotNull() .isNotNull()
.contains(String.format("%s/changesets/%s", repositoryUrl, changeset.getId())); .contains(String.format("%s/changesets/%s", repositoryUrl, changeset.getId()));

View File

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

View File

@@ -0,0 +1,113 @@
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();
}
@Test
public void shouldGet403IfUserIsNotAdmin() {
String newUser = "user";
String password = "pass";
String type = "xml";
TestData.createUser(newUser, password, false, type);
ScmRequests.start()
.given()
.url(TestData.getMeUrl())
.usernameAndPassword(newUser, password)
.getUserResource()
.assertStatusCode(403);
}
}

View File

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

View File

@@ -1,4 +1,4 @@
package sonia.scm.it; package sonia.scm.it.utils;
import org.hamcrest.BaseMatcher; import org.hamcrest.BaseMatcher;
import org.hamcrest.Description; import org.hamcrest.Description;
@@ -6,7 +6,7 @@ import org.hamcrest.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
class RegExMatcher extends BaseMatcher<String> { public class RegExMatcher extends BaseMatcher<String> {
public static Matcher<String> matchesPattern(String pattern) { public static Matcher<String> matchesPattern(String pattern) {
return new RegExMatcher(pattern); return new RegExMatcher(pattern);
} }

View File

@@ -1,4 +1,4 @@
package sonia.scm.it; package sonia.scm.it.utils;
import com.google.common.base.Charsets; import com.google.common.base.Charsets;
import com.google.common.io.Files; import com.google.common.io.Files;
@@ -24,11 +24,11 @@ public class RepositoryUtil {
private static final RepositoryClientFactory REPOSITORY_CLIENT_FACTORY = new RepositoryClientFactory(); 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"); 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) String httpProtocolUrl = TestData.callRepository(username, password, repositoryType, HttpStatus.SC_OK)
.extract() .extract()
.path("_links.protocol.find{it.name=='http'}.href"); .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); 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 uuid = UUID.randomUUID().toString();
String name = "file-" + uuid + ".uuid"; String name = "file-" + uuid + ".uuid";
createAndCommitFile(client, username, name, uuid); createAndCommitFile(client, username, name, uuid);
return name; 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); writeAndAddFile(repositoryClient, fileName, content);
return commit(repositoryClient, username, "added " + fileName); return commit(repositoryClient, username, "added " + fileName);
} }
@@ -59,7 +59,7 @@ public class RepositoryUtil {
* @return the changeset with all modifications * @return the changeset with all modifications
* @throws IOException * @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()) { for (String fileName : addedFiles.keySet()) {
writeAndAddFile(repositoryClient, fileName, addedFiles.get(fileName)); writeAndAddFile(repositoryClient, fileName, addedFiles.get(fileName));
} }
@@ -80,7 +80,7 @@ public class RepositoryUtil {
return file; 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); deleteFileAndApplyRemoveCommand(repositoryClient, fileName);
return commit(repositoryClient, username, "removed " + fileName); return commit(repositoryClient, username, "removed " + fileName);
} }
@@ -115,7 +115,7 @@ public class RepositoryUtil {
return changeset; 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)) { if (repositoryClient.isCommandSupported(ClientCommand.TAG)) {
Tag tag = repositoryClient.getTagCommand().setRevision(revision).tag(tagName, TestData.USER_SCM_ADMIN); Tag tag = repositoryClient.getTagCommand().setRevision(revision).tag(tagName, TestData.USER_SCM_ADMIN);
if (repositoryClient.isCommandSupported(ClientCommand.PUSH)) { if (repositoryClient.isCommandSupported(ClientCommand.PUSH)) {

View File

@@ -1,4 +1,4 @@
package sonia.scm.it; package sonia.scm.it.utils;
import io.restassured.RestAssured; import io.restassured.RestAssured;
import io.restassured.specification.RequestSpecification; import io.restassured.specification.RequestSpecification;

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

View File

@@ -1,12 +1,12 @@
package sonia.scm.it; package sonia.scm.it.utils;
import sonia.scm.util.IOUtil; import sonia.scm.util.IOUtil;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
class ScmTypes { public class ScmTypes {
static Collection<String> availableScmTypes() { public static Collection<String> availableScmTypes() {
Collection<String> params = new ArrayList<>(); Collection<String> params = new ArrayList<>();
params.add("git"); params.add("git");

View File

@@ -1,4 +1,4 @@
package sonia.scm.it; package sonia.scm.it.utils;
import io.restassured.response.ValidatableResponse; import io.restassured.response.ValidatableResponse;
import org.apache.http.HttpStatus; import org.apache.http.HttpStatus;
@@ -8,14 +8,16 @@ import sonia.scm.repository.PermissionType;
import sonia.scm.web.VndMediaType; import sonia.scm.web.VndMediaType;
import javax.json.Json; import javax.json.Json;
import javax.json.JsonObjectBuilder;
import java.net.URI;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import static java.util.Arrays.asList; import static java.util.Arrays.asList;
import static sonia.scm.it.RestUtil.createResourceUrl; import static sonia.scm.it.utils.RestUtil.createResourceUrl;
import static sonia.scm.it.RestUtil.given; import static sonia.scm.it.utils.RestUtil.given;
import static sonia.scm.it.ScmTypes.availableScmTypes; import static sonia.scm.it.utils.ScmTypes.availableScmTypes;
public class TestData { 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 final List<String> PROTECTED_USERS = asList(USER_SCM_ADMIN, USER_ANONYMOUS);
private static Map<String, String> DEFAULT_REPOSITORIES = new HashMap<>(); private static Map<String, String> DEFAULT_REPOSITORIES = new HashMap<>();
public static final JsonObjectBuilder JSON_BUILDER = NullAwareJsonObjectBuilder.wrap(Json.createObjectBuilder());
public static void createDefault() { public static void createDefault() {
cleanup(); cleanup();
@@ -44,27 +47,31 @@ public class TestData {
} }
public static void createUser(String username, String password) { 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); LOG.info("create user with username: {}", username);
String admin = isAdmin ? "true" : "false";
given(VndMediaType.USER) given(VndMediaType.USER)
.when() .when()
.content(" {\n" + .content(new StringBuilder()
" \"active\": true,\n" + .append(" {\n")
" \"admin\": false,\n" + .append(" \"active\": true,\n")
" \"creationDate\": \"2018-08-21T12:26:46.084Z\",\n" + .append(" \"admin\": ").append(admin).append(",\n")
" \"displayName\": \"" + username + "\",\n" + .append(" \"creationDate\": \"2018-08-21T12:26:46.084Z\",\n")
" \"mail\": \"user1@scm-manager.org\",\n" + .append(" \"displayName\": \"").append(username).append("\",\n")
" \"name\": \"" + username + "\",\n" + .append(" \"mail\": \"user1@scm-manager.org\",\n")
" \"password\": \"" + password + "\",\n" + .append(" \"name\": \"").append(username).append("\",\n")
" \"type\": \"xml\"\n" + .append(" \"password\": \"").append(password).append("\",\n")
" \n" + .append(" \"type\": \"").append(type).append("\"\n")
" }") .append(" }").toString())
.post(createResourceUrl("users")) .post(getUsersUrl())
.then() .then()
.statusCode(HttpStatus.SC_CREATED) .statusCode(HttpStatus.SC_CREATED)
; ;
} }
public static void createUserPermission(String name, PermissionType permissionType, String repositoryType) { public static void createUserPermission(String name, PermissionType permissionType, String repositoryType) {
String defaultPermissionUrl = TestData.getDefaultPermissionUrl(USER_SCM_ADMIN, USER_SCM_ADMIN, 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); 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) { public static String repositoryJson(String repositoryType) {
return Json.createObjectBuilder() return JSON_BUILDER
.add("contact", "zaphod.beeblebrox@hitchhiker.com") .add("contact", "zaphod.beeblebrox@hitchhiker.com")
.add("description", "Heart of Gold") .add("description", "Heart of Gold")
.add("name", "HeartOfGold-" + repositoryType) .add("name", "HeartOfGold-" + repositoryType)
@@ -192,6 +199,29 @@ public class TestData {
.build().toString(); .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) { public static void main(String[] args) {
cleanup(); cleanup();
} }

View File

@@ -1,5 +1,6 @@
//@flow //@flow
import React from "react"; import React from "react";
import { repositories } from "@scm-manager/ui-components";
import type { Repository } from "@scm-manager/ui-types"; import type { Repository } from "@scm-manager/ui-types";
type Props = { type Props = {
@@ -10,14 +11,16 @@ class ProtocolInformation extends React.Component<Props> {
render() { render() {
const { repository } = this.props; const { repository } = this.props;
if (!repository._links.httpProtocol) { const href = repositories.getProtocolLinkByType(repository, "http");
if (!href) {
return null; return null;
} }
return ( return (
<div> <div>
<h4>Clone the repository</h4> <h4>Clone the repository</h4>
<pre> <pre>
<code>git clone {repository._links.httpProtocol.href}</code> <code>git clone {href}</code>
</pre> </pre>
<h4>Create a new repository</h4> <h4>Create a new repository</h4>
<pre> <pre>
@@ -30,7 +33,7 @@ class ProtocolInformation extends React.Component<Props> {
<br /> <br />
git commit -m "added readme" git commit -m "added readme"
<br /> <br />
git remote add origin {repository._links.httpProtocol.href} git remote add origin {href}
<br /> <br />
git push -u origin master git push -u origin master
<br /> <br />
@@ -39,7 +42,7 @@ class ProtocolInformation extends React.Component<Props> {
<h4>Push an existing repository</h4> <h4>Push an existing repository</h4>
<pre> <pre>
<code> <code>
git remote add origin {repository._links.httpProtocol.href} git remote add origin {href}
<br /> <br />
git push -u origin master git push -u origin master
<br /> <br />

View File

@@ -1,5 +1,6 @@
//@flow //@flow
import React from "react"; import React from "react";
import { repositories } from "@scm-manager/ui-components";
import type { Repository } from "@scm-manager/ui-types"; import type { Repository } from "@scm-manager/ui-types";
type Props = { type Props = {
@@ -10,14 +11,15 @@ class ProtocolInformation extends React.Component<Props> {
render() { render() {
const { repository } = this.props; const { repository } = this.props;
if (!repository._links.httpProtocol) { const href = repositories.getProtocolLinkByType(repository, "http");
if (!href) {
return null; return null;
} }
return ( return (
<div> <div>
<h4>Clone the repository</h4> <h4>Clone the repository</h4>
<pre> <pre>
<code>hg clone {repository._links.httpProtocol.href}</code> <code>hg clone {href}</code>
</pre> </pre>
<h4>Create a new repository</h4> <h4>Create a new repository</h4>
<pre> <pre>
@@ -26,7 +28,7 @@ class ProtocolInformation extends React.Component<Props> {
<br /> <br />
echo "[paths]" > .hg/hgrc echo "[paths]" > .hg/hgrc
<br /> <br />
echo "default = {repository._links.httpProtocol.href}" > .hg/hgrc echo "default = {href}" > .hg/hgrc
<br /> <br />
echo "# {repository.name}" > README.md echo "# {repository.name}" > README.md
<br /> <br />
@@ -44,7 +46,7 @@ class ProtocolInformation extends React.Component<Props> {
<code> <code>
# add the repository url as default to your .hg/hgrc e.g: # add the repository url as default to your .hg/hgrc e.g:
<br /> <br />
default = {repository._links.httpProtocol.href} default = {href}
<br /> <br />
# push to remote repository # push to remote repository
<br /> <br />

View File

@@ -1,5 +1,6 @@
//@flow //@flow
import React from "react"; import React from "react";
import { repositories } from "@scm-manager/ui-components";
import type { Repository } from "@scm-manager/ui-types"; import type { Repository } from "@scm-manager/ui-types";
type Props = { type Props = {
@@ -10,14 +11,15 @@ class ProtocolInformation extends React.Component<Props> {
render() { render() {
const { repository } = this.props; const { repository } = this.props;
if (!repository._links.httpProtocol) { const href = repositories.getProtocolLinkByType(repository, "http");
if (!href) {
return null; return null;
} }
return ( return (
<div> <div>
<h4>Checkout the repository</h4> <h4>Checkout the repository</h4>
<pre> <pre>
<code>svn checkout {repository._links.httpProtocol.href}</code> <code>svn checkout {href}</code>
</pre> </pre>
</div> </div>
); );

View File

@@ -2,8 +2,9 @@
import * as validation from "./validation.js"; import * as validation from "./validation.js";
import * as urls from "./urls"; 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 DateFromNow } from "./DateFromNow.js";
export { default as ErrorNotification } from "./ErrorNotification.js"; export { default as ErrorNotification } from "./ErrorNotification.js";
@@ -18,6 +19,8 @@ export { default as ProtectedRoute } from "./ProtectedRoute.js";
export { apiClient, NOT_FOUND_ERROR, UNAUTHORIZED_ERROR } from "./apiclient.js"; export { apiClient, NOT_FOUND_ERROR, UNAUTHORIZED_ERROR } from "./apiclient.js";
export * from "./buttons"; export * from "./buttons";
export * from "./forms"; export * from "./forms";
export * from "./layout"; export * from "./layout";

View 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;
}

View File

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

View File

@@ -1,9 +1,10 @@
// @flow // @flow
export type Link = { export type Link = {
href: string href: string,
name?: string
}; };
export type Links = { [string]: Link }; export type Links = { [string]: Link | Link[] };
export type Collection = { export type Collection = {
_embedded: Object, _embedded: Object,

View File

@@ -56,7 +56,7 @@ const userZaphod = {
displayName: "Z. Beeblebrox", displayName: "Z. Beeblebrox",
mail: "president@heartofgold.universe", mail: "president@heartofgold.universe",
name: "zaphod", name: "zaphod",
password: "__dummypassword__", password: "",
type: "xml", type: "xml",
properties: {}, properties: {},
_links: { _links: {
@@ -79,7 +79,7 @@ const userFord = {
displayName: "F. Prefect", displayName: "F. Prefect",
mail: "ford@prefect.universe", mail: "ford@prefect.universe",
name: "ford", name: "ford",
password: "__dummypassword__", password: "",
type: "xml", type: "xml",
properties: {}, properties: {},
_links: { _links: {

View File

@@ -0,0 +1,26 @@
package sonia.scm.api.v2.resources;
import sonia.scm.PageResult;
import sonia.scm.repository.Changeset;
import sonia.scm.repository.Repository;
import javax.inject.Inject;
public class BranchChangesetCollectionToDtoMapper extends ChangesetCollectionToDtoMapperBase {
private final ResourceLinks resourceLinks;
@Inject
public BranchChangesetCollectionToDtoMapper(ChangesetToChangesetDtoMapper changesetToChangesetDtoMapper, ResourceLinks resourceLinks) {
super(changesetToChangesetDtoMapper);
this.resourceLinks = resourceLinks;
}
public CollectionDto map(int pageNumber, int pageSize, PageResult<Changeset> pageResult, Repository repository, String branch) {
return this.map(pageNumber, pageSize, pageResult, repository, () -> createSelfLink(repository, branch));
}
private String createSelfLink(Repository repository, String branch) {
return resourceLinks.branch().history(repository.getNamespaceAndName(), branch);
}
}

View File

@@ -3,6 +3,7 @@ package sonia.scm.api.v2.resources;
import com.webcohesion.enunciate.metadata.rs.ResponseCode; import com.webcohesion.enunciate.metadata.rs.ResponseCode;
import com.webcohesion.enunciate.metadata.rs.StatusCodes; import com.webcohesion.enunciate.metadata.rs.StatusCodes;
import com.webcohesion.enunciate.metadata.rs.TypeHint; import com.webcohesion.enunciate.metadata.rs.TypeHint;
import sonia.scm.NotFoundException;
import sonia.scm.PageResult; import sonia.scm.PageResult;
import sonia.scm.repository.Branches; import sonia.scm.repository.Branches;
import sonia.scm.repository.Changeset; import sonia.scm.repository.Changeset;
@@ -32,14 +33,14 @@ public class BranchRootResource {
private final BranchToBranchDtoMapper branchToDtoMapper; private final BranchToBranchDtoMapper branchToDtoMapper;
private final BranchCollectionToDtoMapper branchCollectionToDtoMapper; private final BranchCollectionToDtoMapper branchCollectionToDtoMapper;
private final ChangesetCollectionToDtoMapper changesetCollectionToDtoMapper; private final BranchChangesetCollectionToDtoMapper branchChangesetCollectionToDtoMapper;
@Inject @Inject
public BranchRootResource(RepositoryServiceFactory serviceFactory, BranchToBranchDtoMapper branchToDtoMapper, BranchCollectionToDtoMapper branchCollectionToDtoMapper, ChangesetCollectionToDtoMapper changesetCollectionToDtoMapper) { public BranchRootResource(RepositoryServiceFactory serviceFactory, BranchToBranchDtoMapper branchToDtoMapper, BranchCollectionToDtoMapper branchCollectionToDtoMapper, BranchChangesetCollectionToDtoMapper changesetCollectionToDtoMapper) {
this.serviceFactory = serviceFactory; this.serviceFactory = serviceFactory;
this.branchToDtoMapper = branchToDtoMapper; this.branchToDtoMapper = branchToDtoMapper;
this.branchCollectionToDtoMapper = branchCollectionToDtoMapper; this.branchCollectionToDtoMapper = branchCollectionToDtoMapper;
this.changesetCollectionToDtoMapper = changesetCollectionToDtoMapper; this.branchChangesetCollectionToDtoMapper = changesetCollectionToDtoMapper;
} }
/** /**
@@ -98,6 +99,14 @@ public class BranchRootResource {
@DefaultValue("0") @QueryParam("page") int page, @DefaultValue("0") @QueryParam("page") int page,
@DefaultValue("10") @QueryParam("pageSize") int pageSize) throws Exception { @DefaultValue("10") @QueryParam("pageSize") int pageSize) throws Exception {
try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) { try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) {
boolean branchExists = repositoryService.getBranchesCommand()
.getBranches()
.getBranches()
.stream()
.anyMatch(branch -> branchName.equals(branch.getName()));
if (!branchExists){
throw new NotFoundException("branch", branchName);
}
Repository repository = repositoryService.getRepository(); Repository repository = repositoryService.getRepository();
RepositoryPermissions.read(repository).check(); RepositoryPermissions.read(repository).check();
ChangesetPagingResult changesets = new PagedLogCommandBuilder(repositoryService) ChangesetPagingResult changesets = new PagedLogCommandBuilder(repositoryService)
@@ -108,7 +117,7 @@ public class BranchRootResource {
.getChangesets(); .getChangesets();
if (changesets != null && changesets.getChangesets() != null) { if (changesets != null && changesets.getChangesets() != null) {
PageResult<Changeset> pageResult = new PageResult<>(changesets.getChangesets(), changesets.getTotal()); PageResult<Changeset> pageResult = new PageResult<>(changesets.getChangesets(), changesets.getTotal());
return Response.ok(changesetCollectionToDtoMapper.map(page, pageSize, pageResult, repository)).build(); return Response.ok(branchChangesetCollectionToDtoMapper.map(page, pageSize, pageResult, repository, branchName)).build();
} else { } else {
return Response.ok().build(); return Response.ok().build();
} }

View File

@@ -0,0 +1,17 @@
package sonia.scm.api.v2.resources;
import sonia.scm.user.ChangePasswordNotAllowedException;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;
@Provider
public class ChangePasswordNotAllowedExceptionMapper implements ExceptionMapper<ChangePasswordNotAllowedException> {
@Override
public Response toResponse(ChangePasswordNotAllowedException exception) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(exception.getMessage())
.build();
}
}

View File

@@ -5,31 +5,22 @@ import sonia.scm.repository.Changeset;
import sonia.scm.repository.Repository; import sonia.scm.repository.Repository;
import javax.inject.Inject; import javax.inject.Inject;
import java.util.Optional;
import java.util.function.Supplier;
public class ChangesetCollectionToDtoMapper extends PagedCollectionToDtoMapper<Changeset, ChangesetDto> { public class ChangesetCollectionToDtoMapper extends ChangesetCollectionToDtoMapperBase {
private final ChangesetToChangesetDtoMapper changesetToChangesetDtoMapper; private final ResourceLinks resourceLinks;
protected final ResourceLinks resourceLinks;
@Inject @Inject
public ChangesetCollectionToDtoMapper(ChangesetToChangesetDtoMapper changesetToChangesetDtoMapper, ResourceLinks resourceLinks) { public ChangesetCollectionToDtoMapper(ChangesetToChangesetDtoMapper changesetToChangesetDtoMapper, ResourceLinks resourceLinks) {
super("changesets"); super(changesetToChangesetDtoMapper);
this.changesetToChangesetDtoMapper = changesetToChangesetDtoMapper;
this.resourceLinks = resourceLinks; this.resourceLinks = resourceLinks;
} }
public CollectionDto map(int pageNumber, int pageSize, PageResult<Changeset> pageResult, Repository repository) { public CollectionDto map(int pageNumber, int pageSize, PageResult<Changeset> pageResult, Repository repository) {
return this.map(pageNumber, pageSize, pageResult, repository, () -> createSelfLink(repository)); return super.map(pageNumber, pageSize, pageResult, repository, () -> createSelfLink(repository));
} }
public CollectionDto map(int pageNumber, int pageSize, PageResult<Changeset> pageResult, Repository repository, Supplier<String> selfLinkSupplier) { private String createSelfLink(Repository repository) {
return super.map(pageNumber, pageSize, pageResult, selfLinkSupplier.get(), Optional.empty(), changeset -> changesetToChangesetDtoMapper.map(changeset, repository));
}
protected String createSelfLink(Repository repository) {
return resourceLinks.changeset().all(repository.getNamespace(), repository.getName()); return resourceLinks.changeset().all(repository.getNamespace(), repository.getName());
} }
} }

View File

@@ -0,0 +1,23 @@
package sonia.scm.api.v2.resources;
import sonia.scm.PageResult;
import sonia.scm.repository.Changeset;
import sonia.scm.repository.Repository;
import java.util.Optional;
import java.util.function.Supplier;
class ChangesetCollectionToDtoMapperBase extends PagedCollectionToDtoMapper<Changeset, ChangesetDto> {
private final ChangesetToChangesetDtoMapper changesetToChangesetDtoMapper;
ChangesetCollectionToDtoMapperBase(ChangesetToChangesetDtoMapper changesetToChangesetDtoMapper) {
super("changesets");
this.changesetToChangesetDtoMapper = changesetToChangesetDtoMapper;
}
CollectionDto map(int pageNumber, int pageSize, PageResult<Changeset> pageResult, Repository repository, Supplier<String> selfLinkSupplier) {
return super.map(pageNumber, pageSize, pageResult, selfLinkSupplier.get(), Optional.empty(), changeset -> changesetToChangesetDtoMapper.map(changeset, repository));
}
}

View File

@@ -6,19 +6,22 @@ import sonia.scm.repository.Repository;
import javax.inject.Inject; import javax.inject.Inject;
public class FileHistoryCollectionToDtoMapper extends ChangesetCollectionToDtoMapper { public class FileHistoryCollectionToDtoMapper extends ChangesetCollectionToDtoMapperBase {
private final ResourceLinks resourceLinks;
@Inject @Inject
public FileHistoryCollectionToDtoMapper(ChangesetToChangesetDtoMapper changesetToChangesetDtoMapper, ResourceLinks resourceLinks) { public FileHistoryCollectionToDtoMapper(ChangesetToChangesetDtoMapper changesetToChangesetDtoMapper, ResourceLinks resourceLinks) {
super(changesetToChangesetDtoMapper, resourceLinks); super(changesetToChangesetDtoMapper);
this.resourceLinks = resourceLinks;
} }
public CollectionDto map(int pageNumber, int pageSize, PageResult<Changeset> pageResult, Repository repository, String revision, String path) { public CollectionDto map(int pageNumber, int pageSize, PageResult<Changeset> pageResult, Repository repository, String revision, String path) {
return super.map(pageNumber, pageSize, pageResult, repository, () -> createSelfLink(repository, revision, path)); return super.map(pageNumber, pageSize, pageResult, repository, () -> createSelfLink(repository, revision, path));
} }
protected String createSelfLink(Repository repository, String revision, String path) { private String createSelfLink(Repository repository, String revision, String path) {
return super.resourceLinks.fileHistory().self(repository.getNamespace(), repository.getName(), revision, path); return resourceLinks.fileHistory().self(repository.getNamespace(), repository.getName(), revision, path);
} }
} }

View File

@@ -10,6 +10,7 @@ import sonia.scm.PageResult;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import java.util.Optional; import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Function; import java.util.function.Function;
import java.util.function.Predicate; import java.util.function.Predicate;
import java.util.function.Supplier; import java.util.function.Supplier;
@@ -37,6 +38,15 @@ class IdResourceManagerAdapter<MODEL_OBJECT extends ModelObject,
return singleAdapter.get(loadBy(id), mapToDto); return singleAdapter.get(loadBy(id), mapToDto);
} }
public Response update(String id, Function<MODEL_OBJECT, MODEL_OBJECT> applyChanges, Consumer<MODEL_OBJECT> checker) throws NotFoundException, ConcurrentModificationException {
return singleAdapter.update(
loadBy(id),
applyChanges,
idStaysTheSame(id),
checker
);
}
public Response update(String id, Function<MODEL_OBJECT, MODEL_OBJECT> applyChanges) throws NotFoundException, ConcurrentModificationException { public Response update(String id, Function<MODEL_OBJECT, MODEL_OBJECT> applyChanges) throws NotFoundException, ConcurrentModificationException {
return singleAdapter.update( return singleAdapter.update(
loadBy(id), loadBy(id),

View File

@@ -0,0 +1,17 @@
package sonia.scm.api.v2.resources;
import sonia.scm.user.InvalidPasswordException;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;
@Provider
public class InvalidPasswordExceptionMapper implements ExceptionMapper<InvalidPasswordException> {
@Override
public Response toResponse(InvalidPasswordException exception) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(exception.getMessage())
.build();
}
}

View File

@@ -8,6 +8,7 @@ public class MapperModule extends AbstractModule {
@Override @Override
protected void configure() { protected void configure() {
bind(UserDtoToUserMapper.class).to(Mappers.getMapper(UserDtoToUserMapper.class).getClass()); bind(UserDtoToUserMapper.class).to(Mappers.getMapper(UserDtoToUserMapper.class).getClass());
bind(MeToUserDtoMapper.class).to(Mappers.getMapper(MeToUserDtoMapper.class).getClass());
bind(UserToUserDtoMapper.class).to(Mappers.getMapper(UserToUserDtoMapper.class).getClass()); bind(UserToUserDtoMapper.class).to(Mappers.getMapper(UserToUserDtoMapper.class).getClass());
bind(UserCollectionToDtoMapper.class); bind(UserCollectionToDtoMapper.class);

View File

@@ -4,19 +4,27 @@ import com.webcohesion.enunciate.metadata.rs.ResponseCode;
import com.webcohesion.enunciate.metadata.rs.StatusCodes; import com.webcohesion.enunciate.metadata.rs.StatusCodes;
import com.webcohesion.enunciate.metadata.rs.TypeHint; import com.webcohesion.enunciate.metadata.rs.TypeHint;
import org.apache.shiro.SecurityUtils; import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.credential.PasswordService;
import sonia.scm.ConcurrentModificationException;
import sonia.scm.NotFoundException; import sonia.scm.NotFoundException;
import sonia.scm.user.InvalidPasswordException;
import sonia.scm.user.User; import sonia.scm.user.User;
import sonia.scm.user.UserManager; import sonia.scm.user.UserManager;
import sonia.scm.web.VndMediaType; import sonia.scm.web.VndMediaType;
import javax.inject.Inject; import javax.inject.Inject;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET; import javax.ws.rs.GET;
import javax.ws.rs.PUT;
import javax.ws.rs.Path; import javax.ws.rs.Path;
import javax.ws.rs.Produces; import javax.ws.rs.Produces;
import javax.ws.rs.core.Context; import javax.ws.rs.core.Context;
import javax.ws.rs.core.Request; import javax.ws.rs.core.Request;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo; import javax.ws.rs.core.UriInfo;
import java.util.function.Consumer;
import static sonia.scm.user.InvalidPasswordException.INVALID_MATCHING;
/** /**
@@ -24,15 +32,20 @@ import javax.ws.rs.core.UriInfo;
*/ */
@Path(MeResource.ME_PATH_V2) @Path(MeResource.ME_PATH_V2)
public class MeResource { public class MeResource {
static final String ME_PATH_V2 = "v2/me/"; public static final String ME_PATH_V2 = "v2/me/";
private final UserToUserDtoMapper userToDtoMapper; private final MeToUserDtoMapper meToUserDtoMapper;
private final IdResourceManagerAdapter<User, UserDto> adapter; private final IdResourceManagerAdapter<User, UserDto> adapter;
private final PasswordService passwordService;
private final UserManager userManager;
@Inject @Inject
public MeResource(UserToUserDtoMapper userToDtoMapper, UserManager manager) { public MeResource(MeToUserDtoMapper meToUserDtoMapper, UserManager manager, PasswordService passwordService) {
this.userToDtoMapper = userToDtoMapper; this.meToUserDtoMapper = meToUserDtoMapper;
this.adapter = new IdResourceManagerAdapter<>(manager, User.class); this.adapter = new IdResourceManagerAdapter<>(manager, User.class);
this.passwordService = passwordService;
this.userManager = manager;
} }
/** /**
@@ -50,6 +63,34 @@ public class MeResource {
public Response get(@Context Request request, @Context UriInfo uriInfo) throws NotFoundException { public Response get(@Context Request request, @Context UriInfo uriInfo) throws NotFoundException {
String id = (String) SecurityUtils.getSubject().getPrincipals().getPrimaryPrincipal(); String id = (String) SecurityUtils.getSubject().getPrincipals().getPrimaryPrincipal();
return adapter.get(id, userToDtoMapper::map); return adapter.get(id, meToUserDtoMapper::map);
}
/**
* Change password of the current user
*/
@PUT
@Path("password")
@StatusCodes({
@ResponseCode(code = 204, condition = "update success"),
@ResponseCode(code = 401, condition = "not authenticated / invalid credentials"),
@ResponseCode(code = 500, condition = "internal server error")
})
@TypeHint(TypeHint.NO_CONTENT.class)
@Consumes(VndMediaType.PASSWORD_CHANGE)
public Response changePassword(PasswordChangeDto passwordChangeDto) throws NotFoundException, ConcurrentModificationException {
String name = (String) SecurityUtils.getSubject().getPrincipals().getPrimaryPrincipal();
return adapter.update(name, user -> user.changePassword(passwordService.encryptPassword(passwordChangeDto.getNewPassword())), userManager.getUserTypeChecker().andThen(getOldOriginalPasswordChecker(passwordChangeDto.getOldPassword())));
}
/**
* Match given old password from the dto with the stored password before updating
*/
private Consumer<User> getOldOriginalPasswordChecker(String oldPassword) {
return user -> {
if (!user.getPassword().equals(passwordService.encryptPassword(oldPassword))) {
throw new InvalidPasswordException(INVALID_MATCHING);
}
};
} }
} }

View File

@@ -0,0 +1,42 @@
package sonia.scm.api.v2.resources;
import de.otto.edison.hal.Links;
import org.mapstruct.AfterMapping;
import org.mapstruct.Mapper;
import org.mapstruct.MappingTarget;
import sonia.scm.user.User;
import sonia.scm.user.UserManager;
import sonia.scm.user.UserPermissions;
import javax.inject.Inject;
import static de.otto.edison.hal.Link.link;
import static de.otto.edison.hal.Links.linkingTo;
@Mapper
public abstract class MeToUserDtoMapper extends UserToUserDtoMapper{
@Inject
private UserManager userManager;
@Inject
private ResourceLinks resourceLinks;
@Override
@AfterMapping
protected void appendLinks(User user, @MappingTarget UserDto target) {
Links.Builder linksBuilder = linkingTo().self(resourceLinks.me().self());
if (UserPermissions.delete(user).isPermitted()) {
linksBuilder.single(link("delete", resourceLinks.me().delete(target.getName())));
}
if (UserPermissions.modify(user).isPermitted()) {
linksBuilder.single(link("update", resourceLinks.me().update(target.getName())));
}
if (userManager.isTypeDefault(user)) {
linksBuilder.single(link("password", resourceLinks.me().passwordChange()));
}
target.add(linksBuilder.build());
}
}

View File

@@ -0,0 +1,17 @@
package sonia.scm.api.v2.resources;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.hibernate.validator.constraints.NotEmpty;
@Getter
@Setter
@ToString
public class PasswordChangeDto {
private String oldPassword;
@NotEmpty
private String newPassword;
}

View File

@@ -162,7 +162,7 @@ public class PermissionRootResource {
RepositoryPermissions.permissionWrite(repository).check(); RepositoryPermissions.permissionWrite(repository).check();
String extractedPermissionName = getPermissionName(permissionName); String extractedPermissionName = getPermissionName(permissionName);
if (!isPermissionExist(new PermissionDto(extractedPermissionName, isGroupPermission(permissionName)), repository)) { if (!isPermissionExist(new PermissionDto(extractedPermissionName, isGroupPermission(permissionName)), repository)) {
throw new NotFoundException("the permission " + extractedPermissionName + " does not exist"); throw new NotFoundException("permission", extractedPermissionName);
} }
permission.setGroupPermission(isGroupPermission(permissionName)); permission.setGroupPermission(isGroupPermission(permissionName));
if (!extractedPermissionName.equals(permission.getName())) { if (!extractedPermissionName.equals(permission.getName())) {

View File

@@ -3,6 +3,7 @@ package sonia.scm.api.v2.resources;
import sonia.scm.repository.NamespaceAndName; import sonia.scm.repository.NamespaceAndName;
import javax.inject.Inject; import javax.inject.Inject;
import javax.ws.rs.core.UriInfo;
import java.net.URI; import java.net.URI;
class ResourceLinks { class ResourceLinks {
@@ -85,7 +86,42 @@ class ResourceLinks {
String update(String name) { String update(String name) {
return userLinkBuilder.method("getUserResource").parameters(name).method("update").parameters().href(); return userLinkBuilder.method("getUserResource").parameters(name).method("update").parameters().href();
} }
public String passwordChange(String name) {
return userLinkBuilder.method("getUserResource").parameters(name).method("changePassword").parameters().href();
} }
}
MeLinks me() {
return new MeLinks(scmPathInfoStore.get(), this.user());
}
static class MeLinks {
private final LinkBuilder meLinkBuilder;
private UserLinks userLinks;
MeLinks(ScmPathInfo pathInfo, UserLinks user) {
meLinkBuilder = new LinkBuilder(pathInfo, MeResource.class);
userLinks = user;
}
String self() {
return meLinkBuilder.method("get").parameters().href();
}
String delete(String name) {
return userLinks.delete(name);
}
String update(String name) {
return userLinks.update(name);
}
public String passwordChange() {
return meLinkBuilder.method("changePassword").parameters().href();
}
}
UserCollectionLinks userCollection() { UserCollectionLinks userCollection() {
return new UserCollectionLinks(scmPathInfoStore.get()); return new UserCollectionLinks(scmPathInfoStore.get());

View File

@@ -11,6 +11,7 @@ import javax.ws.rs.core.GenericEntity;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import java.util.Collection; import java.util.Collection;
import java.util.Optional; import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Function; import java.util.function.Function;
import java.util.function.Predicate; import java.util.function.Predicate;
import java.util.function.Supplier; import java.util.function.Supplier;
@@ -53,6 +54,11 @@ class SingleResourceManagerAdapter<MODEL_OBJECT extends ModelObject,
.map(Response.ResponseBuilder::build) .map(Response.ResponseBuilder::build)
.orElseThrow(NotFoundException::new); .orElseThrow(NotFoundException::new);
} }
public Response update(Supplier<Optional<MODEL_OBJECT>> reader, Function<MODEL_OBJECT, MODEL_OBJECT> applyChanges, Predicate<MODEL_OBJECT> hasSameKey, Consumer<MODEL_OBJECT> checker) throws NotFoundException, ConcurrentModificationException {
MODEL_OBJECT existingModelObject = reader.get().orElseThrow(NotFoundException::new);
checker.accept(existingModelObject);
return update(reader,applyChanges,hasSameKey);
}
/** /**
* Update the model object for the given id according to the given function and returns a corresponding http response. * Update the model object for the given id according to the given function and returns a corresponding http response.

View File

@@ -28,7 +28,7 @@ public abstract class TagToTagDtoMapper {
Links.Builder linksBuilder = linkingTo() Links.Builder linksBuilder = linkingTo()
.self(resourceLinks.tag().self(namespaceAndName.getNamespace(), namespaceAndName.getName(), target.getName())) .self(resourceLinks.tag().self(namespaceAndName.getNamespace(), namespaceAndName.getName(), target.getName()))
.single(link("sources", resourceLinks.source().self(namespaceAndName.getNamespace(), namespaceAndName.getName(), target.getRevision()))) .single(link("sources", resourceLinks.source().self(namespaceAndName.getNamespace(), namespaceAndName.getName(), target.getRevision())))
.single(link("changesets", resourceLinks.changeset().self(namespaceAndName.getNamespace(), namespaceAndName.getName(), target.getRevision()))); .single(link("changeset", resourceLinks.changeset().self(namespaceAndName.getNamespace(), namespaceAndName.getName(), target.getRevision())));
target.add(linksBuilder.build()); target.add(linksBuilder.build());
} }
} }

View File

@@ -5,6 +5,7 @@ import com.webcohesion.enunciate.metadata.rs.ResponseHeader;
import com.webcohesion.enunciate.metadata.rs.ResponseHeaders; import com.webcohesion.enunciate.metadata.rs.ResponseHeaders;
import com.webcohesion.enunciate.metadata.rs.StatusCodes; import com.webcohesion.enunciate.metadata.rs.StatusCodes;
import com.webcohesion.enunciate.metadata.rs.TypeHint; import com.webcohesion.enunciate.metadata.rs.TypeHint;
import org.apache.shiro.authc.credential.PasswordService;
import sonia.scm.AlreadyExistsException; import sonia.scm.AlreadyExistsException;
import sonia.scm.user.User; import sonia.scm.user.User;
import sonia.scm.user.UserManager; import sonia.scm.user.UserManager;
@@ -29,14 +30,16 @@ public class UserCollectionResource {
private final ResourceLinks resourceLinks; private final ResourceLinks resourceLinks;
private final IdResourceManagerAdapter<User, UserDto> adapter; private final IdResourceManagerAdapter<User, UserDto> adapter;
private final PasswordService passwordService;
@Inject @Inject
public UserCollectionResource(UserManager manager, UserDtoToUserMapper dtoToUserMapper, public UserCollectionResource(UserManager manager, UserDtoToUserMapper dtoToUserMapper,
UserCollectionToDtoMapper userCollectionToDtoMapper, ResourceLinks resourceLinks) { UserCollectionToDtoMapper userCollectionToDtoMapper, ResourceLinks resourceLinks, PasswordService passwordService) {
this.dtoToUserMapper = dtoToUserMapper; this.dtoToUserMapper = dtoToUserMapper;
this.userCollectionToDtoMapper = userCollectionToDtoMapper; this.userCollectionToDtoMapper = userCollectionToDtoMapper;
this.adapter = new IdResourceManagerAdapter<>(manager, User.class); this.adapter = new IdResourceManagerAdapter<>(manager, User.class);
this.resourceLinks = resourceLinks; this.resourceLinks = resourceLinks;
this.passwordService = passwordService;
} }
/** /**
@@ -89,8 +92,6 @@ public class UserCollectionResource {
@TypeHint(TypeHint.NO_CONTENT.class) @TypeHint(TypeHint.NO_CONTENT.class)
@ResponseHeaders(@ResponseHeader(name = "Location", description = "uri to the created user")) @ResponseHeaders(@ResponseHeader(name = "Location", description = "uri to the created user"))
public Response create(@Valid UserDto userDto) throws AlreadyExistsException { public Response create(@Valid UserDto userDto) throws AlreadyExistsException {
return adapter.create(userDto, return adapter.create(userDto, () -> dtoToUserMapper.map(userDto, passwordService.encryptPassword(userDto.getPassword())), user -> resourceLinks.user().self(user.getName()));
() -> dtoToUserMapper.map(userDto, ""),
user -> resourceLinks.user().self(user.getName()));
} }
} }

View File

@@ -26,6 +26,7 @@ public class UserDto extends HalRepresentation {
private String mail; private String mail;
@Pattern(regexp = "^[A-z0-9\\.\\-_@]|[^ ]([A-z0-9\\.\\-_@ ]*[A-z0-9\\.\\-_@]|[^ ])?$") @Pattern(regexp = "^[A-z0-9\\.\\-_@]|[^ ]([A-z0-9\\.\\-_@ ]*[A-z0-9\\.\\-_@]|[^ ])?$")
private String name; private String name;
@JsonInclude(JsonInclude.Include.NON_NULL)
private String password; private String password;
private String type; private String type;
private Map<String, String> properties; private Map<String, String> properties;

View File

@@ -1,37 +1,35 @@
package sonia.scm.api.v2.resources; package sonia.scm.api.v2.resources;
import org.apache.shiro.authc.credential.PasswordService; import org.mapstruct.AfterMapping;
import org.mapstruct.Context; import org.mapstruct.Context;
import org.mapstruct.Mapper; import org.mapstruct.Mapper;
import org.mapstruct.Mapping; import org.mapstruct.Mapping;
import org.mapstruct.Named; import org.mapstruct.MappingTarget;
import sonia.scm.user.User; import sonia.scm.user.User;
import javax.inject.Inject;
import java.time.Instant;
import static sonia.scm.api.rest.resources.UserResource.DUMMY_PASSWORT;
// Mapstruct does not support parameterized (i.e. non-default) constructors. Thus, we need to use field injection. // Mapstruct does not support parameterized (i.e. non-default) constructors. Thus, we need to use field injection.
@SuppressWarnings("squid:S3306") @SuppressWarnings("squid:S3306")
@Mapper @Mapper
public abstract class UserDtoToUserMapper extends BaseDtoMapper { public abstract class UserDtoToUserMapper extends BaseDtoMapper {
@Inject
private PasswordService passwordService;
@Mapping(source = "password", target = "password", qualifiedByName = "encrypt")
@Mapping(target = "creationDate", ignore = true) @Mapping(target = "creationDate", ignore = true)
public abstract User map(UserDto userDto, @Context String originalPassword); public abstract User map(UserDto userDto, @Context String usedPassword);
@Named("encrypt")
String encrypt(String password, @Context String originalPassword) {
if (DUMMY_PASSWORT.equals(password)) { /**
return originalPassword; * depends on the use case the right password will be mapped.
} else { * The given Password in the context parameter will be set.
return passwordService.encryptPassword(password); * The mapper consumer have the control of what password should be set.
} * </p>
* eg. for update user action the password will be set to the original password
* for create user and change password actions the password is the user input
*
* @param usedPassword the password to be set
* @param user the target
*/
@AfterMapping
void overridePassword(@MappingTarget User user, @Context String usedPassword) {
user.setPassword(usedPassword);
} }
} }

View File

@@ -3,6 +3,7 @@ package sonia.scm.api.v2.resources;
import com.webcohesion.enunciate.metadata.rs.ResponseCode; import com.webcohesion.enunciate.metadata.rs.ResponseCode;
import com.webcohesion.enunciate.metadata.rs.StatusCodes; import com.webcohesion.enunciate.metadata.rs.StatusCodes;
import com.webcohesion.enunciate.metadata.rs.TypeHint; import com.webcohesion.enunciate.metadata.rs.TypeHint;
import org.apache.shiro.authc.credential.PasswordService;
import sonia.scm.ConcurrentModificationException; import sonia.scm.ConcurrentModificationException;
import sonia.scm.NotFoundException; import sonia.scm.NotFoundException;
import sonia.scm.user.User; import sonia.scm.user.User;
@@ -26,12 +27,16 @@ public class UserResource {
private final UserToUserDtoMapper userToDtoMapper; private final UserToUserDtoMapper userToDtoMapper;
private final IdResourceManagerAdapter<User, UserDto> adapter; private final IdResourceManagerAdapter<User, UserDto> adapter;
private final UserManager userManager;
private final PasswordService passwordService;
@Inject @Inject
public UserResource(UserDtoToUserMapper dtoToUserMapper, UserToUserDtoMapper userToDtoMapper, UserManager manager) { public UserResource(UserDtoToUserMapper dtoToUserMapper, UserToUserDtoMapper userToDtoMapper, UserManager manager, PasswordService passwordService) {
this.dtoToUserMapper = dtoToUserMapper; this.dtoToUserMapper = dtoToUserMapper;
this.userToDtoMapper = userToDtoMapper; this.userToDtoMapper = userToDtoMapper;
this.adapter = new IdResourceManagerAdapter<>(manager, User.class); this.adapter = new IdResourceManagerAdapter<>(manager, User.class);
this.userManager = manager;
this.passwordService = passwordService;
} }
/** /**
@@ -40,7 +45,6 @@ public class UserResource {
* <strong>Note:</strong> This method requires "user" privilege. * <strong>Note:</strong> This method requires "user" privilege.
* *
* @param id the id/name of the user * @param id the id/name of the user
*
*/ */
@GET @GET
@Path("") @Path("")
@@ -63,7 +67,6 @@ public class UserResource {
* <strong>Note:</strong> This method requires "user" privilege. * <strong>Note:</strong> This method requires "user" privilege.
* *
* @param name the name of the user to delete. * @param name the name of the user to delete.
*
*/ */
@DELETE @DELETE
@Path("") @Path("")
@@ -80,6 +83,7 @@ public class UserResource {
/** /**
* Modifies the given user. * Modifies the given user.
* The given Password in the payload will be ignored. To Change Password use the changePassword endpoint
* *
* <strong>Note:</strong> This method requires "user" privilege. * <strong>Note:</strong> This method requires "user" privilege.
* *
@@ -101,4 +105,30 @@ public class UserResource {
public Response update(@PathParam("id") String name, @Valid UserDto userDto) throws NotFoundException, ConcurrentModificationException { public Response update(@PathParam("id") String name, @Valid UserDto userDto) throws NotFoundException, ConcurrentModificationException {
return adapter.update(name, existing -> dtoToUserMapper.map(userDto, existing.getPassword())); return adapter.update(name, existing -> dtoToUserMapper.map(userDto, existing.getPassword()));
} }
/**
* This Endpoint is for Admin user to modify a user password.
* The oldPassword property of the DTO is not needed here. it will be ignored.
* The oldPassword property is needed in the MeResources when the actual user change the own password.
*
* <strong>Note:</strong> This method requires "user:modify" privilege.
* @param name name of the user to be modified
* @param passwordChangeDto change password object to modify password. the old password is here not required
*/
@PUT
@Path("password")
@Consumes(VndMediaType.PASSWORD_CHANGE)
@StatusCodes({
@ResponseCode(code = 204, condition = "update success"),
@ResponseCode(code = 400, condition = "Invalid body, e.g. the user type is not xml or the given oldPassword do not match the stored one"),
@ResponseCode(code = 401, condition = "not authenticated / invalid credentials"),
@ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"user\" privilege"),
@ResponseCode(code = 404, condition = "not found, no user with the specified id/name available"),
@ResponseCode(code = 500, condition = "internal server error")
})
@TypeHint(TypeHint.NO_CONTENT.class)
public Response changePassword(@PathParam("id") String name, @Valid PasswordChangeDto passwordChangeDto) throws NotFoundException, ConcurrentModificationException {
return adapter.update(name, user -> user.changePassword(passwordService.encryptPassword(passwordChangeDto.getNewPassword())), userManager.getUserTypeChecker());
}
} }

View File

@@ -4,9 +4,10 @@ import com.google.common.annotations.VisibleForTesting;
import de.otto.edison.hal.Links; import de.otto.edison.hal.Links;
import org.mapstruct.AfterMapping; import org.mapstruct.AfterMapping;
import org.mapstruct.Mapper; import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.MappingTarget; import org.mapstruct.MappingTarget;
import sonia.scm.api.rest.resources.UserResource;
import sonia.scm.user.User; import sonia.scm.user.User;
import sonia.scm.user.UserManager;
import sonia.scm.user.UserPermissions; import sonia.scm.user.UserPermissions;
import javax.inject.Inject; import javax.inject.Inject;
@@ -19,19 +20,17 @@ import static de.otto.edison.hal.Links.linkingTo;
@Mapper @Mapper
public abstract class UserToUserDtoMapper extends BaseMapper<User, UserDto> { public abstract class UserToUserDtoMapper extends BaseMapper<User, UserDto> {
@Inject
private UserManager userManager;
@Override
@Mapping(target = "attributes", ignore = true)
@Mapping(target = "password", ignore = true)
public abstract UserDto map(User modelObject);
@Inject @Inject
private ResourceLinks resourceLinks; private ResourceLinks resourceLinks;
@VisibleForTesting
void setResourceLinks(ResourceLinks resourceLinks) {
this.resourceLinks = resourceLinks;
}
@AfterMapping
void removePassword(@MappingTarget UserDto target) {
target.setPassword(UserResource.DUMMY_PASSWORT);
}
@AfterMapping @AfterMapping
protected void appendLinks(User user, @MappingTarget UserDto target) { protected void appendLinks(User user, @MappingTarget UserDto target) {
Links.Builder linksBuilder = linkingTo().self(resourceLinks.user().self(target.getName())); Links.Builder linksBuilder = linkingTo().self(resourceLinks.user().self(target.getName()));
@@ -41,6 +40,9 @@ public abstract class UserToUserDtoMapper extends BaseMapper<User, UserDto> {
if (UserPermissions.modify(user).isPermitted()) { if (UserPermissions.modify(user).isPermitted()) {
linksBuilder.single(link("update", resourceLinks.user().update(target.getName()))); linksBuilder.single(link("update", resourceLinks.user().update(target.getName())));
} }
if (userManager.isTypeDefault(user)) {
linksBuilder.single(link("password", resourceLinks.user().passwordChange(target.getName())));
}
target.add(linksBuilder.build()); target.add(linksBuilder.build());
} }

View File

@@ -67,7 +67,7 @@ public class BranchRootResourceTest extends RepositoryTestBase {
@InjectMocks @InjectMocks
private BranchToBranchDtoMapperImpl branchToDtoMapper; private BranchToBranchDtoMapperImpl branchToDtoMapper;
private ChangesetCollectionToDtoMapper changesetCollectionToDtoMapper; private BranchChangesetCollectionToDtoMapper changesetCollectionToDtoMapper;
private BranchRootResource branchRootResource; private BranchRootResource branchRootResource;
@@ -90,7 +90,7 @@ public class BranchRootResourceTest extends RepositoryTestBase {
@Before @Before
public void prepareEnvironment() throws Exception { public void prepareEnvironment() throws Exception {
changesetCollectionToDtoMapper = new ChangesetCollectionToDtoMapper(changesetToChangesetDtoMapper, resourceLinks); changesetCollectionToDtoMapper = new BranchChangesetCollectionToDtoMapper(changesetToChangesetDtoMapper, resourceLinks);
BranchCollectionToDtoMapper branchCollectionToDtoMapper = new BranchCollectionToDtoMapper(branchToDtoMapper, resourceLinks); BranchCollectionToDtoMapper branchCollectionToDtoMapper = new BranchCollectionToDtoMapper(branchToDtoMapper, resourceLinks);
branchRootResource = new BranchRootResource(serviceFactory, branchToDtoMapper, branchCollectionToDtoMapper, changesetCollectionToDtoMapper); branchRootResource = new BranchRootResource(serviceFactory, branchToDtoMapper, branchCollectionToDtoMapper, changesetCollectionToDtoMapper);
super.branchRootResource = Providers.of(branchRootResource); super.branchRootResource = Providers.of(branchRootResource);
@@ -152,6 +152,10 @@ public class BranchRootResourceTest extends RepositoryTestBase {
when(logCommandBuilder.setPagingLimit(anyInt())).thenReturn(logCommandBuilder); when(logCommandBuilder.setPagingLimit(anyInt())).thenReturn(logCommandBuilder);
when(logCommandBuilder.setBranch(anyString())).thenReturn(logCommandBuilder); when(logCommandBuilder.setBranch(anyString())).thenReturn(logCommandBuilder);
when(logCommandBuilder.getChangesets()).thenReturn(changesetPagingResult); when(logCommandBuilder.getChangesets()).thenReturn(changesetPagingResult);
Branches branches = mock(Branches.class);
List<Branch> branchList = Lists.newArrayList(new Branch("master",id));
when(branches.getBranches()).thenReturn(branchList);
when(branchesCommandBuilder.getBranches()).thenReturn(branches);
MockHttpRequest request = MockHttpRequest.get(BRANCH_URL + "/changesets/"); MockHttpRequest request = MockHttpRequest.get(BRANCH_URL + "/changesets/");
MockHttpResponse response = new MockHttpResponse(); MockHttpResponse response = new MockHttpResponse();
dispatcher.invoke(request, response); dispatcher.invoke(request, response);
@@ -161,6 +165,5 @@ public class BranchRootResourceTest extends RepositoryTestBase {
assertTrue(response.getContentAsString().contains(String.format("\"name\":\"%s\"", authorName))); assertTrue(response.getContentAsString().contains(String.format("\"name\":\"%s\"", authorName)));
assertTrue(response.getContentAsString().contains(String.format("\"mail\":\"%s\"", authorEmail))); assertTrue(response.getContentAsString().contains(String.format("\"mail\":\"%s\"", authorEmail)));
assertTrue(response.getContentAsString().contains(String.format("\"description\":\"%s\"", commit))); assertTrue(response.getContentAsString().contains(String.format("\"description\":\"%s\"", commit)));
assertTrue(response.getContentAsString().contains(String.format("\"description\":\"%s\"", commit)));
} }
} }

View File

@@ -15,6 +15,8 @@ public class DispatcherMock {
dispatcher.getProviderFactory().registerProvider(AuthorizationExceptionMapper.class); dispatcher.getProviderFactory().registerProvider(AuthorizationExceptionMapper.class);
dispatcher.getProviderFactory().registerProvider(ConcurrentModificationExceptionMapper.class); dispatcher.getProviderFactory().registerProvider(ConcurrentModificationExceptionMapper.class);
dispatcher.getProviderFactory().registerProvider(InternalRepositoryExceptionMapper.class); dispatcher.getProviderFactory().registerProvider(InternalRepositoryExceptionMapper.class);
dispatcher.getProviderFactory().registerProvider(ChangePasswordNotAllowedExceptionMapper.class);
dispatcher.getProviderFactory().registerProvider(InvalidPasswordExceptionMapper.class);
return dispatcher; return dispatcher;
} }
} }

View File

@@ -2,11 +2,12 @@ package sonia.scm.api.v2.resources;
import com.github.sdorra.shiro.ShiroRule; import com.github.sdorra.shiro.ShiroRule;
import com.github.sdorra.shiro.SubjectAware; import com.github.sdorra.shiro.SubjectAware;
import org.apache.shiro.authc.credential.PasswordService;
import org.jboss.resteasy.core.Dispatcher; import org.jboss.resteasy.core.Dispatcher;
import org.jboss.resteasy.mock.MockDispatcherFactory;
import org.jboss.resteasy.mock.MockHttpRequest; import org.jboss.resteasy.mock.MockHttpRequest;
import org.jboss.resteasy.mock.MockHttpResponse; import org.jboss.resteasy.mock.MockHttpResponse;
import org.junit.Before; import org.junit.Before;
import org.junit.Ignore;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.mockito.ArgumentCaptor; import org.mockito.ArgumentCaptor;
@@ -22,11 +23,17 @@ import java.net.URISyntaxException;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import static org.mockito.MockitoAnnotations.initMocks; import static org.mockito.MockitoAnnotations.initMocks;
import static sonia.scm.api.v2.resources.DispatcherMock.createDispatcher;
@SubjectAware( @SubjectAware(
username = "trillian",
password = "secret",
configuration = "classpath:sonia/scm/repository/shiro.ini" configuration = "classpath:sonia/scm/repository/shiro.ini"
) )
public class MeResourceTest { public class MeResourceTest {
@@ -34,8 +41,7 @@ public class MeResourceTest {
@Rule @Rule
public ShiroRule shiro = new ShiroRule(); public ShiroRule shiro = new ShiroRule();
private Dispatcher dispatcher = MockDispatcherFactory.createDispatcher(); private Dispatcher dispatcher;
private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(URI.create("/")); private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(URI.create("/"));
@Mock @Mock
@@ -47,22 +53,28 @@ public class MeResourceTest {
private UserManager userManager; private UserManager userManager;
@InjectMocks @InjectMocks
private UserToUserDtoMapperImpl userToDtoMapper; private MeToUserDtoMapperImpl userToDtoMapper;
private ArgumentCaptor<User> userCaptor = ArgumentCaptor.forClass(User.class); private ArgumentCaptor<User> userCaptor = ArgumentCaptor.forClass(User.class);
@Mock
private PasswordService passwordService;
private User originalUser;
@Before @Before
public void prepareEnvironment() throws Exception { public void prepareEnvironment() throws Exception {
initMocks(this); initMocks(this);
createDummyUser("trillian"); originalUser = createDummyUser("trillian");
when(userManager.create(userCaptor.capture())).thenAnswer(invocation -> invocation.getArguments()[0]); when(userManager.create(userCaptor.capture())).thenAnswer(invocation -> invocation.getArguments()[0]);
doNothing().when(userManager).modify(userCaptor.capture()); doNothing().when(userManager).modify(userCaptor.capture());
doNothing().when(userManager).delete(userCaptor.capture()); doNothing().when(userManager).delete(userCaptor.capture());
userToDtoMapper.setResourceLinks(resourceLinks); when(userManager.isTypeDefault(userCaptor.capture())).thenCallRealMethod();
MeResource meResource = new MeResource(userToDtoMapper, userManager); when(userManager.getUserTypeChecker()).thenCallRealMethod();
dispatcher.getRegistry().addSingletonResource(meResource); when(userManager.getDefaultType()).thenReturn("xml");
MeResource meResource = new MeResource(userToDtoMapper, userManager, passwordService);
when(uriInfo.getApiRestUri()).thenReturn(URI.create("/")); when(uriInfo.getApiRestUri()).thenReturn(URI.create("/"));
when(scmPathInfoStore.get()).thenReturn(uriInfo); when(scmPathInfoStore.get()).thenReturn(uriInfo);
dispatcher = createDispatcher(meResource);
} }
@Test @Test
@@ -76,14 +88,77 @@ public class MeResourceTest {
assertEquals(HttpServletResponse.SC_OK, response.getStatus()); assertEquals(HttpServletResponse.SC_OK, response.getStatus());
assertTrue(response.getContentAsString().contains("\"name\":\"trillian\"")); assertTrue(response.getContentAsString().contains("\"name\":\"trillian\""));
assertTrue(response.getContentAsString().contains("\"password\":\"__dummypassword__\"")); assertTrue(response.getContentAsString().contains("\"self\":{\"href\":\"/v2/me/\"}"));
assertTrue(response.getContentAsString().contains("\"self\":{\"href\":\"/v2/users/trillian\"}"));
assertTrue(response.getContentAsString().contains("\"delete\":{\"href\":\"/v2/users/trillian\"}")); assertTrue(response.getContentAsString().contains("\"delete\":{\"href\":\"/v2/users/trillian\"}"));
} }
@Test
@SubjectAware(username = "trillian", password = "secret")
public void shouldEncryptPasswordBeforeChanging() throws Exception {
String newPassword = "pwd123";
String encryptedNewPassword = "encrypted123";
String oldPassword = "notEncriptedSecret";
String content = String.format("{ \"oldPassword\": \"%s\" , \"newPassword\": \"%s\" }", oldPassword, newPassword);
MockHttpRequest request = MockHttpRequest
.put("/" + MeResource.ME_PATH_V2 + "password")
.contentType(VndMediaType.PASSWORD_CHANGE)
.content(content.getBytes());
MockHttpResponse response = new MockHttpResponse();
when(passwordService.encryptPassword(eq(newPassword))).thenReturn(encryptedNewPassword);
when(passwordService.encryptPassword(eq(oldPassword))).thenReturn("secret");
dispatcher.invoke(request, response);
assertEquals(HttpServletResponse.SC_NO_CONTENT, response.getStatus());
verify(userManager).modify(any(User.class));
User updatedUser = userCaptor.getValue();
assertEquals(encryptedNewPassword, updatedUser.getPassword());
}
@Test
@SubjectAware(username = "trillian", password = "secret")
public void shouldGet400OnChangePasswordOfUserWithNonDefaultType() throws Exception {
originalUser.setType("not an xml type");
String newPassword = "pwd123";
String oldPassword = "notEncriptedSecret";
String content = String.format("{ \"oldPassword\": \"%s\" , \"newPassword\": \"%s\" }", oldPassword, newPassword);
MockHttpRequest request = MockHttpRequest
.put("/" + MeResource.ME_PATH_V2 + "password")
.contentType(VndMediaType.PASSWORD_CHANGE)
.content(content.getBytes());
MockHttpResponse response = new MockHttpResponse();
when(passwordService.encryptPassword(newPassword)).thenReturn("encrypted123");
when(passwordService.encryptPassword(eq(oldPassword))).thenReturn("secret");
dispatcher.invoke(request, response);
assertEquals(HttpServletResponse.SC_BAD_REQUEST, response.getStatus());
}
@Test
@SubjectAware(username = "trillian", password = "secret")
public void shouldGet400OnChangePasswordIfOldPasswordDoesNotMatchOriginalPassword() throws Exception {
String newPassword = "pwd123";
String oldPassword = "notEncriptedSecret";
String content = String.format("{ \"oldPassword\": \"%s\" , \"newPassword\": \"%s\" }", oldPassword, newPassword);
MockHttpRequest request = MockHttpRequest
.put("/" + MeResource.ME_PATH_V2 + "password")
.contentType(VndMediaType.PASSWORD_CHANGE)
.content(content.getBytes());
MockHttpResponse response = new MockHttpResponse();
when(passwordService.encryptPassword(newPassword)).thenReturn("encrypted123");
when(passwordService.encryptPassword(eq(oldPassword))).thenReturn("differentThanSecret");
dispatcher.invoke(request, response);
assertEquals(HttpServletResponse.SC_BAD_REQUEST, response.getStatus());
}
private User createDummyUser(String name) { private User createDummyUser(String name) {
User user = new User(); User user = new User();
user.setName(name); user.setName(name);
user.setType("xml");
user.setPassword("secret"); user.setPassword("secret");
user.setCreationDate(System.currentTimeMillis()); user.setCreationDate(System.currentTimeMillis());
when(userManager.get(name)).thenReturn(user); when(userManager.get(name)).thenReturn(user);

View File

@@ -0,0 +1,135 @@
package sonia.scm.api.v2.resources;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.subject.support.SubjectThreadState;
import org.apache.shiro.util.ThreadContext;
import org.apache.shiro.util.ThreadState;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import sonia.scm.user.User;
import sonia.scm.user.UserManager;
import java.net.URI;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.mockito.MockitoAnnotations.initMocks;
public class MeToUserDtoMapperTest {
private final URI baseUri = URI.create("http://example.com/base/");
@SuppressWarnings("unused") // Is injected
private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri);
@Mock
private UserManager userManager;
@InjectMocks
private MeToUserDtoMapperImpl mapper;
private final Subject subject = mock(Subject.class);
private final ThreadState subjectThreadState = new SubjectThreadState(subject);
private URI expectedBaseUri;
private URI expectedUserBaseUri;
@Before
public void init() {
initMocks(this);
when(userManager.getDefaultType()).thenReturn("xml");
expectedBaseUri = baseUri.resolve(MeResource.ME_PATH_V2 + "/");
expectedUserBaseUri = baseUri.resolve(UserRootResource.USERS_PATH_V2 + "/");
subjectThreadState.bind();
ThreadContext.bind(subject);
}
@After
public void unbindSubject() {
ThreadContext.unbindSubject();
}
@Test
public void shouldMapTheUpdateLink() {
User user = createDefaultUser();
when(subject.isPermitted("user:modify:abc")).thenReturn(true);
UserDto userDto = mapper.map(user);
assertEquals("expected update link", expectedUserBaseUri.resolve("abc").toString(), userDto.getLinks().getLinkBy("update").get().getHref());
when(subject.isPermitted("user:modify:abc")).thenReturn(false);
userDto = mapper.map(user);
assertFalse("expected no update link", userDto.getLinks().getLinkBy("update").isPresent());
}
@Test
public void shouldMapTheSelfLink() {
User user = createDefaultUser();
when(subject.isPermitted("user:modify:abc")).thenReturn(true);
UserDto userDto = mapper.map(user);
assertEquals("expected self link", expectedBaseUri.toString(), userDto.getLinks().getLinkBy("self").get().getHref());
}
@Test
public void shouldMapTheDeleteLink() {
User user = createDefaultUser();
when(subject.isPermitted("user:delete:abc")).thenReturn(true);
UserDto userDto = mapper.map(user);
assertEquals("expected update link", expectedUserBaseUri.resolve("abc").toString(), userDto.getLinks().getLinkBy("delete").get().getHref());
when(subject.isPermitted("user:delete:abc")).thenReturn(false);
userDto = mapper.map(user);
assertFalse("expected no delete link", userDto.getLinks().getLinkBy("delete").isPresent());
}
@Test
public void shouldGetPasswordLinkOnlyForDefaultUserType() {
User user = createDefaultUser();
when(subject.isPermitted("user:modify:abc")).thenReturn(true);
when(userManager.isTypeDefault(eq(user))).thenReturn(true);
UserDto userDto = mapper.map(user);
assertEquals("expected password link with modify permission", expectedBaseUri.resolve("password").toString(), userDto.getLinks().getLinkBy("password").get().getHref());
when(subject.isPermitted("user:modify:abc")).thenReturn(false);
userDto = mapper.map(user);
assertEquals("expected password link on mission modify permission", expectedBaseUri.resolve("password").toString(), userDto.getLinks().getLinkBy("password").get().getHref());
when(userManager.isTypeDefault(eq(user))).thenReturn(false);
userDto = mapper.map(user);
assertFalse("expected no password link", userDto.getLinks().getLinkBy("password").isPresent());
}
@Test
public void shouldGetEmptyPasswordProperty() {
User user = createDefaultUser();
user.setPassword("myHighSecurePassword");
when(subject.isPermitted("user:modify:abc")).thenReturn(true);
UserDto userDto = mapper.map(user);
assertThat(userDto.getPassword()).as("hide password for the me resource").isBlank();
}
private User createDefaultUser() {
User user = new User();
user.setName("abc");
user.setCreationDate(1L);
return user;
}
}

View File

@@ -12,7 +12,9 @@ public class ResourceLinksMock {
ScmPathInfo uriInfo = mock(ScmPathInfo.class); ScmPathInfo uriInfo = mock(ScmPathInfo.class);
when(uriInfo.getApiRestUri()).thenReturn(baseUri); when(uriInfo.getApiRestUri()).thenReturn(baseUri);
when(resourceLinks.user()).thenReturn(new ResourceLinks.UserLinks(uriInfo)); ResourceLinks.UserLinks userLinks = new ResourceLinks.UserLinks(uriInfo);
when(resourceLinks.user()).thenReturn(userLinks);
when(resourceLinks.me()).thenReturn(new ResourceLinks.MeLinks(uriInfo,userLinks));
when(resourceLinks.userCollection()).thenReturn(new ResourceLinks.UserCollectionLinks(uriInfo)); when(resourceLinks.userCollection()).thenReturn(new ResourceLinks.UserCollectionLinks(uriInfo));
when(resourceLinks.group()).thenReturn(new ResourceLinks.GroupLinks(uriInfo)); when(resourceLinks.group()).thenReturn(new ResourceLinks.GroupLinks(uriInfo));
when(resourceLinks.groupCollection()).thenReturn(new ResourceLinks.GroupCollectionLinks(uriInfo)); when(resourceLinks.groupCollection()).thenReturn(new ResourceLinks.GroupCollectionLinks(uriInfo));

View File

@@ -23,18 +23,9 @@ public class UserDtoToUserMapperTest {
@Test @Test
public void shouldMapFields() { public void shouldMapFields() {
UserDto dto = createDefaultDto(); UserDto dto = createDefaultDto();
User user = mapper.map(dto, "original password"); User user = mapper.map(dto, "used password");
assertEquals("abc" , user.getName()); assertEquals("abc" , user.getName());
} assertEquals("used password" , user.getPassword());
@Test
public void shouldEncodePassword() {
when(passwordService.encryptPassword("unencrypted")).thenReturn("encrypted");
UserDto dto = createDefaultDto();
dto.setPassword("unencrypted");
User user = mapper.map(dto, "original password");
assertEquals("encrypted" , user.getPassword());
} }
@Before @Before

View File

@@ -23,6 +23,7 @@ import javax.servlet.http.HttpServletResponse;
import java.net.URI; import java.net.URI;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.net.URL; import java.net.URL;
import java.text.MessageFormat;
import static java.util.Collections.singletonList; import static java.util.Collections.singletonList;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
@@ -61,19 +62,23 @@ public class UserRootResourceTest {
private UserToUserDtoMapperImpl userToDtoMapper; private UserToUserDtoMapperImpl userToDtoMapper;
private ArgumentCaptor<User> userCaptor = ArgumentCaptor.forClass(User.class); private ArgumentCaptor<User> userCaptor = ArgumentCaptor.forClass(User.class);
private User originalUser;
@Before @Before
public void prepareEnvironment() throws Exception { public void prepareEnvironment() throws Exception {
initMocks(this); initMocks(this);
User dummyUser = createDummyUser("Neo"); originalUser = createDummyUser("Neo");
when(userManager.create(userCaptor.capture())).thenAnswer(invocation -> invocation.getArguments()[0]); when(userManager.create(userCaptor.capture())).thenAnswer(invocation -> invocation.getArguments()[0]);
when(userManager.isTypeDefault(userCaptor.capture())).thenCallRealMethod();
when(userManager.getUserTypeChecker()).thenCallRealMethod();
doNothing().when(userManager).modify(userCaptor.capture()); doNothing().when(userManager).modify(userCaptor.capture());
doNothing().when(userManager).delete(userCaptor.capture()); doNothing().when(userManager).delete(userCaptor.capture());
when(userManager.getDefaultType()).thenReturn("xml");
UserCollectionToDtoMapper userCollectionToDtoMapper = new UserCollectionToDtoMapper(userToDtoMapper, resourceLinks); UserCollectionToDtoMapper userCollectionToDtoMapper = new UserCollectionToDtoMapper(userToDtoMapper, resourceLinks);
UserCollectionResource userCollectionResource = new UserCollectionResource(userManager, dtoToUserMapper, UserCollectionResource userCollectionResource = new UserCollectionResource(userManager, dtoToUserMapper,
userCollectionToDtoMapper, resourceLinks); userCollectionToDtoMapper, resourceLinks, passwordService);
UserResource userResource = new UserResource(dtoToUserMapper, userToDtoMapper, userManager); UserResource userResource = new UserResource(dtoToUserMapper, userToDtoMapper, userManager, passwordService);
UserRootResource userRootResource = new UserRootResource(Providers.of(userCollectionResource), UserRootResource userRootResource = new UserRootResource(Providers.of(userCollectionResource),
Providers.of(userResource)); Providers.of(userResource));
@@ -89,7 +94,6 @@ public class UserRootResourceTest {
assertEquals(HttpServletResponse.SC_OK, response.getStatus()); assertEquals(HttpServletResponse.SC_OK, response.getStatus());
assertTrue(response.getContentAsString().contains("\"name\":\"Neo\"")); assertTrue(response.getContentAsString().contains("\"name\":\"Neo\""));
assertTrue(response.getContentAsString().contains("\"password\":\"__dummypassword__\""));
assertTrue(response.getContentAsString().contains("\"self\":{\"href\":\"/v2/users/Neo\"}")); assertTrue(response.getContentAsString().contains("\"self\":{\"href\":\"/v2/users/Neo\"}"));
assertTrue(response.getContentAsString().contains("\"delete\":{\"href\":\"/v2/users/Neo\"}")); assertTrue(response.getContentAsString().contains("\"delete\":{\"href\":\"/v2/users/Neo\"}"));
} }
@@ -104,13 +108,48 @@ public class UserRootResourceTest {
assertEquals(HttpServletResponse.SC_OK, response.getStatus()); assertEquals(HttpServletResponse.SC_OK, response.getStatus());
assertTrue(response.getContentAsString().contains("\"name\":\"Neo\"")); assertTrue(response.getContentAsString().contains("\"name\":\"Neo\""));
assertTrue(response.getContentAsString().contains("\"password\":\"__dummypassword__\""));
assertTrue(response.getContentAsString().contains("\"self\":{\"href\":\"/v2/users/Neo\"}")); assertTrue(response.getContentAsString().contains("\"self\":{\"href\":\"/v2/users/Neo\"}"));
assertFalse(response.getContentAsString().contains("\"delete\":{\"href\":\"/v2/users/Neo\"}")); assertFalse(response.getContentAsString().contains("\"delete\":{\"href\":\"/v2/users/Neo\"}"));
} }
@Test @Test
public void shouldCreateNewUserWithEncryptedPassword() throws Exception { public void shouldEncryptPasswordBeforeChanging() throws Exception {
String newPassword = "pwd123";
String content = String.format("{\"newPassword\": \"%s\"}", newPassword);
MockHttpRequest request = MockHttpRequest
.put("/" + UserRootResource.USERS_PATH_V2 + "Neo/password")
.contentType(VndMediaType.PASSWORD_CHANGE)
.content(content.getBytes());
MockHttpResponse response = new MockHttpResponse();
when(passwordService.encryptPassword(newPassword)).thenReturn("encrypted123");
dispatcher.invoke(request, response);
assertEquals(HttpServletResponse.SC_NO_CONTENT, response.getStatus());
verify(userManager).modify(any(User.class));
User updatedUser = userCaptor.getValue();
assertEquals("encrypted123", updatedUser.getPassword());
}
@Test
public void shouldGet400OnChangePasswordOfUserWithNonDefaultType() throws Exception {
originalUser.setType("not an xml type");
String newPassword = "pwd123";
String content = String.format("{\"newPassword\": \"%s\"}", newPassword);
MockHttpRequest request = MockHttpRequest
.put("/" + UserRootResource.USERS_PATH_V2 + "Neo/password")
.contentType(VndMediaType.PASSWORD_CHANGE)
.content(content.getBytes());
MockHttpResponse response = new MockHttpResponse();
when(passwordService.encryptPassword(newPassword)).thenReturn("encrypted123");
dispatcher.invoke(request, response);
assertEquals(HttpServletResponse.SC_BAD_REQUEST, response.getStatus());
}
@Test
public void shouldEncryptPasswordBeforeCreatingUser() throws Exception {
URL url = Resources.getResource("sonia/scm/api/v2/user-test-create.json"); URL url = Resources.getResource("sonia/scm/api/v2/user-test-create.json");
byte[] userJson = Resources.toByteArray(url); byte[] userJson = Resources.toByteArray(url);
@@ -130,7 +169,7 @@ public class UserRootResourceTest {
} }
@Test @Test
public void shouldUpdateChangedUserWithEncryptedPassword() throws Exception { public void shouldIgnoreGivenPasswordOnUpdatingUser() throws Exception {
URL url = Resources.getResource("sonia/scm/api/v2/user-test-update.json"); URL url = Resources.getResource("sonia/scm/api/v2/user-test-update.json");
byte[] userJson = Resources.toByteArray(url); byte[] userJson = Resources.toByteArray(url);
@@ -139,14 +178,13 @@ public class UserRootResourceTest {
.contentType(VndMediaType.USER) .contentType(VndMediaType.USER)
.content(userJson); .content(userJson);
MockHttpResponse response = new MockHttpResponse(); MockHttpResponse response = new MockHttpResponse();
when(passwordService.encryptPassword("pwd123")).thenReturn("encrypted123");
dispatcher.invoke(request, response); dispatcher.invoke(request, response);
assertEquals(HttpServletResponse.SC_NO_CONTENT, response.getStatus()); assertEquals(HttpServletResponse.SC_NO_CONTENT, response.getStatus());
verify(userManager).modify(any(User.class)); verify(userManager).modify(any(User.class));
User updatedUser = userCaptor.getValue(); User updatedUser = userCaptor.getValue();
assertEquals("encrypted123", updatedUser.getPassword()); assertEquals(originalUser.getPassword(), updatedUser.getPassword());
} }
@Test @Test
@@ -154,7 +192,7 @@ public class UserRootResourceTest {
MockHttpRequest request = MockHttpRequest MockHttpRequest request = MockHttpRequest
.post("/" + UserRootResource.USERS_PATH_V2) .post("/" + UserRootResource.USERS_PATH_V2)
.contentType(VndMediaType.USER) .contentType(VndMediaType.USER)
.content(new byte[] {}); .content(new byte[]{});
MockHttpResponse response = new MockHttpResponse(); MockHttpResponse response = new MockHttpResponse();
when(passwordService.encryptPassword("pwd123")).thenReturn("encrypted123"); when(passwordService.encryptPassword("pwd123")).thenReturn("encrypted123");
@@ -265,6 +303,7 @@ public class UserRootResourceTest {
private User createDummyUser(String name) { private User createDummyUser(String name) {
User user = new User(); User user = new User();
user.setName(name); user.setName(name);
user.setType("xml");
user.setPassword("redpill"); user.setPassword("redpill");
user.setCreationDate(System.currentTimeMillis()); user.setCreationDate(System.currentTimeMillis());
when(userManager.get(name)).thenReturn(user); when(userManager.get(name)).thenReturn(user);

View File

@@ -8,14 +8,17 @@ import org.junit.After;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.mockito.InjectMocks; import org.mockito.InjectMocks;
import sonia.scm.api.rest.resources.UserResource; import org.mockito.Mock;
import sonia.scm.user.User; import sonia.scm.user.User;
import sonia.scm.user.UserManager;
import java.net.URI; import java.net.URI;
import java.time.Instant; import java.time.Instant;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import static org.mockito.MockitoAnnotations.initMocks; import static org.mockito.MockitoAnnotations.initMocks;
@@ -26,6 +29,9 @@ public class UserToUserDtoMapperTest {
@SuppressWarnings("unused") // Is injected @SuppressWarnings("unused") // Is injected
private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri); private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri);
@Mock
private UserManager userManager;
@InjectMocks @InjectMocks
private UserToUserDtoMapperImpl mapper; private UserToUserDtoMapperImpl mapper;
@@ -37,6 +43,7 @@ public class UserToUserDtoMapperTest {
@Before @Before
public void init() { public void init() {
initMocks(this); initMocks(this);
when(userManager.getDefaultType()).thenReturn("xml");
expectedBaseUri = baseUri.resolve(UserRootResource.USERS_PATH_V2 + "/"); expectedBaseUri = baseUri.resolve(UserRootResource.USERS_PATH_V2 + "/");
subjectThreadState.bind(); subjectThreadState.bind();
ThreadContext.bind(subject); ThreadContext.bind(subject);
@@ -53,11 +60,42 @@ public class UserToUserDtoMapperTest {
when(subject.isPermitted("user:modify:abc")).thenReturn(true); when(subject.isPermitted("user:modify:abc")).thenReturn(true);
UserDto userDto = mapper.map(user); UserDto userDto = mapper.map(user);
assertEquals("expected self link", expectedBaseUri.resolve("abc").toString(), userDto.getLinks().getLinkBy("self").get().getHref()); assertEquals("expected self link", expectedBaseUri.resolve("abc").toString(), userDto.getLinks().getLinkBy("self").get().getHref());
assertEquals("expected update link", expectedBaseUri.resolve("abc").toString(), userDto.getLinks().getLinkBy("update").get().getHref()); assertEquals("expected update link", expectedBaseUri.resolve("abc").toString(), userDto.getLinks().getLinkBy("update").get().getHref());
} }
@Test
public void shouldGetPasswordLinkOnlyForDefaultUserType() {
User user = createDefaultUser();
when(subject.isPermitted("user:modify:abc")).thenReturn(true);
when(userManager.isTypeDefault(eq(user))).thenReturn(true);
UserDto userDto = mapper.map(user);
assertEquals("expected password link with modify permission", expectedBaseUri.resolve("abc/password").toString(), userDto.getLinks().getLinkBy("password").get().getHref());
when(subject.isPermitted("user:modify:abc")).thenReturn(false);
userDto = mapper.map(user);
assertEquals("expected password link on mission modify permission", expectedBaseUri.resolve("abc/password").toString(), userDto.getLinks().getLinkBy("password").get().getHref());
when(userManager.isTypeDefault(eq(user))).thenReturn(false);
userDto = mapper.map(user);
assertFalse("expected no password link", userDto.getLinks().getLinkBy("password").isPresent());
}
@Test
public void shouldGetEmptyPasswordProperty() {
User user = createDefaultUser();
user.setPassword("myHighSecurePassword");
when(subject.isPermitted("user:modify:abc")).thenReturn(true);
UserDto userDto = mapper.map(user);
assertThat(userDto.getPassword()).isBlank();
}
@Test @Test
public void shouldMapLinks_forDelete() { public void shouldMapLinks_forDelete() {
User user = createDefaultUser(); User user = createDefaultUser();
@@ -97,16 +135,6 @@ public class UserToUserDtoMapperTest {
assertEquals("abc", userDto.getName()); assertEquals("abc", userDto.getName());
} }
@Test
public void shouldRemovePassword() {
User user = createDefaultUser();
user.setPassword("password");
UserDto userDto = mapper.map(user);
assertEquals(UserResource.DUMMY_PASSWORT, userDto.getPassword());
}
@Test @Test
public void shouldMapTimes() { public void shouldMapTimes() {
User user = createDefaultUser(); User user = createDefaultUser();

View File

@@ -38,6 +38,7 @@ import com.github.sdorra.shiro.SubjectAware;
import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets; import com.google.common.collect.Sets;
import org.apache.shiro.authz.UnauthorizedException; import org.apache.shiro.authz.UnauthorizedException;
import org.apache.shiro.util.ThreadContext;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.rules.ExpectedException; import org.junit.rules.ExpectedException;
@@ -94,6 +95,10 @@ import static org.mockito.Mockito.when;
) )
public class DefaultRepositoryManagerTest extends ManagerTestBase<Repository> { public class DefaultRepositoryManagerTest extends ManagerTestBase<Repository> {
{
ThreadContext.unbindSubject();
}
@Rule @Rule
public ShiroRule shiro = new ShiroRule(); public ShiroRule shiro = new ShiroRule();