mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
feat(admin): show reported message content in report detail (#3149)
## Summary - include the reported event's complete stored content, author, creation time, and deletion state in the admin report detail response - resolve the event through a community-scoped join so an event ID collision cannot cross tenant boundaries - render the message only on report detail, with an explicit unavailable state when retention has removed it - preserve the existing report list contract so message bodies are not returned during queue browsing ## Security - the existing admin host/origin authorization runs before the detail database read; a route test pins that ordering - the target event is selected using both `events.community_id = moderation_reports.community_id` and `events.id = moderation_reports.target_event_id` - the client supplies only the report UUID; it cannot choose a community or arbitrary event ID - soft-deleted content is visible only through this restricted admin detail route and is labeled deleted - responses retain the admin API's `no-store`, CSP, `nosniff`, frame denial, and referrer policy middleware ## Testing - `pnpm -C admin-web check` - `pnpm -C admin-web test:e2e` (10 passed) - `cargo test -p buzz-db` (84 passed, 130 ignored) - `cargo clippy -p buzz-db -p buzz-relay --all-targets -- -D warnings` - focused admin authorization tests - pre-push Rust and desktop/Tauri suites passed The new Postgres integration test is ignored under the repository convention and will run when explicitly enabled against migrated Postgres. Local Postgres and Redis were unavailable, so the full `buzz-relay --lib` run had 8 existing infrastructure-dependent failures after 749 tests passed. --------- Signed-off-by: npub1dccv64krpcpse5cmkzfeh998cftungyatw3djt8jwdw6g43f7fyqzzmrf7 <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz> Signed-off-by: Kalvin Chau <kalvin@block.xyz> Signed-off-by: npub122y0pqkertljmedu303rl0aqrj3w8pvu43t6jxm6875lzg6f2pwqegc3xc <5288f082d91aff2de5bc8be23fbfa01ca2e3859cac57a91b7a3fa9f12349505c@buzz.block.builderlab.xyz> Co-authored-by: npub1dccv64krpcpse5cmkzfeh998cftungyatw3djt8jwdw6g43f7fyqzzmrf7 <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz> Co-authored-by: npub122y0pqkertljmedu303rl0aqrj3w8pvu43t6jxm6875lzg6f2pwqegc3xc <5288f082d91aff2de5bc8be23fbfa01ca2e3859cac57a91b7a3fa9f12349505c@buzz.block.builderlab.xyz>
This commit is contained in:
co-authored by
npub1dccv64krpcpse5cmkzfeh998cftungyatw3djt8jwdw6g43f7fyqzzmrf7
npub122y0pqkertljmedu303rl0aqrj3w8pvu43t6jxm6875lzg6f2pwqegc3xc
parent
174c38e4bd
commit
f069a85503
+36
-2
@@ -7,7 +7,12 @@ import {
|
||||
useState,
|
||||
} from "react";
|
||||
import { ApiFailure, request } from "./api";
|
||||
import type { FeedbackDetail, FeedbackSummary, Report } from "./types";
|
||||
import type {
|
||||
FeedbackDetail,
|
||||
FeedbackSummary,
|
||||
Report,
|
||||
ReportDetail as ReportDetailData,
|
||||
} from "./types";
|
||||
import { useResource } from "./useResource";
|
||||
|
||||
function usePath() {
|
||||
@@ -131,7 +136,10 @@ function Reports() {
|
||||
}
|
||||
|
||||
function ReportDetail({ id }: { id: string }) {
|
||||
const resource = useResource(() => request<Report>(`/reports/${id}`), id);
|
||||
const resource = useResource(
|
||||
() => request<ReportDetailData>(`/reports/${id}`),
|
||||
id,
|
||||
);
|
||||
return (
|
||||
<Page
|
||||
eyebrow="Moderation"
|
||||
@@ -164,6 +172,32 @@ function ReportDetail({ id }: { id: string }) {
|
||||
<dd>
|
||||
<code>{report.target}</code>
|
||||
</dd>
|
||||
{report.targetKind === "event" ? (
|
||||
<>
|
||||
<dt>Message</dt>
|
||||
<dd>
|
||||
{report.message ? (
|
||||
<div className="reported-message">
|
||||
{report.message.deletedAt ? (
|
||||
<span className="status">Deleted</span>
|
||||
) : null}
|
||||
<p>{report.message.content}</p>
|
||||
<div className="reported-message-meta">
|
||||
<span>Author</span>
|
||||
<code>{report.message.authorPubkey}</code>
|
||||
<span>Created</span>
|
||||
<time>{date(report.message.createdAt)}</time>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<p className="message-unavailable">
|
||||
Message content is unavailable. It may have expired or
|
||||
been removed from event storage.
|
||||
</p>
|
||||
)}
|
||||
</dd>
|
||||
</>
|
||||
) : null}
|
||||
<dt>Note</dt>
|
||||
<dd className="sensitive">
|
||||
{report.note ?? "No note provided."}
|
||||
|
||||
@@ -496,6 +496,38 @@ dd {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.reported-message {
|
||||
display: grid;
|
||||
justify-items: start;
|
||||
gap: 1rem;
|
||||
border-left: 3px solid #d7d72e;
|
||||
border-radius: 0 0.8rem 0.8rem 0;
|
||||
background: #f6f6f1;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.reported-message p,
|
||||
.message-unavailable {
|
||||
margin: 0;
|
||||
overflow-wrap: anywhere;
|
||||
line-height: 1.5;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.reported-message-meta {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr);
|
||||
gap: 0.4rem 0.75rem;
|
||||
width: 100%;
|
||||
color: rgb(35 30 30 / 48%);
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
.message-unavailable {
|
||||
color: rgb(35 30 30 / 60%);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.sensitive {
|
||||
border-left: 3px solid #d7d72e;
|
||||
border-radius: 0 0.8rem 0.8rem 0;
|
||||
|
||||
@@ -12,6 +12,17 @@ export interface Report {
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface ReportedMessage {
|
||||
authorPubkey: string;
|
||||
content: string;
|
||||
createdAt: string;
|
||||
deletedAt: string | null;
|
||||
}
|
||||
|
||||
export interface ReportDetail extends Report {
|
||||
message: ReportedMessage | null;
|
||||
}
|
||||
|
||||
export interface FeedbackSummary {
|
||||
id: string;
|
||||
communityId: string;
|
||||
|
||||
@@ -58,6 +58,72 @@ test("report rows render the relay response contract", async ({ page }) => {
|
||||
await expect(page.getByText("Unknown date")).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("event report detail renders the reported message content", async ({
|
||||
page,
|
||||
}) => {
|
||||
const id = "0e6caad8-1e18-4cd7-84fa-7264103f0a08";
|
||||
await page.route(`**/api/admin/v1/reports/${id}`, (route) =>
|
||||
route.fulfill({
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
id,
|
||||
communityId: "6d474feb-c50a-44e4-a0b5-f30532df49bc",
|
||||
communityHost: "design.buzz.xyz",
|
||||
reporterPubkey: "21".repeat(32),
|
||||
targetKind: "event",
|
||||
target: "12".repeat(32),
|
||||
reportType: "spam",
|
||||
status: "open",
|
||||
createdAt: "2026-07-17T17:30:00Z",
|
||||
message: {
|
||||
authorPubkey: "31".repeat(32),
|
||||
content:
|
||||
"This is the complete reported message.\nIt preserves lines.",
|
||||
createdAt: "2026-07-17T17:25:00Z",
|
||||
deletedAt: null,
|
||||
},
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
await page.goto(`/reports/${id}`);
|
||||
await expect(
|
||||
page.getByText("This is the complete reported message.", { exact: false }),
|
||||
).toBeVisible();
|
||||
await expect(page.getByText("31".repeat(32))).toBeVisible();
|
||||
await expect(
|
||||
page.getByText("Message content is unavailable", { exact: false }),
|
||||
).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("event report detail explains when message content is unavailable", async ({
|
||||
page,
|
||||
}) => {
|
||||
const id = "0e6caad8-1e18-4cd7-84fa-7264103f0a09";
|
||||
await page.route(`**/api/admin/v1/reports/${id}`, (route) =>
|
||||
route.fulfill({
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
id,
|
||||
communityId: "6d474feb-c50a-44e4-a0b5-f30532df49bc",
|
||||
communityHost: "design.buzz.xyz",
|
||||
reporterPubkey: "21".repeat(32),
|
||||
targetKind: "event",
|
||||
target: "12".repeat(32),
|
||||
reportType: "spam",
|
||||
status: "open",
|
||||
createdAt: "2026-07-17T17:30:00Z",
|
||||
message: null,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
await page.goto(`/reports/${id}`);
|
||||
await expect(
|
||||
page.getByText("Message content is unavailable", { exact: false }),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("feedback cards open the complete submission", async ({ page }) => {
|
||||
const id = "feed0000-0000-4000-8000-000000000001";
|
||||
const fullBody = `${"Long feedback ".repeat(30)}end of feedback`;
|
||||
|
||||
@@ -54,6 +54,31 @@ pub struct AdminReport {
|
||||
pub created_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
/// Reported message details available only on the admin report detail read.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AdminReportedMessage {
|
||||
/// Message author public key.
|
||||
pub author_pubkey: String,
|
||||
/// Complete message content.
|
||||
pub content: String,
|
||||
/// Timestamp signed into the message event.
|
||||
pub created_at: DateTime<Utc>,
|
||||
/// Soft-deletion time, when the message has since been deleted.
|
||||
pub deleted_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
/// Deployment-global moderation report detail.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AdminReportDetail {
|
||||
/// Report metadata.
|
||||
#[serde(flatten)]
|
||||
pub report: AdminReport,
|
||||
/// Reported message when the report targets a stored event.
|
||||
pub message: Option<AdminReportedMessage>,
|
||||
}
|
||||
|
||||
/// Deployment-global product feedback with source-community provenance.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@@ -128,24 +153,54 @@ pub async fn list_reports(
|
||||
rows.into_iter().map(row_to_report).collect()
|
||||
}
|
||||
|
||||
/// Fetch one report globally by its row id.
|
||||
pub async fn get_report(pool: &PgPool, report_id: Uuid) -> Result<Option<AdminReport>> {
|
||||
/// Fetch one report globally by its row id, including its event target content.
|
||||
pub async fn get_report(pool: &PgPool, report_id: Uuid) -> Result<Option<AdminReportDetail>> {
|
||||
let row = sqlx::query(
|
||||
r#"
|
||||
SELECT r.id, r.community_id, c.host AS community_host,
|
||||
r.report_event_id, r.reporter_pubkey, r.target_kind,
|
||||
r.target_event_id, r.target_pubkey, r.target_blob_sha256,
|
||||
r.channel_id, r.report_type, r.note, r.status, r.resolved_by,
|
||||
r.resolved_at, r.action_id, r.created_at
|
||||
r.resolved_at, r.action_id, r.created_at,
|
||||
target.pubkey AS message_author_pubkey,
|
||||
target.content AS message_content,
|
||||
target.created_at AS message_created_at,
|
||||
target.deleted_at AS message_deleted_at
|
||||
FROM moderation_reports r
|
||||
JOIN communities c ON c.id = r.community_id
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT e.pubkey, e.content, e.created_at, e.deleted_at
|
||||
FROM events e
|
||||
WHERE r.target_kind = 'event'
|
||||
AND e.community_id = r.community_id
|
||||
AND e.id = r.target_event_id
|
||||
ORDER BY e.created_at DESC
|
||||
LIMIT 1
|
||||
) target ON TRUE
|
||||
WHERE r.id = $1
|
||||
"#,
|
||||
)
|
||||
.bind(report_id)
|
||||
.fetch_optional(pool)
|
||||
.await?;
|
||||
row.map(row_to_report).transpose()
|
||||
row.map(|row| {
|
||||
let message = row
|
||||
.try_get::<Option<Vec<u8>>, _>("message_author_pubkey")?
|
||||
.map(|author_pubkey| -> Result<AdminReportedMessage> {
|
||||
Ok(AdminReportedMessage {
|
||||
author_pubkey: hex::encode(author_pubkey),
|
||||
content: row.try_get("message_content")?,
|
||||
created_at: row.try_get("message_created_at")?,
|
||||
deleted_at: row.try_get("message_deleted_at")?,
|
||||
})
|
||||
})
|
||||
.transpose()?;
|
||||
Ok(AdminReportDetail {
|
||||
report: row_to_report(row)?,
|
||||
message,
|
||||
})
|
||||
})
|
||||
.transpose()
|
||||
}
|
||||
|
||||
fn row_to_report(row: sqlx::postgres::PgRow) -> Result<AdminReport> {
|
||||
@@ -228,3 +283,206 @@ fn row_to_feedback(row: sqlx::postgres::PgRow) -> Result<AdminFeedback> {
|
||||
received_at: row.try_get("received_at")?,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
const TEST_DB_URL: &str = "postgres://buzz:buzz_dev@localhost:5432/buzz";
|
||||
|
||||
async fn setup_pool() -> PgPool {
|
||||
let database_url = std::env::var("BUZZ_TEST_DATABASE_URL")
|
||||
.or_else(|_| std::env::var("DATABASE_URL"))
|
||||
.unwrap_or_else(|_| TEST_DB_URL.to_owned());
|
||||
PgPool::connect(&database_url)
|
||||
.await
|
||||
.expect("connect to test DB")
|
||||
}
|
||||
|
||||
async fn insert_community(pool: &PgPool, label: &str) -> Uuid {
|
||||
let id = Uuid::new_v4();
|
||||
sqlx::query("INSERT INTO communities (id, host) VALUES ($1, $2)")
|
||||
.bind(id)
|
||||
.bind(format!("admin-report-{label}-{}.example", id.simple()))
|
||||
.execute(pool)
|
||||
.await
|
||||
.expect("insert community");
|
||||
id
|
||||
}
|
||||
|
||||
async fn insert_event(
|
||||
pool: &PgPool,
|
||||
community_id: Uuid,
|
||||
event_id: &[u8],
|
||||
author: &[u8],
|
||||
content: &str,
|
||||
deleted_at: Option<DateTime<Utc>>,
|
||||
) {
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO events (
|
||||
community_id, id, pubkey, created_at, kind, tags, content, sig, deleted_at
|
||||
) VALUES ($1, $2, $3, $4, 9, '[]'::jsonb, $5, $6, $7)
|
||||
"#,
|
||||
)
|
||||
.bind(community_id)
|
||||
.bind(event_id)
|
||||
.bind(author)
|
||||
.bind(Utc::now())
|
||||
.bind(content)
|
||||
.bind(vec![3_u8; 64])
|
||||
.bind(deleted_at)
|
||||
.execute(pool)
|
||||
.await
|
||||
.expect("insert event");
|
||||
}
|
||||
|
||||
async fn insert_event_report(
|
||||
pool: &PgPool,
|
||||
community_id: Uuid,
|
||||
target_event_id: &[u8],
|
||||
) -> Uuid {
|
||||
let id = Uuid::new_v4();
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO moderation_reports (
|
||||
community_id, id, report_event_id, reporter_pubkey,
|
||||
target_kind, target_event_id, report_type
|
||||
) VALUES ($1, $2, $3, $4, 'event', $5, 'spam')
|
||||
"#,
|
||||
)
|
||||
.bind(community_id)
|
||||
.bind(id)
|
||||
.bind(Uuid::new_v4().as_bytes().repeat(2))
|
||||
.bind(vec![4_u8; 32])
|
||||
.bind(target_event_id)
|
||||
.execute(pool)
|
||||
.await
|
||||
.expect("insert report");
|
||||
id
|
||||
}
|
||||
|
||||
async fn insert_pubkey_report(pool: &PgPool, community_id: Uuid) -> Uuid {
|
||||
let id = Uuid::new_v4();
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO moderation_reports (
|
||||
community_id, id, report_event_id, reporter_pubkey,
|
||||
target_kind, target_pubkey, report_type
|
||||
) VALUES ($1, $2, $3, $4, 'pubkey', $5, 'spam')
|
||||
"#,
|
||||
)
|
||||
.bind(community_id)
|
||||
.bind(id)
|
||||
.bind(Uuid::new_v4().as_bytes().repeat(2))
|
||||
.bind(vec![4_u8; 32])
|
||||
.bind(vec![7_u8; 32])
|
||||
.execute(pool)
|
||||
.await
|
||||
.expect("insert report");
|
||||
id
|
||||
}
|
||||
|
||||
async fn delete_report_fixture(pool: &PgPool, community_id: Uuid) {
|
||||
sqlx::query("DELETE FROM moderation_reports WHERE community_id = $1")
|
||||
.bind(community_id)
|
||||
.execute(pool)
|
||||
.await
|
||||
.expect("delete report fixture");
|
||||
sqlx::query("DELETE FROM communities WHERE id = $1")
|
||||
.bind(community_id)
|
||||
.execute(pool)
|
||||
.await
|
||||
.expect("delete community fixture");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "requires Postgres"]
|
||||
async fn report_detail_reads_only_the_same_community_target_and_includes_deleted_content() {
|
||||
let pool = setup_pool().await;
|
||||
let report_community = insert_community(&pool, "reported").await;
|
||||
let other_community = insert_community(&pool, "other").await;
|
||||
let event_id = vec![1_u8; 32];
|
||||
let deleted_at = Utc::now();
|
||||
insert_event(
|
||||
&pool,
|
||||
report_community,
|
||||
&event_id,
|
||||
&[5_u8; 32],
|
||||
"reported message",
|
||||
Some(deleted_at),
|
||||
)
|
||||
.await;
|
||||
insert_event(
|
||||
&pool,
|
||||
other_community,
|
||||
&event_id,
|
||||
&[6_u8; 32],
|
||||
"wrong tenant message",
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
let report_id = insert_event_report(&pool, report_community, &event_id).await;
|
||||
|
||||
let detail = get_report(&pool, report_id)
|
||||
.await
|
||||
.expect("query report")
|
||||
.expect("report exists");
|
||||
let message = detail.message.expect("reported message exists");
|
||||
assert_eq!(message.content, "reported message");
|
||||
assert_eq!(message.author_pubkey, hex::encode([5_u8; 32]));
|
||||
assert!(message.deleted_at.is_some());
|
||||
|
||||
sqlx::query("DELETE FROM moderation_reports WHERE community_id = $1")
|
||||
.bind(report_community)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.expect("delete report fixture");
|
||||
sqlx::query("DELETE FROM events WHERE community_id = ANY($1)")
|
||||
.bind(vec![report_community, other_community])
|
||||
.execute(&pool)
|
||||
.await
|
||||
.expect("delete event fixtures");
|
||||
sqlx::query("DELETE FROM communities WHERE id = ANY($1)")
|
||||
.bind(vec![report_community, other_community])
|
||||
.execute(&pool)
|
||||
.await
|
||||
.expect("delete community fixtures");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "requires Postgres"]
|
||||
async fn report_detail_has_no_message_for_non_event_target() {
|
||||
let pool = setup_pool().await;
|
||||
let community_id = insert_community(&pool, "pubkey-target").await;
|
||||
let report_id = insert_pubkey_report(&pool, community_id).await;
|
||||
|
||||
let detail = get_report(&pool, report_id)
|
||||
.await
|
||||
.expect("query report")
|
||||
.expect("report exists");
|
||||
assert_eq!(detail.report.target_kind, "pubkey");
|
||||
assert!(detail.message.is_none());
|
||||
|
||||
delete_report_fixture(&pool, community_id).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "requires Postgres"]
|
||||
async fn report_detail_has_no_message_when_event_row_is_missing() {
|
||||
let pool = setup_pool().await;
|
||||
let community_id = insert_community(&pool, "missing-event").await;
|
||||
let missing_event_id = vec![8_u8; 32];
|
||||
let report_id = insert_event_report(&pool, community_id, &missing_event_id).await;
|
||||
|
||||
let detail = get_report(&pool, report_id)
|
||||
.await
|
||||
.expect("query report")
|
||||
.expect("report exists");
|
||||
assert_eq!(detail.report.target_kind, "event");
|
||||
assert_eq!(detail.report.target, hex::encode(missing_event_id));
|
||||
assert!(detail.message.is_none());
|
||||
|
||||
delete_report_fixture(&pool, community_id).await;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -563,7 +563,7 @@ impl Db {
|
||||
pub async fn admin_get_report(
|
||||
&self,
|
||||
id: Uuid,
|
||||
) -> Result<Option<admin_moderation::AdminReport>> {
|
||||
) -> Result<Option<admin_moderation::AdminReportDetail>> {
|
||||
admin_moderation::get_report(&self.pool, id).await
|
||||
}
|
||||
|
||||
|
||||
@@ -126,7 +126,7 @@ async fn report_detail(
|
||||
State(state): State<Arc<crate::state::AppState>>,
|
||||
headers: HeaderMap,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<Json<buzz_db::admin_moderation::AdminReport>, ApiError> {
|
||||
) -> Result<Json<buzz_db::admin_moderation::AdminReportDetail>, ApiError> {
|
||||
authorize(&state, &headers)?;
|
||||
state
|
||||
.db
|
||||
@@ -374,6 +374,36 @@ mod tests {
|
||||
|
||||
const HASH: &str = "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789";
|
||||
|
||||
#[tokio::test]
|
||||
async fn report_detail_requires_admin_host_before_database_access() {
|
||||
let response = router(test_state().await)
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.uri(format!("/reports/{}", Uuid::nil()))
|
||||
.header(header::HOST, "community.example")
|
||||
.body(Body::empty())
|
||||
.expect("request"),
|
||||
)
|
||||
.await
|
||||
.expect("response");
|
||||
assert_eq!(response.status(), axum::http::StatusCode::FORBIDDEN);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn report_detail_rejects_unknown_report() {
|
||||
let response = router(test_state().await)
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.uri(format!("/reports/{}", Uuid::nil()))
|
||||
.header(header::HOST, "admin.example")
|
||||
.body(Body::empty())
|
||||
.expect("request"),
|
||||
)
|
||||
.await
|
||||
.expect("response");
|
||||
assert_eq!(response.status(), axum::http::StatusCode::NOT_FOUND);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn feedback_attachment_requires_admin_host_before_database_access() {
|
||||
let response = router(test_state().await)
|
||||
|
||||
Reference in New Issue
Block a user