@synopackageland/templates 0.9.4 → 0.9.6
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.
- package/README.md +27 -4
- package/package.json +2 -2
- package/templates/_shared/AGENTS.md +95 -18
- package/templates/native-go/backend/THIRD_PARTY_NOTICES.txt +1 -1
- package/templates/native-go/backend/cmd/app/main.go +15 -6
- package/templates/native-go/backend/cmd/background-worker/main.go +5 -1
- package/templates/native-go/backend/go.mod +1 -1
- package/templates/native-go/backend/go.sum +2 -2
- package/templates/native-go/backend/internal/app/background.go +19 -8
- package/templates/native-go/backend/internal/app/background_owner_test.go +2 -2
- package/templates/native-go/backend/internal/app/background_test.go +24 -0
- package/templates/native-go/backend/internal/app/background_work.go +94 -0
- package/templates/native-go/backend/internal/app/background_work_test.go +95 -0
- package/templates/native-go/backend/internal/app/portal.go +49 -4
- package/templates/native-go/backend/internal/app/portal_test.go +128 -0
- package/templates/native-go/backend/internal/app/routes.go +6 -4
- package/templates/native-go/backend/internal/app/uninstall.go +19 -0
- package/templates/native-go/backend/internal/app/uninstall_test.go +33 -0
- package/templates/native-go/dsm-package.yaml +18 -0
- package/templates/native-go-delegate/backend/cmd/background-worker/main.go +1 -1
- package/templates/native-go-delegate/backend/internal/app/delegate_routes_test.go +2 -0
- package/templates/native-go-jobs/backend/internal/app/background_routes_test.go +4 -0
- package/templates/native-go-jobs/frontend/test/background-page.test.ts +21 -0
- package/templates/native-go-vue/frontend/package-lock.json +8 -8
- package/templates/native-go-vue/frontend/package.json +2 -2
- package/templates/native-rust/README.md +381 -0
- package/templates/native-rust/backend/Cargo.lock +125 -0
- package/templates/native-rust/backend/Cargo.toml +24 -0
- package/templates/native-rust/backend/THIRD_PARTY_NOTICES.txt +207 -0
- package/templates/native-rust/backend/permissions.json +5 -0
- package/templates/native-rust/backend/rust-toolchain.toml +4 -0
- package/templates/native-rust/backend/src/app/background/mod.rs +424 -0
- package/templates/native-rust/backend/src/app/background/owner.rs +77 -0
- package/templates/native-rust/backend/src/app/background/owner_tests.rs +174 -0
- package/templates/native-rust/backend/src/app/background/schedule.rs +126 -0
- package/templates/native-rust/backend/src/app/background/schedule_tests.rs +108 -0
- package/templates/native-rust/backend/src/app/background/tests.rs +505 -0
- package/templates/native-rust/backend/src/app/background/work.rs +78 -0
- package/templates/native-rust/backend/src/app/background/work_tests.rs +123 -0
- package/templates/native-rust/backend/src/app/background_routes.rs +22 -0
- package/templates/native-rust/backend/src/app/delegate_config.rs +188 -0
- package/templates/native-rust/backend/src/app/delegate_routes.rs +22 -0
- package/templates/native-rust/backend/src/app/identity.rs +56 -0
- package/templates/native-rust/backend/src/app/migrations.rs +79 -0
- package/templates/native-rust/backend/src/app/mod.rs +30 -0
- package/templates/native-rust/backend/src/app/portal.rs +317 -0
- package/templates/native-rust/backend/src/app/root_routes.rs +23 -0
- package/templates/native-rust/backend/src/app/routes.rs +315 -0
- package/templates/native-rust/backend/src/app/session_files.rs +19 -0
- package/templates/native-rust/backend/src/app/shared_folder.rs +383 -0
- package/templates/native-rust/backend/src/app/test_support.rs +200 -0
- package/templates/native-rust/backend/src/app/uninstall.rs +38 -0
- package/templates/native-rust/backend/src/bin/api.rs +72 -0
- package/templates/native-rust/backend/src/bin/background-worker.rs +37 -0
- package/templates/native-rust/backend/src/build_env.rs +155 -0
- package/templates/native-rust/backend/src/lib.rs +6 -0
- package/templates/native-rust/backend/src/permissions.rs +54 -0
- package/templates/native-rust/backend/src/process.rs +91 -0
- package/templates/native-rust/dsm-package.yaml +89 -0
- package/templates/native-rust/gitignore +2 -0
- package/templates/native-rust/i18n/glossary.json +663 -0
- package/templates/native-rust/i18n/messages/de.json +29 -0
- package/templates/native-rust/i18n/messages/en.json +29 -0
- package/templates/native-rust/i18n/messages/es.json +29 -0
- package/templates/native-rust/i18n/messages/fr.json +29 -0
- package/templates/native-rust/i18n/messages/it.json +29 -0
- package/templates/native-rust/i18n/messages/ja.json +29 -0
- package/templates/native-rust/i18n/messages/ko.json +29 -0
- package/templates/native-rust/i18n/messages/zh-CN.json +29 -0
- package/templates/native-rust/i18n/messages/zh-TW.json +29 -0
- package/templates/native-rust/ui/app.css +45 -0
- package/templates/native-rust/ui/app.js +111 -0
- package/templates/native-rust-delegate/backend/src/app/delegate_routes.rs +92 -0
- package/templates/native-rust-delegate/backend/src/bin/background-worker.rs +52 -0
- package/templates/native-rust-jobs/backend/src/app/background_routes.rs +109 -0
- package/templates/native-rust-root-ops/backend/src/app/root_routes.rs +475 -0
- package/templates/native-rust-vue/README.md +55 -0
- package/templates/native-rust-vue/gitignore +3 -0
|
@@ -3,28 +3,73 @@ package app
|
|
|
3
3
|
import (
|
|
4
4
|
"context"
|
|
5
5
|
"encoding/json"
|
|
6
|
+
"errors"
|
|
7
|
+
"net/http"
|
|
6
8
|
|
|
7
9
|
"github.com/synopackageland/syno-pkg-land-sdk/api"
|
|
10
|
+
"github.com/synopackageland/syno-pkg-land-sdk/delegate"
|
|
8
11
|
"github.com/synopackageland/syno-pkg-land-sdk/portal"
|
|
9
12
|
)
|
|
10
13
|
|
|
11
14
|
// portalRoutes serves portal.status when dsm-package.yaml declares `portal`.
|
|
12
15
|
// An administrator sets the alias with DSM's own AppPortal API from the UI;
|
|
13
|
-
// the package only reads DSM's saved setting.
|
|
16
|
+
// the package only reads DSM's saved setting. Below DSM 7.2 the executor
|
|
17
|
+
// registers the App's entry (PortalExecutor): portal.status then reports it,
|
|
18
|
+
// and portal.register and portal.unregister change it.
|
|
14
19
|
func (cfg Config) portalRoutes() []api.Route {
|
|
15
20
|
if !cfg.Portal {
|
|
16
21
|
return nil
|
|
17
22
|
}
|
|
18
|
-
|
|
23
|
+
routes := []api.Route{api.Handle("portal.status", cfg.portalStatus)}
|
|
24
|
+
if cfg.PortalExecutor {
|
|
25
|
+
routes = append(routes,
|
|
26
|
+
api.HandleRequest("portal.register", cfg.portalChange(portal.Register)),
|
|
27
|
+
api.HandleRequest("portal.unregister", cfg.portalChange(portal.Unregister)))
|
|
28
|
+
}
|
|
29
|
+
return routes
|
|
19
30
|
}
|
|
20
31
|
|
|
21
|
-
|
|
32
|
+
// portalExecutor is a variable so tests can replace the executor.
|
|
33
|
+
var portalExecutor = func(cfg Config) portal.Executor { return delegate.Local(cfg.PackageID) }
|
|
34
|
+
|
|
35
|
+
func (cfg Config) portalStatus(ctx context.Context, _ api.Caller, params json.RawMessage) (any, error) {
|
|
22
36
|
if err := decodeParams(params, &struct{}{}); err != nil {
|
|
23
37
|
return nil, err
|
|
24
38
|
}
|
|
39
|
+
return cfg.readPortal(ctx), nil
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
func (cfg Config) readPortal(ctx context.Context) portal.Status {
|
|
25
43
|
path := cfg.PortalConfig
|
|
26
44
|
if path == "" {
|
|
27
45
|
path = portal.ConfigPath(cfg.PackageID)
|
|
28
46
|
}
|
|
29
|
-
|
|
47
|
+
s := portal.Read(path, "SYNO.SDS."+cfg.PackageID+".Instance")
|
|
48
|
+
if !cfg.PortalExecutor {
|
|
49
|
+
return s
|
|
50
|
+
}
|
|
51
|
+
// An unreadable entry is not a known one: WithEntry reports unavailable.
|
|
52
|
+
entry, _ := portal.ExecutorEntry(ctx, portalExecutor(cfg))
|
|
53
|
+
return portal.WithEntry(s, entry)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// portalChange forwards the administrator's DSM session; the executor checks
|
|
57
|
+
// it again and keeps its refusal codes, such as PORTAL_FAILED.
|
|
58
|
+
func (cfg Config) portalChange(change func(context.Context, portal.Executor, map[string]string) (string, error)) api.RequestHandler {
|
|
59
|
+
return func(ctx context.Context, caller api.Caller, req *http.Request, params json.RawMessage) (any, error) {
|
|
60
|
+
if !caller.Admin() {
|
|
61
|
+
return nil, errors.New("admin_required")
|
|
62
|
+
}
|
|
63
|
+
if err := decodeParams(params, &struct{}{}); err != nil {
|
|
64
|
+
return nil, err
|
|
65
|
+
}
|
|
66
|
+
auth, err := api.DSMSession(req)
|
|
67
|
+
if err != nil {
|
|
68
|
+
return nil, err
|
|
69
|
+
}
|
|
70
|
+
if _, err := change(ctx, portalExecutor(cfg), auth); err != nil {
|
|
71
|
+
return nil, err
|
|
72
|
+
}
|
|
73
|
+
return cfg.readPortal(ctx), nil
|
|
74
|
+
}
|
|
30
75
|
}
|
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
package app
|
|
2
2
|
|
|
3
3
|
import (
|
|
4
|
+
"context"
|
|
5
|
+
"encoding/json"
|
|
6
|
+
"errors"
|
|
4
7
|
"os"
|
|
5
8
|
"path/filepath"
|
|
6
9
|
"testing"
|
|
10
|
+
|
|
11
|
+
"github.com/synopackageland/syno-pkg-land-sdk/api"
|
|
12
|
+
"github.com/synopackageland/syno-pkg-land-sdk/portal"
|
|
13
|
+
|
|
14
|
+
appapi "__PACKAGE__/internal/api"
|
|
7
15
|
)
|
|
8
16
|
|
|
9
17
|
func portalConfig(t *testing.T, saved string) Config {
|
|
@@ -53,3 +61,123 @@ func TestPortalStatusExistsOnlyForAPortalBuild(t *testing.T) {
|
|
|
53
61
|
t.Fatalf("%q", code)
|
|
54
62
|
}
|
|
55
63
|
}
|
|
64
|
+
|
|
65
|
+
// fakeEntry is the executor as the portal routes see it.
|
|
66
|
+
type fakeEntry struct {
|
|
67
|
+
entry string
|
|
68
|
+
err error
|
|
69
|
+
calls []string
|
|
70
|
+
auth map[string]string
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
func (f *fakeEntry) Call(_ context.Context, op string, auth map[string]string, _, result any) error {
|
|
74
|
+
f.calls = append(f.calls, op)
|
|
75
|
+
if f.err != nil {
|
|
76
|
+
return f.err
|
|
77
|
+
}
|
|
78
|
+
switch op {
|
|
79
|
+
case "portal.register":
|
|
80
|
+
f.entry, f.auth = portal.EntryRegistered, auth
|
|
81
|
+
case "portal.unregister":
|
|
82
|
+
f.entry, f.auth = portal.EntryUnregistered, auth
|
|
83
|
+
}
|
|
84
|
+
key := "entry"
|
|
85
|
+
if op == "status" {
|
|
86
|
+
key = "portal"
|
|
87
|
+
}
|
|
88
|
+
b, _ := json.Marshal(map[string]string{key: f.entry})
|
|
89
|
+
return json.Unmarshal(b, result)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
func executorPortalConfig(t *testing.T, saved string, executor *fakeEntry) Config {
|
|
93
|
+
cfg := portalConfig(t, saved)
|
|
94
|
+
cfg.PortalExecutor = true
|
|
95
|
+
previous := portalExecutor
|
|
96
|
+
portalExecutor = func(Config) portal.Executor { return executor }
|
|
97
|
+
t.Cleanup(func() { portalExecutor = previous })
|
|
98
|
+
return cfg
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
func sessionCall(t *testing.T, cfg Config, method string) (string, map[string]any) {
|
|
102
|
+
t.Helper()
|
|
103
|
+
permissions, err := appapi.Permissions()
|
|
104
|
+
if err != nil {
|
|
105
|
+
t.Fatal(err)
|
|
106
|
+
}
|
|
107
|
+
req := apiRequest(method, `{}`)
|
|
108
|
+
req.Header.Set("Cookie", "id=session")
|
|
109
|
+
req.Header.Set("X-SYNO-TOKEN", "token")
|
|
110
|
+
return serve(t, cfg, alice, req, permissions)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
func TestExecutorPortalStatusReportsTheEntry(t *testing.T) {
|
|
114
|
+
saved := `{"alias":"hello-go","id":"SYNO.SDS.HelloGo.Instance"}`
|
|
115
|
+
for _, c := range []struct {
|
|
116
|
+
entry string
|
|
117
|
+
err error
|
|
118
|
+
state any
|
|
119
|
+
alias any
|
|
120
|
+
}{
|
|
121
|
+
{portal.EntryRegistered, nil, "active", "hello-go"},
|
|
122
|
+
{portal.EntryCleanupRequired, nil, "active", "hello-go"},
|
|
123
|
+
{portal.EntryUnregistered, nil, "inactive", nil},
|
|
124
|
+
{"", errors.New("executor down"), "unavailable", nil},
|
|
125
|
+
} {
|
|
126
|
+
cfg := executorPortalConfig(t, saved, &fakeEntry{entry: c.entry, err: c.err})
|
|
127
|
+
code, out := call(t, cfg, bob, "portal.status", `{}`)
|
|
128
|
+
data := out["data"].(map[string]any)
|
|
129
|
+
if code != "" || data["state"] != c.state || data["alias"] != c.alias {
|
|
130
|
+
t.Fatalf("%s: %q %v", c.entry, code, data)
|
|
131
|
+
}
|
|
132
|
+
if c.err == nil && data["entry"] != c.entry {
|
|
133
|
+
t.Fatalf("%s: entry %v", c.entry, data)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
func TestExecutorPortalRegisterAndUnregisterReturnTheFreshStatus(t *testing.T) {
|
|
139
|
+
executor := &fakeEntry{entry: portal.EntryUnregistered}
|
|
140
|
+
cfg := executorPortalConfig(t, `{"alias":"hello-go","id":"SYNO.SDS.HelloGo.Instance"}`, executor)
|
|
141
|
+
code, out := sessionCall(t, cfg, "portal.register")
|
|
142
|
+
if data, _ := out["data"].(map[string]any); code != "" || data["entry"] != portal.EntryRegistered || data["state"] != "active" {
|
|
143
|
+
t.Fatalf("register: %q %v", code, out)
|
|
144
|
+
}
|
|
145
|
+
if executor.auth["dsm_cookie"] != "session" {
|
|
146
|
+
t.Fatalf("register did not forward the DSM session: %v", executor.auth)
|
|
147
|
+
}
|
|
148
|
+
code, out = sessionCall(t, cfg, "portal.unregister")
|
|
149
|
+
if data, _ := out["data"].(map[string]any); code != "" || data["entry"] != portal.EntryUnregistered || data["state"] != "inactive" {
|
|
150
|
+
t.Fatalf("unregister: %q %v", code, out)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
func TestExecutorPortalChangesNeedAnAdministratorsDSMSession(t *testing.T) {
|
|
155
|
+
executor := &fakeEntry{entry: portal.EntryUnregistered}
|
|
156
|
+
cfg := executorPortalConfig(t, "", executor)
|
|
157
|
+
for _, method := range []string{"portal.register", "portal.unregister"} {
|
|
158
|
+
if code, _ := call(t, cfg, bob, method, `{}`); code == "" {
|
|
159
|
+
t.Fatalf("%s: an ordinary user changed the entry", method)
|
|
160
|
+
}
|
|
161
|
+
if code, _ := call(t, cfg, alice, method, `{}`); code != "session_required" {
|
|
162
|
+
t.Fatalf("%s without a DSM session: %q", method, code)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if len(executor.calls) != 0 {
|
|
166
|
+
t.Fatalf("the executor was asked: %v", executor.calls)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
func TestExecutorPortalRefusalKeepsTheExecutorsCode(t *testing.T) {
|
|
171
|
+
cfg := executorPortalConfig(t, "", &fakeEntry{err: &api.Error{Code: "PORTAL_FAILED", Message: "x"}})
|
|
172
|
+
if code, _ := sessionCall(t, cfg, "portal.register"); code != "PORTAL_FAILED" {
|
|
173
|
+
t.Fatalf("%q", code)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
func TestPortalChangesExistOnlyInAnExecutorPortalBuild(t *testing.T) {
|
|
178
|
+
for _, method := range []string{"portal.register", "portal.unregister"} {
|
|
179
|
+
if code, _ := call(t, portalConfig(t, ""), alice, method, `{}`); code != "unknown_method" {
|
|
180
|
+
t.Fatalf("%s: %q", method, code)
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
@@ -37,10 +37,12 @@ type Config struct {
|
|
|
37
37
|
DataDir string
|
|
38
38
|
// VolumeRoot is where /volumeN lives; empty means "/". Tests set it.
|
|
39
39
|
VolumeRoot string
|
|
40
|
-
// Portal serves portal.status;
|
|
41
|
-
// AppPortal.json path in
|
|
42
|
-
|
|
43
|
-
|
|
40
|
+
// Portal serves portal.status; PortalExecutor adds the executor's entry
|
|
41
|
+
// below DSM 7.2. PortalConfig overrides DSM's saved AppPortal.json path in
|
|
42
|
+
// tests.
|
|
43
|
+
Portal bool
|
|
44
|
+
PortalExecutor bool
|
|
45
|
+
PortalConfig string
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
func Routes(cfg Config) []api.Route {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
package app
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"os"
|
|
5
|
+
"path/filepath"
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
// Uninstall is the App's purge hook (dsm-package.yaml uninstall.purge): DSM
|
|
9
|
+
// runs `api.cgi uninstall` as the package account before the executor is
|
|
10
|
+
// removed, with the admin's "delete state" answer. Release here what the App
|
|
11
|
+
// holds outside the package. With deleteState it deletes the state that
|
|
12
|
+
// uninstall.state lists, the background job spool; package data stays for
|
|
13
|
+
// DSM's own "remove data" option.
|
|
14
|
+
func Uninstall(dataDir string, deleteState bool) error {
|
|
15
|
+
if !deleteState {
|
|
16
|
+
return nil
|
|
17
|
+
}
|
|
18
|
+
return os.RemoveAll(filepath.Join(dataDir, "background"))
|
|
19
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
package app
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"os"
|
|
5
|
+
"path/filepath"
|
|
6
|
+
"testing"
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
func TestUninstallDeletesTheJobSpoolOnlyWhenAsked(t *testing.T) {
|
|
10
|
+
dir := t.TempDir()
|
|
11
|
+
spool := filepath.Join(dir, "background")
|
|
12
|
+
if err := os.MkdirAll(filepath.Join(spool, "inputs"), 0o700); err != nil {
|
|
13
|
+
t.Fatal(err)
|
|
14
|
+
}
|
|
15
|
+
if err := Uninstall(dir, false); err != nil {
|
|
16
|
+
t.Fatal(err)
|
|
17
|
+
}
|
|
18
|
+
if _, err := os.Stat(spool); err != nil {
|
|
19
|
+
t.Fatalf("kept state is gone: %v", err)
|
|
20
|
+
}
|
|
21
|
+
if err := Uninstall(dir, true); err != nil {
|
|
22
|
+
t.Fatal(err)
|
|
23
|
+
}
|
|
24
|
+
if _, err := os.Stat(spool); !os.IsNotExist(err) {
|
|
25
|
+
t.Fatalf("state not deleted: %v", err)
|
|
26
|
+
}
|
|
27
|
+
if _, err := os.Stat(dir); err != nil {
|
|
28
|
+
t.Fatalf("package data must stay: %v", err)
|
|
29
|
+
}
|
|
30
|
+
if err := Uninstall(dir, true); err != nil {
|
|
31
|
+
t.Fatalf("nothing left to delete: %v", err)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -69,3 +69,21 @@ i18n:
|
|
|
69
69
|
|
|
70
70
|
# Optional SDK capabilities; delegate/rootOps share administrator-approved executor setup.
|
|
71
71
|
capabilities: {}
|
|
72
|
+
|
|
73
|
+
# Optional uninstall hook. DSM runs `exec args` as the package account on a
|
|
74
|
+
# real uninstall (not an upgrade), before the executor is removed, with
|
|
75
|
+
# SYNOPKGLAND_DELETE_STATE=0|1 and SYNOPKGLAND_REMOVE_DATA=0|1 from the wizard.
|
|
76
|
+
# A failure or timeout is logged (syslog tag synopkgland-uninstall, output in
|
|
77
|
+
# var/uninstall-hook.log) and never blocks the uninstall. The backend's
|
|
78
|
+
# `uninstall` mode deletes the background job spool when the admin ticks
|
|
79
|
+
# "delete state"; add uninstall.state.jobs to i18n/messages in every language.
|
|
80
|
+
# uninstall:
|
|
81
|
+
# purge:
|
|
82
|
+
# exec: ui/cgi/api.cgi
|
|
83
|
+
# args: [uninstall]
|
|
84
|
+
# timeout: 60 # seconds, 2..120
|
|
85
|
+
# # root: purge # rootOps only: root/purge.sh runs as root at
|
|
86
|
+
# # # uninstall, and only while its approval stands
|
|
87
|
+
# state:
|
|
88
|
+
# items:
|
|
89
|
+
# - key: jobs # i18n key uninstall.state.jobs
|
|
@@ -37,7 +37,7 @@ func main() {
|
|
|
37
37
|
defer stop()
|
|
38
38
|
log.Printf("started as uid %s", strconv.Itoa(info.UID))
|
|
39
39
|
dataDir := "/var/packages/" + packageID + "/var"
|
|
40
|
-
service := &delegate.Service{Client: delegate.Local(packageID), Store: background.NewStore(dataDir + "/background"), StagingDir: dataDir + "/executor-staging"}
|
|
40
|
+
service := &delegate.Service{Client: delegate.Local(packageID), Store: background.NewStore(dataDir + "/background"), StagingDir: dataDir + "/executor-staging", Work: app.HashWork}
|
|
41
41
|
settings, err := app.ParseDelegateConfig(app.Config{DelegateScope: delegateScope, DelegateOperationsJSON: delegateOperationsJSON, DelegateDestinationsJSON: delegateDestinationsJSON, DelegateLifetime: delegateLifetime})
|
|
42
42
|
if err != nil {
|
|
43
43
|
log.Fatal(err)
|
|
@@ -22,6 +22,8 @@ func init() {
|
|
|
22
22
|
sampleParams["delegate.jobs.submit"] = `{"key":"download","url":"http://localhost/file","dest":"/home","name":"file"}`
|
|
23
23
|
sampleParams["delegate.jobs.list"] = `{}`
|
|
24
24
|
sampleParams["delegate.jobs.cancel"] = `{"id":"missing"}`
|
|
25
|
+
// delegate.status judges the caller's Application Privileges, so it needs the DSM session too.
|
|
26
|
+
sampleOutcome["delegate.status"] = "session_required"
|
|
25
27
|
sampleOutcome["delegate.create"] = "session_required"
|
|
26
28
|
sampleOutcome["delegate.list"] = "session_required"
|
|
27
29
|
sampleOutcome["delegate.revoke"] = "session_required"
|
|
@@ -35,11 +35,15 @@ func init() {
|
|
|
35
35
|
sampleParams["bg.jobs.list"] = `{}`
|
|
36
36
|
sampleParams["bg.jobs.get"] = id
|
|
37
37
|
sampleParams["bg.jobs.cancel"] = id
|
|
38
|
+
sampleParams["bg.jobs.pause"] = id
|
|
39
|
+
sampleParams["bg.jobs.resume"] = id
|
|
38
40
|
sampleParams["bg.jobs.result"] = id
|
|
39
41
|
sampleOutcome["bg.browse"] = "session_required"
|
|
40
42
|
sampleOutcome["bg.jobs.submit"] = "session_required"
|
|
41
43
|
sampleOutcome["bg.jobs.get"] = "not_found"
|
|
42
44
|
sampleOutcome["bg.jobs.cancel"] = "not_found"
|
|
45
|
+
sampleOutcome["bg.jobs.pause"] = "not_found"
|
|
46
|
+
sampleOutcome["bg.jobs.resume"] = "not_found"
|
|
43
47
|
sampleOutcome["bg.jobs.result"] = "not_found"
|
|
44
48
|
}
|
|
45
49
|
|
|
@@ -18,3 +18,24 @@ test('a jobs project mounts the background panel for the signed-in user',async()
|
|
|
18
18
|
expect(root.querySelector('[data-page="background"]')).not.toBeNull()
|
|
19
19
|
}finally{app.unmount();root.remove()}
|
|
20
20
|
})
|
|
21
|
+
test('the background panel pauses a running job and resumes a paused one',async()=>{
|
|
22
|
+
const submitter={user:'alice',uid:'1024'}
|
|
23
|
+
const jobs=[{id:'run',name:'a.bin',state:'running',submitter},{id:'held',name:'b.bin',state:'paused',pauseReason:'user',submitter}]
|
|
24
|
+
const call=vi.fn(async(method:string):Promise<unknown>=>{
|
|
25
|
+
if(method==='app.status')return {package:'Jobs',version:'1',user:'alice',admin:false}
|
|
26
|
+
if(method==='bg.status')return {owner:submitter,admin:false,revision:1,worker:{username:'pkg',alive:true},limits:{maxInputBytes:1024,maxQueued:8,maxOwnerPending:4,maxOwnerBytes:4096,jobTimeoutSeconds:60}}
|
|
27
|
+
if(method==='bg.jobs.list')return {jobs}
|
|
28
|
+
if(method==='bg.schedules.list')return {schedules:[]}
|
|
29
|
+
return {}
|
|
30
|
+
})
|
|
31
|
+
const root=document.createElement('div');document.body.append(root)
|
|
32
|
+
const app=mount(root,{locale:'en',call,reopen:()=>()=>{}} as unknown as Context)
|
|
33
|
+
const button=(id:string,label:string)=>Array.from(root.querySelectorAll(`[data-job-id="${id}"] button`)).find(b=>b.textContent===label) as HTMLButtonElement|undefined
|
|
34
|
+
try{
|
|
35
|
+
await settle()
|
|
36
|
+
button('run','Pause')!.click();await settle()
|
|
37
|
+
expect(call).toHaveBeenCalledWith('bg.jobs.pause',{id:'run'})
|
|
38
|
+
button('held','Resume')!.click();await settle()
|
|
39
|
+
expect(call).toHaveBeenCalledWith('bg.jobs.resume',{id:'held'})
|
|
40
|
+
}finally{app.unmount();root.remove()}
|
|
41
|
+
})
|
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
"name": "native-dsm-vue-app",
|
|
8
8
|
"dependencies": {
|
|
9
9
|
"@lucide/vue": "1.48.0",
|
|
10
|
-
"@synopackageland/sdk": "0.9.
|
|
11
|
-
"@synopackageland/ui": "0.9.
|
|
10
|
+
"@synopackageland/sdk": "0.9.7",
|
|
11
|
+
"@synopackageland/ui": "0.9.2",
|
|
12
12
|
"vue": "3.5.43"
|
|
13
13
|
},
|
|
14
14
|
"devDependencies": {
|
|
@@ -1148,9 +1148,9 @@
|
|
|
1148
1148
|
}
|
|
1149
1149
|
},
|
|
1150
1150
|
"node_modules/@synopackageland/sdk": {
|
|
1151
|
-
"version": "0.9.
|
|
1152
|
-
"resolved": "https://registry.npmjs.org/@synopackageland/sdk/-/sdk-0.9.
|
|
1153
|
-
"integrity": "sha512-
|
|
1151
|
+
"version": "0.9.7",
|
|
1152
|
+
"resolved": "https://registry.npmjs.org/@synopackageland/sdk/-/sdk-0.9.7.tgz",
|
|
1153
|
+
"integrity": "sha512-uU0GvgMXmTRX4UMrm6NUDQEiPdVze1Y6uE/mwmM8kqrhnLhe90kt0RrcSoEpSj9KMH+bXACz1Dw3OBRygiA2wg==",
|
|
1154
1154
|
"license": "MIT",
|
|
1155
1155
|
"peerDependencies": {
|
|
1156
1156
|
"@lucide/vue": "^1.48.0",
|
|
@@ -1167,9 +1167,9 @@
|
|
|
1167
1167
|
}
|
|
1168
1168
|
},
|
|
1169
1169
|
"node_modules/@synopackageland/ui": {
|
|
1170
|
-
"version": "0.9.
|
|
1171
|
-
"resolved": "https://registry.npmjs.org/@synopackageland/ui/-/ui-0.9.
|
|
1172
|
-
"integrity": "sha512-
|
|
1170
|
+
"version": "0.9.2",
|
|
1171
|
+
"resolved": "https://registry.npmjs.org/@synopackageland/ui/-/ui-0.9.2.tgz",
|
|
1172
|
+
"integrity": "sha512-GXYs6oVed0tIb4J5D5aDYoLJMs7haq0DBWKrPP1cLU6QKUQ6EKaIwqWHv0sVdu530FCARjcB1PrWwWVS9WOsvA==",
|
|
1173
1173
|
"license": "MIT",
|
|
1174
1174
|
"dependencies": {
|
|
1175
1175
|
"@internationalized/date": "3.12.4",
|