Skip to content

Commit 830d9c9

Browse files
iQQBotkylos101
andauthored
[server] tolerate deleted users for listWorkspaceSessions (#20943)
Co-authored-by: Kyle Brennan <[email protected]>
1 parent b0df8ec commit 830d9c9

File tree

2 files changed

+261
-11
lines changed

2 files changed

+261
-11
lines changed
Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
/**
2+
* Copyright (c) 2023 Gitpod GmbH. All rights reserved.
3+
* Licensed under the GNU Affero General Public License (AGPL).
4+
* See License.AGPL.txt in the project root for license information.
5+
*/
6+
7+
import * as chai from "chai";
8+
import { WorkspaceServiceAPI } from "./workspace-service-api";
9+
import { WorkspaceService } from "../workspace/workspace-service";
10+
import { UserService } from "../user/user-service";
11+
import { PublicAPIConverter } from "@gitpod/public-api-common/lib/public-api-converter";
12+
import { ContextService } from "../workspace/context-service";
13+
import { ContextParser } from "../workspace/context-parser-service";
14+
import { ListWorkspaceSessionsRequest, WorkspaceSession_Owner } from "@gitpod/public-api/lib/gitpod/v1/workspace_pb";
15+
import { ApplicationError, ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error";
16+
import { WorkspaceSession, User } from "@gitpod/gitpod-protocol";
17+
18+
const expect = chai.expect;
19+
20+
describe("WorkspaceServiceAPI", function () {
21+
let workspaceServiceAPI: WorkspaceServiceAPI;
22+
let originalCtxUserId: any;
23+
let originalRunWithSubjectId: any;
24+
25+
beforeEach(function () {
26+
workspaceServiceAPI = new WorkspaceServiceAPI();
27+
28+
// Mock the request context functions
29+
const requestContext = require("../util/request-context");
30+
originalCtxUserId = requestContext.ctxUserId;
31+
originalRunWithSubjectId = requestContext.runWithSubjectId;
32+
33+
// Stub context functions
34+
requestContext.ctxUserId = () => "current-user-id";
35+
requestContext.runWithSubjectId = async (userId: any, fn: any) => fn();
36+
});
37+
38+
afterEach(function () {
39+
// Restore original functions
40+
if (originalCtxUserId) {
41+
require("../util/request-context").ctxUserId = originalCtxUserId;
42+
}
43+
if (originalRunWithSubjectId) {
44+
require("../util/request-context").runWithSubjectId = originalRunWithSubjectId;
45+
}
46+
});
47+
48+
describe("listWorkspaceSessions with deleted users", function () {
49+
it("should handle deleted users gracefully and show 'Deleted User'", async function () {
50+
// Create mock dependencies
51+
const mockWorkspaceService = {
52+
listWorkspaceSessions: async () =>
53+
[
54+
{
55+
workspace: { id: "ws-1", ownerId: "active-user-1" },
56+
instance: { id: "inst-1" },
57+
},
58+
{
59+
workspace: { id: "ws-2", ownerId: "deleted-user-1" },
60+
instance: { id: "inst-2" },
61+
},
62+
] as WorkspaceSession[],
63+
} as Partial<WorkspaceService>;
64+
65+
const mockUserService = {
66+
findUserById: async (currentUserId: string, userId: string) => {
67+
if (userId === "deleted-user-1") {
68+
throw new ApplicationError(ErrorCodes.NOT_FOUND, "not found: user deleted", {
69+
userDeleted: true,
70+
});
71+
}
72+
return {
73+
id: userId,
74+
fullName: "Active User",
75+
avatarUrl: "https://example.com/avatar.jpg",
76+
} as User;
77+
},
78+
} as Partial<UserService>;
79+
80+
const mockApiConverter = {
81+
toWorkspaceSession: (session: WorkspaceSession, owner?: WorkspaceSession_Owner) => ({
82+
id: session.instance.id,
83+
workspaceId: session.workspace.id,
84+
instanceId: session.instance.id,
85+
owner: owner,
86+
}),
87+
} as any;
88+
89+
// Inject mock dependencies
90+
(workspaceServiceAPI as any).workspaceService = mockWorkspaceService;
91+
(workspaceServiceAPI as any).userService = mockUserService;
92+
(workspaceServiceAPI as any).apiConverter = mockApiConverter;
93+
(workspaceServiceAPI as any).contextService = {} as ContextService;
94+
(workspaceServiceAPI as any).contextParser = {} as ContextParser;
95+
96+
// Create request
97+
const request = new ListWorkspaceSessionsRequest();
98+
request.organizationId = "550e8400-e29b-41d4-a716-446655440000"; // Valid UUID
99+
100+
// Call the actual method
101+
const response = await workspaceServiceAPI.listWorkspaceSessions(request, {} as any);
102+
103+
// Assertions
104+
expect(response.workspaceSessions).to.have.length(2);
105+
106+
// First session should have active user
107+
const firstSession = response.workspaceSessions[0];
108+
expect(firstSession.owner?.name).to.equal("Active User");
109+
expect(firstSession.owner?.id).to.equal("active-user-1");
110+
expect(firstSession.owner?.avatarUrl).to.equal("https://example.com/avatar.jpg");
111+
112+
// Second session should have "Deleted User" placeholder
113+
const secondSession = response.workspaceSessions[1];
114+
expect(secondSession.owner?.name).to.equal("Deleted User");
115+
expect(secondSession.owner?.id).to.equal("deleted-user-1");
116+
expect(secondSession.owner?.avatarUrl).to.equal("");
117+
});
118+
119+
it("should re-throw non-deletion errors", async function () {
120+
// Create mock dependencies that throw non-deletion error
121+
const mockWorkspaceService = {
122+
listWorkspaceSessions: async () =>
123+
[
124+
{
125+
workspace: { id: "ws-1", ownerId: "user-1" },
126+
instance: { id: "inst-1" },
127+
},
128+
] as WorkspaceSession[],
129+
} as Partial<WorkspaceService>;
130+
131+
const mockUserService = {
132+
findUserById: async () => {
133+
throw new ApplicationError(ErrorCodes.INTERNAL_SERVER_ERROR, "Database connection failed");
134+
},
135+
} as Partial<UserService>;
136+
137+
// Inject mock dependencies
138+
(workspaceServiceAPI as any).workspaceService = mockWorkspaceService;
139+
(workspaceServiceAPI as any).userService = mockUserService;
140+
(workspaceServiceAPI as any).apiConverter = {} as PublicAPIConverter;
141+
(workspaceServiceAPI as any).contextService = {} as ContextService;
142+
(workspaceServiceAPI as any).contextParser = {} as ContextParser;
143+
144+
// Create request
145+
const request = new ListWorkspaceSessionsRequest();
146+
request.organizationId = "550e8400-e29b-41d4-a716-446655440000"; // Valid UUID
147+
148+
// Should re-throw the non-deletion error
149+
try {
150+
await workspaceServiceAPI.listWorkspaceSessions(request, {} as any);
151+
expect.fail("Should have thrown an error");
152+
} catch (error) {
153+
expect(error.message).to.equal("Database connection failed");
154+
expect(error.code).to.equal(ErrorCodes.INTERNAL_SERVER_ERROR);
155+
}
156+
});
157+
158+
it("should handle sessions without ownerId", async function () {
159+
// Create mock dependencies
160+
const mockWorkspaceService = {
161+
listWorkspaceSessions: async () =>
162+
[
163+
{
164+
workspace: { id: "ws-1", ownerId: undefined },
165+
instance: { id: "inst-1" },
166+
},
167+
] as any,
168+
} as Partial<WorkspaceService>;
169+
170+
const mockUserService = {
171+
findUserById: async () => {
172+
throw new Error("Should not be called for sessions without owner");
173+
},
174+
} as Partial<UserService>;
175+
176+
const mockApiConverter = {
177+
toWorkspaceSession: (session: WorkspaceSession, owner?: WorkspaceSession_Owner) => ({
178+
id: session.instance.id,
179+
workspaceId: session.workspace.id,
180+
instanceId: session.instance.id,
181+
owner: owner,
182+
}),
183+
} as any;
184+
185+
// Inject mock dependencies
186+
(workspaceServiceAPI as any).workspaceService = mockWorkspaceService;
187+
(workspaceServiceAPI as any).userService = mockUserService;
188+
(workspaceServiceAPI as any).apiConverter = mockApiConverter;
189+
(workspaceServiceAPI as any).contextService = {} as ContextService;
190+
(workspaceServiceAPI as any).contextParser = {} as ContextParser;
191+
192+
// Create request
193+
const request = new ListWorkspaceSessionsRequest();
194+
request.organizationId = "550e8400-e29b-41d4-a716-446655440000"; // Valid UUID
195+
196+
// Should not call user service and should succeed
197+
const response = await workspaceServiceAPI.listWorkspaceSessions(request, {} as any);
198+
expect(response.workspaceSessions).to.have.length(1);
199+
expect(response.workspaceSessions[0].owner).to.be.undefined;
200+
});
201+
});
202+
203+
describe("WorkspaceSession_Owner placeholder creation", function () {
204+
it("should create correct placeholder for deleted user", function () {
205+
const ownerId = "deleted-user-123";
206+
207+
// This simulates the WorkspaceSession_Owner creation logic in our fix
208+
const ownerPlaceholder = {
209+
id: ownerId,
210+
name: "Deleted User",
211+
avatarUrl: "",
212+
};
213+
214+
expect(ownerPlaceholder.id).to.equal(ownerId);
215+
expect(ownerPlaceholder.name).to.equal("Deleted User");
216+
expect(ownerPlaceholder.avatarUrl).to.equal("");
217+
});
218+
219+
it("should preserve original user ID in placeholder", function () {
220+
const testOwnerIds = ["user-1", "user-abc-123", "some-long-uuid-string"];
221+
222+
testOwnerIds.forEach((ownerId) => {
223+
const ownerPlaceholder = {
224+
id: ownerId,
225+
name: "Deleted User",
226+
avatarUrl: "",
227+
};
228+
229+
expect(ownerPlaceholder.id).to.equal(ownerId);
230+
expect(ownerPlaceholder.name).to.equal("Deleted User");
231+
});
232+
});
233+
});
234+
});

components/server/src/api/workspace-service-api.ts

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -147,17 +147,33 @@ export class WorkspaceServiceAPI implements ServiceImpl<typeof WorkspaceServiceI
147147
const { ownerId } = workspace;
148148
if (ownerId) {
149149
if (!ownerMeta.has(ownerId)) {
150-
const user = await runWithSubjectId(SYSTEM_USER, async () =>
151-
this.userService.findUserById(SYSTEM_USER_ID, ownerId),
152-
);
153-
ownerMeta.set(
154-
ownerId,
155-
new WorkspaceSession_Owner({
156-
id: ownerId,
157-
name: user.fullName,
158-
avatarUrl: user.avatarUrl,
159-
}),
160-
);
150+
try {
151+
const user = await runWithSubjectId(SYSTEM_USER, async () =>
152+
this.userService.findUserById(SYSTEM_USER_ID, ownerId),
153+
);
154+
ownerMeta.set(
155+
ownerId,
156+
new WorkspaceSession_Owner({
157+
id: ownerId,
158+
name: user.fullName,
159+
avatarUrl: user.avatarUrl,
160+
}),
161+
);
162+
} catch (error) {
163+
if (error.data?.userDeleted) {
164+
// Handle deleted user gracefully
165+
ownerMeta.set(
166+
ownerId,
167+
new WorkspaceSession_Owner({
168+
id: ownerId,
169+
name: "Deleted User",
170+
avatarUrl: "",
171+
}),
172+
);
173+
} else {
174+
throw error; // Re-throw other errors
175+
}
176+
}
161177
}
162178
}
163179
}

0 commit comments

Comments
 (0)