acinguiux-dnr-utils 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/README.md +43 -0
  3. package/package.json +57 -0
  4. package/src/application-insights/index.ts +37 -0
  5. package/src/authentication/auth.ts +59 -0
  6. package/src/authentication/callbacks.test.ts +226 -0
  7. package/src/authentication/callbacks.ts +68 -0
  8. package/src/authentication/client-auth-guard.test.ts +116 -0
  9. package/src/authentication/client-auth-guard.ts +29 -0
  10. package/src/authentication/helpers.test.ts +41 -0
  11. package/src/authentication/helpers.ts +14 -0
  12. package/src/authentication/index.ts +8 -0
  13. package/src/authentication/middleware.ts +17 -0
  14. package/src/authentication/provider.test.ts +168 -0
  15. package/src/authentication/provider.ts +56 -0
  16. package/src/authentication/services/permissions.test.ts +102 -0
  17. package/src/authentication/services/permissions.ts +52 -0
  18. package/src/formatters/index.ts +1 -0
  19. package/src/formatters/number.test.ts +49 -0
  20. package/src/formatters/number.ts +25 -0
  21. package/src/instrumentation/browser.test.ts +118 -0
  22. package/src/instrumentation/browser.ts +24 -0
  23. package/src/instrumentation/node.test.ts +63 -0
  24. package/src/instrumentation/node.ts +81 -0
  25. package/src/loggers/auth-fetch.test.ts +383 -0
  26. package/src/loggers/auth-fetch.ts +79 -0
  27. package/src/loggers/index.ts +1 -0
  28. package/src/mappers/index.ts +1 -0
  29. package/src/mappers/name.test.ts +45 -0
  30. package/src/mappers/name.ts +16 -0
  31. package/src/ms-graph/README.md +47 -0
  32. package/src/ms-graph/get-graph-token.test.ts +116 -0
  33. package/src/ms-graph/get-graph-token.ts +41 -0
  34. package/src/ms-graph/index.ts +1 -0
  35. package/src/ms-graph/user-photo.test.ts +108 -0
  36. package/src/ms-graph/user-photo.ts +45 -0
  37. package/tsconfig.json +12 -0
  38. package/types.d.ts +26 -0
  39. package/vitest.config.js +20 -0
