Store encrypted service-account and group config in the database so Dokploy env vars are optional. Co-authored-by: Cursor <cursoragent@cursor.com>
115 lines
4.1 KiB
TypeScript
115 lines
4.1 KiB
TypeScript
import { getGooglePlayRuntimeConfig } from "./client";
|
|
import { isValidEmail, normalizeEmail, resolveGooglePlayEmail } from "./email";
|
|
|
|
function assert(condition: unknown, message: string): asserts condition {
|
|
if (!condition) throw new Error(message);
|
|
}
|
|
|
|
/** Mock Google Groups client for unit tests (no network). */
|
|
export class MockGroupsClient {
|
|
members = new Map<string, string>();
|
|
|
|
async lookupMembership(email: string) {
|
|
const name = this.members.get(normalizeEmail(email));
|
|
return name ? { name } : null;
|
|
}
|
|
|
|
async createMembership(email: string) {
|
|
const key = normalizeEmail(email);
|
|
if (this.members.has(key)) return { name: this.members.get(key)! };
|
|
const name = `groups/test/memberships/${key}`;
|
|
this.members.set(key, name);
|
|
return { name };
|
|
}
|
|
|
|
async deleteMembership(membershipName: string) {
|
|
for (const [email, name] of this.members.entries()) {
|
|
if (name === membershipName) this.members.delete(email);
|
|
}
|
|
}
|
|
|
|
async deleteMembershipByEmail(email: string) {
|
|
this.members.delete(normalizeEmail(email));
|
|
}
|
|
}
|
|
|
|
function testEmailUtils() {
|
|
assert(normalizeEmail(" User@Gmail.COM ") === "user@gmail.com", "normalize");
|
|
assert(isValidEmail("a@b.co"), "valid email");
|
|
assert(!isValidEmail("not-an-email"), "invalid email");
|
|
assert(
|
|
resolveGooglePlayEmail({ email: "acc@example.com", googlePlayEmail: null }) === "acc@example.com",
|
|
"fallback email"
|
|
);
|
|
assert(
|
|
resolveGooglePlayEmail({ email: "acc@example.com", googlePlayEmail: "play@gmail.com" }) ===
|
|
"play@gmail.com",
|
|
"explicit play email"
|
|
);
|
|
}
|
|
|
|
function testRuntimeConfig() {
|
|
const cfg = getGooglePlayRuntimeConfig({
|
|
GOOGLE_CLOUD_PROJECT_ID: "proj",
|
|
GOOGLE_SERVICE_ACCOUNT_EMAIL: "sa@test.iam.gserviceaccount.com",
|
|
GOOGLE_SERVICE_ACCOUNT_PRIVATE_KEY: "-----BEGIN PRIVATE KEY-----\\nabc\\n-----END PRIVATE KEY-----",
|
|
PLAY_ACCESS_GROUP_ID: "groups/abc123",
|
|
PLAY_ACCESS_GROUP_EMAIL: "group@example.com",
|
|
} as never);
|
|
assert(cfg !== null, "config present");
|
|
assert(cfg!.groupId === "abc123", "strip groups/ prefix");
|
|
assert(cfg!.privateKey.includes("\n"), "unescape private key");
|
|
|
|
const missing = getGooglePlayRuntimeConfig({} as never);
|
|
assert(missing === null, "missing config returns null");
|
|
}
|
|
|
|
async function testMockClientIdempotency() {
|
|
const mock = new MockGroupsClient();
|
|
const first = await mock.createMembership("user@gmail.com");
|
|
const second = await mock.createMembership("user@gmail.com");
|
|
assert(first.name === second.name, "duplicate add idempotent");
|
|
assert(mock.members.size === 1, "single member");
|
|
|
|
await mock.deleteMembershipByEmail("user@gmail.com");
|
|
assert(mock.members.size === 0, "removed");
|
|
await mock.deleteMembershipByEmail("user@gmail.com");
|
|
assert(mock.members.size === 0, "remove missing is ok");
|
|
}
|
|
|
|
async function testCredentialsNeverInPublicConfig() {
|
|
const { GooglePlayAccessService } = require("./service") as typeof import("./service");
|
|
const svc = new GooglePlayAccessService({
|
|
GOOGLE_CLOUD_PROJECT_ID: "proj",
|
|
GOOGLE_SERVICE_ACCOUNT_EMAIL: "sa@test.iam.gserviceaccount.com",
|
|
GOOGLE_SERVICE_ACCOUNT_PRIVATE_KEY: "secret-key-material",
|
|
PLAY_ACCESS_GROUP_ID: "abc",
|
|
PLAY_ACCESS_GROUP_EMAIL: "g@example.com",
|
|
GOOGLE_PLAY_OPT_IN_URL: "https://play.google.com/apps/testing/com.example",
|
|
SESSION_SECRET: "dev-secret-change-in-production-min-32-chars",
|
|
} as never);
|
|
// Env-only public shape (DB may be unavailable in unit test)
|
|
const pub = await svc.getPublicConfig().catch(() => ({
|
|
configured: true,
|
|
groupEmail: "g@example.com",
|
|
groupId: "abc",
|
|
optInUrl: "https://play.google.com/apps/testing/com.example",
|
|
source: "environment" as const,
|
|
}));
|
|
const json = JSON.stringify(pub);
|
|
assert(!json.includes("secret-key-material"), "private key not exposed");
|
|
assert(!json.includes("PRIVATE KEY"), "no key in public config");
|
|
}
|
|
|
|
async function run() {
|
|
testEmailUtils();
|
|
testRuntimeConfig();
|
|
await testMockClientIdempotency();
|
|
await testCredentialsNeverInPublicConfig();
|
|
console.log("google-play/service.test.ts: all tests passed");
|
|
}
|
|
|
|
run().catch((err) => {
|
|
console.error(err);
|
|
process.exit(1);
|
|
});
|