diff --git a/Jenkinsfile b/Jenkinsfile index 4d11767e67..21e03f6d0a 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -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') { diff --git a/pom.xml b/pom.xml index 1f33be2b6c..e66e355d59 100644 --- a/pom.xml +++ b/pom.xml @@ -158,6 +158,19 @@ + + + + + com.github.sdorra + shiro-unit + 1.0.1 + test + + + + + diff --git a/scm-core/pom.xml b/scm-core/pom.xml index a06eb544dc..d80ff3bcaa 100644 --- a/scm-core/pom.xml +++ b/scm-core/pom.xml @@ -143,11 +143,10 @@ - + com.github.sdorra shiro-unit - 1.0.0 test diff --git a/scm-test/pom.xml b/scm-test/pom.xml index 94948a830a..086552e864 100644 --- a/scm-test/pom.xml +++ b/scm-test/pom.xml @@ -33,11 +33,11 @@ junit ${junit.version} - + com.github.sdorra shiro-unit - 1.0.0 + test diff --git a/scm-ui/package.json b/scm-ui/package.json index 7269bac4ec..c9d966b99b 100644 --- a/scm-ui/package.json +++ b/scm-ui/package.json @@ -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" } } diff --git a/scm-ui/pom.xml b/scm-ui/pom.xml index b7595d3d76..58c935ad21 100644 --- a/scm-ui/pom.xml +++ b/scm-ui/pom.xml @@ -16,6 +16,15 @@ 2.0.0-SNAPSHOT scm-ui + + js + src + **/*.test.js,src/tests/** + **/*.test.js,src/tests/** + target/jest-reports + target/jest-reports/coverage/lcov.info + + @@ -29,7 +38,7 @@ YARN - 1.3.2 + 1.7.0 @@ -51,10 +60,19 @@ + + test + test + + run + + + + + - diff --git a/scm-ui/public/locales/en/commons.json b/scm-ui/public/locales/en/commons.json new file mode 100644 index 0000000000..f436658763 --- /dev/null +++ b/scm-ui/public/locales/en/commons.json @@ -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" + } +} diff --git a/scm-ui/src/components/ErrorNotification.js b/scm-ui/src/components/ErrorNotification.js index d90caf22a2..9ef3b58653 100644 --- a/scm-ui/src/components/ErrorNotification.js +++ b/scm-ui/src/components/ErrorNotification.js @@ -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 { render() { - const { error } = this.props; + const { t, error } = this.props; if (error) { return ( - Error: {error.message} + {t("error-notification.prefix")}: {error.message} ); } @@ -20,4 +22,4 @@ class ErrorNotification extends React.Component { } } -export default ErrorNotification; +export default translate("commons")(ErrorNotification); diff --git a/scm-ui/src/components/Footer.js b/scm-ui/src/components/Footer.js index fd17c06d8b..db051ef791 100644 --- a/scm-ui/src/components/Footer.js +++ b/scm-ui/src/components/Footer.js @@ -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 { @@ -15,7 +14,7 @@ class Footer extends React.Component { return (
-

{me.username}

+

