This is an automated email from the ASF dual-hosted git repository.
Yicong-Huang pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/texera.git
The following commit(s) were added to refs/heads/main by this push:
new b3706cb66c test: add specs for UserConfigService (#5014)
b3706cb66c is described below
commit b3706cb66c0085ddf8e227336845d42b5face5a9
Author: Matthew B. <[email protected]>
AuthorDate: Sun May 10 23:54:37 2026 -0700
test: add specs for UserConfigService (#5014)
### What changes were proposed in this PR?
Replaces the commented-out user-config.service.spec.ts with a real
Vitest suite that covers fetchAll, fetchKey, set, delete, change
notifications, and login/logout reactions. Re-enables the spec in
angular.json and tsconfig.spec.json.
### Any related issues, documentation, or discussions?
Closes: #4964
### How was this PR tested?
yarn ng test --include="**/user-config.service.spec.ts" All tests pass.
### Was this PR authored or co-authored using generative AI tooling?
Co-Authored with Claude Opus 4.7 in compliance with ASF.
Signed-off-by: Yicong Huang <[email protected]>
Co-authored-by: Yicong Huang
<[email protected]>
---
frontend/angular.json | 1 -
.../user/config/user-config.service.spec.ts | 317 +++++++++++++--------
frontend/src/tsconfig.spec.json | 7 +-
3 files changed, 203 insertions(+), 122 deletions(-)
diff --git a/frontend/angular.json b/frontend/angular.json
index 17a1eb6f8c..b9e9961d02 100644
--- a/frontend/angular.json
+++ b/frontend/angular.json
@@ -94,7 +94,6 @@
"include": ["**/*.spec.ts"],
"setupFiles": ["src/jsdom-svg-polyfill.ts"],
"exclude": [
- "**/app/common/service/user/config/user-config.service.spec.ts",
"**/app/workspace/component/workflow-editor/workflow-editor.component.spec.ts"
]
}
diff --git
a/frontend/src/app/common/service/user/config/user-config.service.spec.ts
b/frontend/src/app/common/service/user/config/user-config.service.spec.ts
index c4f79e4207..0917384fb4 100644
--- a/frontend/src/app/common/service/user/config/user-config.service.spec.ts
+++ b/frontend/src/app/common/service/user/config/user-config.service.spec.ts
@@ -1,4 +1,3 @@
-import type { Mock } from "vitest";
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
@@ -18,117 +17,205 @@ import type { Mock } from "vitest";
* under the License.
*/
-// import { HttpClientTestingModule, HttpTestingController } from
"@angular/common/http/testing";
-// import { fakeAsync, flush, inject, TestBed, tick } from
"@angular/core/testing";
-// import { AppSettings } from "src/app/common/app-setting";
-// import { UserConfigService, UserConfig } from "./user-config.service";
-// import { UserService } from "../user.service";
-// import { StubUserService } from "../stub-user.service";
-
-// describe("DictionaryService", () => {
-// let dictionaryService: UserConfigService;
-// let testDict: UserConfig;
-
-// beforeEach(() => {
-// TestBed.configureTestingModule({
-// providers: [{ provide: UserService, useClass: StubUserService },
UserConfigService],
-// imports: [HttpClientTestingModule],
-// });
-
-// dictionaryService = TestBed.inject(UserConfigService);
-// testDict = { a: "a", b: "b", c: "c" }; // sample dictionary used
throughout testing
-// });
-
-// it("should be created", inject([UserConfigService], (injectedService:
UserConfigService) => {
-// expect(injectedService).toBeTruthy();
-// }));
-
-// describe("Dictionary Service", () => {
-// describe("Backend interface", () => {
-// let httpMock: HttpTestingController;
-// let dictEventSubjectNextSpy: Mock;
-
-// beforeEach(() => {
-// httpMock = TestBed.inject(HttpTestingController);
-// // handle the getAll() request created when initializing
dictionaryService
-//
httpMock.expectOne(`${AppSettings.getApiEndpoint()}/${UserConfigService.USER_DICTIONARY_ENDPOINT}`).flush({});
-
-// // clear dict
-// (dictionaryService as any).updateDict({});
-
-// dictEventSubjectNextSpy = vi.spyOn((dictionaryService as
any).dictionaryChangedSubject, "next");
-// dictEventSubjectNextSpy.calls.reset();
-// });
-
-// it("should produce a GET request when fetchKey() is called",
fakeAsync(() => {
-// const testKey = "test";
-// dictionaryService.fetchKey(testKey);
-// // get() generates a POST request to this url
-// const req = httpMock.expectOne(
-//
`${AppSettings.getApiEndpoint()}/${UserConfigService.USER_DICTIONARY_ENDPOINT}/${testKey}`
-// );
-// // POST request should have a properly formatted json payload
-// expect(req.request.method).toEqual("GET");
-// expect(req.request.responseType).toEqual("json");
-// req.flush("testValue");
-// flush();
-// httpMock.verify();
-// expect(dictEventSubjectNextSpy).toHaveBeenCalled();
-// }));
-
-// it("should produce a GET request when fetchAll() is called",
fakeAsync(() => {
-// dictionaryService.fetchAll();
-// // getAll() generates a POST request to this url
-// const req =
httpMock.expectOne(`${AppSettings.getApiEndpoint()}/${UserConfigService.USER_DICTIONARY_ENDPOINT}`);
-// // POST request should have a properly formatted json payload
-// expect(req.cancelled).toBeFalsy();
-// expect(req.request.method).toEqual("GET");
-// expect(req.request.responseType).toEqual("json");
-// req.flush({ testkey2: "testValue2" });
-// flush();
-// httpMock.verify();
-// expect(dictEventSubjectNextSpy).toHaveBeenCalled();
-// }));
-
-// it("should produce a PUT request when set() is called", fakeAsync(()
=> {
-// const testKey = "testkey3";
-// const testValue = "testValue3";
-// dictionaryService.set(testKey, testValue);
-// // set() generates a POST request to this url
-// const req = httpMock.expectOne(
-//
`${AppSettings.getApiEndpoint()}/${UserConfigService.USER_DICTIONARY_ENDPOINT}/${testKey}`
-// );
-// // POST request should have a properly formatted json payload
-// expect(req.request.method).toEqual("PUT");
-// expect(req.request.body).toEqual({ value: testValue });
-// req.flush({});
-// flush();
-// httpMock.verify();
-// expect(dictEventSubjectNextSpy).toHaveBeenCalled();
-// }));
-
-// it("should produce a DELETE request when delete() is called",
fakeAsync(() => {
-// const testKey = "testkey4";
-// dictionaryService.set(testKey, "testvalue4");
-// const setReq = httpMock.expectOne(
-//
`${AppSettings.getApiEndpoint()}/${UserConfigService.USER_DICTIONARY_ENDPOINT}/${testKey}`
-// );
-// setReq.flush({});
-// dictEventSubjectNextSpy.calls.reset();
-
-// dictionaryService.delete(testKey);
-// // delete() generates a DELETE request to this url
-// const req = httpMock.expectOne(
-//
`${AppSettings.getApiEndpoint()}/${UserConfigService.USER_DICTIONARY_ENDPOINT}/${testKey}`
-// );
-// // DELETE request should have a properly formatted json payload
-// expect(req.cancelled).toBeFalsy();
-// expect(req.request.method).toEqual("DELETE");
-// req.flush({});
-// flush();
-// httpMock.verify();
-// expect(dictEventSubjectNextSpy).toHaveBeenCalled();
-// }));
-// });
-// });
-// });
+import { HttpClientTestingModule, HttpTestingController } from
"@angular/common/http/testing";
+import { TestBed } from "@angular/core/testing";
+import { AppSettings } from "src/app/common/app-setting";
+import { UserConfigService, UserConfig } from "./user-config.service";
+import { UserService } from "../user.service";
+import { StubUserService, MOCK_USER } from "../stub-user.service";
+
+describe("UserConfigService", () => {
+ let service: UserConfigService;
+ let stubUserService: StubUserService;
+ let httpMock: HttpTestingController;
+
+ const endpoint =
`${AppSettings.getApiEndpoint()}/${UserConfigService.USER_DICTIONARY_ENDPOINT}`;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ imports: [HttpClientTestingModule],
+ providers: [{ provide: UserService, useClass: StubUserService },
UserConfigService],
+ });
+
+ stubUserService = TestBed.inject(UserService) as unknown as
StubUserService;
+ service = TestBed.inject(UserConfigService);
+ httpMock = TestBed.inject(HttpTestingController);
+
+ // The constructor calls fetchAll() because StubUserService starts logged
in.
+ // Flush the request with an empty dictionary so each test starts from a
clean slate.
+ httpMock.expectOne(endpoint).flush({});
+ });
+
+ afterEach(() => {
+ httpMock.verify();
+ });
+
+ it("should be created", () => {
+ expect(service).toBeTruthy();
+ });
+
+ it("starts with an empty local dictionary after the initial fetch", () => {
+ expect(service.getDict()).toEqual({});
+ });
+
+ describe("fetchAll", () => {
+ it("issues a GET to the config endpoint and replaces the local
dictionary", () => {
+ const observable = service.fetchAll();
+
+ const req = httpMock.expectOne(endpoint);
+ expect(req.request.method).toEqual("GET");
+
+ const payload: UserConfig = { foo: "1", bar: "2" };
+ req.flush(payload);
+
+ observable.subscribe(value => expect(value).toEqual(payload));
+ expect(service.getDict()).toEqual(payload);
+ });
+
+ it("notifies dictionaryChanged subscribers when the dictionary is
replaced", () => {
+ const next = vi.fn();
+ const sub = (service as any).dictionaryChangedSubject.subscribe(next);
+
+ service.fetchAll();
+ httpMock.expectOne(endpoint).flush({ k: "v" });
+
+ expect(next).toHaveBeenCalledTimes(1);
+ sub.unsubscribe();
+ });
+
+ it("throws when the user is not logged in", () => {
+ stubUserService.user = undefined;
+ expect(() => service.fetchAll()).toThrowError("user not logged in");
+ });
+ });
+
+ describe("fetchKey", () => {
+ it("issues a GET to the per-key endpoint and merges the value into the
local dict", () => {
+ const observable = service.fetchKey("alpha");
+
+ const req = httpMock.expectOne(`${endpoint}/alpha`);
+ expect(req.request.method).toEqual("GET");
+ expect(req.request.responseType).toEqual("text");
+
+ req.flush("one");
+ observable.subscribe(value => expect(value).toEqual("one"));
+
+ expect(service.getDict()).toEqual({ alpha: "one" });
+ });
+
+ it("notifies dictionaryChanged subscribers only when the value actually
changes", () => {
+ const next = vi.fn();
+ const sub = (service as any).dictionaryChangedSubject.subscribe(next);
+
+ service.fetchKey("alpha");
+ httpMock.expectOne(`${endpoint}/alpha`).flush("one");
+ expect(next).toHaveBeenCalledTimes(1);
+
+ service.fetchKey("alpha");
+ httpMock.expectOne(`${endpoint}/alpha`).flush("one");
+ expect(next).toHaveBeenCalledTimes(1);
+
+ sub.unsubscribe();
+ });
+
+ it("throws when the user is not logged in", () => {
+ stubUserService.user = undefined;
+ expect(() => service.fetchKey("alpha")).toThrowError("user not logged
in");
+ });
+
+ it("throws when given an empty key", () => {
+ expect(() => service.fetchKey(" ")).toThrowError(/key cannot be
empty/);
+ });
+ });
+
+ describe("set", () => {
+ it("issues a PUT with the value as the body and updates the local dict",
() => {
+ service.set("alpha", "one");
+
+ const req = httpMock.expectOne(`${endpoint}/alpha`);
+ expect(req.request.method).toEqual("PUT");
+ expect(req.request.body).toEqual("one");
+
+ req.flush(null);
+ expect(service.getDict()).toEqual({ alpha: "one" });
+ });
+
+ it("does not refire dictionaryChanged when setting the same value twice",
() => {
+ service.set("alpha", "one");
+ httpMock.expectOne(`${endpoint}/alpha`).flush(null);
+
+ const next = vi.fn();
+ const sub = (service as any).dictionaryChangedSubject.subscribe(next);
+
+ service.set("alpha", "one");
+ httpMock.expectOne(`${endpoint}/alpha`).flush(null);
+
+ expect(next).not.toHaveBeenCalled();
+ sub.unsubscribe();
+ });
+
+ it("throws when the user is not logged in", () => {
+ stubUserService.user = undefined;
+ expect(() => service.set("alpha", "one")).toThrowError("user not logged
in");
+ });
+
+ it("throws when given an empty key", () => {
+ expect(() => service.set(" ", "one")).toThrowError(/key cannot be
empty/);
+ });
+ });
+
+ describe("delete", () => {
+ beforeEach(() => {
+ service.set("alpha", "one");
+ httpMock.expectOne(`${endpoint}/alpha`).flush(null);
+ });
+
+ it("issues a DELETE to the per-key endpoint and removes the entry from the
local dict", () => {
+ service.delete("alpha");
+
+ const req = httpMock.expectOne(`${endpoint}/alpha`);
+ expect(req.request.method).toEqual("DELETE");
+ req.flush(null);
+
+ expect(service.getDict()).toEqual({});
+ });
+
+ it("is a no-op (no HTTP request) when the key is not present in the local
dict", () => {
+ service.delete("missing");
+ httpMock.expectNone(`${endpoint}/missing`);
+ });
+
+ it("throws when the user is not logged in", () => {
+ stubUserService.user = undefined;
+ expect(() => service.delete("alpha")).toThrowError("user not logged in");
+ });
+
+ it("throws when given an empty key", () => {
+ expect(() => service.delete("")).toThrowError(/key cannot be empty/);
+ });
+ });
+
+ describe("user-change reactions", () => {
+ it("re-fetches the dictionary when a logged-in user is emitted on
userChanged", () => {
+ stubUserService.userChangeSubject.next(MOCK_USER);
+
+ const req = httpMock.expectOne(endpoint);
+ expect(req.request.method).toEqual("GET");
+ req.flush({ rehydrated: "yes" });
+
+ expect(service.getDict()).toEqual({ rehydrated: "yes" });
+ });
+
+ it("clears the local dictionary when the user logs out", () => {
+ service.set("alpha", "one");
+ httpMock.expectOne(`${endpoint}/alpha`).flush(null);
+ expect(service.getDict()).toEqual({ alpha: "one" });
+
+ stubUserService.user = undefined;
+ stubUserService.userChangeSubject.next(undefined);
+
+ expect(service.getDict()).toEqual({});
+ httpMock.expectNone(endpoint);
+ });
+ });
+});
diff --git a/frontend/src/tsconfig.spec.json b/frontend/src/tsconfig.spec.json
index f17cff0ede..2f470a5d06 100644
--- a/frontend/src/tsconfig.spec.json
+++ b/frontend/src/tsconfig.spec.json
@@ -9,10 +9,5 @@
"strictNullInputTypes": false,
"fullTemplateTypeCheck": false
},
- "include": ["**/*.spec.ts", "**/*.d.ts", "vitest-globals.d.ts",
"jsdom-svg-polyfill.ts"],
- "exclude": [
- // Specs whose body is entirely commented out / placeholder — these
- // need real test cases written before they can be re-enabled.
- "**/app/common/service/user/config/user-config.service.spec.ts"
- ]
+ "include": ["**/*.spec.ts", "**/*.d.ts", "vitest-globals.d.ts",
"jsdom-svg-polyfill.ts"]
}