This commit is contained in:
Christoph Wolfes
2018-07-24 15:56:23 +02:00
35 changed files with 590 additions and 311 deletions

2
Jenkinsfile vendored
View File

@@ -31,7 +31,7 @@ node() { // No specific label
}
stage('Unit Test') {
mvn 'test -Dsonia.scm.test.skip.hg=true'
mvn 'test -Dsonia.scm.test.skip.hg=true -Dmaven.test.failure.ignore=true'
}
stage('SonarQube') {

13
pom.xml
View File

@@ -158,6 +158,19 @@
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.github.sdorra</groupId>
<artifactId>shiro-unit</artifactId>
<version>1.0.1</version>
<scope>test</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<pluginManagement>

View File

@@ -143,11 +143,10 @@
</dependency>
<!-- test -->
<dependency>
<groupId>com.github.sdorra</groupId>
<artifactId>shiro-unit</artifactId>
<version>1.0.0</version>
<scope>test</scope>
</dependency>

View File

@@ -33,11 +33,11 @@
<artifactId>junit</artifactId>
<version>${junit.version}</version>
</dependency>
<dependency>
<groupId>com.github.sdorra</groupId>
<artifactId>shiro-unit</artifactId>
<version>1.0.0</version>
<scope>test</scope>
</dependency>
<dependency>

View File

@@ -7,8 +7,12 @@
"bulma": "^0.7.1",
"classnames": "^2.2.5",
"history": "^4.7.2",
"i18next": "^11.4.0",
"i18next-browser-languagedetector": "^2.2.2",
"i18next-fetch-backend": "^0.1.0",
"react": "^16.4.1",
"react-dom": "^16.4.1",
"react-i18next": "^7.9.0",
"react-jss": "^8.6.0",
"react-redux": "^5.0.7",
"react-router-dom": "^4.3.1",
@@ -27,7 +31,8 @@
"build-js": "react-scripts build",
"build": "npm-run-all build-css build-js",
"test": "jest",
"test-coverage": "yarn run test --coverage",
"test-coverage": "jest --coverage",
"test-ci": "jest --ci --coverage",
"eject": "react-scripts eject",
"flow": "flow"
},
@@ -42,6 +47,7 @@
"fetch-mock": "^6.5.0",
"flow-bin": "^0.77.0",
"flow-typed": "^2.5.1",
"jest-junit": "^5.1.0",
"node-sass-chokidar": "^1.3.0",
"npm-run-all": "^4.1.3",
"prettier": "^1.13.7",
@@ -52,5 +58,18 @@
"presets": [
"react-app"
]
},
"jest": {
"coverageDirectory": "target/jest-reports/coverage",
"coveragePathIgnorePatterns": [
"src/tests/.*"
],
"reporters": [
"default",
"jest-junit"
]
},
"jest-junit": {
"output": "./target/jest-reports/TEST-all.xml"
}
}

View File

