service.xpcool.com/internal/service/wallpaper/provider_live_test.go
夏犀麟 582485ba2b feat(wallpaper): 壁纸模块后端(开源平台插件 + 自建图库 + 前后台接口)
需求:前台壁纸站支持切换到开源壁纸平台,并能展示后台上传的自家图库,
保留原有「浏览器实时生成」能力(扩展而非替换),PC / 移动双端一致。

设计要点(受服务器 5M 出口带宽约束):
- 开源平台图片一律走官方 CDN 直链,后端只代理元数据,零带宽消耗。
- 自建图库落盘到 /data/www/wallpaper,由 nginx 的 /wallpaper/ 直出,
  Go 服务不参与传图;三档尺寸(480 缩略 / 1920 预览 / 原图仅供下载)。
- 内存缓存平台响应(Bing 1h、搜索类 10min),避免撞第三方配额。

内容:
- 平台插件:Bing 每日一图、Picsum、Unsplash、Pexels、Wallhaven。
  新增平台 = 写一个 provider_xxx.go 并在 allProviders() 加一行,
  刻意不用 init() 自注册,避免隐式副作用。所有适配器支持 config.baseUrl
  覆盖,用于绕开 DNS 污染(实测 wallhaven.cc 解析到境外无关 IP)。
- 自建图库:上传(MD5 秒传去重 / 尺寸前置校验 / 失败清理孤儿文件)、
  元信息编辑、删除、统计;随机取图用「数总数 → 随机偏移」而非 ORDER BY RAND()。
- 接口:open 组 4 个(sources / list / random / download-track),
  admin 组 8 个(list / upload / save / delete / stats / source.list|save|test)。
  全部 POST 且 URL 无参数,符合工作空间接口规范;上传走 multipart 例外。
- 安全:purity 默认锁死 SFW;apiKey 只回传布尔不回传明文;
  保存时留空表示保留旧值;open 组错误信息对外脱敏。
- 配置:clientMaxBodySize 提到 64M(gf 默认 8M 会截断 20MB 原图);
  wallpaper.root / baseUrl 支持环境变量注入并在占位符未替换时兜底。

依赖:golang.org/x/image@v0.23.0(仅用于缩略图缩放,保持 go 1.23.0 不变)。
2026-09-14 01:39:09 +08:00

126 lines
3.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package wallpaper
import (
"context"
"os"
"testing"
"time"
"service.xpcool.com/internal/model/dto"
)
// 这一组测试会**真实访问各平台接口**,用于验证 JSON 路径解析与尺寸字段映射。
//
// 平台接口的字段名一旦变化,单元测试(打桩)是发现不了的,只有真连一次才知道。
// 因此保留它们,但默认跳过,只在需要时手动开启:
//
// WALLPAPER_LIVE=1 go test ./internal/service/wallpaper/ -run TestLive -v
//
// 需要 Key 的平台会自动跳过(除非另外提供了环境变量)。
func liveEnabled(t *testing.T) {
t.Helper()
if os.Getenv("WALLPAPER_LIVE") != "1" {
t.Skip("跳过联网测试:设置 WALLPAPER_LIVE=1 后运行")
}
}
// runLive 拉一页并检查基本约束。
func runLive(t *testing.T, p Provider, cfg dto.WallpaperSourceConfig, q dto.WallpaperQuery) []dto.WallpaperItem {
t.Helper()
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
items, _, err := p.List(ctx, cfg, q)
if err != nil {
t.Fatalf("%s 拉取失败: %v", p.Code(), err)
}
if len(items) == 0 {
t.Fatalf("%s 未返回任何条目", p.Code())
}
for i, it := range items {
if it.ThumbUrl == "" || it.FullUrl == "" {
t.Errorf("%s 第 %d 条缺少图片地址: %+v", p.Code(), i, it)
}
if it.Width <= 0 || it.Height <= 0 {
t.Errorf("%s 第 %d 条尺寸异常: %dx%d", p.Code(), i, it.Width, it.Height)
}
if it.Orientation != orientationOf(it.Width, it.Height) {
t.Errorf("%s 第 %d 条朝向与尺寸不一致", p.Code(), i)
}
}
t.Logf("%s 取到 %d 条,示例:%s | 缩略 %s", p.Code(), len(items), items[0].Title, items[0].ThumbUrl)
return items
}
func TestLiveBing(t *testing.T) {
liveEnabled(t)
runLive(t, &BingProvider{}, dto.WallpaperSourceConfig{}, dto.WallpaperQuery{Page: 1, Size: 8})
}
func TestLivePicsum(t *testing.T) {
liveEnabled(t)
items := runLive(t, &PicsumProvider{}, dto.WallpaperSourceConfig{}, dto.WallpaperQuery{Page: 1, Size: 6})
// 顺带验证朝向过滤确实生效Picsum 没有服务端筛选,全靠本地过滤)
portrait := runLive(t, &PicsumProvider{}, dto.WallpaperSourceConfig{},
dto.WallpaperQuery{Page: 1, Size: 3, Orientation: 2})
for i, it := range portrait {
if it.Width >= it.Height {
t.Errorf("竖版筛选失效:第 %d 条是 %dx%d", i, it.Width, it.Height)
}
}
_ = items
}
func TestLiveUnsplash(t *testing.T) {
liveEnabled(t)
key := os.Getenv("UNSPLASH_ACCESS_KEY")
if key == "" {
t.Skip("未提供 UNSPLASH_ACCESS_KEY跳过")
}
runLive(t, &UnsplashProvider{}, dto.WallpaperSourceConfig{ApiKey: key},
dto.WallpaperQuery{Page: 1, Size: 5, Query: "mountain"})
}
func TestLivePexels(t *testing.T) {
liveEnabled(t)
key := os.Getenv("PEXELS_API_KEY")
if key == "" {
t.Skip("未提供 PEXELS_API_KEY跳过")
}
runLive(t, &PexelsProvider{}, dto.WallpaperSourceConfig{ApiKey: key},
dto.WallpaperQuery{Page: 1, Size: 5, Query: "mountain"})
}
func TestLiveWallhaven(t *testing.T) {
liveEnabled(t)
// Wallhaven 的 SFW 检索无需 Key故这里不带凭据也应能跑通
items := runLive(t, &WallhavenProvider{}, dto.WallpaperSourceConfig{},
dto.WallpaperQuery{Page: 1, Size: 5})
for i := range items {
if items[i].FullUrl == "" {
t.Errorf("第 %d 条缺少原图地址", i)
}
}
}
// TestLiveBingCopyrightSplit 校验版权串的拆分(纯函数,不联网也跑)。
func TestLiveBingCopyrightSplit(t *testing.T) {
cases := []struct {
in string
wantTitle string
wantAuthor string
}{
{"冰岛某瀑布 (© 张三/Getty Images)", "冰岛某瀑布", "张三/Getty Images"},
{"没有括号的版权串", "没有括号的版权串", ""},
{"", "", ""},
}
for _, c := range cases {
gotTitle, gotAuthor := splitBingCopyright(c.in)
if gotTitle != c.wantTitle || gotAuthor != c.wantAuthor {
t.Errorf("splitBingCopyright(%q) = (%q, %q),期望 (%q, %q)",
c.in, gotTitle, gotAuthor, c.wantTitle, c.wantAuthor)
}
}
}