mirror of
https://github.com/webstudio-is/webstudio.git
synced 2026-10-28 07:37:36 +09:00
Closes #6375 ## Summary - Refresh username, avatar, and provider fields when an existing user logs in through OAuth. - Avoid unnecessary database writes when profile fields are unchanged. - Preserve existing development-login behavior. - Add regression coverage for changed GitHub profile data. ## Testing - `pnpm --filter @webstudio-is/builder exec vitest run --project core-server app/shared/db/user.server.test.ts` - `pnpm --filter @webstudio-is/builder typecheck`
232 lines
6.6 KiB
TypeScript
232 lines
6.6 KiB
TypeScript
import { describe, test, expect } from "vitest";
|
|
import {
|
|
createTestServer,
|
|
db,
|
|
testContext,
|
|
json,
|
|
} from "@webstudio-is/postgrest/testing";
|
|
import type { AppContext } from "@webstudio-is/trpc-interface/index.server";
|
|
import { AuthorizationError } from "@webstudio-is/trpc-interface/index.server";
|
|
import {
|
|
getUserById,
|
|
createOrLoginWithDev,
|
|
createOrLoginWithOAuth,
|
|
updateUserProjectsTags,
|
|
} from "./user.server";
|
|
|
|
const server = createTestServer();
|
|
|
|
const createContext = (overrides: Partial<AppContext> = {}): AppContext =>
|
|
({
|
|
...testContext,
|
|
authorization: { type: "user", userId: "user-1" },
|
|
...overrides,
|
|
}) as unknown as AppContext;
|
|
|
|
const userRow = {
|
|
id: "user-1",
|
|
email: "test@example.com",
|
|
username: "tester",
|
|
image: "",
|
|
provider: "dev",
|
|
projectsTags: [],
|
|
createdAt: new Date().toISOString(),
|
|
team: null,
|
|
};
|
|
|
|
// ─── getUserById ───────────────────────────────────────────────
|
|
|
|
describe("getUserById (msw)", () => {
|
|
test("returns user when found", async () => {
|
|
server.use(db.get("User", () => json(userRow)));
|
|
|
|
const result = await getUserById(createContext(), "user-1");
|
|
expect(result.id).toBe("user-1");
|
|
expect(result.email).toBe("test@example.com");
|
|
expect(result.projectsTags).toEqual([]);
|
|
});
|
|
|
|
test("throws when user not found", async () => {
|
|
server.use(
|
|
db.get("User", () =>
|
|
json(
|
|
{ code: "PGRST116", message: "Row not found", details: "", hint: "" },
|
|
{ status: 406 }
|
|
)
|
|
)
|
|
);
|
|
|
|
await expect(getUserById(createContext(), "missing")).rejects.toThrow(
|
|
"User not found"
|
|
);
|
|
});
|
|
});
|
|
|
|
// ─── createOrLoginWithDev ──────────────────────────────────────
|
|
|
|
describe("createOrLoginWithDev (msw)", () => {
|
|
test("returns existing user when found", async () => {
|
|
let workspaceChecked = false;
|
|
server.use(
|
|
// User SELECT resolves successfully → existing user
|
|
db.get("User", () => json(userRow)),
|
|
// Workspace check — has default workspace
|
|
db.get("Workspace", () => {
|
|
workspaceChecked = true;
|
|
return json({ id: "ws-1" });
|
|
})
|
|
);
|
|
|
|
const result = await createOrLoginWithDev(
|
|
createContext(),
|
|
"test@example.com"
|
|
);
|
|
expect(result.id).toBe("user-1");
|
|
expect(workspaceChecked).toBe(true);
|
|
});
|
|
|
|
test("lazily creates workspace when existing user has none", async () => {
|
|
let workspaceCreated = false;
|
|
server.use(
|
|
db.get("User", () => json(userRow)),
|
|
// maybeSingle returns null when no row exists
|
|
db.get("Workspace", () => json(null)),
|
|
db.post("Workspace", () => {
|
|
workspaceCreated = true;
|
|
return json({ id: "ws-new" }, { status: 201 });
|
|
})
|
|
);
|
|
|
|
const result = await createOrLoginWithDev(
|
|
createContext(),
|
|
"test@example.com"
|
|
);
|
|
expect(result.id).toBe("user-1");
|
|
expect(workspaceCreated).toBe(true);
|
|
});
|
|
|
|
test("creates new user and workspace when email not found", async () => {
|
|
let userInserted = false;
|
|
let workspaceInserted = false;
|
|
server.use(
|
|
// User SELECT → PGRST116 (not found)
|
|
db.get("User", () =>
|
|
json(
|
|
{ code: "PGRST116", message: "Row not found", details: "", hint: "" },
|
|
{ status: 406 }
|
|
)
|
|
),
|
|
db.post("User", () => {
|
|
userInserted = true;
|
|
return json({ ...userRow, id: "new-user" }, { status: 201 });
|
|
}),
|
|
db.post("Workspace", () => {
|
|
workspaceInserted = true;
|
|
return json({ id: "ws-new" }, { status: 201 });
|
|
})
|
|
);
|
|
|
|
const result = await createOrLoginWithDev(
|
|
createContext(),
|
|
"new@example.com"
|
|
);
|
|
|
|
expect(result.email).toBe("test@example.com"); // from userRow mock
|
|
expect(userInserted).toBe(true);
|
|
expect(workspaceInserted).toBe(true);
|
|
});
|
|
|
|
test("throws when User SELECT fails with unexpected error", async () => {
|
|
server.use(
|
|
db.get("User", () =>
|
|
json(
|
|
{ code: "PGRST999", message: "DB error", details: "", hint: "" },
|
|
{ status: 500 }
|
|
)
|
|
)
|
|
);
|
|
|
|
await expect(
|
|
createOrLoginWithDev(createContext(), "fail@example.com")
|
|
).rejects.toThrow("User not found");
|
|
});
|
|
});
|
|
|
|
// ─── createOrLoginWithOAuth ───────────────────────────────────
|
|
|
|
describe("createOrLoginWithOAuth (msw)", () => {
|
|
test("syncs changed profile fields for an existing user", async () => {
|
|
const updatedUser = {
|
|
...userRow,
|
|
username: "new-github-username",
|
|
image: "https://avatars.example.com/new",
|
|
provider: "github",
|
|
};
|
|
let capturedBody: unknown;
|
|
|
|
server.use(
|
|
db.get("User", () => json(userRow)),
|
|
db.patch("User", async ({ request }) => {
|
|
capturedBody = await request.json();
|
|
return json(updatedUser);
|
|
}),
|
|
db.get("Workspace", () => json({ id: "ws-1" }))
|
|
);
|
|
|
|
const result = await createOrLoginWithOAuth(createContext(), {
|
|
provider: "github",
|
|
displayName: "new-github-username",
|
|
emails: [{ value: "test@example.com" }],
|
|
photos: [{ value: "https://avatars.example.com/new" }],
|
|
} as Parameters<typeof createOrLoginWithOAuth>[1]);
|
|
|
|
expect(capturedBody).toEqual({
|
|
username: "new-github-username",
|
|
image: "https://avatars.example.com/new",
|
|
provider: "github",
|
|
});
|
|
expect(result.username).toBe("new-github-username");
|
|
});
|
|
});
|
|
|
|
// ─── updateUserProjectsTags ───────────────────────────────────
|
|
|
|
describe("updateUserProjectsTags (msw)", () => {
|
|
const tags = [{ id: "tag-1", label: "Design" }];
|
|
|
|
test("returns updated tags", async () => {
|
|
server.use(
|
|
db.patch("User", () =>
|
|
json({ ...userRow, projectsTags: tags }, { status: 200 })
|
|
)
|
|
);
|
|
|
|
const result = await updateUserProjectsTags({ tags }, createContext());
|
|
expect(result).toEqual(tags);
|
|
});
|
|
|
|
test("throws AuthorizationError for non-user context", async () => {
|
|
await expect(
|
|
updateUserProjectsTags(
|
|
{ tags },
|
|
createContext({ authorization: { type: "anonymous" } })
|
|
)
|
|
).rejects.toThrow(AuthorizationError);
|
|
});
|
|
|
|
test("throws when DB update fails", async () => {
|
|
server.use(
|
|
db.patch("User", () =>
|
|
json(
|
|
{ code: "PGRST123", message: "Update failed", details: "", hint: "" },
|
|
{ status: 500 }
|
|
)
|
|
)
|
|
);
|
|
|
|
await expect(
|
|
updateUserProjectsTags({ tags }, createContext())
|
|
).rejects.toBeTruthy();
|
|
});
|
|
});
|