This is an automated email from the ASF dual-hosted git repository.

github-merge-queue[bot] 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 39db110e74 test(frontend): add unit tests for AuthService (#6470)
39db110e74 is described below

commit 39db110e74b4bdcf0e4b0df961c25973e6bbb20c
Author: Matthew B. <[email protected]>
AuthorDate: Fri Jul 17 21:00:53 2026 -0700

    test(frontend): add unit tests for AuthService (#6470)
    
    ### What changes were proposed in this PR?
    - Adds auth.service.spec.ts covering AuthService; no production code
    changed.
    - Verifies the static access-token helpers and the login, register and
    Google HTTP endpoints.
    - Covers logout and the loginWithExistingToken branches (no token,
    expired, valid, invite-only inactive).
    ### Any related issues, documentation, discussions?
    Closes: #6458
    ### How was this PR tested?
    - Run `cd frontend && npx nx test gui
    --include="**/auth.service.spec.ts"`, expect 10 passed.
    - Full frontend suite runs in CI via `yarn test:ci`, selected by the
    auto-applied `frontend` label.
    ### Was this PR authored or co-authored using generative AI tooling?
    Co-authored with Claude Opus 4.8 in compliance with ASF
---
 .../app/common/service/user/auth.service.spec.ts   | 209 +++++++++++++++++++++
 1 file changed, 209 insertions(+)

diff --git a/frontend/src/app/common/service/user/auth.service.spec.ts 
b/frontend/src/app/common/service/user/auth.service.spec.ts
new file mode 100644
index 0000000000..25a0dbf645
--- /dev/null
+++ b/frontend/src/app/common/service/user/auth.service.spec.ts
@@ -0,0 +1,209 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { HttpClientTestingModule, HttpTestingController } from 
"@angular/common/http/testing";
+import { TestBed } from "@angular/core/testing";
+import { JwtHelperService } from "@auth0/angular-jwt";
+import { NzModalService } from "ng-zorro-antd/modal";
+import { AppSettings } from "../../app-setting";
+import { Role } from "../../type/user";
+import { AuthService, TOKEN_KEY } from "./auth.service";
+import { NotificationService } from "../notification/notification.service";
+import { GmailService } from "../gmail/gmail.service";
+import { GuiConfigService } from "../gui-config.service";
+
+describe("AuthService", () => {
+  let service: AuthService;
+  let httpMock: HttpTestingController;
+  let jwt: {
+    isTokenExpired: ReturnType<typeof vi.fn>;
+    decodeToken: ReturnType<typeof vi.fn>;
+    getTokenExpirationDate: ReturnType<typeof vi.fn>;
+  };
+  let notification: { error: ReturnType<typeof vi.fn> };
+  let config: { env: { inviteOnly: boolean } };
+  let modal: { info: ReturnType<typeof vi.fn>; create: ReturnType<typeof 
vi.fn> };
+
+  const api = AppSettings.getApiEndpoint();
+  const claims = {
+    role: Role.REGULAR,
+    userId: 5,
+    email: "[email protected]",
+    sub: "Ursula",
+    googleId: "g",
+    googleAvatar: "a",
+    comment: "c",
+    joiningReason: "r",
+  };
+
+  beforeEach(() => {
+    localStorage.clear();
+    jwt = {
+      isTokenExpired: vi.fn().mockReturnValue(false),
+      decodeToken: vi.fn().mockReturnValue(claims),
+      getTokenExpirationDate: vi.fn().mockReturnValue(new Date(Date.now() + 
60_000)),
+    };
+    notification = { error: vi.fn() };
+    config = { env: { inviteOnly: false } };
+    modal = { info: vi.fn(), create: vi.fn() };
+
+    TestBed.configureTestingModule({
+      imports: [HttpClientTestingModule],
+      providers: [
+        AuthService,
+        { provide: JwtHelperService, useValue: jwt },
+        { provide: NotificationService, useValue: notification },
+        { provide: GmailService, useValue: {} },
+        { provide: GuiConfigService, useValue: config },
+        { provide: NzModalService, useValue: modal },
+      ],
+    });
+    service = TestBed.inject(AuthService);
+    httpMock = TestBed.inject(HttpTestingController);
+  });
+
+  afterEach(() => {
+    // loginWithExistingToken()'s valid-token path schedules an auto-logout 
timer;
+    // logout() unsubscribes it so no real timer leaks into later tests / 
keeps Vitest alive.
+    service.logout();
+    httpMock.verify();
+    localStorage.clear();
+  });
+
+  it("should be created", () => {
+    expect(service).toBeTruthy();
+  });
+
+  describe("static access-token helpers", () => {
+    it("set/get/remove roundtrip through localStorage", () => {
+      expect(AuthService.getAccessToken()).toBeNull();
+
+      AuthService.setAccessToken("tok");
+      expect(localStorage.getItem(TOKEN_KEY)).toEqual("tok");
+      expect(AuthService.getAccessToken()).toEqual("tok");
+
+      AuthService.removeAccessToken();
+      expect(AuthService.getAccessToken()).toBeNull();
+    });
+  });
+
+  describe("HTTP auth endpoints", () => {
+    it("register() POSTs username/password to the register endpoint", () => {
+      service.register("alice", "pw").subscribe();
+      const req = 
httpMock.expectOne(`${api}/${AuthService.REGISTER_ENDPOINT}`);
+      expect(req.request.method).toEqual("POST");
+      expect(req.request.body).toEqual({ username: "alice", password: "pw" });
+      req.flush({ accessToken: "t" });
+    });
+
+    it("auth() POSTs username/password to the login endpoint", () => {
+      service.auth("alice", "pw").subscribe();
+      const req = httpMock.expectOne(`${api}/${AuthService.LOGIN_ENDPOINT}`);
+      expect(req.request.method).toEqual("POST");
+      expect(req.request.body).toEqual({ username: "alice", password: "pw" });
+      req.flush({ accessToken: "t" });
+    });
+
+    it("googleAuth() POSTs the raw credential with a text/plain content type", 
() => {
+      service.googleAuth("cred").subscribe();
+      const req = 
httpMock.expectOne(`${api}/${AuthService.GOOGLE_LOGIN_ENDPOINT}`);
+      expect(req.request.method).toEqual("POST");
+      expect(req.request.body).toEqual("cred");
+      expect(req.request.headers.get("Content-Type")).toEqual("text/plain");
+      req.flush({ accessToken: "t" });
+    });
+
+    const errorCases = [
+      { name: "register", call: () => service.register("alice", "pw"), 
endpoint: AuthService.REGISTER_ENDPOINT },
+      { name: "auth", call: () => service.auth("alice", "pw"), endpoint: 
AuthService.LOGIN_ENDPOINT },
+      { name: "googleAuth", call: () => service.googleAuth("cred"), endpoint: 
AuthService.GOOGLE_LOGIN_ENDPOINT },
+    ];
+
+    errorCases.forEach(({ name, call, endpoint }) => {
+      it(`${name}() propagates HTTP errors to the subscriber`, () => {
+        const onError = vi.fn();
+        call().subscribe({ error: onError });
+
+        const req = httpMock.expectOne(`${api}/${endpoint}`);
+        req.flush("nope", { status: 401, statusText: "Unauthorized" });
+
+        expect(onError).toHaveBeenCalledTimes(1);
+        expect(onError.mock.calls[0][0].status).toEqual(401);
+      });
+    });
+  });
+
+  describe("logout", () => {
+    it("removes the stored access token and returns undefined", () => {
+      AuthService.setAccessToken("tok");
+      expect(service.logout()).toBeUndefined();
+      expect(AuthService.getAccessToken()).toBeNull();
+    });
+  });
+
+  describe("loginWithExistingToken", () => {
+    it("logs out and returns undefined when no token is stored", () => {
+      expect(service.loginWithExistingToken()).toBeUndefined();
+    });
+
+    it("errors and logs out when the stored token is expired", () => {
+      AuthService.setAccessToken("tok");
+      jwt.isTokenExpired.mockReturnValue(true);
+
+      const result = service.loginWithExistingToken();
+
+      expect(result).toBeUndefined();
+      expect(notification.error).toHaveBeenCalledWith("Access token is 
expired!");
+      expect(AuthService.getAccessToken()).toBeNull();
+    });
+
+    it("returns a User built from the decoded claims for a valid token", () => 
{
+      AuthService.setAccessToken("tok");
+
+      const user = service.loginWithExistingToken();
+
+      expect(user).toEqual({
+        uid: 5,
+        name: "Ursula",
+        email: "[email protected]",
+        googleId: "g",
+        googleAvatar: "a",
+        role: Role.REGULAR,
+        comment: "c",
+        joiningReason: "r",
+      });
+    });
+
+    it("in invite-only mode, an inactive user is logged out and registration 
is checked", () => {
+      AuthService.setAccessToken("tok");
+      config.env.inviteOnly = true;
+      jwt.decodeToken.mockReturnValue({ ...claims, role: Role.INACTIVE });
+
+      const result = service.loginWithExistingToken();
+
+      expect(result).toBeUndefined();
+      const req = httpMock.expectOne(r => r.url === 
`${api}/user/joining-reason/required`);
+      expect(req.request.params.get("uid")).toEqual("5");
+      req.flush(false);
+
+      expect(modal.info).toHaveBeenCalledTimes(1);
+      expect(AuthService.getAccessToken()).toBeNull();
+    });
+  });
+});

Reply via email to