mirror of
https://github.com/better-auth/better-auth.git
synced 2026-05-23 07:18:56 -05:00
test: add tests for API key quota persistence and magic link origin validation
This commit is contained in:
@@ -2628,6 +2628,45 @@ describe("api-key", async () => {
|
||||
expect(retrievedKey?.id).toBe(createdKey?.id);
|
||||
});
|
||||
|
||||
it("verifyApiKey should persist quota updates to the database when fallbackToDatabase is true", async () => {
|
||||
const { headers, user } = await signInWithTestUser();
|
||||
|
||||
const createdKey = await auth.api.createApiKey({
|
||||
body: {
|
||||
remaining: 1,
|
||||
userId: user.id,
|
||||
},
|
||||
});
|
||||
|
||||
const first = await auth.api.verifyApiKey({
|
||||
body: {
|
||||
key: createdKey.key,
|
||||
},
|
||||
});
|
||||
|
||||
expect(first.valid).toBe(true);
|
||||
expect(first.key?.remaining).toBe(0);
|
||||
|
||||
// Ensure the canonical DB row was updated (not just the cache).
|
||||
const dbAfterFirst = await auth.api.getApiKey({
|
||||
query: { id: createdKey.id },
|
||||
headers,
|
||||
});
|
||||
expect(dbAfterFirst.remaining).toBe(0);
|
||||
|
||||
// Simulate cache eviction/deletion and ensure we don't repopulate stale allowances.
|
||||
store.clear();
|
||||
|
||||
const second = await auth.api.verifyApiKey({
|
||||
body: {
|
||||
key: createdKey.key,
|
||||
},
|
||||
});
|
||||
|
||||
expect(second.valid).toBe(false);
|
||||
expect(second.error?.code).toBe("USAGE_EXCEEDED");
|
||||
});
|
||||
|
||||
it("should fallback to database when not found in storage and auto-populate storage", async () => {
|
||||
const { headers, user } = await signInWithTestUser();
|
||||
|
||||
|
||||
@@ -109,8 +109,20 @@ export async function validateApiKey({
|
||||
if (apiKey.remaining === 0 && apiKey.refillAmount === null) {
|
||||
// if there is no more remaining requests, and there is no refill amount, than the key is revoked
|
||||
try {
|
||||
if (opts.storage === "secondary-storage") {
|
||||
// Secondary storage mode: delete from storage
|
||||
if (opts.storage === "secondary-storage" && opts.fallbackToDatabase) {
|
||||
// Secondary storage with fallback: delete from storage and database
|
||||
await deleteApiKey(ctx, apiKey, opts);
|
||||
await ctx.context.adapter.delete({
|
||||
model: API_KEY_TABLE_NAME,
|
||||
where: [
|
||||
{
|
||||
field: "id",
|
||||
value: apiKey.id,
|
||||
},
|
||||
],
|
||||
});
|
||||
} else if (opts.storage === "secondary-storage") {
|
||||
// Secondary storage mode: delete from storage only
|
||||
await deleteApiKey(ctx, apiKey, opts);
|
||||
} else {
|
||||
// Database mode: delete from DB
|
||||
@@ -182,6 +194,22 @@ export async function validateApiKey({
|
||||
],
|
||||
update: updated,
|
||||
});
|
||||
} else if (opts.storage === "secondary-storage" && opts.fallbackToDatabase) {
|
||||
// Secondary storage with fallback: update database and then update storage
|
||||
const dbUpdated = await ctx.context.adapter.update<ApiKey>({
|
||||
model: API_KEY_TABLE_NAME,
|
||||
where: [
|
||||
{
|
||||
field: "id",
|
||||
value: apiKey.id,
|
||||
},
|
||||
],
|
||||
update: updated,
|
||||
});
|
||||
if (dbUpdated) {
|
||||
await setApiKey(ctx, dbUpdated, opts);
|
||||
newApiKey = dbUpdated;
|
||||
}
|
||||
} else {
|
||||
// Secondary storage mode: update in storage
|
||||
await setApiKey(ctx, updated, opts);
|
||||
|
||||
@@ -326,6 +326,52 @@ describe("magic link verify", async () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("magic link verify origin validation", async () => {
|
||||
it("should reject untrusted callbackURL on verify", async () => {
|
||||
let verificationEmail: VerificationEmail = {
|
||||
email: "",
|
||||
token: "",
|
||||
url: "",
|
||||
};
|
||||
|
||||
const { customFetchImpl, testUser } = await getTestInstance({
|
||||
trustedOrigins: ["http://localhost:3000"],
|
||||
plugins: [
|
||||
magicLink({
|
||||
async sendMagicLink(data) {
|
||||
verificationEmail = data;
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const client = createAuthClient({
|
||||
plugins: [magicLinkClient()],
|
||||
fetchOptions: {
|
||||
customFetchImpl,
|
||||
},
|
||||
baseURL: "http://localhost:3000",
|
||||
basePath: "/api/auth",
|
||||
});
|
||||
|
||||
await client.signIn.magicLink({
|
||||
email: testUser.email,
|
||||
});
|
||||
|
||||
const token =
|
||||
new URL(verificationEmail.url).searchParams.get("token") || "";
|
||||
const res = await client.magicLink.verify({
|
||||
query: {
|
||||
token,
|
||||
callbackURL: "http://malicious.com",
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.error?.status).toBe(403);
|
||||
expect(res.error?.message).toBe("Invalid callbackURL");
|
||||
});
|
||||
});
|
||||
|
||||
describe("magic link storeToken", async () => {
|
||||
it("should store token in hashed", async () => {
|
||||
let verificationEmail: VerificationEmail = {
|
||||
|
||||
Reference in New Issue
Block a user