Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions tests/i18n.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { describe, expect, test } from 'bun:test';
import i18next from 'i18next';
import { t } from '../src/utils/i18n';

describe('i18n t()', () => {
test('returns a non-empty translated string for a known key in English', async () => {
await i18next.changeLanguage('en');
const result = t('cancelled');
expect(typeof result).toBe('string');
expect(result.length).toBeGreaterThan(0);
expect(result).toBe('Cancelled');
});

test('returns a non-empty translated string for a known key in Chinese', () => {
i18next.changeLanguage('zh');
const result = t('cancelled');
expect(typeof result).toBe('string');
expect(result.length).toBeGreaterThan(0);
expect(result).toBe('已取消');
});

test('returns a translated string for a key with interpolation in English', () => {
i18next.changeLanguage('en');
const result = t('createAppSuccess', { id: '12345' });
expect(typeof result).toBe('string');
expect(result).toContain('12345');
});

test('returns a translated string for a key with interpolation in Chinese', () => {
i18next.changeLanguage('zh');
const result = t('createAppSuccess', { id: '67890' });
expect(typeof result).toBe('string');
expect(result).toContain('67890');
});

test('handles multiple interpolation options', () => {
i18next.changeLanguage('en');
const result = t('versionBind', {
version: '1.0.0',
nativeVersion: '2.0',
id: 'abc',
});
expect(result).toContain('1.0.0');
expect(result).toContain('2.0');
expect(result).toContain('abc');
});

test('returns the key itself or a fallback for an unknown key', async () => {
await i18next.changeLanguage('en');
const result = t('this_key_does_not_exist_at_all');
// i18next returns the key string when a key is missing
expect(result).toBe('this_key_does_not_exist_at_all');
});

test('returns different strings for en and zh for the same key', () => {
i18next.changeLanguage('en');
const enResult = t('packing');
i18next.changeLanguage('zh');
const zhResult = t('packing');
// Both should be non-empty strings
expect(enResult.length).toBeGreaterThan(0);
expect(zhResult.length).toBeGreaterThan(0);
// They should differ (different languages)
expect(enResult).not.toBe(zhResult);
});
});
100 changes: 100 additions & 0 deletions tests/plugin-config.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
import fs from 'fs-extra';
import os from 'os';
import path from 'path';

import { plugins } from '../src/utils/plugin-config';

describe('plugin-config - sentry plugin', () => {
let tmpDir: string;

beforeEach(async () => {
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'plugin-config-test-'));
});

afterEach(async () => {
await fs.remove(tmpDir);
});

const sentryPlugin = plugins.find((p) => p.name === 'sentry');

test('sentry plugin exists in the plugins array', () => {
expect(sentryPlugin).toBeDefined();
});

test('sentry bundleParams are { sentry: true, sourcemap: true }', () => {
expect(sentryPlugin?.bundleParams).toEqual({
sentry: true,
sourcemap: true,
});
});

describe('sentry detect', () => {
test('returns false when no sentry.properties exists', async () => {
const origCwd = process.cwd();
process.chdir(tmpDir);
try {
const result = await sentryPlugin?.detect();
expect(result).toBe(false);
} finally {
process.chdir(origCwd);
}
});

test('returns true when ios/sentry.properties exists', async () => {
await fs.ensureDir(path.join(tmpDir, 'ios'));
await fs.writeFile(
path.join(tmpDir, 'ios', 'sentry.properties'),
'defaults.org=test\n',
);

const origCwd = process.cwd();
process.chdir(tmpDir);
try {
const result = await sentryPlugin?.detect();
expect(result).toBe(true);
} finally {
process.chdir(origCwd);
}
});

test('returns true when android/sentry.properties exists', async () => {
await fs.ensureDir(path.join(tmpDir, 'android'));
await fs.writeFile(
path.join(tmpDir, 'android', 'sentry.properties'),
'defaults.org=test\n',
);

const origCwd = process.cwd();
process.chdir(tmpDir);
try {
const result = await sentryPlugin?.detect();
expect(result).toBe(true);
} finally {
process.chdir(origCwd);
}
});

test('returns true when both ios and android sentry.properties exist', async () => {
await fs.ensureDir(path.join(tmpDir, 'ios'));
await fs.ensureDir(path.join(tmpDir, 'android'));
await fs.writeFile(
path.join(tmpDir, 'ios', 'sentry.properties'),
'defaults.org=test\n',
);
await fs.writeFile(
path.join(tmpDir, 'android', 'sentry.properties'),
'defaults.org=test\n',
);

const origCwd = process.cwd();
process.chdir(tmpDir);
try {
const result = await sentryPlugin?.detect();
expect(result).toBe(true);
} finally {
process.chdir(origCwd);
}
});
});
});
190 changes: 190 additions & 0 deletions tests/user.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
import { afterEach, beforeEach, describe, expect, spyOn, test } from 'bun:test';
import crypto from 'crypto';

import * as api from '../src/api';
import { userCommands } from '../src/user';
import * as utils from '../src/utils';

function md5(str: string) {
return crypto.createHash('md5').update(str).digest('hex');
}

