Skip to content

Commit 076c8e0

Browse files
committed
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.
1 parent c4490f6 commit 076c8e0

4 files changed

Lines changed: 461 additions & 0 deletions

File tree

tests/i18n.test.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
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', () => {
7+
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', () => {
49+
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(typeof result).toBe('string');
53+
expect(result.length).toBeGreaterThan(0);
54+
});
55+
56+
test('returns different strings for en and zh for the same key', () => {
57+
i18next.changeLanguage('en');
58+
const enResult = t('packing');
59+
i18next.changeLanguage('zh');
60+
const zhResult = t('packing');
61+
// Both should be non-empty strings
62+
expect(enResult.length).toBeGreaterThan(0);
63+
expect(zhResult.length).toBeGreaterThan(0);
64+
// They should differ (different languages)
65+
expect(enResult).not.toBe(zhResult);
66+
});
67+
});

tests/plugin-config.test.ts

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

tests/user.test.ts

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

0 commit comments

Comments
 (0)