@synopackageland/templates 0.9.5 → 0.9.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/README.md +27 -4
  2. package/package.json +2 -2
  3. package/templates/_shared/AGENTS.md +95 -18
  4. package/templates/native-go/backend/THIRD_PARTY_NOTICES.txt +1 -1
  5. package/templates/native-go/backend/cmd/app/main.go +8 -1
  6. package/templates/native-go/backend/cmd/background-worker/main.go +5 -1
  7. package/templates/native-go/backend/go.mod +1 -1
  8. package/templates/native-go/backend/go.sum +2 -2
  9. package/templates/native-go/backend/internal/app/background.go +19 -8
  10. package/templates/native-go/backend/internal/app/background_owner_test.go +2 -2
  11. package/templates/native-go/backend/internal/app/background_test.go +24 -0
  12. package/templates/native-go/backend/internal/app/background_work.go +94 -0
  13. package/templates/native-go/backend/internal/app/background_work_test.go +95 -0
  14. package/templates/native-go/backend/internal/app/uninstall.go +19 -0
  15. package/templates/native-go/backend/internal/app/uninstall_test.go +33 -0
  16. package/templates/native-go/dsm-package.yaml +18 -0
  17. package/templates/native-go-delegate/backend/cmd/background-worker/main.go +1 -1
  18. package/templates/native-go-jobs/backend/internal/app/background_routes_test.go +4 -0
  19. package/templates/native-go-jobs/frontend/test/background-page.test.ts +21 -0
  20. package/templates/native-go-vue/frontend/package-lock.json +4 -4
  21. package/templates/native-go-vue/frontend/package.json +1 -1
  22. package/templates/native-rust/README.md +381 -0
  23. package/templates/native-rust/backend/Cargo.lock +125 -0
  24. package/templates/native-rust/backend/Cargo.toml +24 -0
  25. package/templates/native-rust/backend/THIRD_PARTY_NOTICES.txt +207 -0
  26. package/templates/native-rust/backend/permissions.json +5 -0
  27. package/templates/native-rust/backend/rust-toolchain.toml +4 -0
  28. package/templates/native-rust/backend/src/app/background/mod.rs +424 -0
  29. package/templates/native-rust/backend/src/app/background/owner.rs +77 -0
  30. package/templates/native-rust/backend/src/app/background/owner_tests.rs +174 -0
  31. package/templates/native-rust/backend/src/app/background/schedule.rs +126 -0
  32. package/templates/native-rust/backend/src/app/background/schedule_tests.rs +108 -0
  33. package/templates/native-rust/backend/src/app/background/tests.rs +505 -0
  34. package/templates/native-rust/backend/src/app/background/work.rs +78 -0
  35. package/templates/native-rust/backend/src/app/background/work_tests.rs +123 -0
  36. package/templates/native-rust/backend/src/app/background_routes.rs +22 -0
  37. package/templates/native-rust/backend/src/app/delegate_config.rs +188 -0
  38. package/templates/native-rust/backend/src/app/delegate_routes.rs +22 -0
  39. package/templates/native-rust/backend/src/app/identity.rs +56 -0
  40. package/templates/native-rust/backend/src/app/migrations.rs +79 -0
  41. package/templates/native-rust/backend/src/app/mod.rs +30 -0
  42. package/templates/native-rust/backend/src/app/portal.rs +317 -0
  43. package/templates/native-rust/backend/src/app/root_routes.rs +23 -0
  44. package/templates/native-rust/backend/src/app/routes.rs +315 -0
  45. package/templates/native-rust/backend/src/app/session_files.rs +19 -0
  46. package/templates/native-rust/backend/src/app/shared_folder.rs +383 -0
  47. package/templates/native-rust/backend/src/app/test_support.rs +200 -0
  48. package/templates/native-rust/backend/src/app/uninstall.rs +38 -0
  49. package/templates/native-rust/backend/src/bin/api.rs +72 -0
  50. package/templates/native-rust/backend/src/bin/background-worker.rs +37 -0
  51. package/templates/native-rust/backend/src/build_env.rs +155 -0
  52. package/templates/native-rust/backend/src/lib.rs +6 -0
  53. package/templates/native-rust/backend/src/permissions.rs +54 -0
  54. package/templates/native-rust/backend/src/process.rs +91 -0
  55. package/templates/native-rust/dsm-package.yaml +89 -0
  56. package/templates/native-rust/gitignore +2 -0
  57. package/templates/native-rust/i18n/glossary.json +663 -0
  58. package/templates/native-rust/i18n/messages/de.json +29 -0
  59. package/templates/native-rust/i18n/messages/en.json +29 -0
  60. package/templates/native-rust/i18n/messages/es.json +29 -0
  61. package/templates/native-rust/i18n/messages/fr.json +29 -0
  62. package/templates/native-rust/i18n/messages/it.json +29 -0
  63. package/templates/native-rust/i18n/messages/ja.json +29 -0
  64. package/templates/native-rust/i18n/messages/ko.json +29 -0
  65. package/templates/native-rust/i18n/messages/zh-CN.json +29 -0
  66. package/templates/native-rust/i18n/messages/zh-TW.json +29 -0
  67. package/templates/native-rust/ui/app.css +45 -0
  68. package/templates/native-rust/ui/app.js +111 -0
  69. package/templates/native-rust-delegate/backend/src/app/delegate_routes.rs +92 -0
  70. package/templates/native-rust-delegate/backend/src/bin/background-worker.rs +52 -0
  71. package/templates/native-rust-jobs/backend/src/app/background_routes.rs +109 -0
  72. package/templates/native-rust-root-ops/backend/src/app/root_routes.rs +475 -0
  73. package/templates/native-rust-vue/README.md +55 -0
  74. package/templates/native-rust-vue/gitignore +3 -0
