diff --git a/internal/acctest/acctest.go b/internal/acctest/acctest.go index 03b92c2..230aca7 100644 --- a/internal/acctest/acctest.go +++ b/internal/acctest/acctest.go @@ -159,6 +159,48 @@ func cleanupProject(t testing.TB, client projectCleaner, id string) { }) } +// CleanupProfile registers a cleanup that deletes the profile from projectID; +// empty means the env-configured default project. +func CleanupProfile(t testing.TB, projectID, id string) { + t.Helper() + + cleanupProfile(t, ClientFromEnv(), projectID, id) +} + +type profileCleaner interface { + DefaultProjectID() string + DeleteProfile(context.Context, string, string) error +} + +func cleanupProfile(t testing.TB, client profileCleaner, 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.DeleteProfile(ctx, projectID, id); err != nil && !IsNotFound(err) { + t.Errorf("cleanup Kernel profile %s: %v", id, err) + } + }) +} + func ClientFromEnv() kernelclient.Clients { return kernelclient.New(kernelclient.Config{ APIKey: os.Getenv(EnvAPIKey), diff --git a/internal/acctest/profile_test.go b/internal/acctest/profile_test.go new file mode 100644 index 0000000..e2b9142 --- /dev/null +++ b/internal/acctest/profile_test.go @@ -0,0 +1,118 @@ +package acctest + +import ( + "context" + "errors" + "testing" +) + +type fakeProfileCleaner struct { + defaultProjectID string + delete func(context.Context, string, string) error +} + +func (f fakeProfileCleaner) DefaultProjectID() string { + return f.defaultProjectID +} + +func (f fakeProfileCleaner) DeleteProfile(ctx context.Context, projectID, id string) error { + return f.delete(ctx, projectID, id) +} + +func TestCleanupProfile(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: "profile_123", + wantFailure: true, + }, + "API key missing": { + acceptance: "1", + id: "profile_123", + wantFailure: true, + }, + "default project is resolved": { + acceptance: "1", + apiKey: "test-key", + defaultProject: "project_default", + id: "profile_123", + wantProjectID: "project_default", + wantCleanups: 1, + wantDelete: true, + }, + "not found is already clean": { + acceptance: "1", + apiKey: "test-key", + projectID: "project_explicit", + id: "profile_123", + deleteErr: notFoundAPIError(), + wantProjectID: "project_explicit", + wantCleanups: 1, + wantDelete: true, + }, + "delete error is reported": { + acceptance: "1", + apiKey: "test-key", + projectID: "project_explicit", + id: "profile_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} + cleanupProfile(recorder, fakeProfileCleaner{ + 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("cleanupProfile registered %d cleanups, want %d", got, want) + } + if test.wantCleanups == 1 { + recorder.cleanups[0]() + } + if recorder.failed != test.wantFailure { + t.Fatalf("cleanupProfile failure = %t, want %t", recorder.failed, test.wantFailure) + } + if deleteCalled != test.wantDelete { + t.Fatalf("cleanupProfile called delete = %t, want %t", deleteCalled, test.wantDelete) + } + if deleteCalled && !deadlineSet { + t.Fatal("cleanupProfile called delete without a context deadline") + } + if test.wantDelete && (gotID != test.id || gotProjectID != test.wantProjectID) { + t.Fatalf("cleanup profile 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 71f39cd..f1c3368 100644 --- a/internal/kernelclient/client.go +++ b/internal/kernelclient/client.go @@ -134,6 +134,14 @@ func (c Clients) GetProfile(ctx context.Context, projectID, idOrName string) (*k return c.profiles.Get(ctx, idOrName, scope(projectID)...) } +func (c Clients) CreateProfile(ctx context.Context, projectID string, params kernel.ProfileNewParams) (*kernel.Profile, error) { + return c.profiles.New(ctx, params, scope(projectID, noMutationRetries())...) +} + +func (c Clients) DeleteProfile(ctx context.Context, projectID, idOrName string) error { + return c.profiles.Delete(ctx, idOrName, scope(projectID, noMutationRetries())...) +} + func (c Clients) ListProfilePage(ctx context.Context, projectID, query string, offset int64) (ProfilePage, error) { var raw *http.Response params := kernel.ProfileListParams{ diff --git a/internal/kernelclient/client_test.go b/internal/kernelclient/client_test.go index 23f9fe0..b408ffa 100644 --- a/internal/kernelclient/client_test.go +++ b/internal/kernelclient/client_test.go @@ -333,6 +333,23 @@ func TestMutationsDisableSDKRetriesAndUseExpectedScope(t *testing.T) { return clients.DeleteProject(ctx, "project_123") }, }, + "profile create": { + method: http.MethodPost, + path: "/profiles", + projectID: "project_123", + call: func(ctx context.Context, clients Clients) error { + _, err := clients.CreateProfile(ctx, "project_123", kernel.ProfileNewParams{Name: kernel.String("Profile")}) + return err + }, + }, + "profile delete": { + method: http.MethodDelete, + path: "/profiles/profile_123", + projectID: "project_123", + call: func(ctx context.Context, clients Clients) error { + return clients.DeleteProfile(ctx, "project_123", "profile_123") + }, + }, "browser pool create": { method: http.MethodPost, path: "/browser_pools", @@ -443,6 +460,14 @@ func TestClientsDoNotExposeRuntimeBrowserPoolMethods(t *testing.T) { } } +func TestClientsDoNotExposeProfileArchiveMethods(t *testing.T) { + t.Parallel() + + if _, ok := reflect.TypeOf(Clients{}).MethodByName("DownloadProfile"); ok { + t.Fatal("Clients exposes profile archive download") + } +} + type capturedRequest struct { Method string Path string