diff --git a/app/components/owners-list.hbs b/app/components/owners-list.hbs
index a4eac17b56b..c70170064f9 100644
--- a/app/components/owners-list.hbs
+++ b/app/components/owners-list.hbs
@@ -7,12 +7,12 @@
diff --git a/app/components/user-avatar.js b/app/components/user-avatar.js
index 37012f4738d..c65ba3daf0c 100644
--- a/app/components/user-avatar.js
+++ b/app/components/user-avatar.js
@@ -13,8 +13,8 @@ export default class UserAvatar extends Component {
get alt() {
return this.args.user.name === null
- ? `(${this.args.user.login})`
- : `${this.args.user.name} (${this.args.user.login})`;
+ ? `(${this.args.user.username})`
+ : `${this.args.user.name} (${this.args.user.username})`;
}
get title() {
diff --git a/app/components/user-link.hbs b/app/components/user-link.hbs
index 13f128379cc..58865fd2dd1 100644
--- a/app/components/user-link.hbs
+++ b/app/components/user-link.hbs
@@ -1 +1 @@
-
{{yield}}
\ No newline at end of file
+
{{yield}}
\ No newline at end of file
diff --git a/app/components/version-list/row.hbs b/app/components/version-list/row.hbs
index e6630036423..c1eaead5a18 100644
--- a/app/components/version-list/row.hbs
+++ b/app/components/version-list/row.hbs
@@ -40,9 +40,9 @@
{{#if @version.published_by}}
by
-
+
- {{or @version.published_by.name @version.published_by.login}}
+ {{or @version.published_by.name @version.published_by.username}}
{{/if}}
diff --git a/app/controllers/crate/settings.js b/app/controllers/crate/settings.js
index 72d38bdce9a..e55bb789cdd 100644
--- a/app/controllers/crate/settings.js
+++ b/app/controllers/crate/settings.js
@@ -32,19 +32,19 @@ export default class CrateSettingsController extends Controller {
removeOwnerTask = task(async owner => {
try {
- await this.crate.removeOwner(owner.get('login'));
+ await this.crate.removeOwner(owner.get('username'));
if (owner.kind === 'team') {
this.notifications.success(`Team ${owner.get('display_name')} removed as crate owner`);
let owner_team = await this.crate.owner_team;
removeOwner(owner_team, owner);
} else {
- this.notifications.success(`User ${owner.get('login')} removed as crate owner`);
+ this.notifications.success(`User ${owner.get('username')} removed as crate owner`);
let owner_user = await this.crate.owner_user;
removeOwner(owner_user, owner);
}
} catch (error) {
- let subject = owner.kind === 'team' ? `team ${owner.get('display_name')}` : `user ${owner.get('login')}`;
+ let subject = owner.kind === 'team' ? `team ${owner.get('display_name')}` : `user ${owner.get('username')}`;
let message = `Failed to remove the ${subject} as crate owner`;
let detail = error.errors?.[0]?.detail;
diff --git a/app/models/team.js b/app/models/team.js
index 2d69e1a108e..07104f40661 100644
--- a/app/models/team.js
+++ b/app/models/team.js
@@ -4,15 +4,16 @@ export default class Team extends Model {
@attr email;
@attr name;
@attr login;
+ @attr username;
@attr api_token;
@attr avatar;
@attr url;
@attr kind;
get org_name() {
- let login = this.login;
- let login_split = login.split(':');
- return login_split[1];
+ let username = this.username;
+ let username_split = username.split(':');
+ return username_split[1];
}
get display_name() {
diff --git a/app/models/user.js b/app/models/user.js
index 9e96f47adcf..22dbff658e0 100644
--- a/app/models/user.js
+++ b/app/models/user.js
@@ -13,6 +13,7 @@ export default class User extends Model {
@attr name;
@attr is_admin;
@attr login;
+ @attr username;
@attr avatar;
@attr url;
@attr kind;
diff --git a/app/services/session.js b/app/services/session.js
index baf5a0af7bb..37f66289039 100644
--- a/app/services/session.js
+++ b/app/services/session.js
@@ -194,7 +194,7 @@ export default class SessionService extends Service {
}
let currentUser = this.store.push(this.store.normalize('user', response.user));
- debug(`User found: ${currentUser.login}`);
+ debug(`User found: ${currentUser.username}`);
let ownedCrates = response.owned_crates.map(c => this.store.push(this.store.normalize('owned-crate', c)));
let { id } = currentUser;
diff --git a/app/templates/crate/settings.hbs b/app/templates/crate/settings.hbs
index 5487e88876c..4d6be016528 100644
--- a/app/templates/crate/settings.hbs
+++ b/app/templates/crate/settings.hbs
@@ -18,11 +18,11 @@
{{#each this.crate.owner_team as |team|}}
-
-
+
+
-
+
{{team.display_name}}
@@ -32,15 +32,15 @@
{{/each}}
{{#each this.crate.owner_user as |user|}}
-
-
+
+
-
+
{{#if user.name}}
{{user.name}}
{{else}}
- {{user.login}}
+ {{user.username}}
{{/if}}
diff --git a/app/templates/settings/profile.hbs b/app/templates/settings/profile.hbs
index c4ab8eb42e1..16f3ca2e1b3 100644
--- a/app/templates/settings/profile.hbs
+++ b/app/templates/settings/profile.hbs
@@ -13,7 +13,7 @@
Name
{{ this.model.user.name }}
GitHub Account
- {{ this.model.user.login }}
+ {{ this.model.user.username }}
diff --git a/app/templates/user.hbs b/app/templates/user.hbs
index 10b8c202970..c70ae7d95aa 100644
--- a/app/templates/user.hbs
+++ b/app/templates/user.hbs
@@ -1,7 +1,7 @@
- {{ this.model.user.login }}
+ {{ this.model.user.username }}
{{svg-jar "github" alt="GitHub profile"}}
diff --git a/crates/crates_io_database/src/models/mod.rs b/crates/crates_io_database/src/models/mod.rs
index 2b6f2698adc..7b75204056a 100644
--- a/crates/crates_io_database/src/models/mod.rs
+++ b/crates/crates_io_database/src/models/mod.rs
@@ -14,7 +14,7 @@ pub use self::krate::{Crate, CrateName, NewCrate, RecentCrateDownloads};
pub use self::owner::{CrateOwner, Owner, OwnerKind};
pub use self::team::{NewTeam, Team};
pub use self::token::ApiToken;
-pub use self::user::{NewUser, User};
+pub use self::user::{AccountProvider, LinkedAccount, NewLinkedAccount, NewUser, User};
pub use self::version::{NewVersion, TopVersions, Version};
pub mod helpers;
diff --git a/crates/crates_io_database/src/models/user.rs b/crates/crates_io_database/src/models/user.rs
index 9d060361ca5..8b85a1dad32 100644
--- a/crates/crates_io_database/src/models/user.rs
+++ b/crates/crates_io_database/src/models/user.rs
@@ -8,8 +8,10 @@ use diesel_async::{AsyncPgConnection, RunQueryDsl};
use secrecy::SecretString;
use crate::models::{Crate, CrateOwner, Email, Owner, OwnerKind};
-use crate::schema::{crate_owners, emails, users};
-use crates_io_diesel_helpers::lower;
+use crate::schema::{crate_owners, emails, linked_accounts, users};
+use crates_io_diesel_helpers::{lower, pg_enum};
+
+use std::fmt::{Display, Formatter};
/// The model representing a row in the `users` database table.
#[derive(Clone, Debug, Queryable, Identifiable, Selectable)]
@@ -25,6 +27,7 @@ pub struct User {
pub account_lock_until: Option>,
pub is_admin: bool,
pub publish_notifications: bool,
+ pub username: Option,
}
impl User {
@@ -76,6 +79,17 @@ impl User {
.await
.optional()
}
+
+ /// Queries for the linked accounts belonging to a particular user
+ pub async fn linked_accounts(
+ &self,
+ conn: &mut AsyncPgConnection,
+ ) -> QueryResult> {
+ LinkedAccount::belonging_to(self)
+ .select(LinkedAccount::as_select())
+ .load(conn)
+ .await
+ }
}
/// Represents a new user record insertable to the `users` table
@@ -85,6 +99,7 @@ pub struct NewUser<'a> {
pub gh_id: i32,
pub gh_login: &'a str,
pub name: Option<&'a str>,
+ pub username: Option<&'a str>,
pub gh_avatar: Option<&'a str>,
pub gh_access_token: &'a str,
}
@@ -114,6 +129,7 @@ impl NewUser<'_> {
.do_update()
.set((
users::gh_login.eq(excluded(users::gh_login)),
+ users::username.eq(excluded(users::username)),
users::name.eq(excluded(users::name)),
users::gh_avatar.eq(excluded(users::gh_avatar)),
users::gh_access_token.eq(excluded(users::gh_access_token)),
@@ -122,3 +138,94 @@ impl NewUser<'_> {
.await
}
}
+
+// Supported OAuth providers. Currently only GitHub.
+pg_enum! {
+ pub enum AccountProvider {
+ Github = 0,
+ }
+}
+
+impl Display for AccountProvider {
+ fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+ match self {
+ Self::Github => write!(f, "GitHub"),
+ }
+ }
+}
+
+impl AccountProvider {
+ pub fn url(&self, login: &str) -> String {
+ match self {
+ Self::Github => format!("https://github.com/{login}"),
+ }
+ }
+}
+
+/// Represents an OAuth account record linked to a user record.
+#[derive(Associations, Identifiable, Selectable, Queryable, Debug, Clone)]
+#[diesel(
+ table_name = linked_accounts,
+ check_for_backend(diesel::pg::Pg),
+ primary_key(provider, account_id),
+ belongs_to(User),
+)]
+pub struct LinkedAccount {
+ pub user_id: i32,
+ pub provider: AccountProvider,
+ pub account_id: i32, // corresponds to user.gh_id
+ #[diesel(deserialize_as = String)]
+ pub access_token: SecretString, // corresponds to user.gh_access_token
+ pub login: String, // corresponds to user.gh_login
+ pub avatar: Option, // corresponds to user.gh_avatar
+}
+
+/// Represents a new linked account record insertable to the `linked_accounts` table
+#[derive(Insertable, Debug, Builder)]
+#[diesel(
+ table_name = linked_accounts,
+ check_for_backend(diesel::pg::Pg),
+ primary_key(provider, account_id),
+ belongs_to(User),
+)]
+pub struct NewLinkedAccount<'a> {
+ pub user_id: i32,
+ pub provider: AccountProvider,
+ pub account_id: i32, // corresponds to user.gh_id
+ pub access_token: &'a str, // corresponds to user.gh_access_token
+ pub login: &'a str, // corresponds to user.gh_login
+ pub avatar: Option<&'a str>, // corresponds to user.gh_avatar
+}
+
+impl NewLinkedAccount<'_> {
+ /// Inserts the linked account into the database, or updates an existing one.
+ ///
+ /// This is to be used for logging in when there is no currently logged-in user, as opposed to
+ /// adding another linked account to a currently-logged-in user. The logic for adding another
+ /// linked account (when that ability gets added) will need to ensure that a particular
+ /// (provider, account_id) combo (ex: GitHub account with GitHub ID 1234) is only associated
+ /// with one crates.io account, so that we know what crates.io account to log in when we get an
+ /// oAuth request from GitHub ID 1234. In other words, we should NOT be updating the user_id on
+ /// an existing (provider, account_id) row when starting from a currently-logged-in crates.io \
+ /// user because that would mean that oAuth account has already been associated with a
+ /// different crates.io account.
+ ///
+ /// This function should be called if there is no current user and should update, say, the
+ /// access token, login, or avatar if those have changed.
+ pub async fn insert_or_update(
+ &self,
+ conn: &mut AsyncPgConnection,
+ ) -> QueryResult {
+ diesel::insert_into(linked_accounts::table)
+ .values(self)
+ .on_conflict((linked_accounts::provider, linked_accounts::account_id))
+ .do_update()
+ .set((
+ linked_accounts::access_token.eq(excluded(linked_accounts::access_token)),
+ linked_accounts::login.eq(excluded(linked_accounts::login)),
+ linked_accounts::avatar.eq(excluded(linked_accounts::avatar)),
+ ))
+ .get_result(conn)
+ .await
+ }
+}
diff --git a/crates/crates_io_database/src/schema.rs b/crates/crates_io_database/src/schema.rs
index 9b05b2434e1..ccf182b95e6 100644
--- a/crates/crates_io_database/src/schema.rs
+++ b/crates/crates_io_database/src/schema.rs
@@ -593,6 +593,50 @@ diesel::table! {
}
}
+diesel::table! {
+ /// Representation of the `linked_accounts` table.
+ ///
+ /// (Automatically generated by Diesel.)
+ linked_accounts (provider, account_id) {
+ /// The `user_id` column of the `linked_accounts` table.
+ ///
+ /// Its SQL type is `Int4`.
+ ///
+ /// (Automatically generated by Diesel.)
+ user_id -> Int4,
+ /// The `provider` column of the `linked_accounts` table.
+ ///
+ /// Its SQL type is `Int4`.
+ ///
+ /// (Automatically generated by Diesel.)
+ provider -> Int4,
+ /// The `account_id` column of the `linked_accounts` table.
+ ///
+ /// Its SQL type is `Int4`.
+ ///
+ /// (Automatically generated by Diesel.)
+ account_id -> Int4,
+ /// The `access_token` column of the `linked_accounts` table.
+ ///
+ /// Its SQL type is `Varchar`.
+ ///
+ /// (Automatically generated by Diesel.)
+ access_token -> Varchar,
+ /// The `login` column of the `linked_accounts` table.
+ ///
+ /// Its SQL type is `Varchar`.
+ ///
+ /// (Automatically generated by Diesel.)
+ login -> Varchar,
+ /// The `avatar` column of the `linked_accounts` table.
+ ///
+ /// Its SQL type is `Nullable`.
+ ///
+ /// (Automatically generated by Diesel.)
+ avatar -> Nullable,
+ }
+}
+
diesel::table! {
/// Representation of the `metadata` table.
///
@@ -826,6 +870,12 @@ diesel::table! {
is_admin -> Bool,
/// Whether or not the user wants to receive notifications when a package they own is published
publish_notifications -> Bool,
+ /// The `username` column of the `users` table.
+ ///
+ /// Its SQL type is `Nullable`.
+ ///
+ /// (Automatically generated by Diesel.)
+ username -> Nullable,
}
}
@@ -1066,6 +1116,7 @@ diesel::joinable!(dependencies -> versions (version_id));
diesel::joinable!(emails -> users (user_id));
diesel::joinable!(follows -> crates (crate_id));
diesel::joinable!(follows -> users (user_id));
+diesel::joinable!(linked_accounts -> users (user_id));
diesel::joinable!(publish_limit_buckets -> users (user_id));
diesel::joinable!(publish_rate_overrides -> users (user_id));
diesel::joinable!(readme_renderings -> versions (version_id));
@@ -1094,6 +1145,7 @@ diesel::allow_tables_to_appear_in_same_query!(
emails,
follows,
keywords,
+ linked_accounts,
metadata,
processed_log_files,
publish_limit_buckets,
diff --git a/crates/crates_io_database_dump/src/dump-db.toml b/crates/crates_io_database_dump/src/dump-db.toml
index e42d45c59dd..9213d7ad5c1 100644
--- a/crates/crates_io_database_dump/src/dump-db.toml
+++ b/crates/crates_io_database_dump/src/dump-db.toml
@@ -154,6 +154,16 @@ keyword = "public"
crates_cnt = "public"
created_at = "public"
+[linked_accounts.columns]
+user_id = "public"
+provider = "public"
+account_id = "public"
+access_token = "private"
+login = "public"
+avatar = "public"
+[linked_accounts.column_defaults]
+access_token = "''"
+
[metadata.columns]
total_downloads = "public"
@@ -206,6 +216,7 @@ account_lock_reason = "private"
account_lock_until = "private"
is_admin = "private"
publish_notifications = "private"
+username = "public"
[users.column_defaults]
gh_access_token = "''"
diff --git a/crates/crates_io_database_dump/src/snapshots/crates_io_database_dump__tests__sql_scripts@export.sql.snap b/crates/crates_io_database_dump/src/snapshots/crates_io_database_dump__tests__sql_scripts@export.sql.snap
index 1d801f192d7..41039804ec0 100644
--- a/crates/crates_io_database_dump/src/snapshots/crates_io_database_dump__tests__sql_scripts@export.sql.snap
+++ b/crates/crates_io_database_dump/src/snapshots/crates_io_database_dump__tests__sql_scripts@export.sql.snap
@@ -1,6 +1,7 @@
---
source: crates/crates_io_database_dump/src/lib.rs
expression: content
+snapshot_kind: text
---
BEGIN ISOLATION LEVEL REPEATABLE READ, READ ONLY;
@@ -8,10 +9,11 @@ BEGIN ISOLATION LEVEL REPEATABLE READ, READ ONLY;
\copy "crate_downloads" ("crate_id", "downloads") TO 'data/crate_downloads.csv' WITH CSV HEADER
\copy "crates" ("created_at", "description", "documentation", "homepage", "id", "max_features", "max_upload_size", "name", "readme", "repository", "updated_at") TO 'data/crates.csv' WITH CSV HEADER
\copy "keywords" ("crates_cnt", "created_at", "id", "keyword") TO 'data/keywords.csv' WITH CSV HEADER
+ \copy "linked_accounts" ("account_id", "avatar", "login", "provider", "user_id") TO 'data/linked_accounts.csv' WITH CSV HEADER
\copy "metadata" ("total_downloads") TO 'data/metadata.csv' WITH CSV HEADER
\copy "reserved_crate_names" ("name") TO 'data/reserved_crate_names.csv' WITH CSV HEADER
\copy "teams" ("avatar", "github_id", "id", "login", "name", "org_id") TO 'data/teams.csv' WITH CSV HEADER
- \copy (SELECT "gh_avatar", "gh_id", "gh_login", "id", "name" FROM "users" WHERE id in ( SELECT owner_id AS user_id FROM crate_owners WHERE NOT deleted AND owner_kind = 0 UNION SELECT published_by as user_id FROM versions )) TO 'data/users.csv' WITH CSV HEADER
+ \copy (SELECT "gh_avatar", "gh_id", "gh_login", "id", "name", "username" FROM "users" WHERE id in ( SELECT owner_id AS user_id FROM crate_owners WHERE NOT deleted AND owner_kind = 0 UNION SELECT published_by as user_id FROM versions )) TO 'data/users.csv' WITH CSV HEADER
\copy "crates_categories" ("category_id", "crate_id") TO 'data/crates_categories.csv' WITH CSV HEADER
\copy "crates_keywords" ("crate_id", "keyword_id") TO 'data/crates_keywords.csv' WITH CSV HEADER
diff --git a/crates/crates_io_database_dump/src/snapshots/crates_io_database_dump__tests__sql_scripts@import.sql.snap b/crates/crates_io_database_dump/src/snapshots/crates_io_database_dump__tests__sql_scripts@import.sql.snap
index f5315ad6929..20a067d5905 100644
--- a/crates/crates_io_database_dump/src/snapshots/crates_io_database_dump__tests__sql_scripts@import.sql.snap
+++ b/crates/crates_io_database_dump/src/snapshots/crates_io_database_dump__tests__sql_scripts@import.sql.snap
@@ -1,6 +1,7 @@
---
source: crates/crates_io_database_dump/src/lib.rs
expression: content
+snapshot_kind: text
---
BEGIN;
-- Disable triggers on each table.
@@ -9,6 +10,7 @@ BEGIN;
ALTER TABLE "crate_downloads" DISABLE TRIGGER ALL;
ALTER TABLE "crates" DISABLE TRIGGER ALL;
ALTER TABLE "keywords" DISABLE TRIGGER ALL;
+ ALTER TABLE "linked_accounts" DISABLE TRIGGER ALL;
ALTER TABLE "metadata" DISABLE TRIGGER ALL;
ALTER TABLE "reserved_crate_names" DISABLE TRIGGER ALL;
ALTER TABLE "teams" DISABLE TRIGGER ALL;
@@ -23,6 +25,7 @@ BEGIN;
-- Set defaults for non-nullable columns not included in the dump.
+ ALTER TABLE "linked_accounts" ALTER COLUMN "access_token" SET DEFAULT '';
ALTER TABLE "users" ALTER COLUMN "gh_access_token" SET DEFAULT '';
-- Truncate all tables.
@@ -31,6 +34,7 @@ BEGIN;
TRUNCATE "crate_downloads" RESTART IDENTITY CASCADE;
TRUNCATE "crates" RESTART IDENTITY CASCADE;
TRUNCATE "keywords" RESTART IDENTITY CASCADE;
+ TRUNCATE "linked_accounts" RESTART IDENTITY CASCADE;
TRUNCATE "metadata" RESTART IDENTITY CASCADE;
TRUNCATE "reserved_crate_names" RESTART IDENTITY CASCADE;
TRUNCATE "teams" RESTART IDENTITY CASCADE;
@@ -52,10 +56,11 @@ BEGIN;
\copy "crate_downloads" ("crate_id", "downloads") FROM 'data/crate_downloads.csv' WITH CSV HEADER
\copy "crates" ("created_at", "description", "documentation", "homepage", "id", "max_features", "max_upload_size", "name", "readme", "repository", "updated_at") FROM 'data/crates.csv' WITH CSV HEADER
\copy "keywords" ("crates_cnt", "created_at", "id", "keyword") FROM 'data/keywords.csv' WITH CSV HEADER
+ \copy "linked_accounts" ("account_id", "avatar", "login", "provider", "user_id") FROM 'data/linked_accounts.csv' WITH CSV HEADER
\copy "metadata" ("total_downloads") FROM 'data/metadata.csv' WITH CSV HEADER
\copy "reserved_crate_names" ("name") FROM 'data/reserved_crate_names.csv' WITH CSV HEADER
\copy "teams" ("avatar", "github_id", "id", "login", "name", "org_id") FROM 'data/teams.csv' WITH CSV HEADER
- \copy "users" ("gh_avatar", "gh_id", "gh_login", "id", "name") FROM 'data/users.csv' WITH CSV HEADER
+ \copy "users" ("gh_avatar", "gh_id", "gh_login", "id", "name", "username") FROM 'data/users.csv' WITH CSV HEADER
\copy "crates_categories" ("category_id", "crate_id") FROM 'data/crates_categories.csv' WITH CSV HEADER
\copy "crates_keywords" ("crate_id", "keyword_id") FROM 'data/crates_keywords.csv' WITH CSV HEADER
\copy "crate_owners" ("crate_id", "created_at", "created_by", "owner_id", "owner_kind") FROM 'data/crate_owners.csv' WITH CSV HEADER
@@ -66,6 +71,7 @@ BEGIN;
-- Drop the defaults again.
+ ALTER TABLE "linked_accounts" ALTER COLUMN "access_token" DROP DEFAULT;
ALTER TABLE "users" ALTER COLUMN "gh_access_token" DROP DEFAULT;
-- Reenable triggers on each table.
@@ -74,6 +80,7 @@ BEGIN;
ALTER TABLE "crate_downloads" ENABLE TRIGGER ALL;
ALTER TABLE "crates" ENABLE TRIGGER ALL;
ALTER TABLE "keywords" ENABLE TRIGGER ALL;
+ ALTER TABLE "linked_accounts" ENABLE TRIGGER ALL;
ALTER TABLE "metadata" ENABLE TRIGGER ALL;
ALTER TABLE "reserved_crate_names" ENABLE TRIGGER ALL;
ALTER TABLE "teams" ENABLE TRIGGER ALL;
diff --git a/e2e/acceptance/api-token.spec.ts b/e2e/acceptance/api-token.spec.ts
index c7a3f2827df..c89819e76c4 100644
--- a/e2e/acceptance/api-token.spec.ts
+++ b/e2e/acceptance/api-token.spec.ts
@@ -5,6 +5,7 @@ test.describe('Acceptance | api-tokens', { tag: '@acceptance' }, () => {
test.beforeEach(async ({ msw }) => {
let user = msw.db.user.create({
login: 'johnnydee',
+ username: 'johnnydee',
name: 'John Doe',
email: 'john@doe.com',
avatar: 'https://avatars2.githubusercontent.com/u/1234567?v=4',
diff --git a/e2e/acceptance/crate.spec.ts b/e2e/acceptance/crate.spec.ts
index bf09fe3628b..d7a77404742 100644
--- a/e2e/acceptance/crate.spec.ts
+++ b/e2e/acceptance/crate.spec.ts
@@ -213,7 +213,7 @@ test.describe('Acceptance | crate page', { tag: '@acceptance' }, () => {
test.skip('crates can be yanked by owner', async ({ page, msw }) => {
loadFixtures(msw.db);
- let user = msw.db.user.findFirst({ where: { login: { equals: 'thehydroimpulse' } } });
+ let user = msw.db.user.findFirst({ where: { username: { equals: 'thehydroimpulse' } } });
await msw.authenticateAs(user);
await page.goto('/crates/nanomsg/0.5.0');
@@ -241,7 +241,7 @@ test.describe('Acceptance | crate page', { tag: '@acceptance' }, () => {
test('navigating to the owners page when not an owner', async ({ page, msw }) => {
loadFixtures(msw.db);
- let user = msw.db.user.findFirst({ where: { login: { equals: 'iain8' } } });
+ let user = msw.db.user.findFirst({ where: { username: { equals: 'iain8' } } });
await msw.authenticateAs(user);
await page.goto('/crates/nanomsg');
@@ -252,7 +252,7 @@ test.describe('Acceptance | crate page', { tag: '@acceptance' }, () => {
test('navigating to the settings page', async ({ page, msw }) => {
loadFixtures(msw.db);
- let user = msw.db.user.findFirst({ where: { login: { equals: 'thehydroimpulse' } } });
+ let user = msw.db.user.findFirst({ where: { username: { equals: 'thehydroimpulse' } } });
await msw.authenticateAs(user);
await page.goto('/crates/nanomsg');
diff --git a/e2e/acceptance/dashboard.spec.ts b/e2e/acceptance/dashboard.spec.ts
index 1baefd56928..1ce2bffe78f 100644
--- a/e2e/acceptance/dashboard.spec.ts
+++ b/e2e/acceptance/dashboard.spec.ts
@@ -12,6 +12,7 @@ test.describe('Acceptance | Dashboard', { tag: '@acceptance' }, () => {
test('shows the dashboard when logged in', async ({ page, msw, percy }) => {
let user = msw.db.user.create({
login: 'johnnydee',
+ username: 'johnnydee',
name: 'John Doe',
email: 'john@doe.com',
avatar: 'https://avatars2.githubusercontent.com/u/1234567?v=4',
diff --git a/e2e/acceptance/login.spec.ts b/e2e/acceptance/login.spec.ts
index aaeb9704747..ab9a9bc2b41 100644
--- a/e2e/acceptance/login.spec.ts
+++ b/e2e/acceptance/login.spec.ts
@@ -28,6 +28,7 @@ test.describe('Acceptance | Login', { tag: '@acceptance' }, () => {
user: {
id: 42,
login: 'johnnydee',
+ username: 'johnnydee',
name: 'John Doe',
email: 'john@doe.name',
avatar: 'https://avatars2.githubusercontent.com/u/12345?v=4',
diff --git a/e2e/acceptance/settings/add-owner.spec.ts b/e2e/acceptance/settings/add-owner.spec.ts
index 43f53cbdf0f..0368096bb10 100644
--- a/e2e/acceptance/settings/add-owner.spec.ts
+++ b/e2e/acceptance/settings/add-owner.spec.ts
@@ -29,7 +29,7 @@ test.describe('Acceptance | Settings | Add Owner', { tag: '@acceptance' }, () =>
await page.click('[data-test-save-button]');
await expect(page.locator('[data-test-notification-message="error"]')).toHaveText(
- 'Error sending invite: could not find user with login `spookyghostboo`',
+ 'Error sending invite: could not find user with username `spookyghostboo`',
);
await expect(page.locator('[data-test-owners] [data-test-owner-team]')).toHaveCount(2);
await expect(page.locator('[data-test-owners] [data-test-owner-user]')).toHaveCount(2);
diff --git a/e2e/acceptance/settings/remove-owner.spec.ts b/e2e/acceptance/settings/remove-owner.spec.ts
index f767ed711c4..093ad53b7fc 100644
--- a/e2e/acceptance/settings/remove-owner.spec.ts
+++ b/e2e/acceptance/settings/remove-owner.spec.ts
@@ -37,10 +37,10 @@ test.describe('Acceptance | Settings | Remove Owner', { tag: '@acceptance' }, ()
await msw.worker.use(http.delete('/api/v1/crates/nanomsg/owners', () => error));
await page.goto(`/crates/${crate.name}/settings`);
- await page.click(`[data-test-owner-user="${user2.login}"] [data-test-remove-owner-button]`);
+ await page.click(`[data-test-owner-user="${user2.username}"] [data-test-remove-owner-button]`);
await expect(page.locator('[data-test-notification-message="error"]')).toHaveText(
- `Failed to remove the user ${user2.login} as crate owner: nope`,
+ `Failed to remove the user ${user2.username} as crate owner: nope`,
);
await expect(page.locator('[data-test-owner-user]')).toHaveCount(2);
});
@@ -62,7 +62,7 @@ test.describe('Acceptance | Settings | Remove Owner', { tag: '@acceptance' }, ()
await msw.worker.use(http.delete('/api/v1/crates/nanomsg/owners', () => error));
await page.goto(`/crates/${crate.name}/settings`);
- await page.click(`[data-test-owner-team="${team1.login}"] [data-test-remove-owner-button]`);
+ await page.click(`[data-test-owner-team="${team1.username}"] [data-test-remove-owner-button]`);
await expect(page.locator('[data-test-notification-message="error"]')).toHaveText(
`Failed to remove the team ${team1.org}/${team1.name} as crate owner: nope`,
diff --git a/e2e/acceptance/sudo.spec.ts b/e2e/acceptance/sudo.spec.ts
index 3970bbd60da..23a0439ff58 100644
--- a/e2e/acceptance/sudo.spec.ts
+++ b/e2e/acceptance/sudo.spec.ts
@@ -5,6 +5,7 @@ test.describe('Acceptance | sudo', { tag: '@acceptance' }, () => {
async function prepare(msw, { isAdmin = false } = {}) {
let user = msw.db.user.create({
login: 'johnnydee',
+ username: 'johnnydee',
name: 'John Doe',
email: 'john@doe.com',
avatar: 'https://avatars2.githubusercontent.com/u/1234567?v=4',
diff --git a/e2e/routes/settings/tokens/index.spec.ts b/e2e/routes/settings/tokens/index.spec.ts
index 63661616294..1bb42528f50 100644
--- a/e2e/routes/settings/tokens/index.spec.ts
+++ b/e2e/routes/settings/tokens/index.spec.ts
@@ -4,6 +4,7 @@ test.describe('/settings/tokens', { tag: '@routes' }, () => {
test('reloads all tokens from the server', async ({ page, msw }) => {
let user = msw.db.user.create({
login: 'johnnydee',
+ username: 'johnnydee',
name: 'John Doe',
email: 'john@doe.com',
avatar: 'https://avatars2.githubusercontent.com/u/1234567?v=4',
diff --git a/e2e/routes/settings/tokens/new.spec.ts b/e2e/routes/settings/tokens/new.spec.ts
index 87b1a8bfb8e..be46a7513cc 100644
--- a/e2e/routes/settings/tokens/new.spec.ts
+++ b/e2e/routes/settings/tokens/new.spec.ts
@@ -6,6 +6,7 @@ test.describe('/settings/tokens/new', { tag: '@routes' }, () => {
async function prepare(msw) {
let user = msw.db.user.create({
login: 'johnnydee',
+ username: 'johnnydee',
name: 'John Doe',
email: 'john@doe.com',
avatar: 'https://avatars2.githubusercontent.com/u/1234567?v=4',
diff --git a/migrations/2025-01-29-205705_linked_accounts_table/down.sql b/migrations/2025-01-29-205705_linked_accounts_table/down.sql
new file mode 100644
index 00000000000..bef2a0aa5ea
--- /dev/null
+++ b/migrations/2025-01-29-205705_linked_accounts_table/down.sql
@@ -0,0 +1 @@
+DROP TABLE linked_accounts;
diff --git a/migrations/2025-01-29-205705_linked_accounts_table/up.sql b/migrations/2025-01-29-205705_linked_accounts_table/up.sql
new file mode 100644
index 00000000000..aa2d79a9fcc
--- /dev/null
+++ b/migrations/2025-01-29-205705_linked_accounts_table/up.sql
@@ -0,0 +1,9 @@
+CREATE TABLE linked_accounts (
+ user_id INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE,
+ provider INTEGER NOT NULL,
+ account_id INTEGER NOT NULL,
+ access_token VARCHAR NOT NULL,
+ login VARCHAR NOT NULL,
+ avatar VARCHAR,
+ PRIMARY KEY (provider, account_id)
+);
diff --git a/migrations/2025-02-19-013433_add_username_to_user/down.sql b/migrations/2025-02-19-013433_add_username_to_user/down.sql
new file mode 100644
index 00000000000..228952f6bf6
--- /dev/null
+++ b/migrations/2025-02-19-013433_add_username_to_user/down.sql
@@ -0,0 +1,4 @@
+DROP INDEX IF EXISTS lower_username;
+
+ALTER TABLE users
+DROP COLUMN username;
diff --git a/migrations/2025-02-19-013433_add_username_to_user/up.sql b/migrations/2025-02-19-013433_add_username_to_user/up.sql
new file mode 100644
index 00000000000..661c0587e8d
--- /dev/null
+++ b/migrations/2025-02-19-013433_add_username_to_user/up.sql
@@ -0,0 +1,6 @@
+ALTER TABLE users
+-- The column needs to be nullable for this migration to be fast; can be changed to non-nullable
+-- after backfill of all records.
+ADD COLUMN username VARCHAR;
+
+CREATE INDEX lower_username ON users (lower(username));
diff --git a/packages/crates-io-msw/fixtures/teams.js b/packages/crates-io-msw/fixtures/teams.js
index 0c753788449..dbd2ca25462 100644
--- a/packages/crates-io-msw/fixtures/teams.js
+++ b/packages/crates-io-msw/fixtures/teams.js
@@ -3,6 +3,7 @@ export default [
avatar: 'https://avatars.githubusercontent.com/u/565790?v=3',
id: 1,
login: 'github:org:thehydroimpulse',
+ username: 'github:org:thehydroimpulse',
name: 'thehydroimpulseteam',
url: 'https://github.com/org_test',
},
@@ -10,6 +11,7 @@ export default [
avatar: 'https://avatars.githubusercontent.com/u/9447137?v=3',
id: 303,
login: 'github:org:blabaere',
+ username: 'github:org:blabaere',
name: 'Team Benoît Labaere',
url: 'https://github.com/blabaere',
},
diff --git a/packages/crates-io-msw/fixtures/users.js b/packages/crates-io-msw/fixtures/users.js
index 00243bdce19..d6628f4b192 100644
--- a/packages/crates-io-msw/fixtures/users.js
+++ b/packages/crates-io-msw/fixtures/users.js
@@ -4,6 +4,7 @@ export default [
email: null,
id: 303,
login: 'blabaere',
+ username: 'blabaere',
name: 'Benoît Labaere',
url: 'https://github.com/blabaere',
},
@@ -12,6 +13,7 @@ export default [
email: 'dnfagnan@gmail.com',
id: 2,
login: 'thehydroimpulse',
+ username: 'thehydroimpulse',
name: 'Daniel Fagnan',
url: 'https://github.com/thehydroimpulse',
},
@@ -20,6 +22,7 @@ export default [
email: 'iain@fastmail.com',
id: 10_982,
login: 'iain8',
+ username: 'iain8',
name: 'Iain Buchanan',
url: 'https://github.com/iain8',
},
diff --git a/packages/crates-io-msw/handlers/crates/add-owners.js b/packages/crates-io-msw/handlers/crates/add-owners.js
index 227d27ef6a8..77f9bcdda4d 100644
--- a/packages/crates-io-msw/handlers/crates/add-owners.js
+++ b/packages/crates-io-msw/handlers/crates/add-owners.js
@@ -20,25 +20,25 @@ export default http.put('/api/v1/crates/:name/owners', async ({ request, params
let users = [];
let teams = [];
let msgs = [];
- for (let login of body.owners) {
- if (login.includes(':')) {
- let team = db.team.findFirst({ where: { login: { equals: login } } });
+ for (let username of body.owners) {
+ if (username.includes(':')) {
+ let team = db.team.findFirst({ where: { username: { equals: username } } });
if (!team) {
- let errorMessage = `could not find team with login \`${login}\``;
+ let errorMessage = `could not find team with username \`${username}\``;
return HttpResponse.json({ errors: [{ detail: errorMessage }] }, { status: 404 });
}
teams.push(team);
- msgs.push(`team ${login} has been added as an owner of crate ${crate.name}`);
+ msgs.push(`team ${username} has been added as an owner of crate ${crate.name}`);
} else {
- let user = db.user.findFirst({ where: { login: { equals: login } } });
+ let user = db.user.findFirst({ where: { username: { equals: username } } });
if (!user) {
- let errorMessage = `could not find user with login \`${login}\``;
+ let errorMessage = `could not find user with username \`${username}\``;
return HttpResponse.json({ errors: [{ detail: errorMessage }] }, { status: 404 });
}
users.push(user);
- msgs.push(`user ${login} has been invited to be an owner of crate ${crate.name}`);
+ msgs.push(`user ${username} has been invited to be an owner of crate ${crate.name}`);
}
}
diff --git a/packages/crates-io-msw/handlers/crates/add-owners.test.js b/packages/crates-io-msw/handlers/crates/add-owners.test.js
index cdaae3e7ae6..e936b6402df 100644
--- a/packages/crates-io-msw/handlers/crates/add-owners.test.js
+++ b/packages/crates-io-msw/handlers/crates/add-owners.test.js
@@ -30,7 +30,7 @@ test('can add new owner', async function () {
let user2 = db.user.create();
- let body = JSON.stringify({ owners: [user2.login] });
+ let body = JSON.stringify({ owners: [user2.username] });
let response = await fetch('/api/v1/crates/foo/owners', { method: 'PUT', body });
assert.strictEqual(response.status, 200);
assert.deepEqual(await response.json(), {
@@ -57,7 +57,7 @@ test('can add team owner', async function () {
let team = db.team.create();
- let body = JSON.stringify({ owners: [team.login] });
+ let body = JSON.stringify({ owners: [team.username] });
let response = await fetch('/api/v1/crates/foo/owners', { method: 'PUT', body });
assert.strictEqual(response.status, 200);
assert.deepEqual(await response.json(), {
@@ -87,7 +87,7 @@ test('can add multiple owners', async function () {
let user2 = db.user.create();
let user3 = db.user.create();
- let body = JSON.stringify({ owners: [user2.login, team.login, user3.login] });
+ let body = JSON.stringify({ owners: [user2.username, team.username, user3.username] });
let response = await fetch('/api/v1/crates/foo/owners', { method: 'PUT', body });
assert.strictEqual(response.status, 200);
assert.deepEqual(await response.json(), {
diff --git a/packages/crates-io-msw/handlers/crates/remove-owners.js b/packages/crates-io-msw/handlers/crates/remove-owners.js
index a221967d4fb..2a9d8f9c7b2 100644
--- a/packages/crates-io-msw/handlers/crates/remove-owners.js
+++ b/packages/crates-io-msw/handlers/crates/remove-owners.js
@@ -19,7 +19,9 @@ export default http.delete('/api/v1/crates/:name/owners', async ({ request, para
for (let owner of body.owners) {
let ownership = db.crateOwnership.findFirst({
- where: owner.includes(':') ? { team: { login: { equals: owner } } } : { user: { login: { equals: owner } } },
+ where: owner.includes(':')
+ ? { team: { username: { equals: owner } } }
+ : { user: { username: { equals: owner } } },
});
if (!ownership) return notFound();
db.crateOwnership.delete({ where: { id: { equals: ownership.id } } });
diff --git a/packages/crates-io-msw/handlers/crates/remove-owners.test.js b/packages/crates-io-msw/handlers/crates/remove-owners.test.js
index d1c84998227..ac5811aef76 100644
--- a/packages/crates-io-msw/handlers/crates/remove-owners.test.js
+++ b/packages/crates-io-msw/handlers/crates/remove-owners.test.js
@@ -31,7 +31,7 @@ test('can remove a user owner', async function () {
let user2 = db.user.create();
db.crateOwnership.create({ crate, user: user2 });
- let body = JSON.stringify({ owners: [user2.login] });
+ let body = JSON.stringify({ owners: [user2.username] });
let response = await fetch('/api/v1/crates/foo/owners', { method: 'DELETE', body });
assert.strictEqual(response.status, 200);
assert.deepEqual(await response.json(), { ok: true, msg: 'owners successfully removed' });
@@ -54,7 +54,7 @@ test('can remove a team owner', async function () {
let team = db.team.create();
db.crateOwnership.create({ crate, team });
- let body = JSON.stringify({ owners: [team.login] });
+ let body = JSON.stringify({ owners: [team.username] });
let response = await fetch('/api/v1/crates/foo/owners', { method: 'DELETE', body });
assert.strictEqual(response.status, 200);
assert.deepEqual(await response.json(), { ok: true, msg: 'owners successfully removed' });
@@ -81,7 +81,7 @@ test('can remove multiple owners', async function () {
let user2 = db.user.create();
db.crateOwnership.create({ crate, user: user2 });
- let body = JSON.stringify({ owners: [user2.login, team.login] });
+ let body = JSON.stringify({ owners: [user2.username, team.username] });
let response = await fetch('/api/v1/crates/foo/owners', { method: 'DELETE', body });
assert.strictEqual(response.status, 200);
assert.deepEqual(await response.json(), { ok: true, msg: 'owners successfully removed' });
diff --git a/packages/crates-io-msw/handlers/crates/team-owners.test.js b/packages/crates-io-msw/handlers/crates/team-owners.test.js
index 25a9981aff5..ea8cb1570fb 100644
--- a/packages/crates-io-msw/handlers/crates/team-owners.test.js
+++ b/packages/crates-io-msw/handlers/crates/team-owners.test.js
@@ -32,6 +32,7 @@ test('returns the list of teams that own the specified crate', async function ()
avatar: 'https://avatars1.githubusercontent.com/u/14631425?v=4',
kind: 'team',
login: 'github:rust-lang:maintainers',
+ username: 'github:rust-lang:maintainers',
name: 'maintainers',
url: 'https://github.com/rust-lang',
},
diff --git a/packages/crates-io-msw/handlers/crates/user-owners.test.js b/packages/crates-io-msw/handlers/crates/user-owners.test.js
index e5426153674..1cbfcc28ab2 100644
--- a/packages/crates-io-msw/handlers/crates/user-owners.test.js
+++ b/packages/crates-io-msw/handlers/crates/user-owners.test.js
@@ -32,6 +32,7 @@ test('returns the list of users that own the specified crate', async function ()
avatar: 'https://avatars1.githubusercontent.com/u/14631425?v=4',
kind: 'user',
login: 'john-doe',
+ username: 'john-doe',
name: 'John Doe',
url: 'https://github.com/john-doe',
},
diff --git a/packages/crates-io-msw/handlers/invites/legacy-list.test.js b/packages/crates-io-msw/handlers/invites/legacy-list.test.js
index 91e7b4c768e..80ca10e7d0a 100644
--- a/packages/crates-io-msw/handlers/invites/legacy-list.test.js
+++ b/packages/crates-io-msw/handlers/invites/legacy-list.test.js
@@ -63,6 +63,7 @@ test('returns the list of invitations for the authenticated user', async functio
avatar: user.avatar,
id: Number(user.id),
login: user.login,
+ username: user.username,
name: user.name,
url: user.url,
},
@@ -70,6 +71,7 @@ test('returns the list of invitations for the authenticated user', async functio
avatar: 'https://avatars1.githubusercontent.com/u/14631425?v=4',
id: Number(inviter.id),
login: 'janed',
+ username: 'janed',
name: 'janed',
url: 'https://github.com/janed',
},
@@ -77,6 +79,7 @@ test('returns the list of invitations for the authenticated user', async functio
avatar: 'https://avatars1.githubusercontent.com/u/14631425?v=4',
id: Number(inviter2.id),
login: 'wycats',
+ username: 'wycats',
name: 'wycats',
url: 'https://github.com/wycats',
},
diff --git a/packages/crates-io-msw/handlers/invites/list.test.js b/packages/crates-io-msw/handlers/invites/list.test.js
index 551bcd05903..9f5df26ecb5 100644
--- a/packages/crates-io-msw/handlers/invites/list.test.js
+++ b/packages/crates-io-msw/handlers/invites/list.test.js
@@ -56,6 +56,7 @@ test('happy path (invitee_id)', async function () {
login: user.login,
name: user.name,
url: user.url,
+ username: user.username,
},
{
avatar: 'https://avatars1.githubusercontent.com/u/14631425?v=4',
@@ -63,6 +64,7 @@ test('happy path (invitee_id)', async function () {
login: 'janed',
name: 'janed',
url: 'https://github.com/janed',
+ username: 'janed',
},
{
avatar: 'https://avatars1.githubusercontent.com/u/14631425?v=4',
@@ -70,6 +72,7 @@ test('happy path (invitee_id)', async function () {
login: 'wycats',
name: 'wycats',
url: 'https://github.com/wycats',
+ username: 'wycats',
},
],
meta: {
@@ -164,6 +167,7 @@ test('happy path (crate_name)', async function () {
login: user.login,
name: user.name,
url: user.url,
+ username: user.username,
},
{
avatar: 'https://avatars1.githubusercontent.com/u/14631425?v=4',
@@ -171,6 +175,7 @@ test('happy path (crate_name)', async function () {
login: 'wycats',
name: 'wycats',
url: 'https://github.com/wycats',
+ username: 'wycats',
},
],
meta: {
diff --git a/packages/crates-io-msw/handlers/teams/get.js b/packages/crates-io-msw/handlers/teams/get.js
index 8fb593aa11f..8e1a70a71d0 100644
--- a/packages/crates-io-msw/handlers/teams/get.js
+++ b/packages/crates-io-msw/handlers/teams/get.js
@@ -5,8 +5,8 @@ import { serializeTeam } from '../../serializers/team.js';
import { notFound } from '../../utils/handlers.js';
export default http.get('/api/v1/teams/:team_id', ({ params }) => {
- let login = params.team_id;
- let team = db.team.findFirst({ where: { login: { equals: login } } });
+ let username = params.team_id;
+ let team = db.team.findFirst({ where: { username: { equals: username } } });
if (!team) {
return notFound();
}
diff --git a/packages/crates-io-msw/handlers/teams/get.test.js b/packages/crates-io-msw/handlers/teams/get.test.js
index 7e164e26f74..35ae9f68d71 100644
--- a/packages/crates-io-msw/handlers/teams/get.test.js
+++ b/packages/crates-io-msw/handlers/teams/get.test.js
@@ -11,13 +11,14 @@ test('returns 404 for unknown teams', async function () {
test('returns a team object for known teams', async function () {
let team = db.team.create({ name: 'maintainers' });
- let response = await fetch(`/api/v1/teams/${team.login}`);
+ let response = await fetch(`/api/v1/teams/${team.username}`);
assert.strictEqual(response.status, 200);
assert.deepEqual(await response.json(), {
team: {
id: 1,
avatar: 'https://avatars1.githubusercontent.com/u/14631425?v=4',
login: 'github:rust-lang:maintainers',
+ username: 'github:rust-lang:maintainers',
name: 'maintainers',
url: 'https://github.com/rust-lang',
},
diff --git a/packages/crates-io-msw/handlers/users/get.js b/packages/crates-io-msw/handlers/users/get.js
index ee299d708d1..ef4a68caf7e 100644
--- a/packages/crates-io-msw/handlers/users/get.js
+++ b/packages/crates-io-msw/handlers/users/get.js
@@ -5,8 +5,8 @@ import { serializeUser } from '../../serializers/user.js';
import { notFound } from '../../utils/handlers.js';
export default http.get('/api/v1/users/:user_id', ({ params }) => {
- let login = params.user_id;
- let user = db.user.findFirst({ where: { login: { equals: login } } });
+ let username = params.user_id;
+ let user = db.user.findFirst({ where: { username: { equals: username } } });
if (!user) {
return notFound();
}
diff --git a/packages/crates-io-msw/handlers/users/get.test.js b/packages/crates-io-msw/handlers/users/get.test.js
index f5314186740..e5e7476e264 100644
--- a/packages/crates-io-msw/handlers/users/get.test.js
+++ b/packages/crates-io-msw/handlers/users/get.test.js
@@ -11,13 +11,14 @@ test('returns 404 for unknown users', async function () {
test('returns a user object for known users', async function () {
let user = db.user.create({ name: 'John Doe' });
- let response = await fetch(`/api/v1/users/${user.login}`);
+ let response = await fetch(`/api/v1/users/${user.username}`);
assert.strictEqual(response.status, 200);
assert.deepEqual(await response.json(), {
user: {
id: 1,
avatar: 'https://avatars1.githubusercontent.com/u/14631425?v=4',
login: 'john-doe',
+ username: 'john-doe',
name: 'John Doe',
url: 'https://github.com/john-doe',
},
diff --git a/packages/crates-io-msw/handlers/users/me.test.js b/packages/crates-io-msw/handlers/users/me.test.js
index fba39926019..f480abf57d7 100644
--- a/packages/crates-io-msw/handlers/users/me.test.js
+++ b/packages/crates-io-msw/handlers/users/me.test.js
@@ -17,6 +17,7 @@ test('returns the `user` resource including the private fields', async function
email_verified: true,
is_admin: false,
login: 'user-1',
+ username: 'user-1',
name: 'User 1',
publish_notifications: true,
url: 'https://github.com/user-1',
diff --git a/packages/crates-io-msw/handlers/versions/list.test.js b/packages/crates-io-msw/handlers/versions/list.test.js
index 263d0f0d007..372e015cbea 100644
--- a/packages/crates-io-msw/handlers/versions/list.test.js
+++ b/packages/crates-io-msw/handlers/versions/list.test.js
@@ -69,6 +69,7 @@ test('returns all versions belonging to the specified crate', async function ()
id: 1,
avatar: 'https://avatars1.githubusercontent.com/u/14631425?v=4',
login: 'user-1',
+ username: 'user-1',
name: 'User 1',
url: 'https://github.com/user-1',
},
diff --git a/packages/crates-io-msw/models/api-token.test.js b/packages/crates-io-msw/models/api-token.test.js
index 135b78352b9..867503fdc99 100644
--- a/packages/crates-io-msw/models/api-token.test.js
+++ b/packages/crates-io-msw/models/api-token.test.js
@@ -34,6 +34,7 @@ test('happy path', ({ expect }) => {
"name": "User 1",
"publishNotifications": true,
"url": "https://github.com/user-1",
+ "username": "user-1",
Symbol(type): "user",
Symbol(primaryKey): "id",
},
diff --git a/packages/crates-io-msw/models/crate-owner-invitation.test.js b/packages/crates-io-msw/models/crate-owner-invitation.test.js
index 32c10b97d52..4e9725b7f1f 100644
--- a/packages/crates-io-msw/models/crate-owner-invitation.test.js
+++ b/packages/crates-io-msw/models/crate-owner-invitation.test.js
@@ -66,6 +66,7 @@ test('happy path', ({ expect }) => {
"name": "User 2",
"publishNotifications": true,
"url": "https://github.com/user-2",
+ "username": "user-2",
Symbol(type): "user",
Symbol(primaryKey): "id",
},
@@ -81,6 +82,7 @@ test('happy path', ({ expect }) => {
"name": "User 1",
"publishNotifications": true,
"url": "https://github.com/user-1",
+ "username": "user-1",
Symbol(type): "user",
Symbol(primaryKey): "id",
},
diff --git a/packages/crates-io-msw/models/crate-ownership.test.js b/packages/crates-io-msw/models/crate-ownership.test.js
index 00f6b389824..3ef87c2d345 100644
--- a/packages/crates-io-msw/models/crate-ownership.test.js
+++ b/packages/crates-io-msw/models/crate-ownership.test.js
@@ -58,6 +58,7 @@ test('can set `team`', ({ expect }) => {
"name": "team-1",
"org": "rust-lang",
"url": "https://github.com/rust-lang",
+ "username": "github:rust-lang:team-1",
Symbol(type): "team",
Symbol(primaryKey): "id",
},
@@ -107,6 +108,7 @@ test('can set `user`', ({ expect }) => {
"name": "User 1",
"publishNotifications": true,
"url": "https://github.com/user-1",
+ "username": "user-1",
Symbol(type): "user",
Symbol(primaryKey): "id",
},
diff --git a/packages/crates-io-msw/models/msw-session.test.js b/packages/crates-io-msw/models/msw-session.test.js
index 5a6874566c9..d01a15f300c 100644
--- a/packages/crates-io-msw/models/msw-session.test.js
+++ b/packages/crates-io-msw/models/msw-session.test.js
@@ -24,6 +24,7 @@ test('happy path', ({ expect }) => {
"name": "User 1",
"publishNotifications": true,
"url": "https://github.com/user-1",
+ "username": "user-1",
Symbol(type): "user",
Symbol(primaryKey): "id",
},
diff --git a/packages/crates-io-msw/models/team.js b/packages/crates-io-msw/models/team.js
index 87df22a6f80..e7eea285e24 100644
--- a/packages/crates-io-msw/models/team.js
+++ b/packages/crates-io-msw/models/team.js
@@ -10,6 +10,7 @@ export default {
name: String,
org: String,
login: String,
+ username: String,
url: String,
avatar: String,
@@ -18,6 +19,7 @@ export default {
applyDefault(attrs, 'name', () => `team-${attrs.id}`);
applyDefault(attrs, 'org', () => ORGS[(attrs.id - 1) % ORGS.length]);
applyDefault(attrs, 'login', () => `github:${attrs.org}:${attrs.name}`);
+ applyDefault(attrs, 'username', () => `github:${attrs.org}:${attrs.name}`);
applyDefault(attrs, 'url', () => `https://github.com/${attrs.org}`);
applyDefault(attrs, 'avatar', () => 'https://avatars1.githubusercontent.com/u/14631425?v=4');
},
diff --git a/packages/crates-io-msw/models/team.test.js b/packages/crates-io-msw/models/team.test.js
index c3aecafd0f1..c5c39ed7bb2 100644
--- a/packages/crates-io-msw/models/team.test.js
+++ b/packages/crates-io-msw/models/team.test.js
@@ -12,6 +12,7 @@ test('default are applied', ({ expect }) => {
"name": "team-1",
"org": "rust-lang",
"url": "https://github.com/rust-lang",
+ "username": "github:rust-lang:team-1",
Symbol(type): "team",
Symbol(primaryKey): "id",
}
@@ -28,6 +29,7 @@ test('attributes can be set', ({ expect }) => {
"name": "axum",
"org": "tokio-rs",
"url": "https://github.com/tokio-rs",
+ "username": "github:tokio-rs:axum",
Symbol(type): "team",
Symbol(primaryKey): "id",
}
diff --git a/packages/crates-io-msw/models/user.js b/packages/crates-io-msw/models/user.js
index 2d3ce6f22f4..9e2999fb91c 100644
--- a/packages/crates-io-msw/models/user.js
+++ b/packages/crates-io-msw/models/user.js
@@ -8,6 +8,7 @@ export default {
name: nullable(String),
login: String,
+ username: String,
url: String,
avatar: String,
email: nullable(String),
@@ -22,7 +23,8 @@ export default {
applyDefault(attrs, 'id', () => counter);
applyDefault(attrs, 'name', () => `User ${attrs.id}`);
applyDefault(attrs, 'login', () => (attrs.name ? dasherize(attrs.name) : `user-${attrs.id}`));
- applyDefault(attrs, 'email', () => `${attrs.login}@crates.io`);
+ applyDefault(attrs, 'username', () => (attrs.name ? dasherize(attrs.name) : `user-${attrs.id}`));
+ applyDefault(attrs, 'email', () => `${attrs.username}@crates.io`);
applyDefault(attrs, 'url', () => `https://github.com/${attrs.login}`);
applyDefault(attrs, 'avatar', () => 'https://avatars1.githubusercontent.com/u/14631425?v=4');
applyDefault(attrs, 'emailVerificationToken', () => null);
diff --git a/packages/crates-io-msw/models/user.test.js b/packages/crates-io-msw/models/user.test.js
index e3db559e569..e15bba659d6 100644
--- a/packages/crates-io-msw/models/user.test.js
+++ b/packages/crates-io-msw/models/user.test.js
@@ -17,6 +17,7 @@ test('default are applied', ({ expect }) => {
"name": "User 1",
"publishNotifications": true,
"url": "https://github.com/user-1",
+ "username": "user-1",
Symbol(type): "user",
Symbol(primaryKey): "id",
}
@@ -38,6 +39,7 @@ test('name can be set', ({ expect }) => {
"name": "John Doe",
"publishNotifications": true,
"url": "https://github.com/john-doe",
+ "username": "john-doe",
Symbol(type): "user",
Symbol(primaryKey): "id",
}
diff --git a/script/backfill-linked-accounts.sql b/script/backfill-linked-accounts.sql
new file mode 100644
index 00000000000..f2413cd110e
--- /dev/null
+++ b/script/backfill-linked-accounts.sql
@@ -0,0 +1,7 @@
+INSERT INTO linked_accounts (user_id, provider, account_id, access_token, login, avatar)
+SELECT id, 0, gh_id, gh_access_token, gh_login, gh_avatar
+FROM users
+LEFT JOIN linked_accounts
+ON users.id = linked_accounts.user_id
+WHERE gh_id != -1
+AND linked_accounts.user_id IS NULL;
diff --git a/script/backfill-usernames.sql b/script/backfill-usernames.sql
new file mode 100644
index 00000000000..59372812ce3
--- /dev/null
+++ b/script/backfill-usernames.sql
@@ -0,0 +1,2 @@
+UPDATE users
+SET username = gh_login;
diff --git a/src/controllers/crate_owner_invitation.rs b/src/controllers/crate_owner_invitation.rs
index 24547c0e650..6fd88928e14 100644
--- a/src/controllers/crate_owner_invitation.rs
+++ b/src/controllers/crate_owner_invitation.rs
@@ -63,7 +63,7 @@ pub async fn list_crate_owner_invitations_for_user(
.iter()
.find(|u| u.id == private.inviter_id)
.ok_or_else(|| internal(format!("missing user {}", private.inviter_id)))?
- .login
+ .username
.clone(),
invitee_id: private.invitee_id,
inviter_id: private.inviter_id,
diff --git a/src/controllers/krate/owners.rs b/src/controllers/krate/owners.rs
index e3482ba6e59..0bebd0cd3cb 100644
--- a/src/controllers/krate/owners.rs
+++ b/src/controllers/krate/owners.rs
@@ -328,7 +328,7 @@ async fn invite_user_owner(
let user = User::find_by_login(conn, login)
.await
.optional()?
- .ok_or_else(|| bad_request(format_args!("could not find user with login `{login}`")))?;
+ .ok_or_else(|| bad_request(format_args!("could not find user with username `{login}`")))?;
// Users are invited and must accept before being added
let expires_at = Utc::now() + app.config.ownership_invitations_expiration;
@@ -498,7 +498,7 @@ impl From for BoxedAppError {
match error {
OwnerRemoveError::Diesel(error) => error.into(),
OwnerRemoveError::NotFound { login } => {
- bad_request(format!("could not find owner with login `{login}`"))
+ bad_request(format!("could not find owner with username `{login}`"))
}
}
}
diff --git a/src/controllers/session.rs b/src/controllers/session.rs
index 44266c356a3..8dd53424e04 100644
--- a/src/controllers/session.rs
+++ b/src/controllers/session.rs
@@ -10,7 +10,7 @@ use crate::app::AppState;
use crate::controllers::user::update::UserConfirmEmail;
use crate::email::Emails;
use crate::middleware::log_request::RequestLogExt;
-use crate::models::{NewEmail, NewUser, User};
+use crate::models::{AccountProvider, NewEmail, NewLinkedAccount, NewUser, User};
use crate::schema::users;
use crate::util::diesel::is_read_only_error;
use crate::util::errors::{AppResult, bad_request, server_error};
@@ -132,6 +132,7 @@ pub async fn save_user_to_database(
let new_user = NewUser::builder()
.gh_id(user.id)
.gh_login(&user.login)
+ .username(&user.login)
.maybe_name(user.name.as_deref())
.maybe_gh_avatar(user.avatar_url.as_deref())
.gh_access_token(access_token)
@@ -162,6 +163,21 @@ async fn create_or_update_user(
async move {
let user = new_user.insert_or_update(conn).await?;
+ // To assist in eventually someday allowing OAuth with more than GitHub, also
+ // write the GitHub info to the `linked_accounts` table. Nothing currently reads
+ // from this table. Only log errors but don't fail login if this writing fails.
+ let new_linked_account = NewLinkedAccount::builder()
+ .user_id(user.id)
+ .provider(AccountProvider::Github)
+ .account_id(user.gh_id)
+ .access_token(new_user.gh_access_token)
+ .login(&user.gh_login)
+ .maybe_avatar(user.gh_avatar.as_deref())
+ .build();
+ if let Err(e) = new_linked_account.insert_or_update(conn).await {
+ info!("Error inserting or updating linked_accounts record: {e}");
+ }
+
// To send the user an account verification email
if let Some(user_email) = email {
let new_email = NewEmail::builder()
diff --git a/src/controllers/user/other.rs b/src/controllers/user/other.rs
index b0ac93b29ba..f903a32b395 100644
--- a/src/controllers/user/other.rs
+++ b/src/controllers/user/other.rs
@@ -16,12 +16,12 @@ pub struct GetResponse {
pub user: EncodablePublicUser,
}
-/// Find user by login.
+/// Find user by username.
#[utoipa::path(
get,
path = "/api/v1/users/{user}",
params(
- ("user" = String, Path, description = "Login name of the user"),
+ ("user" = String, Path, description = "Crates.io username of the user"),
),
tag = "users",
responses((status = 200, description = "Successful Response", body = inline(GetResponse))),
@@ -41,7 +41,11 @@ pub async fn find_user(
.first(&mut conn)
.await?;
- Ok(Json(GetResponse { user: user.into() }))
+ let linked_accounts = user.linked_accounts(&mut conn).await?;
+
+ Ok(Json(GetResponse {
+ user: EncodablePublicUser::with_linked_accounts(user, &linked_accounts),
+ }))
}
#[derive(Debug, Serialize, utoipa::ToSchema)]
diff --git a/src/index.rs b/src/index.rs
index 0e52eb91962..08eacf2266a 100644
--- a/src/index.rs
+++ b/src/index.rs
@@ -153,6 +153,7 @@ mod tests {
let user_id = diesel::insert_into(users::table)
.values((
users::name.eq("user1"),
+ users::username.eq("user1"),
users::gh_login.eq("user1"),
users::gh_id.eq(42),
users::gh_access_token.eq("some random token"),
diff --git a/src/rate_limiter.rs b/src/rate_limiter.rs
index 5416a9ffaa4..4b4da12db2c 100644
--- a/src/rate_limiter.rs
+++ b/src/rate_limiter.rs
@@ -706,6 +706,7 @@ mod tests {
NewUser::builder()
.gh_id(0)
.gh_login(gh_login)
+ .username(gh_login)
.gh_access_token("some random token")
.build()
.insert(conn)
diff --git a/src/snapshots/crates_io__openapi__tests__openapi_snapshot.snap b/src/snapshots/crates_io__openapi__tests__openapi_snapshot.snap
index cb7a074036a..e2f3ea8bce8 100644
--- a/src/snapshots/crates_io__openapi__tests__openapi_snapshot.snap
+++ b/src/snapshots/crates_io__openapi__tests__openapi_snapshot.snap
@@ -1,6 +1,7 @@
---
source: src/openapi.rs
expression: response.json()
+snapshot_kind: text
---
{
"components": {
@@ -117,7 +118,7 @@ expression: response.json()
"type": "boolean"
},
"login": {
- "description": "The user's login name.",
+ "description": "The user's GitHub login.",
"example": "ghost",
"type": "string"
},
@@ -141,11 +142,17 @@ expression: response.json()
"string",
"null"
]
+ },
+ "username": {
+ "description": "The user's crates.io username.",
+ "example": "ghost",
+ "type": "string"
}
},
"required": [
"id",
"login",
+ "username",
"email_verified",
"email_verification_sent",
"is_admin",
@@ -707,7 +714,7 @@ expression: response.json()
"type": "string"
},
"login": {
- "description": "The login name of the team or user.",
+ "description": "The GitHub login of the team or user.",
"example": "ghost",
"type": "string"
},
@@ -726,11 +733,17 @@ expression: response.json()
"string",
"null"
]
+ },
+ "username": {
+ "description": "The crates.io username of the team or user.",
+ "example": "ghost",
+ "type": "string"
}
},
"required": [
"id",
"login",
+ "username",
"kind"
],
"type": "object"
@@ -852,8 +865,19 @@ expression: response.json()
"format": "int32",
"type": "integer"
},
+ "linked_accounts": {
+ "description": "The accounts linked to this crates.io account.",
+ "example": [],
+ "items": {
+ "$ref": "#/components/schemas/LinkedAccount"
+ },
+ "type": [
+ "array",
+ "null"
+ ]
+ },
"login": {
- "description": "The user's login name.",
+ "description": "The user's GitHub login name.",
"example": "ghost",
"type": "string"
},
@@ -869,11 +893,17 @@ expression: response.json()
"description": "The user's GitHub profile URL.",
"example": "https://github.com/ghost",
"type": "string"
+ },
+ "username": {
+ "description": "The user's crates.io username.",
+ "example": "ghost",
+ "type": "string"
}
},
"required": [
"id",
"login",
+ "username",
"url"
],
"type": "object"
@@ -4148,7 +4178,7 @@ expression: response.json()
"operationId": "find_user",
"parameters": [
{
- "description": "Login name of the user",
+ "description": "Crates.io username of the user",
"in": "path",
"name": "user",
"required": true,
@@ -4177,7 +4207,7 @@ expression: response.json()
"description": "Successful Response"
}
},
- "summary": "Find user by login.",
+ "summary": "Find user by username.",
"tags": [
"users"
]
diff --git a/src/tests/dump_db.rs b/src/tests/dump_db.rs
index c7d0e549ac8..bce2be568e8 100644
--- a/src/tests/dump_db.rs
+++ b/src/tests/dump_db.rs
@@ -52,6 +52,7 @@ async fn test_dump_db_job() -> anyhow::Result<()> {
"YYYY-MM-DD-HHMMSS/data/crate_downloads.csv",
"YYYY-MM-DD-HHMMSS/data/crates.csv",
"YYYY-MM-DD-HHMMSS/data/keywords.csv",
+ "YYYY-MM-DD-HHMMSS/data/linked_accounts.csv",
"YYYY-MM-DD-HHMMSS/data/metadata.csv",
"YYYY-MM-DD-HHMMSS/data/reserved_crate_names.csv",
"YYYY-MM-DD-HHMMSS/data/teams.csv",
@@ -84,6 +85,7 @@ async fn test_dump_db_job() -> anyhow::Result<()> {
"data/crate_downloads.csv",
"data/crates.csv",
"data/keywords.csv",
+ "data/linked_accounts.csv",
"data/metadata.csv",
"data/reserved_crate_names.csv",
"data/teams.csv",
diff --git a/src/tests/issues/issue1205.rs b/src/tests/issues/issue1205.rs
index 1229746dc0e..01260cd8209 100644
--- a/src/tests/issues/issue1205.rs
+++ b/src/tests/issues/issue1205.rs
@@ -46,7 +46,7 @@ async fn test_issue_1205() -> anyhow::Result<()> {
.remove_named_owner(CRATE_NAME, "github:rustaudio:owners")
.await;
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
- assert_snapshot!(response.text(), @r#"{"errors":[{"detail":"could not find owner with login `github:rustaudio:owners`"}]}"#);
+ assert_snapshot!(response.text(), @r#"{"errors":[{"detail":"could not find owner with username `github:rustaudio:owners`"}]}"#);
Ok(())
}
diff --git a/src/tests/krate/publish/snapshots/crates_io__tests__krate__publish__edition__edition_is_saved-2.snap b/src/tests/krate/publish/snapshots/crates_io__tests__krate__publish__edition__edition_is_saved-2.snap
index 02666658d2f..ce18851935a 100644
--- a/src/tests/krate/publish/snapshots/crates_io__tests__krate__publish__edition__edition_is_saved-2.snap
+++ b/src/tests/krate/publish/snapshots/crates_io__tests__krate__publish__edition__edition_is_saved-2.snap
@@ -1,6 +1,7 @@
---
source: src/tests/krate/publish/edition.rs
expression: response.json()
+snapshot_kind: text
---
{
"version": {
@@ -13,7 +14,8 @@ expression: response.json()
"id": "[id]",
"login": "foo",
"name": null,
- "url": "https://github.com/foo"
+ "url": "https://github.com/foo",
+ "username": "foo"
}
}
],
@@ -44,7 +46,8 @@ expression: response.json()
"id": "[id]",
"login": "foo",
"name": null,
- "url": "https://github.com/foo"
+ "url": "https://github.com/foo",
+ "username": "foo"
},
"readme_path": "/api/v1/crates/foo/1.0.0/readme",
"repository": null,
diff --git a/src/tests/krate/publish/snapshots/crates_io__tests__krate__publish__links__crate_with_links_field-2.snap b/src/tests/krate/publish/snapshots/crates_io__tests__krate__publish__links__crate_with_links_field-2.snap
index e42da14510a..3c046059c4c 100644
--- a/src/tests/krate/publish/snapshots/crates_io__tests__krate__publish__links__crate_with_links_field-2.snap
+++ b/src/tests/krate/publish/snapshots/crates_io__tests__krate__publish__links__crate_with_links_field-2.snap
@@ -1,6 +1,7 @@
---
source: src/tests/krate/publish/links.rs
expression: response.json()
+snapshot_kind: text
---
{
"version": {
@@ -13,7 +14,8 @@ expression: response.json()
"id": 1,
"login": "foo",
"name": null,
- "url": "https://github.com/foo"
+ "url": "https://github.com/foo",
+ "username": "foo"
}
}
],
@@ -44,7 +46,8 @@ expression: response.json()
"id": "[id]",
"login": "foo",
"name": null,
- "url": "https://github.com/foo"
+ "url": "https://github.com/foo",
+ "username": "foo"
},
"readme_path": "/api/v1/crates/foo/1.0.0/readme",
"repository": null,
diff --git a/src/tests/krate/publish/snapshots/crates_io__tests__krate__publish__manifest__boolean_readme-2.snap b/src/tests/krate/publish/snapshots/crates_io__tests__krate__publish__manifest__boolean_readme-2.snap
index 6f61d116c53..6b939f6d35c 100644
--- a/src/tests/krate/publish/snapshots/crates_io__tests__krate__publish__manifest__boolean_readme-2.snap
+++ b/src/tests/krate/publish/snapshots/crates_io__tests__krate__publish__manifest__boolean_readme-2.snap
@@ -1,6 +1,7 @@
---
source: src/tests/krate/publish/manifest.rs
expression: response.json()
+snapshot_kind: text
---
{
"version": {
@@ -13,7 +14,8 @@ expression: response.json()
"id": "[id]",
"login": "foo",
"name": null,
- "url": "https://github.com/foo"
+ "url": "https://github.com/foo",
+ "username": "foo"
}
}
],
@@ -44,7 +46,8 @@ expression: response.json()
"id": "[id]",
"login": "foo",
"name": null,
- "url": "https://github.com/foo"
+ "url": "https://github.com/foo",
+ "username": "foo"
},
"readme_path": "/api/v1/crates/foo/1.0.0/readme",
"repository": null,
diff --git a/src/tests/krate/publish/snapshots/crates_io__tests__krate__publish__manifest__lib_and_bin_crate-2.snap b/src/tests/krate/publish/snapshots/crates_io__tests__krate__publish__manifest__lib_and_bin_crate-2.snap
index 328af1f7ec4..070885d4fca 100644
--- a/src/tests/krate/publish/snapshots/crates_io__tests__krate__publish__manifest__lib_and_bin_crate-2.snap
+++ b/src/tests/krate/publish/snapshots/crates_io__tests__krate__publish__manifest__lib_and_bin_crate-2.snap
@@ -1,6 +1,7 @@
---
source: src/tests/krate/publish/manifest.rs
expression: response.json()
+snapshot_kind: text
---
{
"version": {
@@ -13,7 +14,8 @@ expression: response.json()
"id": "[id]",
"login": "foo",
"name": null,
- "url": "https://github.com/foo"
+ "url": "https://github.com/foo",
+ "username": "foo"
}
}
],
@@ -47,7 +49,8 @@ expression: response.json()
"id": "[id]",
"login": "foo",
"name": null,
- "url": "https://github.com/foo"
+ "url": "https://github.com/foo",
+ "username": "foo"
},
"readme_path": "/api/v1/crates/foo/1.0.0/readme",
"repository": null,
diff --git a/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-2.snap b/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-2.snap
index 9e8b8ef7e78..6bd08f4220f 100644
--- a/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-2.snap
+++ b/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-2.snap
@@ -1,6 +1,7 @@
---
source: src/tests/krate/yanking.rs
expression: json
+snapshot_kind: text
---
{
"version": {
@@ -26,6 +27,7 @@ expression: json
"published_by": {
"id": 1,
"login": "foo",
+ "username": "foo",
"name": null,
"avatar": null,
"url": "https://github.com/foo"
@@ -36,6 +38,7 @@ expression: json
"user": {
"id": 1,
"login": "foo",
+ "username": "foo",
"name": null,
"avatar": null,
"url": "https://github.com/foo"
@@ -47,6 +50,7 @@ expression: json
"user": {
"id": 1,
"login": "foo",
+ "username": "foo",
"name": null,
"avatar": null,
"url": "https://github.com/foo"
diff --git a/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-3.snap b/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-3.snap
index 896ce3ae55b..3cc88906683 100644
--- a/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-3.snap
+++ b/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-3.snap
@@ -1,6 +1,7 @@
---
source: src/tests/krate/yanking.rs
expression: json
+snapshot_kind: text
---
{
"version": {
@@ -26,6 +27,7 @@ expression: json
"published_by": {
"id": 1,
"login": "foo",
+ "username": "foo",
"name": null,
"avatar": null,
"url": "https://github.com/foo"
@@ -36,6 +38,7 @@ expression: json
"user": {
"id": 1,
"login": "foo",
+ "username": "foo",
"name": null,
"avatar": null,
"url": "https://github.com/foo"
@@ -47,6 +50,7 @@ expression: json
"user": {
"id": 1,
"login": "foo",
+ "username": "foo",
"name": null,
"avatar": null,
"url": "https://github.com/foo"
@@ -58,6 +62,7 @@ expression: json
"user": {
"id": 1,
"login": "foo",
+ "username": "foo",
"name": null,
"avatar": null,
"url": "https://github.com/foo"
diff --git a/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-4.snap b/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-4.snap
index 896ce3ae55b..3cc88906683 100644
--- a/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-4.snap
+++ b/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-4.snap
@@ -1,6 +1,7 @@
---
source: src/tests/krate/yanking.rs
expression: json
+snapshot_kind: text
---
{
"version": {
@@ -26,6 +27,7 @@ expression: json
"published_by": {
"id": 1,
"login": "foo",
+ "username": "foo",
"name": null,
"avatar": null,
"url": "https://github.com/foo"
@@ -36,6 +38,7 @@ expression: json
"user": {
"id": 1,
"login": "foo",
+ "username": "foo",
"name": null,
"avatar": null,
"url": "https://github.com/foo"
@@ -47,6 +50,7 @@ expression: json
"user": {
"id": 1,
"login": "foo",
+ "username": "foo",
"name": null,
"avatar": null,
"url": "https://github.com/foo"
@@ -58,6 +62,7 @@ expression: json
"user": {
"id": 1,
"login": "foo",
+ "username": "foo",
"name": null,
"avatar": null,
"url": "https://github.com/foo"
diff --git a/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-5.snap b/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-5.snap
index e6938557e70..2a931ebc0d9 100644
--- a/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-5.snap
+++ b/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-5.snap
@@ -1,6 +1,7 @@
---
source: src/tests/krate/yanking.rs
expression: json
+snapshot_kind: text
---
{
"version": {
@@ -26,6 +27,7 @@ expression: json
"published_by": {
"id": 1,
"login": "foo",
+ "username": "foo",
"name": null,
"avatar": null,
"url": "https://github.com/foo"
@@ -36,6 +38,7 @@ expression: json
"user": {
"id": 1,
"login": "foo",
+ "username": "foo",
"name": null,
"avatar": null,
"url": "https://github.com/foo"
@@ -47,6 +50,7 @@ expression: json
"user": {
"id": 1,
"login": "foo",
+ "username": "foo",
"name": null,
"avatar": null,
"url": "https://github.com/foo"
@@ -58,6 +62,7 @@ expression: json
"user": {
"id": 1,
"login": "foo",
+ "username": "foo",
"name": null,
"avatar": null,
"url": "https://github.com/foo"
@@ -69,6 +74,7 @@ expression: json
"user": {
"id": 1,
"login": "foo",
+ "username": "foo",
"name": null,
"avatar": null,
"url": "https://github.com/foo"
diff --git a/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-6.snap b/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-6.snap
index e6938557e70..2a931ebc0d9 100644
--- a/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-6.snap
+++ b/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-6.snap
@@ -1,6 +1,7 @@
---
source: src/tests/krate/yanking.rs
expression: json
+snapshot_kind: text
---
{
"version": {
@@ -26,6 +27,7 @@ expression: json
"published_by": {
"id": 1,
"login": "foo",
+ "username": "foo",
"name": null,
"avatar": null,
"url": "https://github.com/foo"
@@ -36,6 +38,7 @@ expression: json
"user": {
"id": 1,
"login": "foo",
+ "username": "foo",
"name": null,
"avatar": null,
"url": "https://github.com/foo"
@@ -47,6 +50,7 @@ expression: json
"user": {
"id": 1,
"login": "foo",
+ "username": "foo",
"name": null,
"avatar": null,
"url": "https://github.com/foo"
@@ -58,6 +62,7 @@ expression: json
"user": {
"id": 1,
"login": "foo",
+ "username": "foo",
"name": null,
"avatar": null,
"url": "https://github.com/foo"
@@ -69,6 +74,7 @@ expression: json
"user": {
"id": 1,
"login": "foo",
+ "username": "foo",
"name": null,
"avatar": null,
"url": "https://github.com/foo"
diff --git a/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank.snap b/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank.snap
index 9e8b8ef7e78..6bd08f4220f 100644
--- a/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank.snap
+++ b/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank.snap
@@ -1,6 +1,7 @@
---
source: src/tests/krate/yanking.rs
expression: json
+snapshot_kind: text
---
{
"version": {
@@ -26,6 +27,7 @@ expression: json
"published_by": {
"id": 1,
"login": "foo",
+ "username": "foo",
"name": null,
"avatar": null,
"url": "https://github.com/foo"
@@ -36,6 +38,7 @@ expression: json
"user": {
"id": 1,
"login": "foo",
+ "username": "foo",
"name": null,
"avatar": null,
"url": "https://github.com/foo"
@@ -47,6 +50,7 @@ expression: json
"user": {
"id": 1,
"login": "foo",
+ "username": "foo",
"name": null,
"avatar": null,
"url": "https://github.com/foo"
diff --git a/src/tests/mod.rs b/src/tests/mod.rs
index 9d0ee1983a3..5b642f63278 100644
--- a/src/tests/mod.rs
+++ b/src/tests/mod.rs
@@ -95,6 +95,7 @@ fn new_user(login: &str) -> NewUser<'_> {
NewUser::builder()
.gh_id(next_gh_id())
.gh_login(login)
+ .username(login)
.gh_access_token("some random token")
.build()
}
diff --git a/src/tests/owners.rs b/src/tests/owners.rs
index c0c7f892a3d..51870813cc1 100644
--- a/src/tests/owners.rs
+++ b/src/tests/owners.rs
@@ -772,6 +772,7 @@ async fn inactive_users_dont_get_invitations() {
NewUser::builder()
.gh_id(-1)
.gh_login(invited_gh_login)
+ .username(invited_gh_login)
.gh_access_token("some random token")
.build()
.insert(&mut conn)
diff --git a/src/tests/routes/crates/owners/add.rs b/src/tests/routes/crates/owners/add.rs
index f2aa0a8f02c..79649abebdc 100644
--- a/src/tests/routes/crates/owners/add.rs
+++ b/src/tests/routes/crates/owners/add.rs
@@ -305,7 +305,7 @@ async fn test_unknown_user() {
let response = cookie.add_named_owner("foo", "unknown").await;
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
- assert_snapshot!(response.text(), @r#"{"errors":[{"detail":"could not find user with login `unknown`"}]}"#);
+ assert_snapshot!(response.text(), @r#"{"errors":[{"detail":"could not find user with username `unknown`"}]}"#);
}
#[tokio::test(flavor = "multi_thread")]
@@ -370,7 +370,7 @@ async fn no_invite_emails_for_txn_rollback() {
let response = token.add_named_owners("crate_name", &usernames).await;
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
- assert_snapshot!(response.text(), @r#"{"errors":[{"detail":"could not find user with login `bananas`"}]}"#);
+ assert_snapshot!(response.text(), @r#"{"errors":[{"detail":"could not find user with username `bananas`"}]}"#);
// No emails should have been sent.
assert_eq!(app.emails().await.len(), 0);
diff --git a/src/tests/routes/crates/owners/remove.rs b/src/tests/routes/crates/owners/remove.rs
index 669248aef07..9ef01b23a5a 100644
--- a/src/tests/routes/crates/owners/remove.rs
+++ b/src/tests/routes/crates/owners/remove.rs
@@ -61,7 +61,7 @@ async fn test_unknown_user() {
let response = cookie.remove_named_owner("foo", "unknown").await;
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
- assert_snapshot!(response.text(), @r#"{"errors":[{"detail":"could not find owner with login `unknown`"}]}"#);
+ assert_snapshot!(response.text(), @r#"{"errors":[{"detail":"could not find owner with username `unknown`"}]}"#);
}
#[tokio::test(flavor = "multi_thread")]
@@ -77,7 +77,7 @@ async fn test_unknown_team() {
.remove_named_owner("foo", "github:unknown:unknown")
.await;
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
- assert_snapshot!(response.text(), @r#"{"errors":[{"detail":"could not find owner with login `github:unknown:unknown`"}]}"#);
+ assert_snapshot!(response.text(), @r#"{"errors":[{"detail":"could not find owner with username `github:unknown:unknown`"}]}"#);
}
#[tokio::test(flavor = "multi_thread")]
diff --git a/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__read__include_default_version.snap b/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__read__include_default_version.snap
index c7b079c37a4..a06971a8993 100644
--- a/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__read__include_default_version.snap
+++ b/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__read__include_default_version.snap
@@ -1,6 +1,7 @@
---
source: src/tests/routes/crates/read.rs
expression: response.json()
+snapshot_kind: text
---
{
"categories": null,
@@ -66,7 +67,8 @@ expression: response.json()
"id": 1,
"login": "foo",
"name": null,
- "url": "https://github.com/foo"
+ "url": "https://github.com/foo",
+ "username": "foo"
},
"readme_path": "/api/v1/crates/foo_default_version/0.5.1/readme",
"repository": null,
diff --git a/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__read__show.snap b/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__read__show.snap
index 75c2cd698be..a4a002164e6 100644
--- a/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__read__show.snap
+++ b/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__read__show.snap
@@ -1,6 +1,7 @@
---
source: src/tests/routes/crates/read.rs
expression: response.json()
+snapshot_kind: text
---
{
"categories": [],
@@ -79,7 +80,8 @@ expression: response.json()
"id": 1,
"login": "foo",
"name": null,
- "url": "https://github.com/foo"
+ "url": "https://github.com/foo",
+ "username": "foo"
},
"readme_path": "/api/v1/crates/foo_show/0.5.1/readme",
"repository": null,
@@ -117,7 +119,8 @@ expression: response.json()
"id": 1,
"login": "foo",
"name": null,
- "url": "https://github.com/foo"
+ "url": "https://github.com/foo",
+ "username": "foo"
},
"readme_path": "/api/v1/crates/foo_show/0.5.0/readme",
"repository": null,
diff --git a/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__read__show_all_yanked.snap b/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__read__show_all_yanked.snap
index 30269ad6920..b20d9bf3bf0 100644
--- a/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__read__show_all_yanked.snap
+++ b/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__read__show_all_yanked.snap
@@ -1,6 +1,7 @@
---
source: src/tests/routes/crates/read.rs
expression: response.json()
+snapshot_kind: text
---
{
"categories": [],
@@ -78,7 +79,8 @@ expression: response.json()
"id": 1,
"login": "foo",
"name": null,
- "url": "https://github.com/foo"
+ "url": "https://github.com/foo",
+ "username": "foo"
},
"readme_path": "/api/v1/crates/foo_show/0.5.0/readme",
"repository": null,
@@ -116,7 +118,8 @@ expression: response.json()
"id": 1,
"login": "foo",
"name": null,
- "url": "https://github.com/foo"
+ "url": "https://github.com/foo",
+ "username": "foo"
},
"readme_path": "/api/v1/crates/foo_show/1.0.0/readme",
"repository": null,
diff --git a/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__prerelease_versions_not_included_in_reverse_dependencies.snap b/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__prerelease_versions_not_included_in_reverse_dependencies.snap
index 27e4773acf2..0b71ee66752 100644
--- a/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__prerelease_versions_not_included_in_reverse_dependencies.snap
+++ b/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__prerelease_versions_not_included_in_reverse_dependencies.snap
@@ -1,6 +1,7 @@
---
source: src/tests/routes/crates/reverse_dependencies.rs
expression: response.json()
+snapshot_kind: text
---
{
"dependencies": [
@@ -50,7 +51,8 @@ expression: response.json()
"id": 1,
"login": "foo",
"name": null,
- "url": "https://github.com/foo"
+ "url": "https://github.com/foo",
+ "username": "foo"
},
"readme_path": "/api/v1/crates/c3/1.0.0/readme",
"repository": null,
diff --git a/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__reverse_dependencies.snap b/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__reverse_dependencies.snap
index 7ca7deae51e..7e06a811058 100644
--- a/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__reverse_dependencies.snap
+++ b/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__reverse_dependencies.snap
@@ -1,6 +1,7 @@
---
source: src/tests/routes/crates/reverse_dependencies.rs
expression: response.json()
+snapshot_kind: text
---
{
"dependencies": [
@@ -50,7 +51,8 @@ expression: response.json()
"id": 1,
"login": "foo",
"name": null,
- "url": "https://github.com/foo"
+ "url": "https://github.com/foo",
+ "username": "foo"
},
"readme_path": "/api/v1/crates/c2/1.1.0/readme",
"repository": null,
diff --git a/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__reverse_dependencies_includes_published_by_user_when_present.snap b/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__reverse_dependencies_includes_published_by_user_when_present.snap
index 94dde205c95..dd548de43c3 100644
--- a/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__reverse_dependencies_includes_published_by_user_when_present.snap
+++ b/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__reverse_dependencies_includes_published_by_user_when_present.snap
@@ -1,6 +1,7 @@
---
source: src/tests/routes/crates/reverse_dependencies.rs
expression: response.json()
+snapshot_kind: text
---
{
"dependencies": [
@@ -62,7 +63,8 @@ expression: response.json()
"id": 1,
"login": "foo",
"name": null,
- "url": "https://github.com/foo"
+ "url": "https://github.com/foo",
+ "username": "foo"
},
"readme_path": "/api/v1/crates/c3/3.0.0/readme",
"repository": null,
diff --git a/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__reverse_dependencies_query_supports_u64_version_number_parts.snap b/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__reverse_dependencies_query_supports_u64_version_number_parts.snap
index 6850cb4856b..8c363689879 100644
--- a/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__reverse_dependencies_query_supports_u64_version_number_parts.snap
+++ b/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__reverse_dependencies_query_supports_u64_version_number_parts.snap
@@ -1,6 +1,7 @@
---
source: src/tests/routes/crates/reverse_dependencies.rs
expression: response.json()
+snapshot_kind: text
---
{
"dependencies": [
@@ -50,7 +51,8 @@ expression: response.json()
"id": 1,
"login": "foo",
"name": null,
- "url": "https://github.com/foo"
+ "url": "https://github.com/foo",
+ "username": "foo"
},
"readme_path": "/api/v1/crates/c2/1.0.18446744073709551615/readme",
"repository": null,
diff --git a/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__reverse_dependencies_when_old_version_doesnt_depend_but_new_does.snap b/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__reverse_dependencies_when_old_version_doesnt_depend_but_new_does.snap
index 6e17a2fbc8f..ab9cafea9d9 100644
--- a/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__reverse_dependencies_when_old_version_doesnt_depend_but_new_does.snap
+++ b/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__reverse_dependencies_when_old_version_doesnt_depend_but_new_does.snap
@@ -1,6 +1,7 @@
---
source: src/tests/routes/crates/reverse_dependencies.rs
expression: response.json()
+snapshot_kind: text
---
{
"dependencies": [
@@ -50,7 +51,8 @@ expression: response.json()
"id": 1,
"login": "foo",
"name": null,
- "url": "https://github.com/foo"
+ "url": "https://github.com/foo",
+ "username": "foo"
},
"readme_path": "/api/v1/crates/c2/2.0.0/readme",
"repository": null,
diff --git a/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__yanked_versions_not_included_in_reverse_dependencies.snap b/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__yanked_versions_not_included_in_reverse_dependencies.snap
index 6e17a2fbc8f..ab9cafea9d9 100644
--- a/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__yanked_versions_not_included_in_reverse_dependencies.snap
+++ b/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__yanked_versions_not_included_in_reverse_dependencies.snap
@@ -1,6 +1,7 @@
---
source: src/tests/routes/crates/reverse_dependencies.rs
expression: response.json()
+snapshot_kind: text
---
{
"dependencies": [
@@ -50,7 +51,8 @@ expression: response.json()
"id": 1,
"login": "foo",
"name": null,
- "url": "https://github.com/foo"
+ "url": "https://github.com/foo",
+ "username": "foo"
},
"readme_path": "/api/v1/crates/c2/2.0.0/readme",
"repository": null,
diff --git a/src/tests/routes/crates/versions/snapshots/crates_io__tests__routes__crates__versions__list__versions.snap b/src/tests/routes/crates/versions/snapshots/crates_io__tests__routes__crates__versions__list__versions.snap
index d0546ef9c91..6a78599d2d5 100644
--- a/src/tests/routes/crates/versions/snapshots/crates_io__tests__routes__crates__versions__list__versions.snap
+++ b/src/tests/routes/crates/versions/snapshots/crates_io__tests__routes__crates__versions__list__versions.snap
@@ -1,6 +1,7 @@
---
source: src/tests/routes/crates/versions/list.rs
expression: response.json()
+snapshot_kind: text
---
{
"meta": {
@@ -69,7 +70,8 @@ expression: response.json()
"id": 1,
"login": "foo",
"name": null,
- "url": "https://github.com/foo"
+ "url": "https://github.com/foo",
+ "username": "foo"
},
"readme_path": "/api/v1/crates/foo_versions/0.5.1/readme",
"repository": null,
@@ -107,7 +109,8 @@ expression: response.json()
"id": 1,
"login": "foo",
"name": null,
- "url": "https://github.com/foo"
+ "url": "https://github.com/foo",
+ "username": "foo"
},
"readme_path": "/api/v1/crates/foo_versions/0.5.0/readme",
"repository": null,
diff --git a/src/tests/routes/crates/versions/snapshots/crates_io__tests__routes__crates__versions__read__show_by_crate_name_and_version.snap b/src/tests/routes/crates/versions/snapshots/crates_io__tests__routes__crates__versions__read__show_by_crate_name_and_version.snap
index 6940293e5ca..96f047cf709 100644
--- a/src/tests/routes/crates/versions/snapshots/crates_io__tests__routes__crates__versions__read__show_by_crate_name_and_version.snap
+++ b/src/tests/routes/crates/versions/snapshots/crates_io__tests__routes__crates__versions__read__show_by_crate_name_and_version.snap
@@ -1,6 +1,7 @@
---
source: src/tests/routes/crates/versions/read.rs
expression: json
+snapshot_kind: text
---
{
"version": {
@@ -32,7 +33,8 @@ expression: json
"id": "[id]",
"login": "foo",
"name": null,
- "url": "https://github.com/foo"
+ "url": "https://github.com/foo",
+ "username": "foo"
},
"readme_path": "/api/v1/crates/foo_vers_show/2.0.0/readme",
"repository": null,
diff --git a/src/tests/routes/me/snapshots/crates_io__tests__routes__me__get__me-2.snap b/src/tests/routes/me/snapshots/crates_io__tests__routes__me__get__me-2.snap
index 5564b16de5e..0275f95e5ff 100644
--- a/src/tests/routes/me/snapshots/crates_io__tests__routes__me__get__me-2.snap
+++ b/src/tests/routes/me/snapshots/crates_io__tests__routes__me__get__me-2.snap
@@ -1,6 +1,7 @@
---
source: src/tests/routes/me/get.rs
expression: response.json()
+snapshot_kind: text
---
{
"owned_crates": [],
@@ -14,6 +15,7 @@ expression: response.json()
"login": "foo",
"name": null,
"publish_notifications": true,
- "url": "https://github.com/foo"
+ "url": "https://github.com/foo",
+ "username": "foo"
}
}
diff --git a/src/tests/routes/me/snapshots/crates_io__tests__routes__me__get__me-3.snap b/src/tests/routes/me/snapshots/crates_io__tests__routes__me__get__me-3.snap
index b0ffc3e7fc8..5d511698d04 100644
--- a/src/tests/routes/me/snapshots/crates_io__tests__routes__me__get__me-3.snap
+++ b/src/tests/routes/me/snapshots/crates_io__tests__routes__me__get__me-3.snap
@@ -1,6 +1,7 @@
---
source: src/tests/routes/me/get.rs
expression: response.json()
+snapshot_kind: text
---
{
"owned_crates": [
@@ -20,6 +21,7 @@ expression: response.json()
"login": "foo",
"name": null,
"publish_notifications": true,
- "url": "https://github.com/foo"
+ "url": "https://github.com/foo",
+ "username": "foo"
}
}
diff --git a/src/tests/routes/me/updates.rs b/src/tests/routes/me/updates.rs
index f81f56e525f..8f15a33467b 100644
--- a/src/tests/routes/me/updates.rs
+++ b/src/tests/routes/me/updates.rs
@@ -78,8 +78,8 @@ async fn following() {
.find(|v| v.krate == "bar_fighters")
.unwrap();
assert_eq!(
- bar_version.published_by.as_ref().unwrap().login,
- user_model.gh_login
+ &bar_version.published_by.as_ref().unwrap().username,
+ user_model.username.as_ref().unwrap()
);
let r: R = user
diff --git a/src/tests/routes/users/read.rs b/src/tests/routes/users/read.rs
index cd697fc53e7..84aba7a33a7 100644
--- a/src/tests/routes/users/read.rs
+++ b/src/tests/routes/users/read.rs
@@ -16,10 +16,19 @@ async fn show() {
let json: UserShowPublicResponse = anon.get("/api/v1/users/foo").await.good();
assert_eq!(json.user.login, "foo");
+ assert_eq!(json.user.username, "foo");
let json: UserShowPublicResponse = anon.get("/api/v1/users/bAr").await.good();
assert_eq!(json.user.login, "Bar");
+ assert_eq!(json.user.username, "Bar");
assert_eq!(json.user.url, "https://github.com/Bar");
+
+ let accounts = json.user.linked_accounts.unwrap();
+ assert_eq!(accounts.len(), 1);
+ let account = &accounts[0];
+ assert_eq!(account.provider, "GitHub");
+ assert_eq!(account.login, "Bar");
+ assert_eq!(account.url, "https://github.com/Bar");
}
#[tokio::test(flavor = "multi_thread")]
@@ -38,6 +47,7 @@ async fn show_latest_user_case_insensitively() {
let user1 = NewUser::builder()
.gh_id(1)
.gh_login("foobar")
+ .username("foobar")
.name("I was first then deleted my github account")
.gh_access_token("bar")
.build();
@@ -45,6 +55,7 @@ async fn show_latest_user_case_insensitively() {
let user2 = NewUser::builder()
.gh_id(2)
.gh_login("FOOBAR")
+ .username("FOOBAR")
.name("I was second, I took the foobar username on github")
.gh_access_token("bar")
.build();
diff --git a/src/tests/team.rs b/src/tests/team.rs
index 34561a20cc0..eb4303f9467 100644
--- a/src/tests/team.rs
+++ b/src/tests/team.rs
@@ -121,6 +121,7 @@ async fn add_renamed_team() -> anyhow::Result<()> {
let json = anon.crate_owner_teams("foo_renamed_team").await.good();
assert_eq!(json.teams.len(), 1);
assert_eq!(json.teams[0].login, "github:test-org:core");
+ assert_eq!(json.teams[0].username, "github:test-org:core");
Ok(())
}
@@ -151,6 +152,7 @@ async fn add_team_mixed_case() -> anyhow::Result<()> {
let json = anon.crate_owner_teams("foo_mixed_case").await.good();
assert_eq!(json.teams.len(), 1);
assert_eq!(json.teams[0].login, "github:test-org:core");
+ assert_eq!(json.teams[0].username, "github:test-org:core");
Ok(())
}
diff --git a/src/tests/user.rs b/src/tests/user.rs
index 33e98796849..e3b662ac036 100644
--- a/src/tests/user.rs
+++ b/src/tests/user.rs
@@ -1,5 +1,6 @@
use crate::controllers::session;
-use crate::models::{ApiToken, Email, User};
+use crate::models::{AccountProvider, ApiToken, Email, LinkedAccount, User};
+use crate::schema::linked_accounts;
use crate::tests::TestApp;
use crate::tests::util::github::next_gh_id;
use crate::tests::util::{MockCookieUser, RequestHelper};
@@ -45,6 +46,7 @@ async fn updating_existing_user_doesnt_change_api_token() -> anyhow::Result<()>
let user = assert_ok!(User::find(&mut conn, api_token.user_id).await);
assert_eq!(user.gh_login, "bar");
+ assert_eq!(user.username.unwrap(), "bar");
assert_eq!(user.gh_access_token.expose_secret(), "bar_token");
Ok(())
@@ -265,3 +267,47 @@ async fn test_existing_user_email() -> anyhow::Result<()> {
Ok(())
}
+
+// To assist in eventually someday allowing OAuth with more than GitHub, verify that we're starting
+// to also write the GitHub info to the `linked_accounts` table. Nothing currently reads from this
+// table other than this test.
+#[tokio::test(flavor = "multi_thread")]
+async fn also_write_to_linked_accounts() -> anyhow::Result<()> {
+ let (app, _) = TestApp::init().empty().await;
+ let mut conn = app.db_conn().await;
+
+ // Simulate logging in via GitHub. Don't use app.db_new_user because it inserts a user record
+ // directly into the database and we want to test the OAuth flow here.
+ let email = "potahto@example.com";
+
+ let emails = &app.as_inner().emails;
+
+ let gh_user = GithubUser {
+ id: next_gh_id(),
+ login: "arbitrary_username".to_string(),
+ name: None,
+ email: Some(email.to_string()),
+ avatar_url: None,
+ };
+
+ let u =
+ session::save_user_to_database(&gh_user, "some random token", emails, &mut conn).await?;
+
+ let linked_accounts = linked_accounts::table
+ .filter(linked_accounts::provider.eq(AccountProvider::Github))
+ .filter(linked_accounts::account_id.eq(u.gh_id))
+ .load::(&mut conn)
+ .await
+ .unwrap();
+
+ assert_eq!(linked_accounts.len(), 1);
+ let linked_account = &linked_accounts[0];
+ assert_eq!(linked_account.user_id, u.id);
+ assert_eq!(linked_account.login, u.gh_login);
+ assert_eq!(
+ linked_account.access_token.expose_secret(),
+ u.gh_access_token.expose_secret()
+ );
+
+ Ok(())
+}
diff --git a/src/tests/util/test_app.rs b/src/tests/util/test_app.rs
index 2e155b3332b..b0b43865b4d 100644
--- a/src/tests/util/test_app.rs
+++ b/src/tests/util/test_app.rs
@@ -3,8 +3,8 @@ use crate::config::{
self, Base, CdnLogQueueConfig, CdnLogStorageConfig, DatabasePools, DbPoolConfig,
};
use crate::middleware::cargo_compat::StatusCodeConfig;
-use crate::models::NewEmail;
use crate::models::token::{CrateScope, EndpointScope};
+use crate::models::{AccountProvider, NewEmail, NewLinkedAccount};
use crate::rate_limiter::{LimitedAction, RateLimiterConfig};
use crate::storage::StorageConfig;
use crate::tests::util::chaosproxy::ChaosProxy;
@@ -114,8 +114,8 @@ impl TestApp {
self.0.test_database.async_connect().await
}
- /// Create a new user with a verified email address in the database
- /// (`@example.com`) and return a mock user session.
+ /// Create a new user with a verified email address (`@example.com`)
+ /// and a linked GitHub account in the database and return a mock user session.
///
/// This method updates the database directly
pub async fn db_new_user(&self, username: &str) -> MockCookieUser {
@@ -123,10 +123,19 @@ impl TestApp {
let email = format!("{username}@example.com");
- let user = crate::tests::new_user(username)
- .insert(&mut conn)
- .await
- .unwrap();
+ let new_user = crate::tests::new_user(username);
+ let user = new_user.insert(&mut conn).await.unwrap();
+
+ let linked_account = NewLinkedAccount::builder()
+ .user_id(user.id)
+ .provider(AccountProvider::Github)
+ .account_id(user.gh_id)
+ .access_token(&new_user.gh_access_token)
+ .login(&user.gh_login)
+ .maybe_avatar(user.gh_avatar.as_deref())
+ .build();
+
+ linked_account.insert_or_update(&mut conn).await.unwrap();
let new_email = NewEmail::builder()
.user_id(user.id)
diff --git a/src/typosquat/test_util.rs b/src/typosquat/test_util.rs
index 69875e1261b..ede066dfdf7 100644
--- a/src/typosquat/test_util.rs
+++ b/src/typosquat/test_util.rs
@@ -41,6 +41,7 @@ pub mod faker {
NewUser::builder()
.gh_id(next_gh_id())
.gh_login(login)
+ .username(login)
.gh_access_token("token")
.build()
.insert(conn)
diff --git a/src/views.rs b/src/views.rs
index c3b3be8443e..824c563c36e 100644
--- a/src/views.rs
+++ b/src/views.rs
@@ -2,8 +2,8 @@ use chrono::{DateTime, Utc};
use crate::external_urls::remove_blocked_urls;
use crate::models::{
- ApiToken, Category, Crate, Dependency, DependencyKind, Keyword, Owner, ReverseDependency, Team,
- TopVersions, User, Version, VersionDownload, VersionOwnerAction,
+ ApiToken, Category, Crate, Dependency, DependencyKind, Keyword, LinkedAccount, Owner,
+ ReverseDependency, Team, TopVersions, User, Version, VersionDownload, VersionOwnerAction,
};
use crates_io_github as github;
@@ -521,10 +521,16 @@ pub struct EncodableOwner {
#[schema(example = 42)]
pub id: i32,
- /// The login name of the team or user.
+ // `login` and `username` should contain the same value for now.
+ // `login` is deprecated; can be removed when all frontends have migrated to `username`.
+ /// The GitHub login of the team or user.
#[schema(example = "ghost")]
pub login: String,
+ /// The crates.io username of the team or user.
+ #[schema(example = "ghost")]
+ pub username: String,
+
/// The kind of the owner (`user` or `team`).
#[schema(example = "user")]
pub kind: String,
@@ -548,14 +554,17 @@ impl From for EncodableOwner {
Owner::User(User {
id,
name,
+ username,
gh_login,
gh_avatar,
..
}) => {
let url = format!("https://github.com/{gh_login}");
+ let username = username.unwrap_or(gh_login);
Self {
id,
- login: gh_login,
+ login: username.clone(),
+ username,
avatar: gh_avatar,
url: Some(url),
name,
@@ -572,7 +581,8 @@ impl From for EncodableOwner {
let url = github::team_url(&login);
Self {
id,
- login,
+ login: login.clone(),
+ username: login,
url: Some(url),
avatar,
name,
@@ -672,10 +682,16 @@ pub struct EncodablePrivateUser {
#[schema(example = 42)]
pub id: i32,
- /// The user's login name.
+ // `login` and `username` should contain the same value for now.
+ // `login` is deprecated; can be removed when all frontends have migrated to `username`.
+ /// The user's GitHub login.
#[schema(example = "ghost")]
pub login: String,
+ /// The user's crates.io username.
+ #[schema(example = "ghost")]
+ pub username: String,
+
/// Whether the user's email address has been verified.
#[schema(example = true)]
pub email_verified: bool,
@@ -720,6 +736,7 @@ impl EncodablePrivateUser {
let User {
id,
name,
+ username,
gh_login,
gh_avatar,
is_admin,
@@ -727,14 +744,16 @@ impl EncodablePrivateUser {
..
} = user;
let url = format!("https://github.com/{gh_login}");
+ let username = username.unwrap_or(gh_login);
EncodablePrivateUser {
id,
+ login: username.clone(),
+ username,
email,
email_verified,
email_verification_sent,
avatar: gh_avatar,
- login: gh_login,
name,
url: Some(url),
is_admin,
@@ -750,10 +769,16 @@ pub struct EncodablePublicUser {
#[schema(example = 42)]
pub id: i32,
- /// The user's login name.
+ // `login` and `username` should contain the same value for now.
+ // `login` is deprecated; can be removed when all frontends have migrated to `username`.
+ /// The user's GitHub login name.
#[schema(example = "ghost")]
pub login: String,
+ /// The user's crates.io username.
+ #[schema(example = "ghost")]
+ pub username: String,
+
/// The user's display name, if set.
#[schema(example = "Kate Morgan")]
pub name: Option,
@@ -765,24 +790,107 @@ pub struct EncodablePublicUser {
/// The user's GitHub profile URL.
#[schema(example = "https://github.com/ghost")]
pub url: String,
+
+ /// The accounts linked to this crates.io account.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ #[schema(no_recursion, example = json!([]))]
+ pub linked_accounts: Option>,
}
-/// Converts a `User` model into an `EncodablePublicUser` for JSON serialization.
+/// Converts a `User` model into an `EncodablePublicUser` for JSON serialization. Does not include
+/// linked accounts.
impl From for EncodablePublicUser {
fn from(user: User) -> Self {
let User {
id,
name,
+ username,
gh_login,
gh_avatar,
..
} = user;
let url = format!("https://github.com/{gh_login}");
+ let username = username.unwrap_or(gh_login);
+
EncodablePublicUser {
id,
+ login: username.clone(),
+ username,
+ name,
avatar: gh_avatar,
- login: gh_login,
+ url,
+ linked_accounts: None,
+ }
+ }
+}
+
+impl EncodablePublicUser {
+ pub fn with_linked_accounts(user: User, linked_accounts: &[LinkedAccount]) -> Self {
+ let User {
+ id,
+ name,
+ username,
+ gh_login,
+ gh_avatar,
+ ..
+ } = user;
+ let url = format!("https://github.com/{gh_login}");
+ let username = username.unwrap_or(gh_login);
+
+ let linked_accounts = if linked_accounts.is_empty() {
+ None
+ } else {
+ Some(linked_accounts.iter().map(Into::into).collect())
+ };
+
+ EncodablePublicUser {
+ id,
+ login: username.clone(),
+ username,
name,
+ avatar: gh_avatar,
+ url,
+ linked_accounts,
+ }
+ }
+}
+
+#[derive(Deserialize, Serialize, Debug, PartialEq, Eq, utoipa::ToSchema)]
+#[schema(as = LinkedAccount)]
+pub struct EncodableLinkedAccount {
+ /// The service providing this linked account.
+ #[schema(example = "GitHub")]
+ pub provider: String,
+
+ /// The linked account's login name.
+ #[schema(example = "ghost")]
+ pub login: String,
+
+ /// The linked account's avatar URL, if set.
+ #[schema(example = "https://avatars2.githubusercontent.com/u/1234567?v=4")]
+ pub avatar: Option,
+
+ /// The linked account's profile URL on the provided service.
+ #[schema(example = "https://github.com/ghost")]
+ pub url: String,
+}
+
+/// Converts a `LinkedAccount` model into an `EncodableLinkedAccount` for JSON serialization.
+impl From<&LinkedAccount> for EncodableLinkedAccount {
+ fn from(linked_account: &LinkedAccount) -> Self {
+ let LinkedAccount {
+ provider,
+ login,
+ avatar,
+ ..
+ } = linked_account;
+
+ let url = provider.url(login);
+
+ Self {
+ provider: provider.to_string(),
+ login: login.clone(),
+ avatar: avatar.clone(),
url,
}
}
@@ -1108,9 +1216,11 @@ mod tests {
user: EncodablePublicUser {
id: 0,
login: String::new(),
+ username: String::new(),
name: None,
avatar: None,
url: String::new(),
+ linked_accounts: None,
},
time: NaiveDate::from_ymd_opt(2017, 1, 6)
.unwrap()
diff --git a/src/worker/jobs/downloads/update_metadata.rs b/src/worker/jobs/downloads/update_metadata.rs
index a2d676c594c..5cc20a90c64 100644
--- a/src/worker/jobs/downloads/update_metadata.rs
+++ b/src/worker/jobs/downloads/update_metadata.rs
@@ -115,6 +115,7 @@ mod tests {
NewUser::builder()
.gh_id(2)
.gh_login("login")
+ .username("login")
.gh_access_token("access_token")
.build()
.insert(conn)
diff --git a/src/worker/jobs/expiry_notification.rs b/src/worker/jobs/expiry_notification.rs
index 4119eab29aa..1c4add805ae 100644
--- a/src/worker/jobs/expiry_notification.rs
+++ b/src/worker/jobs/expiry_notification.rs
@@ -186,6 +186,7 @@ mod tests {
let user = NewUser::builder()
.gh_id(0)
.gh_login("a")
+ .username("a")
.gh_access_token("token")
.build()
.insert(&mut conn)
diff --git a/tests/acceptance/api-token-test.js b/tests/acceptance/api-token-test.js
index 1c18fcac5ac..c6d8c092c93 100644
--- a/tests/acceptance/api-token-test.js
+++ b/tests/acceptance/api-token-test.js
@@ -14,6 +14,7 @@ module('Acceptance | api-tokens', function (hooks) {
function prepare(context) {
let user = context.db.user.create({
login: 'johnnydee',
+ username: 'johnnydee',
name: 'John Doe',
email: 'john@doe.com',
avatar: 'https://avatars2.githubusercontent.com/u/1234567?v=4',
diff --git a/tests/acceptance/crate-test.js b/tests/acceptance/crate-test.js
index d3e03704ba9..52127b3f428 100644
--- a/tests/acceptance/crate-test.js
+++ b/tests/acceptance/crate-test.js
@@ -215,7 +215,7 @@ module('Acceptance | crate page', function (hooks) {
skip('crates can be yanked by owner', async function (assert) {
loadFixtures(this.db);
- let user = this.db.user.findFirst({ where: { login: { equals: 'thehydroimpulse' } } });
+ let user = this.db.user.findFirst({ where: { username: { equals: 'thehydroimpulse' } } });
this.authenticateAs(user);
await visit('/crates/nanomsg/0.5.0');
@@ -242,7 +242,7 @@ module('Acceptance | crate page', function (hooks) {
test('navigating to the owners page when not an owner', async function (assert) {
loadFixtures(this.db);
- let user = this.db.user.findFirst({ where: { login: { equals: 'iain8' } } });
+ let user = this.db.user.findFirst({ where: { username: { equals: 'iain8' } } });
this.authenticateAs(user);
await visit('/crates/nanomsg');
@@ -253,7 +253,7 @@ module('Acceptance | crate page', function (hooks) {
test('navigating to the settings page', async function (assert) {
loadFixtures(this.db);
- let user = this.db.user.findFirst({ where: { login: { equals: 'thehydroimpulse' } } });
+ let user = this.db.user.findFirst({ where: { username: { equals: 'thehydroimpulse' } } });
this.authenticateAs(user);
await visit('/crates/nanomsg');
diff --git a/tests/acceptance/dashboard-test.js b/tests/acceptance/dashboard-test.js
index 0bda0593957..e0a31b381c6 100644
--- a/tests/acceptance/dashboard-test.js
+++ b/tests/acceptance/dashboard-test.js
@@ -21,6 +21,7 @@ module('Acceptance | Dashboard', function (hooks) {
test('shows the dashboard when logged in', async function (assert) {
let user = this.db.user.create({
login: 'johnnydee',
+ username: 'johnnydee',
name: 'John Doe',
email: 'john@doe.com',
avatar: 'https://avatars2.githubusercontent.com/u/1234567?v=4',
diff --git a/tests/acceptance/login-test.js b/tests/acceptance/login-test.js
index 7d000efe358..f3a3e2481ce 100644
--- a/tests/acceptance/login-test.js
+++ b/tests/acceptance/login-test.js
@@ -45,6 +45,7 @@ module('Acceptance | Login', function (hooks) {
user: {
id: 42,
login: 'johnnydee',
+ username: 'johnnydee',
name: 'John Doe',
email: 'john@doe.name',
avatar: 'https://avatars2.githubusercontent.com/u/12345?v=4',
diff --git a/tests/acceptance/settings/add-owner-test.js b/tests/acceptance/settings/add-owner-test.js
index 50ae4130701..1461a8ee73e 100644
--- a/tests/acceptance/settings/add-owner-test.js
+++ b/tests/acceptance/settings/add-owner-test.js
@@ -43,7 +43,7 @@ module('Acceptance | Settings | Add Owner', function (hooks) {
assert
.dom('[data-test-notification-message="error"]')
- .hasText('Error sending invite: could not find user with login `spookyghostboo`');
+ .hasText('Error sending invite: could not find user with username `spookyghostboo`');
assert.dom('[data-test-owners] [data-test-owner-team]').exists({ count: 2 });
assert.dom('[data-test-owners] [data-test-owner-user]').exists({ count: 2 });
});
diff --git a/tests/acceptance/settings/remove-owner-test.js b/tests/acceptance/settings/remove-owner-test.js
index 5bcb55973ed..5814eeabfe7 100644
--- a/tests/acceptance/settings/remove-owner-test.js
+++ b/tests/acceptance/settings/remove-owner-test.js
@@ -48,11 +48,11 @@ module('Acceptance | Settings | Remove Owner', function (hooks) {
);
await visit(`/crates/${crate.name}/settings`);
- await click(`[data-test-owner-user="${user2.login}"] [data-test-remove-owner-button]`);
+ await click(`[data-test-owner-user="${user2.username}"] [data-test-remove-owner-button]`);
assert
.dom('[data-test-notification-message="error"]')
- .hasText(`Failed to remove the user ${user2.login} as crate owner: nope`);
+ .hasText(`Failed to remove the user ${user2.username} as crate owner: nope`);
assert.dom('[data-test-owner-user]').exists({ count: 2 });
});
@@ -76,7 +76,7 @@ module('Acceptance | Settings | Remove Owner', function (hooks) {
);
await visit(`/crates/${crate.name}/settings`);
- await click(`[data-test-owner-team="${team1.login}"] [data-test-remove-owner-button]`);
+ await click(`[data-test-owner-team="${team1.username}"] [data-test-remove-owner-button]`);
assert
.dom('[data-test-notification-message="error"]')
diff --git a/tests/acceptance/sudo-test.js b/tests/acceptance/sudo-test.js
index 274fc7ae412..9bca55687a1 100644
--- a/tests/acceptance/sudo-test.js
+++ b/tests/acceptance/sudo-test.js
@@ -13,6 +13,7 @@ module('Acceptance | sudo', function (hooks) {
function prepare(context, isAdmin) {
const user = context.db.user.create({
login: 'johnnydee',
+ username: 'johnnydee',
name: 'John Doe',
email: 'john@doe.com',
avatar: 'https://avatars2.githubusercontent.com/u/1234567?v=4',
diff --git a/tests/components/owners-list-test.js b/tests/components/owners-list-test.js
index 5e44df4cd74..fdcbd6c64dd 100644
--- a/tests/components/owners-list-test.js
+++ b/tests/components/owners-list-test.js
@@ -26,8 +26,8 @@ module('Component | OwnersList', function (hooks) {
assert.dom('ul > li').exists({ count: 1 });
assert.dom('[data-test-owner-link]').exists({ count: 1 });
- let logins = [...this.element.querySelectorAll('[data-test-owner-link]')].map(it => it.dataset.testOwnerLink);
- assert.deepEqual(logins, ['user-1']);
+ let usernames = [...this.element.querySelectorAll('[data-test-owner-link]')].map(it => it.dataset.testOwnerLink);
+ assert.deepEqual(usernames, ['user-1']);
assert.dom('[data-test-owner-link="user-1"]').hasText('User 1');
assert.dom('[data-test-owner-link="user-1"]').hasAttribute('href', '/users/user-1');
@@ -37,7 +37,7 @@ module('Component | OwnersList', function (hooks) {
let crate = this.db.crate.create();
this.db.version.create({ crate });
- let user = this.db.user.create({ name: null, login: 'anonymous' });
+ let user = this.db.user.create({ name: null, username: 'anonymous' });
this.db.crateOwnership.create({ crate, user });
let store = this.owner.lookup('service:store');
@@ -49,8 +49,8 @@ module('Component | OwnersList', function (hooks) {
assert.dom('ul > li').exists({ count: 1 });
assert.dom('[data-test-owner-link]').exists({ count: 1 });
- let logins = [...this.element.querySelectorAll('[data-test-owner-link]')].map(it => it.dataset.testOwnerLink);
- assert.deepEqual(logins, ['anonymous']);
+ let usernames = [...this.element.querySelectorAll('[data-test-owner-link]')].map(it => it.dataset.testOwnerLink);
+ assert.deepEqual(usernames, ['anonymous']);
assert.dom('[data-test-owner-link="anonymous"]').hasText('anonymous');
assert.dom('[data-test-owner-link="anonymous"]').hasAttribute('href', '/users/anonymous');
@@ -74,8 +74,8 @@ module('Component | OwnersList', function (hooks) {
assert.dom('ul > li').exists({ count: 5 });
assert.dom('[data-test-owner-link]').exists({ count: 5 });
- let logins = [...this.element.querySelectorAll('[data-test-owner-link]')].map(it => it.dataset.testOwnerLink);
- assert.deepEqual(logins, ['user-1', 'user-2', 'user-3', 'user-4', 'user-5']);
+ let usernames = [...this.element.querySelectorAll('[data-test-owner-link]')].map(it => it.dataset.testOwnerLink);
+ assert.deepEqual(usernames, ['user-1', 'user-2', 'user-3', 'user-4', 'user-5']);
});
test('six users', async function (assert) {
@@ -96,8 +96,8 @@ module('Component | OwnersList', function (hooks) {
assert.dom('ul > li').exists({ count: 6 });
assert.dom('[data-test-owner-link]').exists({ count: 6 });
- let logins = [...this.element.querySelectorAll('[data-test-owner-link]')].map(it => it.dataset.testOwnerLink);
- assert.deepEqual(logins, ['user-1', 'user-2', 'user-3', 'user-4', 'user-5', 'user-6']);
+ let usernames = [...this.element.querySelectorAll('[data-test-owner-link]')].map(it => it.dataset.testOwnerLink);
+ assert.deepEqual(usernames, ['user-1', 'user-2', 'user-3', 'user-4', 'user-5', 'user-6']);
});
test('teams mixed with users', async function (assert) {
@@ -122,8 +122,8 @@ module('Component | OwnersList', function (hooks) {
assert.dom('ul > li').exists({ count: 5 });
assert.dom('[data-test-owner-link]').exists({ count: 5 });
- let logins = [...this.element.querySelectorAll('[data-test-owner-link]')].map(it => it.dataset.testOwnerLink);
- assert.deepEqual(logins, ['github:crates-io:team-1', 'github:crates-io:team-2', 'user-1', 'user-2', 'user-3']);
+ let usernames = [...this.element.querySelectorAll('[data-test-owner-link]')].map(it => it.dataset.testOwnerLink);
+ assert.deepEqual(usernames, ['github:crates-io:team-1', 'github:crates-io:team-2', 'user-1', 'user-2', 'user-3']);
assert.dom('[data-test-owner-link="github:crates-io:team-1"]').hasText('crates-io/team-1');
assert
diff --git a/tests/models/crate-test.js b/tests/models/crate-test.js
index bb2e0a8dfde..d677293abc5 100644
--- a/tests/models/crate-test.js
+++ b/tests/models/crate-test.js
@@ -25,7 +25,7 @@ module('Model | Crate', function (hooks) {
let crateRecord = await this.store.findRecord('crate', crate.name);
- let result = await crateRecord.inviteOwner(user2.login);
+ let result = await crateRecord.inviteOwner(user2.username);
assert.deepEqual(result, { ok: true, msg: 'user user-2 has been invited to be an owner of crate crate-1' });
});
@@ -39,7 +39,7 @@ module('Model | Crate', function (hooks) {
let crateRecord = await this.store.findRecord('crate', crate.name);
await assert.rejects(crateRecord.inviteOwner('unknown'), function (error) {
- assert.deepEqual(error.errors, [{ detail: 'could not find user with login `unknown`' }]);
+ assert.deepEqual(error.errors, [{ detail: 'could not find user with username `unknown`' }]);
return true;
});
});
@@ -58,7 +58,7 @@ module('Model | Crate', function (hooks) {
let crateRecord = await this.store.findRecord('crate', crate.name);
- let result = await crateRecord.removeOwner(user2.login);
+ let result = await crateRecord.removeOwner(user2.username);
assert.deepEqual(result, { ok: true, msg: 'owners successfully removed' });
});
diff --git a/tests/routes/settings/tokens/index-test.js b/tests/routes/settings/tokens/index-test.js
index 042442a7d14..29032c96272 100644
--- a/tests/routes/settings/tokens/index-test.js
+++ b/tests/routes/settings/tokens/index-test.js
@@ -11,6 +11,7 @@ module('/settings/tokens', function (hooks) {
function prepare(context) {
let user = context.db.user.create({
login: 'johnnydee',
+ username: 'johnnydee',
name: 'John Doe',
email: 'john@doe.com',
avatar: 'https://avatars2.githubusercontent.com/u/1234567?v=4',
diff --git a/tests/routes/settings/tokens/new-test.js b/tests/routes/settings/tokens/new-test.js
index f1126c0ac34..5183517b044 100644
--- a/tests/routes/settings/tokens/new-test.js
+++ b/tests/routes/settings/tokens/new-test.js
@@ -15,6 +15,7 @@ module('/settings/tokens/new', function (hooks) {
function prepare(context) {
let user = context.db.user.create({
login: 'johnnydee',
+ username: 'johnnydee',
name: 'John Doe',
email: 'john@doe.com',
avatar: 'https://avatars2.githubusercontent.com/u/1234567?v=4',