@useatlas/react 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.
- package/README.md +95 -0
- package/dist/chunk-2WFDP7G5.js +231 -0
- package/dist/chunk-2WFDP7G5.js.map +1 -0
- package/dist/chunk-44HBZYKP.js +224 -0
- package/dist/chunk-44HBZYKP.js.map +1 -0
- package/dist/chunk-5SEVKHS5.cjs +229 -0
- package/dist/chunk-5SEVKHS5.cjs.map +1 -0
- package/dist/chunk-UIRB6L36.cjs +249 -0
- package/dist/chunk-UIRB6L36.cjs.map +1 -0
- package/dist/hooks.cjs +251 -0
- package/dist/hooks.cjs.map +1 -0
- package/dist/hooks.d.cts +132 -0
- package/dist/hooks.d.ts +132 -0
- package/dist/hooks.js +237 -0
- package/dist/hooks.js.map +1 -0
- package/dist/index.cjs +2976 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +69 -0
- package/dist/index.d.ts +69 -0
- package/dist/index.js +2926 -0
- package/dist/index.js.map +1 -0
- package/dist/result-chart-NFAJ4IQ5.js +398 -0
- package/dist/result-chart-NFAJ4IQ5.js.map +1 -0
- package/dist/result-chart-YLCKBNV4.cjs +400 -0
- package/dist/result-chart-YLCKBNV4.cjs.map +1 -0
- package/dist/styles.css +59 -0
- package/dist/use-dark-mode-rFxawUv1.d.cts +123 -0
- package/dist/use-dark-mode-rFxawUv1.d.ts +123 -0
- package/dist/widget.css +2 -0
- package/dist/widget.js +445 -0
- package/package.json +113 -0
- package/src/components/__tests__/tool-renderers.test.tsx +239 -0
- package/src/components/actions/action-approval-card.tsx +296 -0
- package/src/components/actions/action-status-badge.tsx +50 -0
- package/src/components/admin/change-password-dialog.tsx +128 -0
- package/src/components/atlas-chat.tsx +656 -0
- package/src/components/chart/chart-detection.ts +318 -0
- package/src/components/chart/result-chart.tsx +590 -0
- package/src/components/chat/api-key-bar.tsx +66 -0
- package/src/components/chat/copy-button.tsx +25 -0
- package/src/components/chat/data-table.tsx +104 -0
- package/src/components/chat/error-banner.tsx +32 -0
- package/src/components/chat/explore-card.tsx +41 -0
- package/src/components/chat/follow-up-chips.tsx +29 -0
- package/src/components/chat/loading-card.tsx +10 -0
- package/src/components/chat/managed-auth-card.tsx +116 -0
- package/src/components/chat/markdown.tsx +146 -0
- package/src/components/chat/python-result-card.tsx +245 -0
- package/src/components/chat/sql-block.tsx +54 -0
- package/src/components/chat/sql-result-card.tsx +163 -0
- package/src/components/chat/starter-prompts.ts +6 -0
- package/src/components/chat/tool-part.tsx +106 -0
- package/src/components/chat/typing-indicator.tsx +22 -0
- package/src/components/conversations/conversation-item.tsx +135 -0
- package/src/components/conversations/conversation-list.tsx +69 -0
- package/src/components/conversations/conversation-sidebar.tsx +113 -0
- package/src/components/conversations/delete-confirmation.tsx +27 -0
- package/src/components/schema-explorer/schema-explorer.tsx +517 -0
- package/src/components/ui/alert-dialog.tsx +196 -0
- package/src/components/ui/badge.tsx +48 -0
- package/src/components/ui/button.tsx +64 -0
- package/src/components/ui/card.tsx +92 -0
- package/src/components/ui/dialog.tsx +158 -0
- package/src/components/ui/dropdown-menu.tsx +257 -0
- package/src/components/ui/input.tsx +21 -0
- package/src/components/ui/label.tsx +24 -0
- package/src/components/ui/scroll-area.tsx +62 -0
- package/src/components/ui/separator.tsx +28 -0
- package/src/components/ui/sheet.tsx +143 -0
- package/src/components/ui/table.tsx +116 -0
- package/src/components/ui/toggle-group.tsx +83 -0
- package/src/components/ui/toggle.tsx +47 -0
- package/src/context.tsx +85 -0
- package/src/env.d.ts +9 -0
- package/src/hooks/__tests__/provider.test.tsx +83 -0
- package/src/hooks/__tests__/use-atlas-auth.test.tsx +283 -0
- package/src/hooks/__tests__/use-atlas-chat.test.tsx +157 -0
- package/src/hooks/__tests__/use-atlas-conversations.test.tsx +159 -0
- package/src/hooks/__tests__/use-atlas-theme.test.tsx +56 -0
- package/src/hooks/index.ts +47 -0
- package/src/hooks/provider.tsx +77 -0
- package/src/hooks/theme-init-script.ts +17 -0
- package/src/hooks/use-atlas-auth.ts +131 -0
- package/src/hooks/use-atlas-chat.ts +102 -0
- package/src/hooks/use-atlas-conversations.ts +61 -0
- package/src/hooks/use-atlas-theme.ts +34 -0
- package/src/hooks/use-conversations.ts +189 -0
- package/src/hooks/use-dark-mode.ts +150 -0
- package/src/index.ts +36 -0
- package/src/lib/action-types.ts +11 -0
- package/src/lib/helpers.ts +198 -0
- package/src/lib/tool-renderer-types.ts +76 -0
- package/src/lib/types.ts +29 -0
- package/src/lib/utils.ts +6 -0
- package/src/styles.css +59 -0
- package/src/test-setup.ts +55 -0
- package/src/widget-entry.ts +20 -0
- package/src/widget.css +12 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { type VariantProps } from "class-variance-authority"
|
|
5
|
+
import { ToggleGroup as ToggleGroupPrimitive } from "radix-ui"
|
|
6
|
+
|
|
7
|
+
import { cn } from "../../lib/utils"
|
|
8
|
+
import { toggleVariants } from "./toggle"
|
|
9
|
+
|
|
10
|
+
const ToggleGroupContext = React.createContext<
|
|
11
|
+
VariantProps<typeof toggleVariants> & {
|
|
12
|
+
spacing?: number
|
|
13
|
+
}
|
|
14
|
+
>({
|
|
15
|
+
size: "default",
|
|
16
|
+
variant: "default",
|
|
17
|
+
spacing: 0,
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
function ToggleGroup({
|
|
21
|
+
className,
|
|
22
|
+
variant,
|
|
23
|
+
size,
|
|
24
|
+
spacing = 0,
|
|
25
|
+
children,
|
|
26
|
+
...props
|
|
27
|
+
}: React.ComponentProps<typeof ToggleGroupPrimitive.Root> &
|
|
28
|
+
VariantProps<typeof toggleVariants> & {
|
|
29
|
+
spacing?: number
|
|
30
|
+
}) {
|
|
31
|
+
return (
|
|
32
|
+
<ToggleGroupPrimitive.Root
|
|
33
|
+
data-slot="toggle-group"
|
|
34
|
+
data-variant={variant}
|
|
35
|
+
data-size={size}
|
|
36
|
+
data-spacing={spacing}
|
|
37
|
+
style={{ "--gap": spacing } as React.CSSProperties}
|
|
38
|
+
className={cn(
|
|
39
|
+
"group/toggle-group flex w-fit items-center gap-[--spacing(var(--gap))] rounded-md data-[spacing=default]:data-[variant=outline]:shadow-xs",
|
|
40
|
+
className
|
|
41
|
+
)}
|
|
42
|
+
{...props}
|
|
43
|
+
>
|
|
44
|
+
<ToggleGroupContext.Provider value={{ variant, size, spacing }}>
|
|
45
|
+
{children}
|
|
46
|
+
</ToggleGroupContext.Provider>
|
|
47
|
+
</ToggleGroupPrimitive.Root>
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function ToggleGroupItem({
|
|
52
|
+
className,
|
|
53
|
+
children,
|
|
54
|
+
variant,
|
|
55
|
+
size,
|
|
56
|
+
...props
|
|
57
|
+
}: React.ComponentProps<typeof ToggleGroupPrimitive.Item> &
|
|
58
|
+
VariantProps<typeof toggleVariants>) {
|
|
59
|
+
const context = React.useContext(ToggleGroupContext)
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<ToggleGroupPrimitive.Item
|
|
63
|
+
data-slot="toggle-group-item"
|
|
64
|
+
data-variant={context.variant || variant}
|
|
65
|
+
data-size={context.size || size}
|
|
66
|
+
data-spacing={context.spacing}
|
|
67
|
+
className={cn(
|
|
68
|
+
toggleVariants({
|
|
69
|
+
variant: context.variant || variant,
|
|
70
|
+
size: context.size || size,
|
|
71
|
+
}),
|
|
72
|
+
"w-auto min-w-0 shrink-0 px-3 focus:z-10 focus-visible:z-10",
|
|
73
|
+
"data-[spacing=0]:rounded-none data-[spacing=0]:shadow-none data-[spacing=0]:first:rounded-l-md data-[spacing=0]:last:rounded-r-md data-[spacing=0]:data-[variant=outline]:border-l-0 data-[spacing=0]:data-[variant=outline]:first:border-l",
|
|
74
|
+
className
|
|
75
|
+
)}
|
|
76
|
+
{...props}
|
|
77
|
+
>
|
|
78
|
+
{children}
|
|
79
|
+
</ToggleGroupPrimitive.Item>
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export { ToggleGroup, ToggleGroupItem }
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
5
|
+
import { Toggle as TogglePrimitive } from "radix-ui"
|
|
6
|
+
|
|
7
|
+
import { cn } from "../../lib/utils"
|
|
8
|
+
|
|
9
|
+
const toggleVariants = cva(
|
|
10
|
+
"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-[color,box-shadow] outline-none hover:bg-muted hover:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
11
|
+
{
|
|
12
|
+
variants: {
|
|
13
|
+
variant: {
|
|
14
|
+
default: "bg-transparent",
|
|
15
|
+
outline:
|
|
16
|
+
"border border-input bg-transparent shadow-xs hover:bg-accent hover:text-accent-foreground",
|
|
17
|
+
},
|
|
18
|
+
size: {
|
|
19
|
+
default: "h-9 min-w-9 px-2",
|
|
20
|
+
sm: "h-8 min-w-8 px-1.5",
|
|
21
|
+
lg: "h-10 min-w-10 px-2.5",
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
defaultVariants: {
|
|
25
|
+
variant: "default",
|
|
26
|
+
size: "default",
|
|
27
|
+
},
|
|
28
|
+
}
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
function Toggle({
|
|
32
|
+
className,
|
|
33
|
+
variant,
|
|
34
|
+
size,
|
|
35
|
+
...props
|
|
36
|
+
}: React.ComponentProps<typeof TogglePrimitive.Root> &
|
|
37
|
+
VariantProps<typeof toggleVariants>) {
|
|
38
|
+
return (
|
|
39
|
+
<TogglePrimitive.Root
|
|
40
|
+
data-slot="toggle"
|
|
41
|
+
className={cn(toggleVariants({ variant, size, className }))}
|
|
42
|
+
{...props}
|
|
43
|
+
/>
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export { Toggle, toggleVariants }
|
package/src/context.tsx
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { createContext, useContext, useMemo, type ReactNode } from "react";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Duck-typed interface that matches better-auth's client shape.
|
|
7
|
+
* Components like ManagedAuthCard call signIn/signUp/signOut and useSession().
|
|
8
|
+
*/
|
|
9
|
+
export interface AtlasAuthClient {
|
|
10
|
+
signIn: {
|
|
11
|
+
email: (opts: { email: string; password: string }) => Promise<{ error?: { message?: string } | null }>;
|
|
12
|
+
};
|
|
13
|
+
signUp: {
|
|
14
|
+
email: (opts: { email: string; password: string; name: string }) => Promise<{ error?: { message?: string } | null }>;
|
|
15
|
+
};
|
|
16
|
+
signOut: () => Promise<unknown>;
|
|
17
|
+
useSession: () => { data?: { user?: { email?: string } } | null; isPending?: boolean };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface AtlasUIConfig {
|
|
21
|
+
apiUrl: string;
|
|
22
|
+
authClient: AtlasAuthClient;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Internal context value — includes derived `isCrossOrigin` computed by the provider. */
|
|
26
|
+
interface AtlasUIContextValue extends AtlasUIConfig {
|
|
27
|
+
isCrossOrigin: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const AtlasUIContext = createContext<AtlasUIContextValue | null>(null);
|
|
31
|
+
|
|
32
|
+
export function useAtlasConfig(): AtlasUIContextValue {
|
|
33
|
+
const ctx = useContext(AtlasUIContext);
|
|
34
|
+
if (!ctx) throw new Error("useAtlasConfig must be used within <AtlasUIProvider>");
|
|
35
|
+
return ctx;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function AtlasUIProvider({
|
|
39
|
+
config,
|
|
40
|
+
children,
|
|
41
|
+
}: {
|
|
42
|
+
config: AtlasUIConfig;
|
|
43
|
+
children: ReactNode;
|
|
44
|
+
}) {
|
|
45
|
+
const isCrossOrigin = typeof window !== "undefined"
|
|
46
|
+
&& config.apiUrl !== ""
|
|
47
|
+
&& !config.apiUrl.startsWith(window.location.origin);
|
|
48
|
+
return (
|
|
49
|
+
<AtlasUIContext.Provider value={{ ...config, isCrossOrigin }}>
|
|
50
|
+
{children}
|
|
51
|
+
</AtlasUIContext.Provider>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/* ------------------------------------------------------------------ */
|
|
56
|
+
/* ActionAuth — internal context for passing auth to action cards */
|
|
57
|
+
/* ------------------------------------------------------------------ */
|
|
58
|
+
|
|
59
|
+
export interface ActionAuthValue {
|
|
60
|
+
getHeaders: () => Record<string, string>;
|
|
61
|
+
getCredentials: () => RequestCredentials;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const ActionAuthContext = createContext<ActionAuthValue | null>(null);
|
|
65
|
+
|
|
66
|
+
/** Returns auth helpers for action API calls, or null when no provider is present. */
|
|
67
|
+
export function useActionAuth(): ActionAuthValue | null {
|
|
68
|
+
return useContext(ActionAuthContext);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function ActionAuthProvider({
|
|
72
|
+
getHeaders,
|
|
73
|
+
getCredentials,
|
|
74
|
+
children,
|
|
75
|
+
}: ActionAuthValue & { children: ReactNode }) {
|
|
76
|
+
const value = useMemo(
|
|
77
|
+
() => ({ getHeaders, getCredentials }),
|
|
78
|
+
[getHeaders, getCredentials],
|
|
79
|
+
);
|
|
80
|
+
return (
|
|
81
|
+
<ActionAuthContext.Provider value={value}>
|
|
82
|
+
{children}
|
|
83
|
+
</ActionAuthContext.Provider>
|
|
84
|
+
);
|
|
85
|
+
}
|
package/src/env.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Optional runtime dependency — dynamically imported for Excel export. */
|
|
2
|
+
declare module "xlsx" {
|
|
3
|
+
const utils: {
|
|
4
|
+
json_to_sheet: (data: unknown[], opts?: { header?: string[] }) => unknown;
|
|
5
|
+
book_new: () => unknown;
|
|
6
|
+
book_append_sheet: (wb: unknown, ws: unknown, name: string) => void;
|
|
7
|
+
};
|
|
8
|
+
function write(wb: unknown, opts: { bookType: string; type: string }): ArrayBuffer;
|
|
9
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { describe, it, expect } from "bun:test";
|
|
2
|
+
import { renderHook } from "@testing-library/react";
|
|
3
|
+
import { AtlasProvider, useAtlasContext } from "../provider";
|
|
4
|
+
import type { ReactNode } from "react";
|
|
5
|
+
|
|
6
|
+
function wrapper({ children }: { children: ReactNode }) {
|
|
7
|
+
return (
|
|
8
|
+
<AtlasProvider apiUrl="https://api.example.com" apiKey="test-key">
|
|
9
|
+
{children}
|
|
10
|
+
</AtlasProvider>
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe("AtlasProvider", () => {
|
|
15
|
+
it("provides context values to children", () => {
|
|
16
|
+
const { result } = renderHook(() => useAtlasContext(), { wrapper });
|
|
17
|
+
|
|
18
|
+
expect(result.current.apiUrl).toBe("https://api.example.com");
|
|
19
|
+
expect(result.current.apiKey).toBe("test-key");
|
|
20
|
+
expect(result.current.isCrossOrigin).toBe(true);
|
|
21
|
+
expect(result.current.authClient).toBeDefined();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("throws when useAtlasContext is called outside provider", () => {
|
|
25
|
+
expect(() => {
|
|
26
|
+
renderHook(() => useAtlasContext());
|
|
27
|
+
}).toThrow("useAtlasContext must be used within <AtlasProvider>");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("noop auth client warns and returns error on use", async () => {
|
|
31
|
+
const { result } = renderHook(() => useAtlasContext(), { wrapper });
|
|
32
|
+
|
|
33
|
+
const session = result.current.authClient.useSession();
|
|
34
|
+
expect(session.data).toBeNull();
|
|
35
|
+
expect(session.isPending).toBe(false);
|
|
36
|
+
|
|
37
|
+
const signInResult = await result.current.authClient.signIn.email({
|
|
38
|
+
email: "test@test.com",
|
|
39
|
+
password: "pass",
|
|
40
|
+
});
|
|
41
|
+
expect(signInResult.error?.message).toBe("Auth client not configured");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("detects same-origin URLs", () => {
|
|
45
|
+
function sameOriginWrapper({ children }: { children: ReactNode }) {
|
|
46
|
+
return (
|
|
47
|
+
<AtlasProvider apiUrl="">
|
|
48
|
+
{children}
|
|
49
|
+
</AtlasProvider>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const { result } = renderHook(() => useAtlasContext(), {
|
|
54
|
+
wrapper: sameOriginWrapper,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
expect(result.current.isCrossOrigin).toBe(false);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("accepts a custom auth client", () => {
|
|
61
|
+
const customAuthClient = {
|
|
62
|
+
signIn: { email: async () => ({ error: null }) },
|
|
63
|
+
signUp: { email: async () => ({ error: null }) },
|
|
64
|
+
signOut: async () => {},
|
|
65
|
+
useSession: () => ({ data: { user: { email: "test@test.com" } }, isPending: false }),
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
function customWrapper({ children }: { children: ReactNode }) {
|
|
69
|
+
return (
|
|
70
|
+
<AtlasProvider apiUrl="https://api.example.com" authClient={customAuthClient}>
|
|
71
|
+
{children}
|
|
72
|
+
</AtlasProvider>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const { result } = renderHook(() => useAtlasContext(), {
|
|
77
|
+
wrapper: customWrapper,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const session = result.current.authClient.useSession();
|
|
81
|
+
expect(session.data?.user?.email).toBe("test@test.com");
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, mock } from "bun:test";
|
|
2
|
+
import { renderHook, waitFor, act } from "@testing-library/react";
|
|
3
|
+
import { AtlasProvider } from "../provider";
|
|
4
|
+
import { useAtlasAuth } from "../use-atlas-auth";
|
|
5
|
+
import type { ReactNode } from "react";
|
|
6
|
+
|
|
7
|
+
const fetchMock = mock(() =>
|
|
8
|
+
Promise.resolve(
|
|
9
|
+
new Response(
|
|
10
|
+
JSON.stringify({ checks: { auth: { mode: "simple-key" } } }),
|
|
11
|
+
{ status: 200, headers: { "Content-Type": "application/json" } },
|
|
12
|
+
),
|
|
13
|
+
),
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
const originalFetch = globalThis.fetch;
|
|
17
|
+
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
globalThis.fetch = fetchMock as unknown as typeof fetch;
|
|
20
|
+
fetchMock.mockClear();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
globalThis.fetch = originalFetch;
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
function wrapper({ children }: { children: ReactNode }) {
|
|
28
|
+
return (
|
|
29
|
+
<AtlasProvider apiUrl="https://api.example.com" apiKey="test-key">
|
|
30
|
+
{children}
|
|
31
|
+
</AtlasProvider>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe("useAtlasAuth", () => {
|
|
36
|
+
it("starts in loading state", () => {
|
|
37
|
+
const { result } = renderHook(() => useAtlasAuth(), { wrapper });
|
|
38
|
+
expect(result.current.authMode).toBeNull();
|
|
39
|
+
expect(result.current.isLoading).toBe(true);
|
|
40
|
+
expect(result.current.isAuthenticated).toBe(false);
|
|
41
|
+
expect(result.current.error).toBeNull();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("detects auth mode from health endpoint", async () => {
|
|
45
|
+
const { result } = renderHook(() => useAtlasAuth(), { wrapper });
|
|
46
|
+
|
|
47
|
+
await waitFor(() => {
|
|
48
|
+
expect(result.current.authMode).toBe("simple-key");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
expect(result.current.isLoading).toBe(false);
|
|
52
|
+
expect(result.current.isAuthenticated).toBe(true);
|
|
53
|
+
expect(result.current.error).toBeNull();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("reports unauthenticated when no API key for simple-key mode", async () => {
|
|
57
|
+
function noKeyWrapper({ children }: { children: ReactNode }) {
|
|
58
|
+
return (
|
|
59
|
+
<AtlasProvider apiUrl="https://api.example.com">
|
|
60
|
+
{children}
|
|
61
|
+
</AtlasProvider>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const { result } = renderHook(() => useAtlasAuth(), {
|
|
66
|
+
wrapper: noKeyWrapper,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
await waitFor(() => {
|
|
70
|
+
expect(result.current.authMode).toBe("simple-key");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
expect(result.current.isAuthenticated).toBe(false);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("reports authenticated for none auth mode without credentials", async () => {
|
|
77
|
+
fetchMock.mockImplementation(() =>
|
|
78
|
+
Promise.resolve(
|
|
79
|
+
new Response(
|
|
80
|
+
JSON.stringify({ checks: { auth: { mode: "none" } } }),
|
|
81
|
+
{ status: 200, headers: { "Content-Type": "application/json" } },
|
|
82
|
+
),
|
|
83
|
+
),
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
function noKeyWrapper({ children }: { children: ReactNode }) {
|
|
87
|
+
return (
|
|
88
|
+
<AtlasProvider apiUrl="https://api.example.com">
|
|
89
|
+
{children}
|
|
90
|
+
</AtlasProvider>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const { result } = renderHook(() => useAtlasAuth(), {
|
|
95
|
+
wrapper: noKeyWrapper,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
await waitFor(() => {
|
|
99
|
+
expect(result.current.authMode).toBe("none");
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
expect(result.current.isAuthenticated).toBe(true);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("reports authenticated for byot mode with API key", async () => {
|
|
106
|
+
fetchMock.mockImplementation(() =>
|
|
107
|
+
Promise.resolve(
|
|
108
|
+
new Response(
|
|
109
|
+
JSON.stringify({ checks: { auth: { mode: "byot" } } }),
|
|
110
|
+
{ status: 200, headers: { "Content-Type": "application/json" } },
|
|
111
|
+
),
|
|
112
|
+
),
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
const { result } = renderHook(() => useAtlasAuth(), { wrapper });
|
|
116
|
+
|
|
117
|
+
await waitFor(() => {
|
|
118
|
+
expect(result.current.authMode).toBe("byot");
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
expect(result.current.isAuthenticated).toBe(true);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("falls back to none and sets error on health endpoint failure", async () => {
|
|
125
|
+
fetchMock.mockImplementation(() =>
|
|
126
|
+
Promise.resolve(new Response("", { status: 500 })),
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
const { result } = renderHook(() => useAtlasAuth(), { wrapper });
|
|
130
|
+
|
|
131
|
+
await waitFor(
|
|
132
|
+
() => {
|
|
133
|
+
expect(result.current.authMode).toBe("none");
|
|
134
|
+
},
|
|
135
|
+
{ timeout: 10000 },
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
expect(result.current.error).not.toBeNull();
|
|
139
|
+
expect(result.current.error!.message).toContain("500");
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("falls back to none and sets error on network failure", async () => {
|
|
143
|
+
fetchMock.mockImplementation(() =>
|
|
144
|
+
Promise.reject(new Error("Network unreachable")),
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
const { result } = renderHook(() => useAtlasAuth(), { wrapper });
|
|
148
|
+
|
|
149
|
+
await waitFor(
|
|
150
|
+
() => {
|
|
151
|
+
expect(result.current.authMode).toBe("none");
|
|
152
|
+
},
|
|
153
|
+
{ timeout: 10000 },
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
expect(result.current.error).not.toBeNull();
|
|
157
|
+
expect(result.current.error!.message).toBe("Network unreachable");
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("login delegates to authClient and returns error", async () => {
|
|
161
|
+
const mockSignIn = mock(() =>
|
|
162
|
+
Promise.resolve({ error: { message: "Invalid credentials" } }),
|
|
163
|
+
);
|
|
164
|
+
const customAuthClient = {
|
|
165
|
+
signIn: { email: mockSignIn },
|
|
166
|
+
signUp: { email: async () => ({ error: null }) },
|
|
167
|
+
signOut: async () => {},
|
|
168
|
+
useSession: () => ({ data: null, isPending: false }),
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
function customWrapper({ children }: { children: ReactNode }) {
|
|
172
|
+
return (
|
|
173
|
+
<AtlasProvider apiUrl="https://api.example.com" authClient={customAuthClient}>
|
|
174
|
+
{children}
|
|
175
|
+
</AtlasProvider>
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const { result } = renderHook(() => useAtlasAuth(), { wrapper: customWrapper });
|
|
180
|
+
|
|
181
|
+
let loginResult: { error?: string } = {};
|
|
182
|
+
await act(async () => {
|
|
183
|
+
loginResult = await result.current.login("user@test.com", "wrong");
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
expect(mockSignIn).toHaveBeenCalledWith({
|
|
187
|
+
email: "user@test.com",
|
|
188
|
+
password: "wrong",
|
|
189
|
+
});
|
|
190
|
+
expect(loginResult.error).toBe("Invalid credentials");
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("login catches thrown exceptions from auth client", async () => {
|
|
194
|
+
const customAuthClient = {
|
|
195
|
+
signIn: { email: async () => { throw new Error("Network error"); } },
|
|
196
|
+
signUp: { email: async () => ({ error: null }) },
|
|
197
|
+
signOut: async () => {},
|
|
198
|
+
useSession: () => ({ data: null, isPending: false }),
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
function customWrapper({ children }: { children: ReactNode }) {
|
|
202
|
+
return (
|
|
203
|
+
<AtlasProvider apiUrl="https://api.example.com" authClient={customAuthClient}>
|
|
204
|
+
{children}
|
|
205
|
+
</AtlasProvider>
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const { result } = renderHook(() => useAtlasAuth(), { wrapper: customWrapper });
|
|
210
|
+
|
|
211
|
+
let loginResult: { error?: string } = {};
|
|
212
|
+
await act(async () => {
|
|
213
|
+
loginResult = await result.current.login("user@test.com", "pass");
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
expect(loginResult.error).toBe("Network error");
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it("logout returns error instead of throwing", async () => {
|
|
220
|
+
const customAuthClient = {
|
|
221
|
+
signIn: { email: async () => ({ error: null }) },
|
|
222
|
+
signUp: { email: async () => ({ error: null }) },
|
|
223
|
+
signOut: async () => { throw new Error("Session expired"); },
|
|
224
|
+
useSession: () => ({ data: null, isPending: false }),
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
function customWrapper({ children }: { children: ReactNode }) {
|
|
228
|
+
return (
|
|
229
|
+
<AtlasProvider apiUrl="https://api.example.com" authClient={customAuthClient}>
|
|
230
|
+
{children}
|
|
231
|
+
</AtlasProvider>
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const { result } = renderHook(() => useAtlasAuth(), { wrapper: customWrapper });
|
|
236
|
+
|
|
237
|
+
let logoutResult: { error?: string } = {};
|
|
238
|
+
await act(async () => {
|
|
239
|
+
logoutResult = await result.current.logout();
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
expect(logoutResult.error).toBe("Session expired");
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it("managed auth with active session is authenticated", async () => {
|
|
246
|
+
fetchMock.mockImplementation(() =>
|
|
247
|
+
Promise.resolve(
|
|
248
|
+
new Response(
|
|
249
|
+
JSON.stringify({ checks: { auth: { mode: "managed" } } }),
|
|
250
|
+
{ status: 200, headers: { "Content-Type": "application/json" } },
|
|
251
|
+
),
|
|
252
|
+
),
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
const customAuthClient = {
|
|
256
|
+
signIn: { email: async () => ({ error: null }) },
|
|
257
|
+
signUp: { email: async () => ({ error: null }) },
|
|
258
|
+
signOut: async () => {},
|
|
259
|
+
useSession: () => ({
|
|
260
|
+
data: { user: { email: "user@test.com" } },
|
|
261
|
+
isPending: false,
|
|
262
|
+
}),
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
function managedWrapper({ children }: { children: ReactNode }) {
|
|
266
|
+
return (
|
|
267
|
+
<AtlasProvider apiUrl="https://api.example.com" authClient={customAuthClient}>
|
|
268
|
+
{children}
|
|
269
|
+
</AtlasProvider>
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const { result } = renderHook(() => useAtlasAuth(), { wrapper: managedWrapper });
|
|
274
|
+
|
|
275
|
+
await waitFor(() => {
|
|
276
|
+
expect(result.current.authMode).toBe("managed");
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
expect(result.current.isAuthenticated).toBe(true);
|
|
280
|
+
expect(result.current.isLoading).toBe(false);
|
|
281
|
+
expect(result.current.session?.user?.email).toBe("user@test.com");
|
|
282
|
+
});
|
|
283
|
+
});
|