Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
2 changes: 2 additions & 0 deletions src/controller/GroundController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

Expand Down Expand Up @@ -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,
};
Expand Down
3 changes: 3 additions & 0 deletions src/entity/TokenConfiguration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ export class TokenConfiguration {
@Column({ default: true })
level_tips: boolean;

@Column({ default: false })
redacted: boolean;

@Column({ default: "en" })
lang: string;

Expand Down
2 changes: 2 additions & 0 deletions src/openapi/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
Expand Down
54 changes: 0 additions & 54 deletions src/tests/GroundControlToMajorTom.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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", () => {
Expand Down
67 changes: 0 additions & 67 deletions src/tests/GroundController.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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 = {
Expand Down
12 changes: 12 additions & 0 deletions src/worker-sender.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading