Skip to content

Commit

Permalink
Merge pull request #78 from ubuntu/cucumber-tests
Browse files Browse the repository at this point in the history
Migrate tests to cucumber
  • Loading branch information
ZoopOTheGoop authored Feb 22, 2024
2 parents c69f383 + ef22e14 commit 843aff5
Show file tree
Hide file tree
Showing 30 changed files with 1,507 additions and 1,370 deletions.
620 changes: 578 additions & 42 deletions Cargo.lock

Large diffs are not rendered by default.

15 changes: 15 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,18 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] }

[build-dependencies]
tonic-build = { version = "0.11", features = ["prost"] }

[dev-dependencies]
cucumber = { version = "0.20.2", features = ["libtest", "tracing"] }

[[test]]
name = "voting"
harness = false

[[test]]
name = "authentication"
harness = false

[[test]]
name = "chart"
harness = false
45 changes: 43 additions & 2 deletions src/features/common/entities.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@ const INSUFFICIENT_VOTES_QUANTITY: i64 = 25;

/// A descriptive mapping of a number of ratings to a general indicator of "how good"
/// an app can be said to be.
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[allow(missing_docs)]
pub enum RatingsBand {
VeryGood = 0,
Good = 1,
Neutral = 2,
Poor = 3,
VeryPoor = 4,
#[default]
InsufficientVotes = 5,
}

Expand Down Expand Up @@ -45,10 +47,37 @@ impl RatingsBand {
}
}

impl PartialOrd for RatingsBand {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
if matches!(self, RatingsBand::InsufficientVotes)
|| matches!(other, RatingsBand::InsufficientVotes)
{
None
} else {
// Negative ratings have a higher value i.e., 0 = Very Good and 4 = Very Poor
let max = Self::InsufficientVotes as u8;
(max - (*self as u8)).partial_cmp(&(max - (*other as u8)))
}
}
}

impl From<crate::features::pb::common::RatingsBand> for RatingsBand {
fn from(value: crate::features::pb::common::RatingsBand) -> Self {
match value {
pb::RatingsBand::VeryGood => Self::VeryGood,
pb::RatingsBand::Good => Self::Good,
pb::RatingsBand::Neutral => Self::Neutral,
pb::RatingsBand::Poor => Self::Poor,
pb::RatingsBand::VeryPoor => Self::VeryPoor,
pb::RatingsBand::InsufficientVotes => Self::InsufficientVotes,
}
}
}

/// A descriptive rating object, usually used converted and transferred over the wire.
/// This is an aggregated rating for a snap without holding every raw value, as determined
/// by [`RatingsBand`].
#[derive(Debug, Clone, FromRow)]
#[derive(Debug, Clone, FromRow, Default)]
pub struct Rating {
/// The ID of the snap this rating is for
pub snap_id: String,
Expand Down Expand Up @@ -81,6 +110,18 @@ impl Rating {
}
}

impl From<crate::features::pb::common::Rating> for Rating {
fn from(value: crate::features::pb::common::Rating) -> Self {
Self {
snap_id: value.snap_id,
total_votes: value.total_votes,
ratings_band: crate::features::pb::common::RatingsBand::try_from(value.ratings_band)
.unwrap()
.into(),
}
}
}

/// A summary of votes for a given snap, this is then aggregated before transfer.
#[derive(Debug, Clone, FromRow)]
pub struct VoteSummary {
Expand Down
3 changes: 1 addition & 2 deletions src/features/mod.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
//! Contains various feature implementations for the ratings backend.
pub mod chart;
pub mod common;
pub mod pb;
pub mod rating;
pub mod user;

mod common;
17 changes: 17 additions & 0 deletions test.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
APP_ENV=dev
APP_HOST=0.0.0.0
APP_JWT_SECRET=deadbeef
APP_LOG_LEVEL=info
APP_NAME=ratings
APP_PORT=8080
# Update this with some real PostgreSQL details
APP_POSTGRES_URI=postgresql://migration_user:strongpassword@localhost:5433/ratings
APP_MIGRATION_POSTGRES_URI=postgresql://migration_user:strongpassword@localhost:5433/ratings

DOCKER_POSTGRES_USER=postgres
DOCKER_POSTGRES_PASSWORD=@1234
DOCKER_MIGRATION_USER=migration_user
DOCKER_MIGRATION_PASSWORD=strongpassword
DOCKER_SERVICE_USER=service
DOCKER_SERVICE_PASSWORD=covfefe!1
DOCKER_RATINGS_DB=ratings
118 changes: 0 additions & 118 deletions tests/app_tests/lifecycle_test.rs

This file was deleted.

104 changes: 104 additions & 0 deletions tests/authentication.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
use cucumber::{given, then, when, World};

use helpers::client::*;
use ratings::utils::{Config, Infrastructure};
use sqlx::Row;
use tonic::{Code, Status};

mod helpers;

#[derive(Clone, Debug, Default, World)]
struct AuthenticationWorld {
client_hash: String,
client: Option<TestClient>,
tokens: Vec<String>,
auth_error: Option<Status>,
}

#[given(expr = "a valid client hash")]
fn generate_hash(world: &mut AuthenticationWorld) {
world.client_hash = helpers::data_faker::rnd_sha_256();
}

#[given(expr = "a bad client with the hash {word}")]
fn with_hash(world: &mut AuthenticationWorld, hash: String) {
world.client_hash = hash;
}

#[when(expr = "the client attempts to authenticate")]
#[when(expr = "that client authenticates a second time")]
#[given(expr = "an authenticated client")]
async fn authenticate(world: &mut AuthenticationWorld) {
let config = Config::load().expect("Could not load config");

world.client = Some(TestClient::new(config.socket()));

match world
.client
.as_ref()
.unwrap()
.authenticate(&world.client_hash)
.await
{
Ok(resp) => world.tokens.push(resp.into_inner().token),
Err(err) => world.auth_error = Some(err),
}
}

#[then(expr = "the authentication is rejected")]
fn check_rejected(world: &mut AuthenticationWorld) {
assert!(world.auth_error.is_some());

let err = world.auth_error.as_ref().unwrap();

assert_eq!(err.code(), Code::InvalidArgument);
}

#[then(expr = "the returned token is valid")]
#[then(expr = "both tokens are valid")]
fn verify_token(world: &mut AuthenticationWorld) {
assert!(
world.auth_error.is_none(),
"needed clean exit, instead got status {:?}",
world.auth_error
);

let config = Config::load().expect("Could not load config");

for token in world.tokens.iter() {
helpers::assert::assert_token_is_valid(token, &config.jwt_secret);
}
}

#[then(expr = "the hash is only in the database once")]
async fn no_double_auth(world: &mut AuthenticationWorld) {
// In other test scenarios we might do this when we init the world, but
// given authentication only needs this once this is fine
let config = Config::load().expect("Could not load config");
let infra = Infrastructure::new(&config)
.await
.expect("Could not init DB");

// User still registered
let row = sqlx::query("SELECT COUNT(*) FROM users WHERE client_hash = $1")
.bind(&world.client_hash)
.fetch_one(&mut *infra.repository().await.expect("could not connect to DB"))
.await
.unwrap();

let count: i64 = row.try_get("count").expect("Failed to get count");

// Only appears in db once
assert_eq!(count, 1);
}

#[tokio::main]
async fn main() {
dotenv::from_filename("test.env").ok();

AuthenticationWorld::cucumber()
.repeat_skipped()
.init_tracing()
.run_and_exit("tests/features/user/authentication.feature")
.await
}
Loading

0 comments on commit 843aff5

Please sign in to comment.