feat: 优化打包产物与历史筛选
This commit is contained in:
parent
de33b3d4b2
commit
44405e589d
@ -1,5 +1,6 @@
|
||||
"""任务 API"""
|
||||
import os
|
||||
import json
|
||||
import shutil
|
||||
import tempfile
|
||||
import uuid
|
||||
@ -164,6 +165,25 @@ async def delete_task(task_id: str, db: Session = Depends(get_db)):
|
||||
if task.status in ("running", "pending"):
|
||||
raise HTTPException(status_code=400, detail="任务正在运行中,无法删除")
|
||||
|
||||
# 同一个 App / 版本 / 分支重复打包会覆盖并复用同一个远端文件;
|
||||
# 只要仍有其他历史记录引用该下载地址,就不能删除远端产物。
|
||||
has_shared_artifact = bool(
|
||||
task.oss_url and db.query(Task).filter(
|
||||
Task.id != task.id,
|
||||
Task.oss_url == task.oss_url,
|
||||
).first()
|
||||
)
|
||||
if task.oss_url and not has_shared_artifact:
|
||||
if not task.config_json:
|
||||
raise HTTPException(status_code=400, detail="缺少打包配置快照,无法安全删除远端文件")
|
||||
try:
|
||||
build_config = json.loads(task.config_json)
|
||||
from .config import load_config
|
||||
from ..services.distribution import DistributionError, delete_published_artifacts
|
||||
delete_published_artifacts(build_config, load_config().get("upload", {}))
|
||||
except (json.JSONDecodeError, DistributionError) as exc:
|
||||
raise HTTPException(status_code=502, detail=f"远端产物删除失败,记录未删除:{exc}") from exc
|
||||
|
||||
# 删除日志文件
|
||||
log_path = Path(__file__).parent.parent / "logs" / f"{task_id}.log"
|
||||
if log_path.exists():
|
||||
@ -178,7 +198,10 @@ async def delete_task(task_id: str, db: Session = Depends(get_db)):
|
||||
db.delete(task)
|
||||
db.commit()
|
||||
|
||||
return {"message": "记录已删除"}
|
||||
message = "记录已删除"
|
||||
if task.oss_url:
|
||||
message += "(远端产物已删除)" if not has_shared_artifact else "(远端产物仍被其他记录引用,未删除)"
|
||||
return {"message": message}
|
||||
|
||||
|
||||
@router.get("/{task_id}/log")
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
import json
|
||||
import plistlib
|
||||
import posixpath
|
||||
import re
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from urllib.parse import quote
|
||||
@ -15,7 +16,12 @@ class DistributionError(Exception):
|
||||
|
||||
def _artifact_stem(config: dict) -> str:
|
||||
version = config.get("VERSION", "0").replace(".", "_")
|
||||
return f"{config.get('APPID', 'app')}_{version}"
|
||||
# 历史任务快照没有 SOURCE_BRANCH,继续按旧文件名处理,确保删除旧记录时能清理到原产物。
|
||||
if "SOURCE_BRANCH" not in config:
|
||||
return f"{config.get('APPID', 'app')}_{version}"
|
||||
branch = re.sub(r"[^A-Za-z0-9._-]+", "_", config.get("SOURCE_BRANCH", "main")).strip("._-")
|
||||
build_type = "appstore" if config.get("BUILD_TYPE") == "App_Store" else "adhoc"
|
||||
return f"{config.get('APPID', 'app')}_{version}_{branch or 'main'}_{build_type}"
|
||||
|
||||
|
||||
def _write_distribution_files(config: dict, ipa_path: Path, output_dir: Path) -> tuple[Path, Path, Path]:
|
||||
@ -70,6 +76,60 @@ def _remote_paths(config: dict) -> tuple[str, str, str, str]:
|
||||
)
|
||||
|
||||
|
||||
def _delete_webdav(config: dict, remote_paths: list[str]):
|
||||
webdav = config.get("webdav", {})
|
||||
server_url = webdav.get("server_url", "").rstrip("/")
|
||||
if not server_url or not webdav.get("username"):
|
||||
raise DistributionError("WebDAV 配置不完整")
|
||||
|
||||
base_path = webdav.get("base_path", "/ios-builds").strip("/")
|
||||
auth = (webdav.get("username", ""), webdav.get("password", ""))
|
||||
with httpx.Client(auth=auth, timeout=120, follow_redirects=True) as client:
|
||||
for relative_path in remote_paths:
|
||||
remote_path = posixpath.join(base_path, relative_path)
|
||||
response = client.delete(f"{server_url}/{remote_path}")
|
||||
# 404 代表文件已不存在,可视为清理完成。
|
||||
if response.status_code not in (200, 202, 204, 404):
|
||||
raise DistributionError(f"WebDAV 删除失败: {relative_path} ({response.status_code})")
|
||||
|
||||
|
||||
def _delete_oss(config: dict, remote_paths: list[str]):
|
||||
oss = config.get("oss", {})
|
||||
required = ["access_key_id", "access_key_secret", "endpoint", "bucket_name"]
|
||||
if any(not oss.get(key) for key in required):
|
||||
raise DistributionError("OSS 配置不完整")
|
||||
try:
|
||||
import oss2
|
||||
except ImportError as exc:
|
||||
raise DistributionError("未安装 oss2,请重新执行 ./deploy.sh build") from exc
|
||||
|
||||
bucket = oss2.Bucket(
|
||||
oss2.Auth(oss["access_key_id"], oss["access_key_secret"]),
|
||||
oss["endpoint"],
|
||||
oss["bucket_name"],
|
||||
connect_timeout=30,
|
||||
)
|
||||
for relative_path in remote_paths:
|
||||
result = bucket.delete_object(relative_path)
|
||||
if result.status // 100 != 2:
|
||||
raise DistributionError(f"OSS 删除失败: {relative_path}")
|
||||
|
||||
|
||||
def delete_published_artifacts(build_config: dict, upload_config: dict):
|
||||
"""删除任务对应的远端分发产物。"""
|
||||
mode = upload_config.get("mode", "")
|
||||
if mode not in {"oss", "webdav"}:
|
||||
raise DistributionError("请选择 OSS 或 WebDAV 上传方式")
|
||||
|
||||
ipa_remote, manifest_remote, html_remote, qr_remote = _remote_paths(build_config)
|
||||
remote_paths = [ipa_remote]
|
||||
if build_config.get("BUILD_TYPE") != "App_Store":
|
||||
remote_paths.extend([manifest_remote, html_remote, qr_remote])
|
||||
|
||||
deleter = _delete_oss if mode == "oss" else _delete_webdav
|
||||
deleter(upload_config, remote_paths)
|
||||
|
||||
|
||||
def _ensure_webdav_dirs(client: httpx.Client, server_url: str, remote_path: str):
|
||||
path = ""
|
||||
for segment in remote_path.strip("/").split("/")[:-1]:
|
||||
|
||||
@ -7,9 +7,9 @@ global.fetch = vi.fn()
|
||||
window.open = vi.fn()
|
||||
|
||||
const mockTasks = [
|
||||
{ id: '1', app_name: 'App1', build_type: 'Ad_Hoc', scheme_name: 'readoor31OtherPayLongSchemeName', status: 'completed', created_at: '2024-01-01T10:00:00', dsym_path: '/path/dsym', oss_url: 'https://oss.com/app1.ipa', qr_code_path: '/path/qr.png' },
|
||||
{ id: '2', app_name: 'App2', build_type: 'App_Store', scheme_name: 'sch2', status: 'failed', created_at: '2024-01-02T10:00:00', error_message: '构建失败', error_category: 'compilation' },
|
||||
{ id: '3', app_name: 'App3', build_type: 'Ad_Hoc', scheme_name: 'sch1', status: 'pending', created_at: '2024-01-03T10:00:00' },
|
||||
{ id: '1', app_name: 'App1', build_type: 'Ad_Hoc', scheme_name: 'readoor31OtherPayLongSchemeName', status: 'completed', created_at: '2024-01-01T10:00:00', config_json: '{"VERSION":"2.196.0"}', has_log: true, dsym_path: '/path/dsym', oss_url: 'https://oss.com/app1.ipa', qr_code_path: '/path/qr.png' },
|
||||
{ id: '2', app_name: 'App2', build_type: 'App_Store', scheme_name: 'sch2', status: 'failed', created_at: '2024-01-02T10:00:00', config_json: '{"VERSION":"2.195.0"}', error_message: '构建失败', error_category: 'compilation' },
|
||||
{ id: '3', app_name: 'App3', build_type: 'Ad_Hoc', scheme_name: 'sch1', status: 'pending', created_at: '2024-01-03T10:00:00', config_json: '{"VERSION":"2.196.0"}' },
|
||||
]
|
||||
|
||||
function createMockRouter() {
|
||||
@ -86,7 +86,7 @@ describe('HistoryView.vue', () => {
|
||||
await flushPromises()
|
||||
|
||||
const selects = wrapper.findAll('.filter-select')
|
||||
await selects[0].setValue('Ad_Hoc')
|
||||
await selects[2].setValue('Ad_Hoc')
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
const rows = wrapper.findAll('tbody tr')
|
||||
@ -101,13 +101,31 @@ describe('HistoryView.vue', () => {
|
||||
await flushPromises()
|
||||
|
||||
const selects = wrapper.findAll('.filter-select')
|
||||
await selects[1].setValue('failed')
|
||||
await selects[3].setValue('failed')
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
const rows = wrapper.findAll('tbody tr')
|
||||
expect(rows.length).toBe(1) // App2
|
||||
})
|
||||
|
||||
it('按 App 名称和版本号筛选', async () => {
|
||||
const router = createMockRouter()
|
||||
const wrapper = mount(HistoryView, {
|
||||
global: { plugins: [router] },
|
||||
})
|
||||
await flushPromises()
|
||||
|
||||
const selects = wrapper.findAll('.filter-select')
|
||||
await selects[0].setValue('App1')
|
||||
await wrapper.find('.version-filter').setValue('2.196')
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
const rows = wrapper.findAll('tbody tr')
|
||||
expect(rows.length).toBe(1)
|
||||
expect(rows[0].text()).toContain('App1')
|
||||
expect(rows[0].text()).toContain('2.196.0')
|
||||
})
|
||||
|
||||
it('已完成任务显示下载按钮', async () => {
|
||||
const router = createMockRouter()
|
||||
const wrapper = mount(HistoryView, {
|
||||
@ -120,8 +138,7 @@ describe('HistoryView.vue', () => {
|
||||
const buttons = firstRow.findAll('.action-btn')
|
||||
const buttonTexts = buttons.map(b => b.text())
|
||||
expect(buttonTexts).toContain('dSYM')
|
||||
expect(buttonTexts).toContain('下载')
|
||||
expect(buttonTexts).toContain('二维码')
|
||||
expect(firstRow.find('.qr-thumb').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('空列表显示提示', async () => {
|
||||
@ -148,7 +165,7 @@ describe('HistoryView.vue', () => {
|
||||
expect(wrapper.vm.statusText('failed')).toBe('失败')
|
||||
})
|
||||
|
||||
it('时间仅显示月和日,Scheme 与状态单元格允许完整换行', async () => {
|
||||
it('显示 App 版本号,Scheme 与状态单元格允许完整换行', async () => {
|
||||
const router = createMockRouter()
|
||||
const wrapper = mount(HistoryView, {
|
||||
global: { plugins: [router] },
|
||||
@ -156,13 +173,13 @@ describe('HistoryView.vue', () => {
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.vm.formatTime('2024-07-20T10:00:00')).toBe('7/20')
|
||||
expect(wrapper.findAll('tbody tr')[0].text()).toContain('2.196.0')
|
||||
expect(wrapper.find('.scheme-cell').text()).toBe('readoor31OtherPayLongSchemeName')
|
||||
expect(wrapper.find('.status-cell').text()).toContain('已完成')
|
||||
})
|
||||
|
||||
it('点击查看日志跳转', async () => {
|
||||
it('点击查看日志打开日志弹窗', async () => {
|
||||
const router = createMockRouter()
|
||||
router.push = vi.fn()
|
||||
const wrapper = mount(HistoryView, {
|
||||
global: { plugins: [router] },
|
||||
})
|
||||
@ -171,7 +188,7 @@ describe('HistoryView.vue', () => {
|
||||
const logBtn = wrapper.findAll('.action-btn').find(b => b.text() === '日志')
|
||||
await logBtn.trigger('click')
|
||||
|
||||
expect(router.push).toHaveBeenCalledWith({ path: '/build', query: { taskId: '1' } })
|
||||
expect(wrapper.find('.log-modal').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('点击二维码弹出弹窗', async () => {
|
||||
@ -181,11 +198,10 @@ describe('HistoryView.vue', () => {
|
||||
})
|
||||
await flushPromises()
|
||||
|
||||
const qrBtn = wrapper.findAll('.action-btn').find(b => b.text() === '二维码')
|
||||
await qrBtn.trigger('click')
|
||||
await wrapper.find('.qr-thumb').trigger('click')
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
expect(wrapper.find('.qr-modal').exists()).toBe(true)
|
||||
expect(wrapper.find('.qr-preview-modal').exists()).toBe(true)
|
||||
expect(wrapper.text()).toContain('下载二维码')
|
||||
})
|
||||
})
|
||||
|
||||
@ -4,6 +4,11 @@
|
||||
<div class="header-row">
|
||||
<h2>打包历史</h2>
|
||||
<div class="filters">
|
||||
<select v-model="filterAppName" class="filter-select">
|
||||
<option value="">全部 App</option>
|
||||
<option v-for="appName in appNames" :key="appName" :value="appName">{{ appName }}</option>
|
||||
</select>
|
||||
<input v-model.trim="filterVersion" class="filter-select version-filter" type="search" placeholder="筛选 App 版本号">
|
||||
<select v-model="filterBuildType" class="filter-select">
|
||||
<option value="">全部类型</option>
|
||||
<option value="Ad_Hoc">Ad_Hoc</option>
|
||||
@ -22,8 +27,8 @@
|
||||
<table class="config-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>时间</th>
|
||||
<th>App</th>
|
||||
<th>App 名称</th>
|
||||
<th>App 版本号</th>
|
||||
<th>打包类型</th>
|
||||
<th>Scheme</th>
|
||||
<th>状态</th>
|
||||
@ -33,13 +38,8 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="task in filteredTasks" :key="task.id">
|
||||
<td>
|
||||
{{ formatTime(task.created_at) }}
|
||||
<span v-if="task.status === 'completed' || task.status === 'failed'" class="duration-text">
|
||||
{{ formatDuration(task.started_at, task.completed_at) }}
|
||||
</span>
|
||||
</td>
|
||||
<td>{{ task.app_name }}</td>
|
||||
<td>{{ task.app_version }}</td>
|
||||
<td>
|
||||
<span :class="['build-type-badge', task.build_type === 'App_Store' ? 'badge-appstore' : 'badge-adhoc']">
|
||||
{{ task.build_type }}
|
||||
@ -162,7 +162,7 @@
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, nextTick, onUnmounted, inject } from 'vue'
|
||||
|
||||
const getToken = inject('getToken')
|
||||
const getToken = inject('getToken', () => '')
|
||||
const tasks = ref([])
|
||||
|
||||
const authFetch = (url, options = {}) => {
|
||||
@ -174,6 +174,8 @@ const authFetch = (url, options = {}) => {
|
||||
}
|
||||
const filterBuildType = ref('')
|
||||
const filterStatus = ref('')
|
||||
const filterAppName = ref('')
|
||||
const filterVersion = ref('')
|
||||
const showLogModal = ref(false)
|
||||
const logTask = ref(null)
|
||||
const logLines = ref([])
|
||||
@ -182,6 +184,16 @@ const showVerboseLogs = ref(false)
|
||||
const qrPreview = ref(null)
|
||||
let logWs = null
|
||||
|
||||
const getAppVersion = (task) => {
|
||||
try {
|
||||
return JSON.parse(task.config_json || '{}').VERSION || '-'
|
||||
} catch {
|
||||
return '-'
|
||||
}
|
||||
}
|
||||
|
||||
const appNames = computed(() => [...new Set(tasks.value.map(task => task.app_name).filter(Boolean))].sort())
|
||||
|
||||
const showQrPreview = (task) => {
|
||||
qrPreview.value = task
|
||||
}
|
||||
@ -190,6 +202,8 @@ const filteredTasks = computed(() => {
|
||||
return tasks.value.filter(task => {
|
||||
// 默认隐藏已取消的任务
|
||||
if (!filterStatus.value && task.status === 'cancelled') return false
|
||||
if (filterAppName.value && task.app_name !== filterAppName.value) return false
|
||||
if (filterVersion.value && !task.app_version.toLowerCase().includes(filterVersion.value.toLowerCase())) return false
|
||||
if (filterBuildType.value && task.build_type !== filterBuildType.value) return false
|
||||
if (filterStatus.value && task.status !== filterStatus.value) return false
|
||||
return true
|
||||
@ -198,7 +212,8 @@ const filteredTasks = computed(() => {
|
||||
|
||||
onMounted(async () => {
|
||||
const res = await authFetch('/api/tasks?limit=100')
|
||||
tasks.value = await res.json()
|
||||
const data = await res.json()
|
||||
tasks.value = data.map(task => ({ ...task, app_version: getAppVersion(task) }))
|
||||
})
|
||||
|
||||
const viewLogs = async (taskId) => {
|
||||
@ -298,11 +313,18 @@ const downloadObfMaps = (taskId) => {
|
||||
}
|
||||
|
||||
const deleteTask = async (taskId) => {
|
||||
if (!confirm('确定要删除这条打包记录吗?')) return
|
||||
if (!confirm('确定要删除这条打包记录及其远端分发文件吗?')) return
|
||||
try {
|
||||
const res = await authFetch(`/api/tasks/${taskId}/delete`, { method: 'DELETE' })
|
||||
if (res.ok) tasks.value = tasks.value.filter(t => t.id !== taskId)
|
||||
} catch {}
|
||||
if (res.ok) {
|
||||
tasks.value = tasks.value.filter(t => t.id !== taskId)
|
||||
} else {
|
||||
const data = await res.json().catch(() => ({}))
|
||||
alert(data.detail || '删除失败,请稍后重试')
|
||||
}
|
||||
} catch {
|
||||
alert('删除失败,请检查网络后重试')
|
||||
}
|
||||
}
|
||||
|
||||
const formatTime = (t) => {
|
||||
@ -361,13 +383,13 @@ const errorCategoryLabel = (cat) => {
|
||||
.config-table th, .config-table td { padding: 12px; text-align: left; border-bottom: 1px solid #f0f0f0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.config-table th { background: #fafafa; font-weight: 500; color: #666; font-size: 13px; }
|
||||
.config-table td { font-size: 14px; }
|
||||
.config-table th:nth-child(1) { width: 10%; }
|
||||
.config-table th:nth-child(1) { width: 14%; }
|
||||
.config-table th:nth-child(2) { width: 12%; }
|
||||
.config-table th:nth-child(3) { width: 9%; }
|
||||
.config-table th:nth-child(3) { width: 10%; }
|
||||
.config-table th:nth-child(4) { width: 18%; }
|
||||
.config-table th:nth-child(5) { width: 17%; }
|
||||
.config-table th:nth-child(5) { width: 15%; }
|
||||
.config-table th:nth-child(6) { width: 12%; }
|
||||
.config-table th:nth-child(7) { width: 22%; }
|
||||
.config-table th:nth-child(7) { width: 19%; }
|
||||
.config-table tr:hover { background: #fafafa; }
|
||||
|
||||
.build-type-badge { padding: 2px 8px; border-radius: 4px; font-size: 12px; font-weight: 500; }
|
||||
@ -434,9 +456,7 @@ const errorCategoryLabel = (cat) => {
|
||||
.meta-label { font-size: 12px; color: #999; }
|
||||
.meta-value { font-size: 13px; color: #333; font-weight: 500; }
|
||||
|
||||
.duration-text {
|
||||
display: block; font-size: 11px; color: #8b949e; margin-top: 2px;
|
||||
}
|
||||
.version-filter { width: 150px; }
|
||||
|
||||
/* 日志操作栏 */
|
||||
.log-modal-actions {
|
||||
|
||||
@ -167,3 +167,48 @@ def test_cancel_task(mock_queue, client, tmp_config):
|
||||
# 验证状态已更新
|
||||
task = client.get(f"/api/tasks/{task_id}").json()
|
||||
assert task["status"] == "cancelled"
|
||||
|
||||
|
||||
def test_delete_completed_task_removes_remote_artifacts(client, tmp_config):
|
||||
from backend.database import SessionLocal
|
||||
from backend.models import Task
|
||||
|
||||
task = Task(
|
||||
id="completed-task", app_id="1", app_name="测试App", build_type="Ad_Hoc",
|
||||
scheme_id="1", scheme_name="readoor31", branch="main", status="completed",
|
||||
oss_url="https://files.example.com/test/iOS/1_2_0_0_0_main.html",
|
||||
config_json=json.dumps({"APPID": "1", "VERSION": "2.0.0.0", "SOURCE_BRANCH": "main", "BUILD_TYPE": "Ad_Hoc", "OSS_FLODER": "test"}),
|
||||
)
|
||||
db = SessionLocal()
|
||||
db.add(task)
|
||||
db.commit()
|
||||
db.close()
|
||||
|
||||
with patch("backend.services.distribution.delete_published_artifacts") as delete:
|
||||
resp = client.delete("/api/tasks/completed-task/delete")
|
||||
|
||||
assert resp.status_code == 200
|
||||
delete.assert_called_once()
|
||||
assert client.get("/api/tasks/completed-task").status_code == 404
|
||||
|
||||
|
||||
def test_delete_task_keeps_shared_remote_artifact(client, tmp_config):
|
||||
from backend.database import SessionLocal
|
||||
from backend.models import Task
|
||||
|
||||
db = SessionLocal()
|
||||
for task_id in ("old-task", "new-task"):
|
||||
db.add(Task(
|
||||
id=task_id, app_id="1", app_name="测试App", build_type="Ad_Hoc",
|
||||
scheme_id="1", scheme_name="readoor31", branch="main", status="completed",
|
||||
oss_url="https://files.example.com/test/iOS/1_2_0_0_0_main.html",
|
||||
))
|
||||
db.commit()
|
||||
db.close()
|
||||
|
||||
with patch("backend.services.distribution.delete_published_artifacts") as delete:
|
||||
resp = client.delete("/api/tasks/old-task/delete")
|
||||
|
||||
assert resp.status_code == 200
|
||||
assert "仍被其他记录引用" in resp.json()["message"]
|
||||
delete.assert_not_called()
|
||||
|
||||
@ -3,6 +3,8 @@ import plistlib
|
||||
from unittest.mock import patch
|
||||
|
||||
from backend.services.distribution import (
|
||||
_artifact_stem,
|
||||
delete_published_artifacts,
|
||||
_write_distribution_files,
|
||||
_write_download_page,
|
||||
_write_manifest,
|
||||
@ -10,6 +12,11 @@ from backend.services.distribution import (
|
||||
)
|
||||
|
||||
|
||||
def test_distribution_artifact_name_contains_sanitized_branch():
|
||||
assert _artifact_stem({"APPID": "100", "VERSION": "2.0.0.0", "SOURCE_BRANCH": "feature/pay-v2", "BUILD_TYPE": "Ad_Hoc"}) == "100_2_0_0_0_feature_pay-v2_adhoc"
|
||||
assert _artifact_stem({"APPID": "100", "VERSION": "2.0.0.0"}) == "100_2_0_0_0"
|
||||
|
||||
|
||||
def test_distribution_files_use_current_service_config(tmp_path):
|
||||
ipa = tmp_path / "source.ipa"
|
||||
ipa.write_bytes(b"ipa")
|
||||
@ -35,20 +42,21 @@ def test_app_store_distribution_uploads_only_ipa(tmp_path):
|
||||
"APPID": "100",
|
||||
"VERSION": "2.0.0",
|
||||
"BUILD_TYPE": "App_Store",
|
||||
"SOURCE_BRANCH": "main",
|
||||
"OSS_FLODER": "readoor",
|
||||
"_upload_config": {"mode": "oss", "oss": {}},
|
||||
}
|
||||
|
||||
with patch(
|
||||
"backend.services.distribution._upload_oss",
|
||||
return_value={".ipa": "https://files.example.com/readoor/iOS/100_2_0_0.ipa"},
|
||||
return_value={".ipa": "https://files.example.com/readoor/iOS/100_2_0_0_main_appstore.ipa"},
|
||||
) as upload:
|
||||
download_url, qr_path = publish_ipa(config, ipa, tmp_path / "build")
|
||||
|
||||
uploaded_files = upload.call_args.args[1]
|
||||
assert len(uploaded_files) == 1
|
||||
assert uploaded_files[0][0].suffix == ".ipa"
|
||||
assert download_url.endswith("100_2_0_0.ipa")
|
||||
assert download_url.endswith("100_2_0_0_main_appstore.ipa")
|
||||
assert qr_path == ""
|
||||
|
||||
|
||||
@ -59,6 +67,7 @@ def test_adhoc_distribution_uploads_qrcode(tmp_path):
|
||||
"APPID": "100",
|
||||
"VERSION": "2.0.0",
|
||||
"BUILD_TYPE": "Ad_Hoc",
|
||||
"SOURCE_BRANCH": "dev",
|
||||
"OSS_FLODER": "readoor",
|
||||
"_upload_config": {"mode": "oss", "oss": {}},
|
||||
}
|
||||
@ -70,8 +79,21 @@ def test_adhoc_distribution_uploads_qrcode(tmp_path):
|
||||
with patch("backend.services.distribution._upload_oss", side_effect=upload_files) as upload:
|
||||
download_url, qr_url = publish_ipa(config, ipa, tmp_path / "build")
|
||||
|
||||
assert download_url.endswith("100_2_0_0.html")
|
||||
assert qr_url.endswith("100_2_0_0.png")
|
||||
assert download_url.endswith("100_2_0_0_dev_adhoc.html")
|
||||
assert qr_url.endswith("100_2_0_0_dev_adhoc.png")
|
||||
assert [call.args[1][0][0].suffix for call in upload.call_args_list] == [
|
||||
".ipa", ".plist", ".html", ".png",
|
||||
]
|
||||
|
||||
|
||||
def test_delete_adhoc_artifacts_deletes_all_remote_files():
|
||||
config = {"APPID": "100", "VERSION": "2.0.0.0", "SOURCE_BRANCH": "main", "BUILD_TYPE": "Ad_Hoc", "OSS_FLODER": "readoor"}
|
||||
with patch("backend.services.distribution._delete_oss") as delete:
|
||||
delete_published_artifacts(config, {"mode": "oss", "oss": {}})
|
||||
|
||||
assert delete.call_args.args[1] == [
|
||||
"readoor/iOS/100_2_0_0_0_main_adhoc.ipa",
|
||||
"readoor/iOS/100_2_0_0_0_main_adhoc.plist",
|
||||
"readoor/iOS/100_2_0_0_0_main_adhoc.html",
|
||||
"readoor/iOS/100_2_0_0_0_main_adhoc.png",
|
||||
]
|
||||
|
||||
Loading…
Reference in New Issue
Block a user