{me}

); diff --git a/scm-ui/src/components/Loading.js b/scm-ui/src/components/Loading.js index d9b370b40f..88fe427941 100644 --- a/scm-ui/src/components/Loading.js +++ b/scm-ui/src/components/Loading.js @@ -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 { render() { - const { classes } = this.props; + const { t, classes } = this.props; return (
- Loading ... + {t("loading.alt")}
); } } -export default injectSheet(styles)(Loading); +export default injectSheet(styles)(translate("commons")(Loading)); diff --git a/scm-ui/src/components/Logo.js b/scm-ui/src/components/Logo.js index 3ffdb3139e..8dac21309f 100644 --- a/scm-ui/src/components/Logo.js +++ b/scm-ui/src/components/Logo.js @@ -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 { render() { - return SCM-Manager logo; + const { t } = this.props; + return {t("logo.alt")}; } } -export default Logo; +export default translate("commons")(Logo); diff --git a/scm-ui/src/components/PrimaryNavigation.js b/scm-ui/src/components/PrimaryNavigation.js index 3eda2a2937..83be320395 100644 --- a/scm-ui/src/components/PrimaryNavigation.js +++ b/scm-ui/src/components/PrimaryNavigation.js @@ -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 { render() { + const { t } = this.props; return ( ); } } -export default PrimaryNavigation; +export default translate("commons")(PrimaryNavigation); diff --git a/scm-ui/src/containers/App.js b/scm-ui/src/containers/App.js index 4ff9b245f7..47b8b27fb3 100644 --- a/scm-ui/src/containers/App.js +++ b/scm-ui/src/containers/App.js @@ -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 { } render() { - const { entry, loading, error, authenticated } = this.props; + const { loading, error, authenticated, displayName, t } = this.props; let content; const navigation = authenticated ? : ""; @@ -36,8 +39,8 @@ class App extends Component { } else if (error) { content = ( ); @@ -48,7 +51,7 @@ class App extends Component {
{navigation}
{content} -
+
); } @@ -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)) ); diff --git a/scm-ui/src/containers/Login.js b/scm-ui/src/containers/Login.js index c8e3775d44..d661ef2b19 100644 --- a/scm-ui/src/containers/Login.js +++ b/scm-ui/src/containers/Login.js @@ -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 { }; 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 {
-

Login

-

Please login to proceed.

+

{t("login.title")}

+

{t("login.subtitle")}

SCM-Manager
string, loading: boolean, authenticated: boolean, error?: Error, @@ -20,13 +22,13 @@ class Logout extends React.Component { } render() { - const { authenticated, loading, error } = this.props; + const { authenticated, loading, t, error } = this.props; // TODO logout is called twice if (error) { return ( ); @@ -53,4 +55,4 @@ const mapDispatchToProps = dispatch => { export default connect( mapStateToProps, mapDispatchToProps -)(Logout); +)(translate("commons")(Logout)); diff --git a/scm-ui/src/createReduxStore.js b/scm-ui/src/createReduxStore.js index bedddc908e..e8de5d844a 100644 --- a/scm-ui/src/createReduxStore.js +++ b/scm-ui/src/createReduxStore.js @@ -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 }); diff --git a/scm-ui/src/i18n.js b/scm-ui/src/i18n.js new file mode 100644 index 0000000000..954d47c605 --- /dev/null +++ b/scm-ui/src/i18n.js @@ -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; diff --git a/scm-ui/src/index.js b/scm-ui/src/index.js index 564249494a..ea133dd55b 100644 --- a/scm-ui/src/index.js +++ b/scm-ui/src/index.js @@ -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( - {/* ConnectedRouter will use the store from Provider automatically */} - - - + + {/* ConnectedRouter will use the store from Provider automatically */} + + + + , root ); diff --git a/scm-ui/src/repositories/containers/Repositories.js b/scm-ui/src/repositories/containers/Repositories.js index b1f01c5b5b..be87b4c85d 100644 --- a/scm-ui/src/repositories/containers/Repositories.js +++ b/scm-ui/src/repositories/containers/Repositories.js @@ -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 { - componentDidMount() { - this.props.fetchRepositoriesIfNeeded(); - } - render() { - const { login, error, repositories } = this.props; - return (

SCM

-

Startpage

+

Repositories will be shown here.

Users hier!
) @@ -36,16 +24,4 @@ class Repositories extends React.Component { } -const mapStateToProps = (state) => { - return null; -}; - -const mapDispatchToProps = (dispatch) => { - return { - fetchRepositoriesIfNeeded: () => { - dispatch(fetchRepositoriesIfNeeded()) - } - } -}; - -export default connect(mapStateToProps, mapDispatchToProps)(Repositories); +export default (Repositories); diff --git a/scm-ui/src/repositories/modules/repositories.js b/scm-ui/src/repositories/modules/repositories.js deleted file mode 100644 index e4ab290a44..0000000000 --- a/scm-ui/src/repositories/modules/repositories.js +++ /dev/null @@ -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 - } -} diff --git a/scm-ui/src/types/me.js b/scm-ui/src/types/me.js deleted file mode 100644 index d17798bf5f..0000000000 --- a/scm-ui/src/types/me.js +++ /dev/null @@ -1,4 +0,0 @@ -// @flow -export type Me = { - username: string -}; diff --git a/scm-ui/src/users/containers/AddUser.js b/scm-ui/src/users/containers/AddUser.js index 96e93cff04..e39fb5054e 100644 --- a/scm-ui/src/users/containers/AddUser.js +++ b/scm-ui/src/users/containers/AddUser.js @@ -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 { const mapDispatchToProps = dispatch => { return { addUser: (user: User) => { - dispatch(addUser(user)); + dispatch(createUser(user)); } }; }; diff --git a/scm-ui/src/users/containers/EditUser.js b/scm-ui/src/users/containers/EditUser.js index 5b7769e859..ba1b83c83e 100644 --- a/scm-ui/src/users/containers/EditUser.js +++ b/scm-ui/src/users/containers/EditUser.js @@ -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)); } }; }; diff --git a/scm-ui/src/users/containers/EditUserButton.js b/scm-ui/src/users/containers/EditUserButton.js index cb2261ba5e..c7a0a736a0 100644 --- a/scm-ui/src/users/containers/EditUserButton.js +++ b/scm-ui/src/users/containers/EditUserButton.js @@ -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 diff --git a/scm-ui/src/users/containers/UserForm.js b/scm-ui/src/users/containers/UserForm.js index c4075be686..8389c98ad2 100644 --- a/scm-ui/src/users/containers/UserForm.js +++ b/scm-ui/src/users/containers/UserForm.js @@ -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, diff --git a/scm-ui/src/users/containers/Users.js b/scm-ui/src/users/containers/Users.js index 13a4616c5f..9b39cdf6f4 100644 --- a/scm-ui/src/users/containers/Users.js +++ b/scm-ui/src/users/containers/Users.js @@ -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"; diff --git a/scm-ui/src/users/modules/users.js b/scm-ui/src/users/modules/users.js index 374f9210d5..1d6f186c14 100644 --- a/scm-ui/src/users/modules/users.js +++ b/scm-ui/src/users/modules/users.js @@ -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: { diff --git a/scm-ui/src/users/modules/users.test.js b/scm-ui/src/users/modules/users.test.js index aef072da0e..8683a60306 100644 --- a/scm-ui/src/users/modules/users.test.js +++ b/scm-ui/src/users/modules/users.test.js @@ -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(); }); diff --git a/scm-ui/yarn.lock b/scm-ui/yarn.lock index fe4f475a0f..ff76d0efc1 100644 --- a/scm-ui/yarn.lock +++ b/scm-ui/yarn.lock @@ -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" diff --git a/scm-webapp/pom.xml b/scm-webapp/pom.xml index d02db4d203..054ce3d445 100644 --- a/scm-webapp/pom.xml +++ b/scm-webapp/pom.xml @@ -320,7 +320,6 @@ com.github.sdorra shiro-unit - 1.0.0 test diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MeResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MeResource.java index 5646ca60cf..50025d3cdb 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MeResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MeResource.java @@ -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 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); } - } diff --git a/scm-webapp/src/main/java/sonia/scm/filter/SecurityFilter.java b/scm-webapp/src/main/java/sonia/scm/filter/SecurityFilter.java index 965e097f64..01967afd00 100644 --- a/scm-webapp/src/main/java/sonia/scm/filter/SecurityFilter.java +++ b/scm-webapp/src/main/java/sonia/scm/filter/SecurityFilter.java @@ -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 diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MeResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MeResourceTest.java new file mode 100644 index 0000000000..8c08a433f5 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MeResourceTest.java @@ -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 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; + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/filter/SecurityFilterTest.java b/scm-webapp/src/test/java/sonia/scm/filter/SecurityFilterTest.java index 13333cc223..983f8d964a 100644 --- a/scm-webapp/src/test/java/sonia/scm/filter/SecurityFilterTest.java +++ b/scm-webapp/src/test/java/sonia/scm/filter/SecurityFilterTest.java @@ -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 { } -} \ No newline at end of file +} diff --git a/scm.iml b/scm.iml index a0d516ed9a..4ed0077c7f 100644 --- a/scm.iml +++ b/scm.iml @@ -12,5 +12,10 @@ + + + + + \ No newline at end of file