describe('userCommands.login', () => {
let consoleSpy: ReturnType<typeof spyOn>;
let postSpy: ReturnType<typeof spyOn>;
let replaceSessionSpy: ReturnType<typeof spyOn>;
let saveSessionSpy: ReturnType<typeof spyOn>;
let questionSpy: ReturnType<typeof spyOn>;

beforeEach(() => {
consoleSpy = spyOn(console, 'log').mockImplementation(() => {});
postSpy = spyOn(api, 'post').mockResolvedValue({
token: 'session-token-abc',
info: { name: 'TestUser', email: 'test@example.com' },
});
replaceSessionSpy = spyOn(api, 'replaceSession').mockImplementation(
() => {},
);
saveSessionSpy = spyOn(api, 'saveSession').mockResolvedValue(undefined);
questionSpy = spyOn(utils, 'question').mockResolvedValue('fallback');
});

afterEach(() => {
consoleSpy.mockRestore();
postSpy.mockRestore();
replaceSessionSpy.mockRestore();
saveSessionSpy.mockRestore();
questionSpy.mockRestore();
});

test('calls post with /user/login and md5-hashes the password', async () => {
await userCommands.login({
args: ['user@example.com', 'mypassword'],
});

expect(postSpy).toHaveBeenCalledWith('/user/login', {
email: 'user@example.com',
pwd: md5('mypassword'),
});
});

test('md5 hash is a valid 32-char hex string', async () => {
await userCommands.login({
args: ['user@example.com', 'secret123'],
});

const callArgs = postSpy.mock.calls[0];
const pwdHash = callArgs[1].pwd as string;
expect(pwdHash).toHaveLength(32);
expect(pwdHash).toMatch(/^[0-9a-f]{32}$/);
expect(pwdHash).toBe(md5('secret123'));
});

test('calls replaceSession with the returned token', async () => {
await userCommands.login({
args: ['user@example.com', 'mypassword'],
});

expect(replaceSessionSpy).toHaveBeenCalledWith({
token: 'session-token-abc',
});
});

test('calls saveSession after replaceSession', async () => {
await userCommands.login({
args: ['user@example.com', 'mypassword'],
});

expect(saveSessionSpy).toHaveBeenCalled();
expect(replaceSessionSpy).toHaveBeenCalled();

// Verify call order: replaceSession should be called before saveSession
const replaceOrder = replaceSessionSpy.mock.invocationCallOrder[0];
const saveOrder = saveSessionSpy.mock.invocationCallOrder[0];
expect(replaceOrder).toBeLessThan(saveOrder);
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.

test('prompts for email and password when args are missing', async () => {
let _callCount = 0;
questionSpy.mockImplementation(async (prompt: string) => {
_callCount++;
if (prompt === 'email:') return 'asked@email.com';
return 'asked-password';
});

await userCommands.login({ args: [] });

expect(questionSpy).toHaveBeenCalledTimes(2);
expect(postSpy).toHaveBeenCalledWith('/user/login', {
email: 'asked@email.com',
pwd: md5('asked-password'),
});
});

test('logs welcome message with user name', async () => {
await userCommands.login({
args: ['user@example.com', 'mypassword'],
});

expect(consoleSpy).toHaveBeenCalled();
// The welcome message includes the user's name
const logOutput = consoleSpy.mock.calls[0][0] as string;
expect(logOutput).toContain('TestUser');
});
});

describe('userCommands.logout', () => {
let consoleSpy: ReturnType<typeof spyOn>;
let closeSessionSpy: ReturnType<typeof spyOn>;

beforeEach(() => {
consoleSpy = spyOn(console, 'log').mockImplementation(() => {});
closeSessionSpy = spyOn(api, 'closeSession').mockImplementation(() => {});
});

afterEach(() => {
consoleSpy.mockRestore();
closeSessionSpy.mockRestore();
});

test('calls closeSession', async () => {
await userCommands.logout({} as any);

expect(closeSessionSpy).toHaveBeenCalled();
});

test('logs a message after logout', async () => {
await userCommands.logout({} as any);

expect(consoleSpy).toHaveBeenCalled();
});
});

describe('userCommands.me', () => {
let consoleSpy: ReturnType<typeof spyOn>;
let getSpy: ReturnType<typeof spyOn>;

beforeEach(() => {
consoleSpy = spyOn(console, 'log').mockImplementation(() => {});
getSpy = spyOn(api, 'get').mockResolvedValue({
ok: true,
name: 'TestUser',
email: 'test@example.com',
id: '12345',
});
});

afterEach(() => {
consoleSpy.mockRestore();
getSpy.mockRestore();
});

test('calls get with /user/me', async () => {
await userCommands.me();

expect(getSpy).toHaveBeenCalledWith('/user/me');
});

test('logs each field except "ok"', async () => {
await userCommands.me();

// Should log name, email, id but NOT ok
const logCalls = consoleSpy.mock.calls.map((c) => c[0] as string);
expect(logCalls).toContain('name: TestUser');
expect(logCalls).toContain('email: test@example.com');
expect(logCalls).toContain('id: 12345');

// Should not log the "ok" field
const hasOk = logCalls.some((msg) => msg.startsWith('ok:'));
expect(hasOk).toBe(false);
});

test('skips the "ok" field when logging', async () => {
await userCommands.me();

const logCalls = consoleSpy.mock.calls.map((c) => c[0] as string);
for (const msg of logCalls) {
expect(msg).not.toMatch(/^ok:/);
}
});
});
Loading