Skip to content

Commit c2cb2aa

Browse files
authored
Add basic client lib tests (#601)
1 parent 33e42af commit c2cb2aa

7 files changed

Lines changed: 132 additions & 51 deletions

File tree

apps/e2e/tests/js/app.test.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { isUuid } from "@stackframe/stack-shared/dist/utils/uuids";
2+
import { it } from "../helpers";
3+
import { createApp, scaffoldProject } from "./js-helpers";
4+
5+
it("should scaffold the project", async ({ expect }) => {
6+
const { project } = await scaffoldProject();
7+
expect(project.displayName).toBe("New Project");
8+
});
9+
10+
it("should sign up with credential", async ({ expect }) => {
11+
const { clientApp } = await createApp();
12+
const result1 = await clientApp.signUpWithCredential({
13+
email: "test@test.com",
14+
password: "password",
15+
verificationCallbackUrl: "http://localhost:3000",
16+
});
17+
18+
expect(result1).toMatchInlineSnapshot(`
19+
{
20+
"data": undefined,
21+
"status": "ok",
22+
}
23+
`);
24+
25+
const result2 = await clientApp.signInWithCredential({
26+
email: "test@test.com",
27+
password: "password",
28+
});
29+
30+
expect(result2).toMatchInlineSnapshot(`
31+
{
32+
"data": undefined,
33+
"status": "ok",
34+
}
35+
`);
36+
});
37+
38+
it("should create user on the server", async ({ expect }) => {
39+
const { serverApp } = await createApp();
40+
const user = await serverApp.createUser({
41+
primaryEmail: "test@test.com",
42+
password: "password",
43+
primaryEmailAuthEnabled: true,
44+
});
45+
46+
expect(isUuid(user.id)).toBe(true);
47+
48+
const user2 = await serverApp.getUser(user.id);
49+
expect(user2?.id).toBe(user.id);
50+
51+
const result = await serverApp.signInWithCredential({
52+
email: "test@test.com",
53+
password: "password",
54+
});
55+
56+
expect(result).toMatchInlineSnapshot(`
57+
{
58+
"data": undefined,
59+
"status": "ok",
60+
}
61+
`);
62+
});

apps/e2e/tests/js/general.test.ts

Lines changed: 0 additions & 7 deletions
This file was deleted.

apps/e2e/tests/js/js-helpers.ts

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
import { AdminProjectUpdateOptions, StackAdminApp } from '@stackframe/js';
2-
import { wait } from '@stackframe/stack-shared/dist/utils/promises';
1+
import { AdminProjectUpdateOptions, StackAdminApp, StackClientApp, StackServerApp } from '@stackframe/js';
32
import { STACK_BACKEND_BASE_URL, STACK_INTERNAL_PROJECT_ADMIN_KEY, STACK_INTERNAL_PROJECT_CLIENT_KEY, STACK_INTERNAL_PROJECT_SERVER_KEY } from '../helpers';
43

54
export async function scaffoldProject(body?: AdminProjectUpdateOptions) {
@@ -19,17 +18,56 @@ export async function scaffoldProject(body?: AdminProjectUpdateOptions) {
1918
password: "password",
2019
verificationCallbackUrl: "https://stack-js-test.example.com/verify",
2120
});
22-
const user = await internalApp.getUser({
21+
const adminUser = await internalApp.getUser({
2322
or: 'throw',
2423
});
2524

26-
const project = await user.createProject({
25+
const project = await adminUser.createProject({
2726
displayName: body?.displayName || 'New Project',
2827
...body,
2928
});
3029

3130
return {
3231
project,
33-
user,
32+
adminUser,
33+
};
34+
}
35+
36+
export async function createApp(body?: AdminProjectUpdateOptions) {
37+
const { project, adminUser } = await scaffoldProject(body);
38+
const adminApp = new StackAdminApp({
39+
projectId: project.id,
40+
baseUrl: STACK_BACKEND_BASE_URL,
41+
projectOwnerSession: adminUser._internalSession,
42+
tokenStore: "memory",
43+
});
44+
45+
const apiKey = await adminApp.createApiKey({
46+
description: 'test',
47+
expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24 * 30),
48+
hasPublishableClientKey: true,
49+
hasSecretServerKey: true,
50+
hasSuperSecretAdminKey: false,
51+
});
52+
53+
const serverApp = new StackServerApp({
54+
baseUrl: STACK_BACKEND_BASE_URL,
55+
projectId: project.id,
56+
publishableClientKey: apiKey.publishableClientKey,
57+
secretServerKey: apiKey.secretServerKey,
58+
tokenStore: "memory",
59+
});
60+
61+
const clientApp = new StackClientApp({
62+
baseUrl: STACK_BACKEND_BASE_URL,
63+
projectId: project.id,
64+
publishableClientKey: apiKey.publishableClientKey,
65+
tokenStore: "memory",
66+
});
67+
68+
return {
69+
serverApp,
70+
clientApp,
71+
adminApp,
3472
};
3573
}

packages/template/src/lib/auth.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ export async function signInWithOAuth(
1919
const { codeChallenge, state } = await saveVerifierAndState();
2020
const location = await iface.getOAuthUrl({
2121
provider: options.provider,
22-
redirectUrl: constructRedirectUrl(options.redirectUrl),
23-
errorRedirectUrl: constructRedirectUrl(options.errorRedirectUrl),
22+
redirectUrl: constructRedirectUrl(options.redirectUrl, "redirectUrl"),
23+
errorRedirectUrl: constructRedirectUrl(options.errorRedirectUrl, "errorRedirectUrl"),
2424
codeChallenge,
2525
state,
2626
type: "authenticate",
@@ -43,9 +43,9 @@ export async function addNewOAuthProviderOrScope(
4343
const { codeChallenge, state } = await saveVerifierAndState();
4444
const location = await iface.getOAuthUrl({
4545
provider: options.provider,
46-
redirectUrl: constructRedirectUrl(options.redirectUrl),
47-
errorRedirectUrl: constructRedirectUrl(options.errorRedirectUrl),
48-
afterCallbackRedirectUrl: constructRedirectUrl(window.location.href),
46+
redirectUrl: constructRedirectUrl(options.redirectUrl, "redirectUrl"),
47+
errorRedirectUrl: constructRedirectUrl(options.errorRedirectUrl, "errorRedirectUrl"),
48+
afterCallbackRedirectUrl: constructRedirectUrl(window.location.href, "afterCallbackRedirectUrl"),
4949
codeChallenge,
5050
state,
5151
type: "link",
@@ -129,7 +129,7 @@ export async function callOAuthCallback(
129129
try {
130130
return Result.ok(await iface.callOAuthCallback({
131131
oauthParams: consumed.originalUrl.searchParams,
132-
redirectUri: constructRedirectUrl(redirectUrl),
132+
redirectUri: constructRedirectUrl(redirectUrl, "redirectUri"),
133133
codeVerifier: consumed.codeVerifier,
134134
state: consumed.state,
135135
}));

packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts

Lines changed: 15 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -628,14 +628,11 @@ export class _StackClientAppImplIncomplete<HasTokenStore extends boolean, Projec
628628
clientMetadata: crud.client_metadata,
629629
clientReadOnlyMetadata: crud.client_read_only_metadata,
630630
async inviteUser(options: { email: string, callbackUrl?: string }) {
631-
if (!options.callbackUrl && !await app._getCurrentUrl()) {
632-
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: ... })`");
633-
}
634631
await app._interface.sendTeamInvitation({
635632
teamId: crud.id,
636633
email: options.email,
637634
session,
638-
callbackUrl: options.callbackUrl ?? constructRedirectUrl(app.urls.teamInvitation),
635+
callbackUrl: options.callbackUrl ?? constructRedirectUrl(app.urls.teamInvitation, "callbackUrl"),
639636
});
640637
await app._teamInvitationsCache.refresh([session, crud.id]);
641638
},
@@ -680,8 +677,12 @@ export class _StackClientAppImplIncomplete<HasTokenStore extends boolean, Projec
680677
isPrimary: crud.is_primary,
681678
usedForAuth: crud.used_for_auth,
682679

683-
async sendVerificationEmail() {
684-
await app._interface.sendCurrentUserContactChannelVerificationEmail(crud.id, constructRedirectUrl(app.urls.emailVerification), session);
680+
async sendVerificationEmail(options?: { callbackUrl?: string }) {
681+
await app._interface.sendCurrentUserContactChannelVerificationEmail(
682+
crud.id,
683+
options?.callbackUrl || constructRedirectUrl(app.urls.emailVerification, "callbackUrl"),
684+
session
685+
);
685686
},
686687
async update(data: ContactChannelUpdateOptions) {
687688
await app._interface.updateClientContactChannel(crud.id, contactChannelUpdateOptionsToCrud(data), session);
@@ -938,10 +939,11 @@ export class _StackClientAppImplIncomplete<HasTokenStore extends boolean, Projec
938939
if (!crud.primary_email) {
939940
throw new StackAssertionError("User does not have a primary email");
940941
}
941-
if (!options?.callbackUrl && !await app._getCurrentUrl()) {
942-
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: ... })`");
943-
}
944-
return await app._interface.sendVerificationEmail(crud.primary_email, options?.callbackUrl ?? constructRedirectUrl(app.urls.emailVerification), session);
942+
return await app._interface.sendVerificationEmail(
943+
crud.primary_email,
944+
options?.callbackUrl ?? constructRedirectUrl(app.urls.emailVerification, "callbackUrl"),
945+
session
946+
);
945947
},
946948
async updatePassword(options: { oldPassword: string, newPassword: string}) {
947949
const result = await app._interface.updatePassword(options, session);
@@ -1156,17 +1158,11 @@ export class _StackClientAppImplIncomplete<HasTokenStore extends boolean, Projec
11561158
async redirectToTeamInvitation(options?: RedirectToOptions) { return await this._redirectToHandler("teamInvitation", options); }
11571159

11581160
async sendForgotPasswordEmail(email: string, options?: { callbackUrl?: string }): Promise<Result<undefined, KnownErrors["UserNotFound"]>> {
1159-
if (!options?.callbackUrl && !await this._getCurrentUrl()) {
1160-
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: ... })`");
1161-
}
1162-
return await this._interface.sendForgotPasswordEmail(email, options?.callbackUrl ?? constructRedirectUrl(this.urls.passwordReset));
1161+
return await this._interface.sendForgotPasswordEmail(email, options?.callbackUrl ?? constructRedirectUrl(this.urls.passwordReset, "callbackUrl"));
11631162
}
11641163

11651164
async sendMagicLinkEmail(email: string, options?: { callbackUrl?: string }): Promise<Result<{ nonce: string }, KnownErrors["RedirectUrlNotWhitelisted"]>> {
1166-
if (!options?.callbackUrl && !await this._getCurrentUrl()) {
1167-
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: ... })`");
1168-
}
1169-
return await this._interface.sendMagicLinkEmail(email, options?.callbackUrl ?? constructRedirectUrl(this.urls.magicLinkCallback));
1165+
return await this._interface.sendMagicLinkEmail(email, options?.callbackUrl ?? constructRedirectUrl(this.urls.magicLinkCallback, "callbackUrl"));
11701166
}
11711167

11721168
async resetPassword(options: { password: string, code: string }): Promise<Result<undefined, KnownErrors["VerificationCodeError"]>> {
@@ -1398,7 +1394,7 @@ export class _StackClientAppImplIncomplete<HasTokenStore extends boolean, Projec
13981394
}): Promise<Result<undefined, KnownErrors["UserWithEmailAlreadyExists"] | KnownErrors['PasswordRequirementsNotMet']>> {
13991395
this._ensurePersistentTokenStore();
14001396
const session = await this._getSession();
1401-
const emailVerificationRedirectUrl = options.verificationCallbackUrl ?? constructRedirectUrl(this.urls.emailVerification);
1397+
const emailVerificationRedirectUrl = options.verificationCallbackUrl ?? constructRedirectUrl(this.urls.emailVerification, "verificationCallbackUrl");
14021398
const result = await this._interface.signUpWithCredential(
14031399
options.email,
14041400
options.password,

packages/template/src/lib/stack-app/apps/implementations/server-app-impl.ts

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { KnownErrors, StackServerInterface } from "@stackframe/stack-shared";
22
import { ContactChannelsCrud } from "@stackframe/stack-shared/dist/interface/crud/contact-channels";
3+
import { ProjectPermissionDefinitionsCrud, ProjectPermissionsCrud } from "@stackframe/stack-shared/dist/interface/crud/project-permissions";
34
import { TeamInvitationCrud } from "@stackframe/stack-shared/dist/interface/crud/team-invitation";
45
import { TeamMemberProfilesCrud } from "@stackframe/stack-shared/dist/interface/crud/team-member-profiles";
56
import { TeamPermissionDefinitionsCrud, TeamPermissionsCrud } from "@stackframe/stack-shared/dist/interface/crud/team-permissions";
6-
import { ProjectPermissionDefinitionsCrud, ProjectPermissionsCrud } from "@stackframe/stack-shared/dist/interface/crud/project-permissions";
77
import { TeamsCrud } from "@stackframe/stack-shared/dist/interface/crud/teams";
88
import { UsersCrud } from "@stackframe/stack-shared/dist/interface/crud/users";
99
import { InternalSession } from "@stackframe/stack-shared/dist/sessions";
@@ -17,7 +17,7 @@ import { constructRedirectUrl } from "../../../../utils/url";
1717
import { GetUserOptions, HandlerUrls, OAuthScopesOnSignIn, TokenStoreInit } from "../../common";
1818
import { OAuthConnection } from "../../connected-accounts";
1919
import { ServerContactChannel, ServerContactChannelCreateOptions, ServerContactChannelUpdateOptions, serverContactChannelCreateOptionsToCrud, serverContactChannelUpdateOptionsToCrud } from "../../contact-channels";
20-
import { AdminTeamPermission, AdminTeamPermissionDefinition, AdminProjectPermissionDefinition } from "../../permissions";
20+
import { AdminProjectPermissionDefinition, AdminTeamPermission, AdminTeamPermissionDefinition } from "../../permissions";
2121
import { EditableTeamMemberProfile, ServerListUsersOptions, ServerTeam, ServerTeamCreateOptions, ServerTeamUpdateOptions, ServerTeamUser, Team, TeamInvitation, serverTeamCreateOptionsToCrud, serverTeamUpdateOptionsToCrud } from "../../teams";
2222
import { ProjectCurrentServerUser, ServerUser, ServerUserCreateOptions, ServerUserUpdateOptions, serverUserCreateOptionsToCrud, serverUserUpdateOptionsToCrud } from "../../users";
2323
import { StackServerAppConstructorOptions } from "../interfaces/server-app";
@@ -151,11 +151,7 @@ export class _StackServerAppImplIncomplete<HasTokenStore extends boolean, Projec
151151
isPrimary: crud.is_primary,
152152
usedForAuth: crud.used_for_auth,
153153
async sendVerificationEmail(options?: { callbackUrl?: string }) {
154-
if (!options?.callbackUrl && !await app._getCurrentUrl()) {
155-
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: ... })`");
156-
}
157-
158-
await app._interface.sendServerContactChannelVerificationEmail(userId, crud.id, options?.callbackUrl ?? constructRedirectUrl(app.urls.emailVerification));
154+
await app._interface.sendServerContactChannelVerificationEmail(userId, crud.id, options?.callbackUrl ?? constructRedirectUrl(app.urls.emailVerification, "callbackUrl"));
159155
},
160156
async update(data: ServerContactChannelUpdateOptions) {
161157
await app._interface.updateServerContactChannel(userId, crud.id, serverContactChannelUpdateOptionsToCrud(data));
@@ -506,14 +502,10 @@ export class _StackServerAppImplIncomplete<HasTokenStore extends boolean, Projec
506502
await app._serverTeamMemberProfilesCache.refresh([crud.id]);
507503
},
508504
async inviteUser(options: { email: string, callbackUrl?: string }) {
509-
if (!options.callbackUrl && !await app._getCurrentUrl()) {
510-
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: ... })`");
511-
}
512-
513505
await app._interface.sendServerTeamInvitation({
514506
teamId: crud.id,
515507
email: options.email,
516-
callbackUrl: options.callbackUrl ?? constructRedirectUrl(app.urls.teamInvitation),
508+
callbackUrl: options.callbackUrl ?? constructRedirectUrl(app.urls.teamInvitation, "callbackUrl"),
517509
});
518510
await app._serverTeamInvitationsCache.refresh([crud.id]);
519511
},

packages/template/src/utils/url.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors";
22

33

4-
export function constructRedirectUrl(redirectUrl: URL | string | undefined) {
4+
export function constructRedirectUrl(redirectUrl: URL | string | undefined, callbackUrlName: string) {
55
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
66
if (typeof window === 'undefined' || !window.location) {
7-
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 });
7+
throw new StackAssertionError(`${callbackUrlName} option is required in a non-browser environment.`, { redirectUrl });
88
}
99

1010
const retainedQueryParams = ["after_auth_return_to"];

0 commit comments

Comments
 (0)