Merge branch 'master' into blog/bring-the-fun-hackathon-winners

This commit is contained in:
Ant Wilson authored and GitHub committed 2022-04-18 22:07:29 +08:00
commit f7e0ef65e9
57 files changed
+13987 -686

No files matched your search

+91
View File
@@ -0,0 +1,91 @@
# 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
workflow_dispatch:
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
autotests:
name: Run tests and generate Allure Report
strategy:
matrix:
node: ['14']
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./tests
steps:
- uses: actions/checkout@v2
- name: Set up JDK
uses: actions/setup-java@v1
with:
java-version: 1.8
- name: Set up Node
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node }}
- name: Install dependencies
run: npm ci
- name: Run infrastructure
run: |
cp ../docker/.env.example ../docker/.env
npm run docker:up
- name: Run Test
run: npm run test
continue-on-error: true
- name: Stop infrastructure
run: npm run docker:down
- name: Get Allure history
uses: actions/checkout@v2
if: always()
continue-on-error: true
with:
repository: supabase/test-reports
ref: gh-pages
path: gh-pages
- name: Allure Report action
uses: simple-elf/allure-report-action@master
if: always()
with:
github_repo: test-reports
allure_results: allure-results
allure_history: allure-history
subfolder: supabase-integration
keep_reports: 50
- name: Deploy report to Github Pages
if: always()
uses: peaceiris/actions-gh-pages@v2
env:
EXTERNAL_REPOSITORY: supabase/test-reports
ACTIONS_DEPLOY_KEY: ${{ secrets.DEPLOY_TO_TEST_REPORTS_KEY }}
PUBLISH_BRANCH: gh-pages
PUBLISH_DIR: allure-history
- name: Post the link to the report
uses: Sibz/github-status-action@v1
with:
authToken: ${{ secrets.GITHUB_TOKEN }}
context: 'Test report'
state: 'success'
sha: ${{ github.event.pull_request.head.sha || github.sha }}
target_url: https://supabase.github.io/test-reports/supabase-integration/${{ github.run_number }}
+2
View File
@@ -6,6 +6,8 @@ out
tmp
coverage
allure-results
allure-report
.nyc_output
*.log
+3 -3
View File
@@ -16,7 +16,7 @@ DISABLE_SIGNUP=false
## Email auth
ENABLE_EMAIL_SIGNUP=true
ENABLE_EMAIL_AUTOCONFIRM=true
ENABLE_EMAIL_AUTOCONFIRM=false
SMTP_ADMIN_EMAIL=admin@example.com
SMTP_HOST=mail
SMTP_PORT=2500
@@ -25,8 +25,8 @@ SMTP_PASS=fake_mail_password
SMTP_SENDER_NAME=fake_sender
## Phone auth
ENABLE_PHONE_SIGNUP=false
ENABLE_PHONE_AUTOCONFIRM=false
ENABLE_PHONE_SIGNUP=true
ENABLE_PHONE_AUTOCONFIRM=true
# Ports
File diff suppressed because it is too large. Load diff
+3 -3
View File
@@ -9,10 +9,10 @@
"start": "next start"
},
"dependencies": {
"@supabase/supabase-js": "^1.1.2",
"@supabase/supabase-js": "^1.34.1",
"next": "latest",
"react": "^16.7.0",
"react-dom": "^16.7.0",
"react": "^18.0.0",
"react-dom": "^18.0.0",
"sass": "^1.26.2",
"tailwindcss": "^1.1.4"
}
+4 -4
View File
@@ -6,7 +6,7 @@
# Supabase
[Supabase](https://supabase.com) – это альтернатива Firebase с открытым исходным кодом. Мы создаем функционал Firebase, используя инструменты Enterprise уровня с открытым исходным кодом.
[Supabase](https://supabase.com) – это альтернатива Firebase с открытым исходным кодом. Мы создаем функциональность Firebase, используя инструменты Enterprise уровня с открытым исходным кодом.
- [x] Облачная база данных Postgres
- [x] Подписки в режиме реального времени
@@ -41,7 +41,7 @@
## Как это работает
Supabase - это сочетание инструментов с открытым исходным кодом. Мы создаем функционал Firebase, используя инструменты Enterprise уровня с открытым исходным кодом. Если инструмент с открытой лицензией MIT, Apache 2 или аналогичной существует, мы будем использовать и поддерживать его. Если такого инструмента не существует, мы создаем и открываем его в публичный доступ сами. Supabase не является точной копией Firebase. Наша цель - предоставить разработчикам опыт работы с Firebase, используя инструменты с открытым исходным кодом.
Supabase - это сочетание инструментов с открытым исходным кодом. Мы создаем функциональность Firebase, используя инструменты Enterprise уровня с открытым исходным кодом. Если инструмент с открытой лицензией MIT, Apache 2 или аналогичной существует, мы будем использовать и поддерживать его. Если такого инструмента не существует, мы создаем и открываем его в публичный доступ сами. Supabase не является точной копией Firebase. Наша цель - предоставить разработчикам опыт работы с Firebase, используя инструменты с открытым исходным кодом.
**Текущая архитектура**
@@ -49,8 +49,8 @@ Supabase – это [облачная платформа](https://app.supabase.i
![Архитектура](https://supabase.com/docs/assets/images/supabase-architecture-9050a7317e9ec7efb7807f5194122e48.png)
- [PostgreSQL](https://www.postgresql.org/) – это объектно-реляционная система баз данных с более чем 30 годами активной разработки, которая завоевала прочную репутацию благодаря надежности, устойчивости функционала и производительности.
- [Realtime](https://github.com/supabase/realtime) – это сервер, написанный на Elixir, который позволяет cледить за вставками, обновлениями и удалениями в базе PostgreSQL с помощью веб-сокета. Supabase использует встроенный функционал репликации Postgres, преобразует поток байтов репликации в JSON, а затем передает JSON через веб-сокеты.
- [PostgreSQL](https://www.postgresql.org/) – это объектно-реляционная система баз данных с более чем 30 годами активной разработки, которая завоевала прочную репутацию благодаря надежности, устойчивости функциональности и производительности.
- [Realtime](https://github.com/supabase/realtime) – это сервер, написанный на Elixir, который позволяет cледить за вставками, обновлениями и удалениями в базе PostgreSQL с помощью веб-сокета. Supabase использует встроенную функциональность репликации Postgres, преобразует поток байтов репликации в JSON, а затем передает JSON через веб-сокеты.
- [PostgREST](http://postgrest.org/) – это веб-сервер, который превращает вашу базу данных PostgreSQL в RESTful API.
- [Storage](https://github.com/supabase/storage-api) предоставляет RESTful интерфейс для управления файлами, хранящимися в S3, используя Postgres для управления разрешениями.
- [postgres-meta](https://github.com/supabase/postgres-meta) – это RESTful API для управления Postgres, позволяющий получать таблицы, добавлять роли, выполнять запросы и т.д.
@@ -7,6 +7,7 @@ import { post } from 'lib/common/fetch'
import { useStore } from 'hooks'
import { API_URL } from 'lib/constants'
import { UpdateSuccess } from '.'
import BackButton from 'components/ui/BackButton'
interface Props {
onSelectBack: () => void
@@ -21,7 +22,9 @@ const EnterpriseRequest: FC<Props> = ({ onSelectBack }) => {
const [isSuccessful, setIsSuccessful] = useState(false)
const initialValues = {
name: `${profile?.first_name} ${profile?.last_name}`,
name: `${profile?.first_name ? profile?.first_name : ''} ${
profile?.last_name ? profile.last_name : ''
}`,
email: profile?.primary_email,
company: '',
message: '',
@@ -81,43 +84,37 @@ const EnterpriseRequest: FC<Props> = ({ onSelectBack }) => {
enterTo="transform opacity-100 translate-x-0"
>
<div className="space-y-8 w-4/5">
<div className="relative">
<div className="absolute top-[2px] -left-24">
<Button type="text" icon={<IconArrowLeft />} onClick={onSelectBack}>
Back
</Button>
</div>
<div className="space-y-1">
<h4 className="text-lg">Customised plans tailored for your business needs</h4>
<p className="text-scale-1100">
Let us know a few details and we’ll be in contact with you!
</p>
</div>
<Form
validateOnBlur
initialValues={initialValues}
validate={onValidate}
onSubmit={onSubmit}
>
{({ isSubmitting }: { isSubmitting: boolean }) => (
<div className="space-y-12 py-16">
<div className="space-y-4 w-3/5">
<Input layout="horizontal" label="Name" id="name" name="name" />
<Input layout="horizontal" label="Email" id="email" name="email" />
<Input layout="horizontal" label="Company" id="company" name="company" />
</div>
<Input.TextArea
id="message"
name="message"
label="Let us know what you intend to use Supabase for"
/>
<Button htmlType="submit" loading={isSubmitting}>
Submit request
</Button>
</div>
)}
</Form>
<BackButton onClick={() => onSelectBack()} />
<div className="space-y-1">
<h4 className="text-lg">Customised plans tailored for your business needs</h4>
<p className="text-scale-1100">
Let us know a few details and our team will get back to you as soon as possible.
</p>
</div>
<Form
validateOnBlur
initialValues={initialValues}
validate={onValidate}
onSubmit={onSubmit}
>
{({ isSubmitting }: { isSubmitting: boolean }) => (
<div className="space-y-12">
<div className="space-y-4 w-3/5">
<Input layout="horizontal" label="Name" id="name" name="name" />
<Input layout="horizontal" label="Email" id="email" name="email" />
<Input layout="horizontal" label="Company" id="company" name="company" />
</div>
<Input.TextArea
id="message"
name="message"
label="Let us know what you intend to use Supabase for"
/>
<Button htmlType="submit" loading={isSubmitting}>
Submit request
</Button>
</div>
)}
</Form>
</div>
</Transition>
)
@@ -1,7 +1,14 @@
import { FC } from 'react'
import { Listbox, IconLoader, Button, IconPlus, IconAlertCircle } from '@supabase/ui'
import {
Listbox,
IconLoader,
Button,
IconPlus,
IconAlertCircle,
IconCreditCard,
} from '@supabase/ui'
import { useStore } from 'hooks'
import { useFlag, useStore } from 'hooks'
import { STRIPE_PRODUCT_IDS } from 'lib/constants'
import { SubscriptionPreview } from '../Billing.types'
import { getProductPrice } from '../Billing.utils'
@@ -48,6 +55,11 @@ const PaymentSummaryPanel: FC<Props> = ({
onConfirmPayment,
isSubmitting,
}) => {
/**
* Feature flags
*/
const nativeBilling = useFlag('nativeBilling')
const { ui } = useStore()
const projectRegion = ui.selectedProject?.region
@@ -66,14 +78,14 @@ const PaymentSummaryPanel: FC<Props> = ({
return (
<div
className="bg-scale-300 w-full px-20 py-10 space-y-8"
className="bg-panel-body-light dark:bg-panel-body-dark w-full px-6 lg:px-12 py-10 space-y-8 border-l"
style={{ height: 'calc(100vh - 49.5px)' }}
>
<p>Payment Summary</p>
{/* Subscription details */}
<div className="space-y-1">
<p className="text-sm">Selected subscription</p>
<p className="text-sm text-scale-1100">Selected subscription</p>
<div className="flex items-center justify-between">
<p className={`${isChangingPlan ? 'text-scale-1100 line-through' : ''} text-sm`}>
{getPlanName(currentPlan)}
@@ -93,46 +105,54 @@ const PaymentSummaryPanel: FC<Props> = ({
</div>
{/* Add on details */}
{projectRegion !== 'af-south-1' && (
<div className="space-y-1">
<p className="text-sm">Selected add-ons</p>
{currentComputeSize === undefined && selectedComputeSize === undefined && (
<p className="text-scale-1100 text-sm">No add-ons selected</p>
)}
{currentComputeSize !== undefined && (
<div className="flex items-center justify-between">
<p
className={`${isChangingComputeSize ? 'text-scale-1100 line-through' : ''} text-sm`}
>
{currentComputeSize.name}
</p>
<p
className={`${isChangingComputeSize ? 'text-scale-1100 line-through' : ''} text-sm`}
>
${(getProductPrice(currentComputeSize).unit_amount / 100).toFixed(2)}
</p>
{nativeBilling && (
<>
{projectRegion !== 'af-south-1' && (
<div className="space-y-1">
<p className="text-sm">Selected add-ons</p>
{currentComputeSize === undefined && selectedComputeSize === undefined && (
<p className="text-scale-1100 text-sm">No add-ons selected</p>
)}
{currentComputeSize !== undefined && (
<div className="flex items-center justify-between">
<p
className={`${
isChangingComputeSize ? 'text-scale-1100 line-through' : ''
} text-sm`}
>
{currentComputeSize.name}
</p>
<p
className={`${
isChangingComputeSize ? 'text-scale-1100 line-through' : ''
} text-sm`}
>
${(getProductPrice(currentComputeSize).unit_amount / 100).toFixed(2)}
</p>
</div>
)}
{isChangingComputeSize && (
<div className="flex items-center justify-between">
<p className="text-sm">{selectedComputeSize.name}</p>
<p className="text-sm">
${(getProductPrice(selectedComputeSize).unit_amount / 100).toFixed(2)}
</p>
</div>
)}
{isChangingComputeSize && (
<div className="!mt-4">
<InformationBox
hideCollapse
defaultVisibility
icon={<IconAlertCircle strokeWidth={2} />}
title="Changing your compute size"
description="It will take up to 2 minutes for changes to take place, and your project will be unavailable during that time"
/>
</div>
)}
</div>
)}
{isChangingComputeSize && (
<div className="flex items-center justify-between">
<p className="text-sm">{selectedComputeSize.name}</p>
<p className="text-sm">
${(getProductPrice(selectedComputeSize).unit_amount / 100).toFixed(2)}
</p>
</div>
)}
{isChangingComputeSize && (
<div className="!mt-4">
<InformationBox
hideCollapse
defaultVisibility
icon={<IconAlertCircle strokeWidth={2} />}
title="Changing your compute size"
description="It will take up to 2 minutes for changes to take place, and your project will be unavailable during that time"
/>
</div>
)}
</div>
</>
)}
<div className="h-px w-full bg-scale-600" />
@@ -152,12 +172,12 @@ const PaymentSummaryPanel: FC<Props> = ({
<p className="text-sm text-scale-1100">Retrieving payment methods</p>
</div>
) : paymentMethods.length === 0 ? (
<div className="flex items-center justify-between rounded-md px-4 py-2 bg-scale-400 border border-scale-700">
<div className="flex items-center space-x-4">
<IconAlertCircle size={16} />
<p className="text-scale-1100 text-sm">No saved payment methods</p>
<div className="flex items-center justify-between rounded-md px-4 py-2 bg-scale-100 border border-dashed">
<div className="flex items-center space-x-4 text-scale-1100">
<IconAlertCircle size={16} strokeWidth={1.5} />
<p className="text-sm">No saved payment methods</p>
</div>
<Button type="default" icon={<IconPlus />} onClick={onSelectAddNewPaymentMethod}>
<Button type="default" icon={<IconCreditCard />} onClick={onSelectAddNewPaymentMethod}>
Add new
</Button>
</div>
@@ -204,6 +224,8 @@ const PaymentSummaryPanel: FC<Props> = ({
loading={isSubmitting}
disabled={isSubmitting || isLoadingPaymentMethods || !hasChangesToPlan}
onClick={() => onConfirmPayment()}
block
size="medium"
>
Confirm payment
</Button>
@@ -71,7 +71,7 @@ const PaymentTotal: FC<Props> = ({
{amountDueImmediately < 0 ? (
<p className="text-sm text-scale-1100">
A total of ${Math.abs(amountDueImmediately).toFixed(2)} will be returned on{' '}
<span className="font-bold text-green-1100">
<span className="text-brand-900">
{billingDate.toLocaleDateString('en-US', {
day: 'numeric',
month: 'long',
@@ -84,7 +84,7 @@ const PaymentTotal: FC<Props> = ({
<p className="text-sm text-scale-1100">
This amount {!isSpendCapEnabled && !isBillingToday && '+ usage fees '}will be
charged on{' '}
<span className="font-bold text-green-1100">
<span className="text-brand-900">
{billingDate.toLocaleDateString('en-US', {
day: 'numeric',
month: 'long',
@@ -25,10 +25,15 @@ const PlanSelection: FC<Props> = ({ visible, tiers, currentPlan, onSelectPlan })
<div className="space-y-8">
<h4 className="text-lg">Change your project's subscription</h4>
{/* FE will make a call to fetch all plans first at the page level */}
<Plans plans={formattedTiers} currentPlan={currentPlan} onSelectPlan={onSelectPlan} />
<div className="grid lg:grid-cols-3 gap-8 py-8">
<Plans plans={formattedTiers} currentPlan={currentPlan} onSelectPlan={onSelectPlan} />
</div>
<div className="flex justify-center items-center">
<Link href="https://supabase.com/pricing">
<a target="_blank" className="text-sm text-scale-1100 hover:text-scale-1200 transition">
<a
target="_blank"
className="text-sm underline text-scale-1100 hover:text-scale-1200 transition"
>
See detailed comparisons across plans
</a>
</Link>
@@ -59,7 +59,13 @@ const PlanCTAButton: FC<Props> = ({ plan, currentPlan, onSelectPlan }) => {
return (
<div className="flex items-center justify-center">
<Button disabled={disabled} type={type} onClick={() => onSelectPlan(plan)}>
<Button
disabled={disabled}
type={type}
onClick={() => onSelectPlan(plan)}
block
size="medium"
>
{ctaText}
</Button>
</div>
@@ -0,0 +1,71 @@
import { STRIPE_PRODUCT_IDS } from 'lib/constants'
/**
* @mildtomato same plan metadata is also in /www website
* would be good if this constant was shared across apps
*
* https://github.com/supabase/supabase/blob/master/studio/components/interfaces/Billing/PlanSelection/Plans/Plans.Constants.ts
*/
export const PRICING_META = {
[STRIPE_PRODUCT_IDS.FREE]: {
name: 'Free',
href: '#',
priceMonthly: 0,
warning: 'Limit of 2 free projects',
description: 'Perfect for passion projects & simple websites.',
features: [
'Up to 500MB database & 1GB file storage',
'Up to 2GB bandwidth',
'Up to 50MB file uploads',
'Social OAuth providers',
'Up to 500K Edge Function invocations',
'1-day log retention',
'Community support',
],
additional: 'Free projects are paused after 1 week of inactivity.',
cta: 'Get Started',
},
[STRIPE_PRODUCT_IDS.PRO]: {
name: 'Pro',
href: '#',
from: true,
priceMonthly: 25,
warning: '+ additional use',
description: 'For production applications with the option to scale.',
features: [
'8GB database & 100GB file storage',
'50GB bandwith',
'3GB file uploads',
'Social OAuth providers',
'2M Edge Function invocations',
'Daily backups',
'7-day log retention',
'No project pausing',
'Email support',
],
scale: 'Additional fees apply for usage and storage beyond the limits above.',
shutdown: '',
preface: 'Everything below included in the base plan',
additional: 'Need more? Turn off your spend cap to Pay As You Grow ',
cta: 'Get Started',
},
Enterprise: {
name: 'Enterprise',
href: '/contact/enterprise',
description: 'For large-scale applications managing serious workloads.',
features: [
`Point in time recovery`,
`Designated Support manager & SLAs`,
`Enterprise OAuth providers`,
`SSO/ SAML`,
`SOC2`,
`Custom contracts & invoicing`,
`On-premise support`,
`24×7×365 premium enterprise support`,
],
scale: '',
shutdown: '',
cta: 'Contact Us',
},
}
@@ -1,9 +1,9 @@
import { FC } from 'react'
import { Badge } from '@supabase/ui'
import { IconCheck } from '@supabase/ui'
import { STRIPE_PRODUCT_IDS } from 'lib/constants'
import { FC } from 'react'
import { StripeProduct } from '../..'
import PlanCTAButton from './PlanCTAButton'
import { PRICING_META } from './Plans.Constants'
interface Props {
plans: any[]
@@ -23,84 +23,127 @@ const AnimatedGradientBackground = () => (
const CurrentSubscriptionBanner = () => (
<div
className={[
'absolute top-0 right-0 flex items-center justify-center',
'bg-green-900 px-4 py-1 rounded-bl-md',
'absolute h-8 -top-7 left-0 w-full',
'flex items-center justify-center',
'bg-brand-400 text-brand-900',
'rounded-t-md border-t border-l border-r border border-b-0',
].join(' ')}
>
<p className="text-xs">Current plan</p>
</div>
)
/**
* JSX below for the mapped plans is also very similar to www/pages/pricing
* TO DO: move to use shared components for these
*
* https://github.com/supabase/supabase/blob/master/www/pages/pricing/index.tsx
*/
const Plans: FC<Props> = ({ plans, currentPlan, onSelectPlan }) => {
return (
<div className="flex justify-between space-x-4">
<>
{plans.map((plan) => {
const isCurrentPlan =
plan.id === currentPlan?.prod_id ||
(plan.id === STRIPE_PRODUCT_IDS.PRO && currentPlan?.prod_id === STRIPE_PRODUCT_IDS.PRO) ||
(plan.id === STRIPE_PRODUCT_IDS.PRO && currentPlan?.prod_id === STRIPE_PRODUCT_IDS.PAYG)
const pointers = plan.metadata.features.split('\\n').map((x: string) => x.trim())
return (
<div
key={plan.name}
className={[
'flex flex-col justify-between w-1/3 px-6 py-8',
'bg-gray-300 border border-gray-500 rounded-md relative',
'overflow-hidden',
].join(' ')}
>
{/* [Joshen] Trying some animations here to make Pro pop, on the fence tbh */}
{plan.id === STRIPE_PRODUCT_IDS.PRO && <AnimatedGradientBackground />}
{/* Label to show current subscription (Pro and PAYG are treated as the same plan on the UI) */}
{isCurrentPlan && <CurrentSubscriptionBanner />}
<div className="mb-8">
<div className="flex items-center space-x-4">
<h3 className="text-xl">{plan.name}</h3>
{plan.isPopular && <Badge>Popular</Badge>}
<div key={PRICING_META[plan.id].name} className="relative h-full">
{plan.id === STRIPE_PRODUCT_IDS.PRO && (
<div className="absolute overflow-hidden w-full h-full left-0 bottom-0 opacity-50">
<AnimatedGradientBackground />
</div>
<p className="text-scale-1100 text-sm mt-2">{plan.description}</p>
<div className="py-8">
{plan.prices.length === 0 ? (
<p className="text-2xl text-center">Contact us</p>
) : (
<div className="flex items-end justify-center">
<p className="text-3xl">${plan.prices[0].unit_amount / 100}</p>
<p className="text-sm text-scale-1100 relative -top-[2px]"> /month</p>
)}
<div className="flex flex-col rounded border overflow-hidden h-full">
{isCurrentPlan && <CurrentSubscriptionBanner />}
<div className="px-8 py-6 bg-white dark:bg-scale-300">
<span
className="inline-flex text-cd font-normal tracking-wide rounded-full text-base text-scale-1200"
id="tier-standard"
>
{PRICING_META[plan.id].name}
</span>
<div className="flex items-baseline mt-2">
<div className="flex space-x-2">
{PRICING_META[plan.id].priceMonthly !== undefined ? (
<>
<div className="flex items-end gap-1">
{PRICING_META[plan.id].from && (
<span className="text-base font-medium text-scale-1200">From</span>
)}
<div>
<span className="text-2xl">${PRICING_META[plan.id].priceMonthly}</span>
<span className="ml-1 text-xl font-medium text-scale-900">/mo</span>
</div>
</div>
</>
) : (
<span className="text-2xl">Contact Us</span>
)}
<div className="flex h-8">
{PRICING_META[plan.id].warning && (
<div className="px-2 py-1 mt-2 text-xs rounded-md bg-brand-300 bg-opacity-30 text-brand-1000">
{PRICING_META[plan.id].warning}
</div>
)}
</div>
</div>
</div>
<div className="py-4">
<PlanCTAButton
plan={plan}
currentPlan={currentPlan}
onSelectPlan={onSelectPlan}
/>
</div>
<p className="text-sm text-scale-1100">{PRICING_META[plan.id].description}</p>
</div>
<div
className={[
'flex-col justify-between flex-1',
'space-y-6 border-t dark:border-scale-400',
'bg-scale-100 dark:bg-scale-300',
'px-8 py-6 h-full hidden lg:flex',
].join(' ')}
>
{PRICING_META[plan.id].preface && (
<p className="text-sm text-scale-1200">{PRICING_META[plan.id].preface}</p>
)}
<ul role="list" className="divide-y dark:divide-scale-400">
{PRICING_META[plan.id].features.map((feature) => (
<li key={feature} className="flex items-center py-2">
<IconCheck
className="w-3 h-3 text-brand-900 "
aria-hidden="true"
strokeWidth={3}
/>
<p className="mb-0 ml-3 text-xs text-scale-1100 dark:text-scale-1200">
{feature}
</p>
</li>
))}
</ul>
<div className="flex flex-col gap-6">
<div className="space-y-2">
{PRICING_META[plan.id].additional && (
<p className="text-sm text-scale-1200">{PRICING_META[plan.id].additional}</p>
)}
{PRICING_META[plan.id].scale && (
<p className="text-xs text-scale-900">{PRICING_META[plan.id].scale}</p>
)}
{PRICING_META[plan.id].shutdown && (
<p className="text-xs text-scale-900">{PRICING_META[plan.id].shutdown}</p>
)}
</div>
</div>
</div>
<ul className="space-y-4">
{pointers.map((pointer: string, idx: number) => (
<li key={`pointer-${idx}`} className="text-sm flex">
<div className="w-[15%]">
<svg
className={`h-5 w-5 text-green-900`}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fillRule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
clipRule="evenodd"
/>
</svg>
</div>
<div className="w-[85%]">
<span>{pointer}</span>
</div>
</li>
))}
</ul>
</div>
<PlanCTAButton plan={plan} currentPlan={currentPlan} onSelectPlan={onSelectPlan} />
</div>
)
})}
</div>
</>
)
}
@@ -2,7 +2,7 @@ import { FC, useEffect, useState } from 'react'
import { Transition } from '@headlessui/react'
import { Badge, Button, IconArrowLeft, IconHelpCircle, Toggle, Modal } from '@supabase/ui'
import { useStore } from 'hooks'
import { useFlag, useStore } from 'hooks'
import { post, patch } from 'lib/common/fetch'
import { API_URL } from 'lib/constants'
import { getURL } from 'lib/helpers'
@@ -18,6 +18,7 @@ import { SubscriptionPreview } from '../Billing.types'
import UpdateSuccess from '../UpdateSuccess'
import { formatComputeSizes } from '../AddOns/AddOns.utils'
import { formSubscriptionUpdatePayload } from './ProUpgrade.utils'
import BackButton from 'components/ui/BackButton'
// Do not allow compute size changes for af-south-1
@@ -36,6 +37,11 @@ const ProUpgrade: FC<Props> = ({
isLoadingPaymentMethods,
onSelectBack,
}) => {
/**
* Feature flags
*/
const nativeBilling = useFlag('nativeBilling')
const { ui } = useStore()
const { addons } = products
@@ -151,39 +157,37 @@ const ProUpgrade: FC<Props> = ({
className="w-full flex items-start justify-between"
>
<>
<div className="w-3/5 mt-10">
<div className="relative ml-64">
<div className="absolute top-[2px] -left-24">
<Button type="text" icon={<IconArrowLeft />} onClick={onSelectBack}>
Back
</Button>
</div>
<div className="2xl:min-w-5xl mx-auto mt-10">
<div className="relative px-5 space-y-4">
<BackButton onClick={() => onSelectBack()} />
<div className="space-y-8">
<h4 className="text-lg">Change your project's subscription</h4>
<h4 className="text-lg text-scale-900">Change your project's subscription</h4>
<div
className="space-y-8 overflow-scroll pb-8 pr-20"
className="space-y-8 overflow-scroll"
style={{ height: 'calc(100vh - 6.3rem - 49.5px)' }}
>
{!isManagingProSubscription ? (
<div className="space-y-1">
<h3 className="text-xl">
Welcome to <span className="text-green-1100">Pro</span>
<p className="text-sm text-scale-1100">
<div className="space-y-2">
{!isManagingProSubscription ? (
<>
<h3 className="text-xl">
Welcome to <span className="text-brand-900">Pro</span>
</h3>
<p className="text-base text-scale-1100">
Your new subscription will begin immediately after payment
</p>
</h3>
</div>
) : (
<div className="space-y-1">
<h3 className="text-xl">
Managing your <span className="text-green-1100">Pro</span> plan
<p className="text-sm text-scale-1100">
</>
) : (
<>
<h3 className="text-3xl">
Managing your <span className="text-brand-900">Pro</span> plan
</h3>
<p className="text-base text-scale-1100">
Your billing cycle will reset after payment
</p>
</h3>
</div>
)}
<div className="flex items-start justify-between">
</>
)}
</div>
<div className="flex items-center justify-between gap-16 border rounded px-6 py-4 bg-panel-body-light dark:bg-panel-body-dark drop-shadow-sm border-panel-border-light border-panel-border-dark">
<div>
<div className="flex items-center space-x-2">
<p>Enable spend cap</p>
@@ -195,7 +199,7 @@ const ProUpgrade: FC<Props> = ({
/>
</div>
<p className="text-sm text-scale-1100">
If disabled, additional resources will be charged on a per-usage basis
If enabled, additional resources will not be charged on a per-usage basis
</p>
</div>
<Toggle
@@ -203,7 +207,7 @@ const ProUpgrade: FC<Props> = ({
onChange={() => setIsSpendCapEnabled(!isSpendCapEnabled)}
/>
</div>
{projectRegion !== 'af-south-1' && (
{nativeBilling && projectRegion !== 'af-south-1' && (
<>
<Divider light />
<ComputeSizeSelection
@@ -218,7 +222,7 @@ const ProUpgrade: FC<Props> = ({
</div>
</div>
</div>
<div className="w-2/5">
<div className="w-[32rem]">
<PaymentSummaryPanel
isRefreshingPreview={isRefreshingPreview}
subscriptionPreview={subscriptionPreview}
@@ -7,12 +7,11 @@ import { Dictionary } from '@supabase/grid'
import { formatBytes } from 'lib/helpers'
import { STRIPE_PRODUCT_IDS } from 'lib/constants'
import { useStore, useFlag, useSubscriptionStats } from 'hooks'
import { useStore, useSubscriptionStats } from 'hooks'
import CostBreakdownRow from './CostBreakdownRow'
import { StripeSubscription } from './Subscription.types'
import { deriveFeatureCost, deriveProductCost } from '../PAYGUsage/PAYGUsage.utils'
import { chargeableProducts } from '../PAYGUsage/PAYGUsage.constants'
import UpgradeButton from './UpgradeButton'
interface Props {
project: any
@@ -37,7 +36,7 @@ const Subscription: FC<Props> = ({
const { ui } = useStore()
const isOrgOwner = ui.selectedOrganization?.is_owner
const nativeBilling = useFlag('nativeBilling')
// const nativeBilling = useFlag('nativeBilling')
const subscriptionStats = useSubscriptionStats()
const isPayg = subscription?.tier.prod_id === STRIPE_PRODUCT_IDS.PAYG
@@ -65,28 +64,20 @@ const Subscription: FC<Props> = ({
</p>
<h3 className="text-xl mb-0">{subscription?.tier.name ?? '-'}</h3>
</div>
{nativeBilling ? (
<div className="flex flex-col items-end space-y-2">
<Button
disabled={!isOrgOwner}
onClick={() => router.push(`/project/${project.ref}/settings/billing/update`)}
type="primary"
>
Change subscription
</Button>
{!isOrgOwner && (
<p className="text-sm text-scale-1100">
Only the organization owner can amend subscriptions
</p>
)}
</div>
) : (
<UpgradeButton
paid={paid}
projectRef={project.ref}
subscriptionStats={subscriptionStats}
/>
)}
<div className="flex flex-col items-end space-y-2">
<Button
disabled={!isOrgOwner}
onClick={() => router.push(`/project/${project.ref}/settings/billing/update`)}
type="primary"
>
Change subscription
</Button>
{!isOrgOwner && (
<p className="text-sm text-scale-1100">
Only the organization owner can amend subscriptions
</p>
)}
</div>
</div>
{paid && (
<div className="px-6 pt-4">
@@ -11,7 +11,8 @@ import { CancellationReasons } from './SubcriptionCancellation.constants'
/**
* Generates a Stripe Billing Portal link for a project.
* To be deprecate after native billing is completely rolled out
* This is now deprecated since 180422 since we have native billing
* Feel free to delete this file if we'd reckon we no longer need this fallback
*
* @param {Object} props.projectRef Project Ref
* @param {Boolean} props.paid Tier name
@@ -245,9 +245,7 @@ const LogTable = ({
</div>
</div>
)}
<div
className={'flex flex-row h-full ' + (!queryType ? 'border-l border-r' : '')}
>
<div className={`flex flex-row h-full ${!queryType ? 'border-l border-r' : ''}`}>
<DataGrid
style={{ height: '100%' }}
className={`
@@ -265,22 +263,11 @@ const LogTable = ({
<>
<div className="py-4 w-full h-full flex-col space-y-12">
<div
className={`transition-all
duration-500
delay-200
flex
flex-col
items-center
gap-6
text-center
mt-16
opacity-100
scale-100
justify-center
`}
className={[
'transition-all duration-500 delay-200',
'flex flex-col items-center gap-6 text-center',
'mt-16 opacity-100 scale-100 justify-center',
].join(' ')}
>
<div className="flex flex-col gap-1">
<div className="relative border border-scale-600 border-dashed dark:border-scale-400 w-32 h-4 rounded px-2 flex items-center"></div>
+26 -24
View File
@@ -15,32 +15,34 @@ const BillingLayout: FC<Props> = ({ children }) => {
<BaseLayout hideHeader hideIconBar>
<div className="flex flex-col h-full w-full">
{/* Header */}
<div className="px-3 py-2 border-b dark:border-dark flex items-center space-x-3">
<Link href={`/project/${ui.selectedProject?.ref}/settings/billing`}>
<a>
<Button type="text" size="tiny">
<IconX size={22} strokeWidth={1.5} />
</Button>
<div className="py-4 border-b dark:border-dark flex items-center space-x-3 px-5">
<Link href={`/project/${ui.selectedProject?.ref}/settings/billing`} passHref>
<a className="transition-colors text-scale-900 hover:text-scale-1200">
<IconX size={16} strokeWidth={1.5} />
</a>
</Link>
<div className="flex items-center space-x-2">
<p className="text-sm text-scale-1100">{ui.selectedOrganization?.name}</p>
<span className="text-scale-800">
<svg
viewBox="0 0 24 24"
width="16"
height="16"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
fill="none"
shapeRendering="geometricPrecision"
>
<path d="M16 3.549L7.12 20.600"></path>
</svg>
</span>
<p className="text-sm text-scale-1100">{ui.selectedProject?.name}</p>
<div className="flex items-center space-x-6">
<h1 className="text-base text-scale-1200">Customize your plan</h1>
<div className="h-6 w-px bg-scale-600"></div>
<div className="flex space-x-3 items-center">
<p className="text-sm text-scale-1100">{ui.selectedOrganization?.name}</p>
<span className="text-scale-800">
<svg
viewBox="0 0 24 24"
width="16"
height="16"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
fill="none"
shapeRendering="geometricPrecision"
>
<path d="M16 3.549L7.12 20.600"></path>
</svg>
</span>
<p className="text-sm text-scale-1100">{ui.selectedProject?.name}</p>
</div>
</div>
</div>
<div className="overflow-auto">
+13
View File
@@ -0,0 +1,13 @@
import { Button, IconArrowLeft } from '@supabase/ui'
export function BackButton({ onClick }: { onClick: () => void }) {
return (
<div className="2xl:absolute 2xl:top-[2px] -ml-2 2xl:-left-24">
<Button type="text" icon={<IconArrowLeft />} onClick={onClick}>
Back
</Button>
</div>
)
}
export default BackButton
+4 -4
View File
@@ -105,10 +105,10 @@ export const PROJECT_STATUS = {
RESTORING: 'RESTORING',
}
export const STRIPE_PRODUCT_IDS = {
FREE: process?.env?.NEXT_PUBLIC_STRIPE_FREE_TIER_ID,
PRO: process?.env?.NEXT_PUBLIC_STRIPE_PRO_TIER_ID,
PAYG: process?.env?.NEXT_PUBLIC_STRIPE_PAYG_TIER_ID,
export const STRIPE_PRODUCT_IDS: { [x: string]: string } = {
FREE: process?.env?.NEXT_PUBLIC_STRIPE_FREE_TIER_ID || 'prod_Ip4vqwv3EJ7Mi0',
PRO: process?.env?.NEXT_PUBLIC_STRIPE_PRO_TIER_ID || 'prod_IsRLOp58Z7V4XN',
PAYG: process?.env?.NEXT_PUBLIC_STRIPE_PAYG_TIER_ID || 'prod_JlTbw91xcM6NY4',
}
export const DEFAULT_MINIMUM_PASSWORD_STRENGTH = 4
@@ -122,14 +122,12 @@ const EmptyFunctions = () => {
<div className="grid py-12 lg:grid-cols-12 gap-y-12 lg:gap-x-16 max-w-7xl">
<div className="col-span-5 space-y-4">
<p className="max-w-lg text-base text-scale-1200">
Scalable pay-as-you-go functions as a service (FaaS) to run your code with zero server
management.
Scalable functions to run your code with no server management.
</p>
<p className="max-w-lg text-sm text-scale-1100">
No servers to provision, manage, or upgrade Automatically scale based on the load
Integrated monitoring, logging, and debugging capability Built-in security at role and
per function level based on the principle of least privilege Key networking capabilities
for hybrid and multi-cloud scenarios
Edge Functions are server-side Typescript functions, distributed globally at the edge -
close to your users. They can be used for listening to webhooks or integrating your
Supabase project with third-parties.
</p>
<div className="flex gap-2">
<Link passHref href="https://supabase.com/docs/guides/functions">
@@ -106,7 +106,7 @@ const BillingUpdate: NextPage = () => {
return (
<BillingLayout>
<div className="mx-auto max-w-5xl my-10">
<div className="mx-auto max-w-6xl my-10 px-6">
<PlanSelection
visible={!selectedPlan || (selectedPlan && showConfirmDowngrade)}
tiers={products?.tiers ?? []}
@@ -465,7 +465,7 @@ const GeneralSettings: FC<any> = ({ projectRef }) => {
disabled
value={
`user=${connectionInfo.db_user} password=[YOUR-PASSWORD]` +
`host=${connectionInfo.db_host} port=${connectionInfo.db_port.toString()}` +
` host=${connectionInfo.db_host} port=${connectionInfo.db_port.toString()}` +
` database=${connectionInfo.db_name}`
}
/>
+194
View File
@@ -0,0 +1,194 @@
/* 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,5 +1,9 @@
process.env.SUPABASE_DB_HOST = 'localhost'
process.env.SUPABASE_DB_PORT = '5432'
process.env.SUPABASE_DB_PASS = 'your-super-secret-and-long-postgres-password'
process.env.SUPABASE_GOTRUE = 'http://localhost:8000/auth/v1'
process.env.SUPABASE_URL = 'http://localhost:8000'
process.env.SUPABASE_KEY_ANON =
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE'
process.env.SUPABASE_KEY_ADMIN =
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q'
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q'
+4
View File
@@ -0,0 +1,4 @@
api
node_modules
package-lock.json
docker*
+7
View File
@@ -0,0 +1,7 @@
{
"trailingComma": "es5",
"tabWidth": 2,
"semi": false,
"singleQuote": true,
"printWidth": 100
}
+5 -1
View File
@@ -2,6 +2,10 @@
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:
@@ -11,4 +15,4 @@ In the parent folder:
### Clean up
- `npm run docker:remove`
- `npm run docker:remove`
@@ -0,0 +1,331 @@
import { suite, test } 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 { 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 { 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 'new users'() {
// 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 {
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).toBeNull()
// check if user is not signed in cause he has not confirmed his email
const { error: errorInsertFailed } = await this.insertProfile(supabase, user, fakeUser)
log('check if insert failed caused user has not confirmed email')
expect(errorInsertFailed).not.toBeNull()
// confirm user email and sign in
const [{ confirmation_token: token }] = await this.getConfirmationToken(user)
const verifyResponse = await this.verify(token)
expect(verifyResponse.ok).toBeTruthy()
const { session, error: signInError } = await supabase.auth.signIn({
email: fakeUser.email,
password: fakeUser.password,
})
expect(signInError).toBeNull()
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 sign up with phone then he should be logged in')
@test
async 'new users by phone'() {
// 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 { 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'() {
// 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,
error: signInError,
} = await supabase.auth.signIn({
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'() {
// 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.signIn({
email: fakeUser.email,
password: fakeUser.password,
})
// get signed in user data
const user = this.getUser(supabase)
log('Check if user is signed in correctly and can get his data')
expect(user).toBeDefined()
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'() {
// 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({
email: fakeUser.email,
password: fakeUser.password,
})
// get signed in user data
const user = this.getUser(supabase)
// update user
// todo update params
const {
user: updUser,
data: updUserData,
error: updUserError,
} = await this.updateUser(supabase, {})
// todo check if returned user is updated
const updatedUser = this.getUser(supabase)
// todo check if user is updated
const userFromApi = this.getUserFromApi(supabase)
// todo check if user is updated on backend
}
@feature(FEATURE.AUTHENTICATION)
@severity(Severity.NORMAL)
@description('When user changes session then he still should be correctly logined')
@test.skip
async 'set session'() {
// todo
}
@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
}
@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
}
@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
}
): 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()
}
@step('Update user info')
private async updateUser(
supabase: SupabaseClient,
attr: UserAttributes
): Promise<{
data: User
user: User
error: ApiError
}> {
return supabase.auth.update(attr)
}
}
@@ -0,0 +1,208 @@
import { params, suite, test } 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 { 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
}
}
@@ -0,0 +1,20 @@
import { params, suite, test } 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 { FEATURE } from '../templates/enums'
import { description, feature, log, severity, step } from '../../.jest/jest-custom-reporter'
import { Hooks } from './hooks'
@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'() {
// todo
}
}
+137
View File
@@ -0,0 +1,137 @@
import postgres from 'postgres'
import crossFetch from 'cross-fetch'
import { faker } from '@faker-js/faker'
import {
ApiError,
createClient,
Session,
SupabaseClient,
SupabaseClientOptions,
User,
} 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 = {}): SupabaseClient {
return createClient(url, key, options)
}
@step('Create a valid user')
async createUser(data: object = {}): Promise<{
email: string
password: string
username: 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(),
}
const { user, error: signUpError } = await this.signUp(supabase, fakeUser, {
data: data,
})
expect(signUpError).toBeNull()
const [{ confirmation_token: token }] = await this.getConfirmationToken(user)
await this.verify(token)
return fakeUser
}
@step((token: string) => `verify with token ${token}`)
async verify(token: string): Promise<Response> {
return crossFetch(`${process.env.SUPABASE_GOTRUE}/verify`, {
method: 'POST',
body: JSON.stringify({
type: 'signup',
token: token,
}),
})
}
@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
} = {}
): Promise<{
user: User
session: Session
error: ApiError
}> {
return supabase.auth.signUp(
{
email: email,
password: password,
},
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.user()
}
@step('Get user data, if there is a logged in user')
getUserFromApi(supabase: SupabaseClient) {
return supabase.auth.api.getUser(supabase.auth.session().access_token)
}
@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}
`
}
}
+148
View File
@@ -0,0 +1,148 @@
import { params, suite, test } 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 { FEATURE } from '../templates/enums'
import { description, feature, log, severity, step } from '../../.jest/jest-custom-reporter'
import { Hooks } from './hooks'
@suite('realtime')
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
}
@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
}
@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.skip
async 'get subscriptions'() {
// todo
}
@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.skip
async 'unsubscribe from table'() {
// todo
}
@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
}
@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
}
}
+20
View File
@@ -0,0 +1,20 @@
import { params, suite, test } 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 { FEATURE } from '../templates/enums'
import { description, feature, log, severity, step } from '../../.jest/jest-custom-reporter'
import { Hooks } from './hooks'
@suite('rpc')
class Procedures extends Hooks {
@feature(FEATURE.STORAGE)
@severity(Severity.BLOCKER)
@description('When you call rpc then you are able to receive its result')
@test.skip
async 'call rpc'() {
// todo
}
}
+28
View File
@@ -0,0 +1,28 @@
import { params, suite, test } 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 { FEATURE } from '../templates/enums'
import { description, feature, log, severity, step } from '../../.jest/jest-custom-reporter'
import { Hooks } from './hooks'
@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
}
@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
}
}
+44
View File
@@ -0,0 +1,44 @@
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
@@ -0,0 +1,50 @@
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! */
})
})
})
+10
View File
@@ -0,0 +1,10 @@
export enum FEATURE {
AUTHENTICATION = 'authentication',
REALTIME = 'realtime',
STORAGE = 'storage',
FUNCTIONS = 'functions',
RPC = 'rpc',
DB = 'database',
Filters = 'database.filters',
Modifiers = 'database.modifiers',
}
-64
View File
@@ -1,64 +0,0 @@
import * as faker from 'faker'
import { createClient } from '@supabase/supabase-js'
const unauthorized = createClient(process.env.SUPABASE_URL, 'FAKE_KEY')
const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY_ANON)
const admin = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY_ADMIN)
test('Unauthorized', async () => {
const { error } = await unauthorized.from('profiles').select()
expect(error.message).toBe('Invalid authentication credentials')
})
test('Simple test', async () => {
const fakeOne = {
email: faker.internet.email().toLowerCase(),
password: faker.internet.password(),
username: faker.internet.userName(),
}
const fakeTwo = {
email: faker.internet.email().toLowerCase(),
password: faker.internet.password(),
username: faker.internet.userName(),
}
const first = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY_ANON)
const second = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY_ANON)
const { user: firstUser } = await first.auth.signUp({
email: fakeOne.email,
password: fakeOne.password,
})
const { user: secondUser } = await second.auth.signUp({
email: fakeTwo.email,
password: fakeTwo.password,
})
expect(firstUser.email).toEqual(fakeOne.email)
expect(secondUser.email).toEqual(fakeTwo.email)
const { data: firstProfile } = await first
.from('profiles')
.insert({
id: firstUser.id,
username: fakeOne.username,
})
.single()
expect(firstProfile.username).toMatch(fakeOne.username)
// Cannot insert the second user on the first client
const { error: secondProfile } = await first
.from('profiles')
.insert({
id: secondUser.id,
username: fakeTwo.username,
})
.single()
expect(secondProfile.message).toMatch(/new row violates row-level security policy for table/)
const { data: firstProfileList } = await first.from('profiles').select()
const { data: secondProfileList } = await second.from('profiles').select()
const { data: adminProfileList } = await admin.from('profiles').select()
expect(firstProfileList.length).toBe(1)
expect(secondProfileList.length).toBe(0)
expect(adminProfileList.length).toBeGreaterThanOrEqual(1)
})
+27
View File
@@ -0,0 +1,27 @@
import type { Config } from '@jest/types'
const config: Config.InitialOptions = {
preset: 'ts-jest',
transform: {
'^.+\\.ts?$': 'ts-jest',
},
moduleFileExtensions: ['ts', 'js', 'cjs', 'json', 'node'],
// globalSetup: './test/hooks.ts',
// globalTeardown: './teardown.js',
setupFiles: ['<rootDir>/.jest/jest-env.js'],
setupFilesAfterEnv: ['<rootDir>/.jest/jest-custom-reporter.ts'],
testRunner: 'jest-jasmine2',
testTimeout: 5000,
// testEnvironmentOptions: {
// jiraUrl: ""
// },
coverageThreshold: {
global: {
branches: 0,
functions: 0,
lines: 0,
statements: 0,
},
},
}
export default config
+9814
View File
File diff suppressed because it is too large. Load diff
+34
View File
@@ -0,0 +1,34 @@
{
"name": "supabase-tests",
"version": "1.0.0",
"description": "These tests can be run with Docker.",
"scripts": {
"docker:up": "cd ../docker && 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",
"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"
},
"author": "Supabase, Inc.",
"license": "MIT",
"devDependencies": {
"@faker-js/faker": "^6.1.2",
"@supabase/supabase-js": "^1.34.1",
"@testdeck/jest": "^0.2.0",
"@types/jest": "^27.4.0",
"@types/node": "^17.0.24",
"@typescript-eslint/eslint-plugin": "^5.10.0",
"@typescript-eslint/parser": "^5.10.0",
"allure-commandline": "^2.17.2",
"allure-js-commons": "^2.0.0-beta.14",
"cross-fetch": "^3.1.5",
"eslint": "^8.7.0",
"jest": "^27.4.7",
"jest-allure2-adapter": "^0.3.11",
"postgres": "^3.0.5",
"ts-jest": "^27.1.3",
"ts-node": "^10.7.0",
"typescript": "^4.6.3"
}
}
+29
View File
@@ -0,0 +1,29 @@
{
"compilerOptions": {
"target": "es6",
"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
}
@@ -0,0 +1,108 @@
---
title: Supabase Beta March 2022
description: Functions, GraphQL, and much more.
author: ant_wilson
image: 2022-march/thumb.jpg
thumb: 2022-march/thumb.jpg
tags:
- launch-week
date: '2022-04-15T11:00:00'
toc_depth: 3
---
Definitely our most feature-packed month so far. Here's a ton of stuff we shipped throughout March...
## Functions
The one we've all been waiting for. You can now [deploy Javascript & Typescript functions](/blog/2022/03/31/supabase-edge-functions) to Supabase. You can develop and test locally, link a project, and deploy globally to the edge in a matter of seconds!
[![supabase functions](/images/blog/2022-march/functions.png)](/blog/2022/03/31/supabase-edge-functions)
## GraphQL
You can now [query your Supabase database with GraphQL](/blog/2022/03/29/graphql-now-available). We built this as a postgres extension, so it's portable, open source, and solves some of the problems that exist in other GraphQL implementations.
[![graphql](/images/blog/2022-march/graphql.jpg)](/blog/2022/03/29/graphql-now-available)
## Realtime
Supabase Realtime [now supports multiplayer and presence](/blog/2022/04/01/supabase-realtime-with-multiplayer-features), so you can build collaborative applications like Figma and Google Docs with Supabase. Come and have some fun on our demo application.
[![realtime](/images/blog/2022-march/realtime.png)](/blog/2022/04/01/supabase-realtime-with-multiplayer-features)
## Enterprise
[Logging and Observability dashboard](/blog/2022/03/30/supabase-enterprise), Prometheus endpoint, SLAs, premium support, pricing updates, and Point-In-Time-Recovery. Everything you need to use Supabase at scale.
[![enterprise](/images/blog/2022-march/enterprise.png)](/blog/2022/03/30/supabase-enterprise)
## GitHub Secret Scanning
We're now an official [GitHub secret scanning partner](/blog/2022/03/28/community-day#supabase-is-now-a-github-secret-scanning-partner), meaning that if you accidentally push your service_role key to GitHub, you'll get a notification email, and your keys will automatically be rolled on Supabase. For private repos you can disable this.
[![secret scanning](/images/blog/2022-march/secret-scanning.png)](/blog/2022/03/28/community-day#supabase-is-now-a-github-secret-scanning-partner)
## Community Day
[PostgREST 10 pre-release](/blog/2022/03/28/community-day#postgrest-10-pre-release), new OAuth providers, Next-JS auth helpers, new courses, and an open source Partner Gallery.
[![community day](/images/blog/2022-march/community-day.png)](/blog/2022/03/28/community-day)
## Should I open source my company?
[We wrote an article](/blog/2022/03/25/should-i-open-source-my-company) for anyone considoring making their codebase open source. We explore the advantages and tackle some common concerns.
[![open source](/images/blog/2022-march/open-source.png)](/blog/2022/03/25/should-i-open-source-my-company)
## Supabrew - Never Code Thirsty
For April Fools - [we announced a non-alcoholic](/blog/2022/04/01/supabrew), light, and refreshing brew. Perfect for coding late into the night. We're pushing the boundaries of Developer Experience.
[![supabrew](/images/blog/2022-march/supabrew.png)](/blog/2022/04/01/supabrew)
## Video Highlights
Relive all the action from Launch Week 4:
- [Community Day Live Stream](https://www.youtube.com/watch?v=D0gIgjozOzc) on YouTube
- [GraphQL Announcement Live Stream](https://www.youtube.com/watch?v=fApxu_U_hdw) on YouTube
- [Enterprise Announcements Live Stream](https://www.youtube.com/watch?v=UZrl1cwzTDg) on YouTube
- [Functions Announcement with Deno Team](https://www.youtube.com/watch?v=Xci5SOv76k0) on YouTube
- [Realtime Demo App](https://www.youtube.com/watch?v=BelYEMJ2N00) on YouTube
- [Realtime Remix w/Supabase Launch Party](https://www.youtube.com/watch?v=rntEMgaenHs) on YouTube
- [Building an Image Gallery with Vercel](https://www.youtube.com/watch?v=BSoRXk1FIw8) by Lee Robinson
## Hiring
We're Hiring - for a ton of new roles across engineering, marketing, and HR. We hire fully remotely, and you'll get to work alongside some incredible people, on one of the fastest growing open source companies. [See which roles we're hiring for](https://about.supabase.com/careers).
[![supabase is hiring](/images/blog/2021-dec/hiring.png)](https://about.supabase.com/careers)
## Twitter
We're having a 24/7*365 meme-fest on [Twitter](https://twitter.com/supabase). Come join us.
[![supabase twitter](/images/blog/2022-march/twitter.png)](https://twitter.com/supabase)
## TikTok
Check us out on [TikTok](https://www.tiktok.com/@supabase.com?fromUrl=%2Fsupabase.com&lang=en)
[![tiktok](/images/blog/2021-dec/tiktok.png)](https://www.tiktok.com/@supabase.com?fromUrl=%2Fsupabase.com&lang=en)
## Swag Store
A reminder that we have a [Supabase Swag Store](https://store.supabase.com)
[![swag store](/images/blog/2021-dec/swag.jpg)](https://store.supabase.com)
## GitHub
We hit 30K stars!!: [github.com/supabase/supabase](http://github.com/supabase/supabase)
## Get started
- Start using Supabase today: **[app.supabase.io](https://app.supabase.io/)**
- Make sure to **[star us on GitHub](https://github.com/supabase/supabase)**
- Follow us **[on Twitter](https://twitter.com/supabase)**
- Subscribe to our **[YouTube channel](https://www.youtube.com/c/supabase)**
- Become a **[sponsor](https://github.com/sponsors/supabase)**
+1 -1
View File
@@ -56,7 +56,7 @@ function ExampleCard(props: any) {
<div className="flex gap-2 items-center mt-3">
{props.vercel_deploy_url && (
<a target="_blank" href={props.vercel_deploy_url}>
<img src="https://vercel.com/button" />
<img className="h-6" src="https://vercel.com/button" />
</a>
)}
{props.demo_url && (
+6
View File
@@ -21,6 +21,12 @@ export default function IndexPage() {
const meta_description =
'Explore Supabase fees and pricing information. Find our competitive pricing tiers, with no hidden pricing. We have generous free tiers for those getting started, and Pay As You Go for those scaling up.'
/**
* @mildtomato same plan metadata is also in /studio dashboard
* would be good if this constant was shared across apps
*
* https://github.com/supabase/supabase/blob/master/www/pages/pricing/index.tsx
*/
const tiers = [
{
name: 'Free',
Binary file not shown.

After

Width:  |  Height:  |  Size: 214 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 656 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 418 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 261 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

+8
View File
@@ -15,6 +15,14 @@
<description>Celebrating many amazing OSS Hackathon projects using GraphQL and Edge Functions.</description>
<pubDate>Sun, 17 Apr 2022 16:00:00 GMT</pubDate>
</item>
<item>
<guid>https://supabase.com/blog/2022/04/15/beta-update-march-2022</guid>
<title>Supabase Beta March 2022</title>
<link>https://supabase.com/blog/2022/04/15/beta-update-march-2022</link>
<description>Functions, GraphQL, and much more.</description>
<pubDate>Thu, 14 Apr 2022 16:00:00 GMT</pubDate>
</item>
<item>
<guid>https://supabase.com/blog/2022/04/01/supabrew</guid>