@@ -16,6 +16,15 @@
<version>2.0.0-SNAPSHOT</version>
<name>scm-ui</name>
<properties>
<sonar.language>js</sonar.language>
<sonar.sources>src</sonar.sources>
<sonar.test.exclusions>**/*.test.js,src/tests/**</sonar.test.exclusions>
<sonar.coverage.exclusions>**/*.test.js,src/tests/**</sonar.coverage.exclusions>
<sonar.javascript.jstest.reportsPath>target/jest-reports</sonar.javascript.jstest.reportsPath>
<sonar.javascript.lcov.reportPaths>target/jest-reports/coverage/lcov.info</sonar.javascript.lcov.reportPaths>
</properties>
<build>
<plugins>
@@ -29,7 +38,7 @@
</node>
<pkgManager>
<type>YARN</type>
<version>1.3.2</version>
<version>1.7.0</version>
</pkgManager>
<script>run</script>
</configuration>
@@ -51,10 +60,19 @@
<script>build</script>
</configuration>
</execution>
<execution>
<id>test</id>
<phase>test</phase>
<goals>
<goal>run</goal>
</goals>
<configuration>
<script>test-ci</script>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,35 @@
{
"login": {
"title": "Login",
"subtitle": "Please login to proceed.",
"logo-alt": "SCM-Manager",
"username-placeholder": "Your Username",
"password-placeholder": "Your Password",
"submit": "Login"
},
"logout": {
"error": {
"title": "Logout failed",
"subtitle": "Something went wrong during logout"
}
},
"app": {
"error": {
"title": "Error",
"subtitle": "Unknown error occurred"
}
},
"error-notification": {
"prefix": "Error"
},
"loading": {
"alt": "Loading ..."
},
"logo": {
"alt": "SCM-Manager"
},
"primary-navigation": {
"users": "Users",
"logout": "Logout"
}
}

View File

@@ -1,18 +1,20 @@
//@flow
import React from "react";
import { translate } from "react-i18next";
import Notification from "./Notification";
type Props = {
t: string => string,
error?: Error
};
class ErrorNotification extends React.Component<Props> {
render() {
const { error } = this.props;
const { t, error } = this.props;
if (error) {
return (
<Notification type="danger">
<strong>Error:</strong> {error.message}
<strong>{t("error-notification.prefix")}:</strong> {error.message}
</Notification>
);
}
@@ -20,4 +22,4 @@ class ErrorNotification extends React.Component<Props> {
}
}
export default ErrorNotification;
export default translate("commons")(ErrorNotification);

View File

@@ -1,9 +1,8 @@
//@flow
import React from "react";
import type { Me } from "../types/me";
type Props = {
me?: Me
me?: string
};
class Footer extends React.Component<Props> {
@@ -15,7 +14,7 @@ class Footer extends React.Component<Props> {
return (
<footer className="footer">
<div className="container is-centered">
<p className="has-text-centered">{me.username}</p>
<p className="has-text-centered">{me}</p>
</div>
</footer>
);

View File

@@ -1,5 +1,6 @@
//@flow
import React from "react";
import { translate } from "react-i18next";
import injectSheet from "react-jss";
import Image from "../images/loading.svg";
@@ -24,20 +25,21 @@ const styles = {
};
type Props = {
t: string => string,
classes: any
};
class Loading extends React.Component<Props> {
render() {
const { classes } = this.props;
const { t, classes } = this.props;
return (
<div className={classes.wrapper}>
<div className={classes.loading}>
<img className={classes.image} src={Image} alt="Loading ..." />
<img className={classes.image} src={Image} alt={t("loading.alt")} />
</div>
</div>
);
}
}
export default injectSheet(styles)(Loading);
export default injectSheet(styles)(translate("commons")(Loading));

View File

@@ -1,13 +1,17 @@
//@flow
import React from "react";
import { translate } from "react-i18next";
import Image from "../images/logo.png";
type Props = {};
type Props = {
t: string => string
};
class Logo extends React.Component<Props> {
render() {
return <img src={Image} alt="SCM-Manager logo" />;
const { t } = this.props;
return <img src={Image} alt={t("logo.alt")} />;
}
}
export default Logo;
export default translate("commons")(Logo);

View File

@@ -1,20 +1,30 @@
//@flow
import React from "react";
import { translate } from "react-i18next";
import PrimaryNavigationLink from "./PrimaryNavigationLink";
type Props = {};
type Props = {
t: string => string
};
class PrimaryNavigation extends React.Component<Props> {
render() {
const { t } = this.props;
return (
<nav className="tabs is-boxed">
<ul>
<PrimaryNavigationLink to="/users" label="Users" />
<PrimaryNavigationLink to="/logout" label="Logout" />
<PrimaryNavigationLink
to="/users"
label={t("primary-navigation.users")}
/>
<PrimaryNavigationLink
to="/logout"
label={t("primary-navigation.logout")}
/>
</ul>
</nav>
);
}
}
export default PrimaryNavigation;
export default translate("commons")(PrimaryNavigation);

View File

@@ -1,6 +1,7 @@
import React, { Component } from "react";
import Main from "./Main";
import { connect } from "react-redux";
import { translate } from "react-i18next";
import { withRouter } from "react-router-dom";
import { fetchMe } from "../modules/auth";
@@ -17,6 +18,8 @@ type Props = {
error: Error,
loading: boolean,
authenticated?: boolean,
displayName: string,
t: string => string,
fetchMe: () => void
};
@@ -26,7 +29,7 @@ class App extends Component<Props> {
}
render() {
const { entry, loading, error, authenticated } = this.props;
const { loading, error, authenticated, displayName, t } = this.props;
let content;
const navigation = authenticated ? <PrimaryNavigation /> : "";
@@ -36,8 +39,8 @@ class App extends Component<Props> {
} else if (error) {
content = (
<ErrorPage
title="Error"
subtitle="Unknown error occurred"
title={t("app.error.title")}
subtitle={t("app.error.subtitle")}
error={error}
/>
);
@@ -48,7 +51,7 @@ class App extends Component<Props> {
<div className="App">
<Header>{navigation}</Header>
{content}
<Footer me={entry} />
<Footer me={displayName} />
</div>
);
}
@@ -62,15 +65,22 @@ const mapDispatchToProps = (dispatch: any) => {
const mapStateToProps = state => {
let mapped = state.auth.me || {};
let displayName;
if (state.auth.login) {
mapped.authenticated = state.auth.login.authenticated;
}
return mapped;
if (state.auth.me && state.auth.me.entry) {
displayName = state.auth.me.entry.entity.displayName;
}
return {
...mapped,
displayName
};
};
export default withRouter(
connect(
mapStateToProps,
mapDispatchToProps
)(App)
)(translate("commons")(App))
);

View File

@@ -2,6 +2,7 @@
import React from "react";
import { Redirect, withRouter } from "react-router-dom";
import injectSheet from "react-jss";
import { translate } from "react-i18next";
import { login } from "../modules/auth";
import { connect } from "react-redux";
@@ -35,6 +36,7 @@ type Props = {
loading?: boolean,
error?: Error,
t: string => string,
classes: any,
from: any,
@@ -83,7 +85,7 @@ class Login extends React.Component<Props, State> {
};
render() {
const { authenticated, loading, error, classes } = this.props;
const { authenticated, loading, error, t, classes } = this.props;
if (authenticated) {
return this.renderRedirect();
@@ -94,30 +96,30 @@ class Login extends React.Component<Props, State> {
<div className="hero-body">
<div className="container has-text-centered">
<div className="column is-4 is-offset-4">
<h3 className="title">Login</h3>
<p className="subtitle">Please login to proceed.</p>
<h3 className="title">{t("login.title")}</h3>
<p className="subtitle">{t("login.subtitle")}</p>
<div className={classNames("box", classes.avatarSpacing)}>
<figure className={classes.avatar}>
<img
className={classes.avatarImage}
src={Avatar}
alt="SCM-Manager"
alt={t("login.logo-alt")}
/>
</figure>
<ErrorNotification error={error} />
<form onSubmit={this.handleSubmit}>
<InputField
placeholder="Your Username"
placeholder={t("login.username-placeholder")}
autofocus={true}
onChange={this.handleUsernameChange}
/>
<InputField
placeholder="Your Password"
placeholder={t("login.password-placeholder")}
type="password"
onChange={this.handlePasswordChange}
/>
<SubmitButton
label="Login"
label={t("login.submit")}
disabled={this.isInValid()}
fullWidth={true}
loading={loading}
@@ -147,6 +149,6 @@ const StyledLogin = injectSheet(styles)(
connect(
mapStateToProps,
mapDispatchToProps
)(Login)
)(translate("commons")(Login))
);
export default withRouter(StyledLogin);

View File

@@ -1,6 +1,7 @@
//@flow
import React from "react";
import { connect } from "react-redux";
import { translate } from "react-i18next";
import { Redirect } from "react-router-dom";
import { logout, isAuthenticated } from "../modules/auth";
@@ -8,6 +9,7 @@ import ErrorPage from "../components/ErrorPage";
import Loading from "../components/Loading";
type Props = {
t: string => string,
loading: boolean,
authenticated: boolean,
error?: Error,
@@ -20,13 +22,13 @@ class Logout extends React.Component<Props> {
}
render() {
const { authenticated, loading, error } = this.props;
const { authenticated, loading, t, error } = this.props;
// TODO logout is called twice
if (error) {
return (
<ErrorPage
title="Logout failed"
subtitle="Something went wrong durring logout"
title={t("logout.error.title")}
subtitle={t("logout.error.subtitle")}
error={error}
/>
);
@@ -53,4 +55,4 @@ const mapDispatchToProps = dispatch => {
export default connect(
mapStateToProps,
mapDispatchToProps
)(Logout);
)(translate("commons")(Logout));

View File

@@ -4,7 +4,6 @@ import logger from "redux-logger";
import { createStore, compose, applyMiddleware, combineReducers } from "redux";
import { routerReducer, routerMiddleware } from "react-router-redux";
import repositories from "./repositories/modules/repositories";
import users from "./users/modules/users";
import auth from "./modules/auth";
@@ -16,7 +15,6 @@ function createReduxStore(history: BrowserHistory) {
const reducer = combineReducers({
router: routerReducer,
repositories,
users,
auth
});

37
scm-ui/src/i18n.js Normal file
View File

@@ -0,0 +1,37 @@
import i18n from "i18next";
import Backend from "i18next-fetch-backend";
import LanguageDetector from "i18next-browser-languagedetector";
import { reactI18nextModule } from "react-i18next";
const loadPath = process.env.PUBLIC_URL + "/locales/{{lng}}/{{ns}}.json";
i18n
.use(Backend)
.use(LanguageDetector)
.use(reactI18nextModule)
.init({
fallbackLng: "en",
// have a common namespace used around the full app
ns: ["commons"],
defaultNS: "commons",
debug: true,
interpolation: {
escapeValue: false // not needed for react!!
},
react: {
wait: true
},
backend: {
loadPath: loadPath,
init: {
credentials: "same-origin"
}
}
});
export default i18n;

View File

@@ -4,6 +4,9 @@ import ReactDOM from "react-dom";
import App from "./containers/App";
import registerServiceWorker from "./registerServiceWorker";
import { I18nextProvider } from "react-i18next";
import i18n from "./i18n";
import { Provider } from "react-redux";
import createHistory from "history/createBrowserHistory";
@@ -30,10 +33,12 @@ if (!root) {
ReactDOM.render(
<Provider store={store}>
{/* ConnectedRouter will use the store from Provider automatically */}
<ConnectedRouter history={history}>
<App />
</ConnectedRouter>
<I18nextProvider i18n={i18n}>
{/* ConnectedRouter will use the store from Provider automatically */}
<ConnectedRouter history={history}>
<App />
</ConnectedRouter>
</I18nextProvider>
</Provider>,
root
);

