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-6784-31a28b9e567a7f30dc543dc95fda03986893b906 in repository https://gitbox.apache.org/repos/asf/texera.git
commit e345e4559560d4d3daad316d88c9626f5e107b21 Author: Xinyuan Lin <[email protected]> AuthorDate: Wed Jul 22 17:02:16 2026 -0700 test(frontend): extend VirtualEnvironmentService unit test coverage (#6784) ### What changes were proposed in this PR? Extends `virtual-environment.service.spec.ts` with 15 tests (5 -> 20), covering `getAccessToken` (all three branches), the PVE list/save/delete and package endpoints (both the token and no-token `buildBaseParams` branches), `getUserPackages` match/no-match, and `getPveWebSocketUrl` (ws/wss, token presence, package/name encoding). Coverage 30% -> 100%. No existing tests modified. ### Any related issues, documentation, discussions? Closes #6778. ### How was this PR tested? `ng test --include='**/virtual-environment.service.spec.ts'` -> 20/20 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]) --- .../virtual-environment.service.spec.ts | 190 ++++++++++++++++++++- 1 file changed, 189 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/workspace/service/virtual-environment/virtual-environment.service.spec.ts b/frontend/src/app/workspace/service/virtual-environment/virtual-environment.service.spec.ts index c124972baf..51721948da 100644 --- a/frontend/src/app/workspace/service/virtual-environment/virtual-environment.service.spec.ts +++ b/frontend/src/app/workspace/service/virtual-environment/virtual-environment.service.spec.ts @@ -20,8 +20,9 @@ import { TestBed } from "@angular/core/testing"; import { AppSettings } from "../../../common/app-setting"; import { HttpClientTestingModule, HttpTestingController } from "@angular/common/http/testing"; -import { UserPveRecord, WorkflowPveService } from "./virtual-environment.service"; +import { PvePackageResponse, UserPveRecord, WorkflowPveService } from "./virtual-environment.service"; import { commonTestProviders } from "../../../common/testing/test-utils"; +import { AuthService } from "../../../common/service/user/auth.service"; describe("WorkflowPveService", () => { let service: WorkflowPveService; @@ -82,4 +83,191 @@ describe("WorkflowPveService", () => { expect(req.request.method).toBe("DELETE"); req.flush(null); }); + + describe("token-parameterized endpoints", () => { + const endpoint = AppSettings.getApiEndpoint(); + + afterEach(() => { + httpTestingController.verify(); + vi.restoreAllMocks(); + }); + + describe("getAccessToken()", () => { + it("returns the token when AuthService yields a non-empty value", () => { + vi.spyOn(AuthService, "getAccessToken").mockReturnValue("jwt-123"); + expect(service.getAccessToken()).toBe("jwt-123"); + }); + + it("returns null when AuthService yields null", () => { + vi.spyOn(AuthService, "getAccessToken").mockReturnValue(null); + expect(service.getAccessToken()).toBeNull(); + }); + + it("returns null when the token is whitespace-only", () => { + vi.spyOn(AuthService, "getAccessToken").mockReturnValue(" "); + expect(service.getAccessToken()).toBeNull(); + }); + }); + + describe("getSystemPackages()", () => { + it("GETs /pve/system with cuid and the access-token param when authenticated", () => { + vi.spyOn(AuthService, "getAccessToken").mockReturnValue("jwt-123"); + const response = { system: ["numpy", "pandas"] }; + service.getSystemPackages(5).subscribe(resp => { + expect(resp).toEqual(response); + }); + + const req = httpTestingController.expectOne(r => r.url === `${endpoint}/pve/system`); + expect(req.request.method).toBe("GET"); + expect(req.request.params.get("cuid")).toBe("5"); + expect(req.request.params.get("access-token")).toBe("jwt-123"); + req.flush(response); + }); + + it("omits the access-token param when unauthenticated", () => { + vi.spyOn(AuthService, "getAccessToken").mockReturnValue(null); + service.getSystemPackages(8).subscribe(); + + const req = httpTestingController.expectOne(r => r.url === `${endpoint}/pve/system`); + expect(req.request.params.get("cuid")).toBe("8"); + expect(req.request.params.has("access-token")).toBe(false); + req.flush({ system: [] }); + }); + }); + + describe("fetchPVEs()", () => { + it("GETs /pve/pves with the cuid param and returns the records", () => { + vi.spyOn(AuthService, "getAccessToken").mockReturnValue("jwt-123"); + const pves: PvePackageResponse[] = [ + { pveName: "env-a", userPackages: ["numpy"] }, + { pveName: "env-b", userPackages: ["scipy"] }, + ]; + service.fetchPVEs(3).subscribe(resp => { + expect(resp).toEqual(pves); + }); + + const req = httpTestingController.expectOne(r => r.url === `${endpoint}/pve/pves`); + expect(req.request.method).toBe("GET"); + expect(req.request.params.get("cuid")).toBe("3"); + expect(req.request.params.get("access-token")).toBe("jwt-123"); + req.flush(pves); + }); + }); + + describe("getUserPackages()", () => { + it("returns the userPackages of the matching PVE", () => { + vi.spyOn(AuthService, "getAccessToken").mockReturnValue(null); + const pves: PvePackageResponse[] = [ + { pveName: "env-a", userPackages: ["numpy"] }, + { pveName: "env-b", userPackages: ["scipy", "torch"] }, + ]; + let received: string[] | undefined; + service.getUserPackages(4, "env-b").subscribe(resp => { + received = resp; + }); + + const req = httpTestingController.expectOne(r => r.url === `${endpoint}/pve/pves`); + req.flush(pves); + expect(received).toEqual(["scipy", "torch"]); + }); + + it("returns an empty array when no PVE matches the given name", () => { + vi.spyOn(AuthService, "getAccessToken").mockReturnValue(null); + const pves: PvePackageResponse[] = [{ pveName: "env-a", userPackages: ["numpy"] }]; + let received: string[] | undefined; + service.getUserPackages(4, "does-not-exist").subscribe(resp => { + received = resp; + }); + + const req = httpTestingController.expectOne(r => r.url === `${endpoint}/pve/pves`); + req.flush(pves); + expect(received).toEqual([]); + }); + }); + + describe("deleteEnvironments()", () => { + it("DELETEs /pve/pves/{cuid}", () => { + service.deleteEnvironments(11).subscribe(); + + const req = httpTestingController.expectOne(`${endpoint}/pve/pves/11`); + expect(req.request.method).toBe("DELETE"); + req.flush(null); + }); + }); + + describe("deletePackage()", () => { + it("DELETEs a URL-encoded package path with the access-token param when authenticated", () => { + vi.spyOn(AuthService, "getAccessToken").mockReturnValue("jwt-123"); + service.deletePackage(6, "env a", "pkg+x").subscribe(); + + const req = httpTestingController.expectOne(r => r.url === `${endpoint}/pve/6/env%20a/packages/pkg%2Bx`); + expect(req.request.method).toBe("DELETE"); + expect(req.request.params.get("access-token")).toBe("jwt-123"); + req.flush(["remaining"]); + }); + + it("omits the access-token param when unauthenticated", () => { + vi.spyOn(AuthService, "getAccessToken").mockReturnValue(null); + service.deletePackage(6, "env-x", "numpy").subscribe(); + + const req = httpTestingController.expectOne(r => r.url === `${endpoint}/pve/6/env-x/packages/numpy`); + expect(req.request.params.has("access-token")).toBe(false); + req.flush([]); + }); + }); + }); + + describe("getPveWebSocketUrl()", () => { + const withLocation = <T>(overrides: Partial<Location>, fn: () => T): T => { + const original = window.location; + Object.defineProperty(window, "location", { + configurable: true, + value: { ...original, ...overrides }, + }); + try { + return fn(); + } finally { + Object.defineProperty(window, "location", { configurable: true, value: original }); + } + }; + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it("uses the ws:// scheme over http and encodes an empty package list with no token param", () => { + vi.spyOn(AuthService, "getAccessToken").mockReturnValue(null); + const url = withLocation({ protocol: "http:", host: "localhost:9000" }, () => + service.getPveWebSocketUrl(2, "env-a", "install") + ); + expect(url).toBe("ws://localhost:9000/wsapi/pve?packages=%5B%5D&cuid=2&pveName=env-a&action=install"); + }); + + it("uses the wss:// scheme over https", () => { + vi.spyOn(AuthService, "getAccessToken").mockReturnValue(null); + const url = withLocation({ protocol: "https:", host: "example.com" }, () => + service.getPveWebSocketUrl(2, "env-a", "install") + ); + expect(url.startsWith("wss://example.com/wsapi/pve")).toBe(true); + }); + + it("appends the access-token param when authenticated and URL-encodes it", () => { + vi.spyOn(AuthService, "getAccessToken").mockReturnValue("a b/c"); + const url = withLocation({ protocol: "http:", host: "localhost:9000" }, () => + service.getPveWebSocketUrl(2, "env-a", "install") + ); + expect(url.endsWith("&access-token=a%20b%2Fc")).toBe(true); + }); + + it("JSON-encodes the packages list and URL-encodes the pveName", () => { + vi.spyOn(AuthService, "getAccessToken").mockReturnValue(null); + const url = withLocation({ protocol: "http:", host: "localhost:9000" }, () => + service.getPveWebSocketUrl(7, "my env", "uninstall", ["numpy==1.26.0", "scipy"]) + ); + const expectedPackages = encodeURIComponent(JSON.stringify(["numpy==1.26.0", "scipy"])); + expect(url).toBe( + `ws://localhost:9000/wsapi/pve?packages=${expectedPackages}&cuid=7&pveName=my%20env&action=uninstall` + ); + }); + }); });