@@ -0,0 +1,29 @@
1
+ "use client";
2
+
3
+ import { usePathname, useRouter, useSearchParams } from "next/navigation";
4
+ import { useSession } from "next-auth/react";
5
+ import { type ReactNode, useEffect } from "react";
6
+
7
+ export function ClientAuthGuard({ children }: { children: ReactNode }) {
8
+ const isAuthDisabled = process.env.NEXT_PUBLIC_AUTH_DISABLED === "true";
9
+ const { status } = useSession();
10
+ const pathname = usePathname();
11
+ const searchParams = useSearchParams();
12
+ const router = useRouter();
13
+ const currentPath = pathname + (searchParams.toString() ? `?${searchParams}` : "");
14
+
15
+ useEffect(() => {
16
+ if (isAuthDisabled) return;
17
+
18
+ if (status === "unauthenticated") {
19
+ const isSignIn = pathname.startsWith("/auth/signin");
20
+ const callbackUrl = encodeURIComponent(isSignIn ? "/" : currentPath);
21
+ router.push(`/auth/signin?callbackUrl=${callbackUrl}`);
22
+ }
23
+ }, [status, router, currentPath, pathname, isAuthDisabled]);
24
+
25
+ if (isAuthDisabled) return children;
26
+ if (status === "loading") return null;
27
+
28
+ return children;
29
+ }
@@ -0,0 +1,41 @@
1
+ import { describe, expect, type Mock, test, vi } from "vitest";
2
+ import { auth } from "./auth";
3
+ import { extractProfile, getAccessToken } from "./helpers";
4
+
5
+ // @vitest-environment node
6
+
7
+ vi.mock("./auth", () => ({
8
+ auth: vi.fn(),
9
+ }));
10
+
11
+ describe("helpers", () => {
12
+ describe("extractProfile", () => {
13
+ test("should return parsed profile when token is provided", () => {
14
+ const token = `header.${Buffer.from(JSON.stringify({ name: "John Doe" })).toString("base64")}.signature`;
15
+ const profile = extractProfile(token);
16
+ expect(profile).toEqual({ name: "John Doe" });
17
+ });
18
+
19
+ test("should return empty object when token is not provided", () => {
20
+ const profile = extractProfile();
21
+ expect(profile).toEqual({});
22
+ });
23
+ });
24
+
25
+ describe("getAccessToken", () => {
26
+ test("should return access token when session is available", async () => {
27
+ const mockSession = { access_token: "mockAccessToken" };
28
+ (auth as Mock).mockResolvedValue(mockSession);
29
+
30
+ const accessToken = await getAccessToken();
31
+ expect(accessToken).toBe("mockAccessToken");
32
+ });
33
+
34
+ test("should return null when session is not available", async () => {
35
+ (auth as Mock).mockResolvedValue(null);
36
+
37
+ const accessToken = await getAccessToken();
38
+ expect(accessToken).toBeUndefined();
39
+ });
40
+ });
41
+ });
@@ -0,0 +1,14 @@
1
+ import { auth } from "./auth";
2
+ import type { PingIDProfile } from "./provider";
3
+
4
+ export function extractProfile(token?: string): PingIDProfile {
5
+ return token
6
+ ? JSON.parse(Buffer.from(token.split(".")[1], "base64").toString())
7
+ : ({} as PingIDProfile);
8
+ }
9
+
10
+ export const getAccessToken = async () => {
11
+ const session = await auth();
12
+
13
+ return session?.access_token;
14
+ };
@@ -0,0 +1,8 @@
1
+ export { auth, GET, POST, signIn, signOut } from "./auth";
2
+ export { callbacks } from "./callbacks";
3
+ export { ClientAuthGuard } from "./client-auth-guard";
4
+ export { extractProfile, getAccessToken } from "./helpers";
5
+ export { default as middleware } from "./middleware";
6
+ export type { PingIDProfile } from "./provider";
7
+ export { default as PingID } from "./provider";
8
+ export { getUserPermissions } from "./services/permissions";
@@ -0,0 +1,17 @@
1
+ import { NextResponse } from "next/server";
2
+
3
+ import { auth, isAuthDisabled } from "./auth";
4
+
5
+ const middleware = isAuthDisabled
6
+ ? () => NextResponse.next()
7
+ : auth((request) => {
8
+ if (!request?.auth?.access_token) {
9
+ const url = request.nextUrl.clone();
10
+ url.pathname = "/api/auth/signin";
11
+ url.searchParams.set("callbackUrl", `${request.nextUrl.pathname}${request.nextUrl.search}`);
12
+
13
+ return NextResponse.redirect(url);
14
+ }
15
+ });
16
+
17
+ export default middleware;
@@ -0,0 +1,168 @@
1
+ import type { JWT } from "next-auth/jwt";
2
+ import { beforeEach, describe, expect, it, vi } from "vitest";
3
+ import * as helpers from "./helpers";
4
+ import PingID, { type PingIDProfile } from "./provider";
5
+
6
+ vi.mock("./helpers");
7
+
8
+ describe("PingID Provider", () => {
9
+ const mockOptions = {
10
+ clientId: "test-client-id",
11
+ baseUrl: "https://sso.example.com",
12
+ };
13
+
14
+ beforeEach(() => {
15
+ vi.clearAllMocks();
16
+ });
17
+
18
+ describe("provider configuration", () => {
19
+ it("should return a valid OAuthConfig with correct basic properties", () => {
20
+ const provider = PingID(mockOptions);
21
+
22
+ expect(provider.id).toBe("pingid");
23
+ expect(provider.name).toBe("PingID");
24
+ expect(provider.type).toBe("oidc");
25
+ expect(provider.issuer).toBe("https://sso.example.com");
26
+ });
27
+
28
+ it("should set client auth method to none", () => {
29
+ const provider = PingID(mockOptions);
30
+
31
+ expect(provider.client).toEqual({
32
+ token_endpoint_auth_method: "none",
33
+ });
34
+ });
35
+
36
+ it("should include style configuration", () => {
37
+ const provider = PingID(mockOptions);
38
+
39
+ expect(provider.style).toEqual({ logo: "" });
40
+ });
41
+
42
+ it("should include options in returned config", () => {
43
+ const provider = PingID(mockOptions) as any;
44
+
45
+ expect(provider.options).toEqual(mockOptions);
46
+ });
47
+ });
48
+
49
+ describe("userinfo request", () => {
50
+ it("should extract profile from access token", async () => {
51
+ const mockProfile: Partial<PingIDProfile> = {
52
+ mail: "user@example.com",
53
+ givenName: "John",
54
+ sn: "Doe",
55
+ };
56
+
57
+ vi.mocked(helpers.extractProfile).mockReturnValue(mockProfile as any);
58
+
59
+ const provider = PingID(mockOptions);
60
+ const mockToken: JWT = { access_token: "test_token" };
61
+
62
+ const result = await provider.userinfo?.request?.({ tokens: mockToken });
63
+
64
+ expect(helpers.extractProfile).toHaveBeenCalledWith("test_token");
65
+ expect(result).toEqual(mockProfile);
66
+ });
67
+
68
+ it("should handle missing access token", async () => {
69
+ const emptyProfile: PingIDProfile = {
70
+ scope: [],
71
+ authorization_details: [],
72
+ client_id: "",
73
+ accessGrantGuid: "",
74
+ iss: "",
75
+ jti: "",
76
+ aud: "",
77
+ sub: "",
78
+ uid: "",
79
+ clientid: "",
80
+ mail: "",
81
+ givenName: "",
82
+ sn: "",
83
+ exp: 0,
84
+ };
85
+
86
+ vi.mocked(helpers.extractProfile).mockReturnValue(emptyProfile);
87
+
88
+ const provider = PingID(mockOptions);
89
+ const mockToken: JWT = { access_token: undefined };
90
+
91
+ const result = await provider.userinfo?.request?.({ tokens: mockToken });
92
+
93
+ expect(result).toEqual(emptyProfile);
94
+ });
95
+ });
96
+
97
+ describe("profile callback", () => {
98
+ it("should transform profile correctly", () => {
99
+ const provider = PingID(mockOptions);
100
+ const profileFn = provider.profile as any;
101
+ expect(profileFn).toBeDefined();
102
+ expect(typeof profileFn).toBe("function");
103
+
104
+ const mockProfile: PingIDProfile = {
105
+ scope: [],
106
+ authorization_details: [],
107
+ client_id: "client123",
108
+ accessGrantGuid: "guid123",
109
+ iss: "https://sso.example.com",
110
+ jti: "jti123",
111
+ aud: "aud123",
112
+ sub: "sub123",
113
+ uid: "uid123",
114
+ clientid: "clientid123",
115
+ mail: "test@example.com",
116
+ givenName: "John",
117
+ sn: "Doe",
118
+ exp: 1234567890,
119
+ };
120
+
121
+ const result = profileFn(mockProfile);
122
+ expect(result).toBeDefined();
123
+ });
124
+
125
+ it("should have correct provider properties for name and email extraction", () => {
126
+ const provider = PingID(mockOptions);
127
+ expect(provider.id).toBe("pingid");
128
+ expect(provider.type).toBe("oidc");
129
+ expect(provider.issuer).toBe("https://sso.example.com");
130
+ });
131
+
132
+ it("should handle profile with special characters in config", () => {
133
+ const customOptions = {
134
+ clientId: "client123",
135
+ baseUrl: "https://custom-sso.example.com",
136
+ };
137
+
138
+ const provider = PingID(customOptions);
139
+ expect(provider.issuer).toBe("https://custom-sso.example.com");
140
+ });
141
+ });
142
+
143
+ describe("provider initialization with different options", () => {
144
+ it("should accept different issuer URLs", () => {
145
+ const customOptions = {
146
+ clientId: "client-456",
147
+ baseUrl: "https://custom-sso.example.com",
148
+ };
149
+
150
+ const provider = PingID(customOptions);
151
+
152
+ expect(provider.issuer).toBe("https://custom-sso.example.com");
153
+ });
154
+
155
+ it("should maintain options reference in config", () => {
156
+ const customOptions = {
157
+ clientId: "test-id",
158
+ baseUrl: "https://test.com",
159
+ customProperty: "custom-value",
160
+ };
161
+
162
+ const provider = PingID(customOptions) as any;
163
+
164
+ expect(provider.options).toBe(customOptions);
165
+ expect(provider.options.customProperty).toBe("custom-value");
166
+ });
167
+ });
168
+ });
@@ -0,0 +1,56 @@
1
+ import type { OAuthConfig } from "@auth/core/providers";
2
+ import type { JWT } from "next-auth/jwt";
3
+
4
+ import { extractProfile } from "./helpers";
5
+
6
+ export interface PingIDProfile {
7
+ scope: [];
8
+ authorization_details: [];
9
+ client_id: string;
10
+ accessGrantGuid: string;
11
+ iss: string;
12
+ jti: string;
13
+ aud: string;
14
+ sub: string;
15
+ uid: string;
16
+ clientid: string;
17
+ mail: string;
18
+ givenName: string;
19
+ sn: string;
20
+ exp: number;
21
+ }
22
+
23
+ export default function PingID(
24
+ options: Record<string, unknown>,
25
+ ): OAuthConfig<PingIDProfile> & { options: Record<string, unknown> } {
26
+ return {
27
+ id: "pingid",
28
+ name: "PingID",
29
+ type: "oidc",
30
+ client: {
31
+ token_endpoint_auth_method: "none",
32
+ },
33
+ issuer: `${options.baseUrl}`,
34
+ userinfo: {
35
+ // The userinfo endpoint needs to be enabled by SIMAAS so we overwrite
36
+ // the request and decode the JWT instead to get the user profile
37
+
38
+ async request({ tokens }: { tokens: JWT }) {
39
+ return extractProfile(tokens.access_token);
40
+ },
41
+ },
42
+ // Profile is returned from the userinfo request
43
+ profile(profile) {
44
+ // In future we can add more fields here from the user profile
45
+
46
+ return {
47
+ name: `${profile.givenName} ${profile.sn}`,
48
+ email: profile.mail,
49
+ };
50
+ },
51
+ style: {
52
+ logo: "",
53
+ },
54
+ options,
55
+ };
56
+ }
@@ -0,0 +1,102 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+
3
+ import { getAccessToken } from "../helpers";
4
+
5
+ import { getUserPermissions } from "./permissions";
6
+
7
+ vi.mock("../helpers", () => ({
8
+ getAccessToken: vi.fn(),
9
+ }));
10
+
11
+ describe("getUserPermissions", () => {
12
+ const AUTHORISATION_URL = "https://example.com";
13
+ const App_Abbr = "TEST_APP";
14
+
15
+ const mockPermissions = [
16
+ {
17
+ group_name: "Admin",
18
+ items: [
19
+ {
20
+ technical_name: "item1",
21
+ item_name: "Item 1",
22
+ api_endpoint: "/api/item1",
23
+ permissions: {
24
+ create: true,
25
+ update: true,
26
+ delete: true,
27
+ read: true,
28
+ },
29
+ },
30
+ ],
31
+ },
32
+ ];
33
+
34
+ const mockPermissionsNoAccess = {
35
+ context: {
36
+ errors: [
37
+ {
38
+ msg: "Given user is not allowed to access any resource.",
39
+ loc: null,
40
+ },
41
+ ],
42
+ },
43
+ error: "HTTPException",
44
+ };
45
+
46
+ beforeEach(() => {
47
+ vi.resetAllMocks();
48
+ process.env.AUTHORISATION_URL = AUTHORISATION_URL;
49
+ });
50
+
51
+ it("should return user permissions on successful fetch", async () => {
52
+ vi.mocked(getAccessToken).mockResolvedValue("dummy_token");
53
+ global.fetch = vi.fn().mockResolvedValue({
54
+ ok: true,
55
+ json: async () => ({ context: mockPermissions }),
56
+ });
57
+
58
+ const result = await getUserPermissions(App_Abbr);
59
+ expect(result).toEqual(mockPermissions);
60
+ });
61
+
62
+ it("should handle fetch failure due to network issues", async () => {
63
+ vi.mocked(getAccessToken).mockResolvedValue("dummy_token");
64
+ global.fetch = vi.fn().mockRejectedValue(new Error("Network Error"));
65
+
66
+ const result = await getUserPermissions(App_Abbr);
67
+ expect(result).toEqual([]);
68
+ });
69
+
70
+ it("should handle fetch failure if user have no access", async () => {
71
+ vi.mocked(getAccessToken).mockResolvedValue("dummy_token");
72
+ global.fetch = vi.fn().mockResolvedValue({
73
+ ok: false,
74
+ status: 403,
75
+ statusText: "Access Forbidden",
76
+ json: async () => mockPermissionsNoAccess,
77
+ });
78
+
79
+ const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
80
+
81
+ const result = await getUserPermissions(App_Abbr);
82
+
83
+ expect(result).toEqual({
84
+ errors: [
85
+ {
86
+ msg: "Given user is not allowed to access any resource.",
87
+ loc: null,
88
+ },
89
+ ],
90
+ });
91
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
92
+ "fetch ted api/permissions - 403 Access Forbidden",
93
+ );
94
+ });
95
+
96
+ it("should return empty array if AUTHORISATION_URL is not set", async () => {
97
+ delete process.env.AUTHORISATION_URL;
98
+
99
+ const result = await getUserPermissions(App_Abbr);
100
+ expect(result).toEqual([]);
101
+ });
102
+ });
@@ -0,0 +1,52 @@
1
+ import { getAccessToken } from "../helpers";
2
+
3
+ export interface UserAuthorization {
4
+ group_name?: string;
5
+ item_name?: string;
6
+ items?: {
7
+ technical_name: string | null;
8
+ item_name: string;
9
+ api_endpoint: string;
10
+ permissions: {
11
+ create: boolean;
12
+ update: boolean;
13
+ delete: boolean;
14
+ read: boolean;
15
+ };
16
+ }[];
17
+ }
18
+
19
+ export const getUserPermissions = async (App_Abbr: string): Promise<UserAuthorization[]> => {
20
+ let roles: UserAuthorization[] = [];
21
+ const { AUTHORISATION_URL } = process.env;
22
+
23
+ if (AUTHORISATION_URL) {
24
+ try {
25
+ const accessToken = await getAccessToken();
26
+
27
+ const response = await fetch(`${AUTHORISATION_URL}/api/permissions`, {
28
+ next: { revalidate: 54000 }, // 15 minutes
29
+ method: "GET",
30
+ headers: {
31
+ Authorization: `Bearer ${accessToken}`,
32
+ "Content-Type": "application/json",
33
+ "Referrer-Application": `${App_Abbr}`,
34
+ },
35
+ });
36
+
37
+ const tedAuthorization = await response.json();
38
+ roles = tedAuthorization?.context;
39
+
40
+ if (!response.ok) {
41
+ console.error(`fetch ted api/permissions - ${response.status} ${response.statusText}`);
42
+ throw new Error(`${response.status} ${response.statusText}`);
43
+ }
44
+ } catch (error) {
45
+ console.error("Error fetching user permissions:", error);
46
+
47
+ return roles;
48
+ }
49
+ }
50
+
51
+ return roles;
52
+ };
@@ -0,0 +1 @@
1
+ export * from "./number";
@@ -0,0 +1,49 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { formatNumber } from "./number";
4
+
5
+ describe("formatNumber", () => {
6
+ it("should format a positive integer correctly", () => {
7
+ const result = formatNumber(1234);
8
+ expect(result).toBe("1,234.00"); // Assuming default locale with 2 decimal places
9
+ });
10
+
11
+ it("should format a positive number with custom decimal places", () => {
12
+ const result = formatNumber(1234.5678, { decimalPlaces: 3 });
13
+ expect(result).toBe("1,234.568");
14
+ });
15
+
16
+ it("should format a negative number correctly", () => {
17
+ const result = formatNumber(-1234);
18
+ expect(result).toBe("-1,234.00");
19
+ });
20
+
21
+ it("should format a negative number with custom decimal places", () => {
22
+ const result = formatNumber(-1234.5678, { decimalPlaces: 1 });
23
+ expect(result).toBe("-1,234.6");
24
+ });
25
+
26
+ it("should throw an error for NaN input", () => {
27
+ expect(() => formatNumber("abc")).toThrow("Number is NaN. Expected a number or string number.");
28
+ });
29
+
30
+ it("should throw an error for negative decimal places", () => {
31
+ expect(() => formatNumber(1234, { decimalPlaces: -1 })).toThrow(
32
+ "Fraction digits must be a non-negative number.",
33
+ );
34
+ });
35
+
36
+ it.skip("should handle formatting with additional Intl.NumberFormatOptions", () => {
37
+ const result = formatNumber(1234.5678, {
38
+ decimalPlaces: 2,
39
+ style: "currency",
40
+ currency: "USD",
41
+ });
42
+ expect(result).toBe("$1,234.57");
43
+ });
44
+
45
+ it("should handle formatting with number string input", () => {
46
+ const result = formatNumber("1234.5678", { decimalPlaces: 2 });
47
+ expect(result).toBe("1,234.57");
48
+ });
49
+ });
@@ -0,0 +1,25 @@
1
+ interface NumberFormatOptions extends Intl.NumberFormatOptions {
2
+ decimalPlaces?: number;
3
+ }
4
+
5
+ export function formatNumber(number: string | number, options: NumberFormatOptions = {}): string {
6
+ const numValue = Number(number);
7
+
8
+ if (Number.isNaN(numValue)) {
9
+ throw new Error("Number is NaN. Expected a number or string number.");
10
+ }
11
+
12
+ const { decimalPlaces = 2, ...rest } = options;
13
+
14
+ if (decimalPlaces < 0 || decimalPlaces === undefined) {
15
+ throw new Error("Fraction digits must be a non-negative number.");
16
+ }
17
+
18
+ const formattedValue = new Intl.NumberFormat(undefined, {
19
+ ...rest,
20
+ minimumFractionDigits: decimalPlaces,
21
+ maximumFractionDigits: decimalPlaces,
22
+ }).format(numValue);
23
+
24
+ return formattedValue;
25
+ }
@@ -0,0 +1,118 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
+
3
+ // Mock only Grafana Faro SDK
4
+ const mockInitializeFaro = vi.fn();
5
+ const mockGetWebInstrumentations = vi.fn(() => []);
6
+ const mockFaro = { api: null };
7
+
8
+ vi.mock("@grafana/faro-web-sdk", () => ({
9
+ faro: mockFaro,
10
+ initializeFaro: mockInitializeFaro,
11
+ getWebInstrumentations: mockGetWebInstrumentations,
12
+ }));
13
+
14
+ describe("browser instrumentation", () => {
15
+ const originalEnv = process.env.NEXT_PUBLIC_FARO_URL;
16
+
17
+ beforeEach(() => {
18
+ vi.clearAllMocks();
19
+ delete process.env.NEXT_PUBLIC_FARO_URL;
20
+ mockFaro.api = null;
21
+ });
22
+
23
+ afterEach(() => {
24
+ if (originalEnv) {
25
+ process.env.NEXT_PUBLIC_FARO_URL = originalEnv;
26
+ }
27
+ });
28
+
29
+ it("should return null when faro.api is already initialized", async () => {
30
+ mockFaro.api = {} as any;
31
+ process.env.NEXT_PUBLIC_FARO_URL = "https://faro.example.com";
32
+
33
+ const { instrument } = await import("./browser");
34
+ const result = instrument("test-product", { name: "Test", email: "test@example.com" });
35
+
36
+ expect(result).toBeNull();
37
+ expect(mockInitializeFaro).not.toHaveBeenCalled();
38
+ });
39
+
40
+ it("should return null when NEXT_PUBLIC_FARO_URL is not set", async () => {
41
+ delete process.env.NEXT_PUBLIC_FARO_URL;
42
+
43
+ const { instrument } = await import("./browser");
44
+ const result = instrument("test-product", { name: "Test", email: "test@example.com" });
45
+
46
+ expect(result).toBeNull();
47
+ expect(mockInitializeFaro).not.toHaveBeenCalled();
48
+ });
49
+
50
+ it("should initialize Faro when NEXT_PUBLIC_FARO_URL is set and faro.api is null", async () => {
51
+ process.env.NEXT_PUBLIC_FARO_URL = "https://faro.example.com";
52
+
53
+ const { instrument } = await import("./browser");
54
+ instrument("my-product", { name: "John Doe", email: "john@example.com" });
55
+
56
+ expect(mockInitializeFaro).toHaveBeenCalledWith({
57
+ url: "https://faro.example.com/collect",
58
+ app: {
59
+ name: "my-product-browser",
60
+ namespace: "geneva-my-product",
61
+ },
62
+ user: {
63
+ email: "john@example.com",
64
+ fullName: "John Doe",
65
+ },
66
+ pageTracking: {
67
+ generatePageId: expect.any(Function),
68
+ },
69
+ trackGeolocation: true,
70
+ instrumentations: [],
71
+ });
72
+ });
73
+
74
+ it("should handle user with empty email", async () => {
75
+ process.env.NEXT_PUBLIC_FARO_URL = "https://faro.example.com";
76
+
77
+ const { instrument } = await import("./browser");
78
+ instrument("test-product", { email: undefined, name: "Test User" });
79
+
80
+ expect(mockInitializeFaro).toHaveBeenCalledWith(
81
+ expect.objectContaining({
82
+ user: {
83
+ email: "",
84
+ fullName: "Test User",
85
+ },
86
+ }),
87
+ );
88
+ });
89
+
90
+ it("should handle user with empty name", async () => {
91
+ process.env.NEXT_PUBLIC_FARO_URL = "https://faro.example.com";
92
+
93
+ const { instrument } = await import("./browser");
94
+ instrument("test-product", { email: "test@example.com", name: undefined });
95
+
96
+ expect(mockInitializeFaro).toHaveBeenCalledWith(
97
+ expect.objectContaining({
98
+ user: {
99
+ email: "test@example.com",
100
+ fullName: "",
101
+ },
102
+ }),
103
+ );
104
+ });
105
+
106
+ it("should create correct page ID from URL", async () => {
107
+ process.env.NEXT_PUBLIC_FARO_URL = "https://faro.example.com";
108
+
109
+ const { instrument } = await import("./browser");
110
+ instrument("test-product", { email: "test@example.com", name: "Test" });
111
+
112
+ const call = mockInitializeFaro.mock.calls[0][0];
113
+ const generatePageId = call.pageTracking.generatePageId;
114
+
115
+ expect(generatePageId({ pathname: "/home" })).toBe("/home");
116
+ expect(generatePageId({ pathname: "/about" })).toBe("/about");
117
+ });
118
+ });