github-issue-tower-defence-management 1.42.3 → 1.42.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,11 @@
1
+ ## [1.42.4](https://github.com/HiromiShikata/npm-cli-github-issue-tower-defence-management/compare/v1.42.3...v1.42.4) (2026-05-11)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * **core:** mock Google Sheets API in tests to prevent quota exceeded CI failures ([12fb71a](https://github.com/HiromiShikata/npm-cli-github-issue-tower-defence-management/commit/12fb71a696ad3f0b5a5248ec42ae02f677b276b2)), closes [#379](https://github.com/HiromiShikata/npm-cli-github-issue-tower-defence-management/issues/379)
7
+ * **test:** mock Google Sheets API to prevent quota exceeded flakiness ([94266bf](https://github.com/HiromiShikata/npm-cli-github-issue-tower-defence-management/commit/94266bf83792313e3fe2e1be474500adc3f65fd1)), closes [#373](https://github.com/HiromiShikata/npm-cli-github-issue-tower-defence-management/issues/373) [#379](https://github.com/HiromiShikata/npm-cli-github-issue-tower-defence-management/issues/379)
8
+
1
9
  ## [1.42.3](https://github.com/HiromiShikata/npm-cli-github-issue-tower-defence-management/compare/v1.42.2...v1.42.3) (2026-05-11)
2
10
 
3
11
 
@@ -9,7 +9,7 @@ const dotenv_1 = __importDefault(require("dotenv"));
9
9
  dotenv_1.default.config();
10
10
  class GoogleSpreadsheetRepository {
11
11
  constructor(localStorageRepository, serviceAccountKey = process.env.GOOGLE_SERVICE_ACCOUNT_KEY ||
12
- 'dummy') {
12
+ 'dummy', sheetsClientFactory) {
13
13
  this.localStorageRepository = localStorageRepository;
14
14
  this.keyFile = './tmp/service-account-key.json';
15
15
  this.getSpreadsheetId = (spreadsheetUrl) => {
@@ -17,11 +17,7 @@ class GoogleSpreadsheetRepository {
17
17
  return url.pathname.split('/')[3];
18
18
  };
19
19
  this.getSheet = async (spreadsheetUrl, sheetName) => {
20
- const auth = new googleapis_1.google.auth.GoogleAuth({
21
- keyFile: this.keyFile,
22
- scopes: ['https://www.googleapis.com/auth/spreadsheets'],
23
- });
24
- const sheets = googleapis_1.google.sheets({ version: 'v4', auth });
20
+ const sheets = this.sheetsClient;
25
21
  const spreadsheetId = this.getSpreadsheetId(spreadsheetUrl);
26
22
  const responseSheet = await sheets.spreadsheets.get({
27
23
  spreadsheetId,
@@ -46,11 +42,7 @@ class GoogleSpreadsheetRepository {
46
42
  return response.data.values.map((row) => row.map((cell) => String(cell)));
47
43
  };
48
44
  this.updateCell = async (spreadsheetUrl, sheetName, row, column, value) => {
49
- const auth = new googleapis_1.google.auth.GoogleAuth({
50
- keyFile: this.keyFile,
51
- scopes: ['https://www.googleapis.com/auth/spreadsheets'],
52
- });
53
- const sheets = googleapis_1.google.sheets({ version: 'v4', auth });
45
+ const sheets = this.sheetsClient;
54
46
  const spreadsheetId = this.getSpreadsheetId(spreadsheetUrl);
55
47
  await this.createNewSheetIfNotExists(spreadsheetUrl, sheetName);
56
48
  const response = await sheets.spreadsheets.values.update({
@@ -66,11 +58,7 @@ class GoogleSpreadsheetRepository {
66
58
  }
67
59
  };
68
60
  this.createNewSheetIfNotExists = async (spreadsheetUrl, sheetName) => {
69
- const auth = new googleapis_1.google.auth.GoogleAuth({
70
- keyFile: this.keyFile,
71
- scopes: ['https://www.googleapis.com/auth/spreadsheets'],
72
- });
73
- const sheets = googleapis_1.google.sheets({ version: 'v4', auth });
61
+ const sheets = this.sheetsClient;
74
62
  const spreadsheetId = this.getSpreadsheetId(spreadsheetUrl);
75
63
  const sheet = await this.getSheet(spreadsheetUrl, sheetName);
76
64
  if (sheet !== null) {
@@ -95,11 +83,7 @@ class GoogleSpreadsheetRepository {
95
83
  }
96
84
  };
97
85
  this.appendSheetValues = async (spreadsheetUrl, sheetName, values) => {
98
- const auth = new googleapis_1.google.auth.GoogleAuth({
99
- keyFile: this.keyFile,
100
- scopes: ['https://www.googleapis.com/auth/spreadsheets'],
101
- });
102
- const sheets = googleapis_1.google.sheets({ version: 'v4', auth });
86
+ const sheets = this.sheetsClient;
103
87
  const spreadsheetId = this.getSpreadsheetId(spreadsheetUrl);
104
88
  await this.createNewSheetIfNotExists(spreadsheetUrl, sheetName);
105
89
  const sheet = await this.getSheet(spreadsheetUrl, sheetName);
@@ -117,6 +101,26 @@ class GoogleSpreadsheetRepository {
117
101
  }
118
102
  };
119
103
  this.localStorageRepository.write(this.keyFile, serviceAccountKey);
104
+ this.sheetsClient = sheetsClientFactory
105
+ ? sheetsClientFactory()
106
+ : (() => {
107
+ const auth = new googleapis_1.google.auth.GoogleAuth({
108
+ keyFile: this.keyFile,
109
+ scopes: ['https://www.googleapis.com/auth/spreadsheets'],
110
+ });
111
+ const googleSheets = googleapis_1.google.sheets({ version: 'v4', auth });
112
+ return {
113
+ spreadsheets: {
114
+ get: (params) => googleSheets.spreadsheets.get(params),
115
+ values: {
116
+ get: (params) => googleSheets.spreadsheets.values.get(params),
117
+ update: (params) => googleSheets.spreadsheets.values.update(params),
118
+ append: (params) => googleSheets.spreadsheets.values.append(params),
119
+ },
120
+ batchUpdate: (params) => googleSheets.spreadsheets.batchUpdate(params),
121
+ },
122
+ };
123
+ })();
120
124
  }
121
125
  }
122
126
  exports.GoogleSpreadsheetRepository = GoogleSpreadsheetRepository;
@@ -1 +1 @@
1
- {"version":3,"file":"GoogleSpreadsheetRepository.js","sourceRoot":"","sources":["../../../src/adapter/repositories/GoogleSpreadsheetRepository.ts"],"names":[],"mappings":";;;;;;AACA,2CAAoC;AAEpC,oDAA4B;AAC5B,gBAAM,CAAC,MAAM,EAAE,CAAC;AAEhB,MAAa,2BAA2B;IAGtC,YACW,sBAA8C,EACvD,oBAA4B,OAAO,CAAC,GAAG,CAAC,0BAA0B;QAChE,OAAO;QAFA,2BAAsB,GAAtB,sBAAsB,CAAwB;QAHzD,YAAO,GAAG,gCAAgC,CAAC;QAU3C,qBAAgB,GAAG,CAAC,cAAsB,EAAU,EAAE;YACpD,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,cAAc,CAAC,CAAC;YACpC,OAAO,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;QACpC,CAAC,CAAC;QACF,aAAQ,GAAG,KAAK,EACd,cAAsB,EACtB,SAAiB,EACW,EAAE;YAC9B,MAAM,IAAI,GAAG,IAAI,mBAAM,CAAC,IAAI,CAAC,UAAU,CAAC;gBACtC,OAAO,EAAE,IAAI,CAAC,OAAO;gBACrB,MAAM,EAAE,CAAC,8CAA8C,CAAC;aACzD,CAAC,CAAC;YACH,MAAM,MAAM,GAAG,mBAAM,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;YACtD,MAAM,aAAa,GAAG,IAAI,CAAC,gBAAgB,CAAC,cAAc,CAAC,CAAC;YAC5D,MAAM,aAAa,GAAG,MAAM,MAAM,CAAC,YAAY,CAAC,GAAG,CAAC;gBAClD,aAAa;aACd,CAAC,CAAC;YACH,IAAI,aAAa,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;gBACjC,MAAM,IAAI,KAAK,CACb,wBAAwB,aAAa,CAAC,MAAM,KAAK,IAAI,CAAC,SAAS,CAAC,aAAa,CAAC,IAAI,CAAC,EAAE,CACtF,CAAC;YACJ,CAAC;YACD,MAAM,KAAK,GAAG,aAAa,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAC3C,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,EAAE,KAAK,KAAK,SAAS,CACzC,CAAC;YACF,IAAI,CAAC,KAAK,EAAE,CAAC;gBACX,OAAO,IAAI,CAAC;YACd,CAAC;YACD,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,YAAY,CAAC,MAAM,CAAC,GAAG,CAAC;gBACpD,aAAa;gBACb,KAAK,EAAE,SAAS;aACjB,CAAC,CAAC;YACH,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;gBAC5B,MAAM,IAAI,KAAK,CACb,wBAAwB,QAAQ,CAAC,MAAM,KAAK,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAC5E,CAAC;YACJ,CAAC;YACD,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;gBAC1B,OAAO,IAAI,CAAC;YACd,CAAC;YACD,OAAO,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAC5E,CAAC,CAAC;QACF,eAAU,GAAG,KAAK,EAChB,cAAsB,EACtB,SAAiB,EACjB,GAAW,EACX,MAAc,EACd,KAAa,EACE,EAAE;YACjB,MAAM,IAAI,GAAG,IAAI,mBAAM,CAAC,IAAI,CAAC,UAAU,CAAC;gBACtC,OAAO,EAAE,IAAI,CAAC,OAAO;gBACrB,MAAM,EAAE,CAAC,8CAA8C,CAAC;aACzD,CAAC,CAAC;YACH,MAAM,MAAM,GAAG,mBAAM,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;YACtD,MAAM,aAAa,GAAG,IAAI,CAAC,gBAAgB,CAAC,cAAc,CAAC,CAAC;YAC5D,MAAM,IAAI,CAAC,yBAAyB,CAAC,cAAc,EAAE,SAAS,CAAC,CAAC;YAChE,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,YAAY,CAAC,MAAM,CAAC,MAAM,CAAC;gBACvD,aAAa;gBACb,KAAK,EAAE,GAAG,SAAS,IAAI,MAAM,CAAC,YAAY,CAAC,EAAE,GAAG,MAAM,CAAC,GAAG,GAAG,GAAG,CAAC,EAAE;gBACnE,gBAAgB,EAAE,KAAK;gBACvB,WAAW,EAAE;oBACX,MAAM,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC;iBAClB;aACF,CAAC,CAAC;YACH,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;gBAC5B,MAAM,IAAI,KAAK,CACb,0BAA0B,QAAQ,CAAC,MAAM,KAAK,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAC9E,CAAC;YACJ,CAAC;QACH,CAAC,CAAC;QACF,8BAAyB,GAAG,KAAK,EAC/B,cAAsB,EACtB,SAAiB,EACF,EAAE;YACjB,MAAM,IAAI,GAAG,IAAI,mBAAM,CAAC,IAAI,CAAC,UAAU,CAAC;gBACtC,OAAO,EAAE,IAAI,CAAC,OAAO;gBACrB,MAAM,EAAE,CAAC,8CAA8C,CAAC;aACzD,CAAC,CAAC;YACH,MAAM,MAAM,GAAG,mBAAM,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;YACtD,MAAM,aAAa,GAAG,IAAI,CAAC,gBAAgB,CAAC,cAAc,CAAC,CAAC;YAC5D,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,cAAc,EAAE,SAAS,CAAC,CAAC;YAC7D,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;gBACnB,OAAO;YACT,CAAC;YACD,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,YAAY,CAAC,WAAW,CAAC;gBACrD,aAAa;gBACb,WAAW,EAAE;oBACX,QAAQ,EAAE;wBACR;4BACE,QAAQ,EAAE;gCACR,UAAU,EAAE;oCACV,KAAK,EAAE,SAAS;iCACjB;6BACF;yBACF;qBACF;iBACF;aACF,CAAC,CAAC;YACH,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;gBAC5B,MAAM,IAAI,KAAK,CACb,2BAA2B,QAAQ,CAAC,MAAM,KAAK,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAC/E,CAAC;YACJ,CAAC;QACH,CAAC,CAAC;QAEF,sBAAiB,GAAG,KAAK,EACvB,cAAsB,EACtB,SAAiB,EACjB,MAAkB,EACH,EAAE;YACjB,MAAM,IAAI,GAAG,IAAI,mBAAM,CAAC,IAAI,CAAC,UAAU,CAAC;gBACtC,OAAO,EAAE,IAAI,CAAC,OAAO;gBACrB,MAAM,EAAE,CAAC,8CAA8C,CAAC;aACzD,CAAC,CAAC;YACH,MAAM,MAAM,GAAG,mBAAM,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;YACtD,MAAM,aAAa,GAAG,IAAI,CAAC,gBAAgB,CAAC,cAAc,CAAC,CAAC;YAC5D,MAAM,IAAI,CAAC,yBAAyB,CAAC,cAAc,EAAE,SAAS,CAAC,CAAC;YAChE,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,cAAc,EAAE,SAAS,CAAC,CAAC;YAC7D,MAAM,KAAK,GAAG,GAAG,SAAS,KAAK,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;YAChE,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,YAAY,CAAC,MAAM,CAAC,MAAM,CAAC;gBACvD,aAAa;gBACb,KAAK,EAAE,KAAK;gBACZ,gBAAgB,EAAE,KAAK;gBACvB,WAAW,EAAE;oBACX,MAAM;iBACP;aACF,CAAC,CAAC;YACH,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;gBAC5B,MAAM,IAAI,KAAK,CACb,4BAA4B,QAAQ,CAAC,MAAM,KAAK,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAChF,CAAC;YACJ,CAAC;QACH,CAAC,CAAC;QAvIA,IAAI,CAAC,sBAAsB,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,iBAAiB,CAAC,CAAC;IACrE,CAAC;CAuIF;AAhJD,kEAgJC"}
1
+ {"version":3,"file":"GoogleSpreadsheetRepository.js","sourceRoot":"","sources":["../../../src/adapter/repositories/GoogleSpreadsheetRepository.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "github-issue-tower-defence-management",
3
- "version": "1.42.3",
3
+ "version": "1.42.4",
4
4
  "description": "",
5
5
  "main": "bin/index.js",
6
6
  "scripts": {
@@ -0,0 +1,120 @@
1
+ import dotenv from 'dotenv';
2
+ import { GoogleSpreadsheetRepository } from './GoogleSpreadsheetRepository';
3
+ import { LocalStorageRepository } from './LocalStorageRepository';
4
+
5
+ dotenv.config();
6
+
7
+ const GOOGLE_SERVICE_ACCOUNT_KEY = process.env.GOOGLE_SERVICE_ACCOUNT_KEY;
8
+
9
+ if (!GOOGLE_SERVICE_ACCOUNT_KEY) {
10
+ throw new Error('GOOGLE_SERVICE_ACCOUNT_KEY is required');
11
+ }
12
+
13
+ describe('GoogleSpreadsheetRepository integration tests', () => {
14
+ jest.setTimeout(60 * 1000);
15
+ jest.retryTimes(3, { logErrorsBeforeRetry: true });
16
+
17
+ const localStorageRepository = new LocalStorageRepository();
18
+ const spreadsheetUrl =
19
+ 'https://docs.google.com/spreadsheets/d/1N_3y0y46v5tHbra5YSm6PldflcsF1bkfeWDdQ3MRuXM/edit?gid=0#gid=0';
20
+ const repository = new GoogleSpreadsheetRepository(
21
+ localStorageRepository,
22
+ GOOGLE_SERVICE_ACCOUNT_KEY,
23
+ );
24
+
25
+ beforeEach(async () => {
26
+ await new Promise((resolve) => setTimeout(resolve, 2000));
27
+ });
28
+
29
+ describe('getSheet', () => {
30
+ const testCases: [string, string[][] | null][] = [
31
+ ['SheetUndefined', null],
32
+ ['SheetSingleCell', [['test']]],
33
+ [
34
+ 'SheetMultipleRows',
35
+ [
36
+ ['1', '2'],
37
+ ['3', '4'],
38
+ ],
39
+ ],
40
+ ];
41
+
42
+ test.each(testCases)(
43
+ 'gets sheet %s',
44
+ async (sheetName: string, expected: string[][] | null) => {
45
+ const result = await repository.getSheet(spreadsheetUrl, sheetName);
46
+ expect(result).toEqual(expected);
47
+ },
48
+ );
49
+
50
+ test('returns null for non-existent sheet', async () => {
51
+ const result = await repository.getSheet(
52
+ spreadsheetUrl,
53
+ 'NonExistentSheet',
54
+ );
55
+ expect(result).toBeNull();
56
+ });
57
+ });
58
+
59
+ describe('updateCell', () => {
60
+ const testCases: [string, number, number, string][] = [
61
+ ['Sheet1', 0, 0, 'First Value'],
62
+ ['Sheet1', 0, 0, 'Updated Value'],
63
+ ['Sheet1', 1, 1, '123'],
64
+ ['Sheet1', 2, 2, 'Test'],
65
+ ];
66
+
67
+ test.each(testCases)(
68
+ 'updates cell in sheet %s at row %d col %d with value %s',
69
+ async (sheetName: string, row: number, col: number, value: string) => {
70
+ await repository.updateCell(spreadsheetUrl, sheetName, row, col, value);
71
+ const result = await repository.getSheet(spreadsheetUrl, sheetName);
72
+ if (!result) {
73
+ throw new Error('Sheet not found');
74
+ }
75
+ expect(result[row][col]).toBe(value);
76
+ },
77
+ );
78
+ });
79
+
80
+ describe('appendSheetValues', () => {
81
+ const testCases: [string[][]][] = [
82
+ [[['Single Row']]],
83
+ [[['Multiple', 'Columns']]],
84
+ [
85
+ [
86
+ ['Row1Col1', 'Row1Col2'],
87
+ ['Row2Col1', 'Row2Col2'],
88
+ ],
89
+ ],
90
+ ];
91
+
92
+ test.each(testCases)(
93
+ 'appends values %j to sheet',
94
+ async (values: string[][]) => {
95
+ const sheetName = 'AppendTest';
96
+ const initialSheet = await repository.getSheet(
97
+ spreadsheetUrl,
98
+ sheetName,
99
+ );
100
+ const initialLength = initialSheet ? initialSheet.length : 0;
101
+
102
+ await repository.appendSheetValues(spreadsheetUrl, sheetName, values);
103
+
104
+ const updatedSheet = await repository.getSheet(
105
+ spreadsheetUrl,
106
+ sheetName,
107
+ );
108
+ expect(updatedSheet).not.toBeNull();
109
+ if (!updatedSheet) {
110
+ throw new Error('Sheet not found');
111
+ }
112
+ expect(updatedSheet.length).toBe(initialLength + values.length);
113
+
114
+ for (let i = 0; i < values.length; i++) {
115
+ expect(updatedSheet[initialLength + i]).toEqual(values[i]);
116
+ }
117
+ },
118
+ );
119
+ });
120
+ });
@@ -1,13 +1,56 @@
1
1
  import { GoogleSpreadsheetRepository } from './GoogleSpreadsheetRepository';
2
- import { describe, test, expect } from '@jest/globals';
2
+ import { describe, test, expect, jest, beforeEach } from '@jest/globals';
3
3
  import { LocalStorageRepository } from './LocalStorageRepository';
4
4
 
5
+ type SpreadsheetGetResponse = {
6
+ status: number;
7
+ data: {
8
+ sheets?: Array<{
9
+ properties?: { title?: string | null } | null;
10
+ }> | null;
11
+ };
12
+ };
13
+
14
+ type ValuesGetResponse = {
15
+ status: number;
16
+ data: { values?: unknown[][] | null };
17
+ };
18
+
19
+ type SimpleResponse = { status: number; data: unknown };
20
+
5
21
  describe('GoogleSpreadsheetRepository', () => {
6
22
  const localStorageRepository = new LocalStorageRepository();
7
- const repository = new GoogleSpreadsheetRepository(localStorageRepository);
8
23
  const spreadsheetUrl =
9
24
  'https://docs.google.com/spreadsheets/d/1N_3y0y46v5tHbra5YSm6PldflcsF1bkfeWDdQ3MRuXM/edit?gid=0#gid=0';
10
25
 
26
+ const mockSpreadsheetsGet = jest.fn<() => Promise<SpreadsheetGetResponse>>();
27
+ const mockSpreadsheetsValuesGet = jest.fn<() => Promise<ValuesGetResponse>>();
28
+ const mockSpreadsheetsValuesUpdate = jest.fn<() => Promise<SimpleResponse>>();
29
+ const mockSpreadsheetsValuesAppend = jest.fn<() => Promise<SimpleResponse>>();
30
+ const mockSpreadsheetsBatchUpdate = jest.fn<() => Promise<SimpleResponse>>();
31
+
32
+ const mockSheetsClient = {
33
+ spreadsheets: {
34
+ get: mockSpreadsheetsGet,
35
+ values: {
36
+ get: mockSpreadsheetsValuesGet,
37
+ update: mockSpreadsheetsValuesUpdate,
38
+ append: mockSpreadsheetsValuesAppend,
39
+ },
40
+ batchUpdate: mockSpreadsheetsBatchUpdate,
41
+ },
42
+ };
43
+
44
+ const repository = new GoogleSpreadsheetRepository(
45
+ localStorageRepository,
46
+ 'dummy-service-account-key',
47
+ () => mockSheetsClient,
48
+ );
49
+
50
+ beforeEach(() => {
51
+ jest.clearAllMocks();
52
+ });
53
+
11
54
  describe('getSpreadsheetId', () => {
12
55
  const testCases: [string, string][] = [
13
56
  [
@@ -29,96 +72,255 @@ describe('GoogleSpreadsheetRepository', () => {
29
72
  });
30
73
 
31
74
  describe('getSheet', () => {
32
- const testCases: [string, string, string[][] | null][] = [
33
- ['SheetUndefined', 'Undefined Sheet', null],
34
- // ['SheetEmpty', 'Empty Sheet', []],
35
- ['SheetSingleCell', 'Single Cell', [['test']]],
36
- [
37
- 'SheetMultipleRows',
38
- 'Multiple Rows',
39
- [
40
- ['1', '2'],
41
- ['3', '4'],
42
- ],
43
- ],
44
- ];
75
+ test('returns null when sheet is not in spreadsheet', async () => {
76
+ mockSpreadsheetsGet.mockResolvedValue({
77
+ status: 200,
78
+ data: { sheets: [] },
79
+ });
45
80
 
46
- test.each(testCases)(
47
- 'gets sheet %s with %s',
48
- async (sheetName: string, _: string, expected: string[][] | null) => {
49
- const result = await repository.getSheet(spreadsheetUrl, sheetName);
50
- expect(result).toEqual(expected);
51
- },
52
- );
53
-
54
- test('returns null for non-existent sheet', async () => {
55
81
  const result = await repository.getSheet(
56
82
  spreadsheetUrl,
57
83
  'NonExistentSheet',
58
84
  );
85
+
86
+ expect(result).toBeNull();
87
+ });
88
+
89
+ test('returns null when sheet name does not match', async () => {
90
+ mockSpreadsheetsGet.mockResolvedValue({
91
+ status: 200,
92
+ data: {
93
+ sheets: [{ properties: { title: 'OtherSheet' } }],
94
+ },
95
+ });
96
+
97
+ const result = await repository.getSheet(
98
+ spreadsheetUrl,
99
+ 'SheetUndefined',
100
+ );
101
+
102
+ expect(result).toBeNull();
103
+ });
104
+
105
+ test('returns single cell value', async () => {
106
+ mockSpreadsheetsGet.mockResolvedValue({
107
+ status: 200,
108
+ data: {
109
+ sheets: [{ properties: { title: 'SheetSingleCell' } }],
110
+ },
111
+ });
112
+ mockSpreadsheetsValuesGet.mockResolvedValue({
113
+ status: 200,
114
+ data: { values: [['test']] },
115
+ });
116
+
117
+ const result = await repository.getSheet(
118
+ spreadsheetUrl,
119
+ 'SheetSingleCell',
120
+ );
121
+
122
+ expect(result).toEqual([['test']]);
123
+ });
124
+
125
+ test('returns multiple rows', async () => {
126
+ mockSpreadsheetsGet.mockResolvedValue({
127
+ status: 200,
128
+ data: {
129
+ sheets: [{ properties: { title: 'SheetMultipleRows' } }],
130
+ },
131
+ });
132
+ mockSpreadsheetsValuesGet.mockResolvedValue({
133
+ status: 200,
134
+ data: {
135
+ values: [
136
+ ['1', '2'],
137
+ ['3', '4'],
138
+ ],
139
+ },
140
+ });
141
+
142
+ const result = await repository.getSheet(
143
+ spreadsheetUrl,
144
+ 'SheetMultipleRows',
145
+ );
146
+
147
+ expect(result).toEqual([
148
+ ['1', '2'],
149
+ ['3', '4'],
150
+ ]);
151
+ });
152
+
153
+ test('returns null when sheet has no values', async () => {
154
+ mockSpreadsheetsGet.mockResolvedValue({
155
+ status: 200,
156
+ data: {
157
+ sheets: [{ properties: { title: 'EmptySheet' } }],
158
+ },
159
+ });
160
+ mockSpreadsheetsValuesGet.mockResolvedValue({
161
+ status: 200,
162
+ data: {},
163
+ });
164
+
165
+ const result = await repository.getSheet(spreadsheetUrl, 'EmptySheet');
166
+
59
167
  expect(result).toBeNull();
60
168
  });
169
+
170
+ test('converts non-string cell values to strings', async () => {
171
+ mockSpreadsheetsGet.mockResolvedValue({
172
+ status: 200,
173
+ data: {
174
+ sheets: [{ properties: { title: 'MixedTypes' } }],
175
+ },
176
+ });
177
+ mockSpreadsheetsValuesGet.mockResolvedValue({
178
+ status: 200,
179
+ data: { values: [[1, true, 'text']] },
180
+ });
181
+
182
+ const result = await repository.getSheet(spreadsheetUrl, 'MixedTypes');
183
+
184
+ expect(result).toEqual([['1', 'true', 'text']]);
185
+ });
61
186
  });
62
187
 
63
188
  describe('updateCell', () => {
64
- const testCases: [string, number, number, string][] = [
65
- ['Sheet1', 0, 0, 'First Value'],
66
- ['Sheet1', 0, 0, 'Updated Value'],
67
- ['Sheet1', 1, 1, '123'],
68
- ['Sheet1', 2, 2, 'Test'],
189
+ beforeEach(() => {
190
+ mockSpreadsheetsGet.mockResolvedValue({
191
+ status: 200,
192
+ data: {
193
+ sheets: [{ properties: { title: 'Sheet1' } }],
194
+ },
195
+ });
196
+ mockSpreadsheetsValuesGet.mockResolvedValue({
197
+ status: 200,
198
+ data: { values: [['existing']] },
199
+ });
200
+ mockSpreadsheetsValuesUpdate.mockResolvedValue({
201
+ status: 200,
202
+ data: {},
203
+ });
204
+ });
205
+
206
+ const testCases: [string, number, number, string, string][] = [
207
+ ['Sheet1', 0, 0, 'First Value', 'Sheet1!A1'],
208
+ ['Sheet1', 0, 0, 'Updated Value', 'Sheet1!A1'],
209
+ ['Sheet1', 1, 1, '123', 'Sheet1!B2'],
210
+ ['Sheet1', 2, 2, 'Test', 'Sheet1!C3'],
69
211
  ];
70
212
 
71
213
  test.each(testCases)(
72
- 'updates cell in sheet %s at row %d col %d with value %s',
73
- async (sheetName: string, row: number, col: number, value: string) => {
214
+ 'updates cell in sheet %s at row %d col %d with value %s using range %s',
215
+ async (sheetName, row, col, value, expectedRange) => {
74
216
  await repository.updateCell(spreadsheetUrl, sheetName, row, col, value);
75
- const result = await repository.getSheet(spreadsheetUrl, sheetName);
76
- if (!result) {
77
- throw new Error('Sheet not found');
78
- }
79
- expect(result[row][col]).toBe(value);
217
+
218
+ expect(mockSpreadsheetsValuesUpdate).toHaveBeenCalledWith(
219
+ expect.objectContaining({
220
+ range: expectedRange,
221
+ valueInputOption: 'RAW',
222
+ requestBody: { values: [[value]] },
223
+ }),
224
+ );
80
225
  },
81
226
  );
82
227
  });
83
228
 
84
229
  describe('appendSheetValues', () => {
85
- const testCases: [string[][]][] = [
86
- [[['Single Row']]],
87
- [[['Multiple', 'Columns']]],
88
- [
89
- [
90
- ['Row1Col1', 'Row1Col2'],
91
- ['Row2Col1', 'Row2Col2'],
92
- ],
93
- ],
94
- ];
230
+ test('appends to existing sheet starting after last row', async () => {
231
+ mockSpreadsheetsGet.mockResolvedValue({
232
+ status: 200,
233
+ data: {
234
+ sheets: [{ properties: { title: 'AppendTest' } }],
235
+ },
236
+ });
237
+ mockSpreadsheetsValuesGet.mockResolvedValue({
238
+ status: 200,
239
+ data: { values: [['Row1'], ['Row2']] },
240
+ });
241
+ mockSpreadsheetsValuesAppend.mockResolvedValue({
242
+ status: 200,
243
+ data: {},
244
+ });
95
245
 
96
- test.each(testCases)(
97
- 'appends values %j to sheet',
98
- async (values: string[][]) => {
99
- const sheetName = 'AppendTest';
100
- const initialSheet = await repository.getSheet(
101
- spreadsheetUrl,
102
- sheetName,
103
- );
104
- const initialLength = initialSheet ? initialSheet.length : 0;
246
+ await repository.appendSheetValues(spreadsheetUrl, 'AppendTest', [
247
+ ['NewRow'],
248
+ ]);
105
249
 
106
- await repository.appendSheetValues(spreadsheetUrl, sheetName, values);
250
+ expect(mockSpreadsheetsValuesAppend).toHaveBeenCalledWith(
251
+ expect.objectContaining({
252
+ range: 'AppendTest!A3:A',
253
+ valueInputOption: 'RAW',
254
+ requestBody: { values: [['NewRow']] },
255
+ }),
256
+ );
257
+ });
107
258
 
108
- const updatedSheet = await repository.getSheet(
109
- spreadsheetUrl,
110
- sheetName,
111
- );
112
- expect(updatedSheet).not.toBeNull();
113
- if (!updatedSheet) {
114
- throw new Error('Sheet not found');
115
- }
116
- expect(updatedSheet.length).toBe(initialLength + values.length);
117
-
118
- for (let i = 0; i < values.length; i++) {
119
- expect(updatedSheet[initialLength + i]).toEqual(values[i]);
120
- }
121
- },
122
- );
259
+ test('appends multiple rows with correct values', async () => {
260
+ mockSpreadsheetsGet.mockResolvedValue({
261
+ status: 200,
262
+ data: {
263
+ sheets: [{ properties: { title: 'AppendTest' } }],
264
+ },
265
+ });
266
+ mockSpreadsheetsValuesGet.mockResolvedValue({
267
+ status: 200,
268
+ data: { values: [['Existing']] },
269
+ });
270
+ mockSpreadsheetsValuesAppend.mockResolvedValue({
271
+ status: 200,
272
+ data: {},
273
+ });
274
+
275
+ const newValues = [
276
+ ['Row1Col1', 'Row1Col2'],
277
+ ['Row2Col1', 'Row2Col2'],
278
+ ];
279
+ await repository.appendSheetValues(
280
+ spreadsheetUrl,
281
+ 'AppendTest',
282
+ newValues,
283
+ );
284
+
285
+ expect(mockSpreadsheetsValuesAppend).toHaveBeenCalledWith(
286
+ expect.objectContaining({
287
+ requestBody: { values: newValues },
288
+ }),
289
+ );
290
+ });
291
+
292
+ test('creates new sheet via batchUpdate when sheet does not exist', async () => {
293
+ mockSpreadsheetsGet.mockResolvedValue({
294
+ status: 200,
295
+ data: { sheets: [] },
296
+ });
297
+ mockSpreadsheetsBatchUpdate.mockResolvedValue({
298
+ status: 200,
299
+ data: {},
300
+ });
301
+ mockSpreadsheetsValuesGet.mockResolvedValue({
302
+ status: 200,
303
+ data: {},
304
+ });
305
+ mockSpreadsheetsValuesAppend.mockResolvedValue({
306
+ status: 200,
307
+ data: {},
308
+ });
309
+
310
+ await repository.appendSheetValues(spreadsheetUrl, 'NewSheet', [['Row']]);
311
+
312
+ expect(mockSpreadsheetsBatchUpdate).toHaveBeenCalledWith(
313
+ expect.objectContaining({
314
+ requestBody: {
315
+ requests: [{ addSheet: { properties: { title: 'NewSheet' } } }],
316
+ },
317
+ }),
318
+ );
319
+ expect(mockSpreadsheetsValuesAppend).toHaveBeenCalledWith(
320
+ expect.objectContaining({
321
+ range: 'NewSheet!A1:A',
322
+ }),
323
+ );
324
+ });
123
325
  });
124
326
  });
@@ -4,15 +4,93 @@ import { LocalStorageRepository } from './LocalStorageRepository';
4
4
  import dotenv from 'dotenv';
5
5
  dotenv.config();
6
6
 
7
+ interface SheetsApiClient {
8
+ spreadsheets: {
9
+ get(params: { spreadsheetId: string }): Promise<{
10
+ status: number;
11
+ data: {
12
+ sheets?: Array<{
13
+ properties?: { title?: string | null } | null;
14
+ }> | null;
15
+ };
16
+ }>;
17
+ values: {
18
+ get(params: { spreadsheetId: string; range: string }): Promise<{
19
+ status: number;
20
+ data: { values?: unknown[][] | null };
21
+ }>;
22
+ update(params: {
23
+ spreadsheetId: string;
24
+ range: string;
25
+ valueInputOption: string;
26
+ requestBody: { values: string[][] };
27
+ }): Promise<{ status: number; data: unknown }>;
28
+ append(params: {
29
+ spreadsheetId: string;
30
+ range: string;
31
+ valueInputOption: string;
32
+ requestBody: { values: string[][] };
33
+ }): Promise<{ status: number; data: unknown }>;
34
+ };
35
+ batchUpdate(params: {
36
+ spreadsheetId: string;
37
+ requestBody: {
38
+ requests: Array<{ addSheet?: { properties?: { title?: string } } }>;
39
+ };
40
+ }): Promise<{ status: number; data: unknown }>;
41
+ };
42
+ }
43
+
7
44
  export class GoogleSpreadsheetRepository implements SpreadsheetRepository {
8
45
  keyFile = './tmp/service-account-key.json';
46
+ private readonly sheetsClient: SheetsApiClient;
9
47
 
10
48
  constructor(
11
49
  readonly localStorageRepository: LocalStorageRepository,
12
50
  serviceAccountKey: string = process.env.GOOGLE_SERVICE_ACCOUNT_KEY ||
13
51
  'dummy',
52
+ sheetsClientFactory?: () => SheetsApiClient,
14
53
  ) {
15
54
  this.localStorageRepository.write(this.keyFile, serviceAccountKey);
55
+ this.sheetsClient = sheetsClientFactory
56
+ ? sheetsClientFactory()
57
+ : (() => {
58
+ const auth = new google.auth.GoogleAuth({
59
+ keyFile: this.keyFile,
60
+ scopes: ['https://www.googleapis.com/auth/spreadsheets'],
61
+ });
62
+ const googleSheets = google.sheets({ version: 'v4', auth });
63
+ return {
64
+ spreadsheets: {
65
+ get: (params: { spreadsheetId: string }) =>
66
+ googleSheets.spreadsheets.get(params),
67
+ values: {
68
+ get: (params: { spreadsheetId: string; range: string }) =>
69
+ googleSheets.spreadsheets.values.get(params),
70
+ update: (params: {
71
+ spreadsheetId: string;
72
+ range: string;
73
+ valueInputOption: string;
74
+ requestBody: { values: string[][] };
75
+ }) => googleSheets.spreadsheets.values.update(params),
76
+ append: (params: {
77
+ spreadsheetId: string;
78
+ range: string;
79
+ valueInputOption: string;
80
+ requestBody: { values: string[][] };
81
+ }) => googleSheets.spreadsheets.values.append(params),
82
+ },
83
+ batchUpdate: (params: {
84
+ spreadsheetId: string;
85
+ requestBody: {
86
+ requests: Array<{
87
+ addSheet?: { properties?: { title?: string } };
88
+ }>;
89
+ };
90
+ }) => googleSheets.spreadsheets.batchUpdate(params),
91
+ },
92
+ };
93
+ })();
16
94
  }
17
95
 
18
96
  getSpreadsheetId = (spreadsheetUrl: string): string => {
@@ -23,11 +101,7 @@ export class GoogleSpreadsheetRepository implements SpreadsheetRepository {
23
101
  spreadsheetUrl: string,
24
102
  sheetName: string,
25
103
  ): Promise<string[][] | null> => {
26
- const auth = new google.auth.GoogleAuth({
27
- keyFile: this.keyFile,
28
- scopes: ['https://www.googleapis.com/auth/spreadsheets'],
29
- });
30
- const sheets = google.sheets({ version: 'v4', auth });
104
+ const sheets = this.sheetsClient;
31
105
  const spreadsheetId = this.getSpreadsheetId(spreadsheetUrl);
32
106
  const responseSheet = await sheets.spreadsheets.get({
33
107
  spreadsheetId,
@@ -64,11 +138,7 @@ export class GoogleSpreadsheetRepository implements SpreadsheetRepository {
64
138
  column: number,
65
139
  value: string,
66
140
  ): Promise<void> => {
67
- const auth = new google.auth.GoogleAuth({
68
- keyFile: this.keyFile,
69
- scopes: ['https://www.googleapis.com/auth/spreadsheets'],
70
- });
71
- const sheets = google.sheets({ version: 'v4', auth });
141
+ const sheets = this.sheetsClient;
72
142
  const spreadsheetId = this.getSpreadsheetId(spreadsheetUrl);
73
143
  await this.createNewSheetIfNotExists(spreadsheetUrl, sheetName);
74
144
  const response = await sheets.spreadsheets.values.update({
@@ -89,11 +159,7 @@ export class GoogleSpreadsheetRepository implements SpreadsheetRepository {
89
159
  spreadsheetUrl: string,
90
160
  sheetName: string,
91
161
  ): Promise<void> => {
92
- const auth = new google.auth.GoogleAuth({
93
- keyFile: this.keyFile,
94
- scopes: ['https://www.googleapis.com/auth/spreadsheets'],
95
- });
96
- const sheets = google.sheets({ version: 'v4', auth });
162
+ const sheets = this.sheetsClient;
97
163
  const spreadsheetId = this.getSpreadsheetId(spreadsheetUrl);
98
164
  const sheet = await this.getSheet(spreadsheetUrl, sheetName);
99
165
  if (sheet !== null) {
@@ -125,11 +191,7 @@ export class GoogleSpreadsheetRepository implements SpreadsheetRepository {
125
191
  sheetName: string,
126
192
  values: string[][],
127
193
  ): Promise<void> => {
128
- const auth = new google.auth.GoogleAuth({
129
- keyFile: this.keyFile,
130
- scopes: ['https://www.googleapis.com/auth/spreadsheets'],
131
- });
132
- const sheets = google.sheets({ version: 'v4', auth });
194
+ const sheets = this.sheetsClient;
133
195
  const spreadsheetId = this.getSpreadsheetId(spreadsheetUrl);
134
196
  await this.createNewSheetIfNotExists(spreadsheetUrl, sheetName);
135
197
  const sheet = await this.getSheet(spreadsheetUrl, sheetName);
@@ -1,13 +1,79 @@
1
1
  import { SpreadsheetRepository } from '../../domain/usecases/adapter-interfaces/SpreadsheetRepository';
2
2
  import { LocalStorageRepository } from './LocalStorageRepository';
3
+ interface SheetsApiClient {
4
+ spreadsheets: {
5
+ get(params: {
6
+ spreadsheetId: string;
7
+ }): Promise<{
8
+ status: number;
9
+ data: {
10
+ sheets?: Array<{
11
+ properties?: {
12
+ title?: string | null;
13
+ } | null;
14
+ }> | null;
15
+ };
16
+ }>;
17
+ values: {
18
+ get(params: {
19
+ spreadsheetId: string;
20
+ range: string;
21
+ }): Promise<{
22
+ status: number;
23
+ data: {
24
+ values?: unknown[][] | null;
25
+ };
26
+ }>;
27
+ update(params: {
28
+ spreadsheetId: string;
29
+ range: string;
30
+ valueInputOption: string;
31
+ requestBody: {
32
+ values: string[][];
33
+ };
34
+ }): Promise<{
35
+ status: number;
36
+ data: unknown;
37
+ }>;
38
+ append(params: {
39
+ spreadsheetId: string;
40
+ range: string;
41
+ valueInputOption: string;
42
+ requestBody: {
43
+ values: string[][];
44
+ };
45
+ }): Promise<{
46
+ status: number;
47
+ data: unknown;
48
+ }>;
49
+ };
50
+ batchUpdate(params: {
51
+ spreadsheetId: string;
52
+ requestBody: {
53
+ requests: Array<{
54
+ addSheet?: {
55
+ properties?: {
56
+ title?: string;
57
+ };
58
+ };
59
+ }>;
60
+ };
61
+ }): Promise<{
62
+ status: number;
63
+ data: unknown;
64
+ }>;
65
+ };
66
+ }
3
67
  export declare class GoogleSpreadsheetRepository implements SpreadsheetRepository {
4
68
  readonly localStorageRepository: LocalStorageRepository;
5
69
  keyFile: string;
6
- constructor(localStorageRepository: LocalStorageRepository, serviceAccountKey?: string);
70
+ private readonly sheetsClient;
71
+ constructor(localStorageRepository: LocalStorageRepository, serviceAccountKey?: string, sheetsClientFactory?: () => SheetsApiClient);
7
72
  getSpreadsheetId: (spreadsheetUrl: string) => string;
8
73
  getSheet: (spreadsheetUrl: string, sheetName: string) => Promise<string[][] | null>;
9
74
  updateCell: (spreadsheetUrl: string, sheetName: string, row: number, column: number, value: string) => Promise<void>;
10
75
  createNewSheetIfNotExists: (spreadsheetUrl: string, sheetName: string) => Promise<void>;
11
76
  appendSheetValues: (spreadsheetUrl: string, sheetName: string, values: string[][]) => Promise<void>;
12
77
  }
78
+ export {};
13
79
  //# sourceMappingURL=GoogleSpreadsheetRepository.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"GoogleSpreadsheetRepository.d.ts","sourceRoot":"","sources":["../../../src/adapter/repositories/GoogleSpreadsheetRepository.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,MAAM,gEAAgE,CAAC;AAEvG,OAAO,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAIlE,qBAAa,2BAA4B,YAAW,qBAAqB;IAIrE,QAAQ,CAAC,sBAAsB,EAAE,sBAAsB;IAHzD,OAAO,SAAoC;gBAGhC,sBAAsB,EAAE,sBAAsB,EACvD,iBAAiB,GAAE,MACV;IAKX,gBAAgB,GAAI,gBAAgB,MAAM,KAAG,MAAM,CAGjD;IACF,QAAQ,GACN,gBAAgB,MAAM,EACtB,WAAW,MAAM,KAChB,OAAO,CAAC,MAAM,EAAE,EAAE,GAAG,IAAI,CAAC,CAkC3B;IACF,UAAU,GACR,gBAAgB,MAAM,EACtB,WAAW,MAAM,EACjB,KAAK,MAAM,EACX,QAAQ,MAAM,EACd,OAAO,MAAM,KACZ,OAAO,CAAC,IAAI,CAAC,CAqBd;IACF,yBAAyB,GACvB,gBAAgB,MAAM,EACtB,WAAW,MAAM,KAChB,OAAO,CAAC,IAAI,CAAC,CA8Bd;IAEF,iBAAiB,GACf,gBAAgB,MAAM,EACtB,WAAW,MAAM,EACjB,QAAQ,MAAM,EAAE,EAAE,KACjB,OAAO,CAAC,IAAI,CAAC,CAuBd;CACH"}
1
+ {"version":3,"file":"GoogleSpreadsheetRepository.d.ts","sourceRoot":"","sources":["../../../src/adapter/repositories/GoogleSpreadsheetRepository.ts"],"names":[],"mappings":"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"}