Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/crates_io_database/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ utoipa = { version = "=5.5.0", features = ["chrono"] }
[dev-dependencies]
claims = "=0.8.0"
crates_io_test_db = { path = "../crates_io_test_db" }
crates_io_test_utils = { path = "../crates_io_test_utils" }

@Turbo87 Turbo87 Jul 15, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm... I'm feeling a bit uneasy about adding this to the database crate since it pulls in:

crates_io_api_types = { path = "../crates_io_api_types" }
crates_io_cargo_toml = { path = "../crates_io_cargo_toml" }
crates_io_database = { path = "../crates_io_database" }
crates_io_encryption = { path = "../crates_io_encryption" }
crates_io_linecount = { path = "../crates_io_linecount" }
crates_io_tarball = { path = "../crates_io_tarball", features = ["builder"] }

that creates quite a spaghetti in the dependency tree 😅

View changes since the review

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok 🤷🏻‍♀️ i put it back the way it was!

googletest = "=0.14.3"
insta = { version = "=1.48.0", features = ["filters", "json"] }
tokio = { version = "=1.52.3", features = ["macros", "rt"] }
14 changes: 4 additions & 10 deletions crates/crates_io_database/tests/reserved_usernames.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,12 @@
//! directly, asserting the trigger rejects the conflicting writes.

use claims::assert_err;
use crates_io_database::models::NewUser;
use crates_io_database::schema::{reserved_usernames, users};
use crates_io_test_db::TestDatabase;
use crates_io_test_utils::builders::UserBuilder;
use diesel::prelude::*;
use diesel::result::QueryResult;
use diesel_async::{AsyncPgConnection, RunQueryDsl};
use std::sync::atomic::{AtomicI32, Ordering};

static NEXT_GH_ID: AtomicI32 = AtomicI32::new(1);