View File

@@ -1,32 +1,20 @@
// @flow
import React from 'react';
import { connect } from 'react-redux';
import { fetchRepositoriesIfNeeded } from '../modules/repositories';
import { Link } from 'react-router-dom';
type Props = {
login: boolean,
error: Error,
repositories: any,
fetchRepositoriesIfNeeded: () => void
}
class Repositories extends React.Component<Props> {
componentDidMount() {
this.props.fetchRepositoriesIfNeeded();
}
render() {
const { login, error, repositories } = this.props;
return (
<div>
<h1>SCM</h1>
<h2>Startpage</h2>
<h2>Repositories will be shown here.</h2>
<Link to='/users'>Users hier!</Link>
</div>
)
@@ -36,16 +24,4 @@ class Repositories extends React.Component<Props> {
}
const mapStateToProps = (state) => {
return null;
};
const mapDispatchToProps = (dispatch) => {
return {
fetchRepositoriesIfNeeded: () => {
dispatch(fetchRepositoriesIfNeeded())
}
}
};
export default connect(mapStateToProps, mapDispatchToProps)(Repositories);
export default (Repositories);

View File

@@ -1,58 +0,0 @@
const FETCH_REPOSITORIES = 'scm/repositories/FETCH';
const FETCH_REPOSITORIES_SUCCESS = 'scm/repositories/FETCH_SUCCESS';
const FETCH_REPOSITORIES_FAILURE = 'scm/repositories/FETCH_FAILURE';
function requestRepositories() {
return {
type: FETCH_REPOSITORIES
};
}
function fetchRepositories() {
return function(dispatch) {
dispatch(requestRepositories());
return null;
}
}
export function shouldFetchRepositories(state: any): boolean {
const repositories = state.repositories;
return null;
}
export function fetchRepositoriesIfNeeded() {
return (dispatch, getState) => {
if (shouldFetchRepositories(getState())) {
dispatch(fetchRepositories());
}
}
}
export default function reducer(state = {}, action = {}) {
switch (action.type) {
case FETCH_REPOSITORIES:
return {
...state,
login: true,
error: null
};
case FETCH_REPOSITORIES_SUCCESS:
return {
...state,
login: true,
timestamp: action.timestamp,
error: null,
repositories: action.payload
};
case FETCH_REPOSITORIES_FAILURE:
return {
...state,
login: true,
error: action.payload
};
default:
return state
}
}

View File

@@ -1,4 +0,0 @@
// @flow
export type Me = {
username: string
};

View File

@@ -4,7 +4,7 @@ import { connect } from "react-redux";
import UserForm from "./UserForm";
import type { User } from "../types/User";
import { addUser } from "../modules/users";
import { createUser } from "../modules/users";
type Props = {
addUser: User => void,
@@ -29,7 +29,7 @@ class AddUser extends React.Component<Props> {
const mapDispatchToProps = dispatch => {
return {
addUser: (user: User) => {
dispatch(addUser(user));
dispatch(createUser(user));
}
};
};

View File

@@ -6,7 +6,7 @@ import type { User } from "../types/User";
import type { UserEntry } from "../types/UserEntry";
import Loading from "../../components/Loading";
import { updateUser, fetchUser } from "../modules/users";
import { modifyUser, fetchUser } from "../modules/users";
type Props = {
name: string,
@@ -48,7 +48,7 @@ const mapDispatchToProps = dispatch => {
dispatch(fetchUser(name));
},
updateUser: (user: User) => {
dispatch(updateUser(user));
dispatch(modifyUser(user));
}
};
};

View File

@@ -2,7 +2,6 @@
import React from "react";
import EditButton from "../../components/EditButton";
import type { UserEntry } from "../types/UserEntry";
import { Link } from "react-router-dom";
type Props = {
entry: UserEntry

View File

@@ -4,7 +4,6 @@ import type { User } from "../types/User";
import InputField from "../../components/InputField";
import Checkbox from "../../components/Checkbox";
import SubmitButton from "../../components/SubmitButton";
import { connect } from "react-redux";
type Props = {
submitForm: User => void,

View File

@@ -4,7 +4,6 @@ import { connect } from "react-redux";
import { fetchUsers, deleteUser, getUsersFromState } from "../modules/users";
import Page from "../../components/Page";
import ErrorNotification from "../../components/ErrorNotification";
import UserTable from "./UserTable";
import type { User } from "../types/User";
import AddButton from "../../components/AddButton";

View File

@@ -1,27 +1,24 @@
// @flow
import { apiClient, NOT_FOUND_ERROR } from "../../apiclient";
import { apiClient } from "../../apiclient";
import type { User } from "../types/User";
import type { UserEntry } from "../types/UserEntry";
import { Dispatch } from "redux";
export const FETCH_USERS = "scm/users/FETCH";
export const FETCH_USERS_SUCCESS = "scm/users/FETCH_SUCCESS";
export const FETCH_USERS_FAILURE = "scm/users/FETCH_FAILURE";
export const FETCH_USERS_NOTFOUND = "scm/users/FETCH_NOTFOUND";
export const FETCH_USERS_PENDING = "scm/users/FETCH_USERS_PENDING";
export const FETCH_USERS_SUCCESS = "scm/users/FETCH_USERS_SUCCESS";
export const FETCH_USERS_FAILURE = "scm/users/FETCH_USERS_FAILURE";
export const FETCH_USER = "scm/users/FETCH_USER";
export const FETCH_USER_PENDING = "scm/users/FETCH_USER_PENDING";
export const FETCH_USER_SUCCESS = "scm/users/FETCH_USER_SUCCESS";
export const FETCH_USER_FAILURE = "scm/users/FETCH_USER_FAILURE";
export const ADD_USER = "scm/users/ADD";
export const ADD_USER_SUCCESS = "scm/users/ADD_SUCCESS";
export const ADD_USER_FAILURE = "scm/users/ADD_FAILURE";
export const CREATE_USER_PENDING = "scm/users/CREATE_USER_PENDING";
export const CREATE_USER_SUCCESS = "scm/users/CREATE_USER_SUCCESS";
export const CREATE_USER_FAILURE = "scm/users/CREATE_USER_FAILURE";
export const EDIT_USER = "scm/users/EDIT";
export const UPDATE_USER = "scm/users/UPDATE";
export const UPDATE_USER_SUCCESS = "scm/users/UPDATE_SUCCESS";
export const UPDATE_USER_FAILURE = "scm/users/UPDATE_FAILURE";
export const MODIFY_USER_PENDING = "scm/users/MODIFY_USER_PENDING";
export const MODIFY_USER_SUCCESS = "scm/users/MODIFY_USER_SUCCESS";
export const MODIFY_USER_FAILURE = "scm/users/MODIFY_USER_FAILURE";
export const DELETE_USER = "scm/users/DELETE";
export const DELETE_USER_SUCCESS = "scm/users/DELETE_SUCCESS";
@@ -32,25 +29,11 @@ const USER_URL = "users/";
const CONTENT_TYPE_USER = "application/vnd.scmm-user+json;v=2";
export function requestUsers() {
return {
type: FETCH_USERS
};
}
export function failedToFetchUsers(url: string, error: Error) {
return {
type: FETCH_USERS_FAILURE,
payload: {
error,
url
}
};
}
//fetch users
export function fetchUsers() {
return function(dispatch: any) {
dispatch(requestUsers());
dispatch(fetchUsersPending());
return apiClient
.get(USERS_URL)
.then(response => {
@@ -66,11 +49,17 @@ export function fetchUsers() {
})
.catch(cause => {
const error = new Error(`could not fetch users: ${cause.message}`);
dispatch(failedToFetchUsers(USERS_URL, error));
dispatch(fetchUsersFailure(USERS_URL, error));
});
};
}
export function fetchUsersPending() {
return {
type: FETCH_USERS_PENDING
};
}
export function fetchUsersSuccess(users: any) {
return {
type: FETCH_USERS_SUCCESS,
@@ -78,17 +67,22 @@ export function fetchUsersSuccess(users: any) {
};
}
export function requestUser(name: string) {
export function fetchUsersFailure(url: string, error: Error) {
return {
type: FETCH_USER,
payload: { name }
type: FETCH_USERS_FAILURE,
payload: {
error,
url
}
};
}
//fetch user
//TODO: fetchUsersPending and FetchUsersFailure are the wrong functions here!
export function fetchUser(name: string) {
const userUrl = USER_URL + name;
const userUrl = USERS_URL + "/" + name;
return function(dispatch: any) {
dispatch(requestUsers());
dispatch(fetchUsersPending());
return apiClient
.get(userUrl)
.then(response => {
@@ -104,12 +98,19 @@ export function fetchUser(name: string) {
})
.catch(cause => {
const error = new Error(`could not fetch user: ${cause.message}`);
dispatch(failedToFetchUsers(USERS_URL, error));
dispatch(fetchUsersFailure(USERS_URL, error));
});
};
}
export function fetchUserSuccess(user: User) {
export function fetchUserPending(name: string) {
return {
type: FETCH_USER_PENDING,
payload: { name }
};
}
export function fetchUserSuccess(user: any) {
return {
type: FETCH_USER_SUCCESS,
payload: user
@@ -124,25 +125,20 @@ export function fetchUserFailure(user: User, error: Error) {
};
}
export function requestAddUser(user: User) {
return {
type: ADD_USER,
user
};
}
//create user
export function addUser(user: User) {
export function createUser(user: User) {
return function(dispatch: Dispatch) {
dispatch(requestAddUser(user));
dispatch(createUserPending(user));
return apiClient
.postWithContentType(USERS_URL, user, CONTENT_TYPE_USER)
.then(() => {
dispatch(addUserSuccess());
dispatch(createUserSuccess());
dispatch(fetchUsers());
})
.catch(err =>
dispatch(
addUserFailure(
createUserFailure(
user,
new Error(`failed to add user ${user.name}: ${err.message}`)
)
@@ -151,59 +147,89 @@ export function addUser(user: User) {
};
}
export function addUserSuccess() {
export function createUserPending(user: User) {
return {
type: ADD_USER_SUCCESS
type: CREATE_USER_PENDING,
user
};
}
export function addUserFailure(user: User, err: Error) {
export function createUserSuccess() {
return {
type: ADD_USER_FAILURE,
type: CREATE_USER_SUCCESS
};
}
export function createUserFailure(user: User, err: Error) {
return {
type: CREATE_USER_FAILURE,
payload: err,
user
};
}
function requestUpdateUser(user: User) {
return {
type: UPDATE_USER,
user
};
}
//modify user
export function updateUser(user: User) {
export function modifyUser(user: User) {
return function(dispatch: Dispatch) {
dispatch(requestUpdateUser(user));
dispatch(modifyUserPending(user));
return apiClient
.putWithContentType(user._links.update.href, user, CONTENT_TYPE_USER)
.then(() => {
dispatch(updateUserSuccess(user));
dispatch(modifyUserSuccess(user));
dispatch(fetchUsers());
})
.catch(err => {
console.log(err);
dispatch(updateUserFailure(user, err));
dispatch(modifyUserFailure(user, err));
});
};
}
function updateUserSuccess(user: User) {
function modifyUserPending(user: User) {
return {
type: UPDATE_USER_SUCCESS,
type: MODIFY_USER_PENDING,
user
};
}
export function updateUserFailure(user: User, error: Error) {
function modifyUserSuccess(user: User) {
return {
type: UPDATE_USER_FAILURE,
type: MODIFY_USER_SUCCESS,
user
};
}
export function modifyUserFailure(user: User, error: Error) {
return {
type: MODIFY_USER_FAILURE,
payload: error,
user
};
}
export function requestDeleteUser(user: User) {
//delete user
export function deleteUser(user: User) {
return function(dispatch: any) {
dispatch(deleteUserPending(user));
return apiClient
.delete(user._links.delete.href)
.then(() => {
dispatch(deleteUserSuccess(user));
dispatch(fetchUsers());
})
.catch(cause => {
const error = new Error(
`could not delete user ${user.name}: ${cause.message}`
);
dispatch(deleteUserFailure(user, error));
});
};
}
export function deleteUserPending(user: User) {
return {
type: DELETE_USER,
payload: user
@@ -227,25 +253,9 @@ export function deleteUserFailure(user: User, error: Error) {
};
}
export function deleteUser(user: User) {
return function(dispatch: any) {
dispatch(requestDeleteUser(user));
return apiClient
.delete(user._links.delete.href)
.then(() => {
dispatch(deleteUserSuccess(user));
dispatch(fetchUsers());
})
.catch(cause => {
const error = new Error(
`could not delete user ${user.name}: ${cause.message}`
);
dispatch(deleteUserFailure(user, error));
});
};
}
//helper functions
export function getUsersFromState(state) {
export function getUsersFromState(state: any) {
if (!state.users.users) {
return null;
}
@@ -283,7 +293,7 @@ function extractUsersByNames(
function deleteUserInUsersByNames(users: {}, userName: any) {
let newUsers = {};
for (let username in users) {
if (username != userName) newUsers[username] = users[username];
if (username !== userName) newUsers[username] = users[username];
}
return newUsers;
}
@@ -291,7 +301,7 @@ function deleteUserInUsersByNames(users: {}, userName: any) {
function deleteUserInEntries(users: [], userName: any) {
let newUsers = [];
for (let user of users) {
if (user != userName) newUsers.push(user);
if (user !== userName) newUsers.push(user);
}
return newUsers;
}
@@ -315,7 +325,7 @@ const reduceUsersByNames = (
export default function reducer(state: any = {}, action: any = {}) {
switch (action.type) {
// fetch user list cases
case FETCH_USERS:
case FETCH_USERS_PENDING:
return {
...state,
users: {
@@ -351,7 +361,7 @@ export default function reducer(state: any = {}, action: any = {}) {
}
};
// Fetch single user cases
case FETCH_USER:
case FETCH_USER_PENDING:
return reduceUsersByNames(state, action.payload.name, {
loading: true,
error: null
@@ -415,7 +425,7 @@ export default function reducer(state: any = {}, action: any = {}) {
}
};
// Add single user cases
case ADD_USER:
case CREATE_USER_PENDING:
return {
...state,
users: {
@@ -424,7 +434,7 @@ export default function reducer(state: any = {}, action: any = {}) {
error: null
}
};
case ADD_USER_SUCCESS:
case CREATE_USER_SUCCESS:
return {
...state,
users: {
@@ -433,7 +443,7 @@ export default function reducer(state: any = {}, action: any = {}) {
error: null
}
};
case ADD_USER_FAILURE:
case CREATE_USER_FAILURE:
return {
...state,
users: {
@@ -443,7 +453,7 @@ export default function reducer(state: any = {}, action: any = {}) {
}
};
// Update single user cases
case UPDATE_USER:
case MODIFY_USER_PENDING:
return {
...state,
usersByNames: {
@@ -455,7 +465,7 @@ export default function reducer(state: any = {}, action: any = {}) {
}
}
};
case UPDATE_USER_SUCCESS:
case MODIFY_USER_SUCCESS:
return {
...state,
usersByNames: {

View File

@@ -4,34 +4,33 @@ import thunk from "redux-thunk";
import fetchMock from "fetch-mock";
import {
FETCH_USERS,
FETCH_USERS_PENDING,
FETCH_USERS_SUCCESS,
fetchUsers,
FETCH_USERS_FAILURE,
addUser,
ADD_USER,
ADD_USER_SUCCESS,
ADD_USER_FAILURE,
updateUser,
UPDATE_USER,
UPDATE_USER_FAILURE,
UPDATE_USER_SUCCESS,
EDIT_USER,
requestDeleteUser,
createUserPending,
CREATE_USER_PENDING,
CREATE_USER_SUCCESS,
CREATE_USER_FAILURE,
modifyUser,
MODIFY_USER_PENDING,
MODIFY_USER_FAILURE,
MODIFY_USER_SUCCESS,
deleteUserPending,
deleteUserFailure,
DELETE_USER,
DELETE_USER_SUCCESS,
DELETE_USER_FAILURE,
deleteUser,
requestUsers,
fetchUsersFailure,
fetchUsersSuccess,
requestAddUser,
addUserSuccess,
addUserFailure,
updateUserFailure,
createUser,
createUserSuccess,
createUserFailure,
modifyUserFailure,
deleteUserSuccess,
failedToFetchUsers,
requestUser,
fetchUsersPending,
fetchUserPending,
fetchUserFailure
} from "./users";
@@ -143,7 +142,7 @@ describe("users fetch()", () => {
fetchMock.getOnce("/scm/api/rest/v2/users", response);
const expectedActions = [
{ type: FETCH_USERS },
{ type: FETCH_USERS_PENDING },
{
type: FETCH_USERS_SUCCESS,
payload: response
@@ -165,7 +164,7 @@ describe("users fetch()", () => {
const store = mockStore({});
return store.dispatch(fetchUsers()).then(() => {
const actions = store.getActions();
expect(actions[0].type).toEqual(FETCH_USERS);
expect(actions[0].type).toEqual(FETCH_USERS_PENDING);
expect(actions[1].type).toEqual(FETCH_USERS_FAILURE);
expect(actions[1].payload).toBeDefined();
});
@@ -181,11 +180,11 @@ describe("users fetch()", () => {
fetchMock.getOnce("/scm/api/rest/v2/users", response);
const store = mockStore({});
return store.dispatch(addUser(userZaphod)).then(() => {
return store.dispatch(createUser(userZaphod)).then(() => {
const actions = store.getActions();
expect(actions[0].type).toEqual(ADD_USER);
expect(actions[1].type).toEqual(ADD_USER_SUCCESS);
expect(actions[2].type).toEqual(FETCH_USERS);
expect(actions[0].type).toEqual(CREATE_USER_PENDING);
expect(actions[1].type).toEqual(CREATE_USER_SUCCESS);
expect(actions[2].type).toEqual(FETCH_USERS_PENDING);
});
});
@@ -195,10 +194,10 @@ describe("users fetch()", () => {
});
const store = mockStore({});
return store.dispatch(addUser(userZaphod)).then(() => {
return store.dispatch(createUser(userZaphod)).then(() => {
const actions = store.getActions();
expect(actions[0].type).toEqual(ADD_USER);
expect(actions[1].type).toEqual(ADD_USER_FAILURE);
expect(actions[0].type).toEqual(CREATE_USER_PENDING);
expect(actions[1].type).toEqual(CREATE_USER_FAILURE);
expect(actions[1].payload).toBeDefined();
});
});
@@ -211,11 +210,11 @@ describe("users fetch()", () => {
fetchMock.getOnce("/scm/api/rest/v2/users", response);
const store = mockStore({});
return store.dispatch(updateUser(userZaphod)).then(() => {
return store.dispatch(modifyUser(userZaphod)).then(() => {
const actions = store.getActions();
expect(actions[0].type).toEqual(UPDATE_USER);
expect(actions[1].type).toEqual(UPDATE_USER_SUCCESS);
expect(actions[2].type).toEqual(FETCH_USERS);
expect(actions[0].type).toEqual(MODIFY_USER_PENDING);
expect(actions[1].type).toEqual(MODIFY_USER_SUCCESS);
expect(actions[2].type).toEqual(FETCH_USERS_PENDING);
});
});
@@ -225,10 +224,10 @@ describe("users fetch()", () => {
});
const store = mockStore({});
return store.dispatch(updateUser(userZaphod)).then(() => {
return store.dispatch(modifyUser(userZaphod)).then(() => {
const actions = store.getActions();
expect(actions[0].type).toEqual(UPDATE_USER);
expect(actions[1].type).toEqual(UPDATE_USER_FAILURE);
expect(actions[0].type).toEqual(MODIFY_USER_PENDING);
expect(actions[1].type).toEqual(MODIFY_USER_FAILURE);
expect(actions[1].payload).toBeDefined();
});
});
@@ -246,7 +245,7 @@ describe("users fetch()", () => {
expect(actions[0].type).toEqual(DELETE_USER);
expect(actions[0].payload).toBe(userZaphod);
expect(actions[1].type).toEqual(DELETE_USER_SUCCESS);
expect(actions[2].type).toEqual(FETCH_USERS);
expect(actions[2].type).toEqual(FETCH_USERS_PENDING);
});
});
@@ -267,8 +266,8 @@ describe("users fetch()", () => {
});
describe("users reducer", () => {
it("should update state correctly according to FETCH_USERS action", () => {
const newState = reducer({}, requestUsers());
it("should update state correctly according to FETCH_USERS_PENDING action", () => {
const newState = reducer({}, fetchUsersPending());
expect(newState.users.loading).toBeTruthy();
expect(newState.users.error).toBeFalsy();
});
@@ -304,7 +303,7 @@ describe("users reducer", () => {
}
};
const newState = reducer(state, requestDeleteUser(userZaphod));
const newState = reducer(state, deleteUserPending(userZaphod));
const zaphod = newState.usersByNames["zaphod"];
expect(zaphod.loading).toBeTruthy();
expect(zaphod.entry).toBe(userZaphod);
@@ -324,7 +323,7 @@ describe("users reducer", () => {
}
};
const newState = reducer(state, requestDeleteUser(userZaphod));
const newState = reducer(state, deleteUserPending(userZaphod));
const ford = newState.usersByNames["ford"];
expect(ford.loading).toBeFalsy();
});
@@ -386,14 +385,14 @@ describe("users reducer", () => {
expect(newState.users.userCreatePermission).toBeTruthy();
});
it("should update state correctly according to ADD_USER action", () => {
const newState = reducer({}, requestAddUser(userZaphod));
it("should update state correctly according to CREATE_USER_PENDING action", () => {
const newState = reducer({}, createUserPending(userZaphod));
expect(newState.users.loading).toBeTruthy();
expect(newState.users.error).toBeNull();
});
it("should update state correctly according to ADD_USER_SUCCESS action", () => {
const newState = reducer({ loading: true }, addUserSuccess());
it("should update state correctly according to CREATE_USER_SUCCESS action", () => {
const newState = reducer({ loading: true }, createUserSuccess());
expect(newState.users.loading).toBeFalsy();
expect(newState.users.error).toBeNull();
});
@@ -401,14 +400,14 @@ describe("users reducer", () => {
it("should set the loading to false and the error if user could not be added", () => {
const newState = reducer(
{ loading: true, error: null },
addUserFailure(userFord, new Error("kaputt kaputt"))
createUserFailure(userFord, new Error("kaputt kaputt"))
);
expect(newState.users.loading).toBeFalsy();
expect(newState.users.error).toEqual(new Error("kaputt kaputt"));
});
it("should update state according to FETCH_USER action", () => {
const newState = reducer({}, requestUser("zaphod"));
it("should update state according to FETCH_USER_PENDING action", () => {
const newState = reducer({}, fetchUserPending("zaphod"));
expect(newState.usersByNames["zaphod"].loading).toBeTruthy();
});

View File

@@ -3574,7 +3574,7 @@ hoek@2.x.x:
version "2.16.3"
resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed"
hoist-non-react-statics@^2.5.0:
hoist-non-react-statics@^2.3.1, hoist-non-react-statics@^2.5.0:
version "2.5.5"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz#c5903cf409c0dfd908f388e619d86b9c1174cb47"
@@ -3630,6 +3630,12 @@ html-minifier@^3.2.3:
relateurl "0.2.x"
uglify-js "3.4.x"
html-parse-stringify2@2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/html-parse-stringify2/-/html-parse-stringify2-2.0.1.tgz#dc5670b7292ca158b7bc916c9a6735ac8872834a"
dependencies:
void-elements "^2.0.1"
html-webpack-plugin@2.29.0:
version "2.29.0"
resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-2.29.0.tgz#e987f421853d3b6938c8c4c8171842e5fd17af23"
@@ -3742,6 +3748,24 @@ hyphenate-style-name@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/hyphenate-style-name/-/hyphenate-style-name-1.0.2.tgz#31160a36930adaf1fc04c6074f7eb41465d4ec4b"
i18next-browser-languagedetector@^2.2.2:
version "2.2.2"
resolved "https://registry.yarnpkg.com/i18next-browser-languagedetector/-/i18next-browser-languagedetector-2.2.2.tgz#b2599e3e8bc8b66038010e9758c28222688df6aa"
i18next-fetch-backend@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/i18next-fetch-backend/-/i18next-fetch-backend-0.1.0.tgz#18b67920d0e605e616f93bbdf897e59adf9c9c05"
dependencies:
i18next-xhr-backend "^1.4.3"
i18next-xhr-backend@^1.4.3:
version "1.5.1"
resolved "https://registry.yarnpkg.com/i18next-xhr-backend/-/i18next-xhr-backend-1.5.1.tgz#50282610780c6a696d880dfa7f4ac1d01e8c3ad5"
i18next@^11.4.0:
version "11.4.0"
resolved "https://registry.yarnpkg.com/i18next/-/i18next-11.4.0.tgz#9179bc27b74158d773893356f19b039bedbc355a"
iconv-lite@0.4.19:
version "0.4.19"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b"
@@ -4368,6 +4392,10 @@ jest-environment-node@^20.0.3:
jest-mock "^20.0.3"
jest-util "^20.0.3"
jest-get-type@^22.1.0:
version "22.4.3"
resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-22.4.3.tgz#e3a8504d8479342dd4420236b322869f18900ce4"
jest-haste-map@^20.0.4:
version "20.0.5"
resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-20.0.5.tgz#abad74efb1a005974a7b6517e11010709cab9112"
@@ -4393,6 +4421,15 @@ jest-jasmine2@^20.0.4:
once "^1.4.0"
p-map "^1.1.1"
jest-junit@^5.1.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/jest-junit/-/jest-junit-5.1.0.tgz#e8e497d810a829bf02783125aab74b5df6caa8fe"
dependencies:
jest-validate "^23.0.1"
mkdirp "^0.5.1"
strip-ansi "^4.0.0"
xml "^1.0.1"
jest-matcher-utils@^20.0.3:
version "20.0.3"
resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-20.0.3.tgz#b3a6b8e37ca577803b0832a98b164f44b7815612"
@@ -4491,6 +4528,15 @@ jest-validate@^20.0.3:
leven "^2.1.0"
pretty-format "^20.0.3"
jest-validate@^23.0.1:
version "23.4.0"
resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-23.4.0.tgz#d96eede01ef03ac909c009e9c8e455197d48c201"
dependencies:
chalk "^2.0.1"
jest-get-type "^22.1.0"
leven "^2.1.0"
pretty-format "^23.2.0"
jest@20.0.4:
version "20.0.4"
resolved "https://registry.yarnpkg.com/jest/-/jest-20.0.4.tgz#3dd260c2989d6dad678b1e9cc4d91944f6d602ac"
@@ -6218,6 +6264,13 @@ pretty-format@^20.0.3:
ansi-regex "^2.1.1"
ansi-styles "^3.0.0"
pretty-format@^23.2.0:
version "23.2.0"
resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-23.2.0.tgz#3b0aaa63c018a53583373c1cb3a5d96cc5e83017"
dependencies:
ansi-regex "^3.0.0"
ansi-styles "^3.2.0"
private@^0.1.6, private@^0.1.7, private@^0.1.8:
version "0.1.8"
resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff"
@@ -6435,6 +6488,14 @@ react-error-overlay@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-4.0.0.tgz#d198408a85b4070937a98667f500c832f86bd5d4"
react-i18next@^7.9.0:
version "7.9.0"
resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-7.9.0.tgz#9e4bdfbfbc0d084eddf13d1cd337cbd4beea6232"
dependencies:
hoist-non-react-statics "^2.3.1"
html-parse-stringify2 "2.0.1"
prop-types "^15.6.0"
react-is@^16.4.1:
version "16.4.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.4.1.tgz#d624c4650d2c65dbd52c72622bbf389435d9776e"
@@ -7994,6 +8055,10 @@ vm-browserify@0.0.4:
dependencies:
indexof "0.0.1"
void-elements@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-2.0.1.tgz#c066afb582bb1cb4128d60ea92392e94d5e9dbec"
walker@~1.0.5:
version "1.0.7"
resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.7.tgz#2f7f9b8fd10d677262b18a884e28d19618e028fb"
@@ -8242,6 +8307,10 @@ xml-name-validator@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-2.0.1.tgz#4d8b8f1eccd3419aa362061becef515e1e559635"
xml@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/xml/-/xml-1.0.1.tgz#78ba72020029c5bc87b8a81a3cfcd74b4a2fc1e5"
xtend@^4.0.0:
version "4.0.1"
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af"

View File

@@ -320,7 +320,6 @@
<dependency>
<groupId>com.github.sdorra</groupId>
<artifactId>shiro-unit</artifactId>
<version>1.0.0</version>
<scope>test</scope>
</dependency>

View File

@@ -1,34 +1,52 @@
package sonia.scm.api.v2.resources;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import com.webcohesion.enunciate.metadata.rs.ResponseCode;
import com.webcohesion.enunciate.metadata.rs.StatusCodes;
import com.webcohesion.enunciate.metadata.rs.TypeHint;
import org.apache.shiro.SecurityUtils;
import sonia.scm.user.User;
import sonia.scm.user.UserException;
import sonia.scm.user.UserManager;
import sonia.scm.web.VndMediaType;
import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.Request;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
@Path(MeResource.ME_PATH_V2)
public class MeResource {
static final String ME_PATH_V2 = "v2/me/";
private final UserToUserDtoMapper userToDtoMapper;
private final IdResourceManagerAdapter<User, UserDto, UserException> adapter;
@Inject
public MeResource(UserToUserDtoMapper userToDtoMapper, UserManager manager) {
this.userToDtoMapper = userToDtoMapper;
this.adapter = new IdResourceManagerAdapter<>(manager, User.class);
}
/**
* Returns the currently logged in user or a 401 if user is not logged in
*/
@GET
@Produces(VndMediaType.ME)
public Response get() {
MeDto meDto = new MeDto((String) SecurityUtils.getSubject().getPrincipals().getPrimaryPrincipal());
return Response.ok(meDto).build();
}
@Path("")
@Produces(VndMediaType.USER)
@TypeHint(UserDto.class)
@StatusCodes({
@ResponseCode(code = 200, condition = "success"),
@ResponseCode(code = 401, condition = "not authenticated / invalid credentials"),
@ResponseCode(code = 500, condition = "internal server error")
})
public Response get(@Context Request request, @Context UriInfo uriInfo) {
@NoArgsConstructor
@AllArgsConstructor
@Getter
@Setter
class MeDto {
String username;
String id = (String) SecurityUtils.getSubject().getPrincipals().getPrimaryPrincipal();
return adapter.get(id, userToDtoMapper::map);
}
}

View File

@@ -70,11 +70,6 @@ public class SecurityFilter extends HttpFilter
@VisibleForTesting
static final String ATTRIBUTE_REMOTE_USER = "principal";
/** Field description */
public static final String URL_AUTHENTICATION = "/api/rest/auth";
public static final String URLV2_AUTHENTICATION = "/api/rest/v2/auth";
private final ScmConfiguration configuration;
@Inject

View File

@@ -0,0 +1,103 @@
package sonia.scm.api.v2.resources;
import com.github.sdorra.shiro.ShiroRule;
import com.github.sdorra.shiro.SubjectAware;
import com.google.common.io.Resources;
import org.apache.shiro.authc.credential.PasswordService;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.Subject;
import org.jboss.resteasy.core.Dispatcher;
import org.jboss.resteasy.mock.MockDispatcherFactory;
import org.jboss.resteasy.mock.MockHttpRequest;
import org.jboss.resteasy.mock.MockHttpResponse;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import sonia.scm.PageResult;
import sonia.scm.user.User;
import sonia.scm.user.UserException;
import sonia.scm.user.UserManager;
import sonia.scm.web.VndMediaType;
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.core.UriInfo;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import static java.util.Collections.singletonList;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.*;
import static org.mockito.MockitoAnnotations.initMocks;
@SubjectAware(
// username = "trillian",
// password = "secret",
configuration = "classpath:sonia/scm/repository/shiro.ini"
)
public class MeResourceTest {
@Rule
public ShiroRule shiro = new ShiroRule();
private Dispatcher dispatcher = MockDispatcherFactory.createDispatcher();
@Mock
private UriInfo uriInfo;
@Mock
private UriInfoStore uriInfoStore;
@Mock
private UserManager userManager;
@InjectMocks
private UserToUserDtoMapperImpl userToDtoMapper;
private ArgumentCaptor<User> userCaptor = ArgumentCaptor.forClass(User.class);
@Before
public void prepareEnvironment() throws IOException, UserException {
initMocks(this);
createDummyUser("trillian");
doNothing().when(userManager).create(userCaptor.capture());
doNothing().when(userManager).modify(userCaptor.capture());
doNothing().when(userManager).delete(userCaptor.capture());
MeResource meResource = new MeResource(userToDtoMapper, userManager);
dispatcher.getRegistry().addSingletonResource(meResource);
when(uriInfo.getBaseUri()).thenReturn(URI.create("/"));
when(uriInfoStore.get()).thenReturn(uriInfo);
}
@Test
@SubjectAware(username = "trillian", password = "secret")
public void shouldReturnCurrentlyAuthenticatedUser() throws URISyntaxException {
MockHttpRequest request = MockHttpRequest.get("/" + MeResource.ME_PATH_V2);
request.accept(VndMediaType.USER);
MockHttpResponse response = new MockHttpResponse();
dispatcher.invoke(request, response);
assertEquals(HttpServletResponse.SC_OK, response.getStatus());
assertTrue(response.getContentAsString().contains("\"name\":\"trillian\""));
assertTrue(response.getContentAsString().contains("\"password\":\"__dummypassword__\""));
assertTrue(response.getContentAsString().contains("\"self\":{\"href\":\"/v2/users/trillian\"}"));
assertTrue(response.getContentAsString().contains("\"delete\":{\"href\":\"/v2/users/trillian\"}"));
}
private User createDummyUser(String name) {
User user = new User();
user.setName(name);
user.setPassword("secret");
user.setCreationDate(System.currentTimeMillis());
when(userManager.get(name)).thenReturn(user);
return user;
}
}

View File

@@ -100,14 +100,29 @@ public class SecurityFilterTest {
}
/**
* Tests filter on authentication endpoint.
*
* Tests filter on authentication endpoint v1.
*
* @throws IOException
* @throws ServletException
*/
@Test
public void testDoOnAuthenticationUrl() throws IOException, ServletException {
when(request.getRequestURI()).thenReturn("/scm/api/rest/authentication");
public void testDoOnAuthenticationUrlV1() throws IOException, ServletException {
checkIfAuthenticationUrlIsPassedThrough("/scm/api/rest/auth/access_token");
}
/**
* Tests filter on authentication endpoint v2.
*
* @throws IOException
* @throws ServletException
*/
@Test
public void testDoOnAuthenticationUrlV2() throws IOException, ServletException {
checkIfAuthenticationUrlIsPassedThrough("/scm/api/rest/v2/auth/access_token");
}
private void checkIfAuthenticationUrlIsPassedThrough(String uri) throws IOException, ServletException {
when(request.getRequestURI()).thenReturn(uri);
securityFilter.doFilter(request, response, chain);
verify(request, never()).setAttribute(Mockito.anyString(), Mockito.any());
verify(chain).doFilter(request, response);
@@ -235,4 +250,4 @@ public class SecurityFilterTest {
}
}
}

View File

@@ -12,5 +12,10 @@
<orderEntry type="library" scope="TEST" name="Maven: org.hamcrest:hamcrest-core:1.3" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: org.hamcrest:hamcrest-library:1.3" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: org.mockito:mockito-all:1.10.19" level="project" />
<orderEntry type="library" name="Maven: com.github.cloudogu:ces-build-lib:9aadeeb" level="project" />
<orderEntry type="library" name="Maven: com.cloudbees:groovy-cps:1.21" level="project" />
<orderEntry type="library" name="Maven: com.google.guava:guava:11.0.1" level="project" />
<orderEntry type="library" name="Maven: com.google.code.findbugs:jsr305:1.3.9" level="project" />
<orderEntry type="library" name="Maven: org.codehaus.groovy:groovy-all:2.4.11" level="project" />
</component>
</module>