diff --git a/openapi.yaml b/openapi.yaml index c50c89d..d07f392 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -193,6 +193,9 @@ components: type: "boolean" level_tips: type: "boolean" + redacted: + type: "boolean" + description: "when true, server sends generic push text and omits data payload" lang: type: "string" app_version: diff --git a/package-lock.json b/package-lock.json index 60f2a87..24eeca9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "groundcontrol", - "version": "3.1.3", + "version": "3.2.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "groundcontrol", - "version": "3.1.3", + "version": "3.2.0", "dependencies": { "body-parser": "^1.20.5", "cors": "^2.8.5", diff --git a/package.json b/package.json index 9c9209d..fb75ce0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "groundcontrol", - "version": "3.1.3", + "version": "3.2.0", "description": "GroundControl push server API", "devDependencies": { "@types/node": "^22.12.0", diff --git a/src/controller/GroundController.ts b/src/controller/GroundController.ts index ebaa0c9..4b5115c 100644 --- a/src/controller/GroundController.ts +++ b/src/controller/GroundController.ts @@ -268,6 +268,7 @@ export class GroundController { if (typeof body.level_tips !== "undefined") tokenConfig.level_tips = !!body.level_tips; if (typeof body.lang !== "undefined") tokenConfig.lang = String(body.lang); if (typeof body.app_version !== "undefined") tokenConfig.app_version = String(body.app_version); + if (typeof body.redacted !== "undefined") tokenConfig.redacted = !!body.redacted; tokenConfig.last_online = new Date(); } @@ -305,6 +306,7 @@ export class GroundController { level_price: tokenConfig.level_price, level_transactions: tokenConfig.level_transactions, level_tips: tokenConfig.level_tips, + redacted: tokenConfig.redacted, lang: tokenConfig.lang, app_version: tokenConfig.app_version, }; diff --git a/src/entity/TokenConfiguration.ts b/src/entity/TokenConfiguration.ts index d9aba42..d19a638 100644 --- a/src/entity/TokenConfiguration.ts +++ b/src/entity/TokenConfiguration.ts @@ -27,6 +27,9 @@ export class TokenConfiguration { @Column({ default: true }) level_tips: boolean; + @Column({ default: false }) + redacted: boolean; + @Column({ default: "en" }) lang: string; diff --git a/src/openapi/api.ts b/src/openapi/api.ts index 9596e83..7c7cd7f 100644 --- a/src/openapi/api.ts +++ b/src/openapi/api.ts @@ -143,6 +143,8 @@ export type components = { level_news?: boolean; level_price?: boolean; level_tips?: boolean; + /** @description when true, server sends generic push text and omits data payload */ + redacted?: boolean; lang?: string; app_version?: string; } & { [key: string]: unknown }; diff --git a/src/tests/GroundControlToMajorTom.test.ts b/src/tests/GroundControlToMajorTom.test.ts index 2264859..3b76c37 100644 --- a/src/tests/GroundControlToMajorTom.test.ts +++ b/src/tests/GroundControlToMajorTom.test.ts @@ -88,33 +88,6 @@ describe("GroundControlToMajorTom", () => { process.env = { ...originalEnv }; }); - describe("getGoogleCredentials", () => { - it("should return access token from Google Auth", async () => { - const mockToken = "mock-access-token"; - const mockClient = { - getAccessToken: vi.fn().mockResolvedValue({ token: mockToken }), - }; - - // Mock the auth object that's created at module level - we need to spy on the actual instance - const mockAuth = { - getClient: vi.fn().mockResolvedValue(mockClient), - }; - - // Since auth is already created when the module loads, we need to spy on it - const authSpy = vi.spyOn(GroundControlToMajorTom as any, "getGoogleCredentials").mockImplementation(async () => { - const client = await mockAuth.getClient(); - const accessTokenResponse = await client.getAccessToken(); - return accessTokenResponse.token; - }); - - const result = await GroundControlToMajorTom.getGoogleCredentials(); - - expect(result).toBe(mockToken); - expect(authSpy).toHaveBeenCalled(); - authSpy.mockRestore(); - }); - }); - describe("getApnsJwtToken", () => { it("should return cached JWT token if still valid", () => { const mockToken = "cached-jwt-token"; @@ -126,33 +99,6 @@ describe("GroundControlToMajorTom", () => { expect(result).toBe(mockToken); }); - - it("should generate new JWT token if cache is expired", async () => { - const mockNewToken = "new-jwt-token"; - - // Set expired timestamp - (GroundControlToMajorTom as any)._jwtTokenMicroTimestamp = Date.now() - 1900 * 1000; - - // Mock the entire getApnsJwtToken method to test the caching logic - const originalMethod = GroundControlToMajorTom.getApnsJwtToken; - let callCount = 0; - GroundControlToMajorTom.getApnsJwtToken = vi.fn().mockImplementation(() => { - callCount++; - if (callCount === 1) { - // First call should generate new token since cache is expired - return mockNewToken; - } - return mockNewToken; - }); - - const result = GroundControlToMajorTom.getApnsJwtToken(); - - expect(result).toBe(mockNewToken); - expect(GroundControlToMajorTom.getApnsJwtToken).toHaveBeenCalled(); - - // Restore original method - GroundControlToMajorTom.getApnsJwtToken = originalMethod; - }); }); describe("pushOnchainAddressGotUnconfirmedTransaction", () => { diff --git a/src/tests/GroundController.test.ts b/src/tests/GroundController.test.ts index c0d317b..ecb6775 100644 --- a/src/tests/GroundController.test.ts +++ b/src/tests/GroundController.test.ts @@ -177,38 +177,6 @@ describe("GroundController", () => { process.env = { ...originalEnv }; }); - describe("Repository getters", () => { - it("should return tokenToAddressRepository", () => { - const repo = groundController.tokenToAddressRepository; - expect(repo).toBeDefined(); - expect(repo).toBe(mockRepository); - }); - - it("should return tokenToHashRepository", () => { - const repo = groundController.tokenToHashRepository; - expect(repo).toBeDefined(); - expect(repo).toBe(mockRepository); - }); - - it("should return tokenToTxidRepository", () => { - const repo = groundController.tokenToTxidRepository; - expect(repo).toBeDefined(); - expect(repo).toBe(mockRepository); - }); - - it("should return tokenConfigurationRepository", () => { - const repo = groundController.tokenConfigurationRepository; - expect(repo).toBeDefined(); - expect(repo).toBe(mockRepository); - }); - - it("should return sendQueueRepository", () => { - const repo = groundController.sendQueueRepository; - expect(repo).toBeDefined(); - expect(repo).toBe(mockRepository); - }); - }); - describe("majorTomToGroundControl", () => { beforeEach(() => { mockRequest = { @@ -346,41 +314,6 @@ describe("GroundController", () => { }); }); - describe("lightningInvoiceGotSettled", () => { - beforeEach(() => { - mockRequest = { - body: { - preimage: "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", - hash: "6c60f404f8167a38fc70eaf8c17cd92e60f96e3f9dd9b6b5d3b9b5d5c5b5a5a5", // This matches our mock digest output - amt_paid_sat: 1000, - memo: "Test payment", - }, - }; - }); - - it("should process lightning invoice settlement successfully", async () => { - // Test the method structure and basic validation - expect(typeof groundController.lightningInvoiceGotSettled).toBe("function"); - expect(groundController.lightningInvoiceGotSettled.length).toBe(3); // request, response, next parameters - - // Test that the method validates the hash correctly by expecting it to call response.send - await groundController.lightningInvoiceGotSettled(mockRequest, mockResponse, mockNext); - - // Either successful processing (status 200) or hash validation failure (status 500) - expect(mockResponse.status).toHaveBeenCalled(); - expect(mockResponse.send).toHaveBeenCalled(); - }); - }); - - describe("ping", () => { - it("should test ping method structure", async () => { - // The ping method uses a global connection variable that's difficult to mock - // For now, we'll test that the method exists and has the right structure - expect(typeof groundController.ping).toBe("function"); - expect(groundController.ping.length).toBe(3); // request, response, next parameters - }); - }); - describe("setTokenConfiguration", () => { beforeEach(() => { mockRequest = { diff --git a/src/worker-sender.ts b/src/worker-sender.ts index 80416bc..c4b89cc 100644 --- a/src/worker-sender.ts +++ b/src/worker-sender.ts @@ -101,6 +101,18 @@ dataSource continue; } + if (!!tokenConfig.redacted) { + // user wants to hide contents from apple/google + const redactedPayload: components["schemas"]["PushNotificationMessage"] = { + type: 5, + text: "You have a new notification", + token: payload.token, + os: payload.os, + level: NOTIFICATION_LEVEL_TRANSACTIONS, + }; + payload = redactedPayload; + } + const timeoutId = setTimeout(() => { console.error("timeout pushing to token, comitting suicide"); process.exit(2);