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
22 changes: 22 additions & 0 deletions src/crates/contracts/product-domains/src/appearance_market.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@ pub enum AppearanceMarketSubmissionStatus {
Withdrawn,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AppearanceMarketPublicationStatus {
Published,
Yanked,
Unpublished,
}

impl AppearanceMarketSubmissionStatus {
pub fn can_transition_to(self, next: Self) -> bool {
matches!(
Expand Down Expand Up @@ -157,6 +165,8 @@ pub struct AppearanceMarketSubmission {
pub repository_url: Option<String>,
pub status: AppearanceMarketSubmissionStatus,
#[serde(skip_serializing_if = "Option::is_none")]
pub publication_status: Option<AppearanceMarketPublicationStatus>,
#[serde(skip_serializing_if = "Option::is_none")]
pub package_sha256: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub package_size: Option<u64>,
Expand Down Expand Up @@ -299,6 +309,18 @@ mod tests {
);
}

#[test]
fn publication_status_serializes_as_a_stable_api_value() {
assert_eq!(
serde_json::to_string(&AppearanceMarketPublicationStatus::Yanked).unwrap(),
"\"yanked\""
);
assert_eq!(
serde_json::to_string(&AppearanceMarketPublicationStatus::Unpublished).unwrap(),
"\"unpublished\""
);
}

#[test]
fn listing_detail_json_contract_matches_the_shared_typescript_fixture() {
let fixture = include_str!(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ mod tests {
},
repository_url: None,
status,
publication_status: None,
package_sha256: None,
package_size: None,
preview_url: None,
Expand Down
140 changes: 139 additions & 1 deletion src/crates/services/skin-market-service/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,10 @@ mod tests {
}

fn appearance_package() -> Vec<u8> {
appearance_package_with_id("example.aurora")
}

fn appearance_package_with_id(package_id: &str) -> Vec<u8> {
let mut preview_output = Cursor::new(Vec::new());
image::DynamicImage::new_rgba8(8, 6)
.write_to(&mut preview_output, image::ImageFormat::Png)
Expand All @@ -225,7 +229,7 @@ mod tests {
let manifest = serde_json::json!({
"schema": "bitfun.appearance",
"schemaVersion": 1,
"id": "example.aurora",
"id": package_id,
"name": "Aurora",
"description": "A safe appearance",
"version": "1.0.0",
Expand Down Expand Up @@ -284,6 +288,75 @@ mod tests {
.to_string()
}

async fn publish_submission(app: &Router, slug: &str) -> (String, String, String) {
let submission_id = create_submission(app, slug).await;
let mut upload = request(
"PUT",
&format!("/skin/api/v1/submissions/{submission_id}/package"),
Some("owner-token"),
Body::from(appearance_package_with_id(&format!(
"example.{}",
slug.replace('-', ".")
))),
);
upload.headers_mut().insert(
header::CONTENT_TYPE,
HeaderValue::from_static(
bitfun_product_domains::appearance_market::APPEARANCE_MARKET_PACKAGE_CONTENT_TYPE,
),
);
assert_eq!(
app.clone().oneshot(upload).await.unwrap().status(),
StatusCode::OK
);
assert_eq!(
app.clone()
.oneshot(request(
"POST",
&format!("/skin/api/v1/submissions/{submission_id}/submit"),
Some("owner-token"),
Body::empty(),
))
.await
.unwrap()
.status(),
StatusCode::OK
);
let mut approve = request(
"POST",
&format!("/skin/api/v1/admin/submissions/{submission_id}/decision"),
Some("admin-token"),
Body::from(r#"{"decision":"approve","reason":""}"#),
);
approve.headers_mut().insert(
header::CONTENT_TYPE,
HeaderValue::from_static("application/json"),
);
let response = app.clone().oneshot(approve).await.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let approved = json_body(response).await;
assert_eq!(approved["submission"]["publicationStatus"], "published");
let listing_id = approved["submission"]["listingId"]
.as_str()
.unwrap()
.to_string();
let detail = app
.clone()
.oneshot(request(
"GET",
&format!("/skin/api/v1/listings/{slug}"),
None,
Body::empty(),
))
.await
.unwrap();
let release_id = json_body(detail).await["releases"][0]["releaseId"]
.as_str()
.unwrap()
.to_string();
(submission_id, listing_id, release_id)
}

#[tokio::test]
async fn bearer_submission_review_and_public_download_flow() {
let temporary = tempfile::tempdir().unwrap();
Expand Down Expand Up @@ -661,4 +734,69 @@ mod tests {
}
assert_eq!(seen.len(), 7);
}

#[tokio::test]
async fn submission_history_projects_release_and_listing_moderation() {
let temporary = tempfile::tempdir().unwrap();
let app = test_router(&temporary).await;
let (yanked_submission_id, _, release_id) = publish_submission(&app, "yanked-skin").await;
let (unpublished_submission_id, listing_id, _) =
publish_submission(&app, "unpublished-skin").await;

let mut yank = request(
"POST",
&format!("/skin/api/v1/admin/releases/{release_id}/yank"),
Some("admin-token"),
Body::from(r#"{"reason":"Unsafe package content"}"#),
);
yank.headers_mut().insert(
header::CONTENT_TYPE,
HeaderValue::from_static("application/json"),
);
assert_eq!(
app.clone().oneshot(yank).await.unwrap().status(),
StatusCode::NO_CONTENT
);

let mut unpublish = request(
"POST",
&format!("/skin/api/v1/admin/listings/{listing_id}/unpublish"),
Some("admin-token"),
Body::from(r#"{"reason":"Policy violation"}"#),
);
unpublish.headers_mut().insert(
header::CONTENT_TYPE,
HeaderValue::from_static("application/json"),
);
assert_eq!(
app.clone().oneshot(unpublish).await.unwrap().status(),
StatusCode::NO_CONTENT
);

let response = app
.clone()
.oneshot(request(
"GET",
"/skin/api/v1/submissions",
Some("owner-token"),
Body::empty(),
))
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let history = json_body(response).await;
let items = history["items"].as_array().unwrap();
let yanked = items
.iter()
.find(|item| item["submissionId"] == yanked_submission_id)
.unwrap();
assert_eq!(yanked["status"], "approved");
assert_eq!(yanked["publicationStatus"], "yanked");
let unpublished = items
.iter()
.find(|item| item["submissionId"] == unpublished_submission_id)
.unwrap();
assert_eq!(unpublished["status"], "approved");
assert_eq!(unpublished["publicationStatus"], "unpublished");
}
}
Loading