Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore(datastore): Add SchemaDrift integration tests #1800

Merged
merged 6 commits into from
Jul 6, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ __pycache__/
# Credentials
**/awsconfiguration.json
**/amplifyconfiguration.json
**/amplifyconfiguration_v2.json
**/credentials.json
**/google_client_creds.json

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
mutation MyMutation {
createSchemaDrift(input: {enumValue: THREE}) {
_deleted
_lastChangedAt
_version
createdAt
enumValue
updatedAt
id
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
/*
* Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file 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.
*/

package com.amplifyframework.datastore;

import android.content.Context;
import androidx.annotation.RawRes;

import com.amplifyframework.AmplifyException;
import com.amplifyframework.api.ApiCategory;
import com.amplifyframework.api.aws.AWSApiPlugin;
import com.amplifyframework.api.aws.GsonVariablesSerializer;
import com.amplifyframework.api.graphql.SimpleGraphQLRequest;
import com.amplifyframework.core.Amplify;
import com.amplifyframework.core.AmplifyConfiguration;
import com.amplifyframework.core.category.CategoryConfiguration;
import com.amplifyframework.core.category.CategoryType;
import com.amplifyframework.core.model.temporal.Temporal;
import com.amplifyframework.datastore.appsync.AppSyncClient;
import com.amplifyframework.datastore.appsync.SynchronousAppSync;
import com.amplifyframework.hub.HubChannel;
import com.amplifyframework.logging.AndroidLoggingPlugin;
import com.amplifyframework.logging.LogLevel;
import com.amplifyframework.testmodels.transformerV2.schemadrift.EnumDrift;
import com.amplifyframework.testmodels.transformerV2.schemadrift.SchemaDrift;
import com.amplifyframework.testmodels.transformerV2.schemadrift.SchemaDriftModelProvider;
import com.amplifyframework.testutils.Assets;
import com.amplifyframework.testutils.HubAccumulator;
import com.amplifyframework.testutils.Resources;
import com.amplifyframework.testutils.sync.SynchronousApi;
import com.amplifyframework.testutils.sync.SynchronousDataStore;

import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;

import java.util.Date;
import java.util.HashMap;
import java.util.concurrent.TimeUnit;

import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
import static com.amplifyframework.datastore.DataStoreHubEventFilters.publicationOf;
import static com.amplifyframework.datastore.DataStoreHubEventFilters.receiptOf;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;

public final class SchemaDriftTest {
private static final int TIMEOUT_SECONDS = 60;

private static SynchronousApi api;
private static SynchronousAppSync appSync;
private static SynchronousDataStore dataStore;

/**
* Once, before any/all tests in this class, setup miscellaneous dependencies,
* including synchronous API, AppSync, and DataStore interfaces. The API and AppSync instances
* are used to arrange/validate data. The DataStore interface will delegate to an
* {@link AWSDataStorePlugin}, which is the thing we're actually testing.
* @throws AmplifyException On failure to read config, setup API or DataStore categories
*/
@BeforeClass
public static void setup() throws AmplifyException {
Amplify.addPlugin(new AndroidLoggingPlugin(LogLevel.VERBOSE));

StrictMode.enable();
Context context = getApplicationContext();
@RawRes int configResourceId = Resources.getRawResourceId(context, "amplifyconfiguration_v2");

// Setup an API
CategoryConfiguration apiCategoryConfiguration =
AmplifyConfiguration.fromConfigFile(context, configResourceId)
.forCategoryType(CategoryType.API);
ApiCategory apiCategory = new ApiCategory();
apiCategory.addPlugin(new AWSApiPlugin());
apiCategory.configure(apiCategoryConfiguration, context);

// To arrange and verify state, we need to access the supporting AppSync API
api = SynchronousApi.delegatingTo(apiCategory);
appSync = SynchronousAppSync.using(AppSyncClient.via(apiCategory));

long tenMinutesAgo = new Date().getTime() - TimeUnit.MINUTES.toMillis(10);
Temporal.DateTime tenMinutesAgoDateTime = new Temporal.DateTime(new Date(tenMinutesAgo), 0);
DataStoreCategory dataStoreCategory = DataStoreCategoryConfigurator.begin()
.api(apiCategory)
.clearDatabase(true)
.context(context)
.modelProvider(SchemaDriftModelProvider.getInstance())
.resourceId(configResourceId)
.timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
.dataStoreConfiguration(DataStoreConfiguration.builder()
.syncExpression(SchemaDrift.class, () -> SchemaDrift.CREATED_AT.gt(tenMinutesAgoDateTime))
.build())
.finish();
dataStore = SynchronousDataStore.delegatingTo(dataStoreCategory);
}

/**
* Clear the DataStore after each test. Without calling clear in between tests, all tests after the first will fail
* with this error: android.database.sqlite.SQLiteReadOnlyDatabaseException: attempt to write a readonly database.
* @throws DataStoreException On failure to clear DataStore.
*/
@AfterClass
public static void teardown() throws DataStoreException {
if (dataStore != null) {
try {
dataStore.clear();
} catch (Exception error) {
// ok to ignore since problem encountered during tear down of the test.
}
}
}

/**
* Perform DataStore.save.
* Expected result: Model should be published to AppSync.
* @throws AmplifyException Not expected.
*/
@Test
public void testSave() throws AmplifyException {
dataStore.start();
SchemaDrift localSchemaDrift = SchemaDrift.builder()
.createdAt(new Temporal.DateTime(new Date(), 0))
.enumValue(EnumDrift.ONE)
.build();
String modelName = SchemaDrift.class.getSimpleName();
HubAccumulator publishedMutationsAccumulator =
HubAccumulator.create(HubChannel.DATASTORE, publicationOf(modelName, localSchemaDrift.getId()), 1)
.start();

dataStore.save(localSchemaDrift);

// Wait for a Hub event telling us that our model got published to the cloud.
publishedMutationsAccumulator.await(TIMEOUT_SECONDS, TimeUnit.SECONDS);

// Retrieve from the backend.
SchemaDrift remoteModel = api.get(SchemaDrift.class, localSchemaDrift.getId());
assertEquals(remoteModel.getId(), localSchemaDrift.getId());
}

/**
* Save a SchemaDrift model with enum value "THREE" by calling API directly with the
* mutation request document since the code generated EnumDrift was modified to remove the
* case so it wouldn't be possible otherwise due to type safety.
*
* Expected result: Model should be received as a subscription event and synced to local store.
* @throws AmplifyException Not expected.
*/
@Test
public void testSyncEnumWithInvalidValue() throws AmplifyException {
// Send the model directly to API
SchemaDrift directSchemaDrift = api.create(
new SimpleGraphQLRequest<>(
Assets.readAsString("schema-drift-mutation.graphql"),
new HashMap<>(),
SchemaDrift.class,
new GsonVariablesSerializer()
)
);
HubAccumulator receiptOfSchemaDrift =
HubAccumulator.create(HubChannel.DATASTORE, receiptOf(directSchemaDrift.getId()), 1)
.start();

// Retrieve it directly from API
SchemaDrift remoteModel = api.get(SchemaDrift.class, directSchemaDrift.getId());
assertEquals(remoteModel.getId(), directSchemaDrift.getId());

// Start and sync the models from AppSync
dataStore.start();

// Ensure that the model was synced.
receiptOfSchemaDrift.await(TIMEOUT_SECONDS, TimeUnit.SECONDS);

// Query for the model, expect that the enumValue is null.
SchemaDrift getSchemaDrift = dataStore.get(SchemaDrift.class, directSchemaDrift.getId());
assertNull(getSchemaDrift.getEnumValue());
}
}
3 changes: 3 additions & 0 deletions scripts/pull_backend_config_from_s3
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ readonly config_files=(
"aws-datastore/src/androidTest/res/raw/credentials.json"
"aws-datastore/src/androidTest/res/raw/google_client_creds.json"

# DataStore V2
"aws-datastore/src/androidTest/res/raw/amplifyconfiguration_v2.json"

# Predictions
"aws-predictions/src/androidTest/res/raw/amplifyconfiguration.json"
"aws-predictions/src/androidTest/res/raw/awsconfiguration.json"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# The schema is provisioned using Transformer V2 and CLI version 9.0.0
# The amplify/cli.json file needs to have the following:
# - useexperimentalpipelinedtransformer": true
# - "transformerversion": 2
# The build time API configuration were used:
# - Authorization modes: `API key (default, expiration time: 365 days from now)``
# - Conflict detection (required for DataStore): `Enabled`
# - Conflict resolution strategy: `Auto Merge`

# The schema below will provision multiple sets of models, each numerated with a comment dividing
# each set of models, and are exclusive of each other. This is so a single backend can be reused
# across multiple test classes. The generated models and the ModelProvider is placed within its
# directory, ie. `transformerV2.schemadrift/SchemaDriftModelProvider` as an artifact for test
# classes to configure only the models to be tested with. This is especially useful when testing
# DataStore and only wanting syncing down only necessary models.
#
# Having a shared backend also makes it easier to manage the integration tests in the pipeline,
# since adding additional models will only require the existing backend to be updated. Adding
# new models to the schema should consist of a use case that does not break the existing build
# time configuration.

# 1. Schema Drift
# The SchemaDrift model involves using the following schema to set up the backend and local
# modifications to mimic an out-of-date client. The local modifications for `EnumDrift` is to
# remove the enum case THREE, replicating a scenario with which the backend is the updated schema
# and frontend is the older client which has not upgraded to the latest code generated types.

input AMPLIFY { globalAuthRule: AuthRule = { allow: public } } # FOR TESTING ONLY!

type SchemaDrift @model {
id: ID!
createdAt: AWSDateTime!
enumValue: EnumDrift
}

enum EnumDrift {
ONE
TWO
THREE
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.amplifyframework.testmodels.transformerV2.schemadrift;

/**
* This schema has been manually modified to create a schema drift scenario.
* One of the enum cases in EnumDrift has been removed. This allows tests to
* decode data that contains the missing value to further observe the state of the system.
* Data that contains the missing value needs to be persisted with API directly
* using a custom GraphQL document/variables since model objects cannot be created with the
* commented out enum case.
**/

/** Auto generated enum from GraphQL schema. */
@SuppressWarnings("all")
public enum EnumDrift {
ONE,
TWO //,
// THREE
}
Loading