Merge branch 'main' into ipad-fixes

This commit is contained in:
Samuel Newman
2024-03-31 16:30:55 -07:00
940 changed files with 154123 additions and 32274 deletions
+92
View File
@@ -0,0 +1,92 @@
#!/usr/bin/env sh
get_container_id() {
local compose_file=$1
local service=$2
if [ -z "${compose_file}" ] || [ -z "${service}" ]; then
echo "usage: get_container_id <compose_file> <service>"
exit 1
fi
docker compose -f $compose_file ps --format json --status running \
| jq -r '.[]? | select(.Service == "'${service}'") | .ID'
}
# Exports all environment variables
export_env() {
export_pg_env
export_redis_env
}
# Exports postgres environment variables
export_pg_env() {
# Based on creds in compose.yaml
export PGPORT=5433
export PGHOST=localhost
export PGUSER=pg
export PGPASSWORD=password
export PGDATABASE=postgres
export DB_POSTGRES_URL="postgresql://pg:password@127.0.0.1:5433/postgres"
}
# Exports redis environment variables
export_redis_env() {
export REDIS_HOST="127.0.0.1:6380"
}
# Main entry point
main() {
# Expect a SERVICES env var to be set with the docker service names
local services=${SERVICES}
dir=$(dirname $0)
compose_file="${dir}/docker-compose.yaml"
# whether this particular script started the container(s)
started_container=false
# trap SIGINT and performs cleanup as necessary, i.e.
# taking down containers if this script started them
trap "on_sigint ${services}" INT
on_sigint() {
local services=$@
echo # newline
if $started_container; then
docker compose -f $compose_file rm -f --stop --volumes ${services}
fi
exit $?
}
# check if all services are running already
not_running=false
for service in $services; do
container_id=$(get_container_id $compose_file $service)
if [ -z $container_id ]; then
not_running=true
break
fi
done
# if any are missing, recreate all services
if $not_running; then
docker compose -f $compose_file up --wait --force-recreate ${services}
started_container=true
else
echo "all services ${services} are already running"
fi
# setup environment variables and run args
export_env
"$@"
# save return code for later
code=$?
# performs cleanup as necessary, i.e. taking down containers
# if this script started them
echo # newline
if $started_container; then
docker compose -f $compose_file rm -f --stop --volumes ${services}
fi
exit ${code}
}
+49
View File
@@ -0,0 +1,49 @@
version: '3.8'
services:
# An ephermerally-stored postgres database for single-use test runs
db_test: &db_test
image: postgres:14.4-alpine
environment:
- POSTGRES_USER=pg
- POSTGRES_PASSWORD=password
ports:
- '5433:5432'
# Healthcheck ensures db is queryable when `docker-compose up --wait` completes
healthcheck:
test: 'pg_isready -U pg'
interval: 500ms
timeout: 10s
retries: 20
# A persistently-stored postgres database
db:
<<: *db_test
ports:
- '5432:5432'
healthcheck:
disable: true
volumes:
- atp_db:/var/lib/postgresql/data
# An ephermerally-stored redis cache for single-use test runs
redis_test: &redis_test
image: redis:7.0-alpine
ports:
- '6380:6379'
# Healthcheck ensures redis is queryable when `docker-compose up --wait` completes
healthcheck:
test: ['CMD-SHELL', '[ "$$(redis-cli ping)" = "PONG" ]']
interval: 500ms
timeout: 10s
retries: 20
# A persistently-stored redis cache
redis:
<<: *redis_test
command: redis-server --save 60 1 --loglevel warning
ports:
- '6379:6379'
healthcheck:
disable: true
volumes:
- atp_redis:/data
volumes:
atp_db:
atp_redis:
+9
View File
@@ -0,0 +1,9 @@
#!/usr/bin/env sh
# Example usage:
# ./with-test-db.sh psql postgresql://pg:password@localhost:5433/postgres -c 'select 1;'
dir=$(dirname $0)
. ${dir}/_common.sh
SERVICES="db_test" main "$@"
+10
View File
@@ -0,0 +1,10 @@
#!/usr/bin/env sh
# Example usage:
# ./with-test-redis-and-db.sh psql postgresql://pg:password@localhost:5433/postgres -c 'select 1;'
# ./with-test-redis-and-db.sh redis-cli -h localhost -p 6380 ping
dir=$(dirname $0)
. ${dir}/_common.sh
SERVICES="db_test redis_test" main "$@"
+17 -1
View File
@@ -1,6 +1,9 @@
/* global jest */
import {configure} from '@testing-library/react-native'
import 'react-native-gesture-handler/jestSetup'
// IMPORTANT: this is what's used in the native runtime
import 'react-native-url-polyfill/auto'
import {configure} from '@testing-library/react-native'
configure({asyncUtilTimeout: 20000})
@@ -74,3 +77,16 @@ jest.mock('lande', () => ({
__esModule: true, // this property makes it work
default: jest.fn().mockReturnValue([['eng']]),
}))
jest.mock('sentry-expo', () => ({
init: () => jest.fn(),
Native: {
ReactNativeTracing: jest.fn().mockImplementation(() => ({
start: jest.fn(),
stop: jest.fn(),
})),
ReactNavigationInstrumentation: jest.fn(),
},
}))
jest.mock('crypto', () => ({}))
+167 -65
View File
@@ -1,8 +1,8 @@
import {AtUri, BskyAgent} from '@atproto/api'
import {TestNetwork, TestPds} from '@atproto/dev-env'
import fs from 'fs'
import net from 'net'
import path from 'path'
import fs from 'fs'
import {TestNetworkNoAppView} from '@atproto/dev-env'
import {AtUri, BskyAgent} from '@atproto/api'
export interface TestUser {
email: string
@@ -18,19 +18,115 @@ export interface TestPDS {
close: () => Promise<void>
}
class StringIdGenerator {
_nextId = [0]
constructor(
public _chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ',
) {}
next() {
const r = []
for (const char of this._nextId) {
r.unshift(this._chars[char])
}
this._increment()
return r.join('')
}
_increment() {
for (let i = 0; i < this._nextId.length; i++) {
const val = ++this._nextId[i]
if (val >= this._chars.length) {
this._nextId[i] = 0
} else {
return
}
}
this._nextId.push(0)
}
*[Symbol.iterator]() {
while (true) {
yield this.next()
}
}
}
const ids = new StringIdGenerator()
export async function createServer(
{inviteRequired}: {inviteRequired: boolean} = {inviteRequired: false},
{
inviteRequired,
phoneRequired,
}: {inviteRequired: boolean; phoneRequired: boolean} = {
inviteRequired: false,
phoneRequired: false,
},
): Promise<TestPDS> {
const port = await getPort()
const port = 3000
const port2 = await getPort(port + 1)
const port3 = await getPort(port2 + 1)
const pdsUrl = `http://localhost:${port}`
const testNet = await TestNetworkNoAppView.create({
pds: {port, publicUrl: pdsUrl, inviteRequired},
const id = ids.next()
const phoneParams = phoneRequired
? {
phoneVerificationRequired: true,
phoneVerificationProvider: 'twilio',
twilioAccountSid: 'ACXXXXXXX',
twilioAuthToken: 'AUTH',
twilioServiceSid: 'VAXXXXXXXX',
}
: {}
const testNet = await TestNetwork.create({
pds: {
port,
hostname: 'localhost',
dbPostgresSchema: `pds_${id}`,
inviteRequired,
...phoneParams,
},
bsky: {
dbPostgresSchema: `bsky_${id}`,
port: port3,
publicUrl: 'http://localhost:2584',
},
plc: {port: port2},
})
mockTwilio(testNet.pds)
// add the test mod authority
if (!phoneRequired) {
const agent = new BskyAgent({service: pdsUrl})
const res = await agent.api.com.atproto.server.createAccount({
email: 'mod-authority@test.com',
handle: 'mod-authority.test',
password: 'hunter2',
})
agent.api.setHeader('Authorization', `Bearer ${res.data.accessJwt}`)
await agent.api.app.bsky.actor.profile.create(
{repo: res.data.did},
{
displayName: 'Dev-env Moderation',
description: `The pretend version of mod.bsky.app`,
},
)
await agent.api.app.bsky.labeler.service.create(
{repo: res.data.did, rkey: 'self'},
{
policies: {
labelValues: ['!hide', '!warn'],
labelValueDefinitions: [],
},
createdAt: new Date().toISOString(),
},
)
}
const pic = fs.readFileSync(
path.join(__dirname, '..', 'assets', 'default-avatar.jpg'),
path.join(__dirname, '..', 'assets', 'default-avatar.png'),
)
return {
@@ -48,7 +144,7 @@ class Mocker {
users: Record<string, TestUser> = {}
constructor(
public testNet: TestNetworkNoAppView,
public testNet: TestNetwork,
public service: string,
public pic: Uint8Array,
) {
@@ -59,6 +155,10 @@ class Mocker {
return this.testNet.pds
}
get bsky() {
return this.testNet.bsky
}
get plc() {
return this.testNet.plc
}
@@ -81,11 +181,7 @@ class Mocker {
const inviteRes = await agent.api.com.atproto.server.createInviteCode(
{useCount: 1},
{
headers: {
authorization: `Basic ${btoa(
`admin:${this.pds.ctx.cfg.adminPassword}`,
)}`,
},
headers: this.pds.adminAuthHeaders('admin'),
encoding: 'application/json',
},
)
@@ -96,6 +192,8 @@ class Mocker {
email,
handle: name + '.test',
password: 'hunter2',
verificationPhone: '1234567890',
verificationCode: '000000',
})
await agent.upsertProfile(async () => {
const blob = await agent.uploadBlob(this.pic, {
@@ -260,11 +358,7 @@ class Mocker {
await agent.api.com.atproto.server.createInviteCode(
{useCount: 1, forAccount},
{
headers: {
authorization: `Basic ${btoa(
`admin:${this.pds.ctx.cfg.adminPassword}`,
)}`,
},
headers: this.pds.adminAuthHeaders('admin'),
encoding: 'application/json',
},
)
@@ -275,24 +369,22 @@ class Mocker {
if (!did) {
throw new Error(`Invalid user: ${user}`)
}
const ctx = this.pds.ctx
const ctx = this.bsky.ctx
if (!ctx) {
throw new Error('Invalid PDS')
throw new Error('Invalid appview')
}
await ctx.db.db
.insertInto('label')
.values([
{
src: ctx.cfg.labelerDid,
uri: did,
cid: '',
val: label,
neg: 0,
cts: new Date().toISOString(),
},
])
.execute()
const labelSrvc = ctx.services.label(ctx.db.getPrimary())
await labelSrvc.createLabels([
{
// @ts-ignore
src: ctx.cfg.labelerDid,
uri: did,
cid: '',
val: label,
neg: false,
cts: new Date().toISOString(),
},
])
}
async labelProfile(label: string, user: string) {
@@ -307,43 +399,41 @@ class Mocker {
rkey: 'self',
})
const ctx = this.pds.ctx
const ctx = this.bsky.ctx
if (!ctx) {
throw new Error('Invalid PDS')
throw new Error('Invalid appview')
}
await ctx.db.db
.insertInto('label')
.values([
{
src: ctx.cfg.labelerDid,
uri: profile.uri,
cid: profile.cid,
val: label,
neg: 0,
cts: new Date().toISOString(),
},
])
.execute()
const labelSrvc = ctx.services.label(ctx.db.getPrimary())
await labelSrvc.createLabels([
{
// @ts-ignore
src: ctx.cfg.labelerDid,
uri: profile.uri,
cid: profile.cid,
val: label,
neg: false,
cts: new Date().toISOString(),
},
])
}
async labelPost(label: string, {uri, cid}: {uri: string; cid: string}) {
const ctx = this.pds.ctx
const ctx = this.bsky.ctx
if (!ctx) {
throw new Error('Invalid PDS')
throw new Error('Invalid appview')
}
await ctx.db.db
.insertInto('label')
.values([
{
src: ctx.cfg.labelerDid,
uri,
cid,
val: label,
neg: 0,
cts: new Date().toISOString(),
},
])
.execute()
const labelSrvc = ctx.services.label(ctx.db.getPrimary())
await labelSrvc.createLabels([
{
// @ts-ignore
src: ctx.cfg.labelerDid,
uri,
cid,
val: label,
neg: false,
cts: new Date().toISOString(),
},
])
}
async createMuteList(user: string, name: string): Promise<string> {
@@ -393,3 +483,15 @@ async function getPort(start = 3000) {
}
throw new Error('Unable to find an available port')
}
export const mockTwilio = (pds: TestPds) => {
if (!pds.ctx.phoneVerifier) return
pds.ctx.phoneVerifier.sendCode = async (_number: string) => {
// do nothing
}
pds.ctx.phoneVerifier.verifyCode = async (_number: string, code: string) => {
return code === '000000'
}
}