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