mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
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:
1 parent
0706fa8719
commit
1895bf701e
18 files changed
+1603
-469
No files matched your search
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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' } })
|
||||
})
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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')
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
}
|
||||
Generated
+61
-59
@@ -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
@@ -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",
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
# Supabase
|
||||
.branches
|
||||
.temp
|
||||
@@ -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 = ""
|
||||
@@ -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' );
|
||||
Reference in new issue
Block a user