You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

578 lines
24 KiB

"""
面试模块黑盒验收脚本
使用新账号,验证码从数据库读取(模拟后端日志行为)
"""
import requests
import json
import time
import sys
import pymysql
import datetime
import os
BASE = "http://127.0.0.1:8080/api/v1"
HEADERS = {"Content-Type": "application/json"}
# 数据库连接
def get_db():
return pymysql.connect(host='127.0.0.1', port=3307,
user=os.environ.get('DB_USERNAME', 'mydreamjob'),
password=os.environ['DB_PASSWORD'],
database='mydreamjob')
def get_db_code(phone, purpose="login"):
conn = get_db()
cur = conn.cursor()
cur.execute(
"SELECT code FROM sms_verification_codes WHERE phone=%s AND purpose=%s AND used=0 ORDER BY created_at DESC LIMIT 1",
(phone, purpose))
row = cur.fetchone()
conn.close()
return row[0] if row else None
results = []
passed = 0
failed = 0
def test(name, fn):
global passed, failed
try:
fn()
print(f" PASS: {name}")
results.append((name, "PASS", ""))
passed += 1
except AssertionError as e:
print(f" FAIL: {name} - {e}")
results.append((name, "FAIL", str(e)))
failed += 1
except Exception as e:
print(f" FAIL: {name} - {type(e).__name__}: {e}")
results.append((name, "FAIL", f"{type(e).__name__}: {e}"))
failed += 1
def send_code(phone):
r = requests.post(f"{BASE}/auth/sms/send?phone={phone}&purpose=login", headers=HEADERS)
if r.status_code != 200:
raise Exception(f"Send code failed: {r.status_code} {r.text}")
time.sleep(0.5)
code = get_db_code(phone, "login")
if not code:
raise Exception(f"No code found for {phone}")
return code
def login(phone, role):
code = send_code(phone)
r = requests.post(f"{BASE}/auth/login", json={
"phone": phone, "code": code
}, headers=HEADERS)
if r.status_code != 200:
raise Exception(f"Login failed: {r.status_code} {r.text}")
user_id = r.json()["data"]["userId"]
# Set role in DB (mock login always creates STUDENT)
if role == "COMPANY":
conn = get_db()
cur = conn.cursor()
cur.execute(
"INSERT INTO user_roles (user_id, role) VALUES (%s, 'COMPANY') ON DUPLICATE KEY UPDATE role=role", (user_id,))
cur.execute(
"UPDATE users SET role = 'COMPANY' WHERE id = %s", (user_id,))
conn.commit()
conn.close()
print(f" Set user {user_id} to COMPANY role")
return r.json()["data"]["accessToken"]
def auth_header(token):
return {"Content-Type": "application/json", "Authorization": f"Bearer {token}"}
# ── Setup: 创建新账号 ──
print("=== Setup: New Accounts ===")
ts = int(time.time())
# 企业账号
company_phone = f"138{ts % 100000000:08d}"
print(f" Company: {company_phone}")
company_token = login(company_phone, "COMPANY")
print(f" Company token: {company_token[:20]}...")
# 求职者账号
student_phone = f"139{ts % 100000000:08d}"
print(f" Student: {student_phone}")
student_token = login(student_phone, "STUDENT")
print(f" Student token: {student_token[:20]}...")
# 第三方账号(非参与者)
third_phone = f"150{ts % 100000000:08d}"
print(f" Third party: {third_phone}")
third_token = login(third_phone, "STUDENT")
print(f" Third token: {third_token[:20]}...")
# 其他企业
other_company_phone = f"151{ts % 100000000:08d}"
print(f" Other company: {other_company_phone}")
other_company_token = login(other_company_phone, "COMPANY")
print(f" Other company token: {other_company_token[:20]}...")
# ── 创建公司资料 ──
print("=== Company Profile ===")
r = requests.post(f"{BASE}/companies/me", json={
"name": "黑盒测试科技有限公司",
"description": "面试模块黑盒验收测试企业",
"industry": "互联网"
}, headers=auth_header(company_token))
if r.status_code != 200:
print(f" Create company warning: {r.status_code} {r.text}")
else:
print(f" Company created: {r.json()['data']['name']}")
# ── 创建其他企业资料 ──
r = requests.post(f"{BASE}/companies/me", json={
"name": "其他黑盒测试企业",
"description": "其他企业",
"industry": "金融"
}, headers=auth_header(other_company_token))
print(f" Other company profile: {r.status_code}")
# ── 创建学生资料 ──
print("=== Student Profile ===")
r = requests.put(f"{BASE}/profile/me", json={
"realName": "黑盒测试学生",
"education": "本科"
}, headers=auth_header(student_token))
print(f" Student profile: {r.status_code}")
# ── 创建岗位并投递 ──
print("=== Create Job ===")
r = requests.post(f"{BASE}/jobs", json={
"title": "黑盒测试岗位",
"description": "面试模块黑盒验收测试岗位",
"city": "北京",
"employmentType": "FULL_TIME",
"salaryMin": 10000,
"salaryMax": 20000
}, headers=auth_header(company_token))
assert r.status_code == 200, f"Create job failed: {r.status_code} {r.text}"
job_id = r.json()["data"]["id"]
print(f" Job ID: {job_id}")
# 发布岗位 (DRAFT -> PUBLISHED)
print("=== Publish Job ===")
r = requests.patch(f"{BASE}/jobs/{job_id}/status", json={"status": "PUBLISHED"},
headers=auth_header(company_token))
if r.status_code != 200:
print(f" Publish warning: {r.status_code} {r.text}")
else:
print(f" Job published: {r.json()['data']['status']}")
# 投递
print("=== Create Application ===")
r = requests.post(f"{BASE}/applications", json={"jobId": job_id},
headers=auth_header(student_token))
if r.status_code != 200:
print(f" Create application failed: {r.status_code} {r.text}")
# Try listing applications to see if there's already one
r2 = requests.get(f"{BASE}/applications/me", headers=auth_header(student_token))
print(f" My apps: {r2.status_code}")
if r2.status_code == 200:
data = r2.json()["data"]
if data["total"] > 0:
application_id = data["items"][0]["id"]
print(f" Using existing application: {application_id}")
else:
print(" No existing application, exiting")
sys.exit(1)
else:
sys.exit(1)
else:
application_id = r.json()["data"]["id"]
print(f" Application ID: {application_id}")
# ── 黑盒测试 ──
print("\n=== Black-box Tests ===")
interview_id = None
# 1. 企业创建面试
def test_01():
global interview_id
future = (datetime.datetime.now() + datetime.timedelta(days=7)).strftime("%Y-%m-%dT14:00:00")
future_end = (datetime.datetime.now() + datetime.timedelta(days=7)).strftime("%Y-%m-%dT15:00:00")
r = requests.post(f"{BASE}/applications/{application_id}/interviews", json={
"title": "技术面试",
"interviewType": "ONLINE",
"startAt": future,
"endAt": future_end,
"meetingUrl": "https://meet.example.com/blackbox"
}, headers=auth_header(company_token))
assert r.status_code == 200, f"Expected 200, got {r.status_code}: {r.text}"
d = r.json()
assert d["data"]["status"] == "SCHEDULED", f"Expected SCHEDULED, got {d['data']['status']}"
interview_id = d["data"]["id"]
print(f" Interview ID: {interview_id}")
test("1. 企业创建面试", test_01)
# 2. 企业查询该投递的面试列表
def test_02():
r = requests.get(f"{BASE}/applications/{application_id}/interviews",
headers=auth_header(company_token))
assert r.status_code == 200, f"Expected 200, got {r.status_code}: {r.text}"
data = r.json()["data"]
assert len(data) > 0, "Expected at least 1 interview"
assert data[0]["id"] == interview_id
test("2. 企业查询投递面试列表", test_02)
# 3. 求职者查询自己的面试
def test_03():
r = requests.get(f"{BASE}/interviews/me", headers=auth_header(student_token))
assert r.status_code == 200, f"Expected 200, got {r.status_code}: {r.text}"
assert r.json()["data"]["total"] > 0, "Expected at least 1 interview"
test("3. 求职者查询自己的面试", test_03)
# 4. 双方查看面试详情
def test_04():
r1 = requests.get(f"{BASE}/interviews/{interview_id}", headers=auth_header(company_token))
assert r1.status_code == 200, f"Company view failed: {r1.status_code}"
r2 = requests.get(f"{BASE}/interviews/{interview_id}", headers=auth_header(student_token))
assert r2.status_code == 200, f"Student view failed: {r2.status_code}"
assert r1.json()["data"]["status"] == "SCHEDULED"
test("4. 双方查看面试详情", test_04)
# 5. 企业修改 SCHEDULED 面试
def test_05():
new_future = (datetime.datetime.now() + datetime.timedelta(days=14)).strftime("%Y-%m-%dT10:00:00")
new_end = (datetime.datetime.now() + datetime.timedelta(days=14)).strftime("%Y-%m-%dT11:00:00")
r = requests.put(f"{BASE}/interviews/{interview_id}", json={
"title": "技术面试(已修改)",
"startAt": new_future,
"endAt": new_end
}, headers=auth_header(company_token))
assert r.status_code == 200, f"Expected 200, got {r.status_code}: {r.text}"
assert r.json()["data"]["title"] == "技术面试(已修改)"
test("5. 企业修改SCHEDULED面试", test_05)
# 6. 求职者 CONFIRMED
def test_06():
r = requests.post(f"{BASE}/interviews/{interview_id}/respond", json={
"action": "CONFIRM", "reason": "时间可以接受"
}, headers=auth_header(student_token))
assert r.status_code == 200, f"Expected 200, got {r.status_code}: {r.text}"
assert r.json()["data"]["status"] == "CONFIRMED"
test("6. 求职者CONFIRMED", test_06)
# 7. 求职者 DECLINED (需要新面试)
def test_07():
future = (datetime.datetime.now() + datetime.timedelta(days=21)).strftime("%Y-%m-%dT14:00:00")
future_end = (datetime.datetime.now() + datetime.timedelta(days=21)).strftime("%Y-%m-%dT15:00:00")
r = requests.post(f"{BASE}/applications/{application_id}/interviews", json={
"title": "拒绝测试面试",
"interviewType": "ONLINE",
"startAt": future,
"endAt": future_end,
"meetingUrl": "https://meet.example.com/decline"
}, headers=auth_header(company_token))
assert r.status_code == 200, f"Create failed: {r.status_code}: {r.text}"
iid = r.json()["data"]["id"]
r2 = requests.post(f"{BASE}/interviews/{iid}/respond", json={
"action": "DECLINE", "reason": "时间不合适"
}, headers=auth_header(student_token))
assert r2.status_code == 200, f"Expected 200, got {r2.status_code}: {r2.text}"
assert r2.json()["data"]["status"] == "DECLINED"
test("7. 求职者DECLINED", test_07)
# 8. 求职者 RESCHEDULE_REQUESTED (需要新面试)
def test_08():
future = (datetime.datetime.now() + datetime.timedelta(days=28)).strftime("%Y-%m-%dT14:00:00")
future_end = (datetime.datetime.now() + datetime.timedelta(days=28)).strftime("%Y-%m-%dT15:00:00")
r = requests.post(f"{BASE}/applications/{application_id}/interviews", json={
"title": "改期请求测试",
"interviewType": "ONLINE",
"startAt": future,
"endAt": future_end,
"meetingUrl": "https://meet.example.com/reschedule"
}, headers=auth_header(company_token))
assert r.status_code == 200, f"Create failed: {r.status_code}: {r.text}"
iid = r.json()["data"]["id"]
r2 = requests.post(f"{BASE}/interviews/{iid}/respond", json={
"action": "REQUEST_RESCHEDULE", "reason": "希望改到下周"
}, headers=auth_header(student_token))
assert r2.status_code == 200, f"Expected 200, got {r2.status_code}: {r2.text}"
assert r2.json()["data"]["status"] == "RESCHEDULE_REQUESTED"
test("8. 求职者RESCHEDULE_REQUESTED", test_08)
# 9. 企业 CANCEL (需要新面试)
def test_09():
future = (datetime.datetime.now() + datetime.timedelta(days=35)).strftime("%Y-%m-%dT14:00:00")
future_end = (datetime.datetime.now() + datetime.timedelta(days=35)).strftime("%Y-%m-%dT15:00:00")
r = requests.post(f"{BASE}/applications/{application_id}/interviews", json={
"title": "取消测试面试",
"interviewType": "ONLINE",
"startAt": future,
"endAt": future_end,
"meetingUrl": "https://meet.example.com/cancel"
}, headers=auth_header(company_token))
assert r.status_code == 200, f"Create failed: {r.status_code}: {r.text}"
iid = r.json()["data"]["id"]
r2 = requests.post(f"{BASE}/interviews/{iid}/cancel", json={
"reason": "岗位已招满"
}, headers=auth_header(company_token))
assert r2.status_code == 200, f"Expected 200, got {r2.status_code}: {r2.text}"
assert r2.json()["data"]["status"] == "CANCELLED"
test("9. 企业CANCEL", test_09)
# 10. 企业 COMPLETE - 需要 CONFIRMED 且已过 endAt
def test_10():
# 创建一个很快结束的面试
soon_start = (datetime.datetime.now() + datetime.timedelta(minutes=1)).strftime("%Y-%m-%dT%H:%M:%S")
soon_end = (datetime.datetime.now() + datetime.timedelta(minutes=2)).strftime("%Y-%m-%dT%H:%M:%S")
r = requests.post(f"{BASE}/applications/{application_id}/interviews", json={
"title": "完成测试面试",
"interviewType": "ONLINE",
"startAt": soon_start,
"endAt": soon_end,
"meetingUrl": "https://meet.example.com/complete"
}, headers=auth_header(company_token))
assert r.status_code == 200, f"Create failed: {r.status_code}: {r.text}"
iid = r.json()["data"]["id"]
# 确认
r2 = requests.post(f"{BASE}/interviews/{iid}/respond", json={
"action": "CONFIRM"
}, headers=auth_header(student_token))
assert r2.status_code == 200, f"Confirm failed: {r2.status_code}: {r2.text}"
# 等待面试结束
print(f" Waiting for interview to end...")
time.sleep(130) # wait 130 seconds (2+ minutes)
# 完成
r3 = requests.post(f"{BASE}/interviews/{iid}/complete", json={
"notes": "面试表现优秀"
}, headers=auth_header(company_token))
assert r3.status_code == 200, f"Expected 200, got {r3.status_code}: {r3.text}"
assert r3.json()["data"]["status"] == "COMPLETED"
test("10. 企业COMPLETE", test_10)
# 11. 查询 timeline
def test_11():
r = requests.get(f"{BASE}/interviews/{interview_id}/timeline",
headers=auth_header(company_token))
assert r.status_code == 200, f"Expected 200, got {r.status_code}: {r.text}"
data = r.json()["data"]
assert len(data) > 0, "Expected at least 1 timeline entry"
# Check history entries
statuses = [e["toStatus"] for e in data]
assert "SCHEDULED" in statuses, "Timeline should contain SCHEDULED"
assert "CONFIRMED" in statuses, "Timeline should contain CONFIRMED"
test("11. 查询timeline", test_11)
# 12. 确认通知发送 - check notification queue
def test_12():
# 检查 notification 队列是否有消息(通过 RabbitMQ API)
# 注意:消息可能已被消费,所以这里只检查队列存在
r = requests.get("http://127.0.0.1:15672/api/queues/%2F/queue.notification",
auth=("mydreamjob_dev", "Ch8mP@ssw0rd_2024!"))
assert r.status_code == 200, f"Queue check failed: {r.status_code}"
print(f" Notification queue messages: {r.json().get('messages', 'N/A')}")
test("12. 通知队列存在", test_12)
# 13. 幂等测试 - 重复操作
def test_13():
future = (datetime.datetime.now() + datetime.timedelta(days=42)).strftime("%Y-%m-%dT14:00:00")
future_end = (datetime.datetime.now() + datetime.timedelta(days=42)).strftime("%Y-%m-%dT15:00:00")
r = requests.post(f"{BASE}/applications/{application_id}/interviews", json={
"title": "幂等测试面试",
"interviewType": "ONLINE",
"startAt": future,
"endAt": future_end,
"meetingUrl": "https://meet.example.com/idempotent"
}, headers=auth_header(company_token))
assert r.status_code == 200, f"Create failed: {r.status_code}: {r.text}"
iid = r.json()["data"]["id"]
# 重复取消
r2 = requests.post(f"{BASE}/interviews/{iid}/cancel", json={"reason": "第一次取消"},
headers=auth_header(company_token))
assert r2.status_code == 200, f"First cancel: {r2.status_code}: {r2.text}"
r3 = requests.post(f"{BASE}/interviews/{iid}/cancel", json={"reason": "第二次取消"},
headers=auth_header(company_token))
assert r3.status_code == 200, f"Idempotent cancel: {r3.status_code}: {r3.text}"
assert r3.json()["data"]["status"] == "CANCELLED"
# 重复确认
future2 = (datetime.datetime.now() + datetime.timedelta(days=43)).strftime("%Y-%m-%dT14:00:00")
future_end2 = (datetime.datetime.now() + datetime.timedelta(days=43)).strftime("%Y-%m-%dT15:00:00")
r4 = requests.post(f"{BASE}/applications/{application_id}/interviews", json={
"title": "幂等确认测试",
"interviewType": "ONLINE",
"startAt": future2,
"endAt": future_end2,
"meetingUrl": "https://meet.example.com/idem2"
}, headers=auth_header(company_token))
assert r4.status_code == 200
iid2 = r4.json()["data"]["id"]
r5 = requests.post(f"{BASE}/interviews/{iid2}/respond", json={"action": "CONFIRM"},
headers=auth_header(student_token))
assert r5.status_code == 200
r6 = requests.post(f"{BASE}/interviews/{iid2}/respond", json={"action": "CONFIRM"},
headers=auth_header(student_token))
assert r6.status_code == 200, f"Idempotent confirm: {r6.status_code}: {r6.text}"
assert r6.json()["data"]["status"] == "CONFIRMED"
test("13. 幂等测试(重复取消/确认返回200)", test_13)
# 14. 非参与者访问 403
def test_14():
r = requests.get(f"{BASE}/interviews/{interview_id}", headers=auth_header(third_token))
assert r.status_code == 403, f"Expected 403, got {r.status_code}: {r.text}"
test("14. 非参与者访问返回403", test_14)
# 15. 其他企业修改 403
def test_15():
# 其他企业尝试创建面试(不属于他的投递)
future = (datetime.datetime.now() + datetime.timedelta(days=50)).strftime("%Y-%m-%dT14:00:00")
future_end = (datetime.datetime.now() + datetime.timedelta(days=50)).strftime("%Y-%m-%dT15:00:00")
r = requests.post(f"{BASE}/applications/{application_id}/interviews", json={
"title": "越权测试",
"interviewType": "ONLINE",
"startAt": future,
"endAt": future_end,
"meetingUrl": "https://meet.example.com/unauth"
}, headers=auth_header(other_company_token))
assert r.status_code == 403, f"Expected 403, got {r.status_code}: {r.text}"
test("15. 其他企业创建返回403", test_15)
# 16. 非法状态流转 409
def test_16():
future = (datetime.datetime.now() + datetime.timedelta(days=56)).strftime("%Y-%m-%dT14:00:00")
future_end = (datetime.datetime.now() + datetime.timedelta(days=56)).strftime("%Y-%m-%dT15:00:00")
r = requests.post(f"{BASE}/applications/{application_id}/interviews", json={
"title": "状态流转测试",
"interviewType": "ONLINE",
"startAt": future,
"endAt": future_end,
"meetingUrl": "https://meet.example.com/flow"
}, headers=auth_header(company_token))
assert r.status_code == 200, f"Create failed: {r.status_code}: {r.text}"
iid = r.json()["data"]["id"]
# 确认
requests.post(f"{BASE}/interviews/{iid}/respond", json={"action": "CONFIRM"},
headers=auth_header(student_token))
# 再次确认 - 应该返回 200 (幂等) 或 409
r2 = requests.post(f"{BASE}/interviews/{iid}/respond", json={"action": "DECLINE"},
headers=auth_header(student_token))
# CONFIRMED -> DECLINE 是非法流转,预期 409
assert r2.status_code == 409, f"Expected 409, got {r2.status_code}: {r2.text}"
test("16. 非法状态流转返回409", test_16)
# 17. 已取消的面试再次修改
def test_17():
future = (datetime.datetime.now() + datetime.timedelta(days=60)).strftime("%Y-%m-%dT14:00:00")
future_end = (datetime.datetime.now() + datetime.timedelta(days=60)).strftime("%Y-%m-%dT15:00:00")
r = requests.post(f"{BASE}/applications/{application_id}/interviews", json={
"title": "修改已取消测试",
"interviewType": "ONLINE",
"startAt": future,
"endAt": future_end,
"meetingUrl": "https://meet.example.com/cancelled"
}, headers=auth_header(company_token))
assert r.status_code == 200, f"Create failed: {r.status_code}: {r.text}"
iid = r.json()["data"]["id"]
requests.post(f"{BASE}/interviews/{iid}/cancel", json={}, headers=auth_header(company_token))
r2 = requests.put(f"{BASE}/interviews/{iid}", json={"title": "修改已取消"},
headers=auth_header(company_token))
assert r2.status_code != 200, f"Expected error, got {r2.status_code}: {r2.text}"
test("17. 已取消面试不能修改", test_17)
# 18. 缺失字段/过去时间/结束时间早于开始时间
def test_18():
future = (datetime.datetime.now() + datetime.timedelta(days=65)).strftime("%Y-%m-%dT14:00:00")
future_end = (datetime.datetime.now() + datetime.timedelta(days=65)).strftime("%Y-%m-%dT15:00:00")
# 空标题
r = requests.post(f"{BASE}/applications/{application_id}/interviews", json={
"title": "",
"interviewType": "ONLINE",
"startAt": future,
"endAt": future_end,
"meetingUrl": "https://meet.example.com/empty"
}, headers=auth_header(company_token))
assert r.status_code == 400, f"Empty title: expected 400, got {r.status_code}: {r.text}"
# 过去时间
r2 = requests.post(f"{BASE}/applications/{application_id}/interviews", json={
"title": "过去时间",
"interviewType": "ONLINE",
"startAt": "2020-01-01T14:00:00",
"endAt": "2020-01-01T15:00:00",
"meetingUrl": "https://meet.example.com/past"
}, headers=auth_header(company_token))
assert r2.status_code != 200, f"Past time: expected error, got {r2.status_code}: {r2.text}"
# 结束时间早于开始时间
r3 = requests.post(f"{BASE}/applications/{application_id}/interviews", json={
"title": "时间错误",
"interviewType": "ONLINE",
"startAt": future_end,
"endAt": future,
"meetingUrl": "https://meet.example.com/timeerr"
}, headers=auth_header(company_token))
assert r3.status_code != 200, f"End before start: expected error, got {r3.status_code}: {r3.text}"
# 缺失必填字段
r4 = requests.post(f"{BASE}/applications/{application_id}/interviews", json={
"title": "缺字段"
}, headers=auth_header(company_token))
assert r4.status_code == 400, f"Missing fields: expected 400, got {r4.status_code}: {r4.text}"
# ONLINE 缺少 meetingUrl
r5 = requests.post(f"{BASE}/applications/{application_id}/interviews", json={
"title": "缺链接",
"interviewType": "ONLINE",
"startAt": future,
"endAt": future_end
}, headers=auth_header(company_token))
assert r5.status_code == 400, f"Missing meetingUrl: expected 400, got {r5.status_code}: {r5.text}"
test("18. 字段校验(缺失/过去/倒序)", test_18)
# 19. 并发更新不能覆盖状态历史
def test_19():
# 验证 version 乐观锁
# 先获取当前 interview 的 version
r = requests.get(f"{BASE}/interviews/{interview_id}", headers=auth_header(company_token))
assert r.status_code == 200
version = r.json()["data"]["version"]
print(f" Current version: {version}")
# 查看 timeline 记录数量
r2 = requests.get(f"{BASE}/interviews/{interview_id}/timeline",
headers=auth_header(company_token))
assert r2.status_code == 200
timeline_count = len(r2.json()["data"])
print(f" Timeline entries: {timeline_count}")
# 验证 version 随着操作递增
# 状态历史记录数应该 >= 状态变更次数
assert timeline_count >= 2, f"Expected at least 2 timeline entries, got {timeline_count}"
test("19. 状态历史记录完整", test_19)
# ── Summary ──
print(f"\n=== Results: {passed}/{passed+failed} passed ===")
for name, status, detail in results:
if status == "FAIL":
print(f" FAIL: {name} - {detail}")
sys.exit(0 if failed == 0 else 1)