This is an automated email from the ASF dual-hosted git repository. github-merge-queue[bot] pushed a commit to branch gh-readonly-queue/main/pr-5233-d5bc8b7cf8d2e515280934a9c3e52c305fe3c002 in repository https://gitbox.apache.org/repos/asf/texera.git
commit da64ffb3c921be4608a22bd68109f9649e3cdb1b Author: Matthew B. <[email protected]> AuthorDate: Tue May 26 21:49:46 2026 -0700 test: cover ShareAccessComponent behaviors in spec (#5233) ### What changes were proposed in this PR? - Expand `share-access.component.spec.ts` from 4 routing-only tests to 44 tests grouped by behavior: `ngOnInit`, `handleInputConfirm`, `onPaste`, `grantAccess`, `hasWriteAccess`, `verifyRevokeAccess`/`revokeAccess`, `changeAccessLevel`, `verifyPublish`/`verifyUnpublish`, and the individual publish/unpublish methods. - Drive modal-confirmation flows by capturing the config passed to `NzModalService.create` and invoking the relevant `nzFooter` button's `onClick`, so each test asserts against the specific Confirm/Cancel path it cares about. - Parameterize the `setupComponent` helper with `type`, `id`, `inWorkspace`, and `currentEmail` overrides, and reset the TestBed in `beforeEach` so each test reconfigures providers (e.g., a WRITE access entry for `hasWriteAccess`, an already-published item for `verifyPublish`) without bleed-over. ### Any related issues, documentation, or discussions? Closes: #5223 ### How was this PR tested? - Ran `yarn format:fix` (touched the new spec only). - Ran `yarn test --include="src/app/dashboard/component/user/share-access/share-access.component.spec.ts"`: 44/44 passing. ### Was this PR authored or co-authored using generative AI tooling? Co-authored with Claude Opus 4.7 in compliance with ASF --- .../share-access/share-access.component.spec.ts | 473 +++++++++++++++++++-- 1 file changed, 438 insertions(+), 35 deletions(-) diff --git a/frontend/src/app/dashboard/component/user/share-access/share-access.component.spec.ts b/frontend/src/app/dashboard/component/user/share-access/share-access.component.spec.ts index 9227790988..2a40a06e20 100644 --- a/frontend/src/app/dashboard/component/user/share-access/share-access.component.spec.ts +++ b/frontend/src/app/dashboard/component/user/share-access/share-access.component.spec.ts @@ -20,7 +20,8 @@ import { TestBed } from "@angular/core/testing"; import { HttpClientTestingModule } from "@angular/common/http/testing"; import { NoopAnimationsModule } from "@angular/platform-browser/animations"; -import { of } from "rxjs"; +import { HttpErrorResponse } from "@angular/common/http"; +import { of, throwError } from "rxjs"; import { NZ_MODAL_DATA, NzModalRef, NzModalService } from "ng-zorro-antd/modal"; import { NzMessageService } from "ng-zorro-antd/message"; @@ -33,41 +34,62 @@ import { NotificationService } from "../../../../common/service/notification/not import { DatasetService } from "../../../service/user/dataset/dataset.service"; import { WorkflowPersistService } from "src/app/common/service/workflow-persist/workflow-persist.service"; import { WorkflowActionService } from "src/app/workspace/service/workflow-graph/model/workflow-action.service"; +import { Privilege } from "../../../type/share-access.interface"; -describe("ShareAccessComponent.grantAccess", () => { +interface SetupOptions { + type?: string; + id?: number; + inWorkspace?: boolean; + currentEmail?: string | undefined; +} + +describe("ShareAccessComponent", () => { let gmailSpy: { sendEmail: ReturnType<typeof vi.fn> }; let accessServiceSpy: { grantAccess: ReturnType<typeof vi.fn>; getAccessList: ReturnType<typeof vi.fn>; getOwner: ReturnType<typeof vi.fn>; + revokeAccess: ReturnType<typeof vi.fn>; + }; + let notificationSpy: { success: ReturnType<typeof vi.fn>; error: ReturnType<typeof vi.fn> }; + let messageSpy: { error: ReturnType<typeof vi.fn> }; + let modalRefSpy: { close: ReturnType<typeof vi.fn> }; + let modalServiceSpy: { create: ReturnType<typeof vi.fn> }; + let workflowPersistSpy: { + getWorkflowIsPublished: ReturnType<typeof vi.fn>; + updateWorkflowIsPublished: ReturnType<typeof vi.fn>; }; + let datasetServiceSpy: { + getDataset: ReturnType<typeof vi.fn>; + updateDatasetPublicity: ReturnType<typeof vi.fn>; + }; + let workflowActionSpy: { setWorkflowIsPublished: ReturnType<typeof vi.fn> }; + let userServiceCurrentEmail: string | undefined; + let capturedModalConfigs: any[]; + + function setupComponent(opts: SetupOptions = {}): ShareAccessComponent { + const { type = "workflow", id = 1, inWorkspace = false, currentEmail = "[email protected]" } = opts; + userServiceCurrentEmail = currentEmail; - function setupComponent(type: string, id: number): ShareAccessComponent { TestBed.configureTestingModule({ imports: [HttpClientTestingModule, NoopAnimationsModule, ShareAccessComponent], providers: [ - { provide: NZ_MODAL_DATA, useValue: { type, id, allOwners: [], inWorkspace: false } }, + { provide: NZ_MODAL_DATA, useValue: { type, id, allOwners: [], inWorkspace } }, { provide: ShareAccessService, useValue: accessServiceSpy }, { provide: UserService, - useValue: { getCurrentUser: () => ({ email: "[email protected]" }) }, - }, - { provide: GmailService, useValue: gmailSpy }, - { provide: NotificationService, useValue: { success: vi.fn(), error: vi.fn() } }, - { provide: NzMessageService, useValue: { error: vi.fn() } }, - { provide: NzModalService, useValue: {} }, - { provide: NzModalRef, useValue: { close: vi.fn() } }, - { - provide: WorkflowPersistService, - useValue: { getWorkflowIsPublished: vi.fn().mockReturnValue(of("Private")) }, - }, - { - provide: DatasetService, useValue: { - getDataset: vi.fn().mockReturnValue(of({ dataset: { isPublic: false } })), + getCurrentUser: () => (userServiceCurrentEmail ? { email: userServiceCurrentEmail } : undefined), }, }, - { provide: WorkflowActionService, useValue: {} }, + { provide: GmailService, useValue: gmailSpy }, + { provide: NotificationService, useValue: notificationSpy }, + { provide: NzMessageService, useValue: messageSpy }, + { provide: NzModalService, useValue: modalServiceSpy }, + { provide: NzModalRef, useValue: modalRefSpy }, + { provide: WorkflowPersistService, useValue: workflowPersistSpy }, + { provide: DatasetService, useValue: datasetServiceSpy }, + { provide: WorkflowActionService, useValue: workflowActionSpy }, ], }); const fixture = TestBed.createComponent(ShareAccessComponent); @@ -76,37 +98,418 @@ describe("ShareAccessComponent.grantAccess", () => { } beforeEach(() => { + TestBed.resetTestingModule(); + capturedModalConfigs = []; gmailSpy = { sendEmail: vi.fn() }; accessServiceSpy = { grantAccess: vi.fn().mockReturnValue(of(null)), getAccessList: vi.fn().mockReturnValue(of([])), getOwner: vi.fn().mockReturnValue(of("[email protected]")), + revokeAccess: vi.fn().mockReturnValue(of(null)), + }; + notificationSpy = { success: vi.fn(), error: vi.fn() }; + messageSpy = { error: vi.fn() }; + modalRefSpy = { close: vi.fn() }; + modalServiceSpy = { + create: vi.fn().mockImplementation((config: any) => { + capturedModalConfigs.push(config); + return { close: vi.fn() }; + }), + }; + workflowPersistSpy = { + getWorkflowIsPublished: vi.fn().mockReturnValue(of("Private")), + updateWorkflowIsPublished: vi.fn().mockReturnValue(of(null)), + }; + datasetServiceSpy = { + getDataset: vi.fn().mockReturnValue(of({ dataset: { isPublic: false } })), + updateDatasetPublicity: vi.fn().mockReturnValue(of(null)), }; + workflowActionSpy = { setWorkflowIsPublished: vi.fn() }; }); - function grantAndCaptureMessage(c: ShareAccessComponent): string { - c.emailTags = ["[email protected]"]; - c.grantAccess(); - return gmailSpy.sendEmail.mock.calls[0][1] as string; + function getFooterButton(config: any, label: string): { onClick: () => void } { + return config.nzFooter.find((b: any) => b.label === label); } - it("uses the workflow dashboard path when sharing a workflow", () => { - const message = grantAndCaptureMessage(setupComponent("workflow", 11)); - expect(message).toContain("/dashboard/user/workflow/11"); + describe("ngOnInit", () => { + it("loads access list and owner from ShareAccessService", () => { + const accessList = [{ email: "[email protected]", name: "A", privilege: Privilege.READ }]; + accessServiceSpy.getAccessList.mockReturnValue(of(accessList)); + accessServiceSpy.getOwner.mockReturnValue(of("[email protected]")); + const c = setupComponent({ type: "workflow", id: 7 }); + expect(accessServiceSpy.getAccessList).toHaveBeenCalledWith("workflow", 7); + expect(accessServiceSpy.getOwner).toHaveBeenCalledWith("workflow", 7); + expect(c.accessList).toEqual(accessList); + expect(c.owner).toBe("[email protected]"); + }); + + it("loads publish state for workflow via WorkflowPersistService", () => { + workflowPersistSpy.getWorkflowIsPublished.mockReturnValue(of("Public")); + const c = setupComponent({ type: "workflow", id: 9 }); + expect(workflowPersistSpy.getWorkflowIsPublished).toHaveBeenCalledWith(9); + expect(c.isPublic).toBe(true); + }); + + it("sets isPublic to false when workflow publish state is Private", () => { + workflowPersistSpy.getWorkflowIsPublished.mockReturnValue(of("Private")); + const c = setupComponent({ type: "workflow" }); + expect(c.isPublic).toBe(false); + }); + + it("loads publish state for dataset via DatasetService.getDataset", () => { + datasetServiceSpy.getDataset.mockReturnValue(of({ dataset: { isPublic: true } })); + const c = setupComponent({ type: "dataset", id: 12 }); + expect(datasetServiceSpy.getDataset).toHaveBeenCalledWith(12); + expect(c.isPublic).toBe(true); + }); + + it("does not query publish state for non-workflow/dataset types", () => { + setupComponent({ type: "project", id: 4 }); + expect(workflowPersistSpy.getWorkflowIsPublished).not.toHaveBeenCalled(); + expect(datasetServiceSpy.getDataset).not.toHaveBeenCalled(); + }); + }); + + describe("handleInputConfirm", () => { + it("splits input on whitespace, commas, and semicolons into emailTags", () => { + const c = setupComponent(); + c.validateForm.get("email")?.setValue("[email protected], [email protected];[email protected] [email protected]"); + c.handleInputConfirm(); + expect(c.emailTags).toEqual(["[email protected]", "[email protected]", "[email protected]", "[email protected]"]); + }); + + it("rejects invalid emails via NzMessageService.error", () => { + const c = setupComponent(); + c.validateForm.get("email")?.setValue("not-an-email"); + c.handleInputConfirm(); + expect(messageSpy.error).toHaveBeenCalledWith("not-an-email is not a valid email"); + expect(c.emailTags).toEqual([]); + }); + + it("rejects duplicate emails via NzMessageService.error", () => { + const c = setupComponent(); + c.emailTags = ["[email protected]"]; + c.validateForm.get("email")?.setValue("[email protected]"); + c.handleInputConfirm(); + expect(messageSpy.error).toHaveBeenCalledWith("[email protected] is already in the tags"); + expect(c.emailTags).toEqual(["[email protected]"]); + }); + + it("resets the email form control after processing", () => { + const c = setupComponent(); + c.validateForm.get("email")?.setValue("[email protected]"); + c.handleInputConfirm(); + expect(c.validateForm.get("email")?.value).toBeNull(); + }); + + it("calls event.preventDefault when an event is provided", () => { + const c = setupComponent(); + const event = { preventDefault: vi.fn() } as unknown as Event; + c.handleInputConfirm(event); + expect(event.preventDefault).toHaveBeenCalled(); + }); }); - it("uses the dataset dashboard path when sharing a dataset", () => { - const message = grantAndCaptureMessage(setupComponent("dataset", 22)); - expect(message).toContain("/dashboard/user/dataset/22"); + describe("onPaste", () => { + it("concatenates clipboard text to the existing email value and runs handleInputConfirm", () => { + const c = setupComponent(); + c.validateForm.get("email")?.setValue("[email protected],"); + const event = { + preventDefault: vi.fn(), + clipboardData: { getData: vi.fn().mockReturnValue("[email protected]") }, + } as unknown as ClipboardEvent; + c.onPaste(event); + expect(event.preventDefault).toHaveBeenCalled(); + expect(c.emailTags).toEqual(["[email protected]", "[email protected]"]); + }); + + it("is a no-op when clipboard data is empty", () => { + const c = setupComponent(); + const event = { + preventDefault: vi.fn(), + clipboardData: { getData: vi.fn().mockReturnValue("") }, + } as unknown as ClipboardEvent; + c.onPaste(event); + expect(c.emailTags).toEqual([]); + }); }); - it("uses the project dashboard path when sharing a project", () => { - const message = grantAndCaptureMessage(setupComponent("project", 33)); - expect(message).toContain("/dashboard/user/project/33"); + describe("grantAccess", () => { + function grantAndCaptureMessage(c: ShareAccessComponent): string { + c.emailTags = ["[email protected]"]; + c.grantAccess(); + return gmailSpy.sendEmail.mock.calls[0][1] as string; + } + + it("uses the workflow dashboard path when sharing a workflow", () => { + const message = grantAndCaptureMessage(setupComponent({ type: "workflow", id: 11 })); + expect(message).toContain("/dashboard/user/workflow/11"); + }); + + it("uses the dataset dashboard path when sharing a dataset", () => { + const message = grantAndCaptureMessage(setupComponent({ type: "dataset", id: 22 })); + expect(message).toContain("/dashboard/user/dataset/22"); + }); + + it("uses the project dashboard path when sharing a project", () => { + const message = grantAndCaptureMessage(setupComponent({ type: "project", id: 33 })); + expect(message).toContain("/dashboard/user/project/33"); + }); + + it("omits the access URL when sharing a computing-unit", () => { + const message = grantAndCaptureMessage(setupComponent({ type: "computing-unit", id: 44 })); + expect(message).not.toContain("/dashboard/user/"); + }); + + it("calls ShareAccessService.grantAccess with the selected access level for each tag", () => { + const c = setupComponent({ type: "workflow", id: 5 }); + c.validateForm.get("accessLevel")?.setValue("READ"); + c.emailTags = ["[email protected]", "[email protected]"]; + c.grantAccess(); + expect(accessServiceSpy.grantAccess).toHaveBeenCalledWith("workflow", 5, "[email protected]", "READ"); + expect(accessServiceSpy.grantAccess).toHaveBeenCalledWith("workflow", 5, "[email protected]", "READ"); + }); + + it("shows a success notification and clears emailTags after granting", () => { + const c = setupComponent({ type: "workflow", id: 5 }); + c.emailTags = ["[email protected]"]; + c.grantAccess(); + expect(notificationSpy.success).toHaveBeenCalledWith("workflow shared with [email protected] successfully."); + expect(c.emailTags).toEqual([]); + }); + + it("surfaces HttpErrorResponse via NotificationService.error", () => { + accessServiceSpy.grantAccess.mockReturnValue( + throwError(() => new HttpErrorResponse({ error: { message: "boom" }, status: 500 })) + ); + const c = setupComponent(); + c.emailTags = ["[email protected]"]; + c.grantAccess(); + expect(notificationSpy.error).toHaveBeenCalledWith("boom"); + }); }); - it("omits the access URL when sharing a computing-unit", () => { - const message = grantAndCaptureMessage(setupComponent("computing-unit", 44)); - expect(message).not.toContain("/dashboard/user/"); + describe("hasWriteAccess", () => { + it("returns false when there is no current user email", () => { + const c = setupComponent({ currentEmail: undefined }); + expect(c.hasWriteAccess).toBe(false); + }); + + it("returns true when the current user is the owner", () => { + accessServiceSpy.getOwner.mockReturnValue(of("[email protected]")); + const c = setupComponent({ currentEmail: "[email protected]" }); + expect(c.hasWriteAccess).toBe(true); + }); + + it("returns true when the current user has WRITE privilege in the access list", () => { + accessServiceSpy.getAccessList.mockReturnValue( + of([{ email: "[email protected]", name: "Me", privilege: Privilege.WRITE }]) + ); + const c = setupComponent({ currentEmail: "[email protected]" }); + expect(c.hasWriteAccess).toBe(true); + }); + + it("returns false when the current user has READ privilege", () => { + accessServiceSpy.getAccessList.mockReturnValue( + of([{ email: "[email protected]", name: "Me", privilege: Privilege.READ }]) + ); + const c = setupComponent({ currentEmail: "[email protected]" }); + expect(c.hasWriteAccess).toBe(false); + }); + }); + + describe("verifyRevokeAccess / revokeAccess", () => { + it("opens a self-revoke modal when revoking own access", () => { + const c = setupComponent({ currentEmail: "[email protected]", type: "workflow" }); + c.verifyRevokeAccess("[email protected]"); + const config = capturedModalConfigs[0]; + expect(config.nzTitle).toBe("Revoke Your Access"); + expect(config.nzContent).toContain("your own access"); + }); + + it("opens an other-user revoke modal when revoking someone else", () => { + const c = setupComponent({ currentEmail: "[email protected]", type: "workflow" }); + c.verifyRevokeAccess("[email protected]"); + const config = capturedModalConfigs[0]; + expect(config.nzTitle).toBe("Revoke Access"); + expect(config.nzContent).toContain("[email protected]"); + }); + + it("calls revokeAccess on confirm and emits refresh on destroy for self-revoke", () => { + const c = setupComponent({ currentEmail: "[email protected]" }); + const refreshSpy = vi.fn(); + c.refresh.subscribe(refreshSpy); + c.verifyRevokeAccess("[email protected]"); + getFooterButton(capturedModalConfigs[0], "Revoke").onClick(); + expect(accessServiceSpy.revokeAccess).toHaveBeenCalledWith("workflow", 1, "[email protected]"); + expect(modalRefSpy.close).toHaveBeenCalledWith({ userRevokedOwnAccess: true }); + c.ngOnDestroy(); + expect(refreshSpy).toHaveBeenCalled(); + }); + + it("does not close the outer modal when revoking another user", () => { + const c = setupComponent({ currentEmail: "[email protected]" }); + c.verifyRevokeAccess("[email protected]"); + getFooterButton(capturedModalConfigs[0], "Revoke").onClick(); + expect(accessServiceSpy.revokeAccess).toHaveBeenCalledWith("workflow", 1, "[email protected]"); + expect(modalRefSpy.close).not.toHaveBeenCalled(); + }); + + it("surfaces revoke HttpErrorResponse via NotificationService.error", () => { + accessServiceSpy.revokeAccess.mockReturnValue( + throwError(() => new HttpErrorResponse({ error: { message: "nope" }, status: 403 })) + ); + const c = setupComponent({ currentEmail: "[email protected]" }); + c.verifyRevokeAccess("[email protected]"); + getFooterButton(capturedModalConfigs[0], "Revoke").onClick(); + expect(notificationSpy.error).toHaveBeenCalledWith("nope"); + }); + }); + + describe("changeAccessLevel", () => { + it("calls applyAccessLevelChange directly when not a self-downgrade", () => { + const c = setupComponent({ currentEmail: "[email protected]", type: "workflow", id: 3 }); + accessServiceSpy.grantAccess.mockClear(); + c.changeAccessLevel("[email protected]", "READ"); + expect(modalServiceSpy.create).not.toHaveBeenCalled(); + expect(accessServiceSpy.grantAccess).toHaveBeenCalledWith("workflow", 3, "[email protected]", "READ"); + }); + + it("opens a downgrade-confirmation modal when downgrading own WRITE access to READ", () => { + accessServiceSpy.getAccessList.mockReturnValue( + of([{ email: "[email protected]", name: "Me", privilege: Privilege.WRITE }]) + ); + const c = setupComponent({ currentEmail: "[email protected]", type: "workflow", id: 3 }); + accessServiceSpy.grantAccess.mockClear(); + c.changeAccessLevel("[email protected]", "READ"); + expect(modalServiceSpy.create).toHaveBeenCalled(); + expect(capturedModalConfigs[0].nzTitle).toBe("Downgrade Your Access"); + expect(accessServiceSpy.grantAccess).not.toHaveBeenCalled(); + getFooterButton(capturedModalConfigs[0], "Confirm").onClick(); + expect(accessServiceSpy.grantAccess).toHaveBeenCalledWith("workflow", 3, "[email protected]", "READ"); + }); + + it("does not open the downgrade modal when upgrading own access from READ to WRITE", () => { + accessServiceSpy.getAccessList.mockReturnValue( + of([{ email: "[email protected]", name: "Me", privilege: Privilege.READ }]) + ); + const c = setupComponent({ currentEmail: "[email protected]" }); + accessServiceSpy.grantAccess.mockClear(); + c.changeAccessLevel("[email protected]", "WRITE"); + expect(modalServiceSpy.create).not.toHaveBeenCalled(); + expect(accessServiceSpy.grantAccess).toHaveBeenCalled(); + }); + }); + + describe("verifyPublish / verifyUnpublish", () => { + it("publishes a workflow on confirm and updates the action service when inWorkspace", () => { + workflowPersistSpy.getWorkflowIsPublished.mockReturnValue(of("Private")); + const c = setupComponent({ type: "workflow", id: 8, inWorkspace: true }); + c.verifyPublish(); + getFooterButton(capturedModalConfigs[0], "Publish").onClick(); + expect(workflowPersistSpy.updateWorkflowIsPublished).toHaveBeenCalledWith(8, true); + expect(workflowActionSpy.setWorkflowIsPublished).toHaveBeenCalledWith(1); + }); + + it("does not call WorkflowActionService.setWorkflowIsPublished when not inWorkspace", () => { + workflowPersistSpy.getWorkflowIsPublished.mockReturnValue(of("Private")); + const c = setupComponent({ type: "workflow", id: 8, inWorkspace: false }); + c.verifyPublish(); + getFooterButton(capturedModalConfigs[0], "Publish").onClick(); + expect(workflowActionSpy.setWorkflowIsPublished).not.toHaveBeenCalled(); + }); + + it("publishes a dataset on confirm", () => { + datasetServiceSpy.getDataset.mockReturnValue(of({ dataset: { isPublic: false } })); + const c = setupComponent({ type: "dataset", id: 9 }); + c.verifyPublish(); + getFooterButton(capturedModalConfigs[0], "Publish").onClick(); + expect(datasetServiceSpy.updateDatasetPublicity).toHaveBeenCalledWith(9); + }); + + it("does not open the publish modal when the item is already public", () => { + workflowPersistSpy.getWorkflowIsPublished.mockReturnValue(of("Public")); + const c = setupComponent({ type: "workflow" }); + c.verifyPublish(); + expect(modalServiceSpy.create).not.toHaveBeenCalled(); + }); + + it("unpublishes a workflow on confirm and updates the action service when inWorkspace", () => { + workflowPersistSpy.getWorkflowIsPublished.mockReturnValue(of("Public")); + const c = setupComponent({ type: "workflow", id: 8, inWorkspace: true }); + c.verifyUnpublish(); + getFooterButton(capturedModalConfigs[0], "Unpublish").onClick(); + expect(workflowPersistSpy.updateWorkflowIsPublished).toHaveBeenCalledWith(8, false); + expect(workflowActionSpy.setWorkflowIsPublished).toHaveBeenCalledWith(0); + }); + + it("unpublishes a dataset on confirm", () => { + datasetServiceSpy.getDataset.mockReturnValue(of({ dataset: { isPublic: true } })); + const c = setupComponent({ type: "dataset", id: 9 }); + c.verifyUnpublish(); + getFooterButton(capturedModalConfigs[0], "Unpublish").onClick(); + expect(datasetServiceSpy.updateDatasetPublicity).toHaveBeenCalledWith(9); + }); + + it("does not open the unpublish modal when the item is already private", () => { + workflowPersistSpy.getWorkflowIsPublished.mockReturnValue(of("Private")); + const c = setupComponent({ type: "workflow" }); + c.verifyUnpublish(); + expect(modalServiceSpy.create).not.toHaveBeenCalled(); + }); + }); + + describe("publish / unpublish methods", () => { + it("publishWorkflow flips isPublic and shows a success notification", () => { + workflowPersistSpy.getWorkflowIsPublished.mockReturnValue(of("Private")); + const c = setupComponent({ type: "workflow" }); + c.publishWorkflow(); + expect(c.isPublic).toBe(true); + expect(notificationSpy.success).toHaveBeenCalledWith("Workflow published successfully"); + }); + + it("publishWorkflow surfaces HttpErrorResponse via NotificationService.error", () => { + workflowPersistSpy.getWorkflowIsPublished.mockReturnValue(of("Private")); + workflowPersistSpy.updateWorkflowIsPublished.mockReturnValue( + throwError(() => new HttpErrorResponse({ error: { message: "publish failed" }, status: 500 })) + ); + const c = setupComponent({ type: "workflow" }); + c.publishWorkflow(); + expect(notificationSpy.error).toHaveBeenCalledWith("publish failed"); + }); + + it("unpublishWorkflow flips isPublic to false and shows a success notification", () => { + workflowPersistSpy.getWorkflowIsPublished.mockReturnValue(of("Public")); + const c = setupComponent({ type: "workflow" }); + c.unpublishWorkflow(); + expect(c.isPublic).toBe(false); + expect(notificationSpy.success).toHaveBeenCalledWith("Workflow unpublished successfully"); + }); + + it("publishDataset flips isPublic and shows a success notification", () => { + datasetServiceSpy.getDataset.mockReturnValue(of({ dataset: { isPublic: false } })); + const c = setupComponent({ type: "dataset" }); + c.publishDataset(); + expect(c.isPublic).toBe(true); + expect(notificationSpy.success).toHaveBeenCalledWith("Dataset published successfully"); + }); + + it("publishDataset surfaces HttpErrorResponse via NotificationService.error", () => { + datasetServiceSpy.getDataset.mockReturnValue(of({ dataset: { isPublic: false } })); + datasetServiceSpy.updateDatasetPublicity.mockReturnValue( + throwError(() => new HttpErrorResponse({ error: { message: "dataset publish failed" }, status: 500 })) + ); + const c = setupComponent({ type: "dataset" }); + c.publishDataset(); + expect(notificationSpy.error).toHaveBeenCalledWith("dataset publish failed"); + }); + + it("unpublishDataset flips isPublic to false and shows a success notification", () => { + datasetServiceSpy.getDataset.mockReturnValue(of({ dataset: { isPublic: true } })); + const c = setupComponent({ type: "dataset" }); + c.unpublishDataset(); + expect(c.isPublic).toBe(false); + expect(notificationSpy.success).toHaveBeenCalledWith("Dataset unpublished successfully"); + }); }); });
