Merged trivial modules

This commit is contained in:
Antoine Gersant 2022-11-09 00:39:48 -08:00
parent 2873f38e04
commit 822f3ed073
4 changed files with 255 additions and 260 deletions

View file

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

View file

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

View file

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

View file

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