Skip to content

Commit 9744312

Browse files
sunnylqmcoderabbitai[bot]CodeRabbit
authored
test: 补充 zip-options / i18n / user / plugin-config 测试 (#52)
* test: add tests for zip-options file detection, i18n, user commands, and plugin-config New test files: - tests/zip-options-file.test.ts: zipOptionsForPayloadFile with real files (magic bytes, extensions, empty files) - tests/i18n.test.ts: t() translation function (en/zh, interpolation, fallback) - tests/user.test.ts: userCommands login/logout/me with API mocking - tests/plugin-config.test.ts: sentry plugin detection via filesystem 234 tests pass, 0 failures. * fix: lint fixes (biome formatting, unused imports/variables) * fix: apply CodeRabbit auto-fixes Fixed 3 file(s) based on 4 unresolved review comments. Co-authored-by: CodeRabbit <noreply@coderabbit.ai> --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: CodeRabbit <noreply@coderabbit.ai>
1 parent c4490f6 commit 9744312

4 files changed

Lines changed: 448 additions & 0 deletions

File tree

tests/i18n.test.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { describe, expect, test } from 'bun:test';
2+
import i18next from 'i18next';
3+
import { t } from '../src/utils/i18n';
4+
5+
describe('i18n t()', () => {
6+
test('returns a non-empty translated string for a known key in English', async () => {
7+
await i18next.changeLanguage('en');
8+
const result = t('cancelled');
9+
expect(typeof result).toBe('string');
10+
expect(result.length).toBeGreaterThan(0);
11+
expect(result).toBe('Cancelled');
12+
});
13+
14+
test('returns a non-empty translated string for a known key in Chinese', () => {
15+
i18next.changeLanguage('zh');
16+
const result = t('cancelled');
17+
expect(typeof result).toBe('string');
18+
expect(result.length).toBeGreaterThan(0);
19+
expect(result).toBe('已取消');
20+
});
21+
22+
test('returns a translated string for a key with interpolation in English', () => {
23+
i18next.changeLanguage('en');
24+
const result = t('createAppSuccess', { id: '12345' });
25+
expect(typeof result).toBe('string');
26+
expect(result).toContain('12345');
27+
});
28+
29+
test('returns a translated string for a key with interpolation in Chinese', () => {
30+
i18next.changeLanguage('zh');
31+
const result = t('createAppSuccess', { id: '67890' });
32+
expect(typeof result).toBe('string');
33+
expect(result).toContain('67890');
34+
});
35+
36+
test('handles multiple interpolation options', () => {
37+
i18next.changeLanguage('en');
38+
const result = t('versionBind', {
39+
version: '1.0.0',
40+
nativeVersion: '2.0',
41+
id: 'abc',
42+
});
43+
expect(result).toContain('1.0.0');
44+
expect(result).toContain('2.0');
45+
expect(result).toContain('abc');
46+
});
47+
48+
test('returns the key itself or a fallback for an unknown key', async () => {
49+
await i18next.changeLanguage('en');
50+
const result = t('this_key_does_not_exist_at_all');
51+
// i18next returns the key string when a key is missing
52+
expect(result).toBe('this_key_does_not_exist_at_all');
53+
});
54+
55+
test('returns different strings for en and zh for the same key', () => {
56+
i18next.changeLanguage('en');
57+
const enResult = t('packing');
58+
i18next.changeLanguage('zh');
59+
const zhResult = t('packing');
60+
// Both should be non-empty strings
61+
expect(enResult.length).toBeGreaterThan(0);
62+
expect(zhResult.length).toBeGreaterThan(0);
63+
// They should differ (different languages)
64+
expect(enResult).not.toBe(zhResult);
65+
});
66+
});

tests/plugin-config.test.ts

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
2+
import fs from 'fs-extra';
3+
import os from 'os';
4+
import path from 'path';
5+
6+
import { plugins } from '../src/utils/plugin-config';
7+
8+
describe('plugin-config - sentry plugin', () => {
9+
let tmpDir: string;
10+
11+
beforeEach(async () => {
12+
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'plugin-config-test-'));
13+
});
14+
15+
afterEach(async () => {
16+
await fs.remove(tmpDir);
17+
});
18+
19+
const sentryPlugin = plugins.find((p) => p.name === 'sentry');
20+
21+
test('sentry plugin exists in the plugins array', () => {
22+
expect(sentryPlugin).toBeDefined();
23+
});
24+
25+
test('sentry bundleParams are { sentry: true, sourcemap: true }', () => {
26+
expect(sentryPlugin?.bundleParams).toEqual({
27+
sentry: true,
28+
sourcemap: true,
29+
});
30+
});
31+
32+
describe('sentry detect', () => {
33+
test('returns false when no sentry.properties exists', async () => {
34+
const origCwd = process.cwd();
35+
process.chdir(tmpDir);
36+
try {
37+
const result = await sentryPlugin?.detect();
38+
expect(result).toBe(false);
39+
} finally {
40+
process.chdir(origCwd);
41+
}
42+
});
43+
44+
test('returns true when ios/sentry.properties exists', async () => {
45+
await fs.ensureDir(path.join(tmpDir, 'ios'));
46+
await fs.writeFile(
47+
path.join(tmpDir, 'ios', 'sentry.properties'),
48+
'defaults.org=test\n',
49+
);
50+
51+
const origCwd = process.cwd();
52+
process.chdir(tmpDir);
53+
try {
54+
const result = await sentryPlugin?.detect();
55+
expect(result).toBe(true);
56+
} finally {
57+
process.chdir(origCwd);
58+
}
59+
});
60+
61+
test('returns true when android/sentry.properties exists', async () => {
62+
await fs.ensureDir(path.join(tmpDir, 'android'));
63+
await fs.writeFile(
64+
path.join(tmpDir, 'android', 'sentry.properties'),
65+
'defaults.org=test\n',
66+
);
67+
68+
const origCwd = process.cwd();
69+
process.chdir(tmpDir);
70+
try {
71+
const result = await sentryPlugin?.detect();
72+
expect(result).toBe(true);
73+
} finally {
74+
process.chdir(origCwd);
75+
}
76+
});
77+
78+
test('returns true when both ios and android sentry.properties exist', async () => {
79+
await fs.ensureDir(path.join(tmpDir, 'ios'));
80+
await fs.ensureDir(path.join(tmpDir, 'android'));
81+
await fs.writeFile(
82+
path.join(tmpDir, 'ios', 'sentry.properties'),
83+
'defaults.org=test\n',
84+
);
85+
await fs.writeFile(
86+
path.join(tmpDir, 'android', 'sentry.properties'),
87+
'defaults.org=test\n',
88+
);
89+
90+
const origCwd = process.cwd();
91+
process.chdir(tmpDir);
92+
try {
93+
const result = await sentryPlugin?.detect();
94+
expect(result).toBe(true);
95+
} finally {
96+
process.chdir(origCwd);
97+
}
98+
});
99+
});
100+
});

