Merged trivial modules
This commit is contained in:
parent
2873f38e04
commit
822f3ed073
4 changed files with 255 additions and 260 deletions
src/app
263
src/app/user.rs
263
src/app/user.rs
|
@ -9,13 +9,29 @@ use std::time::{SystemTime, UNIX_EPOCH};
|
|||
use crate::app::settings::AuthSecret;
|
||||
use crate::db::{users, DB};
|
||||
|
||||
mod error;
|
||||
mod preferences;
|
||||
#[cfg(test)]
|
||||
mod test;
|
||||
#[derive(thiserror::Error, Debug, PartialEq, Eq)]
|
||||
pub enum Error {
|
||||
#[error("Cannot use empty username")]
|
||||
EmptyUsername,
|
||||
#[error("Cannot use empty password")]
|
||||
EmptyPassword,
|
||||
#[error("Username does not exist")]
|
||||
IncorrectUsername,
|
||||
#[error("Password does not match username")]
|
||||
IncorrectPassword,
|
||||
#[error("Invalid auth token")]
|
||||
InvalidAuthToken,
|
||||
#[error("Incorrect authorization scope")]
|
||||
IncorrectAuthorizationScope,
|
||||
#[error("Unspecified")]
|
||||
Unspecified,
|
||||
}
|
||||
|
||||
pub use error::*;
|
||||
pub use preferences::*;
|
||||
impl From<anyhow::Error> for Error {
|
||||
fn from(_: anyhow::Error) -> Self {
|
||||
Error::Unspecified
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Insertable, Queryable)]
|
||||
#[diesel(table_name = users)]
|
||||
|
@ -53,10 +69,16 @@ pub struct Authorization {
|
|||
pub scope: AuthorizationScope,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Preferences {
|
||||
pub lastfm_username: Option<String>,
|
||||
pub web_theme_base: Option<String>,
|
||||
pub web_theme_accent: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Manager {
|
||||
// TODO make this private and move preferences methods in this file
|
||||
pub db: DB,
|
||||
db: DB,
|
||||
auth_secret: AuthSecret,
|
||||
}
|
||||
|
||||
|
@ -226,6 +248,38 @@ impl Manager {
|
|||
Ok(is_admin != 0)
|
||||
}
|
||||
|
||||
pub fn read_preferences(&self, username: &str) -> Result<Preferences, Error> {
|
||||
use crate::db::users::dsl::*;
|
||||
let mut connection = self.db.connect()?;
|
||||
let (theme_base, theme_accent, read_lastfm_username) = users
|
||||
.select((web_theme_base, web_theme_accent, lastfm_username))
|
||||
.filter(name.eq(username))
|
||||
.get_result(&mut connection)
|
||||
.map_err(|_| Error::Unspecified)?;
|
||||
Ok(Preferences {
|
||||
web_theme_base: theme_base,
|
||||
web_theme_accent: theme_accent,
|
||||
lastfm_username: read_lastfm_username,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn write_preferences(
|
||||
&self,
|
||||
username: &str,
|
||||
preferences: &Preferences,
|
||||
) -> Result<(), Error> {
|
||||
use crate::db::users::dsl::*;
|
||||
let mut connection = self.db.connect()?;
|
||||
diesel::update(users.filter(name.eq(username)))
|
||||
.set((
|
||||
web_theme_base.eq(&preferences.web_theme_base),
|
||||
web_theme_accent.eq(&preferences.web_theme_accent),
|
||||
))
|
||||
.execute(&mut connection)
|
||||
.map_err(|_| Error::Unspecified)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn lastfm_link(
|
||||
&self,
|
||||
username: &str,
|
||||
|
@ -298,3 +352,196 @@ fn verify_password(password_hash: &str, attempted_password: &str) -> bool {
|
|||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::app::test;
|
||||
use crate::test_name;
|
||||
|
||||
const TEST_USERNAME: &str = "Walter";
|
||||
const TEST_PASSWORD: &str = "super_secret!";
|
||||
|
||||
#[test]
|
||||
fn create_delete_user_golden_path() {
|
||||
let ctx = test::ContextBuilder::new(test_name!()).build();
|
||||
|
||||
let new_user = NewUser {
|
||||
name: TEST_USERNAME.to_owned(),
|
||||
password: TEST_PASSWORD.to_owned(),
|
||||
admin: false,
|
||||
};
|
||||
|
||||
ctx.user_manager.create(&new_user).unwrap();
|
||||
assert_eq!(ctx.user_manager.list().unwrap().len(), 1);
|
||||
|
||||
ctx.user_manager.delete(&new_user.name).unwrap();
|
||||
assert_eq!(ctx.user_manager.list().unwrap().len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cannot_create_user_with_blank_username() {
|
||||
let ctx = test::ContextBuilder::new(test_name!()).build();
|
||||
let new_user = NewUser {
|
||||
name: "".to_owned(),
|
||||
password: TEST_PASSWORD.to_owned(),
|
||||
admin: false,
|
||||
};
|
||||
assert_eq!(
|
||||
ctx.user_manager.create(&new_user).unwrap_err(),
|
||||
Error::EmptyUsername
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cannot_create_user_with_blank_password() {
|
||||
let ctx = test::ContextBuilder::new(test_name!()).build();
|
||||
let new_user = NewUser {
|
||||
name: TEST_USERNAME.to_owned(),
|
||||
password: "".to_owned(),
|
||||
admin: false,
|
||||
};
|
||||
assert_eq!(
|
||||
ctx.user_manager.create(&new_user).unwrap_err(),
|
||||
Error::EmptyPassword
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cannot_create_duplicate_user() {
|
||||
let ctx = test::ContextBuilder::new(test_name!()).build();
|
||||
let new_user = NewUser {
|
||||
name: TEST_USERNAME.to_owned(),
|
||||
password: TEST_PASSWORD.to_owned(),
|
||||
admin: false,
|
||||
};
|
||||
ctx.user_manager.create(&new_user).unwrap();
|
||||
ctx.user_manager.create(&new_user).unwrap_err();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_read_write_preferences() {
|
||||
let ctx = test::ContextBuilder::new(test_name!()).build();
|
||||
|
||||
let new_preferences = Preferences {
|
||||
web_theme_base: Some("very-dark-theme".to_owned()),
|
||||
web_theme_accent: Some("#FF0000".to_owned()),
|
||||
lastfm_username: None,
|
||||
};
|
||||
|
||||
let new_user = NewUser {
|
||||
name: TEST_USERNAME.to_owned(),
|
||||
password: TEST_PASSWORD.to_owned(),
|
||||
admin: false,
|
||||
};
|
||||
ctx.user_manager.create(&new_user).unwrap();
|
||||
|
||||
ctx.user_manager
|
||||
.write_preferences(TEST_USERNAME, &new_preferences)
|
||||
.unwrap();
|
||||
|
||||
let read_preferences = ctx.user_manager.read_preferences("Walter").unwrap();
|
||||
assert_eq!(new_preferences, read_preferences);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn login_rejects_bad_password() {
|
||||
let ctx = test::ContextBuilder::new(test_name!()).build();
|
||||
|
||||
let new_user = NewUser {
|
||||
name: TEST_USERNAME.to_owned(),
|
||||
password: TEST_PASSWORD.to_owned(),
|
||||
admin: false,
|
||||
};
|
||||
|
||||
ctx.user_manager.create(&new_user).unwrap();
|
||||
assert_eq!(
|
||||
ctx.user_manager
|
||||
.login(TEST_USERNAME, "not the password")
|
||||
.unwrap_err(),
|
||||
Error::IncorrectPassword
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn login_golden_path() {
|
||||
let ctx = test::ContextBuilder::new(test_name!()).build();
|
||||
let new_user = NewUser {
|
||||
name: TEST_USERNAME.to_owned(),
|
||||
password: TEST_PASSWORD.to_owned(),
|
||||
admin: false,
|
||||
};
|
||||
ctx.user_manager.create(&new_user).unwrap();
|
||||
assert!(ctx.user_manager.login(TEST_USERNAME, TEST_PASSWORD).is_ok())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn authenticate_rejects_bad_token() {
|
||||
let ctx = test::ContextBuilder::new(test_name!()).build();
|
||||
|
||||
let new_user = NewUser {
|
||||
name: TEST_USERNAME.to_owned(),
|
||||
password: TEST_PASSWORD.to_owned(),
|
||||
admin: false,
|
||||
};
|
||||
|
||||
ctx.user_manager.create(&new_user).unwrap();
|
||||
let fake_token = AuthToken("fake token".to_owned());
|
||||
assert!(ctx
|
||||
.user_manager
|
||||
.authenticate(&fake_token, AuthorizationScope::PolarisAuth)
|
||||
.is_err())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn authenticate_golden_path() {
|
||||
let ctx = test::ContextBuilder::new(test_name!()).build();
|
||||
|
||||
let new_user = NewUser {
|
||||
name: TEST_USERNAME.to_owned(),
|
||||
password: TEST_PASSWORD.to_owned(),
|
||||
admin: false,
|
||||
};
|
||||
|
||||
ctx.user_manager.create(&new_user).unwrap();
|
||||
let token = ctx
|
||||
.user_manager
|
||||
.login(TEST_USERNAME, TEST_PASSWORD)
|
||||
.unwrap();
|
||||
let authorization = ctx
|
||||
.user_manager
|
||||
.authenticate(&token, AuthorizationScope::PolarisAuth)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
authorization,
|
||||
Authorization {
|
||||
username: TEST_USERNAME.to_owned(),
|
||||
scope: AuthorizationScope::PolarisAuth,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn authenticate_validates_scope() {
|
||||
let ctx = test::ContextBuilder::new(test_name!()).build();
|
||||
|
||||
let new_user = NewUser {
|
||||
name: TEST_USERNAME.to_owned(),
|
||||
password: TEST_PASSWORD.to_owned(),
|
||||
admin: false,
|
||||
};
|
||||
|
||||
ctx.user_manager.create(&new_user).unwrap();
|
||||
let token = ctx
|
||||
.user_manager
|
||||
.generate_lastfm_link_token(TEST_USERNAME)
|
||||
.unwrap();
|
||||
let authorization = ctx
|
||||
.user_manager
|
||||
.authenticate(&token, AuthorizationScope::PolarisAuth);
|
||||
assert_eq!(
|
||||
authorization.unwrap_err(),
|
||||
Error::IncorrectAuthorizationScope
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,23 +0,0 @@
|
|||
#[derive(thiserror::Error, Debug, PartialEq, Eq)]
|
||||
pub enum Error {
|
||||
#[error("Cannot use empty username")]
|
||||
EmptyUsername,
|
||||
#[error("Cannot use empty password")]
|
||||
EmptyPassword,
|
||||
#[error("Username does not exist")]
|
||||
IncorrectUsername,
|
||||
#[error("Password does not match username")]
|
||||
IncorrectPassword,
|
||||
#[error("Invalid auth token")]
|
||||
InvalidAuthToken,
|
||||
#[error("Incorrect authorization scope")]
|
||||
IncorrectAuthorizationScope,
|
||||
#[error("Unspecified")]
|
||||
Unspecified,
|
||||
}
|
||||
|
||||
impl From<anyhow::Error> for Error {
|
||||
fn from(_: anyhow::Error) -> Self {
|
||||
Error::Unspecified
|
||||
}
|
||||
}
|
|
@ -1,40 +0,0 @@
|
|||
use anyhow::Result;
|
||||
use diesel::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Preferences {
|
||||
pub lastfm_username: Option<String>,
|
||||
pub web_theme_base: Option<String>,
|
||||
pub web_theme_accent: Option<String>,
|
||||
}
|
||||
|
||||
impl Manager {
|
||||
pub fn read_preferences(&self, username: &str) -> Result<Preferences> {
|
||||
use self::users::dsl::*;
|
||||
let mut connection = self.db.connect()?;
|
||||
let (theme_base, theme_accent, read_lastfm_username) = users
|
||||
.select((web_theme_base, web_theme_accent, lastfm_username))
|
||||
.filter(name.eq(username))
|
||||
.get_result(&mut connection)?;
|
||||
Ok(Preferences {
|
||||
web_theme_base: theme_base,
|
||||
web_theme_accent: theme_accent,
|
||||
lastfm_username: read_lastfm_username,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn write_preferences(&self, username: &str, preferences: &Preferences) -> Result<()> {
|
||||
use crate::db::users::dsl::*;
|
||||
let mut connection = self.db.connect()?;
|
||||
diesel::update(users.filter(name.eq(username)))
|
||||
.set((
|
||||
web_theme_base.eq(&preferences.web_theme_base),
|
||||
web_theme_accent.eq(&preferences.web_theme_accent),
|
||||
))
|
||||
.execute(&mut connection)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -1,189 +0,0 @@
|
|||
use super::*;
|
||||
use crate::app::test;
|
||||
use crate::test_name;
|
||||
|
||||
const TEST_USERNAME: &str = "Walter";
|
||||
const TEST_PASSWORD: &str = "super_secret!";
|
||||
|
||||
#[test]
|
||||
fn create_delete_user_golden_path() {
|
||||
let ctx = test::ContextBuilder::new(test_name!()).build();
|
||||
|
||||
let new_user = NewUser {
|
||||
name: TEST_USERNAME.to_owned(),
|
||||
password: TEST_PASSWORD.to_owned(),
|
||||
admin: false,
|
||||
};
|
||||
|
||||
ctx.user_manager.create(&new_user).unwrap();
|
||||
assert_eq!(ctx.user_manager.list().unwrap().len(), 1);
|
||||
|
||||
ctx.user_manager.delete(&new_user.name).unwrap();
|
||||
assert_eq!(ctx.user_manager.list().unwrap().len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cannot_create_user_with_blank_username() {
|
||||
let ctx = test::ContextBuilder::new(test_name!()).build();
|
||||
let new_user = NewUser {
|
||||
name: "".to_owned(),
|
||||
password: TEST_PASSWORD.to_owned(),
|
||||
admin: false,
|
||||
};
|
||||
assert_eq!(
|
||||
ctx.user_manager.create(&new_user).unwrap_err(),
|
||||
Error::EmptyUsername
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cannot_create_user_with_blank_password() {
|
||||
let ctx = test::ContextBuilder::new(test_name!()).build();
|
||||
let new_user = NewUser {
|
||||
name: TEST_USERNAME.to_owned(),
|
||||
password: "".to_owned(),
|
||||
admin: false,
|
||||
};
|
||||
assert_eq!(
|
||||
ctx.user_manager.create(&new_user).unwrap_err(),
|
||||
Error::EmptyPassword
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cannot_create_duplicate_user() {
|
||||
let ctx = test::ContextBuilder::new(test_name!()).build();
|
||||
let new_user = NewUser {
|
||||
name: TEST_USERNAME.to_owned(),
|
||||
password: TEST_PASSWORD.to_owned(),
|
||||
admin: false,
|
||||
};
|
||||
ctx.user_manager.create(&new_user).unwrap();
|
||||
ctx.user_manager.create(&new_user).unwrap_err();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_read_write_preferences() {
|
||||
let ctx = test::ContextBuilder::new(test_name!()).build();
|
||||
|
||||
let new_preferences = Preferences {
|
||||
web_theme_base: Some("very-dark-theme".to_owned()),
|
||||
web_theme_accent: Some("#FF0000".to_owned()),
|
||||
lastfm_username: None,
|
||||
};
|
||||
|
||||
let new_user = NewUser {
|
||||
name: TEST_USERNAME.to_owned(),
|
||||
password: TEST_PASSWORD.to_owned(),
|
||||
admin: false,
|
||||
};
|
||||
ctx.user_manager.create(&new_user).unwrap();
|
||||
|
||||
ctx.user_manager
|
||||
.write_preferences(TEST_USERNAME, &new_preferences)
|
||||
.unwrap();
|
||||
|
||||
let read_preferences = ctx.user_manager.read_preferences("Walter").unwrap();
|
||||
assert_eq!(new_preferences, read_preferences);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn login_rejects_bad_password() {
|
||||
let ctx = test::ContextBuilder::new(test_name!()).build();
|
||||
|
||||
let new_user = NewUser {
|
||||
name: TEST_USERNAME.to_owned(),
|
||||
password: TEST_PASSWORD.to_owned(),
|
||||
admin: false,
|
||||
};
|
||||
|
||||
ctx.user_manager.create(&new_user).unwrap();
|
||||
assert_eq!(
|
||||
ctx.user_manager
|
||||
.login(TEST_USERNAME, "not the password")
|
||||
.unwrap_err(),
|
||||
Error::IncorrectPassword
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn login_golden_path() {
|
||||
let ctx = test::ContextBuilder::new(test_name!()).build();
|
||||
let new_user = NewUser {
|
||||
name: TEST_USERNAME.to_owned(),
|
||||
password: TEST_PASSWORD.to_owned(),
|
||||
admin: false,
|
||||
};
|
||||
ctx.user_manager.create(&new_user).unwrap();
|
||||
assert!(ctx.user_manager.login(TEST_USERNAME, TEST_PASSWORD).is_ok())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn authenticate_rejects_bad_token() {
|
||||
let ctx = test::ContextBuilder::new(test_name!()).build();
|
||||
|
||||
let new_user = NewUser {
|
||||
name: TEST_USERNAME.to_owned(),
|
||||
password: TEST_PASSWORD.to_owned(),
|
||||
admin: false,
|
||||
};
|
||||
|
||||
ctx.user_manager.create(&new_user).unwrap();
|
||||
let fake_token = AuthToken("fake token".to_owned());
|
||||
assert!(ctx
|
||||
.user_manager
|
||||
.authenticate(&fake_token, AuthorizationScope::PolarisAuth)
|
||||
.is_err())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn authenticate_golden_path() {
|
||||
let ctx = test::ContextBuilder::new(test_name!()).build();
|
||||
|
||||
let new_user = NewUser {
|
||||
name: TEST_USERNAME.to_owned(),
|
||||
password: TEST_PASSWORD.to_owned(),
|
||||
admin: false,
|
||||
};
|
||||
|
||||
ctx.user_manager.create(&new_user).unwrap();
|
||||
let token = ctx
|
||||
.user_manager
|
||||
.login(TEST_USERNAME, TEST_PASSWORD)
|
||||
.unwrap();
|
||||
let authorization = ctx
|
||||
.user_manager
|
||||
.authenticate(&token, AuthorizationScope::PolarisAuth)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
authorization,
|
||||
Authorization {
|
||||
username: TEST_USERNAME.to_owned(),
|
||||
scope: AuthorizationScope::PolarisAuth,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn authenticate_validates_scope() {
|
||||
let ctx = test::ContextBuilder::new(test_name!()).build();
|
||||
|
||||
let new_user = NewUser {
|
||||
name: TEST_USERNAME.to_owned(),
|
||||
password: TEST_PASSWORD.to_owned(),
|
||||
admin: false,
|
||||
};
|
||||
|
||||
ctx.user_manager.create(&new_user).unwrap();
|
||||
let token = ctx
|
||||
.user_manager
|
||||
.generate_lastfm_link_token(TEST_USERNAME)
|
||||
.unwrap();
|
||||
let authorization = ctx
|
||||
.user_manager
|
||||
.authenticate(&token, AuthorizationScope::PolarisAuth);
|
||||
assert_eq!(
|
||||
authorization.unwrap_err(),
|
||||
Error::IncorrectAuthorizationScope
|
||||
)
|
||||
}
|
Loading…
Add table
Reference in a new issue