-
Notifications
You must be signed in to change notification settings - Fork 70
Expand file tree
/
Copy pathdesktop_test.go
More file actions
243 lines (207 loc) · 7.72 KB
/
Copy pathdesktop_test.go
File metadata and controls
243 lines (207 loc) · 7.72 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
// Copyright 2026 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"flag"
"fmt"
"net"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"testing"
"time"
"github.com/apache/casbin-gateway/conf"
)
const (
desktopLogPath = "./logs/casbin-gateway-desktop.out"
devUiLogPath = "./logs/web-dev.out"
// devUiAddress is where "yarn dev" serves the frontend, fixed in
// web/vite.config.ts.
devUiAddress = "localhost:16002"
startWait = 90 * time.Second
startPoll = 300 * time.Millisecond
)
// TestDesktopApp starts Casbin Gateway as the desktop app — tray icon and
// native window — rather than the bare server "go run ." leaves in a terminal.
// It is a test only so that running it is one command:
//
// go test -run TestDesktopApp -v .
//
// The window shows the Vite dev server on port 16002, so it runs the frontend
// in web/src with hot reload rather than the compiled copy in web/build, which
// is only as new as the last "yarn build". Set CASBIN_GATEWAY_URL to point the
// window elsewhere — at the server's own port to see the built UI instead.
//
// Everything is rebuilt from source and left running after the test returns:
// quit the app from the tray icon, which stops the server it started, and the
// dev server keeps serving for the next run. Every environment variable reaches
// the executables, so httpport=17001 runs this beside a Gateway already holding
// the configured port.
func TestDesktopApp(t *testing.T) {
// Without this, "go test ./..." would launch the app and never come back.
if pattern := flag.Lookup("test.run"); pattern == nil || pattern.Value.String() == "" {
t.Skip("launching the desktop app is opt-in: go test -run TestDesktopApp -v .")
}
home, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
serverExe := filepath.Join(home, exeName("casbin-gateway"))
desktopExe := filepath.Join(home, exeName("casbin-gateway-desktop"))
port := conf.GetHttpPort()
apiAddress := fmt.Sprintf("127.0.0.1:%d", port)
// Both executables are about to be rebuilt, so a server running from the
// old one has to go: Windows locks a running executable, and one still
// answering on the port is one the tray would attach to instead of
// starting the build under test.
stopped := ""
if _, err := os.Stat(serverExe); err == nil {
output, _ := exec.Command(serverExe, "stop").CombinedOutput()
stopped = strings.TrimSpace(string(output))
if stopped != "" {
t.Log(stopped)
}
}
// Anything still on the port is something "stop" does not recognize as
// Gateway. The tray would attach to it and show its UI, which looks like a
// working desktop app running code that is not the one just built.
if isServing(apiAddress) {
t.Fatalf("port %d is still in use, so the build under test cannot serve it: quit what holds it, or run with httpport=<free port>\n%s", port, stopped)
}
goBuild(t, home, serverExe)
goBuild(t, filepath.Join(home, "desktop"), desktopExe)
url := strings.TrimSpace(os.Getenv("CASBIN_GATEWAY_URL"))
if url == "" {
url = startDevUi(t, home, port)
}
cmd := exec.Command(desktopExe)
cmd.Dir = home
cmd.Env = append(os.Environ(), "CASBIN_GATEWAY_URL="+url)
cmd.Stdout, cmd.Stderr = logWriter(t, desktopLogPath)
if err := cmd.Start(); err != nil {
t.Fatal(err)
}
waitForServing(t, "the desktop app", cmd, apiAddress, desktopLogPath)
t.Logf("Casbin Gateway is running: UI %s, API http://%s (desktop pid %d, log %s)",
url, apiAddress, cmd.Process.Pid, desktopLogPath)
t.Log("it keeps running after this test; quit it from the tray icon")
}
// startDevUi brings up "yarn dev" and returns the URL the window should show.
// The dev server proxies /api and /v1 to the Gateway this run starts, which is
// what keeps one origin — and therefore the session cookie — for the window.
func startDevUi(t *testing.T, home string, backendPort int) string {
t.Helper()
url := "http://" + devUiAddress
if isServing(devUiAddress) {
t.Logf("reusing the dev server already on %s", url)
if backendPort != 17000 {
t.Logf("it proxies the API to whatever backend it was started with, not necessarily port %d", backendPort)
}
return url
}
webDir := filepath.Join(home, "web")
if _, err := os.Stat(filepath.Join(webDir, "node_modules")); err != nil {
t.Fatal("the frontend dependencies are missing: cd web && yarn install")
}
// --strictPort so that a port already taken is an error here rather than a
// dev server quietly moving to the next one and a window showing nothing.
cmd := exec.Command("yarn", "dev", "--strictPort")
cmd.Dir = webDir
cmd.Env = append(os.Environ(), fmt.Sprintf("VITE_BACKEND_URL=http://127.0.0.1:%d", backendPort))
cmd.Stdout, cmd.Stderr = logWriter(t, devUiLogPath)
if err := cmd.Start(); err != nil {
t.Fatalf("could not start the dev server: %v", err)
}
waitForServing(t, "the dev server", cmd, devUiAddress, devUiLogPath)
t.Logf("dev server on %s (log %s)", url, devUiLogPath)
return url
}
// waitForServing blocks until the child answers, watching it for an early exit
// so that a server that failed to start reports its own reason instead of a
// timeout.
func waitForServing(t *testing.T, what string, cmd *exec.Cmd, address string, logPath string) {
t.Helper()
exited := make(chan error, 1)
go func() { exited <- cmd.Wait() }()
deadline := time.Now().Add(startWait)
for time.Now().Before(deadline) {
select {
case err := <-exited:
t.Fatalf("%s exited (%v)\n%s", what, err, logTail(logPath))
default:
}
if isServing(address) {
return
}
time.Sleep(startPoll)
}
t.Fatalf("%s did not answer on %s within %v\n%s", what, address, startWait, logTail(logPath))
}
func exeName(name string) string {
if runtime.GOOS == "windows" {
return name + ".exe"
}
return name
}
func goBuild(t *testing.T, dir string, output string) {
t.Helper()
cmd := exec.Command("go", "build", "-o", output, ".")
cmd.Dir = dir
if out, err := cmd.CombinedOutput(); err != nil {
hint := ""
if runtime.GOOS == "windows" {
hint = " (Windows locks a running executable: quit Casbin Gateway from its tray icon first)"
}
t.Fatalf("building %s failed%s: %v\n%s", dir, hint, err, out)
}
t.Logf("built %s", output)
}
// logWriter is where a child that outlives this test writes. The file is closed
// here because Start() hands the child its own handle.
func logWriter(t *testing.T, path string) (*os.File, *os.File) {
t.Helper()
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
t.Fatal(err)
}
file, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o600)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { file.Close() })
return file, file
}
// isServing probes an address rather than a port because the two servers here
// do not bind the same one: Gateway listens on 127.0.0.1, while the dev server
// listens on whatever "localhost" resolves to, which on Windows is IPv6 first.
func isServing(address string) bool {
conn, err := net.DialTimeout("tcp", address, time.Second)
if err != nil {
return false
}
conn.Close()
return true
}
func logTail(path string) string {
data, err := os.ReadFile(path)
if err != nil {
return ""
}
lines := strings.Split(strings.TrimSpace(string(data)), "\n")
if len(lines) > 20 {
lines = lines[len(lines)-20:]
}
return strings.Join(lines, "\n")
}