tests/user.test.ts

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
import { afterEach, beforeEach, describe, expect, spyOn, test } from 'bun:test';
2+
import crypto from 'crypto';
3+
4+
import * as api from '../src/api';
5+
import { userCommands } from '../src/user';
6+
import * as utils from '../src/utils';
7+
8+
function md5(str: string) {
9+
return crypto.createHash('md5').update(str).digest('hex');
10+
}
11+
12+
describe('userCommands.login', () => {
13+
let consoleSpy: ReturnType<typeof spyOn>;
14+
let postSpy: ReturnType<typeof spyOn>;
15+
let replaceSessionSpy: ReturnType<typeof spyOn>;
16+
let saveSessionSpy: ReturnType<typeof spyOn>;
17+
let questionSpy: ReturnType<typeof spyOn>;
18+
19+
beforeEach(() => {
20+
consoleSpy = spyOn(console, 'log').mockImplementation(() => {});
21+
postSpy = spyOn(api, 'post').mockResolvedValue({
22+
token: 'session-token-abc',
23+
info: { name: 'TestUser', email: 'test@example.com' },
24+
});
25+
replaceSessionSpy = spyOn(api, 'replaceSession').mockImplementation(
26+
() => {},
27+
);
28+
saveSessionSpy = spyOn(api, 'saveSession').mockResolvedValue(undefined);
29+
questionSpy = spyOn(utils, 'question').mockResolvedValue('fallback');
30+
});
31+
32+
afterEach(() => {
33+
consoleSpy.mockRestore();
34+
postSpy.mockRestore();
35+
replaceSessionSpy.mockRestore();
36+
saveSessionSpy.mockRestore();
37+
questionSpy.mockRestore();
38+
});
39+
40+
test('calls post with /user/login and md5-hashes the password', async () => {
41+
await userCommands.login({
42+
args: ['user@example.com', 'mypassword'],
43+
});
44+
45+
expect(postSpy).toHaveBeenCalledWith('/user/login', {
46+
email: 'user@example.com',
47+
pwd: md5('mypassword'),
48+
});
49+
});
50+
51+
test('md5 hash is a valid 32-char hex string', async () => {
52+
await userCommands.login({
53+
args: ['user@example.com', 'secret123'],
54+
});
55+
56+
const callArgs = postSpy.mock.calls[0];
57+
const pwdHash = callArgs[1].pwd as string;
58+
expect(pwdHash).toHaveLength(32);
59+
expect(pwdHash).toMatch(/^[0-9a-f]{32}$/);
60+
expect(pwdHash).toBe(md5('secret123'));
61+
});
62+
63+
test('calls replaceSession with the returned token', async () => {
64+
await userCommands.login({
65+
args: ['user@example.com', 'mypassword'],
66+
});
67+
68+
expect(replaceSessionSpy).toHaveBeenCalledWith({
69+
token: 'session-token-abc',
70+
});
71+
});
72+
73+
test('calls saveSession after replaceSession', async () => {
74+
await userCommands.login({
75+
args: ['user@example.com', 'mypassword'],
76+
});
77+
78+
expect(saveSessionSpy).toHaveBeenCalled();
79+
expect(replaceSessionSpy).toHaveBeenCalled();
80+
81+
// Verify call order: replaceSession should be called before saveSession
82+
const replaceOrder = replaceSessionSpy.mock.invocationCallOrder[0];
83+
const saveOrder = saveSessionSpy.mock.invocationCallOrder[0];
84+
expect(replaceOrder).toBeLessThan(saveOrder);
85+
});
86+
87+
test('prompts for email and password when args are missing', async () => {
88+
let _callCount = 0;
89+
questionSpy.mockImplementation(async (prompt: string) => {
90+
_callCount++;
91+
if (prompt === 'email:') return 'asked@email.com';
92+
return 'asked-password';
93+
});
94+
95+
await userCommands.login({ args: [] });
96+
97+
expect(questionSpy).toHaveBeenCalledTimes(2);
98+
expect(postSpy).toHaveBeenCalledWith('/user/login', {
99+
email: 'asked@email.com',
100+
pwd: md5('asked-password'),
101+
});
102+
});
103+
104+
test('logs welcome message with user name', async () => {
105+
await userCommands.login({
106+
args: ['user@example.com', 'mypassword'],
107+
});
108+
109+
expect(consoleSpy).toHaveBeenCalled();
110+
// The welcome message includes the user's name
111+
const logOutput = consoleSpy.mock.calls[0][0] as string;
112+
expect(logOutput).toContain('TestUser');
113+
});
114+
});
115+
116+
describe('userCommands.logout', () => {
117+
let consoleSpy: ReturnType<typeof spyOn>;
118+
let closeSessionSpy: ReturnType<typeof spyOn>;
119+
120+
beforeEach(() => {
121+
consoleSpy = spyOn(console, 'log').mockImplementation(() => {});
122+
closeSessionSpy = spyOn(api, 'closeSession').mockImplementation(() => {});
123+
});
124+
125+
afterEach(() => {
126+
consoleSpy.mockRestore();
127+
closeSessionSpy.mockRestore();
128+
});
129+
130+
test('calls closeSession', async () => {
131+
await userCommands.logout({} as any);
132+
133+
expect(closeSessionSpy).toHaveBeenCalled();
134+
});
135+
136+
test('logs a message after logout', async () => {
137+
await userCommands.logout({} as any);
138+
139+
expect(consoleSpy).toHaveBeenCalled();
140+
});
141+
});
142+
143+
describe('userCommands.me', () => {
144+
let consoleSpy: ReturnType<typeof spyOn>;
145+
let getSpy: ReturnType<typeof spyOn>;
146+
147+
beforeEach(() => {
148+
consoleSpy = spyOn(console, 'log').mockImplementation(() => {});
149+
getSpy = spyOn(api, 'get').mockResolvedValue({
150+
ok: true,
151+
name: 'TestUser',
152+
email: 'test@example.com',
153+
id: '12345',
154+
});
155+
});
156+
157+
afterEach(() => {
158+
consoleSpy.mockRestore();
159+
getSpy.mockRestore();
160+
});
161+
162+
test('calls get with /user/me', async () => {
163+
await userCommands.me();
164+
165+
expect(getSpy).toHaveBeenCalledWith('/user/me');
166+
});
167+
168+
test('logs each field except "ok"', async () => {
169+
await userCommands.me();
170+
171+
// Should log name, email, id but NOT ok
172+
const logCalls = consoleSpy.mock.calls.map((c) => c[0] as string);
173+
expect(logCalls).toContain('name: TestUser');
174+
expect(logCalls).toContain('email: test@example.com');
175+
expect(logCalls).toContain('id: 12345');
176+
177+
// Should not log the "ok" field
178+
const hasOk = logCalls.some((msg) => msg.startsWith('ok:'));
179+
expect(hasOk).toBe(false);
180+
});
181+
182+
test('skips the "ok" field when logging', async () => {
183+
await userCommands.me();
184+
185+
const logCalls = consoleSpy.mock.calls.map((c) => c[0] as string);
186+
for (const msg of logCalls) {
187+
expect(msg).not.toMatch(/^ok:/);
188+
}
189+
});
190+
});

0 commit comments

Comments
 (0)