mirror of
https://github.com/better-auth/better-auth.git
synced 2026-10-19 11:24:57 -05:00
fix(roblox)!: model scoped userinfo claims
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@better-auth/core": minor
|
||||
---
|
||||
|
||||
Correct Roblox profile types for optional scoped claims and nullable pictures, and normalize missing local images.
|
||||
@@ -1,11 +1,11 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { describe, expect, expectTypeOf, it, vi } from "vitest";
|
||||
|
||||
vi.mock(import("@better-fetch/fetch"), () => ({
|
||||
betterFetch: vi.fn(),
|
||||
}));
|
||||
|
||||
import { betterFetch } from "@better-fetch/fetch";
|
||||
|
||||
import type { RobloxProfile } from "./roblox";
|
||||
import { roblox } from "./roblox";
|
||||
|
||||
const mockedBetterFetch = vi.mocked(betterFetch);
|
||||
@@ -33,3 +33,75 @@ describe("roblox.getUserInfo", () => {
|
||||
expect(result?.user.email).toBe("roblox-user-1@roblox.placeholder.invalid");
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* @see https://create.roblox.com/docs/cloud/auth/oauth2-reference
|
||||
*/
|
||||
describe("Roblox userinfo claims", () => {
|
||||
it("accepts a subject-only response without the profile scope", async () => {
|
||||
const profile = { sub: "roblox-user-1" } satisfies RobloxProfile;
|
||||
mockedBetterFetch.mockResolvedValue({ data: profile, error: null });
|
||||
const provider = roblox({
|
||||
clientId: "roblox-app",
|
||||
clientSecret: "roblox-secret",
|
||||
disableDefaultScope: true,
|
||||
scope: ["openid"],
|
||||
mapProfileToUser(rawProfile) {
|
||||
expectTypeOf(rawProfile.sub).toEqualTypeOf<string>();
|
||||
expectTypeOf(rawProfile.preferred_username).toEqualTypeOf<
|
||||
string | undefined
|
||||
>();
|
||||
expectTypeOf(rawProfile.nickname).toEqualTypeOf<string | undefined>();
|
||||
expectTypeOf(rawProfile.name).toEqualTypeOf<string | undefined>();
|
||||
expectTypeOf(rawProfile.created_at).toEqualTypeOf<number | undefined>();
|
||||
expectTypeOf(rawProfile.profile).toEqualTypeOf<string | undefined>();
|
||||
expectTypeOf(rawProfile.picture).toEqualTypeOf<
|
||||
string | null | undefined
|
||||
>();
|
||||
expectTypeOf(rawProfile.custom_claim).toEqualTypeOf<unknown>();
|
||||
return {};
|
||||
},
|
||||
});
|
||||
const result = await provider.getUserInfo({ accessToken: "access-token" });
|
||||
|
||||
expect(result?.user).toEqual({
|
||||
name: "",
|
||||
image: undefined,
|
||||
email: "roblox-user-1@roblox.placeholder.invalid",
|
||||
emailVerified: false,
|
||||
});
|
||||
expect(result?.data).toEqual(profile);
|
||||
expect(
|
||||
provider.accountSubject({
|
||||
profile,
|
||||
tokens: { accessToken: "access-token" },
|
||||
}),
|
||||
).toBe(profile.sub);
|
||||
});
|
||||
|
||||
it.each([null, "https://example.com/avatar.png"])(
|
||||
"preserves the raw picture %s while normalizing the local image",
|
||||
async (picture) => {
|
||||
const profile = {
|
||||
sub: "roblox-user-1",
|
||||
nickname: "Builderman",
|
||||
picture,
|
||||
} satisfies RobloxProfile;
|
||||
mockedBetterFetch.mockResolvedValue({ data: profile, error: null });
|
||||
const mapProfileToUser = vi.fn((rawProfile: RobloxProfile) => {
|
||||
expect(rawProfile).toEqual(profile);
|
||||
return {};
|
||||
});
|
||||
const result = await roblox({
|
||||
clientId: "roblox-app",
|
||||
clientSecret: "roblox-secret",
|
||||
mapProfileToUser,
|
||||
}).getUserInfo({ accessToken: "access-token" });
|
||||
|
||||
expect(result?.user.image).toBe(picture ?? undefined);
|
||||
expect(result?.user.name).toBe("Builderman");
|
||||
expect(result?.data.picture).toBe(picture);
|
||||
expect(mapProfileToUser).toHaveBeenCalledOnce();
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -7,21 +7,21 @@ import {
|
||||
} from "../oauth2";
|
||||
import { createPlaceholderEmail } from "../utils/email";
|
||||
|
||||
export interface RobloxProfile extends Record<string, any> {
|
||||
export interface RobloxProfile extends Record<string, unknown> {
|
||||
/** the user's id */
|
||||
sub: string;
|
||||
/** the user's username */
|
||||
preferred_username: string;
|
||||
preferred_username?: string | undefined;
|
||||
/** the user's display name, will return the same value as the preferred_username if not set */
|
||||
nickname: string;
|
||||
nickname?: string | undefined;
|
||||
/** the user's display name, again, will return the same value as the preferred_username if not set */
|
||||
name: string;
|
||||
name?: string | undefined;
|
||||
/** the account creation date as a unix timestamp in seconds */
|
||||
created_at: number;
|
||||
created_at?: number | undefined;
|
||||
/** the user's profile URL */
|
||||
profile: string;
|
||||
profile?: string | undefined;
|
||||
/** the user's avatar URL */
|
||||
picture: string;
|
||||
picture?: string | null | undefined;
|
||||
}
|
||||
|
||||
export interface RobloxOptions extends ProviderOptions<RobloxProfile> {
|
||||
@@ -101,7 +101,7 @@ export const roblox = (options: RobloxOptions) => {
|
||||
return {
|
||||
user: {
|
||||
name: profile.nickname || profile.preferred_username || "",
|
||||
image: profile.picture,
|
||||
image: profile.picture ?? undefined,
|
||||
email: createPlaceholderEmail({
|
||||
identifier: profile.sub,
|
||||
namespace: "roblox",
|
||||
|
||||
Reference in New Issue
Block a user