pdk-unit 1.8.0

PDK Unit Test Framework
Documentation
// Copyright (c) 2026, Salesforce, Inc.,
// All rights reserved.
// For full license text, see the LICENSE.txt file
use super::utils::{matches, not_found};
use crate::{Backend, UnitHttpMessage, UnitHttpRequest, UnitHttpResponse};
use regex::Regex;
use serde::Serialize;
use serde_json::json;
use sha2::{Digest, Sha256};
use std::cell::RefCell;

#[derive(Default)]
pub struct AnypointBackend {
    contracts: RefCell<Vec<ContractsDataResponse>>,
}

const CONTRACTS_PATH_PREFIX: &str =
    "/apigateway/ccs/v3/organizations/test-org-id/environments/test-env-id/apis/1/contracts";

thread_local! {
    static LOGIN_PATH: Regex = Regex::new("^/accounts/oauth2/token$").unwrap();
    static CONTRACTS_PATH: Regex = Regex::new("^/apigateway/ccs/v3/organizations/test-org-id/environments/test-env-id/apis/1/contracts$").unwrap();
    static CONTRACTS_UPDATE_PATH: Regex = Regex::new(r"^/apigateway/ccs/v3/organizations/test-org-id/environments/test-env-id/apis/1/contracts/(.*)$").unwrap();
}

impl Backend for AnypointBackend {
    fn call(&self, req: UnitHttpRequest) -> UnitHttpResponse {
        let path = req.header(":path").unwrap();

        if let Some([]) = matches(&LOGIN_PATH, path) {
            self.login()
        } else if let Some([]) = matches(&CONTRACTS_PATH, path) {
            self.contracts(0, path)
        } else if let Some([pos]) = matches(&CONTRACTS_UPDATE_PATH, path) {
            self.contracts(pos.parse().unwrap(), path)
        } else {
            not_found()
        }
    }
}

impl AnypointBackend {
    pub(crate) fn login(&self) -> UnitHttpResponse {
        UnitHttpResponse::new(200)
            .with_header("content-type", "application/json")
            .with_body(json!({"access_token": "test", "token_type": "Bearer"}).to_string())
    }

    pub(crate) fn contracts(&self, start: usize, path: &str) -> UnitHttpResponse {
        UnitHttpResponse::new(200)
            .with_header("content-type", "application/json")
            .with_body(
                serde_json::to_string(&ContractsResponse {
                    links: ContractsLinksResponse {
                        self_link: path.to_string(),
                        next: format!("{CONTRACTS_PATH_PREFIX}/{}", self.contracts.borrow().len()),
                    },
                    data: self.contracts.borrow()[start..].to_vec(),
                })
                .unwrap(),
            )
    }

    pub(crate) fn add_contract(
        &self,
        id: String,
        name: String,
        secret: Option<String>,
        sla_tier_id: Option<String>,
    ) {
        self.contracts
            .borrow_mut()
            .push(ContractsDataResponse::new(secret, id, name, sla_tier_id));
    }

    pub(crate) fn remove_contract(&self, id: String) {
        self.contracts
            .borrow_mut()
            .push(ContractsDataResponse::delete(id));
    }
}

pub fn hash(salt: &str, client_secret: &str) -> String {
    let mut hashed_result = [0u8; 32];

    let mut hasher = Sha256::new();
    hasher.update(format!("{salt}{client_secret}").as_str());

    hashed_result.copy_from_slice(&hasher.finalize());
    hex::encode(hashed_result)
}

#[derive(Serialize, Debug)]
struct ContractsLinksResponse {
    #[serde(rename = "self")]
    self_link: String,
    next: String,
}

#[allow(unused)]
#[derive(Serialize, Debug, Clone)]
struct ContractsDataResponse {
    #[serde(rename = "organizationId")]
    organization_id: Option<String>,
    #[serde(rename = "contractId")]
    contract_id: String,
    #[serde(rename = "apiId")]
    api_id: String,
    #[serde(rename = "versionId")]
    version_id: String,
    #[serde(rename = "slaTierId")]
    sla_tier_id: Option<String>,
    #[serde(rename = "clientId")]
    client_id: String,
    #[serde(rename = "clientSecret")]
    client_secret: Option<String>,
    #[serde(rename = "clientSecretSalt")]
    client_secret_salt: Option<String>,
    #[serde(rename = "contractUpdatedDate")]
    contract_updated_date: Option<String>,
    #[serde(rename = "redirectUris")]
    redirect_uris: Option<Vec<String>>,
    #[serde(rename = "clientName")]
    client_name: Option<String>,
    #[serde(rename = "clientDescription")]
    client_description: Option<String>,
    #[serde(rename = "clientUpdatedDate")]
    client_updated_date: Option<String>,
    #[serde(rename = "updatedDate")]
    updated_date: Option<String>,
    removed: Option<bool>,
}

impl ContractsDataResponse {
    fn new(
        client_secret: Option<String>,
        client_id: String,
        client_name: String,
        sla_tier_id: Option<String>,
    ) -> Self {
        Self {
            organization_id: Some("org".to_string()),
            contract_id: "contract_id".to_string(),
            api_id: "api_id".to_string(),
            version_id: "version_id".to_string(),
            sla_tier_id,
            client_id,
            client_secret: client_secret.map(|secret| hash("client_secret_salt", &secret)),
            client_secret_salt: Some("client_secret_salt".to_string()),
            contract_updated_date: Some("contract_updated_date".to_string()),
            redirect_uris: None,
            client_name: Some(client_name),
            client_description: Some("client_description".to_string()),
            client_updated_date: None,
            updated_date: Some("updated_date".to_string()),
            removed: Some(false),
        }
    }

    fn delete(client_id: String) -> Self {
        Self {
            organization_id: Some("org".to_string()),
            contract_id: "contract_id".to_string(),
            api_id: "api_id".to_string(),
            version_id: "version_id".to_string(),
            sla_tier_id: None,
            client_id,
            client_secret: None,
            client_secret_salt: None,
            contract_updated_date: Some("contract_updated_date".to_string()),
            redirect_uris: None,
            client_name: None,
            client_description: None,
            client_updated_date: None,
            updated_date: Some("updated_date".to_string()),
            removed: Some(true),
        }
    }
}

#[derive(Serialize, Debug)]
pub struct ContractsResponse {
    links: ContractsLinksResponse,
    data: Vec<ContractsDataResponse>,
}