Can not run multiple tests in a file - testing

I'm building a GraphQL API and I want to test some resolvers and the database with jest.
Here is my helper file, where I set up the context and the Prisma Client for testing.
import { PrismaClient } from "#prisma/client";
import { ServerInfo } from "apollo-server";
import { execSync } from "child_process";
import getPort, { makeRange } from "get-port";
import { GraphQLClient } from "graphql-request";
import { nanoid } from "nanoid";
import { join } from "path";
import { Client } from "pg";
import { server } from "../api/server";
type TestContext = {
client: GraphQLClient;
db: PrismaClient;
};
export function createTestContext(): TestContext {
let ctx = {} as TestContext;
const graphqlCtx = graphqlTestContext();
const prismaCtx = prismaTestContext();
beforeEach(async () => {
const client = await graphqlCtx.before();
const db = await prismaCtx.before();
Object.assign(ctx, {
client,
db,
});
});
afterEach(async () => {
await graphqlCtx.after();
await prismaCtx.after();
});
return ctx;
}
function graphqlTestContext() {
let serverInstance: ServerInfo | null = null;
return {
async before() {
const port = await getPort({ port: makeRange(4000, 6000) });
serverInstance = await server.listen({ port });
return new GraphQLClient(`http://localhost:${port}`);
},
async after() {
serverInstance?.server.close();
},
};
}
function prismaTestContext() {
const prismaBinary = join(__dirname, "..", "node_modules", ".bin", "prisma");
let schema = "";
let databaseUrl = "";
let prismaClient: null | PrismaClient = null;
return {
async before() {
schema = `test_${nanoid()}`;
databaseUrl = `postgresql://user:123#localhost:5432/testing?schema=${schema}`;
process.env.DATABASE_URL = databaseUrl;
execSync(`${prismaBinary} migrate up --create-db --experimental`, {
env: {
...process.env,
DATABASE_URL: databaseUrl,
},
});
prismaClient = new PrismaClient();
return prismaClient;
},
async after() {
const client = new Client({
connectionString: databaseUrl,
});
await client.connect();
await client.query(`DROP SCHEMA IF EXISTS "${schema}" CASCADE`);
await client.end();
await prismaClient?.$disconnect();
},
};
}
My test file looks like this:
import { createTestContext } from "./__helpers";
const ctx = createTestContext();
it("register user", async () => {
const testUser = {
username: "Test",
email: "test#test.com",
password: "password",
};
const registerResult = await ctx.client.request(
`
mutation registerNewUser($username: String!, $email: String!, $password: String!) {
register(username: $username, email: $email, password: $password) {
user {
user_id
username
email
}
}
}
`,
{
username: testUser.username,
email: testUser.email,
password: testUser.password,
}
);
const resultUsername = registerResult.register.user.username;
const resultEmail = registerResult.register.user.email;
const resultUserID = registerResult.register.user.user_id;
expect(resultUsername).toBe(testUser.username);
expect(resultEmail).toBe(testUser.email);
expect(resultUserID).not.toBeNull;
const users = await ctx.db.user.findMany();
const savedUser = users[0];
expect(savedUser.username).toBe(testUser.username);
expect(savedUser.email).toBe(testUser.email);
expect(savedUser.user_id).toBe(resultUserID);
expect(savedUser.first_name).toBeNull;
expect(savedUser.last_name).toBeNull;
expect(savedUser.role).toBe("USER");
expect(savedUser.password).not.toBe(testUser.password);
});
it("all events", async () => {
const eventsResult = await ctx.client.request(
`
query {
allEvents {
event_id
title
description
}
}
`
);
expect(eventsResult.allEvents.length).toBe(0)
});
When I just run one file with one test in it, everything works. But when I run multiple tests in one file, the first one runs normal, but the ones after not. I receive this error:
The table `test_LjrcmbMjI4vLaDYM9-lvw.Event` does not exist in the current database.: {"response":{"errors":[{"message":"\nInvalid `prisma.event.findMany()` invocation:\n\n\n The table `test_LjrcmbMjI4vLaDYM9-lvw.Event` does not exist in the current database.","locations":[{"line":3,"column":7}],"path":["allEvents"],"extensions":{"code":"INTERNAL_SERVER_ERROR","exception":{"code":"P2021","clientVersion":"2.11.0","meta":{"table":"test_LjrcmbMjI4vLaDYM9-lvw.Event"}}}}],"data":null,"status":200},"request":{"query":"\n query {\n allEvents {\n event_id\n title\n description\n }\n }\n "}}
Also when I run two tests in separated files, on every second test run I get this error:
listen EADDRINUSE: address already in use :::4200
I did the nexus tutorial (Step 4, and 5), where they explained how to test, but somehow it doesn't work. So please help me.
https://nexusjs.org/docs/getting-started/tutorial