@@ -0,0 +1,77 @@
1
+ //! Who a background request acts for: the authenticated DSM user and the UID
2
+ //! looked up afresh for it.
3
+
4
+ use std::sync::Arc;
5
+
6
+ use serde::Deserialize;
7
+ use serde_json::Value;
8
+ use syno_pkg_land_sdk::BoxError;
9
+ use syno_pkg_land_sdk::api::{self, Caller, Route};
10
+ use syno_pkg_land_sdk::background::{Access, Submitter};
11
+ use syno_pkg_land_sdk::ctx::Context;
12
+ use syno_pkg_land_sdk::http::Request;
13
+
14
+ use super::{BackgroundApi, Handler, background_error, identity_unavailable};
15
+ use crate::app::routes::decode_params;
16
+
17
+ impl BackgroundApi {
18
+ pub(super) fn access(&self, caller: &Caller) -> Access {
19
+ Access {
20
+ admin: caller.admin(),
21
+ owner: Submitter {
22
+ user: caller.user().to_string(),
23
+ uid: (self.uid_lookup)(caller.user()),
24
+ },
25
+ }
26
+ }
27
+
28
+ /// A route for any signed-in user whose UID resolves; administrators see
29
+ /// every owner's jobs and need no UID.
30
+ pub(super) fn owner(self: &Arc<Self>, method: &str, handler: Handler) -> Route {
31
+ let api = self.clone();
32
+ Route::handle_request(method, move |ctx, caller, req, params| {
33
+ let access = api.access(caller);
34
+ if !access.admin && !access.owner.valid() {
35
+ return Err(identity_unavailable().into());
36
+ }
37
+ handler(&api, ctx, caller, req, &access, params)
38
+ })
39
+ }
40
+
41
+ pub(super) fn admin(self: &Arc<Self>, method: &str, handler: Handler) -> Route {
42
+ let api = self.clone();
43
+ Route::handle_request(method, move |ctx, caller, req, params| {
44
+ if !caller.admin() {
45
+ return Err(api::Error::new(
46
+ "unsupported",
47
+ "background jobs need a DSM administrator",
48
+ )
49
+ .into());
50
+ }
51
+ handler(&api, ctx, caller, req, &api.access(caller), params)
52
+ })
53
+ }
54
+
55
+ pub(super) fn purge(
56
+ &self,
57
+ ctx: &Context,
58
+ caller: &Caller,
59
+ _: &Request,
60
+ _: &Access,
61
+ params: &[u8],
62
+ ) -> Result<Value, BoxError> {
63
+ #[derive(Default, Deserialize)]
64
+ #[serde(deny_unknown_fields)]
65
+ struct Params {
66
+ owner: Option<Submitter>,
67
+ }
68
+ let Some(owner) = decode_params::<Params>(params).ok().and_then(|p| p.owner) else {
69
+ return Err(api::Error::bad_request("exact owner is required").into());
70
+ };
71
+ let audit = self
72
+ .store
73
+ .purge_owner(ctx, &owner, caller.user())
74
+ .map_err(background_error)?;
75
+ Ok(serde_json::to_value(audit)?)
76
+ }
77
+ }
@@ -0,0 +1,174 @@
1
+ use serde_json::Value;
2
+ use syno_pkg_land_sdk::background::{JobResult, Outcome, State};
3
+ use syno_pkg_land_sdk::ctx::Context;
4
+
5
+ use super::tests::{Env, data, id_params};
6
+ use crate::app::test_support::{Stub, alice, bob, carol};
7
+
8
+ fn ordinary_env() -> Env {
9
+ let env = Env::new();
10
+ env.set_uid(|name| match name {
11
+ "alice" => "1024".into(),
12
+ "bob" => "1025".into(),
13
+ "carol" => "1026".into(),
14
+ _ => String::new(),
15
+ });
16
+ env
17
+ }
18
+
19
+ fn owner_submit(env: &Env, auth: &Stub, key: &str) -> String {
20
+ let params = format!(r#"{{"key":"{key}","folder":"/share","relative":"a.txt"}}"#);
21
+ let (code, out) = env.call(auth, "bg.jobs.submit", &params);
22
+ assert_eq!(code, "", "submit {key}: {out}");
23
+ data(&out)["job"]["id"].as_str().unwrap().into()
24
+ }
25
+
26
+ fn job_count(env: &Env, auth: &Stub) -> usize {
27
+ let (_, out) = env.call(auth, "bg.jobs.list", "{}");
28
+ data(&out)["jobs"].as_array().unwrap().len()
29
+ }
30
+
31
+ #[test]
32
+ fn owners_are_isolated_including_keys_and_counts() {
33
+ let env = ordinary_env();
34
+ let a = owner_submit(&env, &alice(), "same");
35
+ let b = owner_submit(&env, &bob(), "same");
36
+ let c = owner_submit(&env, &carol(), "same");
37
+ assert!(a != b && b != c, "idempotency key leaked across owners");
38
+ for auth in [bob(), carol()] {
39
+ assert_eq!(job_count(&env, &auth), 1);
40
+ let (_, out) = env.call(&auth, "bg.status", "{}");
41
+ assert_eq!(data(&out)["queued"], 1, "status leaks: {out}");
42
+ assert_eq!(data(&out)["admin"], false);
43
+ }
44
+ for method in [
45
+ "bg.jobs.get",
46
+ "bg.jobs.cancel",
47
+ "bg.jobs.pause",
48
+ "bg.jobs.resume",
49
+ "bg.jobs.result",
50
+ ] {
51
+ for id in [&a, &c, &"0".repeat(32)] {
52
+ let (code, out) = env.call(&bob(), method, &id_params(id));
53
+ assert_eq!(code, "not_found", "{method} {id}: {out}");
54
+ }
55
+ }
56
+ assert_eq!(job_count(&env, &alice()), 3, "admin must see all");
57
+ assert_eq!(env.call(&alice(), "bg.jobs.cancel", &id_params(&b)).0, "");
58
+ }
59
+
60
+ #[test]
61
+ fn owner_result_and_uid_change_fail_closed() {
62
+ let env = ordinary_env();
63
+ let id = owner_submit(&env, &bob(), "one");
64
+ let ctx = Context::background();
65
+ env.store.claim(&ctx).unwrap();
66
+ env.store
67
+ .finish(
68
+ &ctx,
69
+ &id,
70
+ Outcome {
71
+ state: State::Succeeded,
72
+ result: Some(JobResult {
73
+ sha256: "digest".into(),
74
+ bytes: 5,
75
+ }),
76
+ ..Outcome::default()
77
+ },
78
+ )
79
+ .unwrap();
80
+ assert_eq!(env.call(&bob(), "bg.jobs.result", &id_params(&id)).0, "");
81
+ assert_eq!(
82
+ env.call(&carol(), "bg.jobs.result", &id_params(&id)).0,
83
+ "not_found"
84
+ );
85
+ env.set_uid(|_| "2000".into());
86
+ assert_eq!(
87
+ job_count(&env, &bob()),
88
+ 0,
89
+ "same name with new UID saw old jobs"
90
+ );
91
+ assert_eq!(
92
+ env.call(&bob(), "bg.jobs.get", &id_params(&id)).0,
93
+ "not_found"
94
+ );
95
+ env.set_uid(|_| String::new());
96
+ assert_eq!(
97
+ env.call(&bob(), "bg.jobs.list", "{}").0,
98
+ "identity_unavailable"
99
+ );
100
+ }
101
+
102
+ #[test]
103
+ fn personal_quota_and_admin_only_purge() {
104
+ let env = ordinary_env();
105
+ assert_eq!(env.call(&bob(), "bg.jobs.purge", "{}").0, "admin_required");
106
+ owner_submit(&env, &bob(), "one");
107
+ owner_submit(&env, &bob(), "two");
108
+ let params = r#"{"key":"three","folder":"/share","relative":"a.txt"}"#;
109
+ assert_eq!(
110
+ env.call(&bob(), "bg.jobs.submit", params).0,
111
+ "owner-queue-full"
112
+ );
113
+ owner_submit(&env, &carol(), "one");
114
+ }
115
+
116
+ #[test]
117
+ fn purge_requires_an_admin_and_the_exact_owner() {
118
+ let env = ordinary_env();
119
+ let id = owner_submit(&env, &bob(), "one");
120
+ owner_submit(&env, &carol(), "one");
121
+ for params in [
122
+ "{}",
123
+ r#"{"owner":null}"#,
124
+ r#"{"owner":{"user":"bob","uid":"1025"},"x":1}"#,
125
+ ] {
126
+ assert_eq!(
127
+ env.call(&alice(), "bg.jobs.purge", params).0,
128
+ "bad_request",
129
+ "{params}"
130
+ );
131
+ }
132
+ let (code, out) = env.call(
133
+ &alice(),
134
+ "bg.jobs.purge",
135
+ r#"{"owner":{"user":"bob","uid":"1025"}}"#,
136
+ );
137
+ assert_eq!(code, "", "{out}");
138
+ assert_eq!(data(&out)["removed"], 1);
139
+ assert_eq!(
140
+ env.call(&alice(), "bg.jobs.get", &id_params(&id)).0,
141
+ "not_found"
142
+ );
143
+ assert_eq!(job_count(&env, &alice()), 1, "purge touched another owner");
144
+ }
145
+
146
+ #[test]
147
+ fn fresh_ordinary_user_jobs_need_no_admin_setup() {
148
+ let env = Env::new();
149
+ env.set_uid(|name| if name == "bob" { "1025" } else { "1026" }.into());
150
+ let (code, out) = env.call(&bob(), "bg.status", "{}");
151
+ assert_eq!(code, "", "fresh status: {out}");
152
+ for key in ["enabled", "allowOrdinaryUsers"] {
153
+ assert!(
154
+ data(&out).get(key).is_none(),
155
+ "obsolete status switch {key}"
156
+ );
157
+ }
158
+ let id = owner_submit(&env, &bob(), "fresh");
159
+ for method in [
160
+ "bg.jobs.get",
161
+ "bg.jobs.cancel",
162
+ "bg.jobs.pause",
163
+ "bg.jobs.resume",
164
+ ] {
165
+ let (code, _) = env.call(&carol(), method, &id_params(&id));
166
+ assert_eq!(code, "not_found", "other owner {method}");
167
+ }
168
+ assert_eq!(env.call(&bob(), "bg.jobs.get", &id_params(&id)).0, "");
169
+ assert_eq!(env.call(&bob(), "bg.jobs.cancel", &id_params(&id)).0, "");
170
+ for method in ["bg.mode.set", "bg.users.set"] {
171
+ let (code, _): (String, Value) = env.call(&alice(), method, "{}");
172
+ assert_eq!(code, "unknown_method", "obsolete route {method}");
173
+ }
174
+ }
@@ -0,0 +1,126 @@
1
+ //! Recurring copies of an imported job: owners manage their own, an
2
+ //! administrator re-approves them.
3
+
4
+ use serde::Deserialize;
5
+ use serde_json::{Value, json};
6
+ use syno_pkg_land_sdk::api::{self, Caller};
7
+ use syno_pkg_land_sdk::background::{Access, ScheduleRequest};
8
+ use syno_pkg_land_sdk::ctx::Context;
9
+ use syno_pkg_land_sdk::http::Request;
10
+ use syno_pkg_land_sdk::{BoxError, jsonx};
11
+
12
+ use super::{BackgroundApi, background_error, decode_id, identity_unavailable};
13
+ use crate::app::routes::decode_params;
14
+
15
+ impl BackgroundApi {
16
+ pub(super) fn schedules(
17
+ &self,
18
+ ctx: &Context,
19
+ _: &Caller,
20
+ _: &Request,
21
+ access: &Access,
22
+ params: &[u8],
23
+ ) -> Result<Value, BoxError> {
24
+ decode_params::<jsonx::Empty>(params)?;
25
+ let list = self
26
+ .store
27
+ .schedules(ctx, access)
28
+ .map_err(background_error)?;
29
+ Ok(json!({"schedules": list}))
30
+ }
31
+
32
+ pub(super) fn create_schedule(
33
+ &self,
34
+ ctx: &Context,
35
+ _: &Caller,
36
+ _: &Request,
37
+ access: &Access,
38
+ params: &[u8],
39
+ ) -> Result<Value, BoxError> {
40
+ #[derive(Default, Deserialize)]
41
+ #[serde(deny_unknown_fields, rename_all = "camelCase")]
42
+ struct Params {
43
+ #[serde(default)]
44
+ key: String,
45
+ #[serde(default)]
46
+ job_id: String,
47
+ #[serde(default)]
48
+ interval_seconds: i64,
49
+ #[serde(default)]
50
+ consent: bool,
51
+ }
52
+ let params: Params = decode_params(params)?;
53
+ if !access.owner.valid() {
54
+ return Err(identity_unavailable().into());
55
+ }
56
+ let request = ScheduleRequest {
57
+ key: params.key,
58
+ job_id: params.job_id,
59
+ interval_seconds: params.interval_seconds,
60
+ consent: params.consent,
61
+ };
62
+ let (schedule, duplicate) = self
63
+ .store
64
+ .create_schedule(ctx, &request, access)
65
+ .map_err(background_error)?;
66
+ Ok(json!({"schedule": schedule, "duplicate": duplicate}))
67
+ }
68
+
69
+ pub(super) fn disable_schedule(
70
+ &self,
71
+ ctx: &Context,
72
+ _: &Caller,
73
+ _: &Request,
74
+ access: &Access,
75
+ params: &[u8],
76
+ ) -> Result<Value, BoxError> {
77
+ let id = decode_id(params)?;
78
+ let schedule = self
79
+ .store
80
+ .set_schedule(ctx, &id, false, access)
81
+ .map_err(background_error)?;
82
+ Ok(json!({"schedule": schedule}))
83
+ }
84
+
85
+ pub(super) fn enable_schedule(
86
+ &self,
87
+ ctx: &Context,
88
+ _: &Caller,
89
+ _: &Request,
90
+ access: &Access,
91
+ params: &[u8],
92
+ ) -> Result<Value, BoxError> {
93
+ #[derive(Default, Deserialize)]
94
+ #[serde(deny_unknown_fields)]
95
+ struct Params {
96
+ #[serde(default)]
97
+ id: String,
98
+ #[serde(default)]
99
+ consent: bool,
100
+ }
101
+ let params = match decode_params::<Params>(params) {
102
+ Ok(p) if p.consent => p,
103
+ _ => return Err(api::Error::bad_request("explicit consent is required").into()),
104
+ };
105
+ let schedule = self
106
+ .store
107
+ .set_schedule(ctx, &params.id, true, access)
108
+ .map_err(background_error)?;
109
+ Ok(json!({"schedule": schedule}))
110
+ }
111
+
112
+ pub(super) fn delete_schedule(
113
+ &self,
114
+ ctx: &Context,
115
+ _: &Caller,
116
+ _: &Request,
117
+ access: &Access,
118
+ params: &[u8],
119
+ ) -> Result<Value, BoxError> {
120
+ let id = decode_id(params)?;
121
+ self.store
122
+ .delete_schedule(ctx, &id, access)
123
+ .map_err(background_error)?;
124
+ Ok(json!({"deleted": true}))
125
+ }
126
+ }
@@ -0,0 +1,108 @@
1
+ use syno_pkg_land_sdk::background::{JobResult, Outcome, State};
2
+ use syno_pkg_land_sdk::ctx::Context;
3
+
4
+ use super::tests::{Env, data, id_params};
5
+ use crate::app::test_support::{Stub, alice, bob, carol};
6
+
7
+ fn ordinary_env() -> Env {
8
+ let env = Env::new();
9
+ env.set_uid(|name| match name {
10
+ "alice" => "1024".into(),
11
+ "bob" => "1025".into(),
12
+ "carol" => "1026".into(),
13
+ _ => String::new(),
14
+ });
15
+ env
16
+ }
17
+
18
+ fn submit_and_finish(env: &Env, auth: &Stub) -> String {
19
+ let (code, out) = env.call(
20
+ auth,
21
+ "bg.jobs.submit",
22
+ r#"{"key":"copy","folder":"/share","relative":"a.txt"}"#,
23
+ );
24
+ assert_eq!(code, "", "{out}");
25
+ let id: String = data(&out)["job"]["id"].as_str().unwrap().into();
26
+ let ctx = Context::background();
27
+ env.store.claim(&ctx).unwrap();
28
+ env.store
29
+ .finish(
30
+ &ctx,
31
+ &id,
32
+ Outcome {
33
+ state: State::Succeeded,
34
+ result: Some(JobResult {
35
+ bytes: 5,
36
+ ..JobResult::default()
37
+ }),
38
+ ..Outcome::default()
39
+ },
40
+ )
41
+ .unwrap();
42
+ id
43
+ }
44
+
45
+ fn schedule_count(env: &Env, auth: &Stub) -> usize {
46
+ let (_, out) = env.call(auth, "bg.schedules.list", "{}");
47
+ data(&out)["schedules"].as_array().unwrap().len()
48
+ }
49
+
50
+ #[test]
51
+ fn schedule_routes_consent_owner_and_admin() {
52
+ let env = ordinary_env();
53
+ let id = submit_and_finish(&env, &bob());
54
+ let params = format!(r#"{{"key":"s","jobId":"{id}","intervalSeconds":3600,"consent":true}}"#);
55
+ assert_eq!(
56
+ env.call(&carol(), "bg.schedules.create", &params).0,
57
+ "not_found"
58
+ );
59
+ for bad in [
60
+ format!(r#"{{"key":"s","jobId":"{id}","intervalSeconds":3600}}"#),
61
+ format!(
62
+ r#"{{"key":"s","jobId":"{id}","intervalSeconds":3600,"consent":true,"owner":"carol"}}"#
63
+ ),
64
+ ] {
65
+ assert_eq!(
66
+ env.call(&bob(), "bg.schedules.create", &bad).0,
67
+ "bad_request",
68
+ "{bad}"
69
+ );
70
+ }
71
+ let (code, out) = env.call(&bob(), "bg.schedules.create", &params);
72
+ assert_eq!(code, "", "create: {out}");
73
+ let sid = data(&out)["schedule"]["id"].as_str().unwrap().to_string();
74
+ assert_eq!(schedule_count(&env, &carol()), 0, "list leak");
75
+ assert_eq!(schedule_count(&env, &alice()), 1, "admin list");
76
+ for method in ["bg.schedules.disable", "bg.schedules.delete"] {
77
+ assert_eq!(
78
+ env.call(&carol(), method, &id_params(&sid)).0,
79
+ "not_found",
80
+ "{method}"
81
+ );
82
+ }
83
+ let enable = format!(r#"{{"id":"{sid}","consent":true}}"#);
84
+ assert_eq!(
85
+ env.call(&bob(), "bg.schedules.enable", &enable).0,
86
+ "admin_required"
87
+ );
88
+ assert_eq!(
89
+ env.call(&alice(), "bg.schedules.disable", &id_params(&sid))
90
+ .0,
91
+ ""
92
+ );
93
+ assert_eq!(
94
+ env.call(&alice(), "bg.schedules.enable", &id_params(&sid))
95
+ .0,
96
+ "bad_request"
97
+ );
98
+ assert_eq!(env.call(&alice(), "bg.schedules.enable", &enable).0, "");
99
+ assert_eq!(
100
+ env.call(&bob(), "bg.schedules.delete", &id_params(&sid)).0,
101
+ ""
102
+ );
103
+ assert_eq!(
104
+ env.files.calls(),
105
+ 1,
106
+ "no scheduling route calls File Station, uses a root grant or stores a session"
107
+ );
108
+ }