Bluesky app fork with some witchin' additions 💫

Update testrunner to use new dev-env [WIP] (#1575)

* Update testrunner to use new dev-env

* Fix label testcase

* Vendor the dev-infra scripts from the atproto repo for the dev-env server runner

* Bump detox to fix the ios sim control issue

* Use iphone 15 pro for tests

* Ensure the reminders never trigger during tests

* Skip the shell tests due to a crash bug with detox and the drawer

authored by Paul Frazee and committed by GitHub 0b44af38 aad8d12e

+1 -1
.detoxrc.js
··· 41 41 simulator: { 42 42 type: 'ios.simulator', 43 43 device: { 44 - type: 'iPhone 15', 44 + type: 'iPhone 15 Pro', 45 45 }, 46 46 }, 47 47 attached: {
+3
__e2e__/mock-server.ts
··· 502 502 createdAt: new Date().toISOString(), 503 503 }, 504 504 ) 505 + 506 + // flush caches 507 + await server.mocker.testNet.processAll() 505 508 } 506 509 } 507 510 console.log('Ready')
__e2e__/tests/shell.test.ts __e2e__/tests/shell.test.skip.ts
+92
jest/dev-infra/_common.sh
··· 1 + #!/usr/bin/env sh 2 + 3 + get_container_id() { 4 + local compose_file=$1 5 + local service=$2 6 + if [ -z "${compose_file}" ] || [ -z "${service}" ]; then 7 + echo "usage: get_container_id <compose_file> <service>" 8 + exit 1 9 + fi 10 + 11 + docker compose -f $compose_file ps --format json --status running \ 12 + | jq -r '.[]? | select(.Service == "'${service}'") | .ID' 13 + } 14 + 15 + # Exports all environment variables 16 + export_env() { 17 + export_pg_env 18 + export_redis_env 19 + } 20 + 21 + # Exports postgres environment variables 22 + export_pg_env() { 23 + # Based on creds in compose.yaml 24 + export PGPORT=5433 25 + export PGHOST=localhost 26 + export PGUSER=pg 27 + export PGPASSWORD=password 28 + export PGDATABASE=postgres 29 + export DB_POSTGRES_URL="postgresql://pg:password@127.0.0.1:5433/postgres" 30 + } 31 + 32 + # Exports redis environment variables 33 + export_redis_env() { 34 + export REDIS_HOST="127.0.0.1:6380" 35 + } 36 + 37 + # Main entry point 38 + main() { 39 + # Expect a SERVICES env var to be set with the docker service names 40 + local services=${SERVICES} 41 + 42 + dir=$(dirname $0) 43 + compose_file="${dir}/docker-compose.yaml" 44 + 45 + # whether this particular script started the container(s) 46 + started_container=false 47 + 48 + # trap SIGINT and performs cleanup as necessary, i.e. 49 + # taking down containers if this script started them 50 + trap "on_sigint ${services}" INT 51 + on_sigint() { 52 + local services=$@ 53 + echo # newline 54 + if $started_container; then 55 + docker compose -f $compose_file rm -f --stop --volumes ${services} 56 + fi 57 + exit $? 58 + } 59 + 60 + # check if all services are running already 61 + not_running=false 62 + for service in $services; do 63 + container_id=$(get_container_id $compose_file $service) 64 + if [ -z $container_id ]; then 65 + not_running=true 66 + break 67 + fi 68 + done 69 + 70 + # if any are missing, recreate all services 71 + if $not_running; then 72 + docker compose -f $compose_file up --wait --force-recreate ${services} 73 + started_container=true 74 + else 75 + echo "all services ${services} are already running" 76 + fi 77 + 78 + # setup environment variables and run args 79 + export_env 80 + "$@" 81 + # save return code for later 82 + code=$? 83 + 84 + # performs cleanup as necessary, i.e. taking down containers 85 + # if this script started them 86 + echo # newline 87 + if $started_container; then 88 + docker compose -f $compose_file rm -f --stop --volumes ${services} 89 + fi 90 + 91 + exit ${code} 92 + }
+49
jest/dev-infra/docker-compose.yaml
··· 1 + version: '3.8' 2 + services: 3 + # An ephermerally-stored postgres database for single-use test runs 4 + db_test: &db_test 5 + image: postgres:14.4-alpine 6 + environment: 7 + - POSTGRES_USER=pg 8 + - POSTGRES_PASSWORD=password 9 + ports: 10 + - '5433:5432' 11 + # Healthcheck ensures db is queryable when `docker-compose up --wait` completes 12 + healthcheck: 13 + test: 'pg_isready -U pg' 14 + interval: 500ms 15 + timeout: 10s 16 + retries: 20 17 + # A persistently-stored postgres database 18 + db: 19 + <<: *db_test 20 + ports: 21 + - '5432:5432' 22 + healthcheck: 23 + disable: true 24 + volumes: 25 + - atp_db:/var/lib/postgresql/data 26 + # An ephermerally-stored redis cache for single-use test runs 27 + redis_test: &redis_test 28 + image: redis:7.0-alpine 29 + ports: 30 + - '6380:6379' 31 + # Healthcheck ensures redis is queryable when `docker-compose up --wait` completes 32 + healthcheck: 33 + test: ['CMD-SHELL', '[ "$$(redis-cli ping)" = "PONG" ]'] 34 + interval: 500ms 35 + timeout: 10s 36 + retries: 20 37 + # A persistently-stored redis cache 38 + redis: 39 + <<: *redis_test 40 + command: redis-server --save 60 1 --loglevel warning 41 + ports: 42 + - '6379:6379' 43 + healthcheck: 44 + disable: true 45 + volumes: 46 + - atp_redis:/data 47 + volumes: 48 + atp_db: 49 + atp_redis:
+9
jest/dev-infra/with-test-db.sh
··· 1 + #!/usr/bin/env sh 2 + 3 + # Example usage: 4 + # ./with-test-db.sh psql postgresql://pg:password@localhost:5433/postgres -c 'select 1;' 5 + 6 + dir=$(dirname $0) 7 + . ${dir}/_common.sh 8 + 9 + SERVICES="db_test" main "$@"
+10
jest/dev-infra/with-test-redis-and-db.sh
··· 1 + #!/usr/bin/env sh 2 + 3 + # Example usage: 4 + # ./with-test-redis-and-db.sh psql postgresql://pg:password@localhost:5433/postgres -c 'select 1;' 5 + # ./with-test-redis-and-db.sh redis-cli -h localhost -p 6380 ping 6 + 7 + dir=$(dirname $0) 8 + . ${dir}/_common.sh 9 + 10 + SERVICES="db_test redis_test" main "$@"
+94 -60
jest/test-pds.ts
··· 1 1 import net from 'net' 2 2 import path from 'path' 3 3 import fs from 'fs' 4 - import {TestNetworkNoAppView} from '@atproto/dev-env' 4 + import {TestNetwork} from '@atproto/dev-env' 5 5 import {AtUri, BskyAgent} from '@atproto/api' 6 6 7 7 export interface TestUser { ··· 18 18 close: () => Promise<void> 19 19 } 20 20 21 + class StringIdGenerator { 22 + _nextId = [0] 23 + constructor( 24 + public _chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', 25 + ) {} 26 + 27 + next() { 28 + const r = [] 29 + for (const char of this._nextId) { 30 + r.unshift(this._chars[char]) 31 + } 32 + this._increment() 33 + return r.join('') 34 + } 35 + 36 + _increment() { 37 + for (let i = 0; i < this._nextId.length; i++) { 38 + const val = ++this._nextId[i] 39 + if (val >= this._chars.length) { 40 + this._nextId[i] = 0 41 + } else { 42 + return 43 + } 44 + } 45 + this._nextId.push(0) 46 + } 47 + 48 + *[Symbol.iterator]() { 49 + while (true) { 50 + yield this.next() 51 + } 52 + } 53 + } 54 + 55 + const ids = new StringIdGenerator() 56 + 21 57 export async function createServer( 22 58 {inviteRequired}: {inviteRequired: boolean} = {inviteRequired: false}, 23 59 ): Promise<TestPDS> { 24 60 const port = await getPort() 25 61 const port2 = await getPort(port + 1) 26 62 const pdsUrl = `http://localhost:${port}` 27 - const testNet = await TestNetworkNoAppView.create({ 28 - pds: {port, publicUrl: pdsUrl, inviteRequired}, 63 + const id = ids.next() 64 + const testNet = await TestNetwork.create({ 65 + pds: { 66 + port, 67 + publicUrl: pdsUrl, 68 + inviteRequired, 69 + dbPostgresSchema: `pds_${id}`, 70 + }, 71 + bsky: { 72 + dbPostgresSchema: `bsky_${id}`, 73 + }, 29 74 plc: {port: port2}, 30 75 }) 31 76 ··· 48 93 users: Record<string, TestUser> = {} 49 94 50 95 constructor( 51 - public testNet: TestNetworkNoAppView, 96 + public testNet: TestNetwork, 52 97 public service: string, 53 98 public pic: Uint8Array, 54 99 ) { ··· 59 104 return this.testNet.pds 60 105 } 61 106 107 + get bsky() { 108 + return this.testNet.bsky 109 + } 110 + 62 111 get plc() { 63 112 return this.testNet.plc 64 113 } ··· 81 130 const inviteRes = await agent.api.com.atproto.server.createInviteCode( 82 131 {useCount: 1}, 83 132 { 84 - headers: { 85 - authorization: `Basic ${btoa( 86 - `admin:${this.pds.ctx.cfg.adminPassword}`, 87 - )}`, 88 - }, 133 + headers: this.pds.adminAuthHeaders('admin'), 89 134 encoding: 'application/json', 90 135 }, 91 136 ) ··· 260 305 await agent.api.com.atproto.server.createInviteCode( 261 306 {useCount: 1, forAccount}, 262 307 { 263 - headers: { 264 - authorization: `Basic ${btoa( 265 - `admin:${this.pds.ctx.cfg.adminPassword}`, 266 - )}`, 267 - }, 308 + headers: this.pds.adminAuthHeaders('admin'), 268 309 encoding: 'application/json', 269 310 }, 270 311 ) ··· 275 316 if (!did) { 276 317 throw new Error(`Invalid user: ${user}`) 277 318 } 278 - const ctx = this.pds.ctx 319 + const ctx = this.bsky.ctx 279 320 if (!ctx) { 280 - throw new Error('Invalid PDS') 321 + throw new Error('Invalid appview') 281 322 } 282 - 283 - await ctx.db.db 284 - .insertInto('label') 285 - .values([ 286 - { 287 - src: ctx.cfg.labelerDid, 288 - uri: did, 289 - cid: '', 290 - val: label, 291 - neg: 0, 292 - cts: new Date().toISOString(), 293 - }, 294 - ]) 295 - .execute() 323 + const labelSrvc = ctx.services.label(ctx.db.getPrimary()) 324 + await labelSrvc.createLabels([ 325 + { 326 + src: ctx.cfg.labelerDid, 327 + uri: did, 328 + cid: '', 329 + val: label, 330 + neg: false, 331 + cts: new Date().toISOString(), 332 + }, 333 + ]) 296 334 } 297 335 298 336 async labelProfile(label: string, user: string) { ··· 307 345 rkey: 'self', 308 346 }) 309 347 310 - const ctx = this.pds.ctx 348 + const ctx = this.bsky.ctx 311 349 if (!ctx) { 312 - throw new Error('Invalid PDS') 350 + throw new Error('Invalid appview') 313 351 } 314 - await ctx.db.db 315 - .insertInto('label') 316 - .values([ 317 - { 318 - src: ctx.cfg.labelerDid, 319 - uri: profile.uri, 320 - cid: profile.cid, 321 - val: label, 322 - neg: 0, 323 - cts: new Date().toISOString(), 324 - }, 325 - ]) 326 - .execute() 352 + const labelSrvc = ctx.services.label(ctx.db.getPrimary()) 353 + await labelSrvc.createLabels([ 354 + { 355 + src: ctx.cfg.labelerDid, 356 + uri: profile.uri, 357 + cid: profile.cid, 358 + val: label, 359 + neg: false, 360 + cts: new Date().toISOString(), 361 + }, 362 + ]) 327 363 } 328 364 329 365 async labelPost(label: string, {uri, cid}: {uri: string; cid: string}) { 330 - const ctx = this.pds.ctx 366 + const ctx = this.bsky.ctx 331 367 if (!ctx) { 332 - throw new Error('Invalid PDS') 368 + throw new Error('Invalid appview') 333 369 } 334 - await ctx.db.db 335 - .insertInto('label') 336 - .values([ 337 - { 338 - src: ctx.cfg.labelerDid, 339 - uri, 340 - cid, 341 - val: label, 342 - neg: 0, 343 - cts: new Date().toISOString(), 344 - }, 345 - ]) 346 - .execute() 370 + const labelSrvc = ctx.services.label(ctx.db.getPrimary()) 371 + await labelSrvc.createLabels([ 372 + { 373 + src: ctx.cfg.labelerDid, 374 + uri, 375 + cid, 376 + val: label, 377 + neg: false, 378 + cts: new Date().toISOString(), 379 + }, 380 + ]) 347 381 } 348 382 349 383 async createMuteList(user: string, name: string): Promise<string> {
+2 -2
package.json
··· 18 18 "test-coverage": "jest --coverage", 19 19 "lint": "eslint ./src --ext .js,.jsx,.ts,.tsx", 20 20 "typecheck": "tsc --project ./tsconfig.check.json", 21 - "e2e:mock-server": "ts-node __e2e__/mock-server.ts", 21 + "e2e:mock-server": "./jest/dev-infra/with-test-redis-and-db.sh ts-node __e2e__/mock-server.ts", 22 22 "e2e:metro": "RN_SRC_EXT=e2e.ts,e2e.tsx expo run:ios", 23 23 "e2e:build": "detox build -c ios.sim.debug", 24 24 "e2e:run": "detox test --configuration ios.sim.debug --take-screenshots all", ··· 186 186 "babel-loader": "^9.1.2", 187 187 "babel-plugin-module-resolver": "^5.0.0", 188 188 "babel-plugin-react-native-web": "^0.18.12", 189 - "detox": "^20.11.3", 189 + "detox": "^20.13.0", 190 190 "eslint": "^8.19.0", 191 191 "eslint-plugin-detox": "^1.0.0", 192 192 "eslint-plugin-ft-flow": "^2.0.3",
+24
src/state/models/ui/reminders.e2e.ts
··· 1 + import {makeAutoObservable} from 'mobx' 2 + import {RootStoreModel} from '../root-store' 3 + 4 + export class Reminders { 5 + constructor(public rootStore: RootStoreModel) { 6 + makeAutoObservable( 7 + this, 8 + {serialize: false, hydrate: false}, 9 + {autoBind: true}, 10 + ) 11 + } 12 + 13 + serialize() { 14 + return {} 15 + } 16 + 17 + hydrate(_v: unknown) {} 18 + 19 + get shouldRequestEmailConfirmation() { 20 + return false 21 + } 22 + 23 + setEmailConfirmationRequested() {} 24 + }
+4 -4
yarn.lock
··· 8135 8135 address "^1.0.1" 8136 8136 debug "^2.6.0" 8137 8137 8138 - detox@^20.11.3: 8139 - version "20.11.3" 8140 - resolved "https://registry.yarnpkg.com/detox/-/detox-20.11.3.tgz#56d5ea869977f5a747e1be0901b279ab953f8b7b" 8141 - integrity sha512-kdoRAtDLFxXpjt1QlniI+WryMtf7Y8mrZ33Ql8cTR9qoCS/CThi4pweYAQm8yUPqAv1ZtT3eIm3EzRwjEosgLA== 8138 + detox@^20.13.0: 8139 + version "20.13.0" 8140 + resolved "https://registry.yarnpkg.com/detox/-/detox-20.13.0.tgz#923111638dfdb16089eea4f07bf4f0b56468d097" 8141 + integrity sha512-p9MUcoHWFTqSDaoaN+/hnJYdzNYqdelUr/sxzy3zLoS/qehnVJv2yG9pYqz/+gKpJaMIpw2+TVw9imdAx5JpaA== 8142 8142 dependencies: 8143 8143 ajv "^8.6.3" 8144 8144 bunyan "^1.8.12"