From de11cd531aa30dd8cd366f2188be5b1078d9cef9 Mon Sep 17 00:00:00 2001 From: Ilyaas Kapadia <86218345+IlyaasK@users.noreply.github.com> Date: Mon, 27 Jul 2026 17:41:49 -0400 Subject: [PATCH] Add durable extension acceptance fixtures --- internal/acctest/acctest.go | 42 ++++++++++ internal/acctest/extension_test.go | 118 +++++++++++++++++++++++++++ internal/kernelclient/client.go | 8 ++ internal/kernelclient/client_test.go | 31 +++++++ 4 files changed, 199 insertions(+) create mode 100644 internal/acctest/extension_test.go diff --git a/internal/acctest/acctest.go b/internal/acctest/acctest.go index 81c2cbc..52796e9 100644 --- a/internal/acctest/acctest.go +++ b/internal/acctest/acctest.go @@ -243,6 +243,48 @@ func cleanupProxy(t testing.TB, client proxyCleaner, projectID, id string) { }) } +// CleanupExtension registers a cleanup that deletes the extension from projectID; +// empty means the env-configured default project. +func CleanupExtension(t testing.TB, projectID, id string) { + t.Helper() + + cleanupExtension(t, ClientFromEnv(), projectID, id) +} + +type extensionCleaner interface { + DefaultProjectID() string + DeleteExtension(context.Context, string, string) error +} + +func cleanupExtension(t testing.TB, client extensionCleaner, projectID, id string) { + t.Helper() + + if id == "" { + return + } + if !AcceptanceEnabled() { + t.Fatalf("%s must be set to clean up Kernel acceptance test resources", EnvAcceptance) + return + } + if os.Getenv(EnvAPIKey) == "" { + t.Fatalf("%s must be set to clean up Kernel acceptance test resources", EnvAPIKey) + return + } + + if projectID == "" { + projectID = client.DefaultProjectID() + } + + t.Cleanup(func() { + ctx, cancel := context.WithTimeout(context.Background(), cleanupTimeout) + defer cancel() + + if err := client.DeleteExtension(ctx, projectID, id); err != nil && !IsNotFound(err) { + t.Errorf("cleanup Kernel extension %s: %v", id, err) + } + }) +} + func ClientFromEnv() kernelclient.Clients { return kernelclient.New(kernelclient.Config{ APIKey: os.Getenv(EnvAPIKey), diff --git a/internal/acctest/extension_test.go b/internal/acctest/extension_test.go new file mode 100644 index 0000000..f3b0759 --- /dev/null +++ b/internal/acctest/extension_test.go @@ -0,0 +1,118 @@ +package acctest + +import ( + "context" + "errors" + "testing" +) + +type fakeExtensionCleaner struct { + defaultProjectID string + delete func(context.Context, string, string) error +} + +func (f fakeExtensionCleaner) DefaultProjectID() string { + return f.defaultProjectID +} + +func (f fakeExtensionCleaner) DeleteExtension(ctx context.Context, projectID, id string) error { + return f.delete(ctx, projectID, id) +} + +func TestCleanupExtension(t *testing.T) { + tests := map[string]struct { + acceptance string + apiKey string + projectID string + defaultProject string + id string + deleteErr error + wantProjectID string + wantCleanups int + wantDelete bool + wantFailure bool + }{ + "empty ID is ignored": {}, + "acceptance disabled": { + apiKey: "test-key", + id: "extension_123", + wantFailure: true, + }, + "API key missing": { + acceptance: "1", + id: "extension_123", + wantFailure: true, + }, + "default project is resolved": { + acceptance: "1", + apiKey: "test-key", + defaultProject: "project_default", + id: "extension_123", + wantProjectID: "project_default", + wantCleanups: 1, + wantDelete: true, + }, + "not found is already clean": { + acceptance: "1", + apiKey: "test-key", + projectID: "project_explicit", + id: "extension_123", + deleteErr: notFoundAPIError(), + wantProjectID: "project_explicit", + wantCleanups: 1, + wantDelete: true, + }, + "delete error is reported": { + acceptance: "1", + apiKey: "test-key", + projectID: "project_explicit", + id: "extension_123", + deleteErr: errors.New("connection reset"), + wantProjectID: "project_explicit", + wantCleanups: 1, + wantDelete: true, + wantFailure: true, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + t.Setenv(EnvAcceptance, test.acceptance) + t.Setenv(EnvAPIKey, test.apiKey) + + var gotID, gotProjectID string + deleteCalled := false + deadlineSet := false + recorder := &testRecorder{TB: t} + cleanupExtension(recorder, fakeExtensionCleaner{ + defaultProjectID: test.defaultProject, + delete: func(ctx context.Context, projectID, id string) error { + deleteCalled = true + _, deadlineSet = ctx.Deadline() + gotProjectID = projectID + gotID = id + return test.deleteErr + }, + }, test.projectID, test.id) + + if got, want := len(recorder.cleanups), test.wantCleanups; got != want { + t.Fatalf("cleanupExtension registered %d cleanups, want %d", got, want) + } + if test.wantCleanups == 1 { + recorder.cleanups[0]() + } + if recorder.failed != test.wantFailure { + t.Fatalf("cleanupExtension failure = %t, want %t", recorder.failed, test.wantFailure) + } + if deleteCalled != test.wantDelete { + t.Fatalf("cleanupExtension called delete = %t, want %t", deleteCalled, test.wantDelete) + } + if deleteCalled && !deadlineSet { + t.Fatal("cleanupExtension called delete without a context deadline") + } + if test.wantDelete && (gotID != test.id || gotProjectID != test.wantProjectID) { + t.Fatalf("cleanup extension scope/id = %q/%q, want %q/%q", gotProjectID, gotID, test.wantProjectID, test.id) + } + }) + } +} diff --git a/internal/kernelclient/client.go b/internal/kernelclient/client.go index d9ec24b..28edef3 100644 --- a/internal/kernelclient/client.go +++ b/internal/kernelclient/client.go @@ -185,6 +185,14 @@ func (c Clients) GetExtension(ctx context.Context, projectID, idOrName string) ( return c.extensions.Get(ctx, idOrName, scope(projectID)...) } +func (c Clients) UploadExtension(ctx context.Context, projectID string, params kernel.ExtensionUploadParams) (*kernel.ExtensionUploadResponse, error) { + return c.extensions.Upload(ctx, params, scope(projectID, noMutationRetries())...) +} + +func (c Clients) DeleteExtension(ctx context.Context, projectID, id string) error { + return c.extensions.Delete(ctx, id, scope(projectID, noMutationRetries())...) +} + func (c Clients) CreateBrowserPool(ctx context.Context, projectID string, params kernel.BrowserPoolNewParams) (*kernel.BrowserPool, error) { return c.browserPools.New(ctx, params, scope(projectID, noMutationRetries())...) } diff --git a/internal/kernelclient/client_test.go b/internal/kernelclient/client_test.go index b9e034d..812bc0e 100644 --- a/internal/kernelclient/client_test.go +++ b/internal/kernelclient/client_test.go @@ -370,6 +370,26 @@ func TestMutationsDisableSDKRetriesAndUseExpectedScope(t *testing.T) { return clients.DeleteProxy(ctx, "project_123", "proxy_123") }, }, + "extension upload": { + method: http.MethodPost, + path: "/extensions", + projectID: "project_123", + call: func(ctx context.Context, clients Clients) error { + _, err := clients.UploadExtension(ctx, "project_123", kernel.ExtensionUploadParams{ + File: strings.NewReader("extension archive"), + Name: kernel.String("Extension"), + }) + return err + }, + }, + "extension delete": { + method: http.MethodDelete, + path: "/extensions/extension_123", + projectID: "project_123", + call: func(ctx context.Context, clients Clients) error { + return clients.DeleteExtension(ctx, "project_123", "extension_123") + }, + }, "browser pool create": { method: http.MethodPost, path: "/browser_pools", @@ -496,6 +516,17 @@ func TestClientsDoNotExposeProxyHealthCheck(t *testing.T) { } } +func TestClientsDoNotExposeExtensionArchiveMethods(t *testing.T) { + t.Parallel() + + typ := reflect.TypeOf(Clients{}) + for _, name := range []string{"DownloadExtension", "DownloadExtensionFromChromeStore"} { + if _, ok := typ.MethodByName(name); ok { + t.Fatalf("Clients exposes extension archive method %s", name) + } + } +} + type capturedRequest struct { Method string Path string