Skip to content

Commit c2c69df

Browse files
authored
Merge branch 'main' into copilot/fix-i18n-api-request-path
2 parents bf29fc9 + cfd98bc commit c2c69df

File tree

8 files changed

+421
-0
lines changed

8 files changed

+421
-0
lines changed

ROADMAP.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1283,6 +1283,17 @@ Plugin architecture refactoring to support true modular development, plugin isol
12831283

12841284
## 🐛 Bug Fixes
12851285

1286+
### Auth Registration and Login Unavailable in MSW/Server Modes (March 2026)
1287+
1288+
**Root Cause:** `createKernel.ts` (MSW mode) and `objectstack.config.ts` (Server mode) did not load `AuthPlugin`, so the kernel had no 'auth' service. All `/api/v1/auth/*` endpoints (sign-up, sign-in, get-session, sign-out) returned 404.
1289+
1290+
**Fix:**
1291+
1. Added `AuthPlugin` from `@objectstack/plugin-auth` to `objectstack.config.ts` for server mode (`pnpm dev:server`).
1292+
2. Created `authHandlers.ts` with in-memory mock implementations of better-auth endpoints for MSW mode (`pnpm dev`). Mock handlers are added to `customHandlers` in both `browser.ts` and `server.ts`.
1293+
3. Mock handlers support: sign-up/email, sign-in/email, get-session, sign-out, forgot-password, reset-password, update-user.
1294+
1295+
**Tests:** 11 new auth handler tests, all existing MSW (7) and auth (24) tests pass.
1296+
12861297
### Default Navigation Mode (Page) Clicks Have No Effect — Stale Closure (February 2026)
12871298

