Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions internal/acctest/acctest.go
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
118 changes: 118 additions & 0 deletions internal/acctest/extension_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
}
8 changes: 8 additions & 0 deletions internal/kernelclient/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())...)
}
Expand Down
31 changes: 31 additions & 0 deletions internal/kernelclient/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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
Expand Down