1- const test = require ( 'node:test' ) ;
2- const assert = require ( 'assert' ) ;
3- const { Application, MailSystem } = require ( './main' ) ;
1+ import fs from 'fs' ;
2+ import path from 'path' ;
3+ import os from 'os' ;
4+ import test from 'node:test' ;
5+ import assert from 'assert' ;
6+ import { Application , MailSystem } from './main.js' ;
47
5- // TODO: write your tests here
6- // Remember to use Stub, Mock, and Spy when necessary
8+ // 輔助函式:在暫存目錄中建立 name_list.txt,並切換工作目錄
9+ async function withTempNameList ( fn ) {
10+ const originalCwd = process . cwd ( ) ;
11+ const tmpDir = fs . mkdtempSync ( path . join ( os . tmpdir ( ) , 'lab2-' ) ) ;
12+ fs . writeFileSync ( path . join ( tmpDir , 'name_list.txt' ) , 'Alice\nBob\nCharlie' ) ;
13+ process . chdir ( tmpDir ) ;
14+ try {
15+ await fn ( ) ;
16+ } finally {
17+ process . chdir ( originalCwd ) ;
18+ fs . rmSync ( tmpDir , { recursive : true , force : true } ) ;
19+ }
20+ }
21+
22+ // 建立 Stub 輔助函式
23+ const createStub = ( obj , method , implementation ) => {
24+ const original = obj [ method ] ;
25+ obj [ method ] = implementation ;
26+ return { restore : ( ) => { obj [ method ] = original ; } } ;
27+ } ;
28+
29+ // 測試 MailSystem.write()(不需要檔案)
30+ test ( 'MailSystem.write should generate correct mail content' , ( ) => {
31+ const mailSystem = new MailSystem ( ) ;
32+ const result = mailSystem . write ( 'Alice' ) ;
33+ assert . strictEqual ( result , 'Congrats, Alice!' ) ;
34+ } ) ;
35+
36+ // 測試 MailSystem.send():透過 stub 控制隨機行為
37+ test ( 'MailSystem.send should return success or failure' , ( ) => {
38+ const mailSystem = new MailSystem ( ) ;
39+ const randomStub = createStub ( Math , 'random' , ( ) => 0.9 ) ;
40+ assert . strictEqual ( mailSystem . send ( 'Alice' , 'Congrats, Alice!' ) , true ) ;
41+ randomStub . restore ( ) ;
42+ const randomStub2 = createStub ( Math , 'random' , ( ) => 0.4 ) ;
43+ assert . strictEqual ( mailSystem . send ( 'Alice' , 'Congrats, Alice!' ) , false ) ;
44+ randomStub2 . restore ( ) ;
45+ } ) ;
46+
47+ // 以下測試皆需要 name_list.txt,所以採用 withTempNameList 包裹
48+
49+ test ( 'Application.getNames should read and parse names from file' , async ( ) => {
50+ await withTempNameList ( async ( ) => {
51+ const app = new Application ( ) ;
52+ // 等待 constructor 的非同步初始化完成
53+ await new Promise ( resolve => setTimeout ( resolve , 10 ) ) ;
54+ const [ people , selected ] = await app . getNames ( ) ;
55+ assert . deepStrictEqual ( people , [ 'Alice' , 'Bob' , 'Charlie' ] ) ;
56+ assert . deepStrictEqual ( selected , [ ] ) ;
57+ } ) ;
58+ } ) ;
59+
60+ test ( 'Application constructor should initialize people and selected' , async ( ) => {
61+ await withTempNameList ( async ( ) => {
62+ const app = new Application ( ) ;
63+ await new Promise ( resolve => setTimeout ( resolve , 10 ) ) ;
64+ assert . deepStrictEqual ( app . people , [ 'Alice' , 'Bob' , 'Charlie' ] ) ;
65+ assert . deepStrictEqual ( app . selected , [ ] ) ;
66+ } ) ;
67+ } ) ;
68+
69+ test ( 'Application.getRandomPerson should return a person from people list' , async ( ) => {
70+ await withTempNameList ( async ( ) => {
71+ const app = new Application ( ) ;
72+ await new Promise ( resolve => setTimeout ( resolve , 10 ) ) ;
73+ // stub Math.random 固定回傳 0.5 (floor(0.5 * 3) = 1) → 'Bob'
74+ const randomStub = createStub ( Math , 'random' , ( ) => 0.5 ) ;
75+ assert . strictEqual ( app . getRandomPerson ( ) , 'Bob' ) ;
76+ randomStub . restore ( ) ;
77+ } ) ;
78+ } ) ;
79+
80+ test ( 'Application.selectNextPerson should select a unique person in order' , async ( ) => {
81+ await withTempNameList ( async ( ) => {
82+ const app = new Application ( ) ;
83+ await new Promise ( resolve => setTimeout ( resolve , 10 ) ) ;
84+ // stub getRandomPerson 依序回傳:'Alice', 'Bob', 'Charlie'
85+ let sequence = [ 'Alice' , 'Bob' , 'Charlie' ] ;
86+ let index = 0 ;
87+ const stub = createStub ( app , 'getRandomPerson' , ( ) => sequence [ index ++ ] ) ;
88+
89+ const selected1 = app . selectNextPerson ( ) ;
90+ assert . strictEqual ( selected1 , 'Alice' ) ;
91+ assert . strictEqual ( app . selected . length , 1 ) ;
92+
93+ const selected2 = app . selectNextPerson ( ) ;
94+ assert . strictEqual ( selected2 , 'Bob' ) ;
95+ assert . strictEqual ( app . selected . length , 2 ) ;
96+
97+ const selected3 = app . selectNextPerson ( ) ;
98+ assert . strictEqual ( selected3 , 'Charlie' ) ;
99+ assert . strictEqual ( app . selected . length , 3 ) ;
100+
101+ const selected4 = app . selectNextPerson ( ) ;
102+ assert . strictEqual ( selected4 , null ) ;
103+
104+ stub . restore ( ) ;
105+ } ) ;
106+ } ) ;
107+
108+ test ( 'Application.selectNextPerson should retry if duplicate selected' , async ( ) => {
109+ await withTempNameList ( async ( ) => {
110+ const app = new Application ( ) ;
111+ await new Promise ( resolve => setTimeout ( resolve , 10 ) ) ;
112+ // stub 模擬第一次回傳 'Alice'(已被選取),接著回傳 'Bob'
113+ let callCount = 0 ;
114+ const stub = createStub ( app , 'getRandomPerson' , ( ) => {
115+ if ( callCount === 0 ) {
116+ callCount ++ ;
117+ return 'Alice' ;
118+ }
119+ return 'Bob' ;
120+ } ) ;
121+ // 預先將 'Alice' 加入已選名單
122+ app . selected = [ 'Alice' ] ;
123+ const result = app . selectNextPerson ( ) ;
124+ assert . strictEqual ( result , 'Bob' ) ;
125+ stub . restore ( ) ;
126+ } ) ;
127+ } ) ;
128+
129+ test ( 'Application.notifySelected should send mail to selected people' , async ( ) => {
130+ await withTempNameList ( async ( ) => {
131+ const app = new Application ( ) ;
132+ await new Promise ( resolve => setTimeout ( resolve , 10 ) ) ;
133+ app . selected = [ 'Alice' , 'Bob' ] ;
134+ let writeCalls = [ ] ;
135+ let sendCalls = [ ] ;
136+ const writeStub = createStub ( app . mailSystem , 'write' , ( name ) => {
137+ writeCalls . push ( name ) ;
138+ return `Congrats, ${ name } !` ;
139+ } ) ;
140+ const sendStub = createStub ( app . mailSystem , 'send' , ( name , content ) => {
141+ sendCalls . push ( { name, content } ) ;
142+ return true ;
143+ } ) ;
144+
145+ app . notifySelected ( ) ;
146+
147+ assert . deepStrictEqual ( writeCalls , [ 'Alice' , 'Bob' ] ) ;
148+ assert . deepStrictEqual ( sendCalls , [
149+ { name : 'Alice' , content : 'Congrats, Alice!' } ,
150+ { name : 'Bob' , content : 'Congrats, Bob!' }
151+ ] ) ;
152+
153+ writeStub . restore ( ) ;
154+ sendStub . restore ( ) ;
155+ } ) ;
156+ } ) ;
0 commit comments