feat: Implement email/password and X OAuth authentication, replacing the dev-login mechanism.
This commit is contained in:
@@ -5,6 +5,63 @@ const assert = require("node:assert/strict");
|
||||
const { buildApp } = require("../src/app");
|
||||
const { hmacSHA256Hex } = require("../src/lib/signature");
|
||||
|
||||
function getTestCookieValue(cookieHeader, name) {
|
||||
const parts = String(cookieHeader || "").split(";").map((part) => part.trim());
|
||||
const prefix = `${name}=`;
|
||||
const valuePart = parts.find((part) => part.startsWith(prefix));
|
||||
if (!valuePart) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return decodeURIComponent(valuePart.slice(prefix.length));
|
||||
} catch {
|
||||
return valuePart.slice(prefix.length);
|
||||
}
|
||||
}
|
||||
|
||||
function createTestAuthAdapter() {
|
||||
return {
|
||||
isConfigured() {
|
||||
return true;
|
||||
},
|
||||
isXOAuthConfigured() {
|
||||
return true;
|
||||
},
|
||||
handlesPath() {
|
||||
return false;
|
||||
},
|
||||
async handleRoute() {
|
||||
return { status: 404, headers: {}, body: "not_found" };
|
||||
},
|
||||
async getAuthenticatedUserId(headers = {}) {
|
||||
return getTestCookieValue(headers.cookie, "xartaudio_user");
|
||||
},
|
||||
async signInWithEmail({ email }) {
|
||||
return {
|
||||
setCookie: `xartaudio_user=${encodeURIComponent(String(email))}; Path=/; HttpOnly`,
|
||||
};
|
||||
},
|
||||
async signUpWithEmail({ email }) {
|
||||
return {
|
||||
setCookie: `xartaudio_user=${encodeURIComponent(String(email))}; Path=/; HttpOnly`,
|
||||
};
|
||||
},
|
||||
async getXAuthorizationUrl() {
|
||||
return {
|
||||
url: "https://x.com/i/oauth2/authorize?state=test",
|
||||
setCookie: "xartaudio_oauth_state=test; Path=/; HttpOnly",
|
||||
};
|
||||
},
|
||||
async signOut() {
|
||||
return {
|
||||
ok: true,
|
||||
setCookie: "xartaudio_user=; Path=/; Max-Age=0",
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createApp(options = {}) {
|
||||
const baseConfig = {
|
||||
xWebhookSecret: "x-secret",
|
||||
@@ -17,7 +74,8 @@ function createApp(options = {}) {
|
||||
appBaseUrl: "http://localhost:3000",
|
||||
betterAuthSecret: "test-better-auth-secret",
|
||||
betterAuthBasePath: "/api/auth",
|
||||
betterAuthDevPassword: "xartaudio-dev-password",
|
||||
xOAuthClientId: "x-client-id",
|
||||
xOAuthClientSecret: "x-client-secret",
|
||||
internalApiToken: "",
|
||||
convexDeploymentUrl: "",
|
||||
convexAuthToken: "",
|
||||
@@ -75,6 +133,9 @@ function createApp(options = {}) {
|
||||
|
||||
const appOptions = { ...options };
|
||||
delete appOptions.config;
|
||||
if (!appOptions.authAdapter) {
|
||||
appOptions.authAdapter = createTestAuthAdapter();
|
||||
}
|
||||
|
||||
return buildApp({
|
||||
config: mergedConfig,
|
||||
@@ -108,7 +169,7 @@ test("GET / renders landing page", async () => {
|
||||
const app = createApp();
|
||||
const response = await call(app, { method: "GET", path: "/" });
|
||||
assert.equal(response.status, 200);
|
||||
assert.match(response.body, /From X Article to audiobook in one mention/);
|
||||
assert.match(response.body, /From X Article to audiobook/);
|
||||
});
|
||||
|
||||
test("GET /assets/styles.css serves compiled stylesheet", async () => {
|
||||
@@ -127,17 +188,17 @@ test("unauthenticated /app redirects to /login with returnTo", async () => {
|
||||
assert.match(response.headers.location, /returnTo=%2Fapp/);
|
||||
});
|
||||
|
||||
test("POST /auth/dev-login sets cookie and redirects", async () => {
|
||||
test("POST /auth/email/sign-in sets cookie and redirects", async () => {
|
||||
const app = createApp();
|
||||
const response = await call(app, {
|
||||
method: "POST",
|
||||
path: "/auth/dev-login",
|
||||
body: "userId=matiss&returnTo=%2Fapp",
|
||||
path: "/auth/email/sign-in",
|
||||
body: "email=matiss%40example.com&password=password123&returnTo=%2Fapp",
|
||||
});
|
||||
|
||||
assert.equal(response.status, 303);
|
||||
assert.equal(response.headers.location, "/app");
|
||||
assert.match(String(response.headers["set-cookie"]), /HttpOnly/);
|
||||
assert.match(String(response.headers["set-cookie"]), /xartaudio_user=matiss%40example\.com/);
|
||||
});
|
||||
|
||||
test("authenticated dashboard topup + simulate mention flow", async () => {
|
||||
@@ -954,15 +1015,15 @@ test("rate limits repeated login attempts from same IP", async () => {
|
||||
|
||||
const first = await call(app, {
|
||||
method: "POST",
|
||||
path: "/auth/dev-login",
|
||||
path: "/auth/email/sign-in",
|
||||
headers: { "x-forwarded-for": "5.5.5.5" },
|
||||
body: "userId=alice&returnTo=%2Fapp",
|
||||
body: "email=alice%40example.com&password=password123&returnTo=%2Fapp",
|
||||
});
|
||||
const second = await call(app, {
|
||||
method: "POST",
|
||||
path: "/auth/dev-login",
|
||||
path: "/auth/email/sign-in",
|
||||
headers: { "x-forwarded-for": "5.5.5.5" },
|
||||
body: "userId=alice&returnTo=%2Fapp",
|
||||
body: "email=alice%40example.com&password=password123&returnTo=%2Fapp",
|
||||
});
|
||||
|
||||
assert.equal(first.status, 303);
|
||||
|
||||
Reference in New Issue
Block a user