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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- [749](https://github.com/thoth-pub/thoth/pull/749) - Correct locale code formatting in Crossref metadata output
### Changed
- [749](https://github.com/thoth-pub/thoth/pull/749) - Remove ISBN limit in Crossref metadata output (introduced in v0.8.7)
- [748](https://github.com/thoth-pub/thoth/pull/748) - Require endorsement author names and featured video titles

## [[1.2.0]](https://github.com/thoth-pub/thoth/releases/tag/v1.2.0) - 2026-05-04
### Added
Expand Down
7 changes: 7 additions & 0 deletions thoth-api/migrations/20260504_v1.2.0/down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
ALTER TABLE endorsement
DROP CONSTRAINT IF EXISTS endorsement_author_name_check,
ALTER COLUMN author_name DROP NOT NULL;

ALTER TABLE work_featured_video
DROP CONSTRAINT IF EXISTS work_featured_video_title_check,
ALTER COLUMN title DROP NOT NULL;
26 changes: 26 additions & 0 deletions thoth-api/migrations/20260504_v1.2.0/up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
DO $$
BEGIN
IF EXISTS (
SELECT 1
FROM endorsement
WHERE author_name IS NULL OR octet_length(author_name) < 1
) THEN
RAISE EXCEPTION 'Cannot make endorsement.author_name required: existing rows contain NULL or empty values';
END IF;

IF EXISTS (
SELECT 1
FROM work_featured_video
WHERE title IS NULL OR octet_length(title) < 1
) THEN
RAISE EXCEPTION 'Cannot make work_featured_video.title required: existing rows contain NULL or empty values';
END IF;
END $$;

ALTER TABLE endorsement
ALTER COLUMN author_name SET NOT NULL,
ADD CONSTRAINT endorsement_author_name_check CHECK (octet_length(author_name) >= 1);

ALTER TABLE work_featured_video
ALTER COLUMN title SET NOT NULL,
ADD CONSTRAINT work_featured_video_title_check CHECK (octet_length(title) >= 1);
8 changes: 4 additions & 4 deletions thoth-api/src/graphql/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2419,8 +2419,8 @@ impl Endorsement {
}

#[graphql(description = "Name of the endorsement author")]
pub fn author_name(&self) -> Option<&String> {
self.author_name.as_ref()
pub fn author_name(&self) -> &String {
&self.author_name
}

#[graphql(description = "Role of the endorsement author")]
Expand Down Expand Up @@ -2682,8 +2682,8 @@ impl WorkFeaturedVideo {
}

#[graphql(description = "Title or caption of the featured video")]
pub fn title(&self) -> Option<&String> {
self.title.as_ref()
pub fn title(&self) -> &String {
&self.title
}

#[graphql(description = "CDN URL of the featured video")]
Expand Down
8 changes: 4 additions & 4 deletions thoth-api/src/graphql/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2587,7 +2587,7 @@ fn graphql_endorsement_supports_author_identity_fields() {
"endorsementId authorRole(markupFormat: PLAIN_TEXT) authorOrcid authorInstitutionId authorInstitution { institutionId ror } text(markupFormat: PLAIN_TEXT)",
NewEndorsement {
work_id: seed.book_work_id,
author_name: Some("Author".to_string()),
author_name: "Author".to_string(),
author_role: Some("*Visiting Scholar*".to_string()),
author_orcid: Some(Orcid::from_str("https://orcid.org/0000-0001-2345-6789").unwrap()),
author_institution_id: Some(institution.institution_id),
Expand Down Expand Up @@ -2644,7 +2644,7 @@ fn graphql_update_endorsement_supports_author_role_markup() {
"endorsementId authorRole(markupFormat: PLAIN_TEXT)",
NewEndorsement {
work_id: seed.book_work_id,
author_name: Some("Author".to_string()),
author_name: "Author".to_string(),
author_role: Some("Scholar".to_string()),
author_orcid: None,
author_institution_id: None,
Expand All @@ -2665,7 +2665,7 @@ fn graphql_update_endorsement_supports_author_role_markup() {
PatchEndorsement {
endorsement_id,
work_id: seed.book_work_id,
author_name: Some("Author".to_string()),
author_name: "Author".to_string(),
author_role: Some("*Lead Editor*".to_string()),
author_orcid: None,
author_institution_id: None,
Expand Down Expand Up @@ -2731,7 +2731,7 @@ fn graphql_review_and_endorsement_relations_null_after_institution_delete() {
pool.as_ref(),
&NewEndorsement {
work_id: seed.book_work_id,
author_name: Some("Author".to_string()),
author_name: "Author".to_string(),
author_role: Some("Scholar".to_string()),
author_orcid: Some(Orcid::from_str("https://orcid.org/0000-0001-2345-6789").unwrap()),
author_institution_id: Some(institution.institution_id),
Expand Down
6 changes: 3 additions & 3 deletions thoth-api/src/model/endorsement/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ pub enum EndorsementField {
pub struct Endorsement {
pub endorsement_id: Uuid,
pub work_id: Uuid,
pub author_name: Option<String>,
pub author_name: String,
pub author_role: Option<String>,
pub author_orcid: Option<Orcid>,
pub author_institution_id: Option<Uuid>,
Expand All @@ -52,7 +52,7 @@ pub struct Endorsement {
)]
pub struct NewEndorsement {
pub work_id: Uuid,
pub author_name: Option<String>,
pub author_name: String,
pub author_role: Option<String>,
pub author_orcid: Option<Orcid>,
pub author_institution_id: Option<Uuid>,
Expand All @@ -70,7 +70,7 @@ pub struct NewEndorsement {
pub struct PatchEndorsement {
pub endorsement_id: Uuid,
pub work_id: Uuid,
pub author_name: Option<String>,
pub author_name: String,
pub author_role: Option<String>,
pub author_orcid: Option<Orcid>,
pub author_institution_id: Option<Uuid>,
Expand Down
67 changes: 57 additions & 10 deletions thoth-api/src/model/endorsement/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ fn make_endorsement(
pool: &crate::db::PgPool,
work_id: Uuid,
endorsement_ordinal: i32,
author_name: Option<String>,
author_name: String,
) -> Endorsement {
let data = NewEndorsement {
work_id,
Expand Down Expand Up @@ -87,7 +87,7 @@ mod policy {
let work = create_work(pool.as_ref(), &imprint);
let data = NewEndorsement {
work_id: work.work_id,
author_name: Some("Author".to_string()),
author_name: "Author".to_string(),
author_role: Some("Role".to_string()),
author_orcid: None,
author_institution_id: None,
Expand Down Expand Up @@ -122,8 +122,7 @@ mod policy {
let publisher = create_publisher(pool.as_ref());
let imprint = create_imprint(pool.as_ref(), &publisher);
let work = create_work(pool.as_ref(), &imprint);
let endorsement =
make_endorsement(pool.as_ref(), work.work_id, 1, Some("Author".to_string()));
let endorsement = make_endorsement(pool.as_ref(), work.work_id, 1, "Author".to_string());

let patch = PatchEndorsement {
endorsement_id: endorsement.endorsement_id,
Expand All @@ -142,7 +141,7 @@ mod policy {

let data = NewEndorsement {
work_id: work.work_id,
author_name: Some("Author".to_string()),
author_name: "Author".to_string(),
author_role: Some("Role".to_string()),
author_orcid: None,
author_institution_id: None,
Expand Down Expand Up @@ -208,7 +207,7 @@ mod policy {

let data = NewEndorsement {
work_id: chapter.work_id,
author_name: Some("Author".to_string()),
author_name: "Author".to_string(),
author_role: Some("Role".to_string()),
author_orcid: None,
author_institution_id: None,
Expand Down Expand Up @@ -244,7 +243,7 @@ mod crud {

let data = NewEndorsement {
work_id: work.work_id,
author_name: Some("Author".to_string()),
author_name: "Author".to_string(),
author_role: Some("Role".to_string()),
author_orcid: Some(
crate::model::Orcid::from_str("https://orcid.org/0000-0002-1234-5678").unwrap(),
Expand Down Expand Up @@ -280,6 +279,54 @@ mod crud {
assert!(Endorsement::from_id(pool.as_ref(), &deleted.endorsement_id).is_err());
}

#[test]
fn crud_rejects_empty_author_name() {
let (_guard, pool) = setup_test_db();

let publisher = create_publisher(pool.as_ref());
let imprint = create_imprint(pool.as_ref(), &publisher);
let work = create_work(pool.as_ref(), &imprint);

let data = NewEndorsement {
work_id: work.work_id,
author_name: "".to_string(),
author_role: Some("Role".to_string()),
author_orcid: None,
author_institution_id: None,
url: Some("https://example.com/endorsement".to_string()),
text: Some("Endorsement text".to_string()),
endorsement_ordinal: 1,
};

let create_error = Endorsement::create(pool.as_ref(), &data).unwrap_err();
assert!(matches!(
create_error,
thoth_errors::ThothError::DatabaseConstraintError(ref msg)
if msg.as_ref() == "Author name must not be an empty string."
));

let endorsement = make_endorsement(pool.as_ref(), work.work_id, 1, "Author".to_string());
let patch = PatchEndorsement {
endorsement_id: endorsement.endorsement_id,
work_id: endorsement.work_id,
author_name: "".to_string(),
author_role: endorsement.author_role.clone(),
author_orcid: endorsement.author_orcid.clone(),
author_institution_id: endorsement.author_institution_id,
url: endorsement.url.clone(),
text: endorsement.text.clone(),
endorsement_ordinal: endorsement.endorsement_ordinal,
};
let ctx = test_context(pool.clone(), "test-user");

let update_error = endorsement.update(&ctx, &patch).unwrap_err();
assert!(matches!(
update_error,
thoth_errors::ThothError::DatabaseConstraintError(ref msg)
if msg.as_ref() == "Author name must not be an empty string."
));
}

#[test]
fn deleting_author_institution_nulls_relation() {
let (_guard, pool) = setup_test_db();
Expand All @@ -293,7 +340,7 @@ mod crud {
pool.as_ref(),
&NewEndorsement {
work_id: work.work_id,
author_name: Some("Author".to_string()),
author_name: "Author".to_string(),
author_role: Some("Role".to_string()),
author_orcid: Some(
crate::model::Orcid::from_str("https://orcid.org/0000-0002-1234-5678").unwrap(),
Expand Down Expand Up @@ -324,8 +371,8 @@ mod crud {
let imprint = create_imprint(pool.as_ref(), &publisher);
let work = create_work(pool.as_ref(), &imprint);

let first = make_endorsement(pool.as_ref(), work.work_id, 1, Some("Author 1".to_string()));
let second = make_endorsement(pool.as_ref(), work.work_id, 2, Some("Author 2".to_string()));
let first = make_endorsement(pool.as_ref(), work.work_id, 1, "Author 1".to_string());
let second = make_endorsement(pool.as_ref(), work.work_id, 2, "Author 2".to_string());
let ctx = test_context(pool.clone(), "test-user");

let moved = second
Expand Down
2 changes: 1 addition & 1 deletion thoth-api/src/model/file/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ fn create_work_featured_video(

let new_video = NewWorkFeaturedVideo {
work_id,
title: Some("Hosted video".to_string()),
title: "Hosted video".to_string(),
url: None,
width: 560,
height: 315,
Expand Down
6 changes: 3 additions & 3 deletions thoth-api/src/model/work_featured_video/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ pub enum WorkFeaturedVideoField {
pub struct WorkFeaturedVideo {
pub work_featured_video_id: Uuid,
pub work_id: Uuid,
pub title: Option<String>,
pub title: String,
pub url: Option<String>,
pub width: i32,
pub height: i32,
Expand All @@ -49,7 +49,7 @@ pub struct WorkFeaturedVideo {
)]
pub struct NewWorkFeaturedVideo {
pub work_id: Uuid,
pub title: Option<String>,
pub title: String,
pub url: Option<String>,
pub width: i32,
pub height: i32,
Expand All @@ -64,7 +64,7 @@ pub struct NewWorkFeaturedVideo {
pub struct PatchWorkFeaturedVideo {
pub work_featured_video_id: Uuid,
pub work_id: Uuid,
pub title: Option<String>,
pub title: String,
pub url: Option<String>,
pub width: i32,
pub height: i32,
Expand Down
56 changes: 51 additions & 5 deletions thoth-api/src/model/work_featured_video/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ fn make_work_featured_video(
) -> WorkFeaturedVideo {
let data = NewWorkFeaturedVideo {
work_id,
title: Some("Featured video".to_string()),
title: "Featured video".to_string(),
url,
width: 560,
height: 315,
Expand Down Expand Up @@ -80,7 +80,7 @@ mod policy {
let work = create_work(pool.as_ref(), &imprint);
let data = NewWorkFeaturedVideo {
work_id: work.work_id,
title: Some("Featured video".to_string()),
title: "Featured video".to_string(),
url: Some("https://cdn.example.org/video.mp4".to_string()),
width: 560,
height: 315,
Expand Down Expand Up @@ -128,7 +128,7 @@ mod policy {

let data = NewWorkFeaturedVideo {
work_id: work.work_id,
title: Some("Featured video".to_string()),
title: "Featured video".to_string(),
url: Some("https://cdn.example.org/video.mp4".to_string()),
width: 560,
height: 315,
Expand Down Expand Up @@ -190,7 +190,7 @@ mod policy {

let data = NewWorkFeaturedVideo {
work_id: chapter.work_id,
title: Some("Featured video".to_string()),
title: "Featured video".to_string(),
url: Some("https://cdn.example.org/video.mp4".to_string()),
width: 560,
height: 315,
Expand Down Expand Up @@ -231,7 +231,7 @@ mod crud {
let patch = PatchWorkFeaturedVideo {
work_featured_video_id: video.work_featured_video_id,
work_id: video.work_id,
title: Some("Updated featured video".to_string()),
title: "Updated featured video".to_string(),
url: Some("https://cdn.example.org/video-v2.mp4".to_string()),
width: 640,
height: 360,
Expand All @@ -247,6 +247,52 @@ mod crud {
);
}

#[test]
fn crud_rejects_empty_title() {
let (_guard, pool) = setup_test_db();

let publisher = create_publisher(pool.as_ref());
let imprint = create_imprint(pool.as_ref(), &publisher);
let work = create_work(pool.as_ref(), &imprint);

let data = NewWorkFeaturedVideo {
work_id: work.work_id,
title: "".to_string(),
url: Some("https://cdn.example.org/video.mp4".to_string()),
width: 560,
height: 315,
};

let create_error = WorkFeaturedVideo::create(pool.as_ref(), &data).unwrap_err();
assert!(matches!(
create_error,
thoth_errors::ThothError::DatabaseConstraintError(ref msg)
if msg.as_ref() == "Featured video title must not be an empty string."
));

let video = make_work_featured_video(
pool.as_ref(),
work.work_id,
Some("https://cdn.example.org/video.mp4".to_string()),
);
let patch = PatchWorkFeaturedVideo {
work_featured_video_id: video.work_featured_video_id,
work_id: video.work_id,
title: "".to_string(),
url: video.url.clone(),
width: video.width,
height: video.height,
};
let ctx = test_context(pool.clone(), "test-user");

let update_error = video.update(&ctx, &patch).unwrap_err();
assert!(matches!(
update_error,
thoth_errors::ThothError::DatabaseConstraintError(ref msg)
if msg.as_ref() == "Featured video title must not be an empty string."
));
}

#[test]
fn crud_from_work_id_returns_record() {
let (_guard, pool) = setup_test_db();
Expand Down
Loading
Loading