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"]
 }

Reply via email to