use http::StatusCode;
use serde::Deserialize;
use serde_json::json;
use wiremock::ResponseTemplate;
use wiremock::{Request, Respond};
use crate::{GitlabRunnerMock, MockJobState};
#[derive(Default, Deserialize)]
struct FeaturesInfo {
#[serde(default)]
refspecs: bool,
}
#[derive(Default, Deserialize)]
struct VersionInfo {
#[serde(default)]
features: FeaturesInfo,
version: Option<String>,
revision: Option<String>,
platform: Option<String>,
architecture: Option<String>,
}
#[derive(Deserialize)]
pub(crate) struct JobRequest {
token: String,
system_id: String,
#[serde(default)]
info: VersionInfo,
}
pub(crate) struct JobRequestResponder {
mock: GitlabRunnerMock,
}
impl JobRequestResponder {
pub fn new(mock: GitlabRunnerMock) -> Self {
Self { mock }
}
}
impl Respond for JobRequestResponder {
fn respond(&self, request: &Request) -> ResponseTemplate {
let r: JobRequest = serde_json::from_slice(&request.body).unwrap();
if r.token != self.mock.runner_token() {
return ResponseTemplate::new(StatusCode::FORBIDDEN);
}
assert!(r.system_id.starts_with("s_") || r.system_id.starts_with("r_"));
assert!(r.system_id.len() == 14);
let expected = self.mock.expected_metadata();
if let Some(expected) = expected.version {
let version = r.info.version.expect("Missing version in metadata");
assert_eq!(expected, version, "Unexpected version in metadata");
}
if let Some(expected) = expected.revision {
let revision = r.info.revision.expect("Missing revision in metadata");
assert_eq!(expected, revision, "Unexpected revision in metadata");
}
if let Some(expected) = expected.platform {
let platform = r.info.platform.expect("Missing platform in metadata");
assert_eq!(expected, platform, "Unexpected platform in metadata");
}
if let Some(expected) = expected.architecture {
let architecture = r
.info
.architecture
.expect("Missing architecture in metadata");
assert_eq!(
expected, architecture,
"Unexpected architecture in metadata"
);
}
if !r.info.features.refspecs {
return ResponseTemplate::new(StatusCode::NO_CONTENT);
}
if let Some(job) = self.mock.grab_pending_job() {
job.update_state(MockJobState::Running);
let dependencies: Vec<_> = job
.dependencies()
.iter()
.map(|j| {
let artifact_file = j
.uploaded_artifacts()
.find(|a| a.artifact_type.as_deref() == Some("archive"))
.map(|artifact| {
Some(json!({
"filename": artifact.filename,
"size": artifact.data.len(),
}))
});
json!({
"id": j.id() ,
"name": j.name() ,
"token": j.token(),
"artifacts_file": artifact_file
})
})
.collect();
let artifacts: Vec<_> = job
.artifacts()
.iter()
.map(|a| {
let mut value = json!({
"name": a.name,
"paths": a.paths,
"when": a.when,
"exprire_in": a.expire_in,
"artifact_type": a.artifact_type,
"artifact_format": a.artifact_format,
});
if a.untracked {
value
.as_object_mut()
.unwrap()
.insert("untracked".to_owned(), json!(a.untracked));
}
value
})
.collect();
ResponseTemplate::new(StatusCode::CREATED).set_body_json(json!({
"id": job.id(),
"token": job.token(),
"allow_git_fetch": true,
"job_info": {
"id": job.id(),
"name": job.name(),
"stage": "build",
"project_id": 4,
"project_name": "gitlab-test"
},
"git_info": {
"repo_url": "https://bla/dummy.git",
"ref": "custom",
"sha": "265c14cf140a66cfc61e40e4ab45c95ba8df5ed1",
"before_sha": "fc40ad32cdd36b814f07a540605110edc209a38c",
"ref_type": "branch",
"refspecs": [
"+265c14cf140a66cfc61e40e4ab45c95ba8df5ed1:refs/pipelines/120",
"+refs/heads/custom:refs/remotes/origin/custom"
],
"depth": 50
},
"runner_info": {
"timeout": 3600,
"runner_session_url": null
},
"variables": job.variables(),
"steps": job.steps(),
"image": null,
"services": [],
"artifacts": artifacts,
"cache": [],
"credentials": [
{
"type": "registry",
"url": "registry.example.com",
"username": "gitlab-ci-token",
"password": "registry-dummy-tokn"
},
{
"type": "registry",
"url": "registry.example.com.nl:443",
"username": "gitlab-ci-token",
"password": "registry-dummy-tokn"
}
],
"dependencies": dependencies,
"features": {
"trace_sections": true,
"failure_reasons": [
"unknown_failure",
"script_failure",
"api_failure",
"stuck_or_timeout_failure",
"runner_system_failure",
"missing_dependency_failure",
"runner_unsupported",
"stale_schedule",
"job_execution_timeout",
"archived_failure",
"unmet_prerequisites",
"scheduler_failure",
"data_integrity_failure",
"forward_deployment_failure",
"user_blocked",
"project_deleted",
"insufficient_bridge_permissions",
"downstream_bridge_project_not_found",
"invalid_bridge_trigger",
"bridge_pipeline_is_child_pipeline",
"downstream_pipeline_creation_failed",
"secrets_provider_not_found",
"reached_max_descendant_pipelines_depth"
]
}
}))
} else {
ResponseTemplate::new(StatusCode::NO_CONTENT)
}
}
}