825 lines
26 KiB
Rust
825 lines
26 KiB
Rust
use std::sync::{Arc, Mutex};
|
|
|
|
use axum::Router;
|
|
use chrono::Utc;
|
|
use forage_core::auth::*;
|
|
use forage_core::platform::{
|
|
Artifact, ArtifactContext, CreatePolicyInput, CreateReleasePipelineInput, CreateTriggerInput,
|
|
Destination, Environment, ForestPlatform, Organisation, OrgMember, PlatformError, Policy,
|
|
ReleasePipeline, Trigger, UpdatePolicyInput, UpdateReleasePipelineInput, UpdateTriggerInput,
|
|
};
|
|
use forage_core::session::{
|
|
CachedOrg, CachedUser, InMemorySessionStore, SessionData, SessionStore,
|
|
};
|
|
|
|
use crate::state::AppState;
|
|
use crate::templates::TemplateEngine;
|
|
|
|
/// Configurable mock behavior for testing different scenarios.
|
|
#[derive(Default)]
|
|
pub(crate) struct MockBehavior {
|
|
pub register_result: Option<Result<AuthTokens, AuthError>>,
|
|
pub login_result: Option<Result<AuthTokens, AuthError>>,
|
|
pub refresh_result: Option<Result<AuthTokens, AuthError>>,
|
|
pub get_user_result: Option<Result<User, AuthError>>,
|
|
pub list_tokens_result: Option<Result<Vec<PersonalAccessToken>, AuthError>>,
|
|
pub create_token_result: Option<Result<CreatedToken, AuthError>>,
|
|
pub delete_token_result: Option<Result<(), AuthError>>,
|
|
pub update_username_result: Option<Result<User, AuthError>>,
|
|
pub change_password_result: Option<Result<(), AuthError>>,
|
|
pub add_email_result: Option<Result<UserEmail, AuthError>>,
|
|
pub remove_email_result: Option<Result<(), AuthError>>,
|
|
}
|
|
|
|
/// Configurable mock behavior for platform (orgs, projects, artifacts).
|
|
#[derive(Default)]
|
|
pub(crate) struct MockPlatformBehavior {
|
|
pub list_orgs_result: Option<Result<Vec<Organisation>, PlatformError>>,
|
|
pub list_projects_result: Option<Result<Vec<String>, PlatformError>>,
|
|
pub list_artifacts_result: Option<Result<Vec<Artifact>, PlatformError>>,
|
|
pub create_organisation_result: Option<Result<String, PlatformError>>,
|
|
pub list_members_result: Option<Result<Vec<OrgMember>, PlatformError>>,
|
|
pub add_member_result: Option<Result<OrgMember, PlatformError>>,
|
|
pub remove_member_result: Option<Result<(), PlatformError>>,
|
|
pub update_member_role_result: Option<Result<OrgMember, PlatformError>>,
|
|
pub get_artifact_by_slug_result: Option<Result<Artifact, PlatformError>>,
|
|
pub list_environments_result: Option<Result<Vec<Environment>, PlatformError>>,
|
|
pub list_destinations_result: Option<Result<Vec<Destination>, PlatformError>>,
|
|
pub list_triggers_result: Option<Result<Vec<Trigger>, PlatformError>>,
|
|
pub create_trigger_result: Option<Result<Trigger, PlatformError>>,
|
|
pub update_trigger_result: Option<Result<Trigger, PlatformError>>,
|
|
pub delete_trigger_result: Option<Result<(), PlatformError>>,
|
|
pub list_release_pipelines_result: Option<Result<Vec<ReleasePipeline>, PlatformError>>,
|
|
pub create_release_pipeline_result: Option<Result<ReleasePipeline, PlatformError>>,
|
|
pub update_release_pipeline_result: Option<Result<ReleasePipeline, PlatformError>>,
|
|
pub delete_release_pipeline_result: Option<Result<(), PlatformError>>,
|
|
}
|
|
|
|
pub(crate) fn ok_tokens() -> AuthTokens {
|
|
AuthTokens {
|
|
access_token: "mock-access".into(),
|
|
refresh_token: "mock-refresh".into(),
|
|
expires_in_seconds: 3600,
|
|
}
|
|
}
|
|
|
|
pub(crate) fn ok_user() -> User {
|
|
User {
|
|
user_id: "user-123".into(),
|
|
username: "testuser".into(),
|
|
emails: vec![UserEmail {
|
|
email: "test@example.com".into(),
|
|
verified: true,
|
|
}],
|
|
}
|
|
}
|
|
|
|
/// Mock forest client with per-test configurable behavior.
|
|
pub(crate) struct MockForestClient {
|
|
behavior: Mutex<MockBehavior>,
|
|
}
|
|
|
|
impl MockForestClient {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
behavior: Mutex::new(MockBehavior::default()),
|
|
}
|
|
}
|
|
|
|
pub fn with_behavior(behavior: MockBehavior) -> Self {
|
|
Self {
|
|
behavior: Mutex::new(behavior),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[async_trait::async_trait]
|
|
impl ForestAuth for MockForestClient {
|
|
async fn register(
|
|
&self,
|
|
_username: &str,
|
|
_email: &str,
|
|
_password: &str,
|
|
) -> Result<AuthTokens, AuthError> {
|
|
let b = self.behavior.lock().unwrap();
|
|
b.register_result.clone().unwrap_or(Ok(ok_tokens()))
|
|
}
|
|
|
|
async fn login(
|
|
&self,
|
|
identifier: &str,
|
|
password: &str,
|
|
) -> Result<AuthTokens, AuthError> {
|
|
let b = self.behavior.lock().unwrap();
|
|
if let Some(result) = b.login_result.clone() {
|
|
return result;
|
|
}
|
|
if identifier == "testuser" && password == "CorrectPass123" {
|
|
Ok(ok_tokens())
|
|
} else {
|
|
Err(AuthError::InvalidCredentials)
|
|
}
|
|
}
|
|
|
|
async fn refresh_token(&self, _refresh_token: &str) -> Result<AuthTokens, AuthError> {
|
|
let b = self.behavior.lock().unwrap();
|
|
b.refresh_result.clone().unwrap_or(Ok(AuthTokens {
|
|
access_token: "refreshed-access".into(),
|
|
refresh_token: "refreshed-refresh".into(),
|
|
expires_in_seconds: 3600,
|
|
}))
|
|
}
|
|
|
|
async fn logout(&self, _refresh_token: &str) -> Result<(), AuthError> {
|
|
Ok(())
|
|
}
|
|
|
|
async fn get_user(&self, access_token: &str) -> Result<User, AuthError> {
|
|
let b = self.behavior.lock().unwrap();
|
|
if let Some(result) = b.get_user_result.clone() {
|
|
return result;
|
|
}
|
|
if access_token == "mock-access" || access_token == "refreshed-access" {
|
|
Ok(ok_user())
|
|
} else {
|
|
Err(AuthError::NotAuthenticated)
|
|
}
|
|
}
|
|
|
|
async fn list_tokens(
|
|
&self,
|
|
_access_token: &str,
|
|
_user_id: &str,
|
|
) -> Result<Vec<PersonalAccessToken>, AuthError> {
|
|
let b = self.behavior.lock().unwrap();
|
|
b.list_tokens_result.clone().unwrap_or(Ok(vec![]))
|
|
}
|
|
|
|
async fn create_token(
|
|
&self,
|
|
_access_token: &str,
|
|
_user_id: &str,
|
|
name: &str,
|
|
) -> Result<CreatedToken, AuthError> {
|
|
let b = self.behavior.lock().unwrap();
|
|
b.create_token_result.clone().unwrap_or(Ok(CreatedToken {
|
|
token: PersonalAccessToken {
|
|
token_id: "tok-1".into(),
|
|
name: name.into(),
|
|
scopes: vec![],
|
|
created_at: None,
|
|
last_used: None,
|
|
expires_at: None,
|
|
},
|
|
raw_token: "forg_abcdef1234567890".into(),
|
|
}))
|
|
}
|
|
|
|
async fn delete_token(
|
|
&self,
|
|
_access_token: &str,
|
|
_token_id: &str,
|
|
) -> Result<(), AuthError> {
|
|
let b = self.behavior.lock().unwrap();
|
|
b.delete_token_result.clone().unwrap_or(Ok(()))
|
|
}
|
|
|
|
async fn update_username(
|
|
&self,
|
|
_access_token: &str,
|
|
_user_id: &str,
|
|
new_username: &str,
|
|
) -> Result<User, AuthError> {
|
|
let b = self.behavior.lock().unwrap();
|
|
b.update_username_result.clone().unwrap_or(Ok(User {
|
|
user_id: "user-123".into(),
|
|
username: new_username.into(),
|
|
emails: vec![UserEmail {
|
|
email: "test@example.com".into(),
|
|
verified: true,
|
|
}],
|
|
}))
|
|
}
|
|
|
|
async fn change_password(
|
|
&self,
|
|
_access_token: &str,
|
|
_user_id: &str,
|
|
_current_password: &str,
|
|
_new_password: &str,
|
|
) -> Result<(), AuthError> {
|
|
let b = self.behavior.lock().unwrap();
|
|
b.change_password_result.clone().unwrap_or(Ok(()))
|
|
}
|
|
|
|
async fn add_email(
|
|
&self,
|
|
_access_token: &str,
|
|
_user_id: &str,
|
|
email: &str,
|
|
) -> Result<UserEmail, AuthError> {
|
|
let b = self.behavior.lock().unwrap();
|
|
b.add_email_result.clone().unwrap_or(Ok(UserEmail {
|
|
email: email.into(),
|
|
verified: false,
|
|
}))
|
|
}
|
|
|
|
async fn get_user_by_username(
|
|
&self,
|
|
_access_token: &str,
|
|
username: &str,
|
|
) -> Result<UserProfile, AuthError> {
|
|
Ok(UserProfile {
|
|
user_id: "user-123".into(),
|
|
username: username.into(),
|
|
created_at: Some("2025-01-15T10:00:00Z".into()),
|
|
})
|
|
}
|
|
|
|
async fn remove_email(
|
|
&self,
|
|
_access_token: &str,
|
|
_user_id: &str,
|
|
_email: &str,
|
|
) -> Result<(), AuthError> {
|
|
let b = self.behavior.lock().unwrap();
|
|
b.remove_email_result.clone().unwrap_or(Ok(()))
|
|
}
|
|
}
|
|
|
|
pub(crate) struct MockPlatformClient {
|
|
behavior: Mutex<MockPlatformBehavior>,
|
|
}
|
|
|
|
impl MockPlatformClient {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
behavior: Mutex::new(MockPlatformBehavior::default()),
|
|
}
|
|
}
|
|
|
|
pub fn with_behavior(behavior: MockPlatformBehavior) -> Self {
|
|
Self {
|
|
behavior: Mutex::new(behavior),
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(crate) fn default_orgs() -> Vec<Organisation> {
|
|
vec![Organisation {
|
|
organisation_id: "org-1".into(),
|
|
name: "testorg".into(),
|
|
role: "admin".into(),
|
|
}]
|
|
}
|
|
|
|
#[async_trait::async_trait]
|
|
impl ForestPlatform for MockPlatformClient {
|
|
async fn list_my_organisations(
|
|
&self,
|
|
_access_token: &str,
|
|
) -> Result<Vec<Organisation>, PlatformError> {
|
|
let b = self.behavior.lock().unwrap();
|
|
b.list_orgs_result.clone().unwrap_or(Ok(default_orgs()))
|
|
}
|
|
|
|
async fn list_projects(
|
|
&self,
|
|
_access_token: &str,
|
|
_organisation: &str,
|
|
) -> Result<Vec<String>, PlatformError> {
|
|
let b = self.behavior.lock().unwrap();
|
|
b.list_projects_result
|
|
.clone()
|
|
.unwrap_or(Ok(vec!["my-api".into()]))
|
|
}
|
|
|
|
async fn list_artifacts(
|
|
&self,
|
|
_access_token: &str,
|
|
_organisation: &str,
|
|
_project: &str,
|
|
) -> Result<Vec<Artifact>, PlatformError> {
|
|
let b = self.behavior.lock().unwrap();
|
|
b.list_artifacts_result.clone().unwrap_or(Ok(vec![Artifact {
|
|
artifact_id: "art-1".into(),
|
|
slug: "my-api-abc123".into(),
|
|
context: ArtifactContext {
|
|
title: "Deploy v1.0".into(),
|
|
description: Some("Initial release".into()),
|
|
web: None,
|
|
pr: None,
|
|
},
|
|
source: None,
|
|
git_ref: None,
|
|
destinations: vec![],
|
|
created_at: "2026-03-07T12:00:00Z".into(),
|
|
}]))
|
|
}
|
|
|
|
async fn create_organisation(
|
|
&self,
|
|
_access_token: &str,
|
|
name: &str,
|
|
) -> Result<String, PlatformError> {
|
|
let b = self.behavior.lock().unwrap();
|
|
b.create_organisation_result
|
|
.clone()
|
|
.unwrap_or(Ok(format!("org-{name}")))
|
|
}
|
|
|
|
async fn list_members(
|
|
&self,
|
|
_access_token: &str,
|
|
_organisation_id: &str,
|
|
) -> Result<Vec<OrgMember>, PlatformError> {
|
|
let b = self.behavior.lock().unwrap();
|
|
b.list_members_result.clone().unwrap_or(Ok(vec![OrgMember {
|
|
user_id: "user-123".into(),
|
|
username: "testuser".into(),
|
|
role: "owner".into(),
|
|
joined_at: Some("2026-01-01T00:00:00Z".into()),
|
|
}]))
|
|
}
|
|
|
|
async fn add_member(
|
|
&self,
|
|
_access_token: &str,
|
|
_organisation_id: &str,
|
|
user_id: &str,
|
|
role: &str,
|
|
) -> Result<OrgMember, PlatformError> {
|
|
let b = self.behavior.lock().unwrap();
|
|
b.add_member_result.clone().unwrap_or(Ok(OrgMember {
|
|
user_id: user_id.into(),
|
|
username: "newuser".into(),
|
|
role: role.into(),
|
|
joined_at: Some("2026-03-07T00:00:00Z".into()),
|
|
}))
|
|
}
|
|
|
|
async fn remove_member(
|
|
&self,
|
|
_access_token: &str,
|
|
_organisation_id: &str,
|
|
_user_id: &str,
|
|
) -> Result<(), PlatformError> {
|
|
let b = self.behavior.lock().unwrap();
|
|
b.remove_member_result.clone().unwrap_or(Ok(()))
|
|
}
|
|
|
|
async fn update_member_role(
|
|
&self,
|
|
_access_token: &str,
|
|
_organisation_id: &str,
|
|
user_id: &str,
|
|
role: &str,
|
|
) -> Result<OrgMember, PlatformError> {
|
|
let b = self.behavior.lock().unwrap();
|
|
b.update_member_role_result.clone().unwrap_or(Ok(OrgMember {
|
|
user_id: user_id.into(),
|
|
username: "testuser".into(),
|
|
role: role.into(),
|
|
joined_at: Some("2026-01-01T00:00:00Z".into()),
|
|
}))
|
|
}
|
|
|
|
async fn get_artifact_by_slug(
|
|
&self,
|
|
_access_token: &str,
|
|
slug: &str,
|
|
) -> Result<Artifact, PlatformError> {
|
|
let b = self.behavior.lock().unwrap();
|
|
b.get_artifact_by_slug_result
|
|
.clone()
|
|
.unwrap_or(Ok(Artifact {
|
|
artifact_id: "art-1".into(),
|
|
slug: slug.into(),
|
|
context: ArtifactContext {
|
|
title: "Deploy v1.0".into(),
|
|
description: Some("Initial release".into()),
|
|
web: None,
|
|
pr: None,
|
|
},
|
|
source: None,
|
|
git_ref: None,
|
|
destinations: vec![],
|
|
created_at: "2026-03-07T12:00:00Z".into(),
|
|
}))
|
|
}
|
|
|
|
async fn list_environments(
|
|
&self,
|
|
_access_token: &str,
|
|
_organisation: &str,
|
|
) -> Result<Vec<Environment>, PlatformError> {
|
|
let b = self.behavior.lock().unwrap();
|
|
b.list_environments_result.clone().unwrap_or(Ok(vec![]))
|
|
}
|
|
|
|
async fn list_destinations(
|
|
&self,
|
|
_access_token: &str,
|
|
_organisation: &str,
|
|
) -> Result<Vec<Destination>, PlatformError> {
|
|
let b = self.behavior.lock().unwrap();
|
|
b.list_destinations_result.clone().unwrap_or(Ok(vec![]))
|
|
}
|
|
|
|
async fn create_environment(
|
|
&self,
|
|
_access_token: &str,
|
|
organisation: &str,
|
|
name: &str,
|
|
description: Option<&str>,
|
|
sort_order: i32,
|
|
) -> Result<Environment, PlatformError> {
|
|
Ok(Environment {
|
|
id: format!("env-{name}"),
|
|
organisation: organisation.into(),
|
|
name: name.into(),
|
|
description: description.map(|s| s.to_string()),
|
|
sort_order,
|
|
created_at: "2026-03-08T00:00:00Z".into(),
|
|
})
|
|
}
|
|
|
|
async fn create_destination(
|
|
&self,
|
|
_access_token: &str,
|
|
_organisation: &str,
|
|
_name: &str,
|
|
_environment: &str,
|
|
_metadata: &std::collections::HashMap<String, String>,
|
|
_dest_type: Option<&forage_core::platform::DestinationType>,
|
|
) -> Result<(), PlatformError> {
|
|
Ok(())
|
|
}
|
|
|
|
async fn update_destination(
|
|
&self,
|
|
_access_token: &str,
|
|
_name: &str,
|
|
_metadata: &std::collections::HashMap<String, String>,
|
|
) -> Result<(), PlatformError> {
|
|
Ok(())
|
|
}
|
|
|
|
async fn get_destination_states(
|
|
&self,
|
|
_access_token: &str,
|
|
_organisation: &str,
|
|
_project: Option<&str>,
|
|
) -> Result<forage_core::platform::DeploymentStates, PlatformError> {
|
|
Ok(forage_core::platform::DeploymentStates {
|
|
destinations: vec![],
|
|
})
|
|
}
|
|
|
|
async fn get_release_intent_states(
|
|
&self,
|
|
_access_token: &str,
|
|
_organisation: &str,
|
|
_project: Option<&str>,
|
|
_include_completed: bool,
|
|
) -> Result<Vec<forage_core::platform::ReleaseIntentState>, PlatformError> {
|
|
Ok(vec![])
|
|
}
|
|
|
|
async fn release_artifact(
|
|
&self,
|
|
_access_token: &str,
|
|
_artifact_id: &str,
|
|
_destinations: &[String],
|
|
_environments: &[String],
|
|
_use_pipeline: bool,
|
|
) -> Result<(), PlatformError> {
|
|
Ok(())
|
|
}
|
|
|
|
async fn list_triggers(
|
|
&self,
|
|
_access_token: &str,
|
|
_organisation: &str,
|
|
_project: &str,
|
|
) -> Result<Vec<Trigger>, PlatformError> {
|
|
let b = self.behavior.lock().unwrap();
|
|
b.list_triggers_result.clone().unwrap_or(Ok(vec![]))
|
|
}
|
|
|
|
async fn create_trigger(
|
|
&self,
|
|
_access_token: &str,
|
|
_organisation: &str,
|
|
_project: &str,
|
|
input: &CreateTriggerInput,
|
|
) -> Result<Trigger, PlatformError> {
|
|
let b = self.behavior.lock().unwrap();
|
|
b.create_trigger_result.clone().unwrap_or(Ok(Trigger {
|
|
id: "trigger-1".into(),
|
|
name: input.name.clone(),
|
|
enabled: true,
|
|
branch_pattern: input.branch_pattern.clone(),
|
|
title_pattern: input.title_pattern.clone(),
|
|
author_pattern: input.author_pattern.clone(),
|
|
commit_message_pattern: input.commit_message_pattern.clone(),
|
|
source_type_pattern: input.source_type_pattern.clone(),
|
|
target_environments: input.target_environments.clone(),
|
|
target_destinations: input.target_destinations.clone(),
|
|
force_release: input.force_release,
|
|
use_pipeline: input.use_pipeline,
|
|
created_at: "2026-03-08T00:00:00Z".into(),
|
|
updated_at: "2026-03-08T00:00:00Z".into(),
|
|
}))
|
|
}
|
|
|
|
async fn update_trigger(
|
|
&self,
|
|
_access_token: &str,
|
|
_organisation: &str,
|
|
_project: &str,
|
|
name: &str,
|
|
input: &UpdateTriggerInput,
|
|
) -> Result<Trigger, PlatformError> {
|
|
let b = self.behavior.lock().unwrap();
|
|
b.update_trigger_result.clone().unwrap_or(Ok(Trigger {
|
|
id: "trigger-1".into(),
|
|
name: name.into(),
|
|
enabled: input.enabled.unwrap_or(true),
|
|
branch_pattern: input.branch_pattern.clone(),
|
|
title_pattern: input.title_pattern.clone(),
|
|
author_pattern: input.author_pattern.clone(),
|
|
commit_message_pattern: input.commit_message_pattern.clone(),
|
|
source_type_pattern: input.source_type_pattern.clone(),
|
|
target_environments: input.target_environments.clone(),
|
|
target_destinations: input.target_destinations.clone(),
|
|
force_release: input.force_release.unwrap_or(false),
|
|
use_pipeline: input.use_pipeline.unwrap_or(false),
|
|
created_at: "2026-03-08T00:00:00Z".into(),
|
|
updated_at: "2026-03-08T00:00:00Z".into(),
|
|
}))
|
|
}
|
|
|
|
async fn delete_trigger(
|
|
&self,
|
|
_access_token: &str,
|
|
_organisation: &str,
|
|
_project: &str,
|
|
_name: &str,
|
|
) -> Result<(), PlatformError> {
|
|
let b = self.behavior.lock().unwrap();
|
|
b.delete_trigger_result.clone().unwrap_or(Ok(()))
|
|
}
|
|
|
|
async fn list_policies(
|
|
&self,
|
|
_access_token: &str,
|
|
_organisation: &str,
|
|
_project: &str,
|
|
) -> Result<Vec<Policy>, PlatformError> {
|
|
Ok(vec![])
|
|
}
|
|
|
|
async fn create_policy(
|
|
&self,
|
|
_access_token: &str,
|
|
_organisation: &str,
|
|
_project: &str,
|
|
_input: &CreatePolicyInput,
|
|
) -> Result<Policy, PlatformError> {
|
|
Err(PlatformError::Other("not implemented in mock".into()))
|
|
}
|
|
|
|
async fn update_policy(
|
|
&self,
|
|
_access_token: &str,
|
|
_organisation: &str,
|
|
_project: &str,
|
|
_name: &str,
|
|
_input: &UpdatePolicyInput,
|
|
) -> Result<Policy, PlatformError> {
|
|
Err(PlatformError::Other("not implemented in mock".into()))
|
|
}
|
|
|
|
async fn delete_policy(
|
|
&self,
|
|
_access_token: &str,
|
|
_organisation: &str,
|
|
_project: &str,
|
|
_name: &str,
|
|
) -> Result<(), PlatformError> {
|
|
Ok(())
|
|
}
|
|
|
|
async fn list_release_pipelines(
|
|
&self,
|
|
_access_token: &str,
|
|
_organisation: &str,
|
|
_project: &str,
|
|
) -> Result<Vec<ReleasePipeline>, PlatformError> {
|
|
let b = self.behavior.lock().unwrap();
|
|
b.list_release_pipelines_result
|
|
.clone()
|
|
.unwrap_or(Ok(vec![]))
|
|
}
|
|
|
|
async fn create_release_pipeline(
|
|
&self,
|
|
_access_token: &str,
|
|
_organisation: &str,
|
|
_project: &str,
|
|
input: &CreateReleasePipelineInput,
|
|
) -> Result<ReleasePipeline, PlatformError> {
|
|
let b = self.behavior.lock().unwrap();
|
|
b.create_release_pipeline_result
|
|
.clone()
|
|
.unwrap_or(Ok(ReleasePipeline {
|
|
id: "pipeline-1".into(),
|
|
name: input.name.clone(),
|
|
enabled: true,
|
|
stages: input.stages.clone(),
|
|
created_at: "2026-03-08T00:00:00Z".into(),
|
|
updated_at: "2026-03-08T00:00:00Z".into(),
|
|
}))
|
|
}
|
|
|
|
async fn update_release_pipeline(
|
|
&self,
|
|
_access_token: &str,
|
|
_organisation: &str,
|
|
_project: &str,
|
|
name: &str,
|
|
input: &UpdateReleasePipelineInput,
|
|
) -> Result<ReleasePipeline, PlatformError> {
|
|
let b = self.behavior.lock().unwrap();
|
|
b.update_release_pipeline_result
|
|
.clone()
|
|
.unwrap_or(Ok(ReleasePipeline {
|
|
id: "pipeline-1".into(),
|
|
name: name.into(),
|
|
enabled: input.enabled.unwrap_or(true),
|
|
stages: input.stages.clone().unwrap_or_default(),
|
|
created_at: "2026-03-08T00:00:00Z".into(),
|
|
updated_at: "2026-03-08T00:00:00Z".into(),
|
|
}))
|
|
}
|
|
|
|
async fn delete_release_pipeline(
|
|
&self,
|
|
_access_token: &str,
|
|
_organisation: &str,
|
|
_project: &str,
|
|
_name: &str,
|
|
) -> Result<(), PlatformError> {
|
|
let b = self.behavior.lock().unwrap();
|
|
b.delete_release_pipeline_result.clone().unwrap_or(Ok(()))
|
|
}
|
|
}
|
|
|
|
pub(crate) fn make_templates() -> TemplateEngine {
|
|
let workspace_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
|
.parent()
|
|
.unwrap()
|
|
.parent()
|
|
.unwrap();
|
|
TemplateEngine::from_path(&workspace_root.join("templates"))
|
|
.expect("templates must load for tests")
|
|
}
|
|
|
|
pub(crate) fn test_state() -> (AppState, Arc<InMemorySessionStore>) {
|
|
test_state_with(MockForestClient::new(), MockPlatformClient::new())
|
|
}
|
|
|
|
pub(crate) fn test_state_with(
|
|
mock: MockForestClient,
|
|
platform: MockPlatformClient,
|
|
) -> (AppState, Arc<InMemorySessionStore>) {
|
|
let sessions = Arc::new(InMemorySessionStore::new());
|
|
let state = AppState::new(
|
|
make_templates(),
|
|
Arc::new(mock),
|
|
Arc::new(platform),
|
|
sessions.clone(),
|
|
);
|
|
(state, sessions)
|
|
}
|
|
|
|
pub(crate) fn test_app() -> Router {
|
|
let (state, _) = test_state();
|
|
crate::build_router(state)
|
|
}
|
|
|
|
pub(crate) fn test_app_with(mock: MockForestClient) -> Router {
|
|
let (state, _) = test_state_with(mock, MockPlatformClient::new());
|
|
crate::build_router(state)
|
|
}
|
|
|
|
pub(crate) fn default_test_orgs() -> Vec<CachedOrg> {
|
|
vec![CachedOrg {
|
|
organisation_id: "org-1".into(),
|
|
name: "testorg".into(),
|
|
role: "owner".into(),
|
|
}]
|
|
}
|
|
|
|
/// Create a test session and return the cookie header value.
|
|
pub(crate) async fn create_test_session(sessions: &Arc<InMemorySessionStore>) -> String {
|
|
let now = Utc::now();
|
|
let data = SessionData {
|
|
access_token: "mock-access".into(),
|
|
refresh_token: "mock-refresh".into(),
|
|
csrf_token: "test-csrf".into(),
|
|
access_expires_at: now + chrono::Duration::hours(1),
|
|
user: Some(CachedUser {
|
|
user_id: "user-123".into(),
|
|
username: "testuser".into(),
|
|
emails: vec![UserEmail {
|
|
email: "test@example.com".into(),
|
|
verified: true,
|
|
}],
|
|
orgs: default_test_orgs(),
|
|
}),
|
|
created_at: now,
|
|
last_seen_at: now,
|
|
};
|
|
let session_id = sessions.create(data).await.unwrap();
|
|
format!("forage_session={}", session_id)
|
|
}
|
|
|
|
/// Create a test session with an expired access token but valid refresh token.
|
|
pub(crate) async fn create_expired_session(sessions: &Arc<InMemorySessionStore>) -> String {
|
|
let now = Utc::now();
|
|
let data = SessionData {
|
|
access_token: "expired-access".into(),
|
|
refresh_token: "mock-refresh".into(),
|
|
csrf_token: "test-csrf".into(),
|
|
access_expires_at: now - chrono::Duration::seconds(10),
|
|
user: Some(CachedUser {
|
|
user_id: "user-123".into(),
|
|
username: "testuser".into(),
|
|
emails: vec![UserEmail {
|
|
email: "test@example.com".into(),
|
|
verified: true,
|
|
}],
|
|
orgs: default_test_orgs(),
|
|
}),
|
|
created_at: now,
|
|
last_seen_at: now,
|
|
};
|
|
let session_id = sessions.create(data).await.unwrap();
|
|
format!("forage_session={}", session_id)
|
|
}
|
|
|
|
/// Create a test session with "member" role (non-admin, for authorization tests).
|
|
pub(crate) async fn create_test_session_member(sessions: &Arc<InMemorySessionStore>) -> String {
|
|
let now = Utc::now();
|
|
let data = SessionData {
|
|
access_token: "mock-access".into(),
|
|
refresh_token: "mock-refresh".into(),
|
|
csrf_token: "test-csrf".into(),
|
|
access_expires_at: now + chrono::Duration::hours(1),
|
|
user: Some(CachedUser {
|
|
user_id: "user-123".into(),
|
|
username: "testuser".into(),
|
|
emails: vec![UserEmail {
|
|
email: "test@example.com".into(),
|
|
verified: true,
|
|
}],
|
|
orgs: vec![CachedOrg {
|
|
organisation_id: "org-1".into(),
|
|
name: "testorg".into(),
|
|
role: "member".into(),
|
|
}],
|
|
}),
|
|
created_at: now,
|
|
last_seen_at: now,
|
|
};
|
|
let session_id = sessions.create(data).await.unwrap();
|
|
format!("forage_session={}", session_id)
|
|
}
|
|
|
|
/// Create a test session with no cached orgs (for onboarding tests).
|
|
pub(crate) async fn create_test_session_no_orgs(sessions: &Arc<InMemorySessionStore>) -> String {
|
|
let now = Utc::now();
|
|
let data = SessionData {
|
|
access_token: "mock-access".into(),
|
|
refresh_token: "mock-refresh".into(),
|
|
csrf_token: "test-csrf".into(),
|
|
access_expires_at: now + chrono::Duration::hours(1),
|
|
user: Some(CachedUser {
|
|
user_id: "user-123".into(),
|
|
username: "testuser".into(),
|
|
emails: vec![UserEmail {
|
|
email: "test@example.com".into(),
|
|
verified: true,
|
|
}],
|
|
orgs: vec![],
|
|
}),
|
|
created_at: now,
|
|
last_seen_at: now,
|
|
};
|
|
let session_id = sessions.create(data).await.unwrap();
|
|
format!("forage_session={}", session_id)
|
|
}
|