From 0812cd9b887e5e1b4683e5efa88bd7965bb00b32 Mon Sep 17 00:00:00 2001 From: fakeminjun7321 Date: Thu, 16 Jul 2026 12:37:02 +0900 Subject: [PATCH] fix: select connected Drive owner account --- .../server/services/google-drive-files.js | 35 ++++++++++++- .../services/google-drive-files.test.js | 52 +++++++++++++++++++ 2 files changed, 86 insertions(+), 1 deletion(-) diff --git a/apps/classbot/server/services/google-drive-files.js b/apps/classbot/server/services/google-drive-files.js index 01bf16b..1992f09 100644 --- a/apps/classbot/server/services/google-drive-files.js +++ b/apps/classbot/server/services/google-drive-files.js @@ -17,6 +17,18 @@ function defaultDependencies() { const supa = require("../../../../lib/supabase.js"); return { findUserByName: (name) => supa.findUserByName(name), + findUsersByExactName: async (name) => { + const raw = String(name || "").trim(); + const client = supa.getClient(); + if (!client || !raw) return []; + const { data, error } = await client + .from("users") + .select("id,name") + .eq("name", raw) + .limit(20); + if (error) throw new Error(`Google Drive 운영 계정 조회 실패: ${error.message}`); + return (data || []).filter((user) => user?.id && user.name === raw); + }, getCloudConnection: (userId, provider) => supa.getCloudConnection(userId, provider), decryptToken: (value) => cloud.decryptToken(value), getAccessToken: (refreshToken) => cloud.googleAccessToken(refreshToken), @@ -77,7 +89,28 @@ export class GoogleDriveFileProvider { async resolveOwnerUserId(dependencies) { if (this.ownerUserId) return this.ownerUserId; - if (!this.ownerName || typeof dependencies.findUserByName !== "function") { + if (!this.ownerName) { + throw new Error("Google Drive 자료실 운영 계정을 찾을 수 없습니다."); + } + + if (typeof dependencies.findUsersByExactName === "function") { + const candidates = await dependencies.findUsersByExactName(this.ownerName); + const exact = (Array.isArray(candidates) ? candidates : []) + .filter((owner) => owner?.id && owner.name === this.ownerName); + const connected = (await Promise.all(exact.map(async (owner) => ({ + owner, + connection: await dependencies.getCloudConnection(owner.id, "google"), + })))).filter(({ connection }) => connection?.refresh_token); + if (connected.length !== 1) { + throw new Error(connected.length > 1 + ? "동일한 이름의 Google Drive 연결 계정이 여러 개라 운영 계정을 자동 선택할 수 없습니다." + : "Google Drive가 연결된 자료실 운영 계정을 찾을 수 없습니다."); + } + this.ownerUserId = safeIdentifier(connected[0].owner.id, "Google Drive 운영 계정"); + return this.ownerUserId; + } + + if (typeof dependencies.findUserByName !== "function") { throw new Error("Google Drive 자료실 운영 계정을 찾을 수 없습니다."); } const owner = await dependencies.findUserByName(this.ownerName); diff --git a/apps/classbot/server/services/google-drive-files.test.js b/apps/classbot/server/services/google-drive-files.test.js index 87ec791..749a147 100644 --- a/apps/classbot/server/services/google-drive-files.test.js +++ b/apps/classbot/server/services/google-drive-files.test.js @@ -121,6 +121,58 @@ test("owner UUID가 없어도 기존 Quilo 계정의 정확한 이름으로 Driv assert.equal(provider.ownerUserId, "resolved-owner-user"); }); +test("동명이인 계정 중 Google 연결이 정확히 하나인 후보를 운영 계정으로 선택한다", async () => { + const { dependencies, calls } = fixture(); + dependencies.findUsersByExactName = async (name) => { + calls.push(["owners", name]); + return [ + { id: "duplicate-without-drive", name: "구민준" }, + { id: "connected-owner", name: "구민준" }, + { id: "different-name", name: "구민준2" }, + ]; + }; + dependencies.getCloudConnection = async (userId, provider) => { + calls.push(["connection", userId, provider]); + return userId === "connected-owner" ? { refresh_token: "connected-refresh" } : null; + }; + const provider = new GoogleDriveFileProvider({ + ownerName: "구민준", + secret: "drive-provider-test-secret", + dependencies, + }); + + const items = await provider.listFiles(); + assert.equal(items.length, 1); + assert.equal(provider.ownerUserId, "connected-owner"); + assert.deepEqual(calls.find(([kind]) => kind === "owners"), ["owners", "구민준"]); + assert.equal(calls.some((call) => call[0] === "connection" && call[1] === "duplicate-without-drive"), true); + assert.equal(calls.some((call) => call[0] === "connection" && call[1] === "connected-owner"), true); + assert.equal(calls.some((call) => call[0] === "connection" && call[1] === "different-name"), false); +}); + +test("동명이인 중 Google 연결 후보가 0개 또는 여러 개면 fail-closed한다", async () => { + for (const connectedIds of [[], ["owner-1", "owner-2"]]) { + const { dependencies } = fixture(); + dependencies.findUsersByExactName = async () => [ + { id: "owner-1", name: "구민준" }, + { id: "owner-2", name: "구민준" }, + ]; + dependencies.getCloudConnection = async (userId) => ( + connectedIds.includes(userId) ? { refresh_token: `refresh:${userId}` } : null + ); + const provider = new GoogleDriveFileProvider({ + ownerName: "구민준", + secret: "drive-provider-test-secret", + dependencies, + }); + await assert.rejects( + () => provider.listFiles(), + connectedIds.length ? /여러 개라 운영 계정을 자동 선택/ : /연결된 자료실 운영 계정을 찾을 수 없습니다/, + ); + assert.equal(provider.ownerUserId, ""); + } +}); + test("Drive 다운로드는 서명된 provider ID와 설정 폴더 parent를 모두 검증한다", async () => { const { provider, sourceFiles } = fixture(); const [item] = await provider.listFiles();