async fn reserve_username(conn: &mut AsyncPgConnection, username: &str) {
diesel::insert_into(reserved_usernames::table)
Expand All @@ -22,12 +19,9 @@ async fn reserve_username(conn: &mut AsyncPgConnection, username: &str) {
}

async fn insert_user(conn: &AsyncPgConnection, username: &str) -> QueryResult<i32> {
NewUser::builder()
.gh_id(NEXT_GH_ID.fetch_add(1, Ordering::SeqCst))
.gh_login(username)
.username(username)
.gh_encrypted_token(&[])
.build()
UserBuilder::new()
.with_username(username)
.new_user()
.insert(conn)
.await
}
Expand Down
14 changes: 12 additions & 2 deletions crates/crates_io_test_utils/src/builders/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ static ENCRYPTED_TOKEN: LazyLock<Vec<u8>> = LazyLock::new(|| {
/// If you want to test logic that happens as part of signing up or logging in,
pub struct UserBuilder<'a> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wondering at what point we would want to pull bon in, but probably not at two fields.

username: &'a str,
display_name: Option<&'a str>,
}

impl<'a> UserBuilder<'a> {
Expand All @@ -30,18 +31,26 @@ impl<'a> UserBuilder<'a> {
pub fn new() -> Self {
Self {
username: "octocat",
display_name: None,
}
}

pub fn with_username(self, username: &'a str) -> Self {
Self { username }
Self { username, ..self }
}

pub fn with_display_name(self, display_name: &'a str) -> Self {
Self {
display_name: Some(display_name),
..self
}
}

pub fn build(self) -> User {
User {
id: 1,
gh_login: self.username.into(),
name: Some("The Octocat".into()),
name: self.display_name.map(ToString::to_string),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is technically a change in behaviour, but since the CI is green I'm assuming we weren't relying on it anywhere.

gh_id: 123,
gh_avatar: None,
gh_encrypted_token: vec![],
Expand All @@ -59,6 +68,7 @@ impl<'a> UserBuilder<'a> {
.gh_id(next_gh_id())
.gh_login(self.username)
.username(self.username)
.maybe_name(self.display_name)
.gh_encrypted_token(&ENCRYPTED_TOKEN)
.build()
}
Expand Down
13 changes: 5 additions & 8 deletions src/bin/crates-admin/backfill_cache_tags.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,17 +137,14 @@ impl NewBackgroundJob {
mod tests {
use super::*;
use crates_io::schema::cache_tags_backfills;
use crates_io_database::models::{NewCacheTagsBackfillRow, NewUser};
use crates_io_database::models::NewCacheTagsBackfillRow;
use crates_io_test_db::TestDatabase;
use crates_io_test_utils::builders::CrateBuilder;
use crates_io_test_utils::builders::{CrateBuilder, UserBuilder};

async fn create_user(conn: &AsyncPgConnection) -> i32 {
NewUser::builder()
.gh_id(1)
.gh_login("testuser")
.username("testuser")
.gh_encrypted_token(&[])
.build()
UserBuilder::new()
.with_username("testuser")
.new_user()
.insert(conn)
.await
.unwrap()
Expand Down
31 changes: 9 additions & 22 deletions src/index.rs
Original file line number Diff line number Diff line change
Expand Up @@ -142,27 +142,20 @@ pub async fn index_metadata(
#[cfg(test)]
mod tests {
use super::*;
use crate::schema::users;
use chrono::{Days, Utc};
use crates_io_test_db::TestDatabase;
use crates_io_test_utils::builders::{CrateBuilder, VersionBuilder};
use crates_io_test_utils::builders::{CrateBuilder, UserBuilder, VersionBuilder};
use insta::assert_json_snapshot;

#[tokio::test]
async fn test_index_metadata() {
let test_db = TestDatabase::new();
let mut conn = test_db.async_connect().await;

let user_id = diesel::insert_into(users::table)
.values((
users::name.eq("user1"),
users::gh_login.eq("user1"),
users::username.eq("user1"),
users::gh_id.eq(42),
users::gh_encrypted_token.eq(&[]),
))
.returning(users::id)
.get_result::<i32>(&mut conn)
let user_id = UserBuilder::new()
.with_username("user1")
.new_user()
.insert(&conn)
.await
.unwrap();

Expand Down Expand Up @@ -205,16 +198,10 @@ mod tests {
let test_db = TestDatabase::new();
let mut conn = test_db.async_connect().await;

let user_id = diesel::insert_into(users::table)
.values((
users::name.eq("user1"),
users::gh_login.eq("user1"),
users::username.eq("user1"),
users::gh_id.eq(42),
users::gh_encrypted_token.eq(&[]),
))
.returning(users::id)
.get_result::<i32>(&mut conn)
let user_id = UserBuilder::new()
.with_username("user1")
.new_user()
.insert(&conn)
.await
.unwrap();

Expand Down
12 changes: 4 additions & 8 deletions src/rate_limiter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ mod tests {
use super::*;
use chrono::NaiveDateTime;
use crates_io_test_db::TestDatabase;
use crates_io_test_utils::builders::UserBuilder;

#[tokio::test]
async fn default_rate_limits() -> anyhow::Result<()> {
Expand Down Expand Up @@ -703,14 +704,9 @@ mod tests {
}

async fn new_user(conn: &mut AsyncPgConnection, gh_login: &str) -> QueryResult<i32> {
use crate::models::NewUser;

NewUser::builder()
.gh_id(0)
.gh_login(gh_login)
.username(gh_login)
.gh_encrypted_token(&[])
.build()
UserBuilder::new()
.with_username(gh_login)
.new_user()
.insert(conn)
.await
}
Expand Down
11 changes: 4 additions & 7 deletions src/tests/routes/crates/versions/docs.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use crate::builders::{CrateBuilder, VersionBuilder};
use crate::util::{RequestHelper as _, TestApp};
use crates_io_database::models::NewUser;
use crates_io_docs_rs::MockDocsRsClient;
use crates_io_test_utils::builders::UserBuilder;
use insta::assert_snapshot;

#[tokio::test(flavor = "multi_thread")]
Expand Down Expand Up @@ -45,12 +45,9 @@ async fn test_trigger_rebuild_permission_failed() -> anyhow::Result<()> {

let mut conn = app.db_conn().await;

let other_user_id = NewUser::builder()
.gh_id(111)
.gh_login("other_user")
.username("other_user")
.gh_encrypted_token(&[])
.build()
let other_user_id = UserBuilder::new()
.with_username("other_user")
.new_user()
.insert(&conn)
.await?;

Expand Down
43 changes: 17 additions & 26 deletions src/tests/routes/users/read.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
use crate::util::{RequestHelper, TestApp};
use claims::assert_ok;
use crates_io::models::NewUser;
use crates_io::schema::users;
use crates_io::models::{NewUser, User};
use crates_io::views::EncodablePublicUser;
use diesel_async::RunQueryDsl;
use crates_io_test_utils::builders::{OauthGithubBuilder, UserBuilder};
use insta::assert_snapshot;
use serde::Deserialize;

Expand Down Expand Up @@ -33,7 +31,7 @@ async fn show() {
#[tokio::test(flavor = "multi_thread")]
async fn show_latest_user_case_insensitively() {
let (app, anon) = TestApp::init().empty().await;
let mut conn = app.db_conn().await;
let conn = app.db_conn().await;

// Please do not delete or modify the setup of this test in order to get it to pass.
// This setup mimics how GitHub works. If someone abandons a GitHub account, the username is
Expand All @@ -43,28 +41,21 @@ async fn show_latest_user_case_insensitively() {
// crates.io/user/{username} pages, the best we can do is show the last crates.io account
// created with that username.

let user1 = NewUser::builder()
.gh_id(1)
.gh_login("foobar")
.username("foobar")
.name("I was first then deleted my github account")
.gh_encrypted_token(&[])
.build();
let user1 = UserBuilder::new()
.with_username("foobar")
.with_display_name("I was first then deleted my github account")
.new_user();
let user1_id = user1.insert(&conn).await.unwrap();
let user1 = User::find(&conn, user1_id).await.unwrap();
OauthGithubBuilder::for_user(&user1).insert(&conn).await;

let user2 = NewUser::builder()
.gh_id(2)
.gh_login("FOOBAR")
.username("FOOBAR")
.name("I was second, I took the foobar username on github")
.gh_encrypted_token(&[])
.build();

assert_ok!(
diesel::insert_into(users::table)
.values(&vec![user1, user2])
.execute(&mut conn)
.await
);
let user2 = UserBuilder::new()
.with_username("FOOBAR")
.with_display_name("I was second, I took the foobar username on github")
.new_user();
let user2_id = user2.insert(&conn).await.unwrap();
let user2 = User::find(&conn, user2_id).await.unwrap();
OauthGithubBuilder::for_user(&user2).insert(&conn).await;

let json: UserShowPublicResponse = anon.get("/api/v1/users/fOObAr").await.good();
assert_eq!(
Expand Down
13 changes: 5 additions & 8 deletions src/typosquat/test_util.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
use diesel::prelude::*;

use crate::models::{Crate, NewTeam, NewUser, Team};
use crate::models::{Crate, NewTeam, Team};
use crates_io_test_utils::github::next_gh_id;

pub mod faker {
use super::*;
use anyhow::anyhow;
use crates_io_test_utils::builders::CrateBuilder;
use crates_io_test_utils::builders::{CrateBuilder, UserBuilder};
use diesel_async::AsyncPgConnection;

pub async fn crate_and_version(
Expand Down Expand Up @@ -38,12 +38,9 @@ pub mod faker {
}

pub async fn user(conn: &mut AsyncPgConnection, login: &str) -> QueryResult<i32> {
NewUser::builder()
.gh_id(next_gh_id())
.gh_login(login)
.username(login)
.gh_encrypted_token(&[])
.build()
UserBuilder::new()
.with_username(login)
.new_user()
.insert(conn)
.await
}
Expand Down
12 changes: 5 additions & 7 deletions src/worker/jobs/downloads/update_metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,19 +107,17 @@ async fn batch_update(batch_size: i64, conn: &mut AsyncPgConnection) -> QueryRes
#[cfg(test)]
mod tests {
use super::*;
use crate::models::{Crate, NewCrate, NewUser, NewVersion, Version};
use crate::models::{Crate, NewCrate, NewVersion, Version};
use crate::schema::{crate_downloads, crates, versions};
use crates_io_test_db::TestDatabase;
use crates_io_test_utils::builders::UserBuilder;
use diesel::sql_types::Timestamptz;
use diesel_async::AsyncConnection;

async fn user(conn: &mut AsyncPgConnection) -> i32 {
NewUser::builder()
.gh_id(2)
.gh_login("login")
.username("login")
.gh_encrypted_token(&[])
.build()
UserBuilder::new()
.with_username("login")
.new_user()
.insert(conn)
.await
.unwrap()
Expand Down
15 changes: 7 additions & 8 deletions src/worker/jobs/expiry_notification.rs
Original file line number Diff line number Diff line change
Expand Up @@ -142,9 +142,10 @@ pub async fn find_expiring_tokens(
#[cfg(test)]
mod tests {
use super::*;
use crate::models::{NewEmail, NewUser};
use crate::models::NewEmail;
use crate::{models::token::ApiToken, schema::api_tokens, util::token::PlainToken};
use crates_io_test_db::TestDatabase;
use crates_io_test_utils::builders::UserBuilder;
use diesel::dsl::IntervalDsl;
use lettre::Address;

Expand All @@ -154,14 +155,12 @@ mod tests {
let mut conn = test_db.async_connect().await;

// Set up a user and a token that is about to expire.
let user_id = NewUser::builder()
.gh_id(0)
.gh_login("a")
.username("a")
.gh_encrypted_token(&[])
.build()
let user_id = UserBuilder::new()
.with_username("a")
.new_user()
.insert(&conn)
.await?;
.await
.unwrap();

NewEmail::builder()
.user_id(user_id)
Expand Down