chore: upd integration tests (#9320)

* extend integration test suite and move to v2
* update workflow
* change tests to run against cli setup
This commit is contained in:
egor-romanov authored and GitHub committed 2022-10-12 14:50:04 +03:00
1 parent 0706fa8719
commit 1895bf701e
18 files changed
+1603 -469

No files matched your search

+16 -16
View File
@@ -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 }}
+10 -9
View File
@@ -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<UserResponse>[] = []
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)
}
+12
View File
@@ -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' } })
})
+319
View File
@@ -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
}
}
+129 -116
View File
@@ -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')
}
}
@@ -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
}
}
+23 -1
View File
@@ -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')
}
}
+98 -17
View File
@@ -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<AuthResponse> {
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<AuthResponse> {
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<UserResponse> {
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 }
}
}
@@ -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()
}
}
+188 -20
View File
@@ -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<Error> {
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")
}
}
+122 -8
View File
@@ -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<Row[]>) {
await Procedures.sql`${body}`
await Procedures.sql`NOTIFY pgrst, 'reload schema';`
}
@step('drop function')
private async dropFunction(signature: PendingQuery<Row[]>) {
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
}
}
+407 -11
View File
@@ -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<Bucket, 'name'>[] = [] 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<void>[] = []
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)
}
}
+4 -3
View File
@@ -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',
}
+61 -59
View File
@@ -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": {
+4 -1
View File
@@ -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",
+3
View File
@@ -0,0 +1,3 @@
# Supabase
.branches
.temp
+71
View File
@@ -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 = ""
+50
View File
@@ -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' );