pstream-backend/.github/workflows/pull-request-testing.yaml

308 lines
11 KiB
YAML

name: PR Test - Build and Integration Test
on:
pull_request:
branches: [master]
env:
NODE_VERSION: '22'
jobs:
build-and-test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_USER: testuser
POSTGRES_PASSWORD: testpassword
POSTGRES_DB: testdb
ports:
- 5432:5432
options: >-
--health-cmd="pg_isready -U testuser -d testdb"
--health-interval=10s
--health-timeout=5s
--health-retries=5
steps:
- name: Checkout PR
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install dependencies
run: npm install
- name: Generate Prisma client
env:
DATABASE_URL: postgresql://testuser:testpassword@localhost:5432/testdb
run: npx prisma generate
- name: Run database migrations
env:
DATABASE_URL: postgresql://testuser:testpassword@localhost:5432/testdb
run: npx prisma migrate deploy
- name: Build the backend
env:
DATABASE_URL: postgresql://testuser:testpassword@localhost:5432/testdb
META_NAME: 'CI Backend Test server'
META_DESCRIPTION: 'CI Test Instance'
CRYPTO_SECRET: '0GwvbW7ZHlpZ6y0uSkU22Xi7XjoMpHX' # This is just for the CI server. DO NOT USE IN PROD
run: npm run build
- name: Start the backend server
env:
DATABASE_URL: postgresql://testuser:testpassword@localhost:5432/testdb
META_NAME: 'Test Backend'
META_DESCRIPTION: 'CI Test Instance'
CRYPTO_SECRET: '0GwvbW7ZHlpZ6y0uSkU22Xi7XjoMpHX'
run: |
node .output/server/index.mjs &
echo $! > server.pid
# Wait for server to be ready
for i in {1..30}; do
if curl -s http://localhost:3000 > /dev/null; then
echo "Server is ready!"
break
fi
echo "Waiting for server... attempt $i"
sleep 2
done
- name: Verify server is running
run: |
response=$(curl -s http://localhost:3000)
echo "Server response: $response"
if echo "$response" | grep -q "Backend is working"; then
echo "+------------------+------+"
echo "| Backend Working? | True |"
echo "+------------------+------+"
else
echo "+------------------+--------+"
echo "| Backend Working? | FAILED |"
echo "+------------------+--------+"
exit 1
fi
- name: Run account creation integration test
run: |
# Create a Node.js script to handle the crypto operations
cat > /tmp/auth-test.mjs << 'EOF'
import crypto from 'crypto';
import nacl from 'tweetnacl';
const TEST_MNEMONIC = "awkward safe differ large subway junk gallery flight left glue fault glory";
function toBase64Url(buffer) {
return Buffer.from(buffer)
.toString('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/g, '');
}
async function pbkdf2Async(password, salt, iterations, keyLen, digest) {
return new Promise((resolve, reject) => {
crypto.pbkdf2(password, salt, iterations, keyLen, digest, (err, derivedKey) => {
if (err) return reject(err);
resolve(new Uint8Array(derivedKey));
});
});
}
async function main() {
console.log("=== Step 1: Getting challenge code ===");
const challengeRes = await fetch('http://localhost:3000/auth/register/start', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({})
});
const challengeData = await challengeRes.json();
const challengeCode = challengeData.challenge;
if (!challengeCode) {
console.log("+----------------+-----------+");
console.log("| Challenge Code | NOT FOUND |");
console.log("+----------------+-----------+");
process.exit(1);
}
console.log("+----------------+-------+");
console.log("| Challenge Code | FOUND |");
console.log("+----------------+-------+");
console.log(`Challenge: ${challengeCode}`);
console.log("\n=== Step 2: Deriving keypair from mnemonic ===");
const seed = await pbkdf2Async(TEST_MNEMONIC, 'mnemonic', 2048, 32, 'sha256');
const keyPair = nacl.sign.keyPair.fromSeed(seed);
const publicKeyBase64Url = toBase64Url(keyPair.publicKey);
console.log(`Public Key: ${publicKeyBase64Url}`);
console.log("+------------+---------+");
console.log("| Public Key | DERIVED |");
console.log("+------------+---------+");
const messageBuffer = Buffer.from(challengeCode);
const signature = nacl.sign.detached(messageBuffer, keyPair.secretKey);
const signatureBase64Url = toBase64Url(signature);
console.log(`Signature: ${signatureBase64Url}`);
console.log("+------------------+--------+");
console.log("| Challenge Signed | SUCCESS |");
console.log("+------------------+--------+");
const registerRes = await fetch('http://localhost:3000/auth/register/complete', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'User-Agent': 'CI-Test-Agent/1.0'
},
body: JSON.stringify({
publicKey: publicKeyBase64Url,
challenge: {
code: challengeCode,
signature: signatureBase64Url
},
namespace: "ci-test",
device: "CI-Test-Device",
profile: {
colorA: "#FF0000",
colorB: "#00FF00",
icon: "user"
}
})
});
const registerData = await registerRes.json();
if (registerData.user && registerData.token) {
console.log("+---------------+---------+");
console.log("| Registration | SUCCESS |");
console.log("+---------------+---------+");
console.log(`User ID: ${registerData.user.id}`);
console.log(`Nickname: ${registerData.user.nickname}`);
console.log(`Token: ${registerData.token.substring(0, 50)}...`);
const meRes = await fetch('http://localhost:3000/users/@me', {
headers: {
'Authorization': `Bearer ${registerData.token}`
}
});
const meData = await meRes.json();
if (meData.user && meData.user.id === registerData.user.id) {
console.log("+------------------+---------+");
console.log("| Auth Validation | SUCCESS |");
console.log("+------------------+---------+");
} else {
console.log("+------------------+--------+");
console.log("| Auth Validation | FAILED |");
console.log("+------------------+--------+");
process.exit(1);
}
const loginStartRes = await fetch('http://localhost:3000/auth/login/start', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ publicKey: publicKeyBase64Url })
});
const loginStartData = await loginStartRes.json();
if (loginStartData.challenge) {
const loginChallenge = loginStartData.challenge;
const loginSignature = nacl.sign.detached(Buffer.from(loginChallenge), keyPair.secretKey);
const loginSignatureBase64Url = toBase64Url(loginSignature);
const loginCompleteRes = await fetch('http://localhost:3000/auth/login/complete', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'User-Agent': 'CI-Test-Agent/1.0'
},
body: JSON.stringify({
publicKey: publicKeyBase64Url,
challenge: {
code: loginChallenge,
signature: loginSignatureBase64Url
},
device: "CI-Test-Device-Login"
})
});
const loginData = await loginCompleteRes.json();
if (loginData.user && loginData.token) {
console.log("+------------+---------+");
console.log("| Login Flow | SUCCESS |");
console.log("+------------+---------+");
} else {
console.log("+------------+--------+");
console.log("| Login Flow | FAILED |");
console.log("+------------+--------+");
console.log(JSON.stringify(loginData, null, 2));
process.exit(1);
}
}
} else {
console.log("+---------------+--------+");
console.log("| Registration | FAILED |");
console.log("+---------------+--------+");
console.log(JSON.stringify(registerData, null, 2));
process.exit(1);
}
console.log("\n+---------------------------+---------+");
console.log("| All Auth Tests Completed | SUCCESS |");
console.log("+---------------------------+---------+");
}
main().catch(err => {
console.error("Test failed:", err);
process.exit(1);
});
EOF
node /tmp/auth-test.mjs
- name: Run API endpoint tests
run: |
echo "=== Testing /meta endpoint ==="
META_RESPONSE=$(curl -s http://localhost:3000/meta)
echo "Meta response: $META_RESPONSE"
if echo "$META_RESPONSE" | grep -q "name"; then
echo "+---------------+---------+"
echo "| Meta Endpoint | SUCCESS |"
echo "+---------------+---------+"
else
echo "+---------------+--------+"
echo "| Meta Endpoint | FAILED |"
echo "+---------------+--------+"
exit 1
fi
echo ""
echo "+-----------------------------+---------+"
echo "| All Integration Tests | PASSED |"
echo "+-----------------------------+---------+"
- name: Stop the server
if: always()
run: |
if [ -f server.pid ]; then
kill $(cat server.pid) 2>/dev/null || true
fi
- name: Upload build artifacts
if: failure()
uses: actions/upload-artifact@v4
with:
name: build-output
path: .output/
retention-days: 5