I have created a repo with parallel tests for the same here. The test environment setup is in the prisma folder and a similar helper is created in the tests folder.

Related

(redis-om & fastify) this.writeEntity is not a function

I'm learning to use the Redis for my backend database and I would like to try using redis-om for fastify not sure whether they are compatible or not, but I got error.
I use service of app.redislabs.com
I don't know what I just messed up? And how can I fix the problem?
server.js
const { createCar, createIndex } = require("./redis");
app.post("/add", async (req, res) => {
await createIndex();
const { make, model, image, description } = req.body;
const data = { make, model, image, description };
await createCar(data);
res.code(200).send('ok');
});
const PORT = 5000;
app.listen(PORT, function (err) {
if (err) {
app.log.error(err);
process.exit(1);
}
});
redis.js
const { Client, Entity, Schema, Repository } = require("redis-om");
const client = new Client();
const connect = async () => {
if (!client.isOpen()) {
await client.open("redis://default:password#localhost:6379");
} else {
console.log("CONNECTED");
}
};
class Car extends Entity {}
let schema = new Schema(
Car,
{
make: { type: "string" },
model: { type: "string" },
image: { type: "string" },
description: { type: "string" },
},
{ dataStructure: "JSON" }
);
const createCar = async (data) => {
await connect();
const repository = new Repository(schema, client);
const car = repository.createEntity(data);
const id = await repository.save(car);
return id;
};
const createIndex = async () => {
await connect();
const repository = new Repository(schema, client);
await repository.createIndex();
};
module.exports = {
createCar,
createIndex,
};
My JSON Body
You cannot call new on Repository. This is a breaking change I introduced in version 0.2.0 of Redis OM. There are a couple of others that are documented in the CHANGELOG.
Call const repository = client.fetchRepository(schema) instead, as shown here. Unfortunately, there are some videos and blogs that have the older syntax and so this crops up from time to time.
Thanks for using my library!

Why is firebase authentication not persisting on refresh in Ionic with Vue and Pinia?

