chore: Delete the tests package (#32527)
* Remove the tests package. * Remove all workspace configs for the tests package.
This commit is contained in:
@@ -22,10 +22,6 @@
|
||||
"typecheck": "turbo --continue typecheck",
|
||||
"test:prettier": "prettier -c '{apps,packages}/**/*.{js,jsx,ts,tsx,css,md,mdx,json}'",
|
||||
"format": "prettier --write '{apps,packages}/**/*.{js,jsx,ts,tsx,css,md,mdx,json}'",
|
||||
"docker:dev": "cd docker && docker compose -f docker-compose.yml -f ./dev/docker-compose.dev.yml up --renew-anon-volumes",
|
||||
"docker:up": "cd docker && docker compose up",
|
||||
"docker:down": "cd docker && docker compose -f docker-compose.yml -f ./dev/docker-compose.dev.yml down --remove-orphans",
|
||||
"docker:remove": "cd docker && docker compose -f docker-compose.yml -f ./dev/docker-compose.dev.yml rm -vfs",
|
||||
"test:docs": "turbo run test --filter=docs",
|
||||
"test:ui": "turbo run test --filter=ui",
|
||||
"test:ui-patterns": "turbo run test --filter=ui-patterns",
|
||||
|
||||
720
pnpm-lock.yaml
generated
720
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,6 @@
|
||||
packages:
|
||||
- "apps/*"
|
||||
- "packages/*"
|
||||
- "tests"
|
||||
- "playwright-tests"
|
||||
|
||||
catalog:
|
||||
|
||||
@@ -1,194 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/ban-types */
|
||||
import { AllureReporterApi, jasmine_, registerAllureReporter } from 'jest-allure2-adapter'
|
||||
import { ContentType, Severity } from 'allure-js-commons'
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
type TestDecorator = (
|
||||
target: object,
|
||||
property: string,
|
||||
descriptor: PropertyDescriptor
|
||||
) => PropertyDescriptor
|
||||
export class JasmineAllureReporter implements jasmine_.CustomReporter {
|
||||
allure: AllureReporterApi
|
||||
|
||||
constructor(allure: AllureReporterApi) {
|
||||
this.allure = allure
|
||||
}
|
||||
|
||||
suiteStarted(suite?: jasmine_.CustomReporterResult): void {
|
||||
this.allure.startGroup(suite.description)
|
||||
// some actions here on suite started
|
||||
}
|
||||
|
||||
suiteDone(): void {
|
||||
// some actions here on suite end
|
||||
this.allure.endGroup()
|
||||
}
|
||||
|
||||
specStarted(spec: jasmine_.CustomReporterResult): void {
|
||||
this.allure.startTest(spec)
|
||||
// some actions here on test started
|
||||
}
|
||||
|
||||
specDone(spec: jasmine_.CustomReporterResult): void {
|
||||
// some actions here on spec end
|
||||
this.allure.endTest(spec)
|
||||
}
|
||||
}
|
||||
|
||||
registerAllureReporter(undefined, (allure) => new JasmineAllureReporter(allure))
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace NodeJS {
|
||||
interface Global {
|
||||
reporter: AllureReporterApi
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getAllure(): AllureReporterApi {
|
||||
// @ts-ignore - we are checking if reporter is defined
|
||||
if (!global.reporter) {
|
||||
throw new Error('Unable to find Allure implementation')
|
||||
}
|
||||
// @ts-ignore - we know that reporter is an AllureReporterApi
|
||||
return global.reporter
|
||||
}
|
||||
|
||||
export function step<T>(nameFn: string | ((arg: T) => string)): TestDecorator {
|
||||
return (target: object, propertyKey: string, descriptor: PropertyDescriptor) => {
|
||||
const original: object = descriptor.value
|
||||
let callable: (args: T) => void = () => {
|
||||
/* */
|
||||
}
|
||||
|
||||
if (typeof original === 'function') {
|
||||
descriptor.value = function (...args: [T]) {
|
||||
try {
|
||||
const value: string = typeof nameFn === 'function' ? nameFn.apply(this, args) : nameFn
|
||||
callable = () => getAllure().step(value, () => original.apply(this, args))
|
||||
// tslint:disable-next-line:no-console
|
||||
console.info(`Step: ${value || nameFn}`)
|
||||
} catch (e) {
|
||||
// tslint:disable-next-line:no-console
|
||||
console.error(`[ERROR] Failed to apply decorator: ${e}`)
|
||||
}
|
||||
return callable.apply(this, args)
|
||||
}
|
||||
}
|
||||
return descriptor
|
||||
}
|
||||
}
|
||||
|
||||
export function attachment<T>(name: string, type: ContentType) {
|
||||
return (
|
||||
_target: object,
|
||||
_propertyKey: string,
|
||||
descriptor: PropertyDescriptor
|
||||
): PropertyDescriptor => {
|
||||
const original: object = descriptor.value
|
||||
let callable: (args: T) => void = () => {
|
||||
/* */
|
||||
}
|
||||
|
||||
if (typeof original === 'function') {
|
||||
descriptor.value = async function (...args: [T]) {
|
||||
try {
|
||||
const content: Buffer | string = await original.apply(this, args)
|
||||
callable = () =>
|
||||
getAllure().step(name, () => {
|
||||
getAllure().attachment(type.toString(), content, type)
|
||||
})
|
||||
} catch (e) {
|
||||
// tslint:disable-next-line:no-console
|
||||
console.error(`[ERROR] Failed to apply decorator: ${e}`)
|
||||
}
|
||||
return callable.apply(this, args)
|
||||
}
|
||||
}
|
||||
return descriptor
|
||||
}
|
||||
}
|
||||
|
||||
export function attach(name: string, content: string | Buffer, type: ContentType): void {
|
||||
getAllure().step(name, () => {
|
||||
getAllure().attachment(type.toString(), content, type)
|
||||
})
|
||||
}
|
||||
|
||||
export function log(name: string, description?: string): void {
|
||||
console.info(description ? `${name}: ${description}` : name)
|
||||
getAllure().step(name, () => {
|
||||
if (description) {
|
||||
getAllure().step(description, () => {
|
||||
/* */
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function feature<T>(featureFn: string | ((arg: T) => string)): TestDecorator {
|
||||
return processDecorator(featureFn, (name) => getAllure().feature(name))
|
||||
}
|
||||
|
||||
export function story<T>(storyFn: string | ((arg: T) => string)): TestDecorator {
|
||||
return processDecorator(storyFn, (name) => getAllure().story(name))
|
||||
}
|
||||
|
||||
export function severity<T>(
|
||||
severityFn: Severity | string | ((arg: T) => string | Severity)
|
||||
): TestDecorator {
|
||||
return processDecorator(severityFn, (name: Severity) => getAllure().severity(name))
|
||||
}
|
||||
|
||||
export function tag<T>(tagFn: string | ((arg: T) => string)): TestDecorator {
|
||||
return processDecorator(tagFn, (name) => getAllure().tag(name))
|
||||
}
|
||||
|
||||
export function owner<T>(ownerFn: string | ((arg: T) => string)): TestDecorator {
|
||||
return processDecorator(ownerFn, (name) => getAllure().owner(name))
|
||||
}
|
||||
|
||||
export function description<T>(descriptionFn: string | ((arg: T) => string)): TestDecorator {
|
||||
return processDecorator(descriptionFn, (text) => getAllure().description(text))
|
||||
}
|
||||
|
||||
function processDecorator<T>(
|
||||
parameterFn: string | ((arg: T) => string),
|
||||
reporterFn: (arg: string) => void
|
||||
): TestDecorator {
|
||||
return (target: object, property: string, descriptor: PropertyDescriptor) => {
|
||||
return processDescriptor(parameterFn, reporterFn, descriptor)
|
||||
}
|
||||
}
|
||||
|
||||
function processDescriptor<T>(
|
||||
parameterFn: string | ((arg: T) => string),
|
||||
reporterFn: (arg: string) => void,
|
||||
descriptor: PropertyDescriptor
|
||||
): PropertyDescriptor {
|
||||
const original: object = descriptor.value
|
||||
if (typeof original === 'function') {
|
||||
descriptor.value = function (...args: [T]) {
|
||||
try {
|
||||
const value: string =
|
||||
typeof parameterFn === 'function' ? parameterFn.apply(this, args) : parameterFn
|
||||
reporterFn(value)
|
||||
} catch (e) {
|
||||
// tslint:disable-next-line:no-console
|
||||
console.error(`[ERROR] Failed to apply decorator: ${e}`)
|
||||
}
|
||||
return original.apply(this, args)
|
||||
}
|
||||
}
|
||||
|
||||
for (const prop of Object.keys(original)) {
|
||||
if (original.hasOwnProperty(prop) && prop.startsWith('__testdeck_')) {
|
||||
// @ts-ignore - we know that prop exists
|
||||
descriptor.value[prop] = original[prop]
|
||||
}
|
||||
}
|
||||
|
||||
return descriptor
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
export default () => {
|
||||
process.env.SUPABASE_DB_HOST = process.env.SUPABASE_DB_HOST ?? 'localhost'
|
||||
process.env.SUPABASE_DB_PORT = process.env.SUPABASE_DB_PORT ?? '5432'
|
||||
process.env.SUPABASE_DB_PASS =
|
||||
process.env.SUPABASE_DB_PASS ?? 'your-super-secret-and-long-postgres-password'
|
||||
process.env.SUPABASE_GOTRUE = process.env.SUPABASE_GOTRUE ?? 'http://localhost:8000/auth/v1'
|
||||
process.env.SUPABASE_URL = process.env.SUPABASE_URL ?? 'http://localhost:8000'
|
||||
process.env.SUPABASE_KEY_ANON =
|
||||
process.env.SUPABASE_KEY_ANON ??
|
||||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE'
|
||||
process.env.SUPABASE_KEY_ADMIN =
|
||||
process.env.SUPABASE_KEY_ADMIN ??
|
||||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q'
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
import { createClient, User, UserResponse } from '@supabase/supabase-js'
|
||||
|
||||
const removeAllUsers = async () => {
|
||||
const sb = createClient(
|
||||
process.env.SUPABASE_URL as string,
|
||||
process.env.SUPABASE_KEY_ADMIN as string
|
||||
)
|
||||
|
||||
const {
|
||||
data: { users },
|
||||
} = await sb.auth.admin.listUsers()
|
||||
|
||||
const promises: Promise<UserResponse>[] = []
|
||||
users.map((u) => {
|
||||
sb.from('profiles')
|
||||
.delete()
|
||||
.match({ id: u.id })
|
||||
.then(() => promises.push(sb.auth.admin.deleteUser(u.id)))
|
||||
})
|
||||
await Promise.all(promises)
|
||||
}
|
||||
|
||||
export default async () => {
|
||||
await removeAllUsers()
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
api
|
||||
node_modules
|
||||
package-lock.json
|
||||
docker*
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"trailingComma": "es5",
|
||||
"tabWidth": 2,
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"printWidth": 100
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
## Tests
|
||||
|
||||
These tests can be run with Docker.
|
||||
|
||||
We can use either [allure-commandline](https://github.com/allure-framework/allure2) as it is open source and <https://github.com/marketplace/actions/allure-report-with-history> as an github action to host reports with history on GitHub Pages.
|
||||
|
||||
Or use their paid version if we want a bit more groove and possibility to manage test cases not only as code but also manually: <https://qameta.io>. It can be self-hosted or cloud. Not sure that we need it now, but we can always migrate seamlessly to it if we would need it.
|
||||
|
||||
### Steps
|
||||
|
||||
In the parent folder:
|
||||
|
||||
- `npm run docker:dev`
|
||||
- `npm run test`
|
||||
|
||||
### Clean up
|
||||
|
||||
- `npm run docker:remove`
|
||||
@@ -1,12 +0,0 @@
|
||||
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' } })
|
||||
})
|
||||
@@ -1,319 +0,0 @@
|
||||
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,330 +0,0 @@
|
||||
import { suite, test, timeout } from '@testdeck/jest'
|
||||
import { faker } from '@faker-js/faker'
|
||||
import { Severity } from 'allure-js-commons'
|
||||
|
||||
import { AuthChangeEvent, Session } 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')
|
||||
class Authentication extends Hooks {
|
||||
@feature(FEATURE.AUTHENTICATION)
|
||||
@severity(Severity.BLOCKER)
|
||||
@description('When user sign up then corresponding user in auth schema should be created')
|
||||
@test
|
||||
async 'signup should create user'() {
|
||||
const supabase = this.createSupaClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY_ANON)
|
||||
|
||||
const fakeUser = {
|
||||
email: faker.internet.exampleEmail(),
|
||||
password: faker.internet.password(),
|
||||
username: faker.internet.userName(),
|
||||
}
|
||||
const {
|
||||
data: { user, session },
|
||||
error: signUpError,
|
||||
} = await this.signUp(supabase, fakeUser)
|
||||
|
||||
expect(signUpError).toBeNull()
|
||||
expect(user).toBeDefined()
|
||||
expect(user.email).toEqual(fakeUser.email.toLowerCase())
|
||||
expect(session).toBeDefined()
|
||||
|
||||
const [createdUser] = await this.selectUser(user)
|
||||
expect(createdUser.email).toEqual(fakeUser.email.toLowerCase())
|
||||
}
|
||||
|
||||
@feature(FEATURE.AUTHENTICATION)
|
||||
@severity(Severity.BLOCKER)
|
||||
@description('When user sign up then he should not be logged in until he confirms his email')
|
||||
@test
|
||||
async 'sing up new user and sign in'() {
|
||||
// sign up user
|
||||
const supabase = this.createSupaClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY_ANON)
|
||||
|
||||
const fakeUser = {
|
||||
email: faker.internet.exampleEmail(),
|
||||
password: faker.internet.password(),
|
||||
username: faker.internet.userName(),
|
||||
}
|
||||
const {
|
||||
data: { user, session: emptySession },
|
||||
error: signUpError,
|
||||
} = await this.signUp(supabase, fakeUser)
|
||||
|
||||
expect(signUpError).toBeNull()
|
||||
expect(user).toBeDefined()
|
||||
expect(user.email).toEqual(fakeUser.email.toLowerCase())
|
||||
expect(emptySession).not.toBeNull()
|
||||
|
||||
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)
|
||||
expect(errorInsert).toBeNull()
|
||||
expect(profile.username).toMatch(fakeUser.username)
|
||||
|
||||
const { data: profileGot } = await this.getUserProfile(supabase)
|
||||
expect(profileGot.username).toMatch(profile.username)
|
||||
|
||||
// check if user is able to sign out
|
||||
const { error } = await this.signOut(supabase)
|
||||
expect(error).toBeNull()
|
||||
}
|
||||
|
||||
@feature(FEATURE.AUTHENTICATION)
|
||||
@severity(Severity.BLOCKER)
|
||||
@description('When user sign up with phone then he should be logged in')
|
||||
@test
|
||||
async 'create new users by phone auth'() {
|
||||
// sign up user
|
||||
const supabase = this.createSupaClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY_ANON)
|
||||
|
||||
const fakeUser = {
|
||||
password: faker.internet.password(),
|
||||
username: faker.internet.userName(),
|
||||
phone: faker.phone.phoneNumber('!#!##!######'),
|
||||
}
|
||||
const {
|
||||
data: { user, session },
|
||||
error: signUpError,
|
||||
} = await this.signUpByPhone(supabase, fakeUser)
|
||||
|
||||
expect(signUpError).toBeNull()
|
||||
expect(user).toBeDefined()
|
||||
expect(user.phone).toEqual(fakeUser.phone)
|
||||
expect(session).toBeDefined()
|
||||
|
||||
// check if user is signed in
|
||||
const {
|
||||
data: [profile],
|
||||
error: errorInsert,
|
||||
} = await this.insertProfile(supabase, user, fakeUser)
|
||||
expect(errorInsert).toBeNull()
|
||||
expect(profile.username).toMatch(fakeUser.username)
|
||||
|
||||
const { data: profileGot } = await this.getUserProfile(supabase)
|
||||
expect(profileGot.username).toMatch(profile.username)
|
||||
|
||||
// check if user is able to sign out
|
||||
const { error } = await this.signOut(supabase)
|
||||
expect(error).toBeNull()
|
||||
}
|
||||
|
||||
@feature(FEATURE.AUTHENTICATION)
|
||||
@severity(Severity.BLOCKER)
|
||||
@description('When user is already signed up then he should be able to log in')
|
||||
@test
|
||||
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 {
|
||||
data: { session, user },
|
||||
error: signInError,
|
||||
} = await supabase.auth.signInWithPassword({
|
||||
email: fakeUser.email,
|
||||
password: fakeUser.password,
|
||||
})
|
||||
|
||||
expect(signInError).toBeNull()
|
||||
expect(session).toBeDefined()
|
||||
expect(user).toBeDefined()
|
||||
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)
|
||||
expect(errorInsert).toBeNull()
|
||||
expect(profileInserted.username).toMatch(fakeUser.username)
|
||||
|
||||
const { data: profileGot } = await this.getUserProfile(supabase)
|
||||
expect(profileGot.username).toMatch(profileInserted.username)
|
||||
|
||||
// check if user is able to sign out
|
||||
const { error } = await this.signOut(supabase)
|
||||
expect(error).toBeNull()
|
||||
}
|
||||
|
||||
@feature(FEATURE.AUTHENTICATION)
|
||||
@severity(Severity.NORMAL)
|
||||
@description('When user is signed in then he should be able to get his info and metadata')
|
||||
@test
|
||||
async 'get user should return logged in user'() {
|
||||
// create user
|
||||
const username = faker.internet.userName()
|
||||
const date = faker.date.recent().toUTCString()
|
||||
const fakeUser = await this.createUser({
|
||||
username: username,
|
||||
date: date,
|
||||
})
|
||||
|
||||
// sign in as user
|
||||
const supabase = this.createSupaClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY_ANON)
|
||||
await supabase.auth.signInWithPassword({
|
||||
email: fakeUser.email,
|
||||
password: fakeUser.password,
|
||||
})
|
||||
|
||||
// get signed in user data
|
||||
const {
|
||||
data: { user },
|
||||
error: getUserErr,
|
||||
} = await this.getUser(supabase)
|
||||
|
||||
log('Check if user is signed in correctly and can get his data')
|
||||
expect(getUserErr).toBeNull()
|
||||
expect(user).not.toBeNull()
|
||||
expect(user.email).toEqual(fakeUser.email.toLowerCase())
|
||||
expect(user.role).toEqual('authenticated')
|
||||
expect(user.aud).toEqual('authenticated')
|
||||
// verify if metadata is correctly set after sing up
|
||||
expect(user.user_metadata).toEqual({
|
||||
username: username,
|
||||
date: date,
|
||||
})
|
||||
}
|
||||
|
||||
@feature(FEATURE.AUTHENTICATION)
|
||||
@severity(Severity.NORMAL)
|
||||
@description('When user is signed in then he should be able update himself in auth schema')
|
||||
@test.skip
|
||||
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.signInWithPassword({
|
||||
email: fakeUser.email,
|
||||
password: fakeUser.password,
|
||||
})
|
||||
|
||||
// get signed in user data
|
||||
const user = await this.getUser(supabase)
|
||||
|
||||
// update user
|
||||
const updParams = {
|
||||
email: faker.internet.email(),
|
||||
password: faker.internet.password(),
|
||||
phone: faker.phone.phoneNumber('!#!##!######'),
|
||||
}
|
||||
const {
|
||||
data: { user: updUser },
|
||||
error: updUserError,
|
||||
} = 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)
|
||||
|
||||
// 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)
|
||||
|
||||
// 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
|
||||
async 'set session should set new auth'() {
|
||||
// create user
|
||||
const fakeUser = await this.createUser()
|
||||
|
||||
// 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,
|
||||
})
|
||||
|
||||
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
|
||||
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 })
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
||||
// 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,42 +0,0 @@
|
||||
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 {
|
||||
@feature(FEATURE.STORAGE)
|
||||
@severity(Severity.NORMAL)
|
||||
@description('When you get functions client then you are able to set auth')
|
||||
@test.skip
|
||||
async 'set auth'() {
|
||||
// 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')
|
||||
}
|
||||
}
|
||||
@@ -1,211 +0,0 @@
|
||||
import postgres from 'postgres'
|
||||
import crossFetch from 'cross-fetch'
|
||||
import { faker } from '@faker-js/faker'
|
||||
import {
|
||||
AuthResponse,
|
||||
createClient,
|
||||
SupabaseClient,
|
||||
SupabaseClientOptions,
|
||||
User,
|
||||
UserAttributes,
|
||||
UserResponse,
|
||||
} from '@supabase/supabase-js'
|
||||
|
||||
import { JasmineAllureReporter, step } from '../../.jest/jest-custom-reporter'
|
||||
|
||||
export abstract class Hooks {
|
||||
static sql = postgres({
|
||||
host: process.env.SUPABASE_DB_HOST,
|
||||
port: parseInt(process.env.SUPABASE_DB_PORT),
|
||||
database: 'postgres',
|
||||
username: 'postgres',
|
||||
password: process.env.SUPABASE_DB_PASS,
|
||||
})
|
||||
|
||||
@step('terminate sql connection')
|
||||
static async after(): Promise<any> {
|
||||
try {
|
||||
Hooks.sql.end({ timeout: 100 })
|
||||
return Promise.resolve(null)
|
||||
} catch (err) {
|
||||
return Promise.reject(err)
|
||||
}
|
||||
}
|
||||
|
||||
@step('Create Supabase client')
|
||||
createSupaClient(
|
||||
url: string,
|
||||
key: string,
|
||||
options: SupabaseClientOptions<'public'> = {}
|
||||
): SupabaseClient {
|
||||
options.auth = options.auth || {}
|
||||
options.auth.persistSession = false
|
||||
|
||||
return createClient(url, key, options)
|
||||
}
|
||||
|
||||
@step('Create a valid user')
|
||||
async createUser(data: object = {}): Promise<{
|
||||
email: string
|
||||
password: string
|
||||
username: string
|
||||
id: string
|
||||
}> {
|
||||
const supabase = this.createSupaClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY_ANON)
|
||||
|
||||
const fakeUser = {
|
||||
email: faker.internet.exampleEmail(),
|
||||
password: faker.internet.password(),
|
||||
username: faker.internet.userName(),
|
||||
id: '',
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
// todo: rework this
|
||||
@step((token: string) => `verify with token ${token}`)
|
||||
async verify(token: string, email: string): Promise<Response> {
|
||||
return crossFetch(`${process.env.SUPABASE_GOTRUE}/verify`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
type: 'signup',
|
||||
token: token,
|
||||
email: email,
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
@step((user: User) => `get confirmation token for user ${user.id}`)
|
||||
async getConfirmationToken(user: User): Promise<[{ confirmation_token: any }]> {
|
||||
return Hooks.sql`
|
||||
select confirmation_token
|
||||
from auth.users
|
||||
where id = ${user.id}
|
||||
`
|
||||
}
|
||||
|
||||
@step('I sign up with a valid email and password')
|
||||
async signUp(
|
||||
supabase: SupabaseClient,
|
||||
{
|
||||
email = faker.internet.exampleEmail(),
|
||||
password = faker.internet.password(),
|
||||
}: {
|
||||
email?: string
|
||||
password?: string
|
||||
} = {},
|
||||
options: {
|
||||
redirectTo?: string
|
||||
data?: object
|
||||
captchaToken?: string
|
||||
} = {}
|
||||
): Promise<AuthResponse> {
|
||||
return supabase.auth.signUp({
|
||||
email: email,
|
||||
password: password,
|
||||
options: options,
|
||||
})
|
||||
}
|
||||
|
||||
@step('Check if I am being able to log out')
|
||||
async signOut(supabase: SupabaseClient): Promise<{ error: any }> {
|
||||
return supabase.auth.signOut()
|
||||
}
|
||||
|
||||
@step('Get user data, if there is a logged in user')
|
||||
getUser(supabase: SupabaseClient) {
|
||||
return supabase.auth.getUser()
|
||||
}
|
||||
|
||||
@step((user: User) => `Get user by ID (${user.id}) from Supabase auth schema`)
|
||||
async selectUser(user: User): Promise<[{ email: string }]> {
|
||||
return Hooks.sql`
|
||||
select
|
||||
email
|
||||
from auth.users
|
||||
where
|
||||
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 }
|
||||
}
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
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,316 +0,0 @@
|
||||
import { params, suite, test, timeout } from '@testdeck/jest'
|
||||
import { faker } from '@faker-js/faker'
|
||||
import { Severity } from 'allure-js-commons'
|
||||
|
||||
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
|
||||
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
|
||||
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)
|
||||
@severity(Severity.BLOCKER)
|
||||
@description(
|
||||
'When you create 2 subs (1 subscribed and 1 not yet) then both should be returned on get subs'
|
||||
)
|
||||
@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)
|
||||
@severity(Severity.NORMAL)
|
||||
@description('When you subscribe on table then corresponding events have to be returned')
|
||||
@params.skip({ event: 'INSERT', returnedTypes: ['insert'] })
|
||||
@params.skip({ event: 'UPDATE', returnedTypes: ['update'] })
|
||||
@params.skip({ event: 'DELETE', returnedTypes: ['delete'] })
|
||||
@params.skip({ event: '*', returnedTypes: ['insert', 'update', 'delete'] })
|
||||
async 'subscribe on table'() {
|
||||
// todo
|
||||
}
|
||||
|
||||
@feature(FEATURE.REALTIME)
|
||||
@severity(Severity.NORMAL)
|
||||
@description(
|
||||
'When you create multiple subscriptions then corresponding events have to be returned'
|
||||
)
|
||||
@params.skip({
|
||||
events: [
|
||||
{ event: 'INSERT', table: 'rt_table1' },
|
||||
{ event: '*', table: 'rt_table1' },
|
||||
],
|
||||
returnedIds: [[], []],
|
||||
})
|
||||
@params.skip({
|
||||
events: [
|
||||
{ event: 'UPDATE', table: 'rt_table1' },
|
||||
{ event: 'DELETE', table: 'rt_table1' },
|
||||
],
|
||||
returnedIds: [[], []],
|
||||
})
|
||||
@params.skip({
|
||||
events: [
|
||||
{ event: 'INSERT', table: 'rt_table1' },
|
||||
{ event: 'INSERT', table: 'rt_table2' },
|
||||
],
|
||||
returnedIds: [[], []],
|
||||
})
|
||||
async 'multiple subscriptions'() {
|
||||
// todo
|
||||
}
|
||||
|
||||
@feature(FEATURE.REALTIME)
|
||||
@severity(Severity.NORMAL)
|
||||
@description('When you subscribe on table with RLS then corresponding events have to be returned')
|
||||
@params.skip({ event: 'INSERT', returnedTypes: ['insert'] })
|
||||
@params.skip({ event: 'UPDATE', returnedTypes: ['update'] })
|
||||
@params.skip({ event: 'DELETE', returnedTypes: ['delete'] })
|
||||
@params.skip({ event: '*', returnedTypes: ['insert', 'update', 'delete'] })
|
||||
async 'subscribe on table with RLS'() {
|
||||
// todo
|
||||
}
|
||||
|
||||
@feature(FEATURE.REALTIME)
|
||||
@severity(Severity.CRITICAL)
|
||||
@description('When you subscribe on table with RLS then only allowed events have to be returned')
|
||||
@params.skip({ event: 'INSERT', returnedIds: [] })
|
||||
@params.skip({ event: 'UPDATE', returnedIds: [] })
|
||||
@params.skip({ event: 'DELETE', returnedIds: [] })
|
||||
@params.skip({ event: '*', returnedIds: [] })
|
||||
async 'subscribe on table with RLS policies'() {
|
||||
// todo
|
||||
}
|
||||
|
||||
@feature(FEATURE.REALTIME)
|
||||
@severity(Severity.CRITICAL)
|
||||
@description(
|
||||
'When you subscribe on table with 2 clients then they have to receive different events'
|
||||
)
|
||||
@test.skip
|
||||
async 'subscribe from 2 clients'() {
|
||||
// todo
|
||||
}
|
||||
|
||||
@feature(FEATURE.REALTIME)
|
||||
@severity(Severity.NORMAL)
|
||||
@description(
|
||||
'When you subscribe on table with RLS policies then service key subscription has to bypass RLS'
|
||||
)
|
||||
@params.skip({ event: 'INSERT', returnedTypes: ['insert'] })
|
||||
@params.skip({ event: 'UPDATE', returnedTypes: ['update'] })
|
||||
@params.skip({ event: 'DELETE', returnedTypes: ['delete'] })
|
||||
@params.skip({ event: '*', returnedTypes: ['insert', 'update', 'delete'] })
|
||||
async 'subscribe on RLS table with service key'() {
|
||||
// todo
|
||||
}
|
||||
|
||||
@feature(FEATURE.REALTIME)
|
||||
@severity(Severity.CRITICAL)
|
||||
@description('When you unsubscribe from table then no events have to be returned')
|
||||
@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
|
||||
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
|
||||
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,134 +0,0 @@
|
||||
import { suite, test, timeout } from '@testdeck/jest'
|
||||
import { Severity } from 'allure-js-commons'
|
||||
|
||||
import { SupabaseClient } from '@supabase/supabase-js'
|
||||
|
||||
import { FEATURE } from '../templates/enums'
|
||||
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.RPC)
|
||||
@severity(Severity.BLOCKER)
|
||||
@description('When you call rpc then you are able to receive its result')
|
||||
@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
|
||||
}
|
||||
}
|
||||
@@ -1,424 +0,0 @@
|
||||
import { params, suite, test } from '@testdeck/jest'
|
||||
import { faker } from '@faker-js/faker'
|
||||
import { Severity } from 'allure-js-commons'
|
||||
|
||||
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 {
|
||||
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 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,44 +0,0 @@
|
||||
import allure
|
||||
|
||||
@allure.severity(allure.severity_level.BLOCKER)
|
||||
@allure.suite("authentication")
|
||||
@allure.feature("authentication")
|
||||
class TestAuthentication(object):
|
||||
@allure.description("""When user sign up then he should be logged in""")
|
||||
def test_new_users(self):
|
||||
self.create_supabase_anonymous_client()
|
||||
self.sign_up_valid("email", "password")
|
||||
self.check_logged_in()
|
||||
self.check_log_out()
|
||||
|
||||
def test_existing_users(self):
|
||||
""" When user is already signed up then he should be able to logged in """
|
||||
self.create_valid_user()
|
||||
self.sign_in_valid("email", "password")
|
||||
self.check_logged_in()
|
||||
self.check_log_out()
|
||||
|
||||
@allure.step("Create Supabase anonymous client")
|
||||
def create_supabase_anonymous_client(self):
|
||||
pass
|
||||
|
||||
@allure.step("I sign up with a valid {email} and {password}")
|
||||
def sign_up_valid(self, email, password="password"):
|
||||
pass
|
||||
|
||||
@allure.step("Create a valid user")
|
||||
def create_valid_user(self):
|
||||
pass
|
||||
|
||||
@allure.step("I sign in with a valid {email} and {password}")
|
||||
def sign_in_valid(self, email, password="password"):
|
||||
pass
|
||||
|
||||
@allure.step("Check if I am logged in")
|
||||
def check_logged_in(self):
|
||||
pass
|
||||
|
||||
@allure.step("Check if I am being able to log out")
|
||||
def check_log_out(self):
|
||||
pass
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
import { Severity } from 'allure-js-commons'
|
||||
|
||||
import { FEATURE } from './enums'
|
||||
|
||||
describe('authentication', () => {
|
||||
beforeEach(() => {
|
||||
reporter.feature(FEATURE.AUTHENTICATION)
|
||||
reporter.severity(Severity.BLOCKER)
|
||||
})
|
||||
|
||||
test('New users', () => {
|
||||
reporter.description('When user sign up then he should be logged in')
|
||||
|
||||
reporter.step('Create Supabase anonymous client', () => {
|
||||
/* don't modify! */
|
||||
})
|
||||
|
||||
reporter.step('I sign up with a valid email and password', () => {
|
||||
/* don't modify! */
|
||||
})
|
||||
|
||||
reporter.step('Check if I am logged in', () => {
|
||||
/* don't modify! */
|
||||
})
|
||||
|
||||
reporter.step('Check if I am being able to log out', () => {
|
||||
/* don't modify! */
|
||||
})
|
||||
})
|
||||
|
||||
test('Existing users', () => {
|
||||
reporter.description('When user is already signed up then he should be able to logged in')
|
||||
|
||||
reporter.step('Create a valid user', () => {
|
||||
/* don't modify! */
|
||||
})
|
||||
|
||||
reporter.step('I sign in with a valid email and password', () => {
|
||||
/* don't modify! */
|
||||
})
|
||||
|
||||
reporter.step('Check if I am logged in', () => {
|
||||
/* don't modify! */
|
||||
})
|
||||
|
||||
reporter.step('Check if I am being able to log out', () => {
|
||||
/* don't modify! */
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,11 +0,0 @@
|
||||
export enum FEATURE {
|
||||
AUTHENTICATION = 'authentication',
|
||||
AUTH_ADMIN = 'auth_admin',
|
||||
REALTIME = 'realtime',
|
||||
STORAGE = 'storage',
|
||||
FUNCTIONS = 'functions',
|
||||
RPC = 'rpc',
|
||||
POSTGREST = 'postgrest',
|
||||
FILTERS = 'postgrest_filters',
|
||||
MODIFIERS = 'postgrest_modifiers',
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import type { Config } from '@jest/types'
|
||||
|
||||
const config: Config.InitialOptions = {
|
||||
preset: 'ts-jest',
|
||||
transform: {
|
||||
'^.+\\.ts?$': 'ts-jest',
|
||||
},
|
||||
moduleFileExtensions: ['ts', 'js', 'cjs', 'json', 'node'],
|
||||
globalSetup: '<rootDir>/.jest/jest-env.ts',
|
||||
globalTeardown: '<rootDir>/.jest/teardown.ts',
|
||||
// setupFiles: ['<rootDir>/.jest/jest-env.js'],
|
||||
setupFilesAfterEnv: ['<rootDir>/.jest/jest-custom-reporter.ts'],
|
||||
testRunner: 'jest-jasmine2',
|
||||
testTimeout: 15000,
|
||||
coverageThreshold: {
|
||||
global: {
|
||||
branches: 0,
|
||||
functions: 0,
|
||||
lines: 0,
|
||||
statements: 0,
|
||||
},
|
||||
},
|
||||
}
|
||||
export default config
|
||||
@@ -1,39 +0,0 @@
|
||||
{
|
||||
"name": "supabase-tests",
|
||||
"version": "1.0.0",
|
||||
"description": "These tests can be run with Docker.",
|
||||
"scripts": {
|
||||
"preinstall": "npx only-allow pnpm",
|
||||
"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": "pnpm run allure:generate && pnpm run allure:serve",
|
||||
"clean": "rimraf node_modules",
|
||||
"format": "prettier --write \"./**/*.{js,jsx,ts,tsx,css,md}\""
|
||||
},
|
||||
"author": "Supabase, Inc.",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@faker-js/faker": "^6.1.2",
|
||||
"@supabase/supabase-js": "^2.44.3",
|
||||
"@testdeck/jest": "^0.3.3",
|
||||
"@types/jest": "^29.5.4",
|
||||
"@types/node": "^20.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.19.1",
|
||||
"@typescript-eslint/parser": "^6.19.1",
|
||||
"allure-commandline": "^2.17.2",
|
||||
"allure-js-commons": "^2.0.0-beta.14",
|
||||
"cross-fetch": "^3.1.5",
|
||||
"jest": "^29.7.0",
|
||||
"jest-allure2-adapter": "^0.3.12",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"postgres": "^3.0.5",
|
||||
"ts-jest": "^29.1.1",
|
||||
"typescript": "~5.5.0"
|
||||
}
|
||||
}
|
||||
3
tests/supabase/.gitignore
vendored
3
tests/supabase/.gitignore
vendored
@@ -1,3 +0,0 @@
|
||||
# Supabase
|
||||
.branches
|
||||
.temp
|
||||
@@ -1,71 +0,0 @@
|
||||
# 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 = ""
|
||||
@@ -1,50 +0,0 @@
|
||||
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' );
|
||||
@@ -1,29 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2021",
|
||||
"module": "commonjs",
|
||||
"esModuleInterop": true,
|
||||
"inlineSources": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"lib": ["es6", "DOM"],
|
||||
"removeComments": true,
|
||||
"noImplicitAny": true,
|
||||
"noImplicitReturns": true,
|
||||
"noImplicitThis": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"moduleResolution": "node",
|
||||
"sourceMap": true,
|
||||
"declaration": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"*": ["node_modules/*", "./types/*"]
|
||||
},
|
||||
"typeRoots": [
|
||||
"./node_modules/@types/",
|
||||
"./node_modules/allure2-js-commons/dist/declarations/**/"
|
||||
]
|
||||
},
|
||||
"exclude": ["node_modules"],
|
||||
"compileOnSave": false
|
||||
}
|
||||
Reference in New Issue
Block a user