Files
openccb/scripts/smoke_audio_roles.sh
T

243 lines
8.6 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
DB_CONTAINER="openccb-db"
DB_NAME="openccb_lms"
DB_USER="user"
API_RUNNER_CONTAINER="${API_RUNNER_CONTAINER:-openccb-studio}"
INTERNAL_BASE_URL="${INTERNAL_BASE_URL:-http://experience:3002}"
JWT_SECRET="${JWT_SECRET:-}"
if [[ -z "$JWT_SECRET" ]]; then
if [[ -f .env ]]; then
JWT_SECRET="$(grep -E '^JWT_SECRET=' .env | head -n1 | cut -d'=' -f2-)"
fi
fi
if [[ -z "$JWT_SECRET" ]]; then
echo "ERROR: JWT_SECRET not found (env or .env)"
exit 1
fi
if ! command -v jq >/dev/null 2>&1; then
echo "ERROR: jq is required"
exit 1
fi
if ! command -v openssl >/dev/null 2>&1; then
echo "ERROR: openssl is required"
exit 1
fi
if ! command -v docker >/dev/null 2>&1; then
echo "ERROR: docker is required"
exit 1
fi
b64url() {
openssl base64 -A | tr '+/' '-_' | tr -d '='
}
make_jwt() {
local user_id="$1"
local org_id="$2"
local role="$3"
local now exp header payload header_b64 payload_b64 signature
now="$(date +%s)"
exp="$((now + 86400))"
header='{"alg":"HS256","typ":"JWT"}'
payload="$(jq -cn \
--arg sub "$user_id" \
--arg org "$org_id" \
--arg role "$role" \
--argjson exp "$exp" \
'{sub:$sub,org:$org,exp:$exp,role:$role,course_id:null,token_type:"access"}')"
header_b64="$(printf '%s' "$header" | b64url)"
payload_b64="$(printf '%s' "$payload" | b64url)"
signature="$(printf '%s' "${header_b64}.${payload_b64}" | openssl dgst -sha256 -hmac "$JWT_SECRET" -binary | b64url)"
printf '%s.%s.%s' "$header_b64" "$payload_b64" "$signature"
}
run_sql() {
local sql="$1"
docker exec -i "$DB_CONTAINER" psql -U "$DB_USER" -d "$DB_NAME" -v ON_ERROR_STOP=1 -t -A -c "$sql"
}
check_http() {
local label="$1"
local expected="$2"
local token="$3"
local path="$4"
local method="${5:-GET}"
local body="${6:-}"
local response
response="$(docker exec \
-e TARGET_URL="${INTERNAL_BASE_URL}${path}" \
-e METHOD="$method" \
-e TOKEN="$token" \
-e ORG_ID="$ORG_ID" \
-e REQ_BODY="$body" \
"$API_RUNNER_CONTAINER" \
node -e "
const url = process.env.TARGET_URL;
const method = process.env.METHOD || 'GET';
const token = process.env.TOKEN;
const orgId = process.env.ORG_ID;
const body = process.env.REQ_BODY || '';
const headers = {
'Authorization': 'Bearer ' + token,
'X-Organization-Id': orgId,
};
const init = { method, headers };
if (body) {
headers['Content-Type'] = 'application/json';
init.body = body;
}
fetch(url, init)
.then(async (res) => {
const text = await res.text();
process.stdout.write(String(res.status) + '\n');
process.stdout.write(text);
})
.catch((err) => {
console.error('FETCH_ERROR:' + err.message);
process.exit(2);
});
")"
local code
code="$(printf '%s' "$response" | head -n1)"
local response_body
response_body="$(printf '%s' "$response" | tail -n +2)"
local ok="0"
IFS='|' read -r -a expected_codes <<< "$expected"
for ec in "${expected_codes[@]}"; do
if [[ "$code" == "$ec" ]]; then
ok="1"
break
fi
done
if [[ "$ok" == "1" ]]; then
echo "PASS [$label] -> HTTP $code"
else
echo "FAIL [$label] -> expected $expected, got $code"
echo "Response body:"
printf '%s\n' "$response_body"
return 1
fi
}
cleanup() {
if [[ "${KEEP_FIXTURES:-0}" == "1" ]]; then
echo "KEEP_FIXTURES=1 -> skipping cleanup for debugging"
return
fi
run_sql "DELETE FROM audio_responses WHERE id IN ('${RESP_A_ID}', '${RESP_B_ID}');" >/dev/null || true
run_sql "DELETE FROM course_instructors WHERE course_id IN ('${COURSE_A_ID}', '${COURSE_B_ID}') AND user_id='${INSTRUCTOR_ID}';" >/dev/null || true
run_sql "DELETE FROM lessons WHERE id IN ('${LESSON_A_ID}', '${LESSON_B_ID}');" >/dev/null || true
run_sql "DELETE FROM modules WHERE id IN ('${MODULE_A_ID}', '${MODULE_B_ID}');" >/dev/null || true
run_sql "DELETE FROM courses WHERE id IN ('${COURSE_A_ID}', '${COURSE_B_ID}');" >/dev/null || true
run_sql "DELETE FROM users WHERE id IN ('${ADMIN_ID}', '${INSTRUCTOR_ID}', '${STUDENT_OWNER_ID}', '${STUDENT_OTHER_ID}');" >/dev/null || true
}
trap cleanup EXIT
ORG_ID="00000000-0000-0000-0000-000000000001"
ADMIN_ID="$(cat /proc/sys/kernel/random/uuid)"
INSTRUCTOR_ID="$(cat /proc/sys/kernel/random/uuid)"
STUDENT_OWNER_ID="$(cat /proc/sys/kernel/random/uuid)"
STUDENT_OTHER_ID="$(cat /proc/sys/kernel/random/uuid)"
COURSE_A_ID="$(cat /proc/sys/kernel/random/uuid)"
COURSE_B_ID="$(cat /proc/sys/kernel/random/uuid)"
MODULE_A_ID="$(cat /proc/sys/kernel/random/uuid)"
MODULE_B_ID="$(cat /proc/sys/kernel/random/uuid)"
LESSON_A_ID="$(cat /proc/sys/kernel/random/uuid)"
LESSON_B_ID="$(cat /proc/sys/kernel/random/uuid)"
BLOCK_A_ID="$(cat /proc/sys/kernel/random/uuid)"
BLOCK_B_ID="$(cat /proc/sys/kernel/random/uuid)"
RESP_A_ID="$(cat /proc/sys/kernel/random/uuid)"
RESP_B_ID="$(cat /proc/sys/kernel/random/uuid)"
SUFFIX="$(date +%s)"
echo "Creating temporary LMS fixtures..."
echo "ORG_ID=${ORG_ID}"
echo "RESP_A_ID=${RESP_A_ID}"
echo "RESP_B_ID=${RESP_B_ID}"
run_sql "
INSERT INTO organizations (id, name)
VALUES ('${ORG_ID}', 'OpenCCB Default Org')
ON CONFLICT (id) DO NOTHING;
INSERT INTO users (id, email, password_hash, full_name, role, organization_id)
VALUES
('${ADMIN_ID}', 'smoke-admin-${SUFFIX}@local.test', 'x', 'Smoke Admin', 'admin', '${ORG_ID}'),
('${INSTRUCTOR_ID}', 'smoke-instructor-${SUFFIX}@local.test', 'x', 'Smoke Instructor', 'instructor', '${ORG_ID}'),
('${STUDENT_OWNER_ID}', 'smoke-student-owner-${SUFFIX}@local.test', 'x', 'Smoke Student Owner', 'student', '${ORG_ID}'),
('${STUDENT_OTHER_ID}', 'smoke-student-other-${SUFFIX}@local.test', 'x', 'Smoke Student Other', 'student', '${ORG_ID}');
INSERT INTO courses (id, title, description, instructor_id, organization_id)
VALUES
('${COURSE_A_ID}', 'Smoke Course A', 'Course with instructor access', '${INSTRUCTOR_ID}', '${ORG_ID}'),
('${COURSE_B_ID}', 'Smoke Course B', 'Course without instructor access', '${ADMIN_ID}', '${ORG_ID}');
INSERT INTO modules (id, course_id, title, position, organization_id)
VALUES
('${MODULE_A_ID}', '${COURSE_A_ID}', 'Module A', 1, '${ORG_ID}'),
('${MODULE_B_ID}', '${COURSE_B_ID}', 'Module B', 1, '${ORG_ID}');
INSERT INTO lessons (id, module_id, title, content_type, position, organization_id)
VALUES
('${LESSON_A_ID}', '${MODULE_A_ID}', 'Lesson A', 'video', 1, '${ORG_ID}'),
('${LESSON_B_ID}', '${MODULE_B_ID}', 'Lesson B', 'video', 1, '${ORG_ID}');
INSERT INTO course_instructors (organization_id, course_id, user_id, role)
VALUES
('${ORG_ID}', '${COURSE_A_ID}', '${INSTRUCTOR_ID}', 'instructor');
INSERT INTO audio_responses (
id, organization_id, user_id, course_id, lesson_id, block_id, prompt,
transcript, audio_data, ai_score, ai_found_keywords, ai_feedback, ai_evaluated_at,
status, attempt_number, duration_seconds
)
VALUES
(
'${RESP_A_ID}', '${ORG_ID}', '${STUDENT_OWNER_ID}', '${COURSE_A_ID}', '${LESSON_A_ID}', '${BLOCK_A_ID}',
'Prompt A', 'Transcript A', convert_to('aGVsbG8=', 'UTF8'), 80, ARRAY['keyword'], 'good', now(),
'ai_evaluated', 1, 12
),
(
'${RESP_B_ID}', '${ORG_ID}', '${STUDENT_OWNER_ID}', '${COURSE_B_ID}', '${LESSON_B_ID}', '${BLOCK_B_ID}',
'Prompt B', 'Transcript B', convert_to('aGVsbG8=', 'UTF8'), 75, ARRAY['keyword'], 'pending review', now(),
'pending', 1, 11
);
" >/dev/null
echo "Generating role tokens..."
ADMIN_TOKEN="$(make_jwt "$ADMIN_ID" "$ORG_ID" "admin")"
INSTRUCTOR_TOKEN="$(make_jwt "$INSTRUCTOR_ID" "$ORG_ID" "instructor")"
OWNER_STUDENT_TOKEN="$(make_jwt "$STUDENT_OWNER_ID" "$ORG_ID" "student")"
OTHER_STUDENT_TOKEN="$(make_jwt "$STUDENT_OTHER_ID" "$ORG_ID" "student")"
echo "Running role-based smoke checks..."
check_http "admin pending_instructor list" "200" "$ADMIN_TOKEN" "/audio-responses?status=pending_instructor"
check_http "instructor list scoped" "200" "$INSTRUCTOR_TOKEN" "/audio-responses"
check_http "instructor forbidden detail out-of-course" "403|404" "$INSTRUCTOR_TOKEN" "/audio-responses/${RESP_B_ID}"
check_http "owner student audio access" "200" "$OWNER_STUDENT_TOKEN" "/audio-responses/${RESP_A_ID}/audio"
check_http "other student denied audio access" "403" "$OTHER_STUDENT_TOKEN" "/audio-responses/${RESP_A_ID}/audio"
check_http "instructor evaluate in-course" "200" "$INSTRUCTOR_TOKEN" "/audio-responses/${RESP_A_ID}/evaluate" "POST" '{"teacher_score":90,"teacher_feedback":"ok"}'
check_http "instructor denied evaluate out-of-course" "403" "$INSTRUCTOR_TOKEN" "/audio-responses/${RESP_B_ID}/evaluate" "POST" '{"teacher_score":70,"teacher_feedback":"n/a"}'
check_http "admin stats course A" "200" "$ADMIN_TOKEN" "/courses/${COURSE_A_ID}/audio-responses/stats"
check_http "instructor denied stats course B" "403" "$INSTRUCTOR_TOKEN" "/courses/${COURSE_B_ID}/audio-responses/stats"
echo "All smoke checks passed."