1package cef
2
3import (
4 "bytes"
5 "os"
6 "path/filepath"
7 "testing"
8
9 "github.com/bnema/dumber/internal/application/port"
10 "github.com/bnema/dumber/internal/infrastructure/audio/factory"
11 "github.com/bnema/dumber/internal/infrastructure/runtimeprofile"
12 "github.com/rs/zerolog"
13 "github.com/stretchr/testify/require"
14)
15
16func TestPrepareCEFSettings_IsolatedRootOverridesInheritedSession(t *testing.T) {
17 t.Setenv(CEFRootCachePathEnvVar, "/normal-session")
18 paths := RuntimePaths{StateRoot: "/normal", IsolatedRoot: "/tmp/instance/cef"}
19 opts := port.EngineOptions{CacheDir: "/cache", DataDir: "/data"}
20 logger := zerolog.Nop()
21 settings, err := prepareCEFSettings(opts, paths, RuntimeConfig{}, &logger)
22 require.NoError(t, err)
23 require.Equal(t, paths.IsolatedRoot, settings.RootCachePath)
24 require.Equal(t, settings.RootCachePath, paths.stateRoot(opts))
25 require.Equal(t, settings.RootCachePath, settings.CachePath)
26 require.Equal(t, "/normal-session", os.Getenv(CEFRootCachePathEnvVar))
27}
28
29func TestResolvedStateRoot_PrefersCacheDir(t *testing.T) {
30 profile := testCEFDevProfile(t)
31 root := resolvedStateRoot(profile.CEFUserDataDir(), port.EngineOptions{DataDir: "/tmp/data", CacheDir: "/tmp/cache"})
32 require.Equal(t, "/tmp/cache", root)
33}
34
35func TestResolvedStateRoot_UsesEnvOverride(t *testing.T) {
36 profile := testCEFDevProfile(t)
37 t.Setenv(CEFRootCachePathEnvVar, "/tmp/override")
38
39 root := resolvedStateRoot(profile.CEFUserDataDir(), port.EngineOptions{})
40 require.Equal(t, "/tmp/override", root)
41
42 root = resolvedStateRoot(profile.CEFUserDataDir(), port.EngineOptions{DataDir: "/tmp/data", CacheDir: "/tmp/cache"})
43 require.Equal(t, "/tmp/override", root)
44}
45
46func TestResolvedStateRoot_UsesProfileDefault(t *testing.T) {
47 profile := testCEFProdProfile(t)
48 root := resolvedStateRoot(profile.CEFUserDataDir(), port.EngineOptions{})
49 require.Equal(t, profile.CEFUserDataDir(), root)
50}
51
52func TestConfigureCEFSubprocessRuntime_PropagatesConfiguredRuntimeBeforeInit(t *testing.T) {
53 t.Setenv("CEF_DIR", "")
54
55 runtime := filepath.Join(t.TempDir(), "cef-147")
56 require.Equal(t, runtime, configureCEFSubprocessRuntime(runtime))
57 require.Equal(t, runtime, os.Getenv("CEF_DIR"))
58}
59
60func TestConfigureCEFSubprocessRuntime_PreservesExplicitEnvironmentOverride(t *testing.T) {
61 explicitRuntime := filepath.Join(t.TempDir(), "cef-env")
62 t.Setenv("CEF_DIR", explicitRuntime)
63
64 require.Equal(t, explicitRuntime, configureCEFSubprocessRuntime(filepath.Join(t.TempDir(), "cef-config")))
65 require.Equal(t, explicitRuntime, os.Getenv("CEF_DIR"))
66}
67
68func TestPrepareCEFSettings_UsesResolvedProfilePaths(t *testing.T) {
69 logger := zerolog.Nop()
70 profile := testCEFDevProfile(t)
71 settings, err := prepareCEFSettings(port.EngineOptions{}, RuntimePaths{StateRoot: profile.CEFUserDataDir(), LogFile: profile.CEFLogFile()}, RuntimeConfig{}, &logger)
72 require.NoError(t, err)
73 require.Equal(t, profile.CEFUserDataDir(), settings.RootCachePath)
74 require.Equal(t, profile.CEFLogFile(), settings.LogFile)
75 require.Empty(t, settings.BrowserSubprocessPath)
76}
77
78// TestPrepareCEFSettings_ConfiguresPersistentGlobalRequestContext locks the
79// purego-cef contract needed for profile cookies and localStorage.
80func TestPrepareCEFSettings_ConfiguresPersistentGlobalRequestContext(t *testing.T) {
81 logger := zerolog.Nop()
82 profile := testCEFDevProfile(t)
83 settings, err := prepareCEFSettings(port.EngineOptions{}, RuntimePaths{StateRoot: profile.CEFUserDataDir(), LogFile: profile.CEFLogFile()}, RuntimeConfig{}, &logger)
84 require.NoError(t, err)
85 require.NotEmpty(t, settings.RootCachePath)
86 require.Equal(t, settings.RootCachePath, settings.CachePath)
87}
88
89func TestParseLoadedLibCEFPath(t *testing.T) {
90 maps := "7f5fd4000000-7f5fd4200000 r--p 00000000 00:00 0 /usr/lib/libvulkan.so.1\n" +
91 "7f5fe0000000-7f5fe8200000 r-xp 00000000 00:00 0 /opt/cef-vaapi-runtime/usr/lib/cef/libcef.so\n" +
92 "7f5fe8200000-7f5fe8400000 r--p 08200000 00:00 0 /opt/cef-vaapi-runtime/usr/lib/cef/libcef.so\n"
93
94 require.Equal(t, "/opt/cef-vaapi-runtime/usr/lib/cef/libcef.so", parseLoadedLibCEFPath(maps))
95}
96
97func TestParseLoadedLibCEFPath_ReturnsEmptyWhenAbsent(t *testing.T) {
98 require.Empty(t, parseLoadedLibCEFPath("7f5fd4000000-7f5fd4200000 r--p 00000000 00:00 0 /usr/lib/libvulkan.so.1\n"))
99}
100
101func TestPrepareCEFSettings_RejectsNonDefaultCookiePolicy(t *testing.T) {
102 logger := zerolog.Nop()
103 profile := testCEFDevProfile(t)
104 _, err := prepareCEFSettings(port.EngineOptions{CookiePolicy: port.CookiePolicyNever}, RuntimePaths{StateRoot: profile.CEFUserDataDir(), LogFile: profile.CEFLogFile()}, RuntimeConfig{}, &logger)
105 require.ErrorIs(t, err, ErrCookiePolicyUnsupported)
106}
107
108func TestPrepareCEFSettings_AllowsNoThirdPartyCookiePolicy(t *testing.T) {
109 logger := zerolog.Nop()
110 profile := testCEFDevProfile(t)
111 _, err := prepareCEFSettings(port.EngineOptions{CookiePolicy: port.CookiePolicyNoThirdParty}, RuntimePaths{StateRoot: profile.CEFUserDataDir(), LogFile: profile.CEFLogFile()}, RuntimeConfig{}, &logger)
112 require.NoError(t, err)
113}
114
115func TestResolveCEFInitTraceFile_EmptyWithoutLogPath(t *testing.T) {
116 require.Empty(t, resolveCEFInitTraceFile("", ""))
117}
118
119func TestPrepareCEFLogFile_CreatesPrivateDirectoryAndFile(t *testing.T) {
120 logFile := filepath.Join(t.TempDir(), "private", "cef_runtime.log")
121
122 path, err := prepareCEFLogFile("", logFile)
123 require.NoError(t, err)
124 require.Equal(t, logFile, path)
125
126 dirInfo, err := os.Stat(filepath.Dir(logFile))
127 require.NoError(t, err)
128 require.Equal(t, os.FileMode(dirPerm), dirInfo.Mode().Perm())
129
130 fileInfo, err := os.Stat(logFile)
131 require.NoError(t, err)
132 require.Equal(t, os.FileMode(filePerm), fileInfo.Mode().Perm())
133}
134
135func TestPrepareCEFLogFile_TightensExistingFilePermissions(t *testing.T) {
136 logFile := filepath.Join(t.TempDir(), "private", "cef_runtime.log")
137 require.NoError(t, os.MkdirAll(filepath.Dir(logFile), dirPerm))
138 require.NoError(t, os.WriteFile(logFile, []byte("existing"), 0o644))
139
140 path, err := prepareCEFLogFile("", logFile)
141 require.NoError(t, err)
142 require.Equal(t, logFile, path)
143
144 fileInfo, err := os.Stat(logFile)
145 require.NoError(t, err)
146 require.Equal(t, os.FileMode(filePerm), fileInfo.Mode().Perm())
147}
148
149func TestPrepareCEFLogFile_RejectsSymlink(t *testing.T) {
150 target := filepath.Join(t.TempDir(), "target.log")
151 require.NoError(t, os.WriteFile(target, []byte("existing"), 0o644))
152
153 logFile := filepath.Join(t.TempDir(), "private", "cef_runtime.log")
154 require.NoError(t, os.MkdirAll(filepath.Dir(logFile), dirPerm))
155 require.NoError(t, os.Symlink(target, logFile))
156
157 path, err := prepareCEFLogFile("", logFile)
158 require.ErrorContains(t, err, "must not be a symlink")
159 require.Empty(t, path)
160
161 targetInfo, statErr := os.Stat(target)
162 require.NoError(t, statErr)
163 require.Equal(t, os.FileMode(0o644), targetInfo.Mode().Perm())
164}
165
166func TestPrepareCEFLogFile_RejectsDirectoryPath(t *testing.T) {
167 logFile := filepath.Join(t.TempDir(), "private", "cef_runtime.log")
168 require.NoError(t, os.MkdirAll(logFile, dirPerm))
169
170 path, err := prepareCEFLogFile("", logFile)
171 require.ErrorContains(t, err, "not a regular file")
172 require.Empty(t, path)
173}
174
175func TestResolveCEFInitTraceFile_UsesExplicitLogFile(t *testing.T) {
176 logFile := filepath.Join(t.TempDir(), "custom", "cef_runtime.log")
177
178 path := resolveCEFInitTraceFile("", logFile)
179 require.Equal(t, filepath.Join(filepath.Dir(logFile), "cef_runtime.bootstrap.log"), path)
180}
181
182func TestResolveCEFInitTraceFile_UsesResolvedProfileLogDirByDefault(t *testing.T) {
183 profile := testCEFDevProfile(t)
184
185 path := resolveCEFInitTraceFile(profile.CEFLogFile(), "")
186 require.Equal(t, filepath.Join(profile.EnginePaths.LogDir, "cef_runtime.bootstrap.log"), path)
187}
188
189func TestPrepareCEFSettings_IgnoresInitTraceRequestWithoutTouchingFilesystem(t *testing.T) {
190 t.Setenv(puregoCEFInitTraceEnvVar, "1")
191 profile := testCEFDevProfile(t)
192 bootstrapLogFile := resolveCEFInitTraceFile(profile.CEFLogFile(), "")
193
194 var logs bytes.Buffer
195 logger := zerolog.New(&logs)
196
197 settings, err := prepareCEFSettings(
198 port.EngineOptions{},
199 RuntimePaths{StateRoot: profile.CEFUserDataDir(), LogFile: profile.CEFLogFile()},
200 RuntimeConfig{},
201 &logger,
202 )
203 require.NoError(t, err)
204 require.Equal(t, profile.CEFLogFile(), settings.LogFile)
205
206 _, statErr := os.Stat(bootstrapLogFile)
207 require.ErrorIs(t, statErr, os.ErrNotExist)
208 require.Contains(t, logs.String(), puregoCEFInitTraceEnvVar)
209 require.Contains(t, logs.String(), bootstrapLogFile)
210}
211
212// TestCreateAudioOutputFactory_CreatesUsableFactory verifies that the audio
213// factory can be created and produces working streams. This tests the runtime
214// wiring decision to create the factory during CEF engine setup.
215func TestCreateAudioOutputFactory_ReturnsFactory(t *testing.T) {
216 audioFactory := factory.NewAudioOutputFactory()
217
218 require.NotNil(t, audioFactory, "Audio factory should not be nil")
219}
220
221func testCEFDevProfile(t *testing.T) runtimeprofile.Profile {
222 t.Helper()
223 cwd := t.TempDir()
224 runtimeDir := shortRuntimeDir(t)
225 t.Setenv("XDG_RUNTIME_DIR", runtimeDir)
226 profile, err := runtimeprofile.Resolve(runtimeprofile.ResolveInput{
227 Env: func(key string) string {
228 switch key {
229 case "ENV":
230 return "dev"
231 case "XDG_RUNTIME_DIR":
232 return runtimeDir
233 default:
234 return ""
235 }
236 },
237 Engine: "cef",
238 CWD: func() (string, error) { return cwd, nil },
239 })
240 require.NoError(t, err)
241 return profile
242}
243
244func shortRuntimeDir(t *testing.T) string {
245 t.Helper()
246
247 dir, err := os.MkdirTemp("", "dbr-")
248 require.NoError(t, err)
249 t.Cleanup(func() {
250 _ = os.RemoveAll(dir)
251 })
252 return dir
253}
254
255func testCEFProdProfile(t *testing.T) runtimeprofile.Profile {
256 t.Helper()
257 root := t.TempDir()
258 profile, err := runtimeprofile.Resolve(runtimeprofile.ResolveInput{
259 Env: func(string) string { return "" },
260 Engine: "cef",
261 CWD: func() (string, error) { return root, nil },
262 Base: runtimeprofile.BasePaths{
263 ConfigHome: filepath.Join(root, "config"),
264 DataHome: filepath.Join(root, "data"),
265 StateHome: filepath.Join(root, "state"),
266 CacheHome: filepath.Join(root, "cache"),
267 },
268 })
269 require.NoError(t, err)
270 return profile
271}