mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-04 21:04:37 +08:00
Add basic client lib tests (#601)
This commit is contained in:
parent
33e42afd77
commit
c2cb2aac76
62
apps/e2e/tests/js/app.test.ts
Normal file
62
apps/e2e/tests/js/app.test.ts
Normal file
@ -0,0 +1,62 @@
|
||||
import { isUuid } from "@stackframe/stack-shared/dist/utils/uuids";
|
||||
import { it } from "../helpers";
|
||||
import { createApp, scaffoldProject } from "./js-helpers";
|
||||
|
||||
it("should scaffold the project", async ({ expect }) => {
|
||||
const { project } = await scaffoldProject();
|
||||
expect(project.displayName).toBe("New Project");
|
||||
});
|
||||
|
||||
it("should sign up with credential", async ({ expect }) => {
|
||||
const { clientApp } = await createApp();
|
||||
const result1 = await clientApp.signUpWithCredential({
|
||||
email: "test@test.com",
|
||||
password: "password",
|
||||
verificationCallbackUrl: "http://localhost:3000",
|
||||
});
|
||||
|
||||
expect(result1).toMatchInlineSnapshot(`
|
||||
{
|
||||
"data": undefined,
|
||||
"status": "ok",
|
||||
}
|
||||
`);
|
||||
|
||||
const result2 = await clientApp.signInWithCredential({
|
||||
email: "test@test.com",
|
||||
password: "password",
|
||||
});
|
||||
|
||||
expect(result2).toMatchInlineSnapshot(`
|
||||
{
|
||||
"data": undefined,
|
||||
"status": "ok",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it("should create user on the server", async ({ expect }) => {
|
||||
const { serverApp } = await createApp();
|
||||
const user = await serverApp.createUser({
|
||||
primaryEmail: "test@test.com",
|
||||
password: "password",
|
||||
primaryEmailAuthEnabled: true,
|
||||
});
|
||||
|
||||
expect(isUuid(user.id)).toBe(true);
|
||||
|
||||
const user2 = await serverApp.getUser(user.id);
|
||||
expect(user2?.id).toBe(user.id);
|
||||
|
||||
const result = await serverApp.signInWithCredential({
|
||||
email: "test@test.com",
|
||||
password: "password",
|
||||
});
|
||||
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
{
|
||||
"data": undefined,
|
||||
"status": "ok",
|
||||
}
|
||||
`);
|
||||
});
|
||||
@ -1,7 +0,0 @@
|
||||
import { it } from "../helpers";
|
||||
import { scaffoldProject } from "./js-helpers";
|
||||
|
||||
it("should scaffold the project", async ({ expect }) => {
|
||||
const { project } = await scaffoldProject();
|
||||
expect(project.displayName).toBe("New Project");
|
||||
});
|
||||
@ -1,5 +1,4 @@
|
||||
import { AdminProjectUpdateOptions, StackAdminApp } from '@stackframe/js';
|
||||
import { wait } from '@stackframe/stack-shared/dist/utils/promises';
|
||||
import { AdminProjectUpdateOptions, StackAdminApp, StackClientApp, StackServerApp } from '@stackframe/js';
|
||||
import { STACK_BACKEND_BASE_URL, STACK_INTERNAL_PROJECT_ADMIN_KEY, STACK_INTERNAL_PROJECT_CLIENT_KEY, STACK_INTERNAL_PROJECT_SERVER_KEY } from '../helpers';
|
||||
|
||||
export async function scaffoldProject(body?: AdminProjectUpdateOptions) {
|
||||
@ -19,17 +18,56 @@ export async function scaffoldProject(body?: AdminProjectUpdateOptions) {
|
||||
password: "password",
|
||||
verificationCallbackUrl: "https://stack-js-test.example.com/verify",
|
||||
});
|
||||
const user = await internalApp.getUser({
|
||||
const adminUser = await internalApp.getUser({
|
||||
or: 'throw',
|
||||
});
|
||||
|
||||
const project = await user.createProject({
|
||||
const project = await adminUser.createProject({
|
||||
displayName: body?.displayName || 'New Project',
|
||||
...body,
|
||||
});
|
||||
|
||||
return {
|
||||
project,
|
||||
user,
|
||||
adminUser,
|
||||
};
|
||||
}
|
||||
|
||||
export async function createApp(body?: AdminProjectUpdateOptions) {
|
||||
const { project, adminUser } = await scaffoldProject(body);
|
||||
const adminApp = new StackAdminApp({
|
||||
projectId: project.id,
|
||||
baseUrl: STACK_BACKEND_BASE_URL,
|
||||
projectOwnerSession: adminUser._internalSession,
|
||||
tokenStore: "memory",
|
||||
});
|
||||
|
||||
const apiKey = await adminApp.createApiKey({
|
||||
description: 'test',
|
||||
expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24 * 30),
|
||||
hasPublishableClientKey: true,
|
||||
hasSecretServerKey: true,
|
||||
hasSuperSecretAdminKey: false,
|
||||
});
|
||||
|
||||
const serverApp = new StackServerApp({
|
||||
baseUrl: STACK_BACKEND_BASE_URL,
|
||||
projectId: project.id,
|
||||
publishableClientKey: apiKey.publishableClientKey,
|
||||
secretServerKey: apiKey.secretServerKey,
|
||||
tokenStore: "memory",
|
||||
});
|
||||
|
||||
const clientApp = new StackClientApp({
|
||||
baseUrl: STACK_BACKEND_BASE_URL,
|
||||
projectId: project.id,
|
||||
publishableClientKey: apiKey.publishableClientKey,
|
||||
tokenStore: "memory",
|
||||
});
|
||||
|
||||
return {
|
||||
serverApp,
|
||||
clientApp,
|
||||
adminApp,
|
||||
};
|
||||
}
|
||||
|
||||
@ -19,8 +19,8 @@ export async function signInWithOAuth(
|
||||
const { codeChallenge, state } = await saveVerifierAndState();
|
||||
const location = await iface.getOAuthUrl({
|
||||
provider: options.provider,
|
||||
redirectUrl: constructRedirectUrl(options.redirectUrl),
|
||||
errorRedirectUrl: constructRedirectUrl(options.errorRedirectUrl),
|
||||
redirectUrl: constructRedirectUrl(options.redirectUrl, "redirectUrl"),
|
||||
errorRedirectUrl: constructRedirectUrl(options.errorRedirectUrl, "errorRedirectUrl"),
|
||||
codeChallenge,
|
||||
state,
|
||||
type: "authenticate",
|
||||
@ -43,9 +43,9 @@ export async function addNewOAuthProviderOrScope(
|
||||
const { codeChallenge, state } = await saveVerifierAndState();
|
||||
const location = await iface.getOAuthUrl({
|
||||
provider: options.provider,
|
||||
redirectUrl: constructRedirectUrl(options.redirectUrl),
|
||||
errorRedirectUrl: constructRedirectUrl(options.errorRedirectUrl),
|
||||
afterCallbackRedirectUrl: constructRedirectUrl(window.location.href),
|
||||
redirectUrl: constructRedirectUrl(options.redirectUrl, "redirectUrl"),
|
||||
errorRedirectUrl: constructRedirectUrl(options.errorRedirectUrl, "errorRedirectUrl"),
|
||||
afterCallbackRedirectUrl: constructRedirectUrl(window.location.href, "afterCallbackRedirectUrl"),
|
||||
codeChallenge,
|
||||
state,
|
||||
type: "link",
|
||||
@ -129,7 +129,7 @@ export async function callOAuthCallback(
|
||||
try {
|
||||
return Result.ok(await iface.callOAuthCallback({
|
||||
oauthParams: consumed.originalUrl.searchParams,
|
||||
redirectUri: constructRedirectUrl(redirectUrl),
|
||||
redirectUri: constructRedirectUrl(redirectUrl, "redirectUri"),
|
||||
codeVerifier: consumed.codeVerifier,
|
||||
state: consumed.state,
|
||||
}));
|
||||
|
||||
@ -628,14 +628,11 @@ export class _StackClientAppImplIncomplete<HasTokenStore extends boolean, Projec
|
||||
clientMetadata: crud.client_metadata,
|
||||
clientReadOnlyMetadata: crud.client_read_only_metadata,
|
||||
async inviteUser(options: { email: string, callbackUrl?: string }) {
|
||||
if (!options.callbackUrl && !await app._getCurrentUrl()) {
|
||||
throw new Error("Cannot invite user without a callback URL from the server or without a redirect method. Make sure you pass the `callbackUrl` option: `inviteUser({ email, callbackUrl: ... })`");
|
||||
}
|
||||
await app._interface.sendTeamInvitation({
|
||||
teamId: crud.id,
|
||||
email: options.email,
|
||||
session,
|
||||
callbackUrl: options.callbackUrl ?? constructRedirectUrl(app.urls.teamInvitation),
|
||||
callbackUrl: options.callbackUrl ?? constructRedirectUrl(app.urls.teamInvitation, "callbackUrl"),
|
||||
});
|
||||
await app._teamInvitationsCache.refresh([session, crud.id]);
|
||||
},
|
||||
@ -680,8 +677,12 @@ export class _StackClientAppImplIncomplete<HasTokenStore extends boolean, Projec
|
||||
isPrimary: crud.is_primary,
|
||||
usedForAuth: crud.used_for_auth,
|
||||
|
||||
async sendVerificationEmail() {
|
||||
await app._interface.sendCurrentUserContactChannelVerificationEmail(crud.id, constructRedirectUrl(app.urls.emailVerification), session);
|
||||
async sendVerificationEmail(options?: { callbackUrl?: string }) {
|
||||
await app._interface.sendCurrentUserContactChannelVerificationEmail(
|
||||
crud.id,
|
||||
options?.callbackUrl || constructRedirectUrl(app.urls.emailVerification, "callbackUrl"),
|
||||
session
|
||||
);
|
||||
},
|
||||
async update(data: ContactChannelUpdateOptions) {
|
||||
await app._interface.updateClientContactChannel(crud.id, contactChannelUpdateOptionsToCrud(data), session);
|
||||
@ -938,10 +939,11 @@ export class _StackClientAppImplIncomplete<HasTokenStore extends boolean, Projec
|
||||
if (!crud.primary_email) {
|
||||
throw new StackAssertionError("User does not have a primary email");
|
||||
}
|
||||
if (!options?.callbackUrl && !await app._getCurrentUrl()) {
|
||||
throw new Error("Cannot send verification email without a callback URL from the server or without a redirect method. Make sure you pass the `callbackUrl` option: `sendVerificationEmail({ callbackUrl: ... })`");
|
||||
}
|
||||
return await app._interface.sendVerificationEmail(crud.primary_email, options?.callbackUrl ?? constructRedirectUrl(app.urls.emailVerification), session);
|
||||
return await app._interface.sendVerificationEmail(
|
||||
crud.primary_email,
|
||||
options?.callbackUrl ?? constructRedirectUrl(app.urls.emailVerification, "callbackUrl"),
|
||||
session
|
||||
);
|
||||
},
|
||||
async updatePassword(options: { oldPassword: string, newPassword: string}) {
|
||||
const result = await app._interface.updatePassword(options, session);
|
||||
@ -1156,17 +1158,11 @@ export class _StackClientAppImplIncomplete<HasTokenStore extends boolean, Projec
|
||||
async redirectToTeamInvitation(options?: RedirectToOptions) { return await this._redirectToHandler("teamInvitation", options); }
|
||||
|
||||
async sendForgotPasswordEmail(email: string, options?: { callbackUrl?: string }): Promise<Result<undefined, KnownErrors["UserNotFound"]>> {
|
||||
if (!options?.callbackUrl && !await this._getCurrentUrl()) {
|
||||
throw new Error("Cannot send forgot password email without a callback URL from the server or without a redirect method. Make sure you pass the `callbackUrl` option: `sendForgotPasswordEmail({ email, callbackUrl: ... })`");
|
||||
}
|
||||
return await this._interface.sendForgotPasswordEmail(email, options?.callbackUrl ?? constructRedirectUrl(this.urls.passwordReset));
|
||||
return await this._interface.sendForgotPasswordEmail(email, options?.callbackUrl ?? constructRedirectUrl(this.urls.passwordReset, "callbackUrl"));
|
||||
}
|
||||
|
||||
async sendMagicLinkEmail(email: string, options?: { callbackUrl?: string }): Promise<Result<{ nonce: string }, KnownErrors["RedirectUrlNotWhitelisted"]>> {
|
||||
if (!options?.callbackUrl && !await this._getCurrentUrl()) {
|
||||
throw new Error("Cannot send magic link email without a callback URL from the server or without a redirect method. Make sure you pass the `callbackUrl` option: `sendMagicLinkEmail({ email, callbackUrl: ... })`");
|
||||
}
|
||||
return await this._interface.sendMagicLinkEmail(email, options?.callbackUrl ?? constructRedirectUrl(this.urls.magicLinkCallback));
|
||||
return await this._interface.sendMagicLinkEmail(email, options?.callbackUrl ?? constructRedirectUrl(this.urls.magicLinkCallback, "callbackUrl"));
|
||||
}
|
||||
|
||||
async resetPassword(options: { password: string, code: string }): Promise<Result<undefined, KnownErrors["VerificationCodeError"]>> {
|
||||
@ -1398,7 +1394,7 @@ export class _StackClientAppImplIncomplete<HasTokenStore extends boolean, Projec
|
||||
}): Promise<Result<undefined, KnownErrors["UserWithEmailAlreadyExists"] | KnownErrors['PasswordRequirementsNotMet']>> {
|
||||
this._ensurePersistentTokenStore();
|
||||
const session = await this._getSession();
|
||||
const emailVerificationRedirectUrl = options.verificationCallbackUrl ?? constructRedirectUrl(this.urls.emailVerification);
|
||||
const emailVerificationRedirectUrl = options.verificationCallbackUrl ?? constructRedirectUrl(this.urls.emailVerification, "verificationCallbackUrl");
|
||||
const result = await this._interface.signUpWithCredential(
|
||||
options.email,
|
||||
options.password,
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import { KnownErrors, StackServerInterface } from "@stackframe/stack-shared";
|
||||
import { ContactChannelsCrud } from "@stackframe/stack-shared/dist/interface/crud/contact-channels";
|
||||
import { ProjectPermissionDefinitionsCrud, ProjectPermissionsCrud } from "@stackframe/stack-shared/dist/interface/crud/project-permissions";
|
||||
import { TeamInvitationCrud } from "@stackframe/stack-shared/dist/interface/crud/team-invitation";
|
||||
import { TeamMemberProfilesCrud } from "@stackframe/stack-shared/dist/interface/crud/team-member-profiles";
|
||||
import { TeamPermissionDefinitionsCrud, TeamPermissionsCrud } from "@stackframe/stack-shared/dist/interface/crud/team-permissions";
|
||||
import { ProjectPermissionDefinitionsCrud, ProjectPermissionsCrud } from "@stackframe/stack-shared/dist/interface/crud/project-permissions";
|
||||
import { TeamsCrud } from "@stackframe/stack-shared/dist/interface/crud/teams";
|
||||
import { UsersCrud } from "@stackframe/stack-shared/dist/interface/crud/users";
|
||||
import { InternalSession } from "@stackframe/stack-shared/dist/sessions";
|
||||
@ -17,7 +17,7 @@ import { constructRedirectUrl } from "../../../../utils/url";
|
||||
import { GetUserOptions, HandlerUrls, OAuthScopesOnSignIn, TokenStoreInit } from "../../common";
|
||||
import { OAuthConnection } from "../../connected-accounts";
|
||||
import { ServerContactChannel, ServerContactChannelCreateOptions, ServerContactChannelUpdateOptions, serverContactChannelCreateOptionsToCrud, serverContactChannelUpdateOptionsToCrud } from "../../contact-channels";
|
||||
import { AdminTeamPermission, AdminTeamPermissionDefinition, AdminProjectPermissionDefinition } from "../../permissions";
|
||||
import { AdminProjectPermissionDefinition, AdminTeamPermission, AdminTeamPermissionDefinition } from "../../permissions";
|
||||
import { EditableTeamMemberProfile, ServerListUsersOptions, ServerTeam, ServerTeamCreateOptions, ServerTeamUpdateOptions, ServerTeamUser, Team, TeamInvitation, serverTeamCreateOptionsToCrud, serverTeamUpdateOptionsToCrud } from "../../teams";
|
||||
import { ProjectCurrentServerUser, ServerUser, ServerUserCreateOptions, ServerUserUpdateOptions, serverUserCreateOptionsToCrud, serverUserUpdateOptionsToCrud } from "../../users";
|
||||
import { StackServerAppConstructorOptions } from "../interfaces/server-app";
|
||||
@ -151,11 +151,7 @@ export class _StackServerAppImplIncomplete<HasTokenStore extends boolean, Projec
|
||||
isPrimary: crud.is_primary,
|
||||
usedForAuth: crud.used_for_auth,
|
||||
async sendVerificationEmail(options?: { callbackUrl?: string }) {
|
||||
if (!options?.callbackUrl && !await app._getCurrentUrl()) {
|
||||
throw new Error("Cannot send verification email without a callback URL from the server or without a redirect method. Make sure you pass the `callbackUrl` option: `sendVerificationEmail({ callbackUrl: ... })`");
|
||||
}
|
||||
|
||||
await app._interface.sendServerContactChannelVerificationEmail(userId, crud.id, options?.callbackUrl ?? constructRedirectUrl(app.urls.emailVerification));
|
||||
await app._interface.sendServerContactChannelVerificationEmail(userId, crud.id, options?.callbackUrl ?? constructRedirectUrl(app.urls.emailVerification, "callbackUrl"));
|
||||
},
|
||||
async update(data: ServerContactChannelUpdateOptions) {
|
||||
await app._interface.updateServerContactChannel(userId, crud.id, serverContactChannelUpdateOptionsToCrud(data));
|
||||
@ -506,14 +502,10 @@ export class _StackServerAppImplIncomplete<HasTokenStore extends boolean, Projec
|
||||
await app._serverTeamMemberProfilesCache.refresh([crud.id]);
|
||||
},
|
||||
async inviteUser(options: { email: string, callbackUrl?: string }) {
|
||||
if (!options.callbackUrl && !await app._getCurrentUrl()) {
|
||||
throw new Error("Cannot invite user without a callback URL from the server or without a redirect method. Make sure you pass the `callbackUrl` option: `inviteUser({ email, callbackUrl: ... })`");
|
||||
}
|
||||
|
||||
await app._interface.sendServerTeamInvitation({
|
||||
teamId: crud.id,
|
||||
email: options.email,
|
||||
callbackUrl: options.callbackUrl ?? constructRedirectUrl(app.urls.teamInvitation),
|
||||
callbackUrl: options.callbackUrl ?? constructRedirectUrl(app.urls.teamInvitation, "callbackUrl"),
|
||||
});
|
||||
await app._serverTeamInvitationsCache.refresh([crud.id]);
|
||||
},
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors";
|
||||
|
||||
|
||||
export function constructRedirectUrl(redirectUrl: URL | string | undefined) {
|
||||
export function constructRedirectUrl(redirectUrl: URL | string | undefined, callbackUrlName: string) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
if (typeof window === 'undefined' || !window.location) {
|
||||
throw new StackAssertionError("Attempted to call constructRedirectUrl in a non-browser environment. You may be able to fix this by passing the `callbackUrl` option with your function call.", { redirectUrl });
|
||||
throw new StackAssertionError(`${callbackUrlName} option is required in a non-browser environment.`, { redirectUrl });
|
||||
}
|
||||
|
||||
const retainedQueryParams = ["after_auth_return_to"];
|
||||
|
||||
Loading…
Reference in New Issue
Block a user