@@ -582,7 +582,7 @@ async fn projects_list_non_member_returns_403() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn projects_list_platform_unavailable_degrades_gracefully() {
|
||||
async fn projects_list_platform_unavailable_returns_500() {
|
||||
let platform = MockPlatformClient::with_behavior(MockPlatformBehavior {
|
||||
list_projects_result: Some(Err(PlatformError::Unavailable(
|
||||
"connection refused".into(),
|
||||
@@ -603,12 +603,13 @@ async fn projects_list_platform_unavailable_degrades_gracefully() {
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR);
|
||||
let body = axum::body::to_bytes(response.into_body(), usize::MAX)
|
||||
.await
|
||||
.unwrap();
|
||||
let html = String::from_utf8(body.to_vec()).unwrap();
|
||||
assert!(html.contains("No projects yet"));
|
||||
assert!(html.contains("Something went wrong"));
|
||||
assert!(html.contains("connection refused"));
|
||||
}
|
||||
|
||||
// ─── Project detail ─────────────────────────────────────────────────
|
||||
@@ -634,9 +635,10 @@ async fn project_detail_returns_200_with_artifacts() {
|
||||
.await
|
||||
.unwrap();
|
||||
let html = String::from_utf8(body.to_vec()).unwrap();
|
||||
assert!(html.contains("my-api"));
|
||||
assert!(html.contains("Deploy v1.0"));
|
||||
assert!(html.contains("my-api-abc123"));
|
||||
// The timeline is now rendered by a Svelte web component
|
||||
assert!(html.contains("release-timeline"));
|
||||
assert!(html.contains("org=\"testorg\""));
|
||||
assert!(html.contains("project=\"my-api\""));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -664,7 +666,9 @@ async fn project_detail_empty_artifacts_shows_empty_state() {
|
||||
.await
|
||||
.unwrap();
|
||||
let html = String::from_utf8(body.to_vec()).unwrap();
|
||||
assert!(html.contains("No releases yet"));
|
||||
// Empty state is now rendered client-side by the Svelte component
|
||||
assert!(html.contains("release-timeline"));
|
||||
assert!(html.contains("project=\"my-api\""));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -698,6 +702,7 @@ async fn project_detail_shows_enriched_artifact_data() {
|
||||
type_organisation: None,
|
||||
type_name: None,
|
||||
type_version: None,
|
||||
status: None,
|
||||
}],
|
||||
created_at: "2026-03-07T12:00:00Z".into(),
|
||||
}])),
|
||||
@@ -722,10 +727,79 @@ async fn project_detail_shows_enriched_artifact_data() {
|
||||
.await
|
||||
.unwrap();
|
||||
let html = String::from_utf8(body.to_vec()).unwrap();
|
||||
assert!(html.contains("v2.0.0"));
|
||||
assert!(html.contains("main"));
|
||||
assert!(html.contains("abc1234"));
|
||||
assert!(html.contains("production"));
|
||||
// Enriched data is now rendered client-side by the Svelte component
|
||||
assert!(html.contains("release-timeline"));
|
||||
assert!(html.contains("project=\"my-api\""));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn timeline_api_returns_json_with_artifacts() {
|
||||
let (state, sessions) = test_state();
|
||||
let cookie = create_test_session(&sessions).await;
|
||||
let app = build_router(state);
|
||||
|
||||
let response = app
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.uri("/api/orgs/testorg/projects/my-api/timeline")
|
||||
.header("cookie", &cookie)
|
||||
.body(Body::empty())
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let body = axum::body::to_bytes(response.into_body(), usize::MAX)
|
||||
.await
|
||||
.unwrap();
|
||||
let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
|
||||
assert!(json["timeline"].is_array());
|
||||
assert!(json["lanes"].is_array());
|
||||
// Should have at least one timeline item from the mock data
|
||||
assert!(!json["timeline"].as_array().unwrap().is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn org_timeline_api_returns_json() {
|
||||
let (state, sessions) = test_state();
|
||||
let cookie = create_test_session(&sessions).await;
|
||||
let app = build_router(state);
|
||||
|
||||
let response = app
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.uri("/api/orgs/testorg/timeline")
|
||||
.header("cookie", &cookie)
|
||||
.body(Body::empty())
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let body = axum::body::to_bytes(response.into_body(), usize::MAX)
|
||||
.await
|
||||
.unwrap();
|
||||
let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
|
||||
assert!(json["timeline"].is_array());
|
||||
assert!(json["lanes"].is_array());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn timeline_api_requires_auth() {
|
||||
let (state, _sessions) = test_state();
|
||||
let app = build_router(state);
|
||||
|
||||
let response = app
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.uri("/api/orgs/testorg/projects/my-api/timeline")
|
||||
.body(Body::empty())
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
// Should redirect to login (302) when not authenticated
|
||||
assert_eq!(response.status(), StatusCode::SEE_OTHER);
|
||||
}
|
||||
|
||||
// ─── Artifact detail ────────────────────────────────────────────────
|
||||
@@ -787,6 +861,7 @@ async fn artifact_detail_shows_enriched_data() {
|
||||
type_organisation: None,
|
||||
type_name: None,
|
||||
type_version: None,
|
||||
status: None,
|
||||
},
|
||||
ArtifactDestination {
|
||||
name: "staging".into(),
|
||||
@@ -794,6 +869,7 @@ async fn artifact_detail_shows_enriched_data() {
|
||||
type_organisation: None,
|
||||
type_name: None,
|
||||
type_version: None,
|
||||
status: None,
|
||||
},
|
||||
],
|
||||
created_at: "2026-03-07T12:00:00Z".into(),
|
||||
@@ -1081,7 +1157,7 @@ async fn destinations_page_shows_empty_state() {
|
||||
.await
|
||||
.unwrap();
|
||||
let html = String::from_utf8(body.to_vec()).unwrap();
|
||||
assert!(html.contains("No destinations yet"));
|
||||
assert!(html.contains("No environments yet"));
|
||||
}
|
||||
|
||||
// ─── Releases ────────────────────────────────────────────────────────
|
||||
@@ -1169,5 +1245,288 @@ async fn releases_page_shows_empty_state() {
|
||||
.await
|
||||
.unwrap();
|
||||
let html = String::from_utf8(body.to_vec()).unwrap();
|
||||
assert!(html.contains("No releases yet"));
|
||||
// Empty state is now rendered client-side by the Svelte component
|
||||
assert!(html.contains("release-timeline"));
|
||||
assert!(html.contains("org=\"testorg\""));
|
||||
}
|
||||
|
||||
// ─── User profile ──────────────────────────────────────────────────
|
||||
|
||||
#[tokio::test]
|
||||
async fn user_profile_shows_username() {
|
||||
let (state, sessions) = test_state();
|
||||
let cookie = create_test_session(&sessions).await;
|
||||
let app = build_router(state);
|
||||
|
||||
let response = app
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.uri("/users/testuser")
|
||||
.header("cookie", &cookie)
|
||||
.body(Body::empty())
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let body = axum::body::to_bytes(response.into_body(), usize::MAX)
|
||||
.await
|
||||
.unwrap();
|
||||
let html = String::from_utf8(body.to_vec()).unwrap();
|
||||
assert!(html.contains("testuser"));
|
||||
assert!(html.contains("Member since"));
|
||||
}
|
||||
|
||||
// ─── Triggers (auto-release) ────────────────────────────────────────
|
||||
|
||||
#[tokio::test]
|
||||
async fn triggers_page_returns_200() {
|
||||
let (state, sessions) = test_state();
|
||||
let cookie = create_test_session(&sessions).await;
|
||||
let app = build_router(state);
|
||||
|
||||
let response = app
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.uri("/orgs/testorg/projects/my-api/triggers")
|
||||
.header("cookie", &cookie)
|
||||
.body(Body::empty())
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let body = axum::body::to_bytes(response.into_body(), usize::MAX)
|
||||
.await
|
||||
.unwrap();
|
||||
let html = String::from_utf8(body.to_vec()).unwrap();
|
||||
assert!(html.contains("Triggers"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn triggers_page_shows_existing_triggers() {
|
||||
use forage_core::platform::Trigger;
|
||||
|
||||
let platform = MockPlatformClient::with_behavior(MockPlatformBehavior {
|
||||
list_triggers_result: Some(Ok(vec![Trigger {
|
||||
id: "t1".into(),
|
||||
name: "deploy-main".into(),
|
||||
enabled: true,
|
||||
branch_pattern: Some("main".into()),
|
||||
title_pattern: None,
|
||||
author_pattern: None,
|
||||
commit_message_pattern: None,
|
||||
source_type_pattern: None,
|
||||
target_environments: vec!["staging".into()],
|
||||
target_destinations: vec![],
|
||||
force_release: false,
|
||||
use_pipeline: false,
|
||||
created_at: "2026-03-08T00:00:00Z".into(),
|
||||
updated_at: "2026-03-08T00:00:00Z".into(),
|
||||
}])),
|
||||
..Default::default()
|
||||
});
|
||||
let (state, sessions) = test_state_with(MockForestClient::new(), platform);
|
||||
let cookie = create_test_session(&sessions).await;
|
||||
let app = build_router(state);
|
||||
|
||||
let response = app
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.uri("/orgs/testorg/projects/my-api/triggers")
|
||||
.header("cookie", &cookie)
|
||||
.body(Body::empty())
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let body = axum::body::to_bytes(response.into_body(), usize::MAX)
|
||||
.await
|
||||
.unwrap();
|
||||
let html = String::from_utf8(body.to_vec()).unwrap();
|
||||
assert!(html.contains("deploy-main"));
|
||||
assert!(html.contains("staging"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn create_trigger_requires_admin() {
|
||||
let (state, sessions) = test_state();
|
||||
let cookie = create_test_session_member(&sessions).await;
|
||||
let app = build_router(state);
|
||||
|
||||
let response = app
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("POST")
|
||||
.uri("/orgs/testorg/projects/my-api/triggers")
|
||||
.header("cookie", &cookie)
|
||||
.header("content-type", "application/x-www-form-urlencoded")
|
||||
.body(Body::from("csrf_token=test-csrf&name=test-trigger"))
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(response.status(), StatusCode::FORBIDDEN);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn create_trigger_requires_csrf() {
|
||||
let (state, sessions) = test_state();
|
||||
let cookie = create_test_session(&sessions).await;
|
||||
let app = build_router(state);
|
||||
|
||||
let response = app
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("POST")
|
||||
.uri("/orgs/testorg/projects/my-api/triggers")
|
||||
.header("cookie", &cookie)
|
||||
.header("content-type", "application/x-www-form-urlencoded")
|
||||
.body(Body::from("csrf_token=wrong-token&name=test-trigger"))
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(response.status(), StatusCode::FORBIDDEN);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn create_trigger_success_redirects() {
|
||||
let (state, sessions) = test_state();
|
||||
let cookie = create_test_session(&sessions).await;
|
||||
let app = build_router(state);
|
||||
|
||||
let response = app
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("POST")
|
||||
.uri("/orgs/testorg/projects/my-api/triggers")
|
||||
.header("cookie", &cookie)
|
||||
.header("content-type", "application/x-www-form-urlencoded")
|
||||
.body(Body::from("csrf_token=test-csrf&name=deploy-main&branch_pattern=main&target_environments=staging")
|
||||
)
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(response.status(), StatusCode::SEE_OTHER);
|
||||
assert_eq!(
|
||||
response.headers().get("location").unwrap(),
|
||||
"/orgs/testorg/projects/my-api/triggers"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn toggle_trigger_requires_admin() {
|
||||
let (state, sessions) = test_state();
|
||||
let cookie = create_test_session_member(&sessions).await;
|
||||
let app = build_router(state);
|
||||
|
||||
let response = app
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("POST")
|
||||
.uri("/orgs/testorg/projects/my-api/triggers/deploy-main/toggle")
|
||||
.header("cookie", &cookie)
|
||||
.header("content-type", "application/x-www-form-urlencoded")
|
||||
.body(Body::from("csrf_token=test-csrf"))
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(response.status(), StatusCode::FORBIDDEN);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn delete_trigger_success_redirects() {
|
||||
let (state, sessions) = test_state();
|
||||
let cookie = create_test_session(&sessions).await;
|
||||
let app = build_router(state);
|
||||
|
||||
let response = app
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("POST")
|
||||
.uri("/orgs/testorg/projects/my-api/triggers/deploy-main/delete")
|
||||
.header("cookie", &cookie)
|
||||
.header("content-type", "application/x-www-form-urlencoded")
|
||||
.body(Body::from("csrf_token=test-csrf"))
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(response.status(), StatusCode::SEE_OTHER);
|
||||
assert_eq!(
|
||||
response.headers().get("location").unwrap(),
|
||||
"/orgs/testorg/projects/my-api/triggers"
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Deployment Policies ────────────────────────────────────────────
|
||||
|
||||
#[tokio::test]
|
||||
async fn policies_page_returns_200() {
|
||||
let (state, sessions) = test_state();
|
||||
let cookie = create_test_session(&sessions).await;
|
||||
let app = build_router(state);
|
||||
|
||||
let response = app
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.uri("/orgs/testorg/projects/my-api/policies")
|
||||
.header("cookie", &cookie)
|
||||
.body(Body::empty())
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let body = axum::body::to_bytes(response.into_body(), usize::MAX)
|
||||
.await
|
||||
.unwrap();
|
||||
let html = String::from_utf8(body.to_vec()).unwrap();
|
||||
assert!(html.contains("Deployment Policies"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn create_policy_requires_admin() {
|
||||
let (state, sessions) = test_state();
|
||||
let cookie = create_test_session_member(&sessions).await;
|
||||
let app = build_router(state);
|
||||
|
||||
let response = app
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("POST")
|
||||
.uri("/orgs/testorg/projects/my-api/policies")
|
||||
.header("cookie", &cookie)
|
||||
.header("content-type", "application/x-www-form-urlencoded")
|
||||
.body(Body::from("csrf_token=test-csrf&name=test-policy&policy_type=soak_time"))
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(response.status(), StatusCode::FORBIDDEN);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn create_policy_requires_csrf() {
|
||||
let (state, sessions) = test_state();
|
||||
let cookie = create_test_session(&sessions).await;
|
||||
let app = build_router(state);
|
||||
|
||||
let response = app
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("POST")
|
||||
.uri("/orgs/testorg/projects/my-api/policies")
|
||||
.header("cookie", &cookie)
|
||||
.header("content-type", "application/x-www-form-urlencoded")
|
||||
.body(Body::from("csrf_token=wrong-token&name=test-policy&policy_type=soak_time"))
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(response.status(), StatusCode::FORBIDDEN);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user