I've been generally following along with this code here: https://github.com/aaronksaunders/ionic-v6-firebase-tabs-auth
The issue I'm having is my auth state is not persisting when I refresh the page when using ionic serve and loading the app in the web browser.
code for pinia store:
import { defineStore } from "pinia";
import { User } from "firebase/auth";
import {
Profile,
getProfile,
setProfile,
} from "#/firebase/helpers/firestore/profileManager";
import { onSnapshot, Unsubscribe, doc } from "#firebase/firestore";
import { db } from "#/firebase/connectEmulators";
import { getAuth } from "#firebase/auth";
import {
onAuthStateChanged,
signInWithEmailAndPassword,
signOut,
createUserWithEmailAndPassword,
updateProfile as updateAuthProfile,
} from "firebase/auth";
import errorHandler from "#/helpers/errorHandler";
/**#see {#link Profile} */
export enum UserType {
DNE,
uploader,
checker,
host,
}
interface State {
user: User | null;
profile: Profile | null;
error: null | any;
unsub: Unsubscribe | null;
}
export const useUserStore = defineStore("user", {
state: (): State => {
return {
user: null,
profile: null,
error: null,
unsub: null,
};
},
getters: {
isLoggedIn: (state) => state.user !== null,
//DEV: do we need this to be a getter?
userError: (state) => {
if(state.error){
switch (state.error.code) {
case "auth/user-not-found":
return "Email or Password incorrect!";
case "auth/wrong-password":
return "Email or Password incorrect!";
default:
return state.error;
}
}
return null;
},
/**
* #see Profile
*/
getType: (state): UserType => {
if (state.user === null) return UserType.DNE;
if (!state.profile) return UserType.DNE;
if (state.profile.locations.length > 0) return UserType.host;
if (state.profile.queues.length > 0) return UserType.checker;
return UserType.uploader;
},
},
actions: {
initializeAuthListener() {
return new Promise((resolve) => {
const auth = getAuth();
onAuthStateChanged(auth, (user) => {
console.log("AuthListener Initialized");
if (user) {
console.log("AuthListener: User exists!");
this.user = user;
getProfile(user.uid).then((profile) => {
if (profile) {
this.profile = profile;
this.initializeProfileListener();
} else {
this.profile = null;
if (this.unsub) this.unsub();
}
});
} else {
console.log("AuthListener: User does not exist!");
this.user = null;
}
resolve(true);
});
});
},
/**
*
* #param email email for login
* #param password password for login
*/
async signInEmailPassword(email: string, password: string) {
try {
const auth = getAuth();
const userCredential = await signInWithEmailAndPassword(
auth,
email,
password
);
this.user = userCredential.user ? userCredential.user : null;
this.error = null;
return true;
} catch (error: any) {
console.log(typeof error.code);
console.log(error.code);
this.user = null;
this.error = error;
return false;
}
},
async logoutUser() {
try {
const auth = getAuth();
await signOut(auth);
this.user = null;
this.profile = null;
this.error = null;
if (this.unsub) this.unsub();
return true;
} catch (error: any) {
this.error = error;
return false;
}
},
async createEmailPasswordAccount(
email: string,
password: string,
userName: string,
refSource: string
) {
try {
const auth = getAuth();
const userCredential = await createUserWithEmailAndPassword(
auth,
email,
password
);
//Add username to fireauth profile
//DEV: test for xss vulnerabilities
await updateAuthProfile(userCredential.user, { displayName: userName });
//create user profile data in firestore
let profile: Profile | undefined = new Profile(
userCredential.user.uid,
refSource
);
await setProfile(profile);
profile = await getProfile(userCredential.user.uid);
//set local store
this.user = userCredential.user ? userCredential.user : null;
this.profile = profile ? profile : null;
this.error = null;
//TODO: send email verification
return true;
} catch (error: any) {
this.user = null;
this.error = error;
return false;
}
},
initializeProfileListener() {
try {
if (!this.profile) errorHandler(Error("Profile not set in state!"));
else {
const uid = this.profile.uid;
const unsub: Unsubscribe = onSnapshot(
doc(db, "profiles", uid),
(snapshot) => {
const fbData = snapshot.data();
if (!fbData)
errorHandler(Error("Profile Listener snapshot.data() Null!"));
else {
const profile = new Profile(
snapshot.id,
fbData.data.referralSource
);
profile.data = fbData.data;
profile.settings = fbData.settings;
profile.locations = fbData.locations;
profile.queues = fbData.queues;
profile.checkers = fbData.checkers;
profile.uploadHistory = fbData.uploadHistory;
profile.hostLevel = fbData.hostLevel;
this.profile = profile;
}
},
(error) => {
errorHandler(error);
}
);
this.unsub = unsub;
}
} catch (error) {
errorHandler(error as Error);
}
},
},
});
main.ts where I initialize auth listener:
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
import { IonicVue } from "#ionic/vue";
/* Core CSS required for Ionic components to work properly */
import "#ionic/vue/css/core.css";
/* Basic CSS for apps built with Ionic */
import "#ionic/vue/css/normalize.css";
import "#ionic/vue/css/structure.css";
import "#ionic/vue/css/typography.css";
/* Optional CSS utils that can be commented out */
import "#ionic/vue/css/padding.css";
import "#ionic/vue/css/float-elements.css";
import "#ionic/vue/css/text-alignment.css";
import "#ionic/vue/css/text-transformation.css";
import "#ionic/vue/css/flex-utils.css";
import "#ionic/vue/css/display.css";
/* Theme variables */
import "./theme/variables.css";
/* PWA elements for using Capacitor plugins */
import { defineCustomElements } from "#ionic/pwa-elements/loader";
/* Pinia used for state management */
import { createPinia } from "pinia";
import { useUserStore } from "./store/userStore";
const pinia = createPinia();
const app = createApp(App)
.use(IonicVue, {
// TODO: remove for production
mode: process.env.VUE_APP_IONIC_MODE,
})
.use(pinia);
defineCustomElements(window);
//get the store
const store = useUserStore();
store.initializeAuthListener().then(() => {
app.use(router);
});
router.isReady().then(() => {
app.mount("#app");
});
I've tried refactoring main.ts to mount the app inside the callback for initialize auth listener and I've tried making my code exactly like the code in the main.ts of the above link. Neither solved the issue.
I also looked at the question here: https://stackoverflow.com/a/67774186/9230780
Most of the points in the answer shouldn't be related because I'm currently using the firebase emulators to test the app.
Even still, I've verified my api key is correct.
I can see that cookies are created in the browser when I launch the app, so I don't think its an issue with them being wiped.
Ideally I'd like to avoid implementing #capacitor/storage here because it shouldn't be necessary.
I do plan to implement this library to handle authentication for ios and android: https://github.com/baumblatt/capacitor-firebase-auth
but that shouldn't be pertinent to the web version of the app.
Edit:
Realized I was missing a piece of code pertinent to the question. Not sure how I didn't copy it over. Code added is the initialize Profile listener function.
I ended up doing a refactor of the pinia store and that solved the issue. I believe the issue may have been caused by how the auth listener called initializeProfileListener. I didn't have code in the auth listener to check if the profile listener was already initialized, so everytime the authstate changed or it would initialize a new profile listener without unsubbing the old one. I'm not absolutely certain that is what was causing the issue though.
Below is the new code that functions properly.
pinia store:
import { defineStore } from "pinia";
import { User } from "firebase/auth";
import {
Profile,
getProfile,
profileListener,
} from "#/firebase/helpers/firestore/profileManager";
import {
fbCreateAccount,
fbSignIn,
fbAuthStateListener,
fbSignOut,
} from "#/firebase/helpers/firestore/authHelper";
import {Unsubscribe} from "#firebase/firestore";
import errorHandler from "#/helpers/errorHandler";
/**#see {#link Profile} */
export enum UserType {
DNE,
uploader,
checker,
host,
}
interface State {
user: User | null;
profile: Profile | null;
error: null | any;
unsub: Unsubscribe | null;
}
export const useUserStore = defineStore("user", {
state: (): State => {
return {
user: null,
profile: null,
error: null,
unsub: null,
};
},
getters: {
isLoggedIn: (state) => state.user !== null,
//DEV: do we need this to be a getter?
userError: (state) => {
if (state.error) {
switch (state.error.code) {
case "auth/user-not-found":
return "Email or Password incorrect!";
case "auth/wrong-password":
return "Email or Password incorrect!";
default:
return state.error;
}
}
return null;
},
/**
* #see Profile
*/
getType: (state): UserType => {
if (state.user === null) return UserType.DNE;
if (!state.profile) return UserType.DNE;
if (state.profile.locations.length > 0) return UserType.host;
if (state.profile.queues.length > 0) return UserType.checker;
return UserType.uploader;
},
},
actions: {
initializeAuthListener() {
return new Promise((resolve) => {
fbAuthStateListener(async (user: any) => {
if (user) {
this.user = user;
const profile = await getProfile(user.uid);
if (profile) {
this.profile = profile;
//TODO: initialize profile listener
if(this.unsub === null) {
this.initializeProfileListener();
}
}
}
resolve(true);
});
});
},
/**
*
* #param email email for login
* #param password password for login
*/
async signInEmailPassword(email: string, password: string) {
try {
const userCredential = await fbSignIn(email, password);
this.user = userCredential.user ? userCredential.user : null;
this.error = null;
return true;
} catch (error: any) {
console.log(typeof error.code);
console.log(error.code);
this.user = null;
this.error = error;
return false;
}
},
async logoutUser() {
try {
await fbSignOut();
this.user = null;
this.profile = null;
this.error = null;
if (this.unsub) this.unsub();
return true;
} catch (error: any) {
this.error = error;
return false;
}
},
async createEmailPasswordAccount(
email: string,
password: string,
userName: string,
refSource: string
) {
try {
const { user, profile } = await fbCreateAccount(
email,
password,
userName,
refSource
);
//set local store
this.user = user ? user : null;
this.profile = profile ? profile : null;
this.error = null;
//TODO: send email verification
return true;
} catch (error: any) {
this.user = null;
this.error = error;
return false;
}
},
initializeProfileListener() {
try {
if (this.user) {
const unsub = profileListener(
this.user?.uid,
async (profile: any) => {
if (profile) {
this.profile = profile;
}
}
);
this.unsub = unsub;
}
} catch (error) {
errorHandler(error as Error);
}
},
},
});
authHelper.ts
import { auth } from "#/firebase/firebase";
import {
createUserWithEmailAndPassword,
signInWithEmailAndPassword,
signOut,
onAuthStateChanged,
updateProfile as updateAuthProfile,
} from "#firebase/auth";
import { Profile, setProfile, getProfile } from "./profileManager";
/**
* #param email
* #param password
* #param userName
* #param refSource #see profileManager
* #returns
*/
export const fbCreateAccount = async (
email: string,
password: string,
userName: string,
refSource: string
) => {
//DEBUG: creating a user works but throws an error.
const userCredential = await createUserWithEmailAndPassword(
auth,
email,
password
);
if (userCredential) {
//add username to fireauth profile
await updateAuthProfile(userCredential.user, { displayName: userName });
//create user profile data in firestore
let profile: Profile | undefined = new Profile(
userCredential.user.uid,
refSource
);
await setProfile(profile);
profile = await getProfile(userCredential.user.uid);
//TODO: errorHandling for setProfile and getProfile
return {
user: userCredential.user,
profile: profile,
};
} else {
return {
user: null,
profile: null,
};
}
};
/**
*
* #param email
* #param password
* #returns UserCredential {#link https://firebase.google.com/docs/reference/js/auth.usercredential.md?authuser=0#usercredential_interface}
*/
export const fbSignIn = async (email: string, password: string) => {
const userCredential = signInWithEmailAndPassword(auth, email, password);
//TODO: add call to add to profile signins array
return userCredential;
};
export const fbSignOut = async () => {
await signOut(auth);
return true;
};
/**
* #see {#link https://firebase.google.com/docs/reference/js/auth.md?authuser=0&hl=en#onauthstatechanged}
* #param callback contains either user or null
*/
export const fbAuthStateListener = (callback: any) => {
onAuthStateChanged(auth, (user) => {
if (user) {
//user is signed in
callback(user);
} else {
//user is signed out
callback(null);
}
});
};

