diff --git a/.github/workflows/webview-profiles.yml b/.github/workflows/webview-profiles.yml
deleted file mode 100644
index 85bb6d99..00000000
--- a/.github/workflows/webview-profiles.yml
+++ /dev/null
@@ -1,54 +0,0 @@
-name: WebView host profiles
-on:
- push:
- paths: ['go.mod', 'go.sum', 'internal/webview/**', 'pkg/download/extension_webview*', 'ui/flutter/third_party/**', 'ui/flutter/lib/app/rpc/webview*', 'ui/flutter/tool/webview_profile_check.dart', '.github/workflows/webview-profiles.yml']
- pull_request:
- paths: ['go.mod', 'go.sum', 'internal/webview/**', 'pkg/download/extension_webview*', 'ui/flutter/third_party/**', 'ui/flutter/lib/app/rpc/webview*', 'ui/flutter/tool/webview_profile_check.dart', '.github/workflows/webview-profiles.yml']
- workflow_dispatch:
-jobs:
- android-plugin:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v4
- - uses: actions/setup-java@v4
- with:
- distribution: temurin
- java-version: '17'
- - uses: subosito/flutter-action@v2
- with:
- flutter-version: '3.41.6'
- - run: flutter pub get
- working-directory: ui/flutter
- - uses: gradle/actions/setup-gradle@v4
- with:
- gradle-version: '8.14'
- - name: Compile Android native plugin
- working-directory: ui/flutter/android
- run: |
- printf 'flutter.sdk=%s\nsdk.dir=%s\n' "$FLUTTER_ROOT" "$ANDROID_HOME" > local.properties
- gradle :flutter_inappwebview_android:compileDebugJavaWithJavac
- apple-plugin:
- runs-on: macos-14
- steps:
- - uses: actions/checkout@v4
- - uses: actions/setup-go@v5
- with:
- go-version: '1.25.4'
- - uses: subosito/flutter-action@v2
- with:
- flutter-version: '3.41.6'
- - name: Build Go host
- run: go build -buildmode=c-shared -o ui/flutter/macos/Frameworks/libgopeed.dylib ./bind/desktop
- - name: Check native macOS profiles
- working-directory: ui/flutter
- run: |
- flutter pub get
- flutter build macos --debug -t tool/webview_profile_check.dart
- build/macos/Build/Products/Debug/Gopeed.app/Contents/MacOS/Gopeed
- - name: Compile iOS native plugin
- working-directory: ui/flutter
- run: |
- flutter build ios --simulator --config-only
- cd ios
- pod install
- xcodebuild -project Pods/Pods.xcodeproj -target flutter_inappwebview_ios -sdk iphonesimulator -configuration Debug CODE_SIGNING_ALLOWED=NO build
diff --git a/internal/webview/README.md b/internal/webview/README.md
index 5e849659..a644bd7f 100644
--- a/internal/webview/README.md
+++ b/internal/webview/README.md
@@ -46,10 +46,14 @@ ignoring the proxy.
- `go test -race ./internal/webview/proxy ./pkg/download/engine/webview`
- `go test ./pkg/download -run 'TestWebViewProfileBound|TestDownloader_ExtensionRuntimeWebView'`
-- In webview_go: `go run ./examples/profilecheck` (Linux: `xvfb-run -a ...`).
-- In ui/flutter: `flutter run -d macos -t tool/webview_profile_check.dart`.
+- Windows/Linux: `go test -tags webview -v ./internal/webview/goprovider` (Linux: run under `xvfb-run -a`).
+- macOS: start the normal Flutter app, then run `go test -tags webview -v ./internal/webview/rpcprovider` with its socket address. See `rpcprovider/README.md`.
+- In webview_go: `go test -tags webview_integration -v -count=1 .` (Linux: run under `xvfb-run -a`).
-The native smoke tests cover routing and persistent profile isolation. The Flutter
-smoke test also checks concurrent pages, HttpOnly cookies, LocalStorage, IndexedDB,
-profile-scoped clearing, and reopening. The self-signed TLS fixture is trusted only
-by that test's callback; production certificate validation is unchanged.
+The shared provider contract covers HTTP/HTTPS proxy routing, concurrent pages,
+HttpOnly cookies, LocalStorage, IndexedDB, profile-scoped clearing, reopening,
+and removal without affecting another profile. HTTPS routing is checked with a
+rejecting upstream tunnel; no test-only certificate bypass is added to the app.
+Go proxy unit tests separately cover successful TLS forwarding and authentication.
+These cases run in the existing `test.yml` WebView jobs; existing mobile build jobs
+compile the Android/iOS bridge. No separate Flutter test application is required.
diff --git a/internal/webview/goprovider/provider_integration_test.go b/internal/webview/goprovider/provider_integration_test.go
index 92f226e7..e6d9e63e 100644
--- a/internal/webview/goprovider/provider_integration_test.go
+++ b/internal/webview/goprovider/provider_integration_test.go
@@ -13,3 +13,5 @@ func TestProviderContract(t *testing.T) {
CookieDomainMode: integrationtest.CookieDomainModeRequired,
})
}
+
+func TestProviderProfiles(t *testing.T) { integrationtest.RunProfileContract(t, New()) }
diff --git a/internal/webview/integrationtest/contract.go b/internal/webview/integrationtest/contract.go
index 4116a211..364fe5c2 100644
--- a/internal/webview/integrationtest/contract.go
+++ b/internal/webview/integrationtest/contract.go
@@ -248,7 +248,14 @@ func openTestPage(t *testing.T, provider enginewebview.Provider, opts enginewebv
t.Fatalf("failed to open webview: %v", err)
}
t.Cleanup(func() {
- _ = runtime.Close()
+ if err := runtime.Close(); err != nil {
+ t.Errorf("close contract page: %v", err)
+ }
+ if remover, ok := provider.(enginewebview.ProfileRemover); ok {
+ if err := remover.RemoveProfile(profileID, path); err != nil {
+ t.Errorf("remove contract profile: %v", err)
+ }
+ }
})
return page, server, server.URL
diff --git a/internal/webview/integrationtest/profiles.go b/internal/webview/integrationtest/profiles.go
new file mode 100644
index 00000000..101a23a3
--- /dev/null
+++ b/internal/webview/integrationtest/profiles.go
@@ -0,0 +1,234 @@
+package integrationtest
+
+import (
+ "crypto/sha256"
+ "fmt"
+ "io"
+ "net"
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+ "path/filepath"
+ "reflect"
+ "runtime"
+ "sync/atomic"
+ "testing"
+ "time"
+
+ webviewproxy "github.com/GopeedLab/gopeed/internal/webview/proxy"
+ webview "github.com/GopeedLab/gopeed/pkg/download/engine/webview"
+)
+
+// RunProfileContract exercises the same host API through both native and RPC
+// providers. It never bypasses the host bridge with direct Flutter plugin calls.
+func RunProfileContract(t *testing.T, provider webview.Provider) {
+ t.Helper()
+ waitForProviderAvailable(t, provider, defaultAvailabilityTimeout)
+ remover, ok := provider.(webview.ProfileRemover)
+ if !ok {
+ t.Fatal("provider does not implement host profile removal")
+ }
+ proxyURL, secureHits := profileProxy(t)
+ a := newContractProfile(t, provider, remover, proxyURL)
+ b := newContractProfile(t, provider, remover, proxyURL)
+ first := a.open(t)
+ assertProfileValue(t, first, `async () => { localStorage.setItem('owner','a'); return 'a'; }`, "a")
+ assertProfileValue(t, first, profileDatabaseScript(true), "saved")
+ if err := first.SetCookie(webview.Cookie{Name: "session", Value: "a", Domain: "profiles.invalid", Path: "/", HTTPOnly: true, Expires: time.Now().Add(time.Hour)}); err != nil {
+ t.Fatal(err)
+ }
+ second := b.open(t)
+ assertProfileValue(t, second, `() => localStorage.getItem('owner')`, nil)
+ assertProfileValue(t, second, profileDatabaseScript(false), nil)
+ assertProfileCookies(t, second, "")
+ assertProfileValue(t, second, `() => { localStorage.setItem('owner','b'); return 'b'; }`, "b")
+ if err := second.SetCookie(webview.Cookie{Name: "session", Value: "b", Domain: "profiles.invalid", Path: "/", HTTPOnly: true}); err != nil {
+ t.Fatal(err)
+ }
+ shared := a.open(t)
+ assertProfileValue(t, shared, `() => localStorage.getItem('owner')`, "a")
+ assertProfileValue(t, shared, profileDatabaseScript(false), "saved")
+ assertProfileCookies(t, shared, "a")
+ assertProfileValue(t, shared, `() => document.cookie`, "") // HttpOnly stays hidden.
+ if err := second.ClearCookies(); err != nil {
+ t.Fatal(err)
+ }
+ assertProfileCookies(t, first, "a")
+ if err := second.SetCookie(webview.Cookie{Name: "session", Value: "b", Domain: "profiles.invalid", Path: "/", HTTPOnly: true}); err != nil {
+ t.Fatal(err)
+ }
+ a.close(t)
+ reopened := a.open(t)
+ assertProfileValue(t, reopened, `() => localStorage.getItem('owner')`, "a")
+ assertProfileValue(t, reopened, profileDatabaseScript(false), "saved")
+ assertProfileCookies(t, reopened, "a")
+ // Include a session cookie immediately before deletion to catch stale
+ // in-process CookieStore caches when the same profile is recreated.
+ if err := reopened.SetCookie(webview.Cookie{Name: "session", Value: "remove-me", Domain: "profiles.invalid", Path: "/", HTTPOnly: true}); err != nil {
+ t.Fatal(err)
+ }
+ a.close(t)
+ a.remove(t)
+ a.remove(t) // Retrying a successful uninstall is harmless.
+ reinstalled := a.open(t)
+ assertProfileValue(t, reinstalled, `() => localStorage.getItem('owner')`, nil)
+ assertProfileValue(t, reinstalled, profileDatabaseScript(false), nil)
+ assertProfileCookies(t, reinstalled, "")
+ assertProfileValue(t, second, `() => localStorage.getItem('owner')`, "b")
+ assertProfileCookies(t, second, "b")
+ // Refuse the HTTPS tunnel deliberately. Seeing it at our proxy proves HTTPS
+ // routing without adding a certificate-validation bypass to the test host.
+ if err := reinstalled.Goto("https://profiles.invalid/", webview.GotoOptions{TimeoutMS: 5000}); err == nil {
+ t.Fatal("HTTPS unexpectedly succeeded through a rejecting proxy")
+ }
+ if secureHits.Load() == 0 {
+ t.Fatal("HTTPS bypassed the host proxy")
+ }
+}
+
+type contractProfile struct {
+ provider webview.Provider
+ remover webview.ProfileRemover
+ options webview.OpenOptions
+ pages []webview.Page
+}
+
+func newContractProfile(t *testing.T, provider webview.Provider, remover webview.ProfileRemover, proxyURL string) *contractProfile {
+ t.Helper()
+ path := filepath.Join(t.TempDir(), "profile")
+ hash := sha256.Sum256([]byte(path))
+ id := fmt.Sprintf("%x-%x-%x-%x-%x", hash[:4], hash[4:6], hash[6:8], hash[8:10], hash[10:16])
+ p := &contractProfile{provider: provider, remover: remover, options: webview.OpenOptions{ProfileID: id, DataPath: path, ProxyURL: proxyURL, Headless: true, Title: "Gopeed profile contract"}}
+ t.Cleanup(func() { p.close(t); p.remove(t) })
+ return p
+}
+func (p *contractProfile) open(t *testing.T) webview.Page {
+ t.Helper()
+ page, err := p.provider.Open(p.options)
+ if err != nil {
+ t.Fatal(err)
+ }
+ p.pages = append(p.pages, page)
+ if err := page.Goto("http://profiles.invalid/", webview.GotoOptions{TimeoutMS: 10000}); err != nil {
+ t.Fatal(err)
+ }
+ return page
+}
+func (p *contractProfile) close(t *testing.T) {
+ t.Helper()
+ for _, page := range p.pages {
+ if err := page.Close(); err != nil {
+ t.Errorf("close profile page: %v", err)
+ }
+ }
+ p.pages = nil
+}
+func (p *contractProfile) remove(t *testing.T) {
+ t.Helper()
+ if err := p.remover.RemoveProfile(p.options.ProfileID, p.options.DataPath); err != nil {
+ t.Errorf("remove profile: %v", err)
+ }
+}
+func assertProfileValue(t *testing.T, page webview.Page, script string, want any) {
+ t.Helper()
+ got, err := page.Execute(script)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !reflect.DeepEqual(got, want) {
+ t.Fatalf("script %s: got %#v, want %#v", script, got, want)
+ }
+}
+func assertProfileCookies(t *testing.T, page webview.Page, want string) {
+ t.Helper()
+ cookies, err := page.GetCookies()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if want == "" {
+ if len(cookies) != 0 {
+ t.Fatalf("profile cookies survived removal: %#v", cookies)
+ }
+ return
+ }
+ if len(cookies) != 1 || cookies[0].Name != "session" || cookies[0].Value != want || !cookies[0].HTTPOnly {
+ t.Fatalf("unexpected profile cookie store: %#v", cookies)
+ }
+}
+func profileDatabaseScript(write bool) string {
+ operation := "store.get('owner')"
+ if write {
+ operation = "store.put('saved','owner')"
+ }
+ return fmt.Sprintf(`async () => await new Promise((resolve,reject)=>{
+ const request=indexedDB.open('profile-contract',1);
+ request.onupgradeneeded=()=>request.result.createObjectStore('state');
+ request.onerror=()=>reject(String(request.error));
+ request.onsuccess=()=>{
+ const db=request.result, tx=db.transaction('state','readwrite'), store=tx.objectStore('state');
+ const op=%s; let value;
+ op.onsuccess=()=>value=%s;
+ tx.oncomplete=()=>{db.close();resolve(value===undefined?null:value)};
+ tx.onerror=()=>{db.close();reject(String(tx.error))};
+ };
+})`, operation, map[bool]string{true: "'saved'", false: "op.result"}[write])
+}
+
+func profileProxy(t *testing.T) (string, *atomic.Int32) {
+ t.Helper()
+ origin := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "text/html")
+ w.Header().Set("Cache-Control", "no-store")
+ fmt.Fprint(w, "
Profile contract")
+ }))
+ t.Cleanup(origin.Close)
+ secureHits := &atomic.Int32{}
+ upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodConnect {
+ origin.Config.Handler.ServeHTTP(w, r)
+ return
+ }
+ if r.Host == "profiles.invalid:443" {
+ secureHits.Add(1)
+ http.Error(w, "HTTPS routing fixture rejects tunnels", http.StatusBadGateway)
+ return
+ }
+ if r.Host != "profiles.invalid:80" {
+ http.Error(w, "unexpected target", http.StatusBadGateway)
+ return
+ }
+ remote, err := net.DialTimeout("tcp", origin.Listener.Addr().String(), 5*time.Second)
+ if err != nil {
+ http.Error(w, "origin unavailable", http.StatusBadGateway)
+ return
+ }
+ defer remote.Close()
+ client, buffer, err := w.(http.Hijacker).Hijack()
+ if err != nil {
+ return
+ }
+ defer client.Close()
+ if _, err := buffer.WriteString("HTTP/1.1 200 Connection Established\r\n\r\n"); err != nil {
+ return
+ }
+ if err := buffer.Flush(); err != nil {
+ return
+ }
+ go func() { io.Copy(remote, buffer); remote.Close() }()
+ io.Copy(client, remote)
+ }))
+ t.Cleanup(upstream.Close)
+ upstreamURL, err := url.Parse(upstream.URL)
+ if err != nil {
+ t.Fatal(err)
+ }
+ proxy, err := webviewproxy.New(func(*http.Request) (*url.URL, error) { return upstreamURL, nil })
+ if err != nil {
+ t.Fatal(err)
+ }
+ t.Cleanup(func() { proxy.Close() })
+ if runtime.GOOS == "darwin" {
+ return proxy.SOCKSURL(), secureHits
+ }
+ return proxy.URL(), secureHits
+}
diff --git a/internal/webview/rpcprovider/provider_integration_test.go b/internal/webview/rpcprovider/provider_integration_test.go
index 6ade9458..5f26ca56 100644
--- a/internal/webview/rpcprovider/provider_integration_test.go
+++ b/internal/webview/rpcprovider/provider_integration_test.go
@@ -37,3 +37,8 @@ func defaultWebViewRPCAddress() string {
}
return filepath.Join(homeDir, "Library", "Application Support", "com.gopeed.gopeed", "gopeed_webview.sock")
}
+
+func TestProviderProfiles(t *testing.T) {
+ provider := New(enginewebview.RPCConfig{Network: *webViewRPCNetwork, Address: *webViewRPCAddress, Token: *webViewRPCToken})
+ integrationtest.RunProfileContract(t, provider)
+}
diff --git a/ui/flutter/third_party/README.md b/ui/flutter/third_party/README.md
index af35170b..9a0d8cc6 100644
--- a/ui/flutter/third_party/README.md
+++ b/ui/flutter/third_party/README.md
@@ -27,7 +27,8 @@ Android requires `MULTI_PROFILE` and `PROXY_OVERRIDE`; unsupported runtimes retu
an explicit unavailable error rather than opening a shared, unproxied browser.
When updating upstream, refresh these three packages, reapply the above changes,
-and run `tool/webview_profile_check.dart` on a native Apple host. Do not patch the
+and run the shared Go provider integration tests against the normal Flutter app
+on a native Apple host (see `internal/webview/rpcprovider/README.md`). Do not patch the
user's pub cache: dependency_overrides selects these tracked sources reproducibly.
Android uses AndroidX WebKit 1.13.0 for profile-scoped full browsing-data deletion.
diff --git a/ui/flutter/tool/webview_profile_check.dart b/ui/flutter/tool/webview_profile_check.dart
deleted file mode 100644
index 2a84af54..00000000
--- a/ui/flutter/tool/webview_profile_check.dart
+++ /dev/null
@@ -1,271 +0,0 @@
-// Real native smoke test: flutter run -d macos -t tool/webview_profile_check.dart
-import 'dart:async';
-import 'dart:io';
-import 'dart:convert';
-import 'package:flutter/material.dart';
-import 'package:flutter_inappwebview/flutter_inappwebview.dart';
-import 'package:gopeed/app/rpc/webview_profile.dart';
-
-Future main() async {
- WidgetsFlutterBinding.ensureInitialized();
- runApp(const MaterialApp(home: SizedBox()));
- try {
- await checkProfiles();
- stdout.writeln(
- 'PASS: native proxy, concurrent profiles, cookies, LocalStorage, IndexedDB, reopening and profile deletion',
- );
- exit(0);
- } catch (error, stack) {
- stderr.writeln('FAIL: $error\n$stack');
- exit(1);
- }
-}
-
-void expectValue(dynamic actual, dynamic expected) {
- if (actual != expected) throw StateError('Expected $expected, got $actual');
-}
-
-Future checkProfiles() async {
- final context = SecurityContext()
- ..useCertificateChainBytes(utf8.encode(fixtureCertificate))
- ..usePrivateKeyBytes(utf8.encode(fixtureKey));
- final origin = await HttpServer.bindSecure(InternetAddress.loopbackIPv4, 0, context);
- origin.listen((request) async {
- request.response.headers.contentType = ContentType.html;
- request.response.write('profile fixture');
- await request.response.close();
- });
- final plainOrigin = await HttpServer.bind(InternetAddress.loopbackIPv4, 0);
- plainOrigin.listen((request) async {
- request.response.headers.contentType = ContentType.html;
- request.response.write('profile fixture');
- await request.response.close();
- });
- var proxyHits = 0;
- final proxy = await ServerSocket.bind(InternetAddress.loopbackIPv4, 0);
- proxy.listen((socket) async {
- final input = StreamIterator(socket);
- final buffer = [];
- Future> read(int count) async {
- while (buffer.length < count) {
- if (!await input.moveNext()) throw StateError('Incomplete SOCKS handshake');
- buffer.addAll(input.current);
- }
- final value = buffer.sublist(0, count);
- buffer.removeRange(0, count);
- return value;
- }
-
- try {
- final hello = await read(2);
- await read(hello[1]);
- socket.add([5, 0]);
- await socket.flush();
- final header = await read(4);
- if (header[3] == 1) {
- await read(4);
- } else if (header[3] == 4) {
- await read(16);
- } else {
- final size = await read(1);
- await read(size[0]);
- }
- final port = await read(2);
- final targetPort = port[0] * 256 + port[1];
- proxyHits++;
- final remote = await Socket.connect(
- InternetAddress.loopbackIPv4,
- targetPort == 443 ? origin.port : plainOrigin.port,
- );
- socket.add([5, 0, 0, 1, 127, 0, 0, 1, 0, 0]);
- await socket.flush();
- remote.listen(socket.add, onDone: socket.destroy, onError: (_) => socket.destroy());
- if (buffer.isNotEmpty) remote.add(buffer);
- while (await input.moveNext()) {
- remote.add(input.current);
- }
- remote.destroy();
- } finally {
- await input.cancel();
- socket.destroy();
- }
- });
- final suffix = DateTime.now().microsecondsSinceEpoch.toRadixString(16).padLeft(12, '0');
- final a = WebViewProfile('11111111-2222-3333-4444-${suffix.substring(suffix.length - 12)}');
- final b = WebViewProfile('22222222-2222-3333-4444-${suffix.substring(suffix.length - 12)}');
- final proxyUrl = 'socks5://127.0.0.1:${proxy.port}';
- await a.prepare(proxyUrl);
- await b.prepare(proxyUrl);
- final target = WebUri('https://profiles.invalid/');
- final pages = [];
- Future open(WebViewProfile profile) async {
- await profile.prepare(proxyUrl);
- final ready = Completer();
- final page = HeadlessInAppWebView(
- onReceivedServerTrustAuthRequest: (_, _) async =>
- ServerTrustAuthResponse(action: ServerTrustAuthResponseAction.PROCEED),
- initialSettings: WebViewProfileSettings(profileId: profile.id, debug: true),
- onWebViewCreated: (controller) {
- controller.loadUrl(urlRequest: URLRequest(url: target));
- },
- onLoadStop: (controller, url) {
- if (url?.host == target.host && !ready.isCompleted) ready.complete(controller);
- },
- onReceivedError: (_, _, error) {
- if (!ready.isCompleted) ready.completeError(StateError(error.description));
- },
- );
- pages.add(page);
- await page.run();
- return ready.future.timeout(const Duration(seconds: 20));
- }
-
- Future js(InAppWebViewController c, String source) => c.evaluateJavascript(source: source);
- Future database(InAppWebViewController c, bool write) async {
- final result = await c.callAsyncJavaScript(
- functionBody:
- '''
- return await new Promise((resolve,reject)=>{
- const request=indexedDB.open('profile-check',1);
- request.onupgradeneeded=()=>request.result.createObjectStore('state');
- request.onerror=()=>reject(String(request.error));
- request.onsuccess=()=>{
- const db=request.result;
- const tx=db.transaction('state','readwrite');
- const store=tx.objectStore('state');
- const op=${write ? "store.put('saved','owner')" : "store.get('owner')"};
- let value;
- op.onsuccess=()=>value=op.result;
- tx.oncomplete=()=>{db.close();resolve(value===undefined?null:value);};
- tx.onerror=()=>{db.close();reject(String(tx.error));};
- };
- });
- ''',
- );
- if (result?.error != null) throw StateError(result!.error.toString());
- return result?.value;
- }
-
- try {
- final plainReady = Completer();
- final plain = HeadlessInAppWebView(
- initialSettings: WebViewProfileSettings(profileId: a.id, debug: true),
- initialUrlRequest: URLRequest(url: WebUri('http://profiles.invalid/')),
- onLoadStop: (_, url) {
- if (url?.host == 'profiles.invalid' && !plainReady.isCompleted) plainReady.complete();
- },
- onReceivedError: (_, _, error) {
- if (!plainReady.isCompleted) plainReady.completeError(StateError(error.description));
- },
- );
- pages.add(plain);
- await plain.run();
- await plainReady.future.timeout(const Duration(seconds: 20));
- if (proxyHits == 0) throw StateError('HTTP bypassed proxy');
- final first = await open(a);
- await js(first, "localStorage.setItem('owner','saved')");
- await database(first, true);
- await a.setCookie(
- url: target,
- name: 'session',
- value: 'a',
- domain: 'profiles.invalid',
- isHttpOnly: true,
- expiresDate: DateTime.now().add(const Duration(days: 1)).millisecondsSinceEpoch,
- );
- final second = await open(b);
- expectValue(await js(second, "localStorage.getItem('owner')"), null);
- expectValue(await database(second, false), null);
- expectValue((await b.getCookies(url: target)).length, 0);
- final shared = await open(a);
- expectValue(await js(shared, "localStorage.getItem('owner')"), 'saved');
- expectValue(await database(shared, false), 'saved');
- expectValue((await a.getCookies(url: target)).single.value, 'a');
- await b.deleteAllCookies();
- expectValue((await a.getCookies(url: target)).single.value, 'a');
- for (final page in pages) {
- await page.dispose();
- }
- pages.clear();
- final reopened = await open(a);
- expectValue(await js(reopened, "localStorage.getItem('owner')"), 'saved');
- expectValue((await a.getCookies(url: target)).single.isHttpOnly, true);
- await a.deleteCookie(url: target, name: 'session', domain: 'profiles.invalid');
- expectValue((await a.getCookies(url: target)).length, 0);
- for (final page in pages) {
- await page.dispose();
- }
- pages.clear();
- final untouched = await open(b);
- await js(untouched, "localStorage.setItem('owner','other')");
- await a.setCookie(url: target, name: 'delete-me', value: 'secret', isHttpOnly: true);
- await a.remove();
- await a.remove(); // Uninstall retries must be safe.
- final reinstalled = await open(a);
- expectValue(await js(reinstalled, "localStorage.getItem('owner')"), null);
- expectValue(await database(reinstalled, false), null);
- expectValue((await a.getCookies(url: target)).length, 0);
- expectValue(await js(untouched, "localStorage.getItem('owner')"), 'other');
- if (proxyHits == 0) throw StateError('Browser bypassed proxy');
- } finally {
- for (final page in pages) {
- await page.dispose();
- }
- await a.remove();
- await b.remove();
- await proxy.close();
- await plainOrigin.close(force: true);
- await origin.close(force: true);
- }
-}
-
-// Self-signed test fixture; trusted only by the smoke-test callback.
-const fixtureCertificate = '''-----BEGIN CERTIFICATE-----
-MIIDFzCCAf+gAwIBAgIUTVTmWtXNVthZEKsowr8XXq+WPf0wDQYJKoZIhvcNAQEL
-BQAwGzEZMBcGA1UEAwwQcHJvZmlsZXMuaW52YWxpZDAeFw0yNjA5MTMxNjE4NTFa
-Fw0yNjA5MTYxNjE4NTFaMBsxGTAXBgNVBAMMEHByb2ZpbGVzLmludmFsaWQwggEi
-MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDM8AJ8i1fy++T4VjH1pyRo96Hw
-7yQnjAKhbh4kD4w145bc1RgmWgJLqSSZaegXKsn5Cw2FKxCjA0TyZCTGq+fkDp45
-ERWN2TACMWGyz/7ROuAOVfIo92AnDMpdinn+wn9DdWo8z9UEnzCjs613CLCXmGOQ
-9R/rTO2VlTlAM5VJygqOpBrtPzrx1ojJhL3fY9e5yYP0yxJqCf7ukCG/lrhHj5fK
-7twD+tJMkkGkEMHqavmmTPlD8TsUYKB2Qf9orRPQIxNZkbtG+DEzckRhUJDBprI/
-pKVwG680d/40AZ3NvAjbz8WjmWGj2gBkSC1NdQ1k6a6MFJOPGnKfDepMk4S7AgMB
-AAGjUzBRMB0GA1UdDgQWBBTwL0eACgwkw0isYAWpZwp1uqULWjAfBgNVHSMEGDAW
-gBTwL0eACgwkw0isYAWpZwp1uqULWjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3
-DQEBCwUAA4IBAQCplXaffZy1mYexyq744NFnIDBlnb3FLbBSG7tJt4SAY1dDysr9
-lNd8C5udrPxlmezUmlihzv8BqJcUNA0T9iYX6rOlNmD6xaaHs7akAGxvd8upIuM6
-iuKt3lXrE+xhVKGLs1XNYVOC+03H+sI1RxSOIpVPY43zSZXRXd8ADQ/+74KLsjIP
-uQ1qUPG59C36Tfmp8UBA4pb3inPyRxcpGAREbXeX1zMuyoOs4c80CsV26Jh+sQFZ
-VdSqtQrwzCLxb51D1MzeOakD69CWgM1hMIG12wqKvOz0d7uHLCXgIYLIYbaqgsF3
-FdpVXgGYktDl/GfVa2i9RLCZKUGOl2OuPFie
------END CERTIFICATE-----
-''';
-const fixtureKey = '''-----BEGIN PRIVATE KEY-----
-MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDM8AJ8i1fy++T4
-VjH1pyRo96Hw7yQnjAKhbh4kD4w145bc1RgmWgJLqSSZaegXKsn5Cw2FKxCjA0Ty
-ZCTGq+fkDp45ERWN2TACMWGyz/7ROuAOVfIo92AnDMpdinn+wn9DdWo8z9UEnzCj
-s613CLCXmGOQ9R/rTO2VlTlAM5VJygqOpBrtPzrx1ojJhL3fY9e5yYP0yxJqCf7u
-kCG/lrhHj5fK7twD+tJMkkGkEMHqavmmTPlD8TsUYKB2Qf9orRPQIxNZkbtG+DEz
-ckRhUJDBprI/pKVwG680d/40AZ3NvAjbz8WjmWGj2gBkSC1NdQ1k6a6MFJOPGnKf
-DepMk4S7AgMBAAECggEACWGTO5BFXmIZgSSw9MXJ7OQpDZZk2UqXdDxONOhhSrY4
-WbOHf3nvUPdVLZCjhyv2Qug2njnycQPdTBs5c766xr3EkGgvzGZ8xBzuk/jheUJw
-rzdP9oAcbq0vNkj4fKj6mmPtkQsSfTusRlIbRq+nJclcBW6zH1pOJeLG1CGIW/BG
-wskEc9/86434Kmmot14f/nIGHPBPEL7MgsoXtMoM7lm/bOPekxd481SOA9j2h8Wl
-wLVyFrtzTqDOeNBCZNBPpQeaWgBg4nSeX8TRUMAxDYhlMu0ENmYKp2LDfw6XXEii
-yJsO07JNfcHQNyFPkW/E840KAvj+RzEUkQ4VhczLgQKBgQDncicAlvRWhCD//Jd7
-1FFMx8zaz6hKRrbdI0zzpzIle3Ebe7zXL84KMzLSP3pwD/ABwktoEhQjTjvo/VCh
-pkvnqOgH8k9QUEyuzVhlhwC5RZOVfz3vnKTJMuCjsQ66OY1ljNVDWWLv5MkbVPlu
-wmziIWqdnitN7X0F9oHnnshuiQKBgQDireplA0+aZfQFsiWm/YSaQa/QJUVBkA0N
-La+C12O+dCr2n3gJsjVPteqVloJRymwpLtOW+3XWD4U4W7JJh3B+TNdtkgjT2jYF
-GK/29B/mspB9aCXuMimq1XAyFC27vSR6PhiYPdQJ+dWtACigRXTZvjTdC7EvxSsZ
-MNqjdO8oIwKBgHsXbATVQ/frZ72dcldqUR7buGi7Pk7akxDDYH7JclMa9sneIk3z
-38nu9t144z5MS6Iz2nTsAIfSaOx+JO8ECaSYYWcwiNw3CMGC9rtwdIMUrOw6cw9H
-qSBUjcKhPSrvxvK2VwnttT/O1uJRbCNrBguyKAjSAUf9wZt1QhuUiBa5AoGAc18p
-Sbhhr6fsh2mgmFm4P/rmzP3rPe9fdTGnfuS5s5nUtCl+IxE86REiEHjGY3KvklZv
-Aw2BcC3+FC03lQ0GSII7s1z8eTc6/2UNSpf+FKEVwX5cr9uAMZ7ot9RlYoRmKIGq
-avItFda+1oqaIti3HIwew9LLoBCuWufun/tILhkCgYACBuavVB5q0JW520hb5KeA
-ScD4+OLyxfKWYPvBNaIZLBP/lWuXQmxoXHvPRbxNjGQTldj1SbGS6yD5yvHs17vb
-cFF+y2NS4hTD6VMMnl5sZT9OQjEEgj5/nnq+TlcHljpQ3q+OQ7rtJJFt832ZAXLd
-ts2luZQYoJ+6aosodHGjCA==
------END PRIVATE KEY-----
-''';