12881299
**Root Cause:** Three compounding issues created a stale closure chain in `ObjectView.tsx`:
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
/**
2+
* Tests for mock auth MSW handlers.
3+
*
4+
* These tests verify the in-memory better-auth mock endpoints
5+
* that enable sign-up / sign-in / session / sign-out flows in the
6+
* MSW (browser & test) environment.
7+
*
8+
* @vitest-environment node
9+
*/
10+
11+
import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest';
12+
import { setupServer } from 'msw/node';
13+
import { createAuthHandlers, resetAuthState } from '../mocks/authHandlers';
14+
15+
const BASE_URL = 'http://localhost/api/v1/auth';
16+
const handlers = createAuthHandlers('/api/v1/auth');
17+
const server = setupServer(...handlers);
18+
19+
beforeAll(() => server.listen({ onUnhandledRequest: 'bypass' }));
20+
afterAll(() => server.close());
21+
beforeEach(() => resetAuthState());
22+
23+
describe('Mock Auth Handlers', () => {
24+
it('should register a new user via sign-up', async () => {
25+
const res = await fetch(`${BASE_URL}/sign-up/email`, {
26+
method: 'POST',
27+
headers: { 'Content-Type': 'application/json' },
28+
body: JSON.stringify({
29+
name: 'Alice',
30+
email: 'alice@example.com',
31+
password: 'secret123',
32+
}),
33+
});
34+
35+
expect(res.ok).toBe(true);
36+
const body = await res.json();
37+
expect(body.user).toBeDefined();
38+
expect(body.user.name).toBe('Alice');
39+
expect(body.user.email).toBe('alice@example.com');
40+
expect(body.session).toBeDefined();
41+
expect(body.session.token).toBeTruthy();
42+
// Password must never be exposed
43+
expect(body.user.password).toBeUndefined();
44+
});
45+
46+
it('should reject duplicate sign-up', async () => {
47+
// Register a user first
48+
await fetch(`${BASE_URL}/sign-up/email`, {
49+
method: 'POST',
50+
headers: { 'Content-Type': 'application/json' },
51+
body: JSON.stringify({ name: 'Alice', email: 'alice@example.com', password: 'secret123' }),
52+
});
53+
54+
const res = await fetch(`${BASE_URL}/sign-up/email`, {
55+
method: 'POST',
56+
headers: { 'Content-Type': 'application/json' },
57+
body: JSON.stringify({
58+
name: 'Alice',
59+
email: 'alice@example.com',
60+
password: 'secret123',
61+
}),
62+
});
63+
64+
expect(res.status).toBe(409);
65+
const body = await res.json();
66+
expect(body.message).toMatch(/already exists/i);
67+
});
68+
69+
it('should reject sign-up without email', async () => {
70+
const res = await fetch(`${BASE_URL}/sign-up/email`, {
71+
method: 'POST',
72+
headers: { 'Content-Type': 'application/json' },
73+
body: JSON.stringify({ password: 'secret123' }),
74+
});
75+
76+
expect(res.status).toBe(400);
77+
});
78+
79+
it('should sign in with correct credentials', async () => {
80+
// Register user first
81+
await fetch(`${BASE_URL}/sign-up/email`, {
82+
method: 'POST',
83+
headers: { 'Content-Type': 'application/json' },
84+
body: JSON.stringify({ name: 'Alice', email: 'alice@example.com', password: 'secret123' }),
85+
});
86+
87+
const res = await fetch(`${BASE_URL}/sign-in/email`, {
88+
method: 'POST',
89+
headers: { 'Content-Type': 'application/json' },
90+
body: JSON.stringify({
91+
email: 'alice@example.com',
92+
password: 'secret123',
93+
}),
94+
});
95+
96+
expect(res.ok).toBe(true);
97+
const body = await res.json();
98+
expect(body.user.email).toBe('alice@example.com');
99+
expect(body.session.token).toBeTruthy();
100+
});
101+
102+
it('should reject sign-in with wrong password', async () => {
103+
const res = await fetch(`${BASE_URL}/sign-in/email`, {
104+
method: 'POST',
105+
headers: { 'Content-Type': 'application/json' },
106+
body: JSON.stringify({
107+
email: 'alice@example.com',
108+
password: 'wrong',
109+
}),
110+
});
111+
112+
expect(res.status).toBe(401);
113+
});
114+
115+
it('should return current session after sign-in', async () => {
116+
// Register and sign in
117+
await fetch(`${BASE_URL}/sign-up/email`, {
118+
method: 'POST',
119+
headers: { 'Content-Type': 'application/json' },
120+
body: JSON.stringify({ name: 'Alice', email: 'alice@example.com', password: 'secret123' }),
121+
});
122+
await fetch(`${BASE_URL}/sign-in/email`, {
123+
method: 'POST',
124+
headers: { 'Content-Type': 'application/json' },
125+
body: JSON.stringify({
126+
email: 'alice@example.com',
127+
password: 'secret123',
128+
}),
129+
});
130+
131+
const res = await fetch(`${BASE_URL}/get-session`);
132+
expect(res.ok).toBe(true);
133+
const body = await res.json();
134+
expect(body.user.email).toBe('alice@example.com');
135+
expect(body.session.token).toBeTruthy();
136+
});
137+
138+
it('should clear session on sign-out', async () => {
139+
const res = await fetch(`${BASE_URL}/sign-out`, { method: 'POST' });
140+
expect(res.ok).toBe(true);
141+
142+
const sessionRes = await fetch(`${BASE_URL}/get-session`);
143+
expect(sessionRes.status).toBe(401);
144+
});
145+
146+
it('should handle forgot-password', async () => {
147+
const res = await fetch(`${BASE_URL}/forgot-password`, {
148+
method: 'POST',
149+
headers: { 'Content-Type': 'application/json' },
150+
body: JSON.stringify({ email: 'alice@example.com' }),
151+
});
152+
expect(res.ok).toBe(true);
153+
});
154+
155+
it('should handle reset-password', async () => {
156+
const res = await fetch(`${BASE_URL}/reset-password`, {
157+
method: 'POST',
158+
headers: { 'Content-Type': 'application/json' },
159+
body: JSON.stringify({ token: 'tok', newPassword: 'newpass' }),
160+
});
161+
expect(res.ok).toBe(true);
162+
});
163+
164+
it('should update user when authenticated', async () => {
165+
// Register and sign in first
166+
await fetch(`${BASE_URL}/sign-up/email`, {
167+
method: 'POST',
168+
headers: { 'Content-Type': 'application/json' },
169+
body: JSON.stringify({ name: 'Alice', email: 'alice@example.com', password: 'secret123' }),
170+
});
171+
await fetch(`${BASE_URL}/sign-in/email`, {
172+
method: 'POST',
173+
headers: { 'Content-Type': 'application/json' },
174+
body: JSON.stringify({
175+
email: 'alice@example.com',
176+
password: 'secret123',
177+
}),
178+
});
179+
180+
const res = await fetch(`${BASE_URL}/update-user`, {
181+
method: 'POST',
182+
headers: { 'Content-Type': 'application/json' },
183+
body: JSON.stringify({ name: 'Alice Updated' }),
184+
});
185+
expect(res.ok).toBe(true);
186+
const body = await res.json();
187+
expect(body.user.name).toBe('Alice Updated');
188+
});
189+
190+
it('should reject update-user when not authenticated', async () => {
191+
const res = await fetch(`${BASE_URL}/update-user`, {
192+
method: 'POST',
193+
headers: { 'Content-Type': 'application/json' },
194+
body: JSON.stringify({ name: 'Nope' }),
195+
});
196+
expect(res.status).toBe(401);
197+
});
198+
});

0 commit comments

Comments
 (0)