why setup() doesnt work "Cannot read property 'username' of undefined "

when i use toLogin in methods it works, but use in setup and return(in this code - test) i get error Cannot read property 'username' of undefined doesnt work, why?
export default {
name: "Login",
setup() {
const username = ref("");
const password = ref("");
const router = useRouter();
const test = async () => {
console.log(this.username.value)
}
return {
username,
password,
router,
test
};
},
methods: {
async toLogin() {
const rep = await axios.post("login", {
username: this.username,
password: this.password,
});
console.log(rep)
//await this.router.push("/successlogin");
},
},
};
You don't need this. Instead, just use console.log(username.value). username is already captured in scope for the test function to use.
The docs here https://v3.vuejs.org/guide/composition-api-introduction.html#setup-component-option contain this example:
setup (props) {
const repositories = ref([])
const getUserRepositories = async () => {
repositories.value = await fetchUserRepositories(props.user)
}
return {
repositories,
getUserRepositories
}
}

Mock data initialization being subject to Firestore rules

I'm testing a simple rule:
match /users/{userId} {
allow write, get: if isSignedIn() && userOwner(userId);
}
function isSignedIn(){
return request.auth != null
}
function userOwner(userId){
return userId == request.auth.uid
}
Here is my test:
test("read succeed only if requested user is authenticated user", async () => {
const db = await setup(
{
uid: "testid",
email: "test#test.com"
},
{
"users/testid": {},
"users/anotherid": {}
}
);
const userRef = db.collection("users");
expect(await assertSucceeds(userRef.doc("testid").get()));
expect(await assertFails(userRef.doc("anotherid").get()));
})
And the setup method:
export const setup = async (auth?: any, data?: any) => {
const projectId = `rules-spec-${Date.now()}`;
const app = firebase.initializeTestApp({
projectId,
auth
});
const db = app.firestore();
if (data) {
for (const key in data) {
const ref = db.doc(key);
await ref.set(data[key]);
}
}
await firebase.loadFirestoreRules({
projectId,
rules: fs.readFileSync("firestore.rules").toString()
});
return db;
};
It throws the following error :
FirebaseError: 7 PERMISSION_DENIED:
false for 'create' # L5, Null value error. for 'create' # L9
It seems that when it tries to set the mock data given in setup, it can't because of the write rule. but I don't understand, I load the rules after the database being set.
Any idea what's going on here?
You can try setting the rules to be open before you populate the database.
After the data is set you load the rules you are trying to test.
export const setup = async(auth ? : any, data ? : any) => {
const projectId = `rules-spec-${Date.now()}`;
const app = firebase.initializeTestApp({
projectId,
auth
});
const db = app.firestore();
await firebase.loadFirestoreRules({
projectId,
rules:
"service cloud.firestore {match/databases/{database}/documents" +
"{match /{document=**} {" +
"allow read, write: if true;" +
"}}}",
});
if (data) {
for (const key in data) {
const ref = db.doc(key);
await ref.set(data[key]);
}
}
await firebase.loadFirestoreRules({
projectId,
rules: fs.readFileSync("firestore.rules").toString()
});
return db;
};

{"errors":[{"message":"Must provide query string."}]}

I want to upload image on by using graphql-upload package
This is my scheme file
const { GraphQLSchema, GraphQLObjectType, GraphQLBoolean } = require("graphql");
const { GraphQLUpload } = require("graphql-upload");
const schema = new GraphQLSchema({
mutation: new GraphQLObjectType({
name: "Mutation",
fields() {
return {
uploadImage: {
description: "Uploads an image.",
type: GraphQLBoolean,
args: {
image: {
description: "Image file.",
type: GraphQLUpload
}
},
async resolve(parent, { image }) {
const { filename, mimetype, createReadStream } = await image;
const stream = createReadStream();
// Promisify the stream and store the file, then…
return true;
}
}
};
}
})
});
This is my server file. When I run the file it gives error on the browser side. it showed: {"errors":[{"message":"Must provide query string."}]}.
const express = require("express");
const graphqlHTTP = require("express-graphql");
const { graphqlUploadExpress } = require("graphql-upload");
const schema = require("./schema");
express()
.use(
"/graphql",
graphqlUploadExpress({ maxFileSize: 10000000, maxFiles: 10 }),
graphqlHTTP({ schema })
)
.listen(3000);
**What am I doing wrong? **