feat(proto): unify time fields on Timestamp/Duration #348
No files matched your search
Generated
+1
@@ -2596,6 +2596,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"prost",
|
||||
"prost-types",
|
||||
"protoc-bin-vendored",
|
||||
"thiserror 1.0.69",
|
||||
"tokio",
|
||||
|
||||
@@ -81,6 +81,7 @@ zip = { version = "8.6", default-features = false, features = ["deflate"] }
|
||||
tonic = "0.14"
|
||||
tonic-prost = "0.14"
|
||||
prost = "0.14"
|
||||
prost-types = "0.14"
|
||||
tonic-prost-build = "0.14"
|
||||
protoc-bin-vendored = "3.2"
|
||||
subtle = "2.6"
|
||||
|
||||
+113
-37
@@ -53,6 +53,48 @@ fn parse_account_protocol(protocol: &str) -> Result<nuncio_proto::v1::AccountPro
|
||||
}
|
||||
}
|
||||
|
||||
/// Renders a wire-format `google.protobuf.Timestamp` as an RFC 3339 UTC
|
||||
/// string for human-readable and `--json` output. `nanos` outside
|
||||
/// `0..1_000_000_000` (never produced by the daemon's own mappers) and an
|
||||
/// absent field both render as `"unknown"` rather than fabricating a time.
|
||||
fn format_timestamp(ts: &Option<nuncio_proto::time::Timestamp>) -> String {
|
||||
ts.as_ref()
|
||||
.and_then(|ts| {
|
||||
u32::try_from(ts.nanos)
|
||||
.ok()
|
||||
.map(|nanos| (ts.seconds, nanos))
|
||||
})
|
||||
.and_then(|(seconds, nanos)| chrono::DateTime::from_timestamp(seconds, nanos))
|
||||
.map(|dt| dt.to_rfc3339())
|
||||
.unwrap_or_else(|| "unknown".to_string())
|
||||
}
|
||||
|
||||
/// Extracts the exact unix-nanosecond instant a wire-format `Timestamp`
|
||||
/// carries, for callers that need the audit ledger's full sub-second
|
||||
/// precision rather than the RFC 3339 rendering.
|
||||
fn timestamp_unix_nanos(ts: &Option<nuncio_proto::time::Timestamp>) -> i64 {
|
||||
ts.as_ref()
|
||||
.map(nuncio_proto::time::timestamp_to_unix_nanos)
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
/// Renders a wire-format `google.protobuf.Duration` as whole seconds for
|
||||
/// human-readable and `--json` output (e.g. `AccountConfig.sync_interval`).
|
||||
fn duration_secs(d: &Option<nuncio_proto::time::Duration>) -> u64 {
|
||||
d.as_ref()
|
||||
.map(nuncio_proto::time::duration_to_secs)
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
/// Renders a wire-format `google.protobuf.Duration` as whole microseconds
|
||||
/// for human-readable and `--json` output (e.g.
|
||||
/// `PreviewRuleResponse.execution_time`).
|
||||
fn duration_micros(d: &Option<nuncio_proto::time::Duration>) -> u64 {
|
||||
d.as_ref()
|
||||
.map(nuncio_proto::time::duration_to_micros)
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
/// Renders a `nuncio.v1.Message` (as returned by the daemon's `Mail` gRPC
|
||||
/// service) into the JSON shape used by `mail list`/`mail read`'s
|
||||
/// `--json` output.
|
||||
@@ -64,7 +106,7 @@ fn message_proto_to_json(message: &nuncio_proto::v1::Message) -> serde_json::Val
|
||||
"subject": message.subject,
|
||||
"sender": message.sender,
|
||||
"recipient": message.recipient,
|
||||
"received_at": message.received_at,
|
||||
"received_at": format_timestamp(&message.received_at),
|
||||
"read": message.read,
|
||||
"body_plain": message.body_plain,
|
||||
"body_html": message.body_html,
|
||||
@@ -80,8 +122,8 @@ fn calendar_event_proto_to_json(event: &nuncio_proto::v1::CalendarEvent) -> serd
|
||||
"account_id": event.account_id,
|
||||
"calendar_id": event.calendar_id,
|
||||
"summary": event.summary,
|
||||
"start_time": event.start_time,
|
||||
"end_time": event.end_time,
|
||||
"start_time": format_timestamp(&event.start_time),
|
||||
"end_time": format_timestamp(&event.end_time),
|
||||
"rrule": event.rrule,
|
||||
"location": event.location,
|
||||
})
|
||||
@@ -150,7 +192,8 @@ fn map_export_format_to_proto(format: nuncio_core::ExportFormat) -> nuncio_proto
|
||||
fn audit_record_proto_to_json(record: &nuncio_proto::v1::AuditRecord) -> serde_json::Value {
|
||||
json!({
|
||||
"sequence": record.sequence,
|
||||
"timestamp_ns": record.timestamp_ns,
|
||||
"timestamp": format_timestamp(&record.timestamp),
|
||||
"timestamp_ns": timestamp_unix_nanos(&record.timestamp),
|
||||
"actor": record.actor,
|
||||
"action": record.action,
|
||||
"data_hash": record.data_hash,
|
||||
@@ -171,8 +214,8 @@ fn filter_rule_proto_to_json(rule: &nuncio_proto::v1::FilterRule) -> serde_json:
|
||||
"enabled": rule.enabled,
|
||||
"nsql_text": rule.nsql_text,
|
||||
"actions": rule.actions,
|
||||
"created_at": rule.created_at,
|
||||
"updated_at": rule.updated_at,
|
||||
"created_at": format_timestamp(&rule.created_at),
|
||||
"updated_at": format_timestamp(&rule.updated_at),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -184,7 +227,7 @@ fn filter_execution_log_proto_to_json(
|
||||
"rule_id": log.rule_id,
|
||||
"message_id": log.message_id,
|
||||
"action_taken": log.action_taken,
|
||||
"matched_at": log.matched_at,
|
||||
"matched_at": format_timestamp(&log.matched_at),
|
||||
"prev_hash": log.prev_hash,
|
||||
"hash": log.hash,
|
||||
})
|
||||
@@ -882,7 +925,10 @@ impl HeadlessRunner {
|
||||
} else {
|
||||
format!(
|
||||
"Message {}: Subject: '{}', From: {}, Date: {}",
|
||||
msg.id, msg.subject, msg.sender, msg.received_at
|
||||
msg.id,
|
||||
msg.subject,
|
||||
msg.sender,
|
||||
format_timestamp(&msg.received_at)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1066,13 +1112,15 @@ impl HeadlessRunner {
|
||||
} else if records.is_empty() {
|
||||
"No audit records recorded.".to_string()
|
||||
} else {
|
||||
let mut out = String::from(
|
||||
"SEQ ACTOR ACTION TIMESTAMP_NS\n",
|
||||
);
|
||||
let mut out =
|
||||
String::from("SEQ ACTOR ACTION TIMESTAMP\n");
|
||||
for r in records {
|
||||
out.push_str(&format!(
|
||||
"{:<4} {:<20} {:<20} {}\n",
|
||||
r.sequence, r.actor, r.action, r.timestamp_ns
|
||||
r.sequence,
|
||||
r.actor,
|
||||
r.action,
|
||||
format_timestamp(&r.timestamp)
|
||||
));
|
||||
}
|
||||
out
|
||||
@@ -1348,13 +1396,15 @@ impl HeadlessRunner {
|
||||
"matched_rule_id": preview.matched_rule_id,
|
||||
"matched_rule_name": preview.matched_rule_name,
|
||||
"actions_evaluated": preview.actions_evaluated,
|
||||
"execution_time_us": preview.execution_time_us,
|
||||
"execution_time_us": duration_micros(&preview.execution_time),
|
||||
"condition_traces": preview.condition_traces,
|
||||
}))
|
||||
} else {
|
||||
format!(
|
||||
"Dry-run evaluation result: matched={}, actions={:?}, elapsed={}us",
|
||||
preview.matched, preview.actions_evaluated, preview.execution_time_us
|
||||
preview.matched,
|
||||
preview.actions_evaluated,
|
||||
duration_micros(&preview.execution_time)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1545,7 +1595,11 @@ impl HeadlessRunner {
|
||||
for l in logs {
|
||||
out.push_str(&format!(
|
||||
"{:<4} {:<10} {:<10} {:<12} {}\n",
|
||||
l.id, l.rule_id, l.message_id, l.action_taken, l.matched_at
|
||||
l.id,
|
||||
l.rule_id,
|
||||
l.message_id,
|
||||
l.action_taken,
|
||||
format_timestamp(&l.matched_at)
|
||||
));
|
||||
}
|
||||
out
|
||||
@@ -1733,7 +1787,7 @@ impl HeadlessRunner {
|
||||
imap_tls_mode: map_tls_mode_to_proto(imap_tls_mode).into(),
|
||||
smtp_tls_mode: map_tls_mode_to_proto(smtp_tls_mode).into(),
|
||||
keyring_secret_key: keyring_key.clone(),
|
||||
sync_interval_secs: 300,
|
||||
sync_interval: Some(nuncio_proto::time::duration_from_secs(300)),
|
||||
smtp_host: if is_caldav {
|
||||
String::new()
|
||||
} else {
|
||||
@@ -1837,7 +1891,7 @@ impl HeadlessRunner {
|
||||
"imap_tls_mode": a.imap_tls_mode().as_str_name(),
|
||||
"smtp_tls_mode": a.smtp_tls_mode().as_str_name(),
|
||||
"keyring_secret_key": a.keyring_secret_key,
|
||||
"sync_interval_secs": a.sync_interval_secs,
|
||||
"sync_interval_secs": duration_secs(&a.sync_interval),
|
||||
"collection_url": a.collection_url,
|
||||
})
|
||||
})
|
||||
@@ -1932,7 +1986,7 @@ impl HeadlessRunner {
|
||||
"imap_tls_mode": account.imap_tls_mode().as_str_name(),
|
||||
"smtp_tls_mode": account.smtp_tls_mode().as_str_name(),
|
||||
"keyring_secret_key": account.keyring_secret_key,
|
||||
"sync_interval_secs": account.sync_interval_secs,
|
||||
"sync_interval_secs": duration_secs(&account.sync_interval),
|
||||
}))
|
||||
} else {
|
||||
format!(
|
||||
@@ -2190,8 +2244,8 @@ impl HeadlessRunner {
|
||||
.list_events(nuncio_proto::v1::ListEventsRequest {
|
||||
account_id: account.to_string(),
|
||||
calendar_id: calendar.to_string(),
|
||||
start_window: start,
|
||||
end_window: end,
|
||||
start_window: Some(nuncio_proto::time::timestamp_from_unix_secs(start)),
|
||||
end_window: Some(nuncio_proto::time::timestamp_from_unix_secs(end)),
|
||||
})
|
||||
.await
|
||||
{
|
||||
@@ -2241,8 +2295,8 @@ impl HeadlessRunner {
|
||||
.sync(nuncio_proto::v1::CalendarSyncRequest {
|
||||
account_id: account.to_string(),
|
||||
calendar_id: calendar.to_string(),
|
||||
start_window: start,
|
||||
end_window: end,
|
||||
start_window: Some(nuncio_proto::time::timestamp_from_unix_secs(start)),
|
||||
end_window: Some(nuncio_proto::time::timestamp_from_unix_secs(end)),
|
||||
})
|
||||
.await
|
||||
{
|
||||
@@ -2933,7 +2987,7 @@ mod tests {
|
||||
imap_tls_mode: TlsModeProto::ImplicitTls.into(),
|
||||
smtp_tls_mode: TlsModeProto::ImplicitTls.into(),
|
||||
keyring_secret_key: "nuncio/acct-stub-1".to_string(),
|
||||
sync_interval_secs: 300,
|
||||
sync_interval: Some(nuncio_proto::time::duration_from_secs(300)),
|
||||
smtp_host: "smtp.nuncio.mx".to_string(),
|
||||
smtp_port: 465,
|
||||
collection_url: String::new(),
|
||||
@@ -3145,7 +3199,7 @@ mod tests {
|
||||
subject: "Stub Subject".to_string(),
|
||||
sender: "alice@nuncio.mx".to_string(),
|
||||
recipient: "bob@nuncio.mx".to_string(),
|
||||
received_at: 1_700_000_000,
|
||||
received_at: Some(nuncio_proto::time::timestamp_from_unix_secs(1_700_000_000)),
|
||||
read: false,
|
||||
body_plain: Some("Stub body text".to_string()),
|
||||
body_html: None,
|
||||
@@ -3598,8 +3652,8 @@ mod tests {
|
||||
account_id: "acct-stub-1".to_string(),
|
||||
calendar_id: "cal-stub".to_string(),
|
||||
summary: "Stub Meeting".to_string(),
|
||||
start_time: 1_700_000_000,
|
||||
end_time: 1_700_003_600,
|
||||
start_time: Some(nuncio_proto::time::timestamp_from_unix_secs(1_700_000_000)),
|
||||
end_time: Some(nuncio_proto::time::timestamp_from_unix_secs(1_700_003_600)),
|
||||
rrule: None,
|
||||
location: Some("Room 1".to_string()),
|
||||
}
|
||||
@@ -3707,8 +3761,14 @@ mod tests {
|
||||
.expect("stub daemon received a list_events request");
|
||||
assert_eq!(recorded_list.account_id, "acct-stub-1");
|
||||
assert_eq!(recorded_list.calendar_id, "cal-stub");
|
||||
assert_eq!(recorded_list.start_window, 0);
|
||||
assert_eq!(recorded_list.end_window, i64::MAX);
|
||||
assert_eq!(
|
||||
recorded_list.start_window,
|
||||
Some(nuncio_proto::time::timestamp_from_unix_secs(0))
|
||||
);
|
||||
assert_eq!(
|
||||
recorded_list.end_window,
|
||||
Some(nuncio_proto::time::timestamp_from_unix_secs(i64::MAX))
|
||||
);
|
||||
|
||||
// `cal sync`: proves the CLI is a real gRPC client of
|
||||
// `Calendar/Sync` and reports the daemon's real `synced_count`,
|
||||
@@ -4024,8 +4084,12 @@ mod tests {
|
||||
enabled: true,
|
||||
nsql_text: req.nsql,
|
||||
actions: vec!["MARK READ".to_string()],
|
||||
created_at: 1_700_000_000,
|
||||
updated_at: 1_700_000_000,
|
||||
created_at: Some(nuncio_proto::time::timestamp_from_unix_secs(
|
||||
1_700_000_000,
|
||||
)),
|
||||
updated_at: Some(nuncio_proto::time::timestamp_from_unix_secs(
|
||||
1_700_000_000,
|
||||
)),
|
||||
}),
|
||||
}))
|
||||
}
|
||||
@@ -4043,8 +4107,12 @@ mod tests {
|
||||
enabled: true,
|
||||
nsql_text: "WHERE subject CONTAINS 'Urgent' ACTION MARK READ".to_string(),
|
||||
actions: vec!["MARK READ".to_string()],
|
||||
created_at: 1_700_000_000,
|
||||
updated_at: 1_700_000_000,
|
||||
created_at: Some(nuncio_proto::time::timestamp_from_unix_secs(
|
||||
1_700_000_000,
|
||||
)),
|
||||
updated_at: Some(nuncio_proto::time::timestamp_from_unix_secs(
|
||||
1_700_000_000,
|
||||
)),
|
||||
}],
|
||||
}))
|
||||
}
|
||||
@@ -4086,7 +4154,7 @@ mod tests {
|
||||
matched_rule_id: Some("rule-stub-1".to_string()),
|
||||
matched_rule_name: Some("Stub Rule".to_string()),
|
||||
actions_evaluated: vec!["MARK READ".to_string()],
|
||||
execution_time_us: 42,
|
||||
execution_time: Some(nuncio_proto::time::duration_from_micros(42)),
|
||||
condition_traces: vec!["Rule 'Stub Rule': MATCH".to_string()],
|
||||
}))
|
||||
}
|
||||
@@ -4111,8 +4179,12 @@ mod tests {
|
||||
.nsql
|
||||
.unwrap_or_else(|| "WHERE subject CONTAINS 'Urgent'".to_string()),
|
||||
actions: vec!["MARK READ".to_string()],
|
||||
created_at: 1_700_000_000,
|
||||
updated_at: 1_700_000_001,
|
||||
created_at: Some(nuncio_proto::time::timestamp_from_unix_secs(
|
||||
1_700_000_000,
|
||||
)),
|
||||
updated_at: Some(nuncio_proto::time::timestamp_from_unix_secs(
|
||||
1_700_000_001,
|
||||
)),
|
||||
}),
|
||||
}))
|
||||
}
|
||||
@@ -4153,7 +4225,9 @@ mod tests {
|
||||
rule_id: "rule-stub-1".to_string(),
|
||||
message_id: "msg-stub-1".to_string(),
|
||||
action_taken: "MARK READ".to_string(),
|
||||
matched_at: 1_700_000_002,
|
||||
matched_at: Some(nuncio_proto::time::timestamp_from_unix_secs(
|
||||
1_700_000_002,
|
||||
)),
|
||||
prev_hash: "prev-hash".to_string(),
|
||||
hash: "hash".to_string(),
|
||||
}],
|
||||
@@ -4640,7 +4714,9 @@ mod tests {
|
||||
Ok(tonic::Response::new(ListRecordsResponse {
|
||||
records: vec![AuditRecord {
|
||||
sequence: 1,
|
||||
timestamp_ns: 1_700_000_000_000_000_000,
|
||||
timestamp: Some(nuncio_proto::time::timestamp_from_unix_nanos(
|
||||
1_700_000_000_000_000_000,
|
||||
)),
|
||||
actor: "system.test".to_string(),
|
||||
action: "data.export".to_string(),
|
||||
data_hash: "deadbeef".to_string(),
|
||||
|
||||
@@ -13,6 +13,7 @@ workspace = true
|
||||
tonic.workspace = true
|
||||
tonic-prost.workspace = true
|
||||
prost.workspace = true
|
||||
prost-types.workspace = true
|
||||
bytes.workspace = true
|
||||
thiserror.workspace = true
|
||||
|
||||
|
||||
Binary file not shown.
@@ -2,6 +2,9 @@ syntax = "proto3";
|
||||
|
||||
package nuncio.v1;
|
||||
|
||||
import "google/protobuf/duration.proto";
|
||||
import "google/protobuf/timestamp.proto";
|
||||
|
||||
// ErrorReason is the stable, machine-readable taxonomy of failure conditions
|
||||
// the daemon reports to clients. It is carried inside an `ErrorInfo` in a
|
||||
// failed RPC's gRPC status-details trailer so a client can branch on a
|
||||
@@ -272,8 +275,8 @@ message AccountConfig {
|
||||
TlsMode smtp_tls_mode = 9;
|
||||
// Keyring entry key used to retrieve OS vault credentials.
|
||||
string keyring_secret_key = 10;
|
||||
// Background sync polling interval in seconds (minimum 10s).
|
||||
uint64 sync_interval_secs = 11;
|
||||
// Background sync polling interval (minimum 10s).
|
||||
google.protobuf.Duration sync_interval = 11;
|
||||
// SMTP server hostname or IP address, used for outbound mail. Distinct
|
||||
// from `server_host` because a mail provider's inbound (IMAP/JMAP) and
|
||||
// outbound (SMTP) endpoints commonly differ.
|
||||
@@ -435,8 +438,8 @@ message Message {
|
||||
string sender = 5;
|
||||
// Recipient address.
|
||||
string recipient = 6;
|
||||
// Unix timestamp of message arrival.
|
||||
int64 received_at = 7;
|
||||
// Timestamp of message arrival.
|
||||
google.protobuf.Timestamp received_at = 7;
|
||||
// Read/unread flag status.
|
||||
bool read = 8;
|
||||
// Plaintext message body (already decrypted by the store).
|
||||
@@ -663,10 +666,10 @@ message FilterRule {
|
||||
// Rendered NSQL action clauses (e.g. "MOVE TO 'Archive'"), derived from
|
||||
// the parsed AST.
|
||||
repeated string actions = 7;
|
||||
// Creation timestamp (unix seconds).
|
||||
int64 created_at = 8;
|
||||
// Last update timestamp (unix seconds).
|
||||
int64 updated_at = 9;
|
||||
// Creation timestamp.
|
||||
google.protobuf.Timestamp created_at = 8;
|
||||
// Last update timestamp.
|
||||
google.protobuf.Timestamp updated_at = 9;
|
||||
}
|
||||
|
||||
// CreateRuleRequest carries a new rule's display name, full NSQL source
|
||||
@@ -750,8 +753,8 @@ message PreviewRuleResponse {
|
||||
optional string matched_rule_name = 4;
|
||||
// NSQL-rendered actions that would be executed on a real match.
|
||||
repeated string actions_evaluated = 5;
|
||||
// Evaluation duration in microseconds.
|
||||
uint64 execution_time_us = 6;
|
||||
// Evaluation duration.
|
||||
google.protobuf.Duration execution_time = 6;
|
||||
// Human-readable step-by-step match traces.
|
||||
repeated string condition_traces = 7;
|
||||
}
|
||||
@@ -825,8 +828,8 @@ message FilterExecutionLog {
|
||||
string message_id = 3;
|
||||
// Action description string.
|
||||
string action_taken = 4;
|
||||
// Timestamp of execution (unix seconds).
|
||||
int64 matched_at = 5;
|
||||
// Timestamp of execution.
|
||||
google.protobuf.Timestamp matched_at = 5;
|
||||
// Hash of the preceding log record in the hash-chain ledger.
|
||||
string prev_hash = 6;
|
||||
// HMAC-SHA256 hash of this record.
|
||||
@@ -973,10 +976,10 @@ message CalendarEvent {
|
||||
string calendar_id = 3;
|
||||
// Event title or summary.
|
||||
string summary = 4;
|
||||
// Unix timestamp for event start.
|
||||
int64 start_time = 5;
|
||||
// Unix timestamp for event end.
|
||||
int64 end_time = 6;
|
||||
// Timestamp for event start.
|
||||
google.protobuf.Timestamp start_time = 5;
|
||||
// Timestamp for event end.
|
||||
google.protobuf.Timestamp end_time = 6;
|
||||
// Recurrence rule string (RFC 5545 RRULE format).
|
||||
optional string rrule = 7;
|
||||
// Location text.
|
||||
@@ -989,8 +992,8 @@ message CalendarEvent {
|
||||
message CalendarSyncRequest {
|
||||
string account_id = 1;
|
||||
string calendar_id = 2;
|
||||
int64 start_window = 3;
|
||||
int64 end_window = 4;
|
||||
google.protobuf.Timestamp start_window = 3;
|
||||
google.protobuf.Timestamp end_window = 4;
|
||||
}
|
||||
|
||||
// CalendarSyncResponse reports how many events were synchronized.
|
||||
@@ -1002,8 +1005,8 @@ message CalendarSyncResponse {
|
||||
message ListEventsRequest {
|
||||
string account_id = 1;
|
||||
string calendar_id = 2;
|
||||
int64 start_window = 3;
|
||||
int64 end_window = 4;
|
||||
google.protobuf.Timestamp start_window = 3;
|
||||
google.protobuf.Timestamp end_window = 4;
|
||||
}
|
||||
|
||||
// ListEventsResponse.
|
||||
@@ -1197,8 +1200,9 @@ service Audit {
|
||||
message AuditRecord {
|
||||
// Monotonically increasing sequence index.
|
||||
uint64 sequence = 1;
|
||||
// UTC timestamp in nanoseconds since Unix epoch.
|
||||
int64 timestamp_ns = 2;
|
||||
// UTC timestamp, preserving the audit ledger's nanosecond precision in
|
||||
// the `nanos` field.
|
||||
google.protobuf.Timestamp timestamp = 2;
|
||||
// Subsystem or actor that performed the audited action.
|
||||
string actor = 3;
|
||||
// Action identifier (e.g. "data.export").
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
pub mod addr;
|
||||
pub mod client;
|
||||
pub mod errors;
|
||||
pub mod time;
|
||||
|
||||
pub use addr::{grpc_addr_from_env, DEFAULT_GRPC_ADDR, GRPC_ADDR_ENV_VAR};
|
||||
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
//! Conversions between the store's native integer instants/durations and the
|
||||
//! well-known `google.protobuf.Timestamp`/`google.protobuf.Duration` wire
|
||||
//! types used at the `nuncio.v1` proto boundary.
|
||||
//!
|
||||
//! The daemon's core/store models keep unix-seconds `i64`/`u64` instants and
|
||||
//! plain scalar durations; these helpers are the single place that maps them
|
||||
//! to/from the well-known types so every RPC mapper does it the same way.
|
||||
//! Re-exporting `Timestamp`/`Duration` here lets callers name the wire types
|
||||
//! without depending on `prost-types` directly.
|
||||
|
||||
pub use prost_types::{Duration, Timestamp};
|
||||
|
||||
/// Converts a unix-seconds instant into a `Timestamp` with a zero `nanos`
|
||||
/// component. Use this for store fields that only ever carry second
|
||||
/// granularity (e.g. message/event/rule timestamps).
|
||||
#[must_use]
|
||||
pub fn timestamp_from_unix_secs(unix_secs: i64) -> Timestamp {
|
||||
Timestamp {
|
||||
seconds: unix_secs,
|
||||
nanos: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Extracts the unix-seconds instant from a `Timestamp`, truncating any
|
||||
/// sub-second `nanos` component. Use this for fields mapped via
|
||||
/// [`timestamp_from_unix_secs`], where `nanos` is always zero.
|
||||
#[must_use]
|
||||
pub fn timestamp_to_unix_secs(ts: &Timestamp) -> i64 {
|
||||
ts.seconds
|
||||
}
|
||||
|
||||
/// Converts a unix-nanoseconds instant into a `Timestamp`, splitting it
|
||||
/// losslessly into whole seconds plus a sub-second `nanos` remainder. Use
|
||||
/// this for fields that carry sub-second precision (e.g. the WORM audit
|
||||
/// ledger's `timestamp_ns`).
|
||||
#[must_use]
|
||||
pub fn timestamp_from_unix_nanos(unix_nanos: i64) -> Timestamp {
|
||||
let seconds = unix_nanos.div_euclid(1_000_000_000);
|
||||
let nanos = unix_nanos.rem_euclid(1_000_000_000);
|
||||
Timestamp {
|
||||
seconds,
|
||||
nanos: nanos as i32,
|
||||
}
|
||||
}
|
||||
|
||||
/// Recovers the exact unix-nanoseconds instant a `Timestamp` was built from
|
||||
/// via [`timestamp_from_unix_nanos`].
|
||||
#[must_use]
|
||||
pub fn timestamp_to_unix_nanos(ts: &Timestamp) -> i64 {
|
||||
ts.seconds
|
||||
.saturating_mul(1_000_000_000)
|
||||
.saturating_add(i64::from(ts.nanos))
|
||||
}
|
||||
|
||||
/// Converts a whole-second duration into a `Duration` with a zero `nanos`
|
||||
/// component (e.g. `AccountConfig`'s sync polling interval).
|
||||
#[must_use]
|
||||
pub fn duration_from_secs(secs: u64) -> Duration {
|
||||
Duration {
|
||||
seconds: secs as i64,
|
||||
nanos: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Extracts a whole-second duration from a `Duration`, truncating any
|
||||
/// sub-second `nanos` component. A negative `seconds` (never produced by
|
||||
/// this crate's own mappers) reads back as zero rather than panicking.
|
||||
#[must_use]
|
||||
pub fn duration_to_secs(d: &Duration) -> u64 {
|
||||
u64::try_from(d.seconds).unwrap_or(0)
|
||||
}
|
||||
|
||||
/// Converts a microsecond duration into a `Duration`, splitting it
|
||||
/// losslessly into whole seconds plus a sub-second `nanos` remainder (e.g.
|
||||
/// filter rule preview evaluation timing).
|
||||
#[must_use]
|
||||
pub fn duration_from_micros(micros: u64) -> Duration {
|
||||
let seconds = micros / 1_000_000;
|
||||
let nanos = (micros % 1_000_000) * 1_000;
|
||||
Duration {
|
||||
seconds: seconds as i64,
|
||||
nanos: nanos as i32,
|
||||
}
|
||||
}
|
||||
|
||||
/// Recovers the exact microsecond duration a `Duration` was built from via
|
||||
/// [`duration_from_micros`].
|
||||
#[must_use]
|
||||
pub fn duration_to_micros(d: &Duration) -> u64 {
|
||||
let seconds = u64::try_from(d.seconds).unwrap_or(0);
|
||||
let nanos = u64::try_from(d.nanos).unwrap_or(0);
|
||||
seconds
|
||||
.saturating_mul(1_000_000)
|
||||
.saturating_add(nanos / 1_000)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn seconds_round_trip_through_timestamp() {
|
||||
let ts = timestamp_from_unix_secs(1_700_000_000);
|
||||
assert_eq!(ts.nanos, 0);
|
||||
assert_eq!(timestamp_to_unix_secs(&ts), 1_700_000_000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nanos_round_trip_through_timestamp() {
|
||||
let original_ns = 1_700_000_000_123_456_789_i64;
|
||||
let ts = timestamp_from_unix_nanos(original_ns);
|
||||
assert_eq!(timestamp_to_unix_nanos(&ts), original_ns);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn negative_epoch_nanos_round_trip() {
|
||||
// Regression: pre-epoch instants must still split into a
|
||||
// non-negative `nanos` remainder per the Timestamp wire contract.
|
||||
let original_ns = -1_500_000_000_i64;
|
||||
let ts = timestamp_from_unix_nanos(original_ns);
|
||||
assert!(ts.nanos >= 0);
|
||||
assert_eq!(timestamp_to_unix_nanos(&ts), original_ns);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn secs_round_trip_through_duration() {
|
||||
let d = duration_from_secs(300);
|
||||
assert_eq!(d.nanos, 0);
|
||||
assert_eq!(duration_to_secs(&d), 300);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn micros_round_trip_through_duration() {
|
||||
let original_us = 1_234_567_u64;
|
||||
let d = duration_from_micros(original_us);
|
||||
assert_eq!(duration_to_micros(&d), original_us);
|
||||
}
|
||||
}
|
||||
+140
-36
@@ -285,7 +285,9 @@ fn map_account_config_to_proto(config: nuncio_core::AccountConfig) -> AccountCon
|
||||
imap_tls_mode: map_tls_mode_to_proto(config.imap_tls_mode).into(),
|
||||
smtp_tls_mode: map_tls_mode_to_proto(config.smtp_tls_mode).into(),
|
||||
keyring_secret_key: config.keyring_secret_key,
|
||||
sync_interval_secs: config.sync_interval_secs,
|
||||
sync_interval: Some(nuncio_proto::time::duration_from_secs(
|
||||
config.sync_interval_secs,
|
||||
)),
|
||||
smtp_host: config.smtp_host,
|
||||
smtp_port: u32::from(config.smtp_port),
|
||||
collection_url: config.collection_url,
|
||||
@@ -305,6 +307,9 @@ fn map_account_config_from_proto(
|
||||
.map_err(|_| Status::invalid_argument("server_port must be in range 1..=65535"))?;
|
||||
let smtp_port = u16::try_from(config.smtp_port)
|
||||
.map_err(|_| Status::invalid_argument("smtp_port must be in range 1..=65535"))?;
|
||||
let sync_interval = config
|
||||
.sync_interval
|
||||
.ok_or_else(|| Status::invalid_argument("sync_interval is required"))?;
|
||||
|
||||
Ok(nuncio_core::AccountConfig {
|
||||
id: config.id,
|
||||
@@ -319,7 +324,7 @@ fn map_account_config_from_proto(
|
||||
imap_tls_mode,
|
||||
smtp_tls_mode,
|
||||
keyring_secret_key: config.keyring_secret_key,
|
||||
sync_interval_secs: config.sync_interval_secs,
|
||||
sync_interval_secs: nuncio_proto::time::duration_to_secs(&sync_interval),
|
||||
collection_url: config.collection_url,
|
||||
})
|
||||
}
|
||||
@@ -777,7 +782,9 @@ fn map_email_to_proto(email: nuncio_core::model::Email) -> MessageProto {
|
||||
subject: email.subject,
|
||||
sender: email.sender,
|
||||
recipient: email.recipient,
|
||||
received_at: email.received_at,
|
||||
received_at: Some(nuncio_proto::time::timestamp_from_unix_secs(
|
||||
email.received_at,
|
||||
)),
|
||||
read: email.read,
|
||||
body_plain: email.body_plain,
|
||||
body_html: email.body_html,
|
||||
@@ -812,8 +819,10 @@ fn map_calendar_event_to_proto(event: nuncio_core::model::CalendarEvent) -> Cale
|
||||
account_id: event.account_id,
|
||||
calendar_id: event.calendar_id,
|
||||
summary: event.summary,
|
||||
start_time: event.start_time,
|
||||
end_time: event.end_time,
|
||||
start_time: Some(nuncio_proto::time::timestamp_from_unix_secs(
|
||||
event.start_time,
|
||||
)),
|
||||
end_time: Some(nuncio_proto::time::timestamp_from_unix_secs(event.end_time)),
|
||||
rrule: event.rrule,
|
||||
location: event.location,
|
||||
}
|
||||
@@ -858,6 +867,17 @@ fn map_contact_to_proto(contact: nuncio_contacts::Contact) -> ContactProto {
|
||||
}
|
||||
}
|
||||
|
||||
/// Extracts a required window bound (`start_window`/`end_window`) from its
|
||||
/// wire-format `Timestamp`, rejecting an absent field with
|
||||
/// `Status::invalid_argument` rather than silently defaulting to the epoch.
|
||||
fn require_window_bound(
|
||||
ts: Option<nuncio_proto::time::Timestamp>,
|
||||
field: &str,
|
||||
) -> Result<i64, Status> {
|
||||
let ts = ts.ok_or_else(|| Status::invalid_argument(format!("{field} is required")))?;
|
||||
Ok(nuncio_proto::time::timestamp_to_unix_secs(&ts))
|
||||
}
|
||||
|
||||
/// Combines an account's in-window events with its recurring masters into the final event
|
||||
/// list for a `ListEvents` query window, expanding each recurring master into its real
|
||||
/// occurrences via [`nuncio_cal::RecurrenceEngine::expand_occurrences`] rather than trusting
|
||||
@@ -956,6 +976,8 @@ impl CalendarGrpcService {
|
||||
async fn sync_configured_caldav_accounts(
|
||||
&self,
|
||||
req: &CalendarSyncRequest,
|
||||
start_window: i64,
|
||||
end_window: i64,
|
||||
) -> Result<usize, Status> {
|
||||
let caldav_accounts: Vec<nuncio_core::AccountConfig> = self
|
||||
.db
|
||||
@@ -1004,8 +1026,8 @@ impl CalendarGrpcService {
|
||||
&account,
|
||||
&password,
|
||||
&req.calendar_id,
|
||||
req.start_window,
|
||||
req.end_window,
|
||||
start_window,
|
||||
end_window,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
@@ -1027,18 +1049,23 @@ impl Calendar for CalendarGrpcService {
|
||||
request: Request<CalendarSyncRequest>,
|
||||
) -> Result<Response<CalendarSyncResponse>, Status> {
|
||||
let req = request.into_inner();
|
||||
let start_window = require_window_bound(req.start_window, "start_window")?;
|
||||
let end_window = require_window_bound(req.end_window, "end_window")?;
|
||||
|
||||
let synced = match &self.overrides.calendar_backend {
|
||||
Some(backend) => crate::calendar_sync::sync_with_backend(
|
||||
&self.db,
|
||||
backend.as_ref(),
|
||||
&req.calendar_id,
|
||||
req.start_window,
|
||||
req.end_window,
|
||||
start_window,
|
||||
end_window,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| Status::internal(format!("calendar sync failed: {e}")))?,
|
||||
None => self.sync_configured_caldav_accounts(&req).await?,
|
||||
None => {
|
||||
self.sync_configured_caldav_accounts(&req, start_window, end_window)
|
||||
.await?
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Response::new(CalendarSyncResponse {
|
||||
@@ -1054,10 +1081,12 @@ impl Calendar for CalendarGrpcService {
|
||||
if req.account_id.is_empty() {
|
||||
return Err(Status::invalid_argument("account_id is required"));
|
||||
}
|
||||
let start_window = require_window_bound(req.start_window, "start_window")?;
|
||||
let end_window = require_window_bound(req.end_window, "end_window")?;
|
||||
|
||||
let all_in_window = self
|
||||
.db
|
||||
.list_calendar_events(&req.account_id, req.start_window, req.end_window)
|
||||
.list_calendar_events(&req.account_id, start_window, end_window)
|
||||
.await
|
||||
.map_err(|e| Status::internal(format!("failed to list calendar events: {e}")))?;
|
||||
|
||||
@@ -1068,7 +1097,7 @@ impl Calendar for CalendarGrpcService {
|
||||
.map_err(|e| Status::internal(format!("failed to list recurring events: {e}")))?;
|
||||
|
||||
let mut events =
|
||||
expand_events_for_window(all_in_window, recurring, req.start_window, req.end_window);
|
||||
expand_events_for_window(all_in_window, recurring, start_window, end_window);
|
||||
|
||||
events.retain(|e| req.calendar_id.is_empty() || e.calendar_id == req.calendar_id);
|
||||
|
||||
@@ -1574,8 +1603,12 @@ fn map_filter_rule_to_proto(rule: nuncio_filter::FilterRule) -> FilterRuleProto
|
||||
enabled: rule.enabled,
|
||||
nsql_text: rule.nsql_text,
|
||||
actions: rule.actions.iter().map(|a| a.to_nsql()).collect(),
|
||||
created_at: rule.created_at,
|
||||
updated_at: rule.updated_at,
|
||||
created_at: Some(nuncio_proto::time::timestamp_from_unix_secs(
|
||||
rule.created_at,
|
||||
)),
|
||||
updated_at: Some(nuncio_proto::time::timestamp_from_unix_secs(
|
||||
rule.updated_at,
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1589,7 +1622,7 @@ fn map_filter_execution_log_to_proto(
|
||||
rule_id: log.rule_id,
|
||||
message_id: log.message_id,
|
||||
action_taken: log.action_taken,
|
||||
matched_at: log.matched_at,
|
||||
matched_at: Some(nuncio_proto::time::timestamp_from_unix_secs(log.matched_at)),
|
||||
prev_hash: log.prev_hash,
|
||||
hash: log.hash,
|
||||
}
|
||||
@@ -1608,7 +1641,9 @@ fn map_preview_result_to_proto(preview: nuncio_filter::FilterPreviewResult) -> P
|
||||
.iter()
|
||||
.map(|a| a.to_nsql())
|
||||
.collect(),
|
||||
execution_time_us: preview.execution_time_us,
|
||||
execution_time: Some(nuncio_proto::time::duration_from_micros(
|
||||
preview.execution_time_us,
|
||||
)),
|
||||
condition_traces: preview.condition_traces,
|
||||
}
|
||||
}
|
||||
@@ -2152,7 +2187,9 @@ const DEFAULT_LIST_RECORDS_LIMIT: u32 = 100;
|
||||
fn map_audit_record_to_proto(record: nuncio_core::WormAuditRecord) -> AuditRecordProto {
|
||||
AuditRecordProto {
|
||||
sequence: record.sequence,
|
||||
timestamp_ns: record.timestamp_ns,
|
||||
timestamp: Some(nuncio_proto::time::timestamp_from_unix_nanos(
|
||||
record.timestamp_ns,
|
||||
)),
|
||||
actor: record.actor,
|
||||
action: record.action,
|
||||
data_hash: record.data_hash,
|
||||
@@ -2575,6 +2612,73 @@ mod tests {
|
||||
use nuncio_proto::v1::system_client::SystemClient;
|
||||
use tonic::Code;
|
||||
|
||||
/// Proves a stored second-granularity instant survives the proto
|
||||
/// boundary exactly: `map_email_to_proto`'s `received_at` decodes back
|
||||
/// to the identical unix-seconds value via `timestamp_to_unix_secs`.
|
||||
#[test]
|
||||
fn map_email_to_proto_round_trips_received_at_exactly() {
|
||||
let original_secs = 1_700_000_123_i64;
|
||||
let email = nuncio_core::model::Email {
|
||||
id: "msg-1".to_string(),
|
||||
account_id: "acct-1".to_string(),
|
||||
folder_id: "inbox".to_string(),
|
||||
subject: "s".to_string(),
|
||||
sender: "a@b.com".to_string(),
|
||||
recipient: "c@d.com".to_string(),
|
||||
received_at: original_secs,
|
||||
read: false,
|
||||
body_plain: None,
|
||||
body_html: None,
|
||||
attachments: Vec::new(),
|
||||
remote_id: "remote-1".to_string(),
|
||||
uid_validity: "1".to_string(),
|
||||
};
|
||||
|
||||
let proto = map_email_to_proto(email);
|
||||
let ts = proto.received_at.expect("received_at is always populated");
|
||||
assert_eq!(
|
||||
nuncio_proto::time::timestamp_to_unix_secs(&ts),
|
||||
original_secs
|
||||
);
|
||||
}
|
||||
|
||||
/// Proves the WORM audit ledger's sub-second precision survives the
|
||||
/// proto boundary losslessly: `map_audit_record_to_proto`'s `timestamp`
|
||||
/// decodes back to the exact original `timestamp_ns`, not just the
|
||||
/// truncated seconds component.
|
||||
#[test]
|
||||
fn map_audit_record_to_proto_preserves_nanosecond_precision() {
|
||||
let original_ns = 1_700_000_123_987_654_321_i64;
|
||||
let record = nuncio_core::WormAuditRecord {
|
||||
sequence: 1,
|
||||
timestamp_ns: original_ns,
|
||||
actor: "system".to_string(),
|
||||
action: "test.action".to_string(),
|
||||
data_hash: "hash".to_string(),
|
||||
previous_block_hash: "GENESIS".to_string(),
|
||||
record_hmac: "mac".to_string(),
|
||||
};
|
||||
|
||||
let proto = map_audit_record_to_proto(record);
|
||||
let ts = proto.timestamp.expect("timestamp is always populated");
|
||||
assert_eq!(
|
||||
nuncio_proto::time::timestamp_to_unix_nanos(&ts),
|
||||
original_ns
|
||||
);
|
||||
}
|
||||
|
||||
/// Proves an account's sync interval survives the proto `Duration`
|
||||
/// boundary in both directions: to-proto then from-proto reconstructs
|
||||
/// the exact original whole-second value.
|
||||
#[test]
|
||||
fn account_sync_interval_round_trips_through_duration() {
|
||||
let mut config = sample_account_config_proto("acct-rt", "nuncio/acct-rt");
|
||||
config.sync_interval = Some(nuncio_proto::time::duration_from_secs(600));
|
||||
|
||||
let core_config = map_account_config_from_proto(config).expect("valid config maps back");
|
||||
assert_eq!(core_config.sync_interval_secs, 600);
|
||||
}
|
||||
|
||||
/// Spawns a test server backed by a fresh ephemeral database, a fresh
|
||||
/// empty [`FilterEngine`], and a fresh [`SecretManager::mock`] vault --
|
||||
/// the right default for every test that only exercises
|
||||
@@ -3137,7 +3241,7 @@ mod tests {
|
||||
imap_tls_mode: TlsModeProto::ImplicitTls.into(),
|
||||
smtp_tls_mode: TlsModeProto::ImplicitTls.into(),
|
||||
keyring_secret_key: keyring_secret_key.to_string(),
|
||||
sync_interval_secs: 60,
|
||||
sync_interval: Some(nuncio_proto::time::duration_from_secs(60)),
|
||||
collection_url: String::new(),
|
||||
smtp_host: "smtp.nuncio.mx".to_string(),
|
||||
smtp_port: 465,
|
||||
@@ -3708,8 +3812,8 @@ mod tests {
|
||||
.list_events(ListEventsRequest {
|
||||
account_id: "acct-1".to_string(),
|
||||
calendar_id: "cal-1".to_string(),
|
||||
start_window: 0,
|
||||
end_window: i64::MAX,
|
||||
start_window: Some(nuncio_proto::time::timestamp_from_unix_secs(0)),
|
||||
end_window: Some(nuncio_proto::time::timestamp_from_unix_secs(i64::MAX)),
|
||||
})
|
||||
.await
|
||||
.expect_err("missing bearer token must be rejected");
|
||||
@@ -3727,8 +3831,8 @@ mod tests {
|
||||
.sync(CalendarSyncRequest {
|
||||
account_id: "acct-1".to_string(),
|
||||
calendar_id: "cal-1".to_string(),
|
||||
start_window: 0,
|
||||
end_window: i64::MAX,
|
||||
start_window: Some(nuncio_proto::time::timestamp_from_unix_secs(0)),
|
||||
end_window: Some(nuncio_proto::time::timestamp_from_unix_secs(i64::MAX)),
|
||||
})
|
||||
.await
|
||||
.expect_err("missing bearer token must be rejected");
|
||||
@@ -5539,8 +5643,8 @@ mod tests {
|
||||
.list_events(authed_bearer_request(ListEventsRequest {
|
||||
account_id: "acct-cal-1".to_string(),
|
||||
calendar_id: "cal-work".to_string(),
|
||||
start_window: 1_699_999_000,
|
||||
end_window: 1_700_004_000,
|
||||
start_window: Some(nuncio_proto::time::timestamp_from_unix_secs(1_699_999_000)),
|
||||
end_window: Some(nuncio_proto::time::timestamp_from_unix_secs(1_700_004_000)),
|
||||
}))
|
||||
.await
|
||||
.expect("list_events succeeds")
|
||||
@@ -5598,8 +5702,8 @@ mod tests {
|
||||
.list_events(authed_bearer_request(ListEventsRequest {
|
||||
account_id: "acct-cal-1".to_string(),
|
||||
calendar_id: "cal-work".to_string(),
|
||||
start_window: 1_699_999_000,
|
||||
end_window: 1_700_004_000,
|
||||
start_window: Some(nuncio_proto::time::timestamp_from_unix_secs(1_699_999_000)),
|
||||
end_window: Some(nuncio_proto::time::timestamp_from_unix_secs(1_700_004_000)),
|
||||
}))
|
||||
.await
|
||||
.expect("list_events succeeds")
|
||||
@@ -5624,8 +5728,8 @@ mod tests {
|
||||
.list_events(authed_bearer_request(ListEventsRequest {
|
||||
account_id: String::new(),
|
||||
calendar_id: "cal-work".to_string(),
|
||||
start_window: 0,
|
||||
end_window: i64::MAX,
|
||||
start_window: Some(nuncio_proto::time::timestamp_from_unix_secs(0)),
|
||||
end_window: Some(nuncio_proto::time::timestamp_from_unix_secs(i64::MAX)),
|
||||
}))
|
||||
.await
|
||||
.expect_err("empty account_id must be rejected");
|
||||
@@ -5717,8 +5821,8 @@ mod tests {
|
||||
.sync(authed_bearer_request(CalendarSyncRequest {
|
||||
account_id: "acct-cal-no-config".to_string(),
|
||||
calendar_id: "cal-work".to_string(),
|
||||
start_window: 0,
|
||||
end_window: i64::MAX,
|
||||
start_window: Some(nuncio_proto::time::timestamp_from_unix_secs(0)),
|
||||
end_window: Some(nuncio_proto::time::timestamp_from_unix_secs(i64::MAX)),
|
||||
}))
|
||||
.await
|
||||
.expect_err("sync with no injected backend and no CalDAV config must fail");
|
||||
@@ -5775,8 +5879,8 @@ mod tests {
|
||||
.sync(authed_bearer_request(CalendarSyncRequest {
|
||||
account_id: "acct-cal-injected".to_string(),
|
||||
calendar_id: "cal-work".to_string(),
|
||||
start_window: 1_699_999_000,
|
||||
end_window: 1_700_004_000,
|
||||
start_window: Some(nuncio_proto::time::timestamp_from_unix_secs(1_699_999_000)),
|
||||
end_window: Some(nuncio_proto::time::timestamp_from_unix_secs(1_700_004_000)),
|
||||
}))
|
||||
.await
|
||||
.expect("sync succeeds")
|
||||
@@ -5788,8 +5892,8 @@ mod tests {
|
||||
.list_events(authed_bearer_request(ListEventsRequest {
|
||||
account_id: "acct-cal-injected".to_string(),
|
||||
calendar_id: "cal-work".to_string(),
|
||||
start_window: 1_699_999_000,
|
||||
end_window: 1_700_004_000,
|
||||
start_window: Some(nuncio_proto::time::timestamp_from_unix_secs(1_699_999_000)),
|
||||
end_window: Some(nuncio_proto::time::timestamp_from_unix_secs(1_700_004_000)),
|
||||
}))
|
||||
.await
|
||||
.expect("list_events succeeds")
|
||||
@@ -6296,8 +6400,8 @@ mod tests {
|
||||
.list_events(ListEventsRequest {
|
||||
account_id: "acct-1".to_string(),
|
||||
calendar_id: "cal-1".to_string(),
|
||||
start_window: 0,
|
||||
end_window: i64::MAX,
|
||||
start_window: Some(nuncio_proto::time::timestamp_from_unix_secs(0)),
|
||||
end_window: Some(nuncio_proto::time::timestamp_from_unix_secs(i64::MAX)),
|
||||
})
|
||||
.await
|
||||
.expect_err("Calendar must reject an unauthenticated call");
|
||||
|
||||
@@ -86,7 +86,7 @@ fn sample_account_config() -> AccountConfig {
|
||||
imap_tls_mode: TlsMode::StartTls.into(),
|
||||
smtp_tls_mode: TlsMode::Plain.into(),
|
||||
keyring_secret_key: KEYRING_KEY.to_string(),
|
||||
sync_interval_secs: 60,
|
||||
sync_interval: Some(nuncio_proto::time::duration_from_secs(60)),
|
||||
smtp_host: "127.0.0.1".to_string(),
|
||||
smtp_port: 25,
|
||||
collection_url: String::new(),
|
||||
|
||||
@@ -165,7 +165,7 @@ async fn caldav_production_sync_builds_real_client_and_persists_events() {
|
||||
imap_tls_mode: TlsModeProto::ImplicitTls.into(),
|
||||
smtp_tls_mode: TlsModeProto::ImplicitTls.into(),
|
||||
keyring_secret_key: KEYRING_KEY.to_string(),
|
||||
sync_interval_secs: 300,
|
||||
sync_interval: Some(nuncio_proto::time::duration_from_secs(300)),
|
||||
smtp_host: String::new(),
|
||||
smtp_port: 0,
|
||||
collection_url: collection_url.clone(),
|
||||
@@ -183,8 +183,8 @@ async fn caldav_production_sync_builds_real_client_and_persists_events() {
|
||||
.sync(CalendarSyncRequest {
|
||||
account_id: ACCOUNT_ID.to_string(),
|
||||
calendar_id: CALENDAR_ID.to_string(),
|
||||
start_window: 1_700_000_000,
|
||||
end_window: 1_800_000_000,
|
||||
start_window: Some(nuncio_proto::time::timestamp_from_unix_secs(1_700_000_000)),
|
||||
end_window: Some(nuncio_proto::time::timestamp_from_unix_secs(1_800_000_000)),
|
||||
})
|
||||
.await
|
||||
.expect("production CalDAV sync succeeds")
|
||||
@@ -196,8 +196,8 @@ async fn caldav_production_sync_builds_real_client_and_persists_events() {
|
||||
.list_events(ListEventsRequest {
|
||||
account_id: ACCOUNT_ID.to_string(),
|
||||
calendar_id: CALENDAR_ID.to_string(),
|
||||
start_window: 1_700_000_000,
|
||||
end_window: 1_800_000_000,
|
||||
start_window: Some(nuncio_proto::time::timestamp_from_unix_secs(1_700_000_000)),
|
||||
end_window: Some(nuncio_proto::time::timestamp_from_unix_secs(1_800_000_000)),
|
||||
})
|
||||
.await
|
||||
.expect("list_events succeeds")
|
||||
@@ -216,8 +216,8 @@ async fn caldav_production_sync_builds_real_client_and_persists_events() {
|
||||
.sync(CalendarSyncRequest {
|
||||
account_id: "acct-does-not-exist".to_string(),
|
||||
calendar_id: CALENDAR_ID.to_string(),
|
||||
start_window: 1_700_000_000,
|
||||
end_window: 1_800_000_000,
|
||||
start_window: Some(nuncio_proto::time::timestamp_from_unix_secs(1_700_000_000)),
|
||||
end_window: Some(nuncio_proto::time::timestamp_from_unix_secs(1_800_000_000)),
|
||||
})
|
||||
.await
|
||||
.expect_err("a sync targeting an unconfigured account must fail honestly");
|
||||
@@ -232,8 +232,8 @@ async fn caldav_production_sync_builds_real_client_and_persists_events() {
|
||||
.list_events(ListEventsRequest {
|
||||
account_id: ACCOUNT_ID.to_string(),
|
||||
calendar_id: CALENDAR_ID.to_string(),
|
||||
start_window: 0,
|
||||
end_window: i64::MAX,
|
||||
start_window: Some(nuncio_proto::time::timestamp_from_unix_secs(0)),
|
||||
end_window: Some(nuncio_proto::time::timestamp_from_unix_secs(i64::MAX)),
|
||||
})
|
||||
.await
|
||||
.expect_err("missing bearer token must be rejected");
|
||||
|
||||
@@ -138,8 +138,8 @@ async fn calendar_sync_then_list_and_get_round_trip_over_authenticated_grpc() {
|
||||
.sync(CalendarSyncRequest {
|
||||
account_id: ACCOUNT_ID.to_string(),
|
||||
calendar_id: CALENDAR_ID.to_string(),
|
||||
start_window: 1_699_999_000,
|
||||
end_window: 1_700_020_000,
|
||||
start_window: Some(nuncio_proto::time::timestamp_from_unix_secs(1_699_999_000)),
|
||||
end_window: Some(nuncio_proto::time::timestamp_from_unix_secs(1_700_020_000)),
|
||||
})
|
||||
.await
|
||||
.expect("sync succeeds")
|
||||
@@ -152,8 +152,8 @@ async fn calendar_sync_then_list_and_get_round_trip_over_authenticated_grpc() {
|
||||
.list_events(ListEventsRequest {
|
||||
account_id: ACCOUNT_ID.to_string(),
|
||||
calendar_id: CALENDAR_ID.to_string(),
|
||||
start_window: 1_699_999_000,
|
||||
end_window: 1_700_020_000,
|
||||
start_window: Some(nuncio_proto::time::timestamp_from_unix_secs(1_699_999_000)),
|
||||
end_window: Some(nuncio_proto::time::timestamp_from_unix_secs(1_700_020_000)),
|
||||
})
|
||||
.await
|
||||
.expect("list_events succeeds")
|
||||
@@ -187,8 +187,8 @@ async fn calendar_sync_then_list_and_get_round_trip_over_authenticated_grpc() {
|
||||
.list_events(ListEventsRequest {
|
||||
account_id: ACCOUNT_ID.to_string(),
|
||||
calendar_id: CALENDAR_ID.to_string(),
|
||||
start_window: 0,
|
||||
end_window: i64::MAX,
|
||||
start_window: Some(nuncio_proto::time::timestamp_from_unix_secs(0)),
|
||||
end_window: Some(nuncio_proto::time::timestamp_from_unix_secs(i64::MAX)),
|
||||
})
|
||||
.await
|
||||
.expect_err("missing bearer token must be rejected");
|
||||
|
||||
@@ -65,7 +65,7 @@ fn sample_account_config() -> AccountConfig {
|
||||
imap_tls_mode: TlsMode::ImplicitTls.into(),
|
||||
smtp_tls_mode: TlsMode::ImplicitTls.into(),
|
||||
keyring_secret_key: KEYRING_KEY.to_string(),
|
||||
sync_interval_secs: 60,
|
||||
sync_interval: Some(nuncio_proto::time::duration_from_secs(60)),
|
||||
smtp_host: "smtp.nuncio.mx".to_string(),
|
||||
smtp_port: 465,
|
||||
collection_url: String::new(),
|
||||
|
||||
@@ -140,8 +140,8 @@ async fn typed_error_reasons_decode_from_status_details_over_the_wire() {
|
||||
.sync(CalendarSyncRequest {
|
||||
account_id: String::new(),
|
||||
calendar_id: "personal".to_string(),
|
||||
start_window: 0,
|
||||
end_window: 0,
|
||||
start_window: Some(nuncio_proto::time::timestamp_from_unix_secs(0)),
|
||||
end_window: Some(nuncio_proto::time::timestamp_from_unix_secs(0)),
|
||||
})
|
||||
.await
|
||||
.expect_err("syncing with no CalDAV account configured must fail");
|
||||
|
||||
Reference in new issue
Block a user