From 1895bf701e1fccb1f5dda05f97eabd36c3aa74e0 Mon Sep 17 00:00:00 2001 From: egor-romanov <58992960+egor-romanov@users.noreply.github.com> Date: Wed, 12 Oct 2022 14:50:04 +0300 Subject: [PATCH] chore: upd integration tests (#9320) * extend integration test suite and move to v2 * update workflow * change tests to run against cli setup --- .github/workflows/integration-tests.yml | 32 +- tests/.jest/teardown.ts | 19 +- tests/data/func.ts | 12 + tests/features/javascript/authAdmin.spec.ts | 319 +++++++++++++ .../javascript/authentication.spec.ts | 245 +++++----- .../javascript/authenticationAPI.spec.ts | 208 --------- tests/features/javascript/functions.spec.ts | 24 +- tests/features/javascript/hooks.ts | 115 ++++- tests/features/javascript/postgrest.spec.ts | 86 ++++ tests/features/javascript/realtime.spec.ts | 208 ++++++++- tests/features/javascript/rpc.spec.ts | 130 +++++- tests/features/javascript/storage.spec.ts | 418 +++++++++++++++++- tests/features/templates/enums.ts | 7 +- tests/package-lock.json | 120 ++--- tests/package.json | 5 +- tests/supabase/.gitignore | 3 + tests/supabase/config.toml | 71 +++ tests/supabase/migrations/data.sql | 50 +++ 18 files changed, 1603 insertions(+), 469 deletions(-) create mode 100644 tests/data/func.ts create mode 100644 tests/features/javascript/authAdmin.spec.ts delete mode 100644 tests/features/javascript/authenticationAPI.spec.ts create mode 100644 tests/features/javascript/postgrest.spec.ts create mode 100644 tests/supabase/.gitignore create mode 100644 tests/supabase/config.toml create mode 100644 tests/supabase/migrations/data.sql diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 7d798d837a8..0c9a3c46ebd 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -1,15 +1,9 @@ -# This is a basic workflow to help you get started with Actions name: Tests # Controls when the workflow will run on: - # Triggers the workflow on push or pull request events but only for the main branch - push: - branches: [master] - pull_request: - branches: [master] - - # Allows you to run this workflow manually from the Actions tab + schedule: + - cron: '0 4/6 * * *' workflow_dispatch: # A workflow run is made up of one or more jobs that can run sequentially or in parallel @@ -41,17 +35,23 @@ jobs: - name: Install dependencies run: npm ci - - name: Run infrastructure - run: | - cp ../docker/.env.example ../docker/.env - npm run docker:up + - uses: supabase/setup-cli@v1 + - run: supabase start - name: Run Test - run: npm run test + run: npm run test:local + env: + SUPABASE_DB_PORT: 54322 + SUPABASE_DB_PASS: postgres + SUPABASE_DB_HOST: localhost + SUPABASE_GOTRUE: http://localhost:54321 + SUPABASE_URL: http://localhost:54321 + SUPABASE_KEY_ANON: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24ifQ.625_WdcF3KHqz5amU0x2X5WWHP-OEs_4qj0ssLNHzTs + SUPABASE_KEY_ADMIN: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSJ9.vI9obAHOGyVVKa3pD--kJlyxp-Z2zV9UUMAhKpNLAcU - name: Stop infrastructure if: always() - run: npm run docker:down + run: supabase stop - name: Get Allure history uses: actions/checkout@v2 @@ -73,7 +73,7 @@ jobs: keep_reports: 50 - name: Deploy report to Github Pages - if: ${{ !github.event.pull_request.head.repo.fork }} + if: always() uses: peaceiris/actions-gh-pages@v2 env: EXTERNAL_REPOSITORY: supabase/test-reports @@ -82,7 +82,7 @@ jobs: PUBLISH_DIR: allure-history - name: Post the link to the report - if: ${{ !github.event.pull_request.head.repo.fork }} + if: always() uses: Sibz/github-status-action@v1 with: authToken: ${{ secrets.GITHUB_TOKEN }} diff --git a/tests/.jest/teardown.ts b/tests/.jest/teardown.ts index 6ef6f4bc7c6..afb2b1cb2c9 100644 --- a/tests/.jest/teardown.ts +++ b/tests/.jest/teardown.ts @@ -1,20 +1,21 @@ -import { ApiError, createClient, User } from '@supabase/supabase-js' +import { createClient, User, UserResponse } from '@supabase/supabase-js' const removeAllUsers = async () => { - const sb = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY_ADMIN) + const sb = createClient( + process.env.SUPABASE_URL as string, + process.env.SUPABASE_KEY_ADMIN as string + ) - const { data: users } = await sb.auth.api.listUsers() + const { + data: { users }, + } = await sb.auth.admin.listUsers() - const promises: Promise<{ - user: User - data: User - error: ApiError - }>[] = [] + const promises: Promise[] = [] users.map((u) => { sb.from('profiles') .delete() .match({ id: u.id }) - .then(() => promises.push(sb.auth.api.deleteUser(u.id))) + .then(() => promises.push(sb.auth.admin.deleteUser(u.id))) }) await Promise.all(promises) } diff --git a/tests/data/func.ts b/tests/data/func.ts new file mode 100644 index 00000000000..37809150683 --- /dev/null +++ b/tests/data/func.ts @@ -0,0 +1,12 @@ +import { serve } from 'https://deno.land/std@0.131.0/http/server.ts' + +console.log('Hello from Functions!') + +serve(async (req) => { + const { name } = await req.json() + const data = { + message: `Hello ${name}!`, + } + + return new Response(JSON.stringify(data), { headers: { 'Content-Type': 'application/json' } }) +}) diff --git a/tests/features/javascript/authAdmin.spec.ts b/tests/features/javascript/authAdmin.spec.ts new file mode 100644 index 00000000000..57b55cf495f --- /dev/null +++ b/tests/features/javascript/authAdmin.spec.ts @@ -0,0 +1,319 @@ +import { params, retries, suite, test } from '@testdeck/jest' +import { faker } from '@faker-js/faker' +import { Severity } from 'allure-js-commons' + +import { AdminUserAttributes, AuthError, SupabaseClient, UserResponse } from '@supabase/supabase-js' + +import { FEATURE } from '../templates/enums' +import { description, feature, log, severity, step } from '../../.jest/jest-custom-reporter' +import { Hooks } from './hooks' + +@suite('auth_admin') +class AuthenticationAPI extends Hooks { + @feature(FEATURE.AUTH_ADMIN) + @severity(Severity.NORMAL) + @description('When you create user then it has to be in auth db schema') + @test + async 'create user via admin api'() { + const { user, error } = await this.createUserAsAdmin() + expect(error).toBeNull() + expect(user).not.toBeNull() + } + + @feature(FEATURE.AUTH_ADMIN) + @severity(Severity.NORMAL) + @description('When you create user then he can sign in') + @test + async 'user created by admin can login'() { + const { user, error } = await this.createUserAsAdmin() + expect(error).toBeNull() + expect(user).not.toBeNull() + + const supabase = this.createSupaClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY_ADMIN) + + const { + data: { user: createdUser }, + error: getErr, + } = await supabase.auth.signInWithPassword({ + email: user.email, + password: user.password, + }) + expect(getErr).toBeNull() + expect(createdUser).not.toBeNull() + expect(createdUser.id).toBe(user.id) + } + + @feature(FEATURE.AUTH_ADMIN) + @severity(Severity.CRITICAL) + @description('When you try to create user with anon key then you should get error') + @test + async 'admin create user with anon key should fail'() { + const fakeUser = { + email: faker.internet.exampleEmail(), + password: faker.internet.password(), + email_confirm: true, + } + const supabase = this.createSupaClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY_ANON) + + const { + error, + data: { user }, + } = await supabase.auth.admin.createUser(fakeUser) + expect(user).toBeNull() + expect(error).not.toBeNull() + } + + @feature(FEATURE.AUTH_ADMIN) + @severity(Severity.CRITICAL) + @description('When you try to create user as logged in user then you should get error') + @test + async 'admin create user with logged in user should fail'() { + const { user } = await this.createUserAsAdmin() + + const supabase = this.createSupaClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY_ANON) + const { error: signInError } = await supabase.auth.signInWithPassword({ + email: user.email, + password: user.password, + }) + expect(signInError).toBeNull() + + const fakeUser = { + email: faker.internet.exampleEmail(), + password: faker.internet.password(), + } + const { + error, + data: { user: newUser }, + } = await supabase.auth.admin.createUser(fakeUser) + expect(newUser).toBeNull() + expect(error).not.toBeNull() + } + + @feature(FEATURE.AUTH_ADMIN) + @severity(Severity.NORMAL) + @description('When you list users then you should get all users') + @test + async 'list users with service key'() { + const { user: user1 } = await this.createUserAsAdmin() + const { user: user2 } = await this.createUserAsAdmin() + + const supabase = this.createSupaClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY_ADMIN) + const { + data: { users }, + error, + } = await supabase.auth.admin.listUsers() + + expect(error).toBeNull() + expect(users).not.toBeNull() + expect(users.length).toBeGreaterThanOrEqual(2) + expect(users.map((u) => u.id)).toEqual(expect.arrayContaining([user1.id, user2.id])) + } + + @feature(FEATURE.AUTH_ADMIN) + @severity(Severity.CRITICAL) + @description('When you try to list user with anon key then you should get error') + @test + async 'list users with anon key should fail'() { + await this.createUserAsAdmin() + await this.createUserAsAdmin() + + const supabase = this.createSupaClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY_ANON) + const { + data: { users }, + error, + } = await supabase.auth.admin.listUsers() + + expect(error).not.toBeNull() + expect(users).toHaveLength(0) + } + + @feature(FEATURE.AUTH_ADMIN) + @severity(Severity.CRITICAL) + @description('When you try to list user as logged in user then you should get error') + @test + async 'list users as logged in user should fail'() { + const { user } = await this.createUserAsAdmin() + await this.createUserAsAdmin() + + const supabase = this.createSupaClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY_ANON) + const { error: signInError } = await supabase.auth.signInWithPassword({ + email: user.email, + password: user.password, + }) + expect(signInError).toBeNull() + + const { + data: { users }, + error, + } = await supabase.auth.admin.listUsers() + expect(error).not.toBeNull() + expect(users).toHaveLength(0) + } + + @feature(FEATURE.AUTH_ADMIN) + @severity(Severity.NORMAL) + @description('When you get provider url then corresponding auth provider url should be returned') + @params.skip({ provider: 'google', options: {}, expectedURL: 'todo' }) + @params.skip({ + provider: 'google', + options: { redirectTo: 'todo', scopes: 'todo' }, + expectedURL: 'todo', + }) + @params.skip({ provider: 'twitter', options: {}, expectedURL: 'todo' }) + // ... + async 'get url for provider'() { + // todo + } + + @feature(FEATURE.AUTH_ADMIN) + @severity(Severity.NORMAL) + @description('When you get user by id he has to be returned') + @test + async 'get user should work'() { + const { user } = await this.createUserAsAdmin() + + const supabase = this.createSupaClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY_ADMIN) + const { + data: { user: foundUser }, + error, + } = await supabase.auth.admin.getUserById(user.id) + + expect(error).toBeNull() + expect(foundUser).not.toBeNull() + expect(foundUser.id).toBe(user.id) + expect(foundUser.email).toBe(user.email) + } + + @feature(FEATURE.AUTH_ADMIN) + @severity(Severity.NORMAL) + @description('When you update user then this user has to be updated') + @test + async 'update user should work'() { + const { user } = await this.createUserAsAdmin() + + const supabase = this.createSupaClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY_ADMIN) + + const updatedUser = { + email: faker.internet.exampleEmail(), + phone: faker.phone.phoneNumber('!#!##!######'), + } + let { + data: { user: resultUser }, + error, + } = await this.updateWithRetries(supabase, user.id, updatedUser) + + expect(error).toBeNull() + expect(resultUser).not.toBeNull() + expect(resultUser.id).toBe(user.id) + expect(resultUser.email).toBe(updatedUser.email) + expect(resultUser.phone).toBe(updatedUser.phone) + } + + @feature(FEATURE.AUTH_ADMIN) + @severity(Severity.NORMAL) + @description('When you delete user then this user has to be removed') + @test + async 'delete user should work'() { + const { user } = await this.createUserAsAdmin() + + const supabase = this.createSupaClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY_ADMIN) + const { + data: { user: deletedUser }, + error, + } = await supabase.auth.admin.deleteUser(user.id) + expect(error).toBeNull() + + const { + data: { user: foundUser }, + error: getError, + } = await supabase.auth.admin.getUserById(user.id) + expect(getError).not.toBeNull() + expect(foundUser).toBeNull() + } + + @feature(FEATURE.AUTH_ADMIN) + @severity(Severity.CRITICAL) + @description('When you delete user with anon key you have to receive an error') + @test + async 'delete user with anon key should fail'() { + const { user } = await this.createUserAsAdmin() + const { user: villain } = await this.createUserAsAdmin() + + const supabase = this.createSupaClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY_ANON) + await supabase.auth.signInWithPassword({ + email: villain.email, + password: villain.password, + }) + + const { + data: { user: deletedUser }, + error, + } = await supabase.auth.admin.deleteUser(user.id) + + expect(error).not.toBeNull() + expect(deletedUser).toBeNull() + + const sbAdmin = this.createSupaClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY_ADMIN) + const { + data: { user: foundUser }, + error: getError, + } = await sbAdmin.auth.admin.getUserById(user.id) + expect(getError).toBeNull() + expect(foundUser).not.toBeNull() + expect(foundUser.email).toBe(user.email) + } + + @step('Create a user as admin') + async createUserAsAdmin(data: AdminUserAttributes = undefined): Promise<{ + user: { + email: string + password: string + username: string + id: string + } + error: AuthError + }> { + const supabase = this.createSupaClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY_ADMIN) + + let fakeUser: AdminUserAttributes + if (data) { + fakeUser = data + } else { + fakeUser = { + email: faker.internet.exampleEmail(), + password: faker.internet.password(), + email_confirm: true, + } + } + + const { + error, + data: { user }, + } = await supabase.auth.admin.createUser(fakeUser) + + return { + error: error, + user: { + email: user?.email, + password: fakeUser.password, + username: faker.internet.userName(), + id: user?.id, + }, + } + } + + @step('Update user with retries') + async updateWithRetries(supabase: SupabaseClient, uid: string, attributes: AdminUserAttributes) { + let result: UserResponse + for (let i = 1; i < 5; i++) { + result = await supabase.auth.admin.updateUserById(uid, attributes) + + if (result.error && result.error.name === 'AuthRetryableFetchError') { + await new Promise((resolve) => setTimeout(resolve, 1000 * i)) + } else { + break + } + } + return result + } +} diff --git a/tests/features/javascript/authentication.spec.ts b/tests/features/javascript/authentication.spec.ts index 6b19db8b198..c63af999c2c 100644 --- a/tests/features/javascript/authentication.spec.ts +++ b/tests/features/javascript/authentication.spec.ts @@ -1,8 +1,8 @@ -import { suite, test } from '@testdeck/jest' +import { suite, test, timeout } from '@testdeck/jest' import { faker } from '@faker-js/faker' import { Severity } from 'allure-js-commons' -import { ApiError, Session, SupabaseClient, User, UserAttributes } from '@supabase/supabase-js' +import { AuthChangeEvent, Session } from '@supabase/supabase-js' import { FEATURE } from '../templates/enums' import { description, feature, log, severity, step } from '../../.jest/jest-custom-reporter' @@ -22,7 +22,10 @@ class Authentication extends Hooks { password: faker.internet.password(), username: faker.internet.userName(), } - const { user, session, error: signUpError } = await this.signUp(supabase, fakeUser) + const { + data: { user, session }, + error: signUpError, + } = await this.signUp(supabase, fakeUser) expect(signUpError).toBeNull() expect(user).toBeDefined() @@ -37,7 +40,7 @@ class Authentication extends Hooks { @severity(Severity.BLOCKER) @description('When user sign up then he should not be logged in until he confirms his email') @test - async 'new users'() { + async 'sing up new user and sign in'() { // sign up user const supabase = this.createSupaClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY_ANON) @@ -47,8 +50,7 @@ class Authentication extends Hooks { username: faker.internet.userName(), } const { - user, - session: emptySession, + data: { user, session: emptySession }, error: signUpError, } = await this.signUp(supabase, fakeUser) @@ -57,15 +59,22 @@ class Authentication extends Hooks { expect(user.email).toEqual(fakeUser.email.toLowerCase()) expect(emptySession).not.toBeNull() - const { session, error: signInError } = await supabase.auth.signIn({ + const { + data: { session }, + error: signInError, + } = await supabase.auth.signInWithPassword({ email: fakeUser.email, password: fakeUser.password, }) expect(signInError).toBeNull() expect(session).toBeDefined() + await supabase.auth.setSession(session) // check if user is signed in - const { data: profile, error: errorInsert } = await this.insertProfile(supabase, user, fakeUser) + const { + data: [profile], + error: errorInsert, + } = await this.insertProfile(supabase, user, fakeUser) expect(errorInsert).toBeNull() expect(profile.username).toMatch(fakeUser.username) @@ -81,7 +90,7 @@ class Authentication extends Hooks { @severity(Severity.BLOCKER) @description('When user sign up with phone then he should be logged in') @test - async 'new users by phone'() { + async 'create new users by phone auth'() { // sign up user const supabase = this.createSupaClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY_ANON) @@ -90,7 +99,10 @@ class Authentication extends Hooks { username: faker.internet.userName(), phone: faker.phone.phoneNumber('!#!##!######'), } - const { user, session, error: signUpError } = await this.signUpByPhone(supabase, fakeUser) + const { + data: { user, session }, + error: signUpError, + } = await this.signUpByPhone(supabase, fakeUser) expect(signUpError).toBeNull() expect(user).toBeDefined() @@ -98,7 +110,10 @@ class Authentication extends Hooks { expect(session).toBeDefined() // check if user is signed in - const { data: profile, error: errorInsert } = await this.insertProfile(supabase, user, fakeUser) + const { + data: [profile], + error: errorInsert, + } = await this.insertProfile(supabase, user, fakeUser) expect(errorInsert).toBeNull() expect(profile.username).toMatch(fakeUser.username) @@ -114,17 +129,16 @@ class Authentication extends Hooks { @severity(Severity.BLOCKER) @description('When user is already signed up then he should be able to log in') @test - async 'existing users'() { + async 'existing users should be able to login'() { // create user const fakeUser = await this.createUser() // sign in as user const supabase = this.createSupaClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY_ANON) const { - session, - user, + data: { session, user }, error: signInError, - } = await supabase.auth.signIn({ + } = await supabase.auth.signInWithPassword({ email: fakeUser.email, password: fakeUser.password, }) @@ -135,11 +149,10 @@ class Authentication extends Hooks { expect(user.email).toEqual(fakeUser.email.toLowerCase()) // check if user is signed in correctly and rls is working - const { data: profileInserted, error: errorInsert } = await this.insertProfile( - supabase, - user, - fakeUser - ) + const { + data: [profileInserted], + error: errorInsert, + } = await this.insertProfile(supabase, user, fakeUser) expect(errorInsert).toBeNull() expect(profileInserted.username).toMatch(fakeUser.username) @@ -155,7 +168,7 @@ class Authentication extends Hooks { @severity(Severity.NORMAL) @description('When user is signed in then he should be able to get his info and metadata') @test - async 'get user'() { + async 'get user should return logged in user'() { // create user const username = faker.internet.userName() const date = faker.date.recent().toUTCString() @@ -166,16 +179,20 @@ class Authentication extends Hooks { // sign in as user const supabase = this.createSupaClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY_ANON) - await supabase.auth.signIn({ + await supabase.auth.signInWithPassword({ email: fakeUser.email, password: fakeUser.password, }) // get signed in user data - const user = this.getUser(supabase) + const { + data: { user }, + error: getUserErr, + } = await this.getUser(supabase) log('Check if user is signed in correctly and can get his data') - expect(user).toBeDefined() + expect(getUserErr).toBeNull() + expect(user).not.toBeNull() expect(user.email).toEqual(fakeUser.email.toLowerCase()) expect(user.role).toEqual('authenticated') expect(user.aud).toEqual('authenticated') @@ -190,128 +207,124 @@ class Authentication extends Hooks { @severity(Severity.NORMAL) @description('When user is signed in then he should be able update himself in auth schema') @test.skip - async 'update user'() { + async 'update user should update logged in user'() { // create user const fakeUser = await this.createUser() // sign in as user const supabase = this.createSupaClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY_ANON) - await supabase.auth.signIn({ + await supabase.auth.signInWithPassword({ email: fakeUser.email, password: fakeUser.password, }) // get signed in user data - const user = this.getUser(supabase) + const user = await this.getUser(supabase) // update user - // todo update params + const updParams = { + email: faker.internet.email(), + password: faker.internet.password(), + phone: faker.phone.phoneNumber('!#!##!######'), + } const { - user: updUser, - data: updUserData, + data: { user: updUser }, error: updUserError, - } = await this.updateUser(supabase, {}) + } = await this.updateUser(supabase, updParams) + expect(updUserError).toBeNull() + expect(updUser).not.toBeNull() + expect(updUser.email).toEqual(updParams.email.toLowerCase()) + expect(updUser.phone).toEqual(updParams.phone) - // todo check if returned user is updated + // get user and check it was updated + const updatedUser = await this.getUser(supabase) + expect(updatedUser.data.user.email).toEqual(updParams.email.toLowerCase()) + expect(updatedUser.data.user.phone).toEqual(updParams.phone) - const updatedUser = this.getUser(supabase) - - // todo check if user is updated on backend + // sign in with new credentials + await supabase.auth.signOut() + const signIn = await supabase.auth.signInWithPassword({ + email: updParams.email, + password: updParams.password, + }) + expect(signIn.error).toBeNull() } @feature(FEATURE.AUTHENTICATION) @severity(Severity.NORMAL) @description('When user changes session then he still should be correctly logined') - @test.skip - async 'set session'() { - // todo - } + @test + async 'set session should set new auth'() { + // create user + const fakeUser = await this.createUser() - @feature(FEATURE.AUTHENTICATION) - @severity(Severity.NORMAL) - @description('When user changes auth then all new requests should have new JWT') - @test.skip - async 'set auth'() { - // todo - } + // sign in as user + const sb = this.createSupaClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY_ANON) + const { + data: { session }, + } = await sb.auth.signInWithPassword({ + email: fakeUser.email, + password: fakeUser.password, + }) - @feature(FEATURE.AUTHENTICATION) - @severity(Severity.NORMAL) - @description('When user refreshes session then user and session have to be updated') - @test.skip - async 'refresh session'() { - // todo + const supabase = this.createSupaClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY_ANON) + const { error: sessionErr } = await supabase.auth.setSession(session) + expect(sessionErr).toBeNull() + + // check if user is signed in correctly and rls is working + const { data: profileInserted, error: errorInsert } = await this.insertProfile( + supabase, + fakeUser, + fakeUser + ) + expect(errorInsert).toBeNull() + expect(profileInserted).toHaveLength(1) + expect(profileInserted[0].username).toMatch(fakeUser.username) } @feature(FEATURE.AUTHENTICATION) @severity(Severity.NORMAL) @description('When user subscribes on auth changes then user has to receive auth updates') - @test.skip - async 'on auth state changed'() { - // todo - } - - // steps - - @step('I sign up with a valid email and password') - private async signUpByPhone( - supabase: SupabaseClient, - { - phone = faker.phone.phoneNumber(), - password = faker.internet.password(), - }: { - phone?: string - password?: string - } = {}, - options: { - redirectTo?: string - data?: object - } = {} - ): Promise<{ - user: User - session: Session - error: ApiError - }> { - return supabase.auth.signUp( - { - phone: phone, - password: password, - }, - options - ) - } - - @step('Check if I am logged in by checking if I can insert my profile') - private async insertProfile( - supabase: SupabaseClient, - user: User, - fakeUser: { - username: string + @test + async 'on auth state changed should return events'() { + // create user + const fakeUser = await this.createUser() + const events: { event: AuthChangeEvent; token: string }[] = [] + const onAuthStateChanged = (event: AuthChangeEvent, session: Session) => { + log('onAuthStateChanged triggered', event) + events.push({ event, token: session?.access_token }) } - ): Promise<{ data: any; error: any }> { - return supabase - .from('profiles') - .insert({ - id: user.id, - username: fakeUser.username, - }) - .single() - } - @step('Check if I am logged in by checking if I can get my profile') - private async getUserProfile(supabase: SupabaseClient): Promise<{ data: any }> { - return supabase.from('profiles').select().maybeSingle() - } + // create client and subscribe on auth state changes + const supabase = this.createSupaClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY_ANON) + const { + data: { subscription }, + } = supabase.auth.onAuthStateChange(onAuthStateChanged) - @step('Update user info') - private async updateUser( - supabase: SupabaseClient, - attr: UserAttributes - ): Promise<{ - data: User - user: User - error: ApiError - }> { - return supabase.auth.update(attr) + // sign in as user + await supabase.auth.signInWithPassword({ + email: fakeUser.email, + password: fakeUser.password, + }) + + // update user + const updParams = { + email: faker.internet.email(), + password: faker.internet.password(), + phone: faker.phone.phoneNumber('!#!##!######'), + } + const { error: updUserError } = await this.updateUser(supabase, updParams) + expect(updUserError).toBeNull() + + // remove subscription and sign out + subscription.unsubscribe() + await supabase.auth.signOut() + + // check if sign in and update events were triggered and sign out event was not triggered + expect(events).toHaveLength(2) + expect(events.map((e) => e.event)).toEqual( + expect.arrayContaining(['SIGNED_IN', 'USER_UPDATED']) + ) + expect(events.map((e) => e.event)).not.toContain('SIGNED_OUT') } } diff --git a/tests/features/javascript/authenticationAPI.spec.ts b/tests/features/javascript/authenticationAPI.spec.ts deleted file mode 100644 index f330c3ef0ab..00000000000 --- a/tests/features/javascript/authenticationAPI.spec.ts +++ /dev/null @@ -1,208 +0,0 @@ -import { params, suite, test } from '@testdeck/jest' -import { faker } from '@faker-js/faker' -import { Severity } from 'allure-js-commons' - -import { Session, SupabaseClient, User, UserAttributes } from '@supabase/supabase-js' - -import { FEATURE } from '../templates/enums' -import { description, feature, log, severity, step } from '../../.jest/jest-custom-reporter' -import { Hooks } from './hooks' - -@suite('authentication API') -class AuthenticationAPI extends Hooks { - @feature(FEATURE.AUTHENTICATION) - @severity(Severity.NORMAL) - @description('When you create user then it has to be in auth db schema') - @test.skip - async 'create user'() { - // todo - } - - @feature(FEATURE.AUTHENTICATION) - @severity(Severity.NORMAL) - @description('When you create user then he can sign in') - @test.skip - async 'create user can login'() { - // todo - } - - @feature(FEATURE.AUTHENTICATION) - @severity(Severity.CRITICAL) - @description('When you try to create user with anon key then you should get error') - @test.skip - async 'create user with anon key'() { - // todo - } - - @feature(FEATURE.AUTHENTICATION) - @severity(Severity.NORMAL) - @description('When you list users then you should get all users') - @test.skip - async 'list users'() { - // todo - } - - @feature(FEATURE.AUTHENTICATION) - @severity(Severity.CRITICAL) - @description('When you try to list user with anon key then you should get error') - @test.skip - async 'list users with anon key'() { - // todo - } - - @feature(FEATURE.AUTHENTICATION) - @severity(Severity.NORMAL) - @description('When you sign up user with email then he should be able to login') - @test.skip - async 'sign up with email'() { - // todo - } - - @feature(FEATURE.AUTHENTICATION) - @severity(Severity.NORMAL) - @description('When you sign in user with email then he should be able to update his profile') - @test.skip - async 'sign in with email'() { - // todo - } - - @feature(FEATURE.AUTHENTICATION) - @severity(Severity.NORMAL) - @description('When you sign up user with phone then he should be able to login') - @test.skip - async 'sign up with phone'() { - // todo - } - - @feature(FEATURE.AUTHENTICATION) - @severity(Severity.NORMAL) - @description('When you sign in user with phone then he should be able to update his profile') - @test.skip - async 'sign in with phone'() { - // todo - } - - @feature(FEATURE.AUTHENTICATION) - @severity(Severity.NORMAL) - @description('When you send magic link then email should be sent to user') - @test.skip - async 'send magic link'() { - // todo - } - - @feature(FEATURE.AUTHENTICATION) - @severity(Severity.NORMAL) - @description('When you send invite then corresponding email should be sent to user') - @test.skip - async 'send invite link'() { - // todo - } - - @feature(FEATURE.AUTHENTICATION) - @severity(Severity.NORMAL) - @description('When you send reset password then corresponding email should be sent to user') - @test.skip - async 'send reset password'() { - // todo - } - - @feature(FEATURE.AUTHENTICATION) - @severity(Severity.NORMAL) - @description('When you sign out user then current session has to be removed') - @test.skip - async 'sign out'() { - // todo - } - - @feature(FEATURE.AUTHENTICATION) - @severity(Severity.NORMAL) - @description('When you get provider url then corresponding auth provider url should be returned') - @params.skip({ provider: 'google', options: {}, expectedURL: 'todo' }) - @params.skip({ - provider: 'google', - options: { redirectTo: 'todo', scopes: 'todo' }, - expectedURL: 'todo', - }) - @params.skip({ provider: 'twitter', options: {}, expectedURL: 'todo' }) - // ... - async 'get url for provider'() { - // todo - } - - @feature(FEATURE.AUTHENTICATION) - @severity(Severity.NORMAL) - @description('When you get user then currently logined user date has to be returned') - @test.skip - async 'get user'() { - // todo - } - - @feature(FEATURE.AUTHENTICATION) - @severity(Severity.NORMAL) - @description('When you update user then currently logined user date has to be updated') - @test.skip - async 'update user'() { - // todo - } - - @feature(FEATURE.AUTHENTICATION) - @severity(Severity.NORMAL) - @description('When you delete user then this user has to be deleted and unable to login') - @test.skip - async 'delete user'() { - // todo - } - - @feature(FEATURE.AUTHENTICATION) - @severity(Severity.CRITICAL) - @description('When you delete user with anon key you have to receive an error') - @test.skip - async 'delete user with anon key'() { - // todo - } - - @feature(FEATURE.AUTHENTICATION) - @severity(Severity.NORMAL) - @description('When you change auth cookie then all new requests should have new JWT') - @test.skip - async 'set auth cookie'() { - // todo - } - - @feature(FEATURE.AUTHENTICATION) - @severity(Severity.NORMAL) - @description('When you refreshes session then user and session have to be updated') - @test.skip - async 'refresh session'() { - // todo - } - - @feature(FEATURE.AUTHENTICATION) - @severity(Severity.NORMAL) - @description('When you get user by cookie then the corresponding user data has to be returned') - @test.skip - async 'get user by cookie'() { - // todo - } - - @feature(FEATURE.AUTHENTICATION) - @severity(Severity.NORMAL) - @description('When you generate link then the right link has to be returned') - @params.skip({ type: 'signup', options: {}, email: 'todo' }) - @params.skip({ - type: 'signup', - options: { - redirectTo: 'todo', - password: 'todo', - data: { - /* todo */ - }, - }, - email: 'todo', - }) - @params.skip({ type: 'magiclink', options: {}, email: 'todo' }) - // ... - async 'generate link'() { - // todo - } -} diff --git a/tests/features/javascript/functions.spec.ts b/tests/features/javascript/functions.spec.ts index f29dc715641..0aa8fc33e42 100644 --- a/tests/features/javascript/functions.spec.ts +++ b/tests/features/javascript/functions.spec.ts @@ -1,12 +1,15 @@ import { params, suite, test } from '@testdeck/jest' import { faker } from '@faker-js/faker' import { Severity } from 'allure-js-commons' +import { exec, ExecException } from 'child_process' +import os from 'os' import { Session, SupabaseClient, User, UserAttributes } from '@supabase/supabase-js' import { FEATURE } from '../templates/enums' import { description, feature, log, severity, step } from '../../.jest/jest-custom-reporter' import { Hooks } from './hooks' +import path from 'path' @suite('functions') class Functions extends Hooks { @@ -15,6 +18,25 @@ class Functions extends Hooks { @description('When you get functions client then you are able to set auth') @test.skip async 'set auth'() { - // todo + // execute cli command + const deno = path.join(os.homedir(), '.supabase', 'deno') + const funcPath = path.join(process.cwd(), 'data', 'func.ts') + let prom = new Promise<{ error: ExecException; stdout: string; stderr: string }>((resolve) => { + exec(`${deno} bundle --no-check=remote --quiet ${funcPath}`, (error, stdout, stderr) => + resolve({ error, stdout, stderr }) + ) + }) + const { supabase } = await this.createSignedInSupaClient() + const sb = this.createSupaClient(process.env.SUPABASE_URL, process.env.SUPABASE_ANON_KEY) + const { + data: { session }, + } = await supabase.auth.getSession() + + sb.functions.setAuth(session.access_token) + const { error, stdout, stderr } = await prom + expect([null, undefined]).toContain(error) + expect([null, undefined]).toContain(stderr) + expect(stdout).toBeDefined() + sb.functions.invoke('get_user') } } diff --git a/tests/features/javascript/hooks.ts b/tests/features/javascript/hooks.ts index d162d8b688f..25993a4ee6e 100644 --- a/tests/features/javascript/hooks.ts +++ b/tests/features/javascript/hooks.ts @@ -2,12 +2,13 @@ import postgres from 'postgres' import crossFetch from 'cross-fetch' import { faker } from '@faker-js/faker' import { - ApiError, + AuthResponse, createClient, - Session, SupabaseClient, SupabaseClientOptions, User, + UserAttributes, + UserResponse, } from '@supabase/supabase-js' import { JasmineAllureReporter, step } from '../../.jest/jest-custom-reporter' @@ -32,7 +33,14 @@ export abstract class Hooks { } @step('Create Supabase client') - createSupaClient(url: string, key: string, options: SupabaseClientOptions = {}): SupabaseClient { + createSupaClient( + url: string, + key: string, + options: SupabaseClientOptions<'public'> = {} + ): SupabaseClient { + options.auth = options.auth || {} + options.auth.persistSession = false + return createClient(url, key, options) } @@ -41,6 +49,7 @@ export abstract class Hooks { email: string password: string username: string + id: string }> { const supabase = this.createSupaClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY_ANON) @@ -48,11 +57,17 @@ export abstract class Hooks { email: faker.internet.exampleEmail(), password: faker.internet.password(), username: faker.internet.userName(), + id: '', } - const { user, error: signUpError } = await this.signUp(supabase, fakeUser, { + const { + error: signUpError, + data: { user }, + } = await this.signUp(supabase, fakeUser, { data: data, }) expect(signUpError).toBeNull() + expect(user).not.toBeNull() + fakeUser.id = user.id return fakeUser } @@ -92,19 +107,14 @@ export abstract class Hooks { options: { redirectTo?: string data?: object + captchaToken?: string } = {} - ): Promise<{ - user: User - session: Session - error: ApiError - }> { - return supabase.auth.signUp( - { - email: email, - password: password, - }, - options - ) + ): Promise { + return supabase.auth.signUp({ + email: email, + password: password, + options: options, + }) } @step('Check if I am being able to log out') @@ -114,7 +124,7 @@ export abstract class Hooks { @step('Get user data, if there is a logged in user') getUser(supabase: SupabaseClient) { - return supabase.auth.user() + return supabase.auth.getUser() } @step((user: User) => `Get user by ID (${user.id}) from Supabase auth schema`) @@ -127,4 +137,75 @@ export abstract class Hooks { id = ${user.id} ` } + + @step('I sign up with a valid email and password') + async signUpByPhone( + supabase: SupabaseClient, + { + phone = faker.phone.phoneNumber(), + password = faker.internet.password(), + }: { + phone?: string + password?: string + } = {}, + options: { + redirectTo?: string + data?: object + } = {} + ): Promise { + return supabase.auth.signUp({ + phone: phone, + password: password, + options: options, + }) + } + + @step('User inserts profile') + async insertProfile( + supabase: SupabaseClient, + user: { + id: string + }, + fakeUser: { + username: string + } + ): Promise<{ data: any; error: any }> { + return await supabase + .from('profiles') + .insert({ + id: user.id, + username: fakeUser.username, + }) + .select() + } + + @step('I can get my profile via postgREST') + async getUserProfile(supabase: SupabaseClient): Promise<{ data: any; error: any }> { + return supabase.from('profiles').select().maybeSingle() + } + + @step('Update user info') + async updateUser(supabase: SupabaseClient, attr: UserAttributes): Promise { + return supabase.auth.updateUser(attr) + } + + @step('Create signed in supabase client') + async createSignedInSupaClient() { + // create user + const fakeUser = await this.createUser() + + // sign in as user + const supabase = this.createSupaClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY_ANON) + const { + data: { user }, + error: signInError, + } = await supabase.auth.signInWithPassword({ + email: fakeUser.email, + password: fakeUser.password, + }) + expect(signInError).toBeNull() + fakeUser.id = user.id + + return { supabase, user: fakeUser } + } } diff --git a/tests/features/javascript/postgrest.spec.ts b/tests/features/javascript/postgrest.spec.ts new file mode 100644 index 00000000000..d0602ce19ce --- /dev/null +++ b/tests/features/javascript/postgrest.spec.ts @@ -0,0 +1,86 @@ +import { suite, test, timeout } from '@testdeck/jest' +import { faker } from '@faker-js/faker' +import { Severity } from 'allure-js-commons' + +import { FEATURE } from '../templates/enums' +import { description, feature, log, severity, step } from '../../.jest/jest-custom-reporter' +import { Hooks } from './hooks' + +@suite('postgrest') +class PostgREST extends Hooks { + @feature(FEATURE.POSTGREST) + @severity(Severity.BLOCKER) + @description('User can insert profile for himself according to RLS to profiles table') + @test + async 'insert row'() { + const { supabase, user } = await this.createSignedInSupaClient() + + // check if user can insert profile for himself + const { + data: [profileInserted], + error: errorInsert, + } = await this.insertProfile(supabase, user, user) + expect(errorInsert).toBeNull() + expect(profileInserted.username).toMatch(user.username) + } + + @feature(FEATURE.POSTGREST) + @severity(Severity.BLOCKER) + @description('User can select his profile from profiles table') + @test + async 'select row'() { + const { supabase, user } = await this.createSignedInSupaClient() + + // insert profile for user + await this.insertProfile(supabase, user, user) + + const { data: profileGot, error } = await this.getUserProfile(supabase) + expect(error).toBeNull() + expect(profileGot.username).toMatch(user.username) + } + + @feature(FEATURE.POSTGREST) + @severity(Severity.BLOCKER) + @description('User can update his profile in profiles table') + @test + async 'update row'() { + const { supabase, user } = await this.createSignedInSupaClient() + + // insert profile for user + await this.insertProfile(supabase, user, user) + + const newName = faker.internet.userName() + // update profile for user + const { data: updUser, error } = await supabase + .from('profiles') + .update({ + id: user.id, + username: newName, + }) + .select() + expect(error).toBeNull() + expect(updUser.length).toEqual(1) + expect(updUser[0].username).toMatch(newName) + + const { data: profileGot } = await this.getUserProfile(supabase) + expect(profileGot.username).toMatch(newName) + } + + @feature(FEATURE.POSTGREST) + @severity(Severity.BLOCKER) + @description('User can delete his profile in profiles table') + @test + async 'delete row'() { + const { supabase, user } = await this.createSignedInSupaClient() + + // insert profile for user + await this.insertProfile(supabase, user, user) + + // delete profile for user + const { error } = await supabase.from('profiles').delete().eq('id', user.id).select() + expect(error).toBeNull() + + const { data: profileGot } = await this.getUserProfile(supabase) + expect(profileGot).toBeNull() + } +} diff --git a/tests/features/javascript/realtime.spec.ts b/tests/features/javascript/realtime.spec.ts index 53988688ff4..59ba68289bf 100644 --- a/tests/features/javascript/realtime.spec.ts +++ b/tests/features/javascript/realtime.spec.ts @@ -1,29 +1,107 @@ -import { params, suite, test } from '@testdeck/jest' +import { params, suite, test, timeout } from '@testdeck/jest' import { faker } from '@faker-js/faker' import { Severity } from 'allure-js-commons' -import { Session, SupabaseClient, User, UserAttributes } from '@supabase/supabase-js' +import { RealtimeChannel } from '@supabase/supabase-js' import { FEATURE } from '../templates/enums' import { description, feature, log, severity, step } from '../../.jest/jest-custom-reporter' import { Hooks } from './hooks' @suite('realtime') +@timeout(30000) class Realtime extends Hooks { @feature(FEATURE.REALTIME) @severity(Severity.BLOCKER) @description('When you call "on" table then connected realtime client should be returned') - @test.skip - async 'realtime connect'() { - // todo + @test + async '[skip-stage] connect to realtime'() { + const { supabase } = await this.createSignedInSupaClient() + + const channel = supabase + .channel('profiles') + .on('postgres_changes', { event: '*', schema: 'public' }, (payload: any) => + console.log(payload) + ) + channel.subscribe() + + expect(channel).toBeDefined() + const err = await this.waitForChannelJoined(channel) + await new Promise((resolve) => setTimeout(resolve, 1000)) + expect(err).toBeNull() + const ok = await supabase.removeChannel(channel) + expect(ok).toBe('ok') + } + + @feature(FEATURE.REALTIME) + @severity(Severity.BLOCKER) + @description('When you subscrive to realtime, you have to receive updates') + @timeout(60000) + @test + async '[skip-stage] receive event when connected to realtime'() { + let res: any + let t: NodeJS.Timeout + const { supabase, user } = await this.createSignedInSupaClient() + + let payloadReceived = (payload: any) => { + if (payload?.eventType !== 'INSERT') { + return + } + clearTimeout(t) + expect(payload.schema).toBe('public') + expect(payload.table).toBe('profiles') + expect(payload.new.id).toBe(user.id) + expect(payload.new.username).toBe(user.username) + expect(payload.old).toEqual({}) + expect(payload.error).toBeUndefined() + res(null) + } + + const channel = supabase + .channel('profiles') + .on('postgres_changes', { event: '*', schema: 'public' }, payloadReceived) + channel.subscribe() + + expect(channel).toBeDefined() + await this.waitForChannelJoined(channel) + // we should wait some time seconds to connect to database changes + await new Promise((resolve) => setTimeout(resolve, 10000)) + + const eventPromise = new Promise((resolve) => { + res = resolve + new Promise(() => { + t = setTimeout(() => resolve(new Error('timeout')), 30000) + }) + }) + await this.insertProfile(supabase, user, user) + expect(await eventPromise).toBeNull() + + const ok = await supabase.removeChannel(channel) + expect(ok).toBe('ok') } @feature(FEATURE.REALTIME) @severity(Severity.NORMAL) @description('When you call "on" table but not subscribe then no events have to be returned') - @test.skip - async 'no event updates until subscribe'() { - // todo + @test + async 'you should get no events until subscribe'() { + const { supabase, user } = await this.createSignedInSupaClient() + + const channel = supabase + .channel('profiles') + .on('postgres_changes', { event: '*', schema: 'public' }, (payload: any) => { + console.log(payload) + expect('event received').toBe('should not receive event') + }) + + expect(channel).toBeDefined() + await this.insertProfile(supabase, user, user) + + // wait for 1 second to see if we receive any events + await new Promise((resolve) => setTimeout(resolve, 1000)) + expect(channel._isClosed).toBeTruthy() + const ok = await supabase.removeChannel(channel) + expect(ok).toBe('ok') } @feature(FEATURE.REALTIME) @@ -31,9 +109,27 @@ class Realtime extends Hooks { @description( 'When you create 2 subs (1 subscribed and 1 not yet) then both should be returned on get subs' ) - @test.skip - async 'get subscriptions'() { - // todo + @test + async 'get supabase client subscriptions'() { + const { supabase } = await this.createSignedInSupaClient() + + const channel1 = supabase + .channel('profiles') + .on('postgres_changes', { event: '*', schema: 'public' }, (payload: any) => { + console.log(payload) + expect('event received').toBe('should not receive event') + }) + const channel2 = supabase + .channel('profiles') + .on('postgres_changes', { event: '*', schema: 'public' }, (payload: any) => { + console.log(payload) + expect('event received').toBe('should not receive event') + }) + + const channels = supabase.getChannels() + expect(channels).toEqual(expect.arrayContaining([channel1, channel2])) + supabase.removeAllChannels() + expect(supabase.getChannels().length).toEqual(0) } @feature(FEATURE.REALTIME) @@ -125,24 +221,96 @@ class Realtime extends Hooks { @feature(FEATURE.REALTIME) @severity(Severity.CRITICAL) @description('When you unsubscribe from table then no events have to be returned') - @test.skip - async 'unsubscribe from table'() { - // todo + @test + async '[skip-stage] unsubscribe from table'() { + const { supabase, user } = await this.createSignedInSupaClient() + + const channel = supabase + .channel('profiles') + .on('postgres_changes', { event: '*', schema: 'public' }, (payload: any) => { + console.log(payload) + expect('event received').toBe('should not receive event') + }) + channel.subscribe() + // wait for subscription to postgres + await new Promise((resolve) => setTimeout(resolve, 8000)) + const ok = await channel.unsubscribe() + expect(ok).toEqual('ok') + + await this.insertProfile(supabase, user, user) + + // wait for 1 second to see if we receive any events + await new Promise((resolve) => setTimeout(resolve, 1000)) + expect(channel._isClosed).toBeTruthy() + await supabase.removeChannel(channel) } @feature(FEATURE.REALTIME) @severity(Severity.CRITICAL) @description('When you remove one subscription then only events from another have to be returned') - @test.skip - async 'remove one subscription'() { - // todo + @test + async '[skip-stage] remove one subscription from client'() { + const { supabase, user } = await this.createSignedInSupaClient() + + const channel = supabase + .channel('profiles') + .on('postgres_changes', { event: '*', schema: 'public' }, (payload: any) => { + console.log(payload) + expect('event received').toBe('should not receive event') + }) + channel.subscribe() + // wait for subscription to postgres + await new Promise((resolve) => setTimeout(resolve, 8000)) + const ok = await supabase.removeChannel(channel) + expect(ok).toEqual('ok') + + await this.insertProfile(supabase, user, user) + + // wait for 1 second to see if we receive any events + await new Promise((resolve) => setTimeout(resolve, 1000)) + expect(channel._isClosed).toBeTruthy() } @feature(FEATURE.REALTIME) @severity(Severity.CRITICAL) @description('When you remove all subscription then no events have to be returned') - @test.skip - async 'remove all subscriptions'() { - // todo + @test + async '[skip-stage] remove all subscriptions from client'() { + const { supabase, user } = await this.createSignedInSupaClient() + + const channel = supabase + .channel('profiles') + .on('postgres_changes', { event: '*', schema: 'public' }, (payload: any) => { + console.log(payload) + expect('event received').toBe('should not receive event') + }) + channel.subscribe() + // wait for subscription to postgres + await new Promise((resolve) => setTimeout(resolve, 8000)) + const ok = await supabase.removeAllChannels() + expect(ok).toEqual(expect.arrayContaining(['ok'])) + + await this.insertProfile(supabase, user, user) + + // wait for 1 second to see if we receive any events + await new Promise((resolve) => setTimeout(resolve, 1000)) + expect(channel._isClosed).toBeTruthy() + } + + @step('Wait until channel is joined') + async waitForChannelJoined(channel: RealtimeChannel): Promise { + for (let i = 0; i < 30; i++) { + if (channel._isJoined()) { + return null + } + if (channel._isLeaving()) { + return new Error('Channel is leaving') + } + if (channel._isClosed()) { + return new Error('Channel is closed') + } + await new Promise((resolve) => setTimeout(resolve, 100)) + } + return new Error("Channel didn't join in 3 seconds") } } diff --git a/tests/features/javascript/rpc.spec.ts b/tests/features/javascript/rpc.spec.ts index d875ad73678..71883f2dd5c 100644 --- a/tests/features/javascript/rpc.spec.ts +++ b/tests/features/javascript/rpc.spec.ts @@ -1,20 +1,134 @@ -import { params, suite, test } from '@testdeck/jest' -import { faker } from '@faker-js/faker' +import { suite, test, timeout } from '@testdeck/jest' import { Severity } from 'allure-js-commons' -import { Session, SupabaseClient, User, UserAttributes } from '@supabase/supabase-js' +import { SupabaseClient } from '@supabase/supabase-js' import { FEATURE } from '../templates/enums' -import { description, feature, log, severity, step } from '../../.jest/jest-custom-reporter' +import { description, feature, severity, step } from '../../.jest/jest-custom-reporter' import { Hooks } from './hooks' +import { PendingQuery, Row } from 'postgres' @suite('rpc') +@timeout(30000) class Procedures extends Hooks { - @feature(FEATURE.STORAGE) + @feature(FEATURE.RPC) @severity(Severity.BLOCKER) @description('When you call rpc then you are able to receive its result') - @test.skip - async 'call rpc'() { - // todo + @test + async 'call rpc and get result'() { + await this.createFunction(Procedures.sql` + CREATE OR REPLACE FUNCTION public.test_procedure() RETURNS int language plpgsql as $$ + declare + profile_count integer; + begin + select count(*) + into profile_count + from profiles; + + return profile_count; + end; + $$;`) + + const { supabase, user } = await this.createSignedInSupaClient() + await this.insertProfile(supabase, user, user) + + const result = await this.callRpc(supabase, 'test_procedure') + expect(result.error).toBeNull() + expect(result.data).toBeDefined() + expect(result.data).toBeGreaterThanOrEqual(1) + + await this.dropFunction(Procedures.sql`test_procedure()`) + } + + @feature(FEATURE.RPC) + @severity(Severity.BLOCKER) + @description('When you call rpc, params should be passed properly') + @test + async 'call rpc method that has params'() { + await this.createFunction(Procedures.sql` + CREATE OR REPLACE FUNCTION public.test_procedure(filter text) + RETURNS int language plpgsql as $$ + declare + profile_count integer; + begin + select count(*) + into profile_count + from profiles + where username LIKE '%' || filter || '%'; + + return profile_count; + end; + $$;`) + + const { supabase, user } = await this.createSignedInSupaClient() + await this.insertProfile(supabase, user, user) + + const result = await this.callRpc(supabase, 'test_procedure', { filter: user.username }) + expect(result.error).toBeNull() + expect(result.data).toBeDefined() + expect(result.data).toBeGreaterThanOrEqual(1) + + await this.dropFunction(Procedures.sql`test_procedure(filter text)`) + } + + @feature(FEATURE.RPC) + @severity(Severity.NORMAL) + @description('When you call rpc with head param, no data should be returned') + @test + async 'call rpc with head option'() { + await this.createFunction(Procedures.sql` + CREATE OR REPLACE FUNCTION public.test_procedure() RETURNS int language plpgsql as $$ + declare + profile_count integer; + begin + select count(*) + into profile_count + from profiles; + + return profile_count; + end; + $$;`) + + const { supabase, user } = await this.createSignedInSupaClient() + await this.insertProfile(supabase, user, user) + + const result = await this.callRpc(supabase, 'test_procedure', {}, { head: true }) + expect(result.error).toBeNull() + expect(result.data).toBeNull() + + await this.dropFunction(Procedures.sql`test_procedure()`) + } + + @step('create function') + private async createFunction(body: PendingQuery) { + await Procedures.sql`${body}` + await Procedures.sql`NOTIFY pgrst, 'reload schema';` + } + + @step('drop function') + private async dropFunction(signature: PendingQuery) { + await Procedures.sql`DROP FUNCTION public.${signature};` + } + + @step('call supabase rpc') + private async callRpc( + supabase: SupabaseClient, + name: string, + args?: any, + options?: { + head?: boolean + count?: 'exact' | 'planned' | 'estimated' + } + ) { + let result = await supabase.rpc(name, args, options) + for (let i = 1; i <= 5; i++) { + if (result.error) { + await new Promise((resolve) => setTimeout(resolve, 0.5 * 1000 * i)) + result = await supabase.rpc(name, args, options) + } else { + break + } + } + return result } } diff --git a/tests/features/javascript/storage.spec.ts b/tests/features/javascript/storage.spec.ts index 1a60f64adf8..455b1d51c75 100644 --- a/tests/features/javascript/storage.spec.ts +++ b/tests/features/javascript/storage.spec.ts @@ -2,27 +2,423 @@ import { params, suite, test } from '@testdeck/jest' import { faker } from '@faker-js/faker' import { Severity } from 'allure-js-commons' -import { Session, SupabaseClient, User, UserAttributes } from '@supabase/supabase-js' +import { createClient, Session, SupabaseClient, User, UserAttributes } from '@supabase/supabase-js' +import { Bucket } from '@supabase/storage-js' import { FEATURE } from '../templates/enums' import { description, feature, log, severity, step } from '../../.jest/jest-custom-reporter' import { Hooks } from './hooks' +import fetch from 'cross-fetch' @suite('storage') class Storage extends Hooks { - @feature(FEATURE.STORAGE) - @severity(Severity.BLOCKER) - @description('When you create public bucket then it has to be available') - @test.skip - async 'create public bucket'() { - // todo + static buckets: Pick[] = [] as any + static async after() { + const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY_ADMIN) + const requests = [] + for (const bucket of this.buckets) { + requests.push( + new Promise((resolve) => + (async () => { + await supabase.storage.emptyBucket(bucket.name) + await supabase.storage.deleteBucket(bucket.name) + resolve(null) + })() + ) + ) + } + await Promise.all(requests) + await Hooks.after() } @feature(FEATURE.STORAGE) @severity(Severity.BLOCKER) - @description('When you create private bucket then it has to be available') - @test.skip - async 'create private bucket'() { - // todo + @description('When you create public bucket then it has to be available') + @params({ public: true }) + @params({ public: false }) + async 'create bucket'(params: { public: boolean }) { + const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY_ADMIN) + const bucketName = faker.unique(faker.random.word) + const { data: bucket, error } = await supabase.storage.createBucket(bucketName, { + public: params.public, + }) + expect(error).toBeNull() + expect(bucket).toBeDefined() + expect(bucket.name).toBe(bucketName) + Storage.buckets.push(bucket) + + const { data: gotBucket, error: error2 } = await supabase.storage.getBucket(bucketName) + expect(error2).toBeNull() + expect(gotBucket).toBeDefined() + expect(gotBucket.name).toBe(bucketName) + expect(gotBucket.public).toBe(params.public) + } + + @feature(FEATURE.STORAGE) + @severity(Severity.BLOCKER) + @description('There has to be default RLS policy to not allow users to create bucket') + @params({ public: true }) + @params({ public: false }) + async 'user cannot create bucket because of RLS'(params: { public: boolean }) { + const { supabase } = await this.createSignedInSupaClient() + const bucketName = faker.unique(faker.random.word) + + const { data: bucket, error } = await supabase.storage.createBucket(bucketName, { + public: params.public, + }) + expect(error).not.toBeNull() + expect(error.message).toContain('row-level security') + expect(error.message).toContain('"buckets"') + } + + @feature(FEATURE.STORAGE) + @severity(Severity.BLOCKER) + @description('list buckets should return all buckets') + @test + async 'list buckets as admin'() { + const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY_ADMIN) + + const bucket1 = await this.createBucket() + const bucket2 = await this.createBucket() + + const { data: buckets, error } = await supabase.storage.listBuckets() + expect(buckets.length).toBeGreaterThanOrEqual(2) + expect(buckets.map((b) => b.name)).toEqual(expect.arrayContaining([bucket1.name, bucket2.name])) + } + + @feature(FEATURE.STORAGE) + @severity(Severity.BLOCKER) + @description('get bucket should return bucket info') + @test + async 'get bucket'() { + const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY_ADMIN) + const bucket = await this.createBucket() + + const { data: gotBucket, error } = await supabase.storage.getBucket(bucket.id) + expect(error).toBeNull() + expect(gotBucket).toEqual(bucket) + } + + @feature(FEATURE.STORAGE) + @severity(Severity.BLOCKER) + @description('update bucket should change bucket both public->private and back') + @params({ public: true }) + @params({ public: false }) + async 'update bucket'(params: { public: boolean }) { + const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY_ADMIN) + const bucket = await this.createBucket(params.public) + + const { + data: { message }, + error, + } = await supabase.storage.updateBucket(bucket.id, { + public: !params.public, + }) + expect(error).toBeNull() + expect(message).toBe('Successfully updated') + + bucket.public = !params.public + const { data: gotBucket } = await supabase.storage.getBucket(bucket.id) + expect(gotBucket).toEqual(bucket) + } + + @feature(FEATURE.STORAGE) + @severity(Severity.BLOCKER) + @description('get bucket should return bucket info') + @test + async 'delete bucket'() { + const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY_ADMIN) + const bucket = await this.createBucket() + + const { + data: { message }, + error, + } = await supabase.storage.deleteBucket(bucket.id) + expect(error).toBeNull() + expect(message).toBe('Successfully deleted') + + const { data: buckets } = await supabase.storage.listBuckets() + expect( + buckets.map((b) => { + return { name: b.name, id: b.id } + }) + ).not.toContain({ name: bucket.name, id: bucket.id }) + + Storage.buckets = Storage.buckets.filter((b) => b.name != bucket.name) + } + + @feature(FEATURE.STORAGE) + @severity(Severity.BLOCKER) + @description('upload to bucket') + @test + async 'upload to bucket'() { + const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY_ADMIN) + const bucket = await this.createBucket() + + const file = { + path: faker.random.word() + '.txt', + data: faker.lorem.paragraph(), + } + const { + data: { path }, + error, + } = await supabase.storage.from(bucket.name).upload(file.path, file.data) + expect(error).toBeNull() + expect(path).toEqual(file.path) + } + + @feature(FEATURE.STORAGE) + @severity(Severity.BLOCKER) + @description('list files in bucket') + @test + async 'list files'() { + const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY_ADMIN) + const bucket = await this.createBucket() + + const files: { path: string; data: string }[] = [] + const requests: Promise[] = [] + for (let i = 0; i < 3; i++) { + requests.push( + new Promise((resolve) => { + const dir = faker.random.word() + const file = { + path: dir + '/' + faker.random.word() + '.txt', + data: faker.lorem.sentence(), + } + files.push(file) + const p1 = supabase.storage.from(bucket.name).upload(file.path, file.data) + const file2 = { + path: dir + '/' + faker.random.word() + '.txt', + data: faker.lorem.sentence(), + } + files.push(file2) + const p2 = supabase.storage.from(bucket.name).upload(file2.path, file2.data) + Promise.all([p1, p2]).finally(() => resolve()) + }) + ) + } + await Promise.all(requests) + + const { data: dirs, error: error1 } = await supabase.storage.from(bucket.name).list() + expect(error1).toBeNull() + expect(dirs.map((d) => d.name)).toEqual( + expect.arrayContaining([...new Set(files.map((f) => f.path.split('/')[0]))]) + ) + + const dir = files[0].path.split('/')[0] + const { data: filesInDir, error: error2 } = await supabase.storage.from(bucket.name).list(dir) + expect(error2).toBeNull() + expect(filesInDir.map((d) => d.name)).toEqual( + expect.arrayContaining( + files.filter((f) => f.path.split('/')[0] === dir).map((f) => f.path.split('/')[1]) + ) + ) + } + + @feature(FEATURE.STORAGE) + @severity(Severity.BLOCKER) + @description('download file') + @test + async 'download file from bucket'() { + const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY_ADMIN) + const bucket = await this.createBucket() + + const file = { + path: faker.random.word() + '/' + faker.random.word() + '.txt', + data: faker.lorem.paragraph(), + } + await supabase.storage.from(bucket.name).upload(file.path, file.data) + + const { data, error } = await supabase.storage.from(bucket.name).download(file.path) + expect(error).toBeNull() + expect(await data.text()).toEqual(file.data) + } + + @feature(FEATURE.STORAGE) + @severity(Severity.BLOCKER) + @description('move file') + @test + async '[skip-local] move file in bucket'() { + const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY_ADMIN) + const bucket = await this.createBucket() + + const file = { + path: faker.random.word() + '/' + faker.random.word() + '.txt', + newPath: faker.random.word() + '/' + faker.random.word() + '.txt', + data: faker.lorem.paragraph(), + } + await supabase.storage.from(bucket.name).upload(file.path, file.data) + + const { error } = await supabase.storage.from(bucket.name).move(file.path, file.newPath) + expect(error).toBeNull() + const { data: filesRoot } = await supabase.storage.from(bucket.name).list() + expect(filesRoot.map((f) => f.name)).not.toContain(file.path.split('/')[0]) + const { data: files } = await supabase.storage + .from(bucket.name) + .list(file.newPath.split('/')[0]) + expect(files.map((f) => f.name)).toEqual(expect.arrayContaining([file.newPath.split('/')[1]])) + } + + @feature(FEATURE.STORAGE) + @severity(Severity.BLOCKER) + @description('copy file') + @test + async '[skip-local] copy file in bucket'() { + const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY_ADMIN) + const bucket = await this.createBucket() + + const file = { + path: faker.random.word() + '/' + faker.random.word() + '.txt', + newPath: faker.random.word() + '/' + faker.random.word() + '.txt', + data: faker.lorem.paragraph(), + } + await supabase.storage.from(bucket.name).upload(file.path, file.data) + + const { error } = await supabase.storage.from(bucket.name).copy(file.path, file.newPath) + expect(error).toBeNull() + + const { data: filesRoot } = await supabase.storage.from(bucket.name).list() + expect(filesRoot.map((f) => f.name)).toEqual( + expect.arrayContaining([file.path.split('/')[0], file.newPath.split('/')[0]]) + ) + const { data: filesNew } = await supabase.storage + .from(bucket.name) + .list(file.newPath.split('/')[0]) + expect(filesNew.map((f) => f.name)).toEqual( + expect.arrayContaining([file.newPath.split('/')[1]]) + ) + const { data: files } = await supabase.storage.from(bucket.name).list(file.path.split('/')[0]) + expect(files.map((f) => f.name)).toEqual(expect.arrayContaining([file.path.split('/')[1]])) + } + + @feature(FEATURE.STORAGE) + @severity(Severity.BLOCKER) + @description('get public link to file in the public bucket') + @test + async 'get public link to file'() { + const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY_ADMIN) + const bucket = await this.createBucket() + + const file = { + path: faker.random.word() + '/' + faker.random.word() + '.txt', + data: faker.lorem.paragraph(), + } + await supabase.storage.from(bucket.name).upload(file.path, file.data) + + const { + data: { publicUrl }, + } = supabase.storage.from(bucket.name).getPublicUrl(file.path) + expect(publicUrl).toBeDefined() + expect(publicUrl.length).toBeGreaterThan(1) + expect(publicUrl).toMatch(new RegExp(`^http[s]?://.*storage.*/${bucket.name}/.*`)) + + const resp = await fetch(publicUrl) + expect(resp.status).toEqual(200) + expect(await resp.text()).toEqual(file.data) + } + + @feature(FEATURE.STORAGE) + @severity(Severity.BLOCKER) + @description('get public link to file in the private bucket') + @test + async 'get public link to private file'() { + const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY_ADMIN) + const bucket = await this.createBucket(false) + + const file = { + path: faker.random.word() + '/' + faker.random.word() + '.txt', + data: faker.lorem.paragraph(), + } + await supabase.storage.from(bucket.name).upload(file.path, file.data) + + const { + data: { publicUrl }, + } = supabase.storage.from(bucket.name).getPublicUrl(file.path) + expect(publicUrl).toBeDefined() + expect(publicUrl.length).toBeGreaterThan(1) + expect(publicUrl).toMatch(new RegExp(`^http[s]?://.*storage.*/${bucket.name}/.*`)) + + const resp = await fetch(publicUrl) + expect(resp.status).toEqual(400) // todo 400 should be 404 too I guess + const error = await resp.json() + expect(error.statusCode).toBe('404') // todo should be a number? + } + + @feature(FEATURE.STORAGE) + @severity(Severity.BLOCKER) + @description('get signed link to file in the bucket') + @params({ public: true }) + @params({ public: false }) + async 'get signed link to file'(params: { public: boolean }) { + const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY_ADMIN) + const bucket = await this.createBucket(params.public) + + const file = { + path: faker.random.word() + '/' + faker.random.word() + '.txt', + data: faker.lorem.paragraph(), + } + await supabase.storage.from(bucket.name).upload(file.path, file.data) + + const { + data: { signedUrl }, + error, + } = await supabase.storage.from(bucket.name).createSignedUrl(file.path, 10000) + expect(error).toBeNull() + expect(signedUrl).toBeDefined() + expect(signedUrl.length).toBeGreaterThan(1) + expect(signedUrl).toMatch(new RegExp(`^http[s]?://.*storage.*/${bucket.name}/.*`)) + + const resp = await fetch(signedUrl) + expect(resp.status).toEqual(200) + expect(await resp.text()).toEqual(file.data) + } + + @feature(FEATURE.STORAGE) + @severity(Severity.BLOCKER) + @description('update file check if it will change') + @test + async 'update file'() { + const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY_ADMIN) + const bucket = await this.createBucket() + + const file = { + path: faker.random.word() + '/' + faker.random.word() + '/' + faker.random.word() + '.txt', + data: faker.lorem.paragraph(), + } + await supabase.storage.from(bucket.name).upload(file.path, file.data) + + const { + data: { publicUrl }, + } = supabase.storage.from(bucket.name).getPublicUrl(file.path) + expect(publicUrl).toBeDefined() + expect(publicUrl.length).toBeGreaterThan(1) + expect(publicUrl).toMatch(new RegExp(`^http[s]?://.*storage.*/${bucket.name}/.*`)) + + const newFile = { + path: file.path, + data: faker.lorem.paragraph(10), + } + await supabase.storage.from(bucket.name).update(newFile.path, newFile.data) + + const resp = await fetch(publicUrl) + expect(resp.status).toEqual(200) + expect(await resp.text()).toEqual(newFile.data) + } + + @step('create bucket') + async createBucket(pub = true) { + const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY_ADMIN) + const bucketName = faker.unique(faker.random.word) + + const { data: bucket, error } = await supabase.storage.createBucket(bucketName, { + public: pub, + }) + expect(error).toBeNull() + expect(bucket).toBeDefined() + expect(bucket.name).toBe(bucketName) + Storage.buckets.push(bucket) + + const { data: buckets } = await supabase.storage.listBuckets() + return buckets.find((b) => b.name === bucketName) } } diff --git a/tests/features/templates/enums.ts b/tests/features/templates/enums.ts index 3ce59684e85..fed746cf37a 100644 --- a/tests/features/templates/enums.ts +++ b/tests/features/templates/enums.ts @@ -1,10 +1,11 @@ export enum FEATURE { AUTHENTICATION = 'authentication', + AUTH_ADMIN = 'auth_admin', REALTIME = 'realtime', STORAGE = 'storage', FUNCTIONS = 'functions', RPC = 'rpc', - DB = 'database', - Filters = 'database.filters', - Modifiers = 'database.modifiers', + POSTGREST = 'postgrest', + FILTERS = 'postgrest_filters', + MODIFIERS = 'postgrest_modifiers', } diff --git a/tests/package-lock.json b/tests/package-lock.json index e0dfc319b5b..16b0e91e8a7 100644 --- a/tests/package-lock.json +++ b/tests/package-lock.json @@ -10,7 +10,7 @@ "license": "MIT", "devDependencies": { "@faker-js/faker": "^6.1.2", - "@supabase/supabase-js": "^1.35.6", + "@supabase/supabase-js": "^2.0.0", "@testdeck/jest": "^0.2.0", "@types/jest": "^27.4.0", "@types/node": "^17.0.24", @@ -1023,36 +1023,36 @@ } }, "node_modules/@supabase/functions-js": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-1.3.4.tgz", - "integrity": "sha512-yYVgkECjv7IZEBKBI3EB5Q7R1p0FJ10g8Q9N7SWKIHUU6i6DnbEGHIMFLyQRm1hmiNWD8fL7bRVEYacmTRJhHw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.0.0.tgz", + "integrity": "sha512-ozb7bds2yvf5k7NM2ZzUkxvsx4S4i2eRKFSJetdTADV91T65g4gCzEs9L3LUXSrghcGIkUaon03VPzOrFredqg==", "dev": true, "dependencies": { "cross-fetch": "^3.1.5" } }, "node_modules/@supabase/gotrue-js": { - "version": "1.22.22", - "resolved": "https://registry.npmjs.org/@supabase/gotrue-js/-/gotrue-js-1.22.22.tgz", - "integrity": "sha512-exbCopLo4tLawKZR25wEdipm+IRVujuqFRR4h2wiXd11YA+N8rfgg/4S4L6BTFHmzV+KMjy0kDF3yMbsjIR/xA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@supabase/gotrue-js/-/gotrue-js-2.0.1.tgz", + "integrity": "sha512-PCzHeFhz33lPWLZT+UiVDpGkZQI3K1wwhkhgAjvJdkyCbwK5u+LWWUzz0AX66bGGLpg1IYQIP7oz8V68XVjzMA==", "dev": true, "dependencies": { - "cross-fetch": "^3.0.6" + "cross-fetch": "^3.1.5" } }, "node_modules/@supabase/postgrest-js": { - "version": "0.37.4", - "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-0.37.4.tgz", - "integrity": "sha512-x+c2rk1fz9s6f1PrGxCJ0QTUgXPDI0G3ngIqD5sSiXhhCyfl8Q5V92mXl2EYtlDhkiUkjFNrOZFhXVbXOHgvDw==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-1.0.0.tgz", + "integrity": "sha512-MRR4g8dThgSnlnjkL2Jvc6gS73W68hLq48/Q245CLvUPt3KtOh3WXpvo4rZDEDHYVkyVga+L9IQixo6YTsjMQg==", "dev": true, "dependencies": { "cross-fetch": "^3.1.5" } }, "node_modules/@supabase/realtime-js": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-1.7.4.tgz", - "integrity": "sha512-h/Jk3PCLkIVonsNavof/LvHbvF41UD+D+mWcA3m8yHzJ2TLbV3S4XDYId+A3AkvFOAork7Ns/9O8rK0uY4F4zw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.0.0.tgz", + "integrity": "sha512-DJVt5Z76pik3Fxt1es4fmquq6GrwR3nLqcb2Q6VpHXSudtixML0Cyxsu0DPzxa6ruSQrO9s08s0OQgwaIA1C0Q==", "dev": true, "dependencies": { "@types/phoenix": "^1.5.4", @@ -1060,25 +1060,26 @@ } }, "node_modules/@supabase/storage-js": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-1.7.3.tgz", - "integrity": "sha512-jnIZWqOc9TGclOozgX9v/RWGFCgJAyW/yvmauexgRZhWknUXoA4b2i8tj7vfwE0WTvNRuA5JpXID98rfJeSG7Q==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.0.0.tgz", + "integrity": "sha512-7kXThdRt/xqnOOvZZxBqNkeX1CFNUWc0hYBJtNN/Uvt8ok9hD14foYmroWrHn046wEYFqUrB9U35JYsfTrvltA==", "dev": true, "dependencies": { - "cross-fetch": "^3.1.0" + "cross-fetch": "^3.1.5" } }, "node_modules/@supabase/supabase-js": { - "version": "1.35.6", - "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-1.35.6.tgz", - "integrity": "sha512-KDRXRr+kdGwruIUizZPALbe5YccMYFVyZJf1sFWKLncaLYSFiM6iKFnqCrNeQ4JFoZZiICkiTl1FUuai62jVpg==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.0.0.tgz", + "integrity": "sha512-8nNMXYzBlQVSDONFaNgyyxsAdUY+C9Q/7vEDYDdTFTyC3i+luXTDOSjHMLcjIky/+x5rIAcNQqqIJ48HJUluXA==", "dev": true, "dependencies": { - "@supabase/functions-js": "^1.3.4", - "@supabase/gotrue-js": "^1.22.21", - "@supabase/postgrest-js": "^0.37.4", - "@supabase/realtime-js": "^1.7.4", - "@supabase/storage-js": "^1.7.2" + "@supabase/functions-js": "^2.0.0", + "@supabase/gotrue-js": "^2.0.1", + "@supabase/postgrest-js": "^1.0.0", + "@supabase/realtime-js": "^2.0.0", + "@supabase/storage-js": "^2.0.0", + "cross-fetch": "^3.1.5" } }, "node_modules/@testdeck/core": { @@ -2652,12 +2653,12 @@ } }, "node_modules/ext": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/ext/-/ext-1.6.0.tgz", - "integrity": "sha512-sdBImtzkq2HpkdRLtlLWDa6w4DX22ijZLKx8BMPUuKe1c5lbN6xwQDQCxSfxBQnHZ13ls/FH0MQZx/q/gr6FQg==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz", + "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==", "dev": true, "dependencies": { - "type": "^2.5.0" + "type": "^2.7.2" } }, "node_modules/ext/node_modules/type": { @@ -6349,36 +6350,36 @@ } }, "@supabase/functions-js": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-1.3.4.tgz", - "integrity": "sha512-yYVgkECjv7IZEBKBI3EB5Q7R1p0FJ10g8Q9N7SWKIHUU6i6DnbEGHIMFLyQRm1hmiNWD8fL7bRVEYacmTRJhHw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.0.0.tgz", + "integrity": "sha512-ozb7bds2yvf5k7NM2ZzUkxvsx4S4i2eRKFSJetdTADV91T65g4gCzEs9L3LUXSrghcGIkUaon03VPzOrFredqg==", "dev": true, "requires": { "cross-fetch": "^3.1.5" } }, "@supabase/gotrue-js": { - "version": "1.22.22", - "resolved": "https://registry.npmjs.org/@supabase/gotrue-js/-/gotrue-js-1.22.22.tgz", - "integrity": "sha512-exbCopLo4tLawKZR25wEdipm+IRVujuqFRR4h2wiXd11YA+N8rfgg/4S4L6BTFHmzV+KMjy0kDF3yMbsjIR/xA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@supabase/gotrue-js/-/gotrue-js-2.0.1.tgz", + "integrity": "sha512-PCzHeFhz33lPWLZT+UiVDpGkZQI3K1wwhkhgAjvJdkyCbwK5u+LWWUzz0AX66bGGLpg1IYQIP7oz8V68XVjzMA==", "dev": true, "requires": { - "cross-fetch": "^3.0.6" + "cross-fetch": "^3.1.5" } }, "@supabase/postgrest-js": { - "version": "0.37.4", - "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-0.37.4.tgz", - "integrity": "sha512-x+c2rk1fz9s6f1PrGxCJ0QTUgXPDI0G3ngIqD5sSiXhhCyfl8Q5V92mXl2EYtlDhkiUkjFNrOZFhXVbXOHgvDw==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-1.0.0.tgz", + "integrity": "sha512-MRR4g8dThgSnlnjkL2Jvc6gS73W68hLq48/Q245CLvUPt3KtOh3WXpvo4rZDEDHYVkyVga+L9IQixo6YTsjMQg==", "dev": true, "requires": { "cross-fetch": "^3.1.5" } }, "@supabase/realtime-js": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-1.7.4.tgz", - "integrity": "sha512-h/Jk3PCLkIVonsNavof/LvHbvF41UD+D+mWcA3m8yHzJ2TLbV3S4XDYId+A3AkvFOAork7Ns/9O8rK0uY4F4zw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.0.0.tgz", + "integrity": "sha512-DJVt5Z76pik3Fxt1es4fmquq6GrwR3nLqcb2Q6VpHXSudtixML0Cyxsu0DPzxa6ruSQrO9s08s0OQgwaIA1C0Q==", "dev": true, "requires": { "@types/phoenix": "^1.5.4", @@ -6386,25 +6387,26 @@ } }, "@supabase/storage-js": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-1.7.3.tgz", - "integrity": "sha512-jnIZWqOc9TGclOozgX9v/RWGFCgJAyW/yvmauexgRZhWknUXoA4b2i8tj7vfwE0WTvNRuA5JpXID98rfJeSG7Q==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.0.0.tgz", + "integrity": "sha512-7kXThdRt/xqnOOvZZxBqNkeX1CFNUWc0hYBJtNN/Uvt8ok9hD14foYmroWrHn046wEYFqUrB9U35JYsfTrvltA==", "dev": true, "requires": { - "cross-fetch": "^3.1.0" + "cross-fetch": "^3.1.5" } }, "@supabase/supabase-js": { - "version": "1.35.6", - "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-1.35.6.tgz", - "integrity": "sha512-KDRXRr+kdGwruIUizZPALbe5YccMYFVyZJf1sFWKLncaLYSFiM6iKFnqCrNeQ4JFoZZiICkiTl1FUuai62jVpg==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.0.0.tgz", + "integrity": "sha512-8nNMXYzBlQVSDONFaNgyyxsAdUY+C9Q/7vEDYDdTFTyC3i+luXTDOSjHMLcjIky/+x5rIAcNQqqIJ48HJUluXA==", "dev": true, "requires": { - "@supabase/functions-js": "^1.3.4", - "@supabase/gotrue-js": "^1.22.21", - "@supabase/postgrest-js": "^0.37.4", - "@supabase/realtime-js": "^1.7.4", - "@supabase/storage-js": "^1.7.2" + "@supabase/functions-js": "^2.0.0", + "@supabase/gotrue-js": "^2.0.1", + "@supabase/postgrest-js": "^1.0.0", + "@supabase/realtime-js": "^2.0.0", + "@supabase/storage-js": "^2.0.0", + "cross-fetch": "^3.1.5" } }, "@testdeck/core": { @@ -7602,12 +7604,12 @@ } }, "ext": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/ext/-/ext-1.6.0.tgz", - "integrity": "sha512-sdBImtzkq2HpkdRLtlLWDa6w4DX22ijZLKx8BMPUuKe1c5lbN6xwQDQCxSfxBQnHZ13ls/FH0MQZx/q/gr6FQg==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz", + "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==", "dev": true, "requires": { - "type": "^2.5.0" + "type": "^2.7.2" }, "dependencies": { "type": { diff --git a/tests/package.json b/tests/package.json index a85bd06ff62..1633462bdbc 100644 --- a/tests/package.json +++ b/tests/package.json @@ -6,6 +6,9 @@ "docker:up": "cd ../docker && ENABLE_EMAIL_AUTOCONFIRM=true docker-compose --env-file ./.env -f docker-compose.yml -f dev/docker-compose.dev.yml up -d", "docker:down": "cd ../docker && docker-compose --env-file ./.env -f docker-compose.yml -f dev/docker-compose.dev.yml down", "test": "jest", + "test:local": "jest --testNamePattern '^((?!\\[skip-local\\]).)*$' --testPathPattern '^((?!realtime).)*$' --detectOpenHandles", + "test:stage": "jest --testNamePattern '^((?!\\[skip-stage\\]).)*$' --detectOpenHandles", + "test:prod": "jest --detectOpenHandles", "allure:generate": "rm -rf allure-report && node_modules/allure-commandline/bin/allure generate", "allure:serve": "node_modules/allure-commandline/bin/allure serve", "test:report": "npm run allure:generate && npm run allure:serve", @@ -15,7 +18,7 @@ "license": "MIT", "devDependencies": { "@faker-js/faker": "^6.1.2", - "@supabase/supabase-js": "^1.35.6", + "@supabase/supabase-js": "^2.0.0", "@testdeck/jest": "^0.2.0", "@types/jest": "^27.4.0", "@types/node": "^17.0.24", diff --git a/tests/supabase/.gitignore b/tests/supabase/.gitignore new file mode 100644 index 00000000000..773c7c3e0a1 --- /dev/null +++ b/tests/supabase/.gitignore @@ -0,0 +1,3 @@ +# Supabase +.branches +.temp diff --git a/tests/supabase/config.toml b/tests/supabase/config.toml new file mode 100644 index 00000000000..2d4498ba196 --- /dev/null +++ b/tests/supabase/config.toml @@ -0,0 +1,71 @@ +# A string used to distinguish different Supabase projects on the same host. Defaults to the working +# directory name when running `supabase init`. +project_id = "tests" + +[api] +# Port to use for the API URL. +port = 54321 +# Schemas to expose in your API. Tables, views and stored procedures in this schema will get API +# endpoints. public and storage are always included. +schemas = [] +# Extra schemas to add to the search_path of every request. +extra_search_path = ["extensions"] +# The maximum number of rows returns from a view, table, or stored procedure. Limits payload size +# for accidental or malicious requests. +max_rows = 1000 + +[db] +# Port to use for the local database URL. +port = 54322 +# The database major version to use. This has to be the same as your remote database's. Run `SHOW +# server_version;` on the remote database to check. +major_version = 14 + +[studio] +# Port to use for Supabase Studio. +port = 54323 + +# Email testing server. Emails sent with the local dev setup are not actually sent - rather, they +# are monitored, and you can view the emails that would have been sent from the web interface. +[inbucket] +# Port to use for the email testing server web interface. +port = 54324 +smtp_port = 54325 +pop3_port = 54326 + +[auth] +# The base URL of your website. Used as an allow-list for redirects and for constructing URLs used +# in emails. +site_url = "http://localhost:3000" +# A list of *exact* URLs that auth providers are permitted to redirect to post authentication. +additional_redirect_urls = ["https://localhost:3000"] +# How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 seconds (one +# week). +jwt_expiry = 3600 +# Allow/disallow new user signups to your project. +enable_signup = true + +[auth.email] +# Allow/disallow new user signups via email to your project. +enable_signup = true +# If enabled, a user will be required to confirm any email change on both the old, and new email +# addresses. If disabled, only the new email is required to confirm. +double_confirm_changes = true +# If enabled, users need to confirm their email address before signing in. +enable_confirmations = false + +[auth.phone] +# Allow/disallow new user signups via phone to your project. +enable_signup = true +double_confirm_changes = true +enable_confirmations = false + +# Use an external OAuth provider. The full list of providers are: `apple`, `azure`, `bitbucket`, +# `discord`, `facebook`, `github`, `gitlab`, `google`, `twitch`, `twitter`, `slack`, `spotify`. +[auth.external.apple] +enabled = false +client_id = "" +secret = "" +# Overrides the default auth provider URL. Used to support self-hosted gitlab, single-tenant Azure, +# or any other third-party OIDC providers. +url = "" diff --git a/tests/supabase/migrations/data.sql b/tests/supabase/migrations/data.sql new file mode 100644 index 00000000000..88b84570933 --- /dev/null +++ b/tests/supabase/migrations/data.sql @@ -0,0 +1,50 @@ +create table if not exists profiles ( + id uuid references auth.users not null, + updated_at timestamp with time zone, + username text unique, + avatar_url text, + website text, + + primary key (id), + unique(username), + constraint username_length check (char_length(username) >= 3) +); + +alter table profiles enable row level security; + +create policy "Public profiles are viewable by the owner." + on profiles for select + using ( auth.uid() = id ); + +create policy "Users can insert their own profile." + on profiles for insert + with check ( auth.uid() = id ); + +create policy "Users can update own profile." + on profiles for update + using ( auth.uid() = id ); + +create policy "Users can delete own profile." + on profiles for delete + using ( auth.uid() = id ); + +-- Set up Realtime +drop publication if exists supabase_realtime; +create publication supabase_realtime; +alter publication supabase_realtime add table profiles; + +-- Set up Storage +insert into storage.buckets (id, name) +values ('avatars', 'avatars') on conflict do nothing; + +create policy "Avatar images are publicly accessible." + on storage.objects for select + using ( bucket_id = 'avatars' ); + +create policy "Anyone can upload an avatar." + on storage.objects for insert + with check ( bucket_id = 'avatars' ); + +create policy "Anyone can update an avatar." + on storage.objects for update + with check ( bucket_id = 'avatars' );