Skip to content

Commit

Permalink
RJS-2204: Re-enabled client reset tests (#6738)
Browse files Browse the repository at this point in the history
* Refactored importer to take a client through config

* Fixing tests to trigger client reset

* Fixing tests after refactor
  • Loading branch information
kraenhansen authored Jun 25, 2024
1 parent 21c932a commit b473415
Show file tree
Hide file tree
Showing 9 changed files with 163 additions and 96 deletions.
35 changes: 4 additions & 31 deletions integration-tests/tests/src/hooks/import-app-before.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,46 +18,19 @@

import Realm, { AppConfiguration } from "realm";

import { AppConfig, AppImporter, Credentials } from "@realm/app-importer";
import { AppConfig } from "@realm/app-importer";
import { mongodbServiceType } from "../utils/ExtendedAppConfigBuilder";
import { printWarningBox } from "../utils/print-warning-box";
import { baasAppImporter } from "../utils/baas-app-importer";

const REALM_LOG_LEVELS = ["all", "trace", "debug", "detail", "info", "warn", "error", "fatal", "off"];

const {
syncLogLevel = "warn",
baseUrl = "http://localhost:9090",
reuseApp = false,
username = "[email protected]",
password = "password",
publicKey,
privateKey,
missingServer,
} = environment;
const { syncLogLevel = "warn", baseUrl = "http://localhost:9090", missingServer } = environment;

export { baseUrl };

const allowSkippingServerTests = typeof environment.baseUrl === "undefined" && missingServer !== false;

const credentials: Credentials =
typeof publicKey === "string" && typeof privateKey === "string"
? {
kind: "api-key",
publicKey,
privateKey,
}
: {
kind: "username-password",
username,
password,
};

const importer = new AppImporter({
baseUrl,
credentials,
reuseApp,
});

function isConnectionRefused(err: unknown) {
return (
err instanceof Error &&
Expand Down Expand Up @@ -105,7 +78,7 @@ export function importAppBefore(
throw new Error("Unexpected app on context, use only one importAppBefore per test");
} else {
try {
const { appId } = await importer.importApp(config);
const { appId } = await baasAppImporter.importApp(config);
this.app = new Realm.App({ id: appId, baseUrl, ...sdkConfig });
} catch (err) {
if (isConnectionRefused(err) && allowSkippingServerTests) {
Expand Down
47 changes: 28 additions & 19 deletions integration-tests/tests/src/tests/sync/client-reset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import { DogSchema, PersonSchema } from "../../schemas/person-and-dog-with-objec
import { expectClientResetError } from "../../utils/expect-sync-error";
import { createPromiseHandle } from "../../utils/promise-handle";
import { buildAppConfig } from "../../utils/build-app-config";
import { baasAdminClient } from "../../utils/baas-admin-api";

const FlexiblePersonSchema = { ...PersonSchema, properties: { ...PersonSchema.properties, nonQueryable: "string?" } };
const FlexibleDogSchema = { ...DogSchema, properties: { ...DogSchema.properties, nonQueryable: "string?" } };
Expand All @@ -53,18 +54,16 @@ function getPartitionValue() {
return new BSON.UUID().toHexString();
}

async function triggerClientReset(app: App, user: User): Promise<void> {
const maxAttempts = 5;
let deleted = false;
let count = maxAttempts;
while (count > 0) {
deleted = (await user.functions.triggerClientReset(app.id, user.id)) as boolean;
if (deleted) {
return;
}
count--;
async function triggerClientReset(app: App, syncSession: Realm.App.Sync.Session): Promise<void> {
const { fileIdent } = syncSession as unknown as Record<string, unknown>;
if (typeof fileIdent !== "bigint") {
throw new Error("Expected the internal file ident");
}
throw new Error(`Cannot trigger client reset in ${maxAttempts} attempts`);
await baasAdminClient.ensureLogIn();
const { _id } = await baasAdminClient.getAppByClientAppId(app.id);
syncSession.pause();
await baasAdminClient.forceSyncReset(_id, Number(fileIdent));
syncSession.resume();
}

async function waitServerSideClientResetDiscardUnsyncedChangesCallbacks(
Expand Down Expand Up @@ -109,8 +108,13 @@ async function waitServerSideClientResetDiscardUnsyncedChangesCallbacks(
addSubscriptions(realm);
}

await realm.syncSession?.uploadAllLocalChanges();
await triggerClientReset(app, user);
const { syncSession } = realm;
if (!syncSession) {
throw new Error("Expected a sync session");
}

await syncSession.uploadAllLocalChanges();
await triggerClientReset(app, syncSession);
await resetHandle;
}

Expand Down Expand Up @@ -157,8 +161,13 @@ async function waitServerSideClientResetRecoveryCallbacks(
addSubscriptions(realm);
}

await realm.syncSession?.uploadAllLocalChanges();
await triggerClientReset(app, user);
const { syncSession } = realm;
if (!syncSession) {
throw new Error("Expected a sync session");
}

await syncSession.uploadAllLocalChanges();
await triggerClientReset(app, syncSession);
await resetHandle;
}

Expand Down Expand Up @@ -300,8 +309,8 @@ function getSchema(useFlexibleSync: boolean) {
this.longTimeout(); // client reset with flexible sync can take quite some time
importAppBefore(
useFlexibleSync
? buildAppConfig("with-flx").anonAuth().flexibleSync() /* .triggerClientResetFunction() */
: buildAppConfig("with-pbs").anonAuth().partitionBasedSync() /* .triggerClientResetFunction() */,
? buildAppConfig("with-flx").anonAuth().flexibleSync()
: buildAppConfig("with-pbs").anonAuth().partitionBasedSync(),
);
authenticateUserBefore();

Expand Down Expand Up @@ -510,7 +519,7 @@ function getSchema(useFlexibleSync: boolean) {
);
});

it.skip(`handles discard local client reset with ${getPartialTestTitle(
it(`handles discard local client reset with ${getPartialTestTitle(
useFlexibleSync,
)} sync enabled`, async function (this: RealmContext) {
// (i) using a client reset in "DiscardUnsyncedChanges" mode, a fresh copy
Expand All @@ -536,7 +545,7 @@ function getSchema(useFlexibleSync: boolean) {
);
});

it.skip(`handles recovery client reset with ${getPartialTestTitle(
it(`handles recovery client reset with ${getPartialTestTitle(
useFlexibleSync,
)} sync enabled`, async function (this: RealmContext) {
// (i) using a client reset in "Recovery" mode, a fresh copy
Expand Down
42 changes: 42 additions & 0 deletions integration-tests/tests/src/utils/baas-admin-api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
////////////////////////////////////////////////////////////////////////////
//
// Copyright 2024 Realm Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
////////////////////////////////////////////////////////////////////////////

import { AdminApiClient, Credentials } from "@realm/app-importer";

const {
baseUrl = "http://localhost:9090",
username = "[email protected]",
password = "password",
publicKey,
privateKey,
} = environment;

export const credentials: Credentials =
typeof publicKey === "string" && typeof privateKey === "string"
? {
kind: "api-key",
publicKey,
privateKey,
}
: {
kind: "username-password",
username,
password,
};

export const baasAdminClient = new AdminApiClient({ baseUrl, credentials });
28 changes: 28 additions & 0 deletions integration-tests/tests/src/utils/baas-app-importer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
////////////////////////////////////////////////////////////////////////////
//
// Copyright 2024 Realm Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
////////////////////////////////////////////////////////////////////////////

import { AppImporter } from "@realm/app-importer";

import { baasAdminClient } from "./baas-admin-api";

const { reuseApp = false } = environment;

export const baasAppImporter = new AppImporter({
client: baasAdminClient,
reuseApp,
});
34 changes: 28 additions & 6 deletions packages/realm-app-importer/src/AdminApiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export type Credentials =
export type AuthenticationMode = "access" | "refresh" | "none";

type ClientFetchRequest = Omit<RequestInit, "headers" | "body"> & {
baseRoute?: string;
route: string[];
headers?: Record<string, string>;
authentication?: AuthenticationMode;
Expand All @@ -61,19 +62,25 @@ type ClientFetchRequest = Omit<RequestInit, "headers" | "body"> & {

type AdminApiClientConfig = {
baseUrl: string;

/**
* Administrative credentials to use when authenticating against the server.
*/
credentials: Credentials;
};

/**
* Simplified client for the Atlas App Services Admin API.
*/
export class AdminApiClient {
private static readonly baseRoute = "api/admin/v3.0";
private static readonly privateBaseRoute = "api/private/v1.0";

private accessToken: string | null = null;
private refreshToken: string | null = null;
private _groupId: Promise<string> | null = null;

constructor(private config: AdminApiClientConfig) {}
constructor(public readonly config: AdminApiClientConfig) {}

public get groupId(): Promise<string> {
if (!this._groupId) {
Expand Down Expand Up @@ -114,9 +121,9 @@ export class AdminApiClient {
}
}

public async ensureLogIn(credentials: Credentials) {
public async ensureLogIn() {
if (!this.accessToken) {
await this.logIn(credentials);
await this.logIn(this.config.credentials);
}
}

Expand Down Expand Up @@ -341,6 +348,14 @@ export class AdminApiClient {
});
}

public async forceSyncReset(appId: string, fileIdent: number) {
await this.fetch({
route: ["groups", await this.groupId, "apps", appId, "sync", "force_reset"],
method: "PUT",
body: { file_ident: fileIdent },
});
}

public async applyAllowedRequestOrigins(appId: string, origins: string[]) {
await this.fetch({
route: ["groups", await this.groupId, "apps", appId, "security", "allowed_request_origins"],
Expand All @@ -363,8 +378,15 @@ export class AdminApiClient {
}

private async fetch(request: ClientFetchRequest): Promise<unknown> {
const { route, body, headers = {}, authentication = "access", ...rest } = request;
const url = [this.config.baseUrl, AdminApiClient.baseRoute, ...route].join("/");
const {
baseRoute = AdminApiClient.baseRoute,
route,
body,
headers = {},
authentication = "access",
...rest
} = request;
const url = [this.config.baseUrl, baseRoute, ...route].join("/");
try {
if (authentication === "access") {
if (!this.accessToken) {
Expand Down Expand Up @@ -394,7 +416,7 @@ export class AdminApiClient {
const error = isErrorResponse(json) ? json.error : "No error message";
throw new Error(`Failed to fetch ${url}: ${error} (${response.status} ${response.statusText})`);
} else {
throw new Error(`Failed to fetch ${url}: ${response.status} ${response.statusText})`);
throw new Error(`Failed to fetch ${url} (${response.status} ${response.statusText})`);
}
} catch (err) {
if (err instanceof Error && err.message.includes("invalid session: access token expired")) {
Expand Down
20 changes: 6 additions & 14 deletions packages/realm-app-importer/src/AppImporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,9 @@ type App = {
};
export interface AppImporterOptions {
/**
* The server's URL.
* The client to use when importing.
*/
baseUrl: string;
/**
* Administrative credentials to use when authenticating against the server.
*/
credentials: Credentials;
client: AdminApiClient;
/**
* Re-use a single app instead of importing individual apps.
* This will redeploy a previously known "clean" version of the app (to revert any configurations) and delete secrets off the app,
Expand All @@ -64,20 +60,16 @@ export interface AppImporterOptions {
}

export class AppImporter {
private readonly baseUrl: string;
private readonly credentials: Credentials;
private readonly reuseApp: boolean;
private readonly awaitDeployments: boolean;
private readonly client: AdminApiClient;
private initialDeployment: Deployment | null = null;
private reusedApp: App | null = null;

constructor({ baseUrl, credentials, reuseApp = false, awaitDeployments = false }: AppImporterOptions) {
this.baseUrl = baseUrl;
this.credentials = credentials;
constructor({ client, reuseApp = false, awaitDeployments = false }: AppImporterOptions) {
this.reuseApp = reuseApp;
this.awaitDeployments = awaitDeployments;
this.client = new AdminApiClient({ baseUrl });
this.client = client;
}

public async createOrReuseApp(name: string) {
Expand Down Expand Up @@ -126,7 +118,7 @@ export class AppImporter {
* @returns A promise of an object containing the app id.
*/
public async importApp(config: AppConfig): Promise<ImportedApp> {
await this.client.ensureLogIn(this.credentials);
await this.client.ensureLogIn();

const app = await this.createOrReuseApp(config.name);
try {
Expand All @@ -141,7 +133,7 @@ export class AppImporter {
}

debug(`The application ${app.client_app_id} was successfully deployed:`);
debug(`${this.baseUrl}/groups/${await this.client.groupId}/apps/${app._id}/dashboard`);
debug(`${this.client.config.baseUrl}/groups/${await this.client.groupId}/apps/${app._id}/dashboard`);

return { appName: config.name, appId: app.client_app_id };
} catch (err) {
Expand Down
2 changes: 2 additions & 0 deletions packages/realm-app-importer/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,7 @@ export type {
CustomTokenAuthMetadataField,
EmailPasswordAuthConfig,
} from "./AppConfigBuilder";

export { AdminApiClient } from "./AdminApiClient";
export { AppImporter } from "./AppImporter";
export { AppConfigBuilder } from "./AppConfigBuilder";
Loading

0 comments on commit b473415

Please sign in to comment.