///|
/// Post-request storage assertion attached to a conformance vector.
pub(all) enum StoreExpectation {
IgnoreStore
ResourceMissing(String)
ResourceState(String, Int64, UploadLength, UploadLifecycle, Bytes)
} derive(Debug, Eq)
///|
pub(all) struct ConformanceExpectation {
status : Int
error_code : String?
response_offset : Int64?
store : StoreExpectation
} derive(Debug, Eq)
///|
pub(all) struct ConformanceVector {
name : String
initial_records : Array[UploadRecord]
request : TusRequest
expected : ConformanceExpectation
} derive(Debug, Eq)
///|
pub(all) struct ConformanceResult {
name : String
passed : Bool
response : TusResponse
findings : Array[String]
} derive(Debug, Eq)
///|
pub(all) struct ConformanceReport {
results : Array[ConformanceResult]
passed : Int
failed : Int
} derive(Debug, Eq)
///|
pub fn run_conformance_vector(
vector : ConformanceVector,
config? : TusConfig = config(limits=TusLimits::strict_test()),
) -> Result[ConformanceResult, TusError] {
let store = MemoryStore::new()
for record in vector.initial_records {
match store.import_record(record, config.limits) {
Err(error) => return Err(error)
Ok(_) => ()
}
}
let engine = match TusEngine::new(config~, store~) {
Err(error) => return Err(error)
Ok(engine) => engine
}
let response = engine.handle(vector.request)
let findings : Array[String] = []
if response.status != vector.expected.status {
findings.push(
"status expected " +
vector.expected.status.to_string() +
" but received " +
response.status.to_string(),
)
}
let actual_error = response_error_code(response)
if actual_error != vector.expected.error_code {
findings.push(
"error code expected " +
optional_text(vector.expected.error_code) +
" but received " +
optional_text(actual_error),
)
}
let actual_offset = response_offset(response)
if actual_offset != vector.expected.response_offset {
findings.push(
"response offset expected " +
optional_offset(vector.expected.response_offset) +
" but received " +
optional_offset(actual_offset),
)
}
check_store_expectation(engine, vector.expected.store, findings)
Ok({ name: vector.name, passed: findings.length() == 0, response, findings, })
}
///|
pub fn run_conformance_suite(
vectors : Array[ConformanceVector],
config? : TusConfig = config(limits=TusLimits::strict_test()),
) -> Result[ConformanceReport, TusError] {
let results : Array[ConformanceResult] = []
let mut passed = 0
let mut failed = 0
for vector in vectors {
let result = match run_conformance_vector(vector, config~) {
Err(error) => return Err(error)
Ok(result) => result
}
if result.passed {
passed = passed + 1
} else {
failed = failed + 1
}
results.push(result)
}
Ok({ results, passed, failed, })
}
///|
pub fn core_conformance_vectors() -> Array[ConformanceVector] {
let version = version_response_headers()
let patch_base = version
.add("Content-Type", "application/offset+octet-stream")
.add("Content-Length", "3")
[
{
name: "options-discovery",
initial_records: [],
request: options_request(),
expected: {
status: 204,
error_code: None,
response_offset: None,
store: IgnoreStore,
},
},
{
name: "create-known-length",
initial_records: [],
request: request(
"POST",
"/files",
headers=version.add("Upload-Length", "3"),
),
expected: {
status: 201,
error_code: None,
response_offset: Some(0L),
store: ResourceState("u000000000001", 0L, Known(3L), Active, b""),
},
},
{
name: "create-missing-length",
initial_records: [],
request: request("POST", "/files", headers=version),
expected: {
status: 400,
error_code: Some("TUS_REQUIRED_HEADER_MISSING"),
response_offset: None,
store: ResourceMissing("u000000000001"),
},
},
{
name: "head-known-resource",
initial_records: [upload_record("seed", Known(3L))],
request: request("HEAD", "/files/seed", headers=version),
expected: {
status: 200,
error_code: None,
response_offset: Some(0L),
store: ResourceState("seed", 0L, Known(3L), Active, b""),
},
},
{
name: "head-missing-resource",
initial_records: [],
request: request("HEAD", "/files/missing", headers=version),
expected: {
status: 404,
error_code: Some("TUS_UPLOAD_NOT_FOUND"),
response_offset: None,
store: ResourceMissing("missing"),
},
},
{
name: "patch-completes-resource",
initial_records: [upload_record("seed", Known(3L))],
request: request(
"PATCH",
"/files/seed",
headers=patch_base.add("Upload-Offset", "0"),
body=b"abc",
),
expected: {
status: 204,
error_code: None,
response_offset: Some(3L),
store: ResourceState("seed", 3L, Known(3L), Complete, b"abc"),
},
},
{
name: "patch-rejects-stale-offset",
initial_records: [seeded_record("seed", 3L, Known(6L), b"abc")],
request: request(
"PATCH",
"/files/seed",
headers=patch_base.add("Upload-Offset", "0"),
body=b"bad",
),
expected: {
status: 409,
error_code: Some("TUS_OFFSET_MISMATCH"),
response_offset: None,
store: ResourceState("seed", 3L, Known(6L), Active, b"abc"),
},
},
{
name: "patch-rejects-wrong-content-type",
initial_records: [upload_record("seed", Known(3L))],
request: request(
"PATCH",
"/files/seed",
headers=version
.add("Content-Type", "text/plain")
.add("Content-Length", "3")
.add("Upload-Offset", "0"),
body=b"abc",
),
expected: {
status: 415,
error_code: Some("TUS_PATCH_CONTENT_TYPE_INVALID"),
response_offset: None,
store: ResourceState("seed", 0L, Known(3L), Active, b""),
},
},
{
name: "patch-declares-deferred-length",
initial_records: [upload_record("seed", Deferred)],
request: request(
"PATCH",
"/files/seed",
headers=patch_base.add("Upload-Offset", "0").add("Upload-Length", "3"),
body=b"abc",
),
expected: {
status: 204,
error_code: None,
response_offset: Some(3L),
store: ResourceState("seed", 3L, Known(3L), Complete, b"abc"),
},
},
{
name: "unsupported-version-before-mutation",
initial_records: [upload_record("seed", Known(3L))],
request: request(
"PATCH",
"/files/seed",
headers=Headers::empty()
.add("Tus-Resumable", "0.2.2")
.add("Content-Type", "application/offset+octet-stream")
.add("Content-Length", "3")
.add("Upload-Offset", "0"),
body=b"abc",
),
expected: {
status: 412,
error_code: Some("TUS_VERSION_UNSUPPORTED"),
response_offset: None,
store: ResourceState("seed", 0L, Known(3L), Active, b""),
},
},
]
}
///|
fn check_store_expectation(
engine : TusEngine,
expectation : StoreExpectation,
findings : Array[String],
) -> Unit {
match expectation {
IgnoreStore => ()
ResourceMissing(identifier) =>
if engine.snapshot(identifier) is Ok(_) {
findings.push("resource " + identifier + " should not exist")
}
ResourceState(identifier, offset, length, state, data) =>
match engine.snapshot(identifier) {
Err(_) => findings.push("resource " + identifier + " is missing")
Ok(record) => {
if record.offset != offset {
findings.push("stored offset differs for " + identifier)
}
if record.length != length {
findings.push("stored length differs for " + identifier)
}
if record.lifecycle != state {
findings.push("stored lifecycle differs for " + identifier)
}
if record.data != data {
findings.push("stored bytes differ for " + identifier)
}
}
}
}
}
///|
fn seeded_record(
identifier : String,
offset : Int64,
length : UploadLength,
data : Bytes,
) -> UploadRecord {
{
..upload_record(identifier, length),
offset,
revision: if offset == 0L {
0L
} else {
1L
},
lifecycle: lifecycle(offset, length),
data,
}
}
///|
fn optional_text(value : String?) -> String {
match value {
None => ""
Some(text) => text
}
}
///|
fn optional_offset(value : Int64?) -> String {
match value {
None => ""
Some(offset) => offset.to_string()
}
}