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-6771-d89107a36598e7b07a16e9d49ecd779ada5ebd1b in repository https://gitbox.apache.org/repos/asf/texera.git
commit aba6e6e162dc9da8fe23c41b4063253403a98c3d Author: Xinyuan Lin <[email protected]> AuthorDate: Wed Jul 22 19:46:14 2026 -0700 test(frontend): extend ShareAccessComponent unit test coverage (#6771) ### What changes were proposed in this PR? Extends `share-access.component.spec.ts` with 19 tests (44 -> 63) for `ShareAccessComponent`, covering the grant/revoke/change-access flows, the publish/unpublish error branches, the modal Cancel guard paths, and the input validation guards. Coverage ~79% -> 100% statements/lines. No existing tests modified. ### Any related issues, documentation, discussions? Closes #6767. ### How was this PR tested? `ng test --include='**/share-access.component.spec.ts'` -> 63/63 passing. `yarn format:ci` passes. ### Was this PR authored or co-authored using generative AI tooling? Generated-by: Claude Code (Opus 4.8 [1M context]) --- .../share-access/share-access.component.spec.ts | 204 +++++++++++++++++++++ 1 file changed, 204 insertions(+) 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 d25ed19622..ea3a5af6c9 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 @@ -512,4 +512,208 @@ describe("ShareAccessComponent", () => { expect(notificationSpy.success).toHaveBeenCalledWith("Dataset unpublished successfully"); }); }); + + describe("hasWriteAccess without a resolved email", () => { + it("returns false when the current user has no email at all", () => { + const c = setupComponent(); + // Exercise the no-email early-return guard directly, independent of how the + // user service happens to resolve an empty/absent email. + c.currentEmail = undefined; + expect(c.hasWriteAccess).toBe(false); + }); + }); + + describe("removeEmailTag", () => { + it("removes the matching email and keeps the others", () => { + const c = setupComponent(); + c.emailTags = ["[email protected]", "[email protected]", "[email protected]"]; + c.removeEmailTag("[email protected]"); + expect(c.emailTags).toEqual(["[email protected]", "[email protected]"]); + }); + + it("leaves tags unchanged when the email is not present", () => { + const c = setupComponent(); + c.emailTags = ["[email protected]"]; + c.removeEmailTag("[email protected]"); + expect(c.emailTags).toEqual(["[email protected]"]); + }); + }); + + describe("onChange", () => { + it("filters allOwners case-insensitively by the typed value", () => { + const c = setupComponent(); + c.allOwners.push("Alice", "Bob", "alfred"); + c.onChange("al"); + expect(c.filteredOwners).toEqual(["Alice", "alfred"]); + }); + + it("clears filteredOwners when the value is null", () => { + const c = setupComponent(); + c.allOwners.push("Alice"); + c.filteredOwners = ["stale"]; + c.onChange(null as unknown as string); + expect(c.filteredOwners).toEqual([]); + }); + }); + + describe("onPaste with an empty existing value", () => { + it("defaults the existing email value to an empty string before appending", () => { + const c = setupComponent(); + c.validateForm.get("email")?.reset(); + const event = { + preventDefault: vi.fn(), + clipboardData: { getData: vi.fn().mockReturnValue("[email protected]") }, + } as unknown as ClipboardEvent; + c.onPaste(event); + expect(c.emailTags).toEqual(["[email protected]"]); + }); + }); + + describe("modal Cancel buttons", () => { + function captureModalRefs(): any[] { + const modalRefs: any[] = []; + modalServiceSpy.create.mockImplementation((config: any) => { + capturedModalConfigs.push(config); + const ref = { close: vi.fn() }; + modalRefs.push(ref); + return ref; + }); + return modalRefs; + } + + it("closes the revoke confirmation modal without revoking when Cancel is clicked", () => { + const modalRefs = captureModalRefs(); + const c = setupComponent({ currentEmail: "[email protected]" }); + c.verifyRevokeAccess("[email protected]"); + getFooterButton(capturedModalConfigs[0], "Cancel").onClick(); + expect(modalRefs[0].close).toHaveBeenCalled(); + expect(accessServiceSpy.revokeAccess).not.toHaveBeenCalled(); + }); + + it("closes the downgrade modal and reloads without granting when Cancel is clicked", () => { + accessServiceSpy.getAccessList.mockReturnValue( + of([{ email: "[email protected]", name: "Me", privilege: Privilege.WRITE }]) + ); + const modalRefs = captureModalRefs(); + const c = setupComponent({ currentEmail: "[email protected]", type: "workflow", id: 3 }); + accessServiceSpy.grantAccess.mockClear(); + accessServiceSpy.getAccessList.mockClear(); + c.changeAccessLevel("[email protected]", "READ"); + getFooterButton(capturedModalConfigs[0], "Cancel").onClick(); + expect(modalRefs[0].close).toHaveBeenCalled(); + expect(accessServiceSpy.grantAccess).not.toHaveBeenCalled(); + // Cancel re-runs ngOnInit to restore the previous access level in the UI + expect(accessServiceSpy.getAccessList).toHaveBeenCalledWith("workflow", 3); + }); + + it("closes the publish modal without publishing when Cancel is clicked", () => { + workflowPersistSpy.getWorkflowIsPublished.mockReturnValue(of("Private")); + const modalRefs = captureModalRefs(); + const c = setupComponent({ type: "workflow", inWorkspace: true }); + c.verifyPublish(); + getFooterButton(capturedModalConfigs[0], "Cancel").onClick(); + expect(modalRefs[0].close).toHaveBeenCalled(); + expect(workflowPersistSpy.updateWorkflowIsPublished).not.toHaveBeenCalled(); + expect(workflowActionSpy.setWorkflowIsPublished).not.toHaveBeenCalled(); + }); + + it("closes the unpublish modal without unpublishing when Cancel is clicked", () => { + workflowPersistSpy.getWorkflowIsPublished.mockReturnValue(of("Public")); + const modalRefs = captureModalRefs(); + const c = setupComponent({ type: "workflow" }); + c.verifyUnpublish(); + getFooterButton(capturedModalConfigs[0], "Cancel").onClick(); + expect(modalRefs[0].close).toHaveBeenCalled(); + expect(workflowPersistSpy.updateWorkflowIsPublished).not.toHaveBeenCalled(); + }); + }); + + describe("applyAccessLevelChange error branch", () => { + it("surfaces HttpErrorResponse and reloads the access list on failure", () => { + accessServiceSpy.grantAccess.mockReturnValue( + throwError(() => new HttpErrorResponse({ error: { message: "change failed" }, status: 500 })) + ); + const c = setupComponent({ currentEmail: "[email protected]", type: "workflow", id: 3 }); + accessServiceSpy.getAccessList.mockClear(); + c.changeAccessLevel("[email protected]", "READ"); + expect(notificationSpy.error).toHaveBeenCalledWith("change failed"); + // the error branch reloads the access list so the UI reflects the unchanged level + expect(accessServiceSpy.getAccessList).toHaveBeenCalledWith("workflow", 3); + }); + }); + + describe("unpublish error branches", () => { + it("unpublishWorkflow surfaces HttpErrorResponse and leaves isPublic unchanged", () => { + workflowPersistSpy.getWorkflowIsPublished.mockReturnValue(of("Public")); + workflowPersistSpy.updateWorkflowIsPublished.mockReturnValue( + throwError(() => new HttpErrorResponse({ error: { message: "unpublish failed" }, status: 500 })) + ); + const c = setupComponent({ type: "workflow" }); + c.unpublishWorkflow(); + expect(notificationSpy.error).toHaveBeenCalledWith("unpublish failed"); + expect(c.isPublic).toBe(true); + }); + + it("unpublishDataset surfaces HttpErrorResponse and leaves isPublic unchanged", () => { + datasetServiceSpy.getDataset.mockReturnValue(of({ dataset: { isPublic: true } })); + datasetServiceSpy.updateDatasetPublicity.mockReturnValue( + throwError(() => new HttpErrorResponse({ error: { message: "dataset unpublish failed" }, status: 500 })) + ); + const c = setupComponent({ type: "dataset" }); + c.unpublishDataset(); + expect(notificationSpy.error).toHaveBeenCalledWith("dataset unpublish failed"); + expect(c.isPublic).toBe(true); + }); + }); + + describe("guard branches (no-ops)", () => { + it("handleInputConfirm skips empty tokens produced by trailing separators", () => { + const c = setupComponent(); + c.validateForm.get("email")?.setValue("[email protected], ; "); + c.handleInputConfirm(); + expect(c.emailTags).toEqual(["[email protected]"]); + expect(messageSpy.error).not.toHaveBeenCalled(); + }); + + it("grantAccess does nothing when there are no email tags", () => { + const c = setupComponent({ type: "workflow", id: 5 }); + accessServiceSpy.grantAccess.mockClear(); + c.emailTags = []; + c.grantAccess(); + expect(accessServiceSpy.grantAccess).not.toHaveBeenCalled(); + expect(gmailSpy.sendEmail).not.toHaveBeenCalled(); + }); + + it("publishWorkflow is a no-op when the workflow is already public", () => { + workflowPersistSpy.getWorkflowIsPublished.mockReturnValue(of("Public")); + const c = setupComponent({ type: "workflow" }); + workflowPersistSpy.updateWorkflowIsPublished.mockClear(); + c.publishWorkflow(); + expect(workflowPersistSpy.updateWorkflowIsPublished).not.toHaveBeenCalled(); + }); + + it("unpublishWorkflow is a no-op when the workflow is already private", () => { + workflowPersistSpy.getWorkflowIsPublished.mockReturnValue(of("Private")); + const c = setupComponent({ type: "workflow" }); + workflowPersistSpy.updateWorkflowIsPublished.mockClear(); + c.unpublishWorkflow(); + expect(workflowPersistSpy.updateWorkflowIsPublished).not.toHaveBeenCalled(); + }); + + it("publishDataset is a no-op when the dataset is already public", () => { + datasetServiceSpy.getDataset.mockReturnValue(of({ dataset: { isPublic: true } })); + const c = setupComponent({ type: "dataset" }); + datasetServiceSpy.updateDatasetPublicity.mockClear(); + c.publishDataset(); + expect(datasetServiceSpy.updateDatasetPublicity).not.toHaveBeenCalled(); + }); + + it("unpublishDataset is a no-op when the dataset is already private", () => { + datasetServiceSpy.getDataset.mockReturnValue(of({ dataset: { isPublic: false } })); + const c = setupComponent({ type: "dataset" }); + datasetServiceSpy.updateDatasetPublicity.mockClear(); + c.unpublishDataset(); + expect(datasetServiceSpy.updateDatasetPublicity).not.toHaveBeenCalled(